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
- 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.
- We may use a special keyword
- The
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 bar
1.
On the other hand, the code below are invalid.
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
.
Using these information, we arrive at the following initial function definition for factorial.
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.
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.
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.
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.
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.
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.
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
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
.
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 toFalse
. - If the value of
is_prime
is never assigned toFalse
, 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
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.
- 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).
- 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.
-
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.
- 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
.
How is this function created?
It definitely cannot be a simple def print()
or def print(x)
.
The definition is closer to the following.
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
.
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.
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
Callee: Factorial
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.
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.
- Evaluate arguments from left to right.
- Pass arguments to parameters according to position (a.k.a. pass by value).
- Evaluate the function body with the parameters initialized.
- Return by value.
- If we encounter a
return expr
statement, the execution continues to the caller immediately and the result of evaluatingexpr
is used to replace the function call. - If we reacht the end of the function without encountering any
return
statement, we simplyreturn None
.
- If we encounter a
-
If you are out of function names, these are what we use. Let us keep the tradition alive. ↩
-
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. ↩