Skip to content

Function Definition

Learning Objectives

At the end of this sub-unit, students should

  • know how to define your own functions.
  • know how arguments are accepted by parameters via call by value.

Defining a Function

We have learnt how to invoke functions but so far we have been using only built-in functions or functions from a module like math. Our task now is to define our own function. This can be done by the keyword def.

Syntax

def  name ( parameters ):
   block 
  • It must start with the def keyword.
    • The def keyword is followed by a function name name .
      • The rule for function name followed the rule for variable name.
    • It is then followed by a sequence of parameters enclosed within parentheses.
      • It may be an empty sequence (i.e., there is no parameter).
      • Each parameter (if any) in the sequence is a variable name.
      • If there is more than one parameter, they are separated by a comma.
    • This line is terminated by a colon (i.e., :).
    • The next line is an inner block called the function body.
      • We may use a special keyword return inside the function body.

Let us unpack the syntax slowly. First, we will show examples of valid function definition. Here, we will simply use pass as the function body. Note that pass is a placeholder. It is a statement that does nothing (i.e., does not change the state). We need this because block requires at least one statement. pass is the simplest statement. Also for simplicity, we will simply call our function foo and bar1.

def foo():
  pass
def foo(x):
  pass
def foo(x, y):
  pass
def foo(x, y, z):
  pass

On the other hand, the code below are invalid.

def bar(,):
  pass
def bar(, x):  # interestingly, a single trailing commas are allowed
  pass
def bar():
pass
def bar()
  pass
def foo bar():
  pass

There is actually nothing much in the semantics of a function definition. One important property of function definition is that it the code within the function is not evaluated when we define the function. The function body is only evaluated when we call the function. This is worth repeating as it is quite a common mistake people who are new to programming make.

Common Mistake

Function body is not evaluated when the function is defined. Function body is only evaluated when the function is invoked.

To define our own factorial function, we will start with the name and parameters first. If we consider the black box representation, we are only at the input stage shown below. Thus, we can see that we need a parameter called n. We also need the name of the function, which we will call factorial.

Def01

Using these information, we arrive at the following initial function definition for factorial.

Initial Factorial

def factorial(n):
  pass

Producing Value

Now that we know how to accept a value, our next task is to produce a value. Our factorial function can accept values assigned to n, but what does it produce? We can check.

1
2
3
>>> factorial(10)
>>> print(factorial(10))
None

The reason we get None is because we have not return any value. In other words, we have returned nothing. Why nothing? That is because pass is not doing anything, it does not even produce any value. Naturally, we want to know how to produce a value. We know that an expression is evaluated into a value, but how is this value returned from a function?

The answer is the keyword return. This keyword is quite special. Firstly, it can only be used inside a function. So if we are not inside a function and we type return, we will get an error.

>>> return
SyntaxError

Secondly, the keyword return can be followed by an expression. If the execution reach this return statement, the expression will be evaluated and the resulting value will be returned. Consider a simple case of a function const10 that will always produce the integer 10 when invoked.

def const10():
  return 10
1
2
3
4
5
>>> const10()      # no argument, but still need parentheses
10
>>> x = const10()  # equivalent to x = 10 after evaluation
>>> x
10

Thirdly, when the execution encounter the return keyword, the function terminates with the given return value. Let us illustrate this with another simple example is a function that simply returns whatever is given. However, we are writing it in a way that is not recommended and is a bad practice. We will add more statements after the return keyword.

1
2
3
def idf(x):
  return x
  print("is it executed?")
1
2
3
4
>>> idf(10)
10
>>> idx('CS1010S')
'CS1010S'

So here we can see clearly that Line 3 is not executed. If it was executed, we would have seen is it executed printed. This shows that the function terminates after Line 2.

As a quick note, now that we have functions, we need to name the components carefully. Notice that the name idf appears in different places. Functionally, there are two kinds of context where the name idf appears. The first is in the context of a function definition and the second is in the context of a function call. The name we give for the first context is the callee while the name we give for the second context is the caller2.

By giving different names, we can explain concepts more precisely. For instance, we can now redefine call by value as follows.

Call by Value

Call by value is a function call convention where we evaluate the arguments in the caller. Then we assign the arguments from the caller to the parameters in the callee before evaluating the function body.

In a similar vein, we can now describe the behavior of return keyword more precisely as follows.

Return Value

When a return expr is executed, we first evaluate expr. This produces a value that we call \(V\). The function ends immediately and the program control is passed back to the caller. The function call is evaluated to \(V\) and the program continues at the caller.

This behavior generalizes the keyword break because it does not matter how many loops the return statement is inside of. As long as all of the outer loops are still within the function, the return statement will exit all of them as it will exit the function. One trivial example is the following.

