06_functions.ipynb - Colab
06_functions.ipynb - Colab
"Programming" can be seen as the process of breaking a large and complex computational
task into smaller and smaller subtasks, until the subtasks are simple enough to be performed
by combining basic instructions. To improve code readability and promote code reuse, it’s
convenient to associate a name with these subtasks, which becomes callable functions.
In the context of programming, a function is thus a named group of statements that performs
a computation. Each function must be before “de6ned” and then “called” by its name.
Optionally, we can specify parameters to supply so as to call a function.
We already used a lot of pre-de6ned functions, by calling them with suitable arguments:
We also used composition of functions, where the return of a function is used in turn as a
argument of another, and so on:
input_num = "44"
print(str(int(input_num) + 1))
Since expressions returns objects, these expressions can be used as function arguments (see
the expression: int(input_num) + 1 used as a parameter of str() ).
input_num = "44"
print(str(int(input_num) + 2))
# the use of str() is not compulsory in print, since all arguments are automaticall
print(int(input_num) + 2)
46
46
DeIning and calling a function
To de6ne and then call/invoke a function, you have to use the following syntax:
....
Therefore, if function arguments are needed, when you deIne the function you have to
introduce one or more variables called parameters between a pair of parentheses. For
example:
This function assigns the arguments, passed at calling time, to the two parameters param1
and param1 . Indeed, when the function print_twice is called, it prints the value of the two
parameters twice. For example, after the deInition, you can call the function as follows:
print_twice("Thanks", 1000)
This function works with any argument that can be printed by the build-in function print .
You can also pass expressions as arguments, which are Irst evaluated before being assigned
to the parameter variables. For example, look at the code below, where we also deIned a
function without parameters:
def print_twice(param1, param2):
print(param1, param2)
print(param1, param2)
def print_my_strings():
msg = "Hello!"
print(msg)
print("How are you from 0 to 10?")
Thanks 1000
Thanks 1000
Hello!
How are you from 0 to 10?
# There are so-called "magic methods" in Jupyther notebook
# For example, %who shows the list of all variables defined within the current note
n = 10
%who
print_twice("aa", "bb")
print(n)
%lsmagic
n print_my_strings print_twice
aa bb
aa bb
10
Available line magics:
%alias %alias_magic %autoawait %autocall %automagic %autosave
%bookmark %cat %cd %clear %colors %conda %config %connect_info %cp
%debug %dhist %dirs %doctest_mode %ed %edit %env %gui %hist
%history %killbgscripts %ldir %less %lf %lk %ll %load %load_ext
%loadpy %logoff %logon %logstart %logstate %logstop %ls %lsmagic
%lx %macro %magic %man %matplotlib %mkdir %more %mv %notebook
%page %pastebin %pdb %pdef %pdoc %pfile %pinfo %pinfo2 %pip %popd
%pprint %precision %prun %psearch %psource %pushd %pwd %pycat
%pylab %qtconsole %quickref %recall %rehashx %reload_ext %rep %rerun
%reset %reset_selective %rm %rmdir %run %save %sc %set_env %shell
%store %sx %system %tb %tensorflow_version %time %timeit %unalias
%unload_ext %who %who_ls %whos %xdel %xmode
Functions and Flow of control/execution
Recall that program execution always begins at the Irst statement of the program.
Statements are run one at a time, in order from top to bottom.
Function de6nitions do not alter the Now of execution of the program, and the statements
inside the function deInition don't run until the function is called.
A function call is like a detour in the ?ow of execution. Instead of going to the next statement,
the ?ow jumps to the body of the function, and runs the statements there, and then comes
back to resume from where it left off.
This execution scheme works well even if the called function calls another, and so on. Python
is good at keeping track of where the Now of execution is, moving forward and backward
between different functions.
In summary, when you read a program, you don't always want to read from top to bottom.
Sometimes it makes more sense if you follow the Now of execution. Use Python tutor to
visualize the execution of a program with function deInitions and function calls.
Parameters are also local. For example, outside print_twice , there is no known variables
param1 or param2 . See the example below, and discuss because they generate run-time
errors.
def foo(val):
incr = 12
a = 10
val = val + incr + a + 10
print("local variable:", val)
a = 20
incr = 1000
foo(8)
print(a) # the variable is **not** the one defined in the body of foo()
print(incr) # the variable is **not** defined in this environment
# print(val)
local variable: 40
20
The local namespace is created when the function is called and immediately populated by the
function's arguments. After the function is Inished, the local namespace is destroyed (with
some exceptions).
A variable deIned in the global namespace (deIned outside, and before the function call), can
be read and used from within the function.
In the following example a is in global namespece, while c is in the local namespace of the
function.
def foo1():
c = a + 1 # access varial a, located in the global space
# a = c + 1 # in general, you cannot change variables in the global namespaces
print("local variable: ", c)
a = 15
foo1()
print(a)
local variable: 16
15
%who
When the name of variable, already deIned in the global space, is reused within the function
to assign (create) a new variable, this new variable in local namespace is distinct from the
variable in the global namaspace.
In the following example the same name a is used in the local and global namespace. Within
the function, we access to the variable in the local namespace, whereas the variable in the
global namespace is left unmodiIed.
def b():
a = c + 1
print('local namespace', a)
a = 12
c = 13
b()
print('global namespace', a)
def b():
#global a
a = c + 1
print('global namespace', c)
print('local namespace', a)
#print('global namespace', a)
a = 12
c = 13
b()
print('global namespace', a)
global namespace 13
local namespace 14
global namespace 12
print_twice('Hello')
print_twice('Ciao')
print_twice('Hello', param2="Giovanni!", param3 = 'How old are you?')
print_twice('Hello', param2="Giovanni!")
print_twice('Ciao', param3="Come stai?")
It is a text put at the beginning of a function to explains the interface (doc is short for
documentation). Here is an example:
def fib(n):
'''
Print a Fibonacci series up to n, n>=0.
example for n=35: 0,1,1,2,3,5,8,13,21,34
Args:
n: limit of the value to include in the series, n>=0.
Returns:
No return value, but print the list.
'''
if n == 0:
print(0) # first Fibonacci value
elif n >= 1:
print(0, 1, 1, end=' ') # first three Fibonacci values
next = 2 # new Fib value (to print)
previous = 1 # previous Fib value (already printed)
while next <= n:
print(next, end=' ')
new_next = next + previous # store in "new_next" the next Fib value (to
print()
By convention, all docstrings are triple-quoted strings, also known as multiline string because
the triple quotes allow the string to span more than one line.
There are tools which use docstrings to automatically produce online or printed
documentation, or to let the user interactively browse through code.
def fib(n):
'''
Print a Fibonacci series up to n, n>=0.
example for n=35: 0,1,1,2,3,5,8,13,21,34
Args:
n: limit of the value to include in the series (n >= 0).
Returns:
No return value, but print the list.
'''
if n == 0:
print(0) # first Fibonacci value
elif n >= 1:
print(0, 1, 1, end=' ') # first three Fibonacci values
next = 2 # new Fib value (to print)
previous = 1 # previous Fib value (already printed)
while next <= n:
print(next, end=' ')
new_next = next + previous # store in "new_next" the next Fib value (to print
0 1 1 2 3 5 8 13 21 34 55 89 144 233
# fib
fib(1000)
But the above functions are all void: they have an effect, as they print values, but they do not
have a return value. How can we write fruitful functions?
Look at this example, a function that returns the area of a circle with the given radius:
def area(radius):
a = 3.14 * radius**2
return a
We can use the return statement with or without arguments. If the function is void, the
return statement forces the exit from function. If the function is fruitful, the return
statement must be followed by an expression, as in the example above.
You can use fruitful functions within expressions, or assign the function, indeed the returned
value, to a variable.
import math
def area_square(side):
a = side**2
return a
def area_circle(radius):
a = math.pi * area_square(radius) #radius**2
return a
314.1592653589793
323.1592653589793
The Irst step is to decide what the parameters should be. In this case it should be clear that
factorial takes an integer:
def factorial(n):
def factorial(n):
if n == 0:
return 1
else:
recurse = factorial(n-1)
result = n * recurse
return result
Since 3 is not 0 , we take the second branch and calculate the factorial of 𝑛 − 1 (which is 2 )...
Since 2 is not 0 , we take the second branch and calculate the factorial of 𝑛 − 1 (which is 1
)...
Since 1 is not 0 , we take the second branch and calculate the factorial of 𝑛 − 1 (which is
0 )...
Since 0 equals 0 , we take the Irst branch and return 1 without making any more
recursive calls.
The return value, 1 , is multiplied by 𝑛 , which is 1 , and the result is returned.
The return value, 1 , is multiplied by 𝑛 , which is 2 , and the result is returned.
The return value (2 ) is multiplied by 𝑛 , which is 3 , and the result, 6 , becomes the return value
of the function call that started the whole process.
Try with Python Tutor by deIning and then calling the function:
result = factorial(3)
def factorial(n):
if n == 0:
return 1
else:
recurse = factorial(n-1)
result = n * recurse
return result
print(factorial(4))
24
Each function is represented by a frame, indeed a box with the name of a function beside it
and the parameters and variables of the function inside it.
The frames are arranged in a stack that indicates which function called which, and so on.
result = factorial(3)
The return values are shown being passed back up the stack. In each frame, the return value
is the value of result , which is the product of 𝑛 and recurse .
In the last frame, the local variables recurse and result do not exist, because the branch
that creates them does not run.
Note how many different instances of variable result are generated during the recursive
call.
Exercises
1. Write a function check_prime that takes an integer and returns True if the number is
prime, and False otherwise.
def check_prime(n):
'''
Return True if the input argument n is prime, False otherwise
Args:
n: integer to be checked.
Returns:
Boolean value.
'''
is_prime = True
return is_prime
10 : False
1 : True
13 : True
# 𝑓𝑖𝑏(0) = 0
# 𝑓𝑖𝑏(1) = 1
# 𝑓𝑖𝑏(𝑛) = 𝑓𝑖𝑏(𝑛−1)+𝑓𝑖𝑏(𝑛−2)
# recursive
def recfib(n):
'''
Print the n-th element of the Fibonacci series (counting from 0)
Args:
n: n-th position in the Fibonacci series.
Returns:
Value of the item in the series at position n.
'''
if n == 0:
return 0
if n == 1:
return 1
return recfib(n-2) + recfib(n-1)
for i in range(20):
print(recfib(i), end=' ')
# 𝑠𝑢𝑚_𝑙𝑖𝑠𝑡(𝑙,𝑙) = 𝑙
# 𝑠𝑢𝑚_𝑙𝑖𝑠𝑡(𝑙,𝑢) = 𝑙 + 𝑠𝑢𝑚_𝑙𝑖𝑠𝑡(𝑙+1,𝑢)
def sum_list(l,u):
'''
Compute and return the sum of the integers x, where l <= x <= u.
Args:
l: lower bound of the series
u: upper bound of the series
requirement: l <= u
Returns:
Sum of the subseries.
'''
if l > u:
s = 0
elif l == u:
s = l
else:
s = l + sum_list(l+1,u)
return s
def sum_list_norec(l,u):
if l > u:
return 0
s = 0
for item in range(l,u+1):
print("-->", item)
s = s + item
return s
print(sum_list(6,5))
print(sum_list(5,5))
print(sum_list(5,10), 10*11/2 - 4*5/2)
print("\n", 6, 5, "*****")
print(sum_list_norec(6,5))
print("\n", 5, 5, "*****")
print(sum_list_norec(5,5))
0
5
45 45.0
6 5 *****
0
5 5 *****
--> 5
5
5 10 *****
--> 5
--> 6
--> 7
--> 8
--> 9
--> 10
45 45.0