1
2
3
4
5
6
7
8
def nested_loop(x):
  while True:      # infinite loop
    while True:    # infinite loop
      while True:  # infinite loop
        return x   # exit out of the function
      print("not printed")
    print("not printed")
  print("not printed")
1
2
3
4
>>> nested_loop(10)
10
>>> nested_loop('CS1010S')
'CS1010S'

Factorial

So to quickly recap, the parameters are the input variables and the return statement produces the output. The only thing left is to write the actual code. But we have done that too. We reproduce the code and the grey box representation below.

1
2
3
4
5
6
7
# Assume n is initialized
res = 1
val = 1
while val <= n:
  res = res * val
  val = val + 1
# res is n!

Grey05

To make this into a function, we simply change the comment about the assumption that n is initialized into the function definition. Then, because res is our output variable, we simply add return res. After also repairing the indentation, we get the code below.

Factorial Function

1
2
3
4
5
6
7
def factorial(n):    # Assume n is initialized
  res = 1
  val = 1
  while val <= n:
    res = res * val
    val = val + 1
  return res         # res is n!
1
2
3
4
5
6
def factorial(n):    # Assume n is initialized
  res = 1
  while n >= 1:
    res = res * n
    n = n - 1
  return res         # res is n!
1
2
3
4
5
6
7
>>> # should be true for n >= 0
>>> factorial(0)
1
>>> factorial(5)
120
>>> factorial(10)
3628800

Prime Check

To highlight the application that return statement exits the function without continuing, we will solve the problem of checking if a positive integer \(n \geq 2\) is a prime number or not. If it is prime, we return True. Otherwise, we return False.

Previously, our code uses break to exit the loop. We can change this into return. But we can only use return in a function, so we will need to convert the code into function. Note that the input is n and the output is is_prime. However, at Line 6 and 7, the value of is_prime is always False. So we can change this to return False.

1
2
3
4
5
6
7
8
9
# Assume n is initialized
is_prime = True
i = 2
while i < n:
  if n % i == 0:
    is_prime = False
    break
  i = i + 1
print(is_prime)
1
2
3
4
5
6
7
8
9
def prime_check(n):
  is_prime = True
  i = 2
  while i < n:
    if n % i == 0:
      is_prime = False
      break
    i = i + 1
  return is_prime
1
2
3
4
5
6
7
8
9
def prime_check(n):
  is_prime = True
  i = 2
  while i < n:
    if n % i == 0:
      # is_prime always False
      return False
    i = i + 1
  return is_prime

Finally --given the latest modification in the tab "Without Break"-- if we ever reach Line 9, then the following must be true.

  • We never reach Line 7 because if we reach Line 7, return False will exit the function.
  • If we never reach Line 7, we must have not reached Line 6.
  • If we never reach Line 6, the value of is_prime is never assigned to False.
  • If the value of is_prime is never assigned to False, it must still be the initial value.
  • The initial value is True from Line 2 (i.e., is_prime = True).

Therefore, return is_prime at Line 9 will always return True. This is the kind of logical thinking expected for computer scientist. Each step need to be a single logical step implied by all the previous steps. There should not be any leap in deductions.

Prime Check

def prime_check(n):
  """
  Assumes n is an integer and n >= 2.
  Returns True if n is prime and False otherwise.
  """
  i = 2
  while i < n:
    if n % i == 0:
      return False  # not prime because there is i such that 2 <= i < n that divides n
    i = i + 1
  return True       # prime
>>> # no smaller test as it is outside of assumption
>>> prime_check(2)
True
>>> prime_check(3)
True
>>> prime_check(4)
False
>>> prime_check(5)
True
>>> prime_check(6)
False

We added a good practice of writing documentation of the function. While this is optional, it will help a lot in your programming journey. The documentation is also known as a docstring. It is a multiline string literal enclosed within triple-quote (i.e., """). It must also be the first line in the function body. There are several advantages to writing documentation.

  1. Assuming the documentation is correct, we do not have to understand the code to know what it is doing.
    • We may also know the condition for which the function behaves correctly (i.e., assumptions).
  2. If the documentation is incorrect, we know what is the expected behavior and the assumptions.
    • We may then try to make the behavior correct (i.e., debugging the code) to follow the documentation.
    • Alternatively, the documentation may be incorrect and we can correct the documentation.
  3. We get help from Python IDLE and Editor as seen in the image below. The text we have written as docstring is displayed as a hint after we put in the opening parentheses.

    DocString01

    • The assumptions are also known as precondition, the condition that needs to be satisfied for the correct execution of the function.
    • The return can be generalized into postcondition, the condition that is satisfied after the correct execution of the function. This can be more than just the return value as it may modified the state in another way.

Variadic Functions

Optional Knowledge

Variadic functions are functions that can accept an arbitrary number of arguments, usually above a certain number of fixed arguments. An example is the print function that can accept 0 or more arguments. So all of the following are valid function invocation of print.

print()
print(1)
print(1, 2)
print(1, 2, 3)

How is this function created? It definitely cannot be a simple def print() or def print(x). The definition is closer to the following.

def print(*args):
  ...

The *args allows an arbitrary number of arguments to be given. In the end, all of the arguments will be aggregated into a single variable called args. This variable must be a data type that can contain multiple elements. We have not learnt such data type yet so we will leave it at that.

The main thing to know is that it is possible to do this in Python. However, it will typically only be used in a small number of cases. Most functions will have a fixed number of parameters. But because Python is a mature language developed from as early as 1989, it has a lot of features. Since the features are only relevant in a small number of cases, we opt not to explain them further as it will only complicates the basic understanding of the important characteristics of functions.

Unevaluated Values

Recap that function body is not evaluated when the function is defined. However, so far we do not have code that illustrates this. Consider the following function that basically is a wrapper for print.

1
2
3
4
5
6
7
8
def display(message):
  print(message)

def initialize():
  display("Initialization Start")
  display("Initializing...")
  display("Initializing...")
  display("Initialization Complete")
1
2
3
4
5
>>> initialize()  # without this, nothing is printed
Initialization Start
Initializing...
Initializing...
Initialization Complete

If we run both function definition, nothing will be printed on the screen. This highlights the fact that function definition will not execute the function body. The function body will only be executed if the function is actually invoked. Retry running the code above with the function invocation uncommented and see the difference.

When writing multiple functions in the same file, it is important to know which part of the code belongs to the function body and which one does not. The indentation plays a critical role here. It does not matter how many blank lines in between, the block is determined by the indentation level. Consider a slight modification below.

1
2
3
4
5
6
7
8
9
def display(message):
  print(message)

def initialize():
  display("Initialization Start")
  display("Initializing...")

  display("Initializing...")
display("Initialization Complete")
1
2
3
4
5
Initialization Complete
>>> initialize()   # the above is printed due to display("Initialization Complete")
Initialization Start
Initializing...
Initializing...

Line 8 is still within the function initialize but Line 9 is outside. Although function body is not executed when defining a function, Line 9 is not part of the function body. So even without function invocation to initialize, the phrase Initialization Complete will be printed. As such, be very very cafeful about indentation level in Python,

Call by Value

Now that we have our own function, we can illustrate the behavior of call by value in more details. In particular, how are arguments are accepted by the parameters. We will use the factorial to illustrate this.

Caller

# { }
m = 4
# { m ↦ 4 }
f5 = factorial(m + 1)
# ⇝ factorial(4 + 1)
# ⇝ factorial(5)  ⟾












f5 = 120 #  ⟽ 120
# { m ↦ 4 , f5 ↦ 120 }

Callee: Factorial






#  5 ⟾ factorial(5)
def factorial(n):
  # n = 5
  # { n ↦ 5 }
  # the states in between are omitted
  res = 1
  val = 1
  while val <= n:
    res = res * val
    val = val + 1
  # { n ↦ 5 , val ↦ 6 , res ↦ 120 }
  return res
  # return 120
  #  ⟽ 120

In the case of multiple arguments and multiple parameters, the evaluation is very similar. Passing the argument to parameter is like having an assignment where the lhs is the parameter name and the rhs is the argument value. This is shown on Line 7 on the right. For functions with multiple parameters, the process is the same but with multiple assignment.

This is similar to our earlier attempt with the right triangle but now we can give names to the lines that match our call by value terminology.

1
2
3
4
5
6
k = i + 1  # row construction input is k
# row construction
# ----------------------------------------
# input: k, n
m1 = k
m2 = n

Line 1 evaluates our first argument and produces a value that is stored in variable k. We do not have evaluation of second argument as the value of n does not change. The function call is indicated by the comment that forms the barrier (i.e., # ----------------------------------------). This is then passed to parameter that we name m1 and m2 with the assignment at Line 5 and Line 6.

Call by Value

The steps in call by value can be summarized as the following four steps.

  1. Evaluate arguments from left to right.
  2. Pass arguments to parameters according to position (a.k.a. pass by value).
  3. Evaluate the function body with the parameters initialized.
  4. Return by value.
    • If we encounter a return expr statement, the execution continues to the caller immediately and the result of evaluating expr is used to replace the function call.
    • If we reacht the end of the function without encountering any return statement, we simply return None.

  1. If you are out of function names, these are what we use. Let us keep the tradition alive. 

  2. The name is following the convention of employee and employer. Employee is the person being employed and the employer is the person perfroming the employing. Similarly, a callee is a function being called and the caller is where the function is called.