Defining functions#
Introduction#
A function is a block of code which takes some parameters and uses them to produce or report some result.
In this notebook we will see how to define functions to reuse code, and talk about variables scope.
See also
For a complete course on functions, see Andrea Passerini’s slides A04
Anatomy of a function definition#
A function in Python is an object, meaning it’s a variable like any other.
The main way it differs from the other types of variables you’ve seen until now is in the way it is assigned, or defined.
For instance, let’s imagine you want to assign a function to a variable named fun
.
In that case, you wouldn’t simply start writing fun = ...
, like a normal variable assignment, but use the reserved keyword def
, like so:
def fun():
"here we enter an indented block"
print(type(fun))
<class 'function'>
So you actually just assigned a function object to the variable fun
with this little piece of code.
As you can see, there’s more after the def fun
: you open a pair of brackets, close it, add a colon :
, and then write more things on the following line, indented by four spaces.
So, first, what are the brackets for? Inside the brackets is where you define the arguments of the function, each separated by a comma ,
:
def fun(arg1, arg2):
"here we enter an indented block"
These are variables that will live inside your function – or, in other words, in its scope, but we’ll get back to that.
You assign values to these variables when you call the function. Similarly to how it’s defined, you call a function by adding a pair of brackets after its name, and passing in them the value of each argument:
fun(1, 2)
Important
Something crucial about function arguments is that when you call a function, all of its arguments must have been assigned some value.
Hence why the following returns an error:
fun(1)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[5], line 1
----> 1 fun(1)
TypeError: fun() missing 1 required positional argument: 'arg2'
To illustrate, when you call it correctly, like
fun(1, 2)
before executing the code block below the def
, the following happens:
arg1 = 1
arg2 = 2
... # whatever code was inside the function
Ok, but for now, we’re not doing anything with our arguments, so what happens when we call the function?
output = fun(1,2)
print(output)
None
It returns something, but in this case it’s just a None
.
The string we added in the body of our function got lost, apparently.
Important
That’s because, in order to get the value of any variable out of a function, you need to return
this variable.
So let’s now introduce a return
statement at the end of our function:
def fun(arg1, arg2):
not_returned = 2
new_output = 1
return new_output
print(fun(1, 2))
print(not_returned)
1
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[7], line 7
4 return new_output
6 print(fun(1, 2))
----> 7 print(not_returned)
NameError: name 'not_returned' is not defined
We did not return not_returned
, so it got lost, while the value 1
we returned through new_output
got printed successfully.
The variable new_output
itself got lost, though:
print(new_output)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[8], line 1
----> 1 print(new_output)
NameError: name 'new_output' is not defined
That’s because only the value of new_output
was returned, not the variable itself!
So in order to store the result of a function call for later, you should assign this value to a new variable.
Let’s see this more clearly with Python tutor:
%%tutor
def fun(arg1, arg2):
not_returned = 2
new_output = 1
return new_output
output = fun(1, 2)
As you can see, when fun
is called, a new frame, which is local to the function, is created.
This frame, and all the variables it contains, is then completely discarded at the end of the function call.
The only thing that gets out of the function is the return value, that we then assign to the variable output
that lives in the global frame.
That’s it, we’ve now seen how to define a function. Still, it’s a shame that until now, we’ve passed values to the arguments of a function that does not even use them! So let’s end this part with the first example of a function that would actually make sense:
def fun(arg1, arg2):
return arg1 + arg2
result_sum = fun(1, 2)
print(result_sum)
3
Why functions?#
We may need functions for a lot of reasons, including:
Reduce code duplication: put in functions parts of code that are needed several times in the whole program, so you don’t need to repeat the same code over and over again;
Decompose a complex task: make the code easier to write and understand by splitting the whole program in several easier functions;
Don’t be shy of creating new variables#
You might be tempted to modify one of the function arguments in a function’s body, in order to avoid typing an extra line of code to assign a new variable. Do that only if your function is actually supposed to modify this input! You’re saving very little time and could create yourself a lot of confusion further down the line.
So, for instance, this is fine:
def append_42_to_list(li):
li.append(42)
While this is not:
def print_last_char_twice(my_str):
my_str = my_str[-1]
my_str = my_str * 2
print(my_str)
s = 'no'
print_last_char_twice(s)
oo
so in this case, prefer:
def print_last_char_twice(my_str):
last_char = my_str[-1]
result = last_char * 2
print(result)
Notice how much clearer this is to read?
What’s in a name#
Functions, like variables, should have a meaningful name! It shouldn’t be too long, but its purpose should be somewhat clear simply from its name
Also, when you allow the forces of evil to take the best of you, you might be tempted to use reserved words like list
as a variable for you own miserable purposes:
list("ciao")
['c', 'i', 'a', 'o']
list = ['my', 'pitiful', 'list']
Python allows you to do so, but we do not, for the consequences are disastrous.
For example, if you now attempt to use list
for its intended purpose like casting to list, it won’t work anymore:
list("ciao")
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-4-c63add832213> in <module>()
----> 1 list("ciao")
TypeError: 'list' object is not callable
In particular, we recommend to not redefine these precious functions:
bool
,int
,float
,tuple
,str
,list
,set
,dict
max
,min
,sum
next
,iter
id
,dir
,vars
,help
In case of doubt, most code editors should color these functions as you type their name. By default in Jupyter, they should appear in green, for instance.
Different function kinds#
You can roughly find 5 different function kinds in the wild:
Produces side effects: prints / asks manual input / writes by modifying the environment in some way - examples: printing characters on the screen, asking interactively input from the user, writing into a file
Returns a value, either as new memory region or a pointer to an existing memory region
Modifies the input
Modifies the input and returns it (allows for call chaining)
Modifies the input and returns something derived from it
Let’s try now to understand the differences with various examples.
Side effects#
Only prints / asks interactive input / writes into a file
Does not modify the input!
Does not return anything!
Example:
%%tutor
def printola(lst):
"""prints the first two elements of the given list
"""
print('the first two elements are', lst[0], lst[1])
la = [8,5,6,2]
printola(la)
Return#
Return some value, either as new memory region or a pointer to an existing memory region according to the function text
Does not modify the input
Does not print anything!
Example:
%%tutor
def returnola(lst):
"""RETURN a NEW list having all the numbers doubled
"""
ret = []
for el in lst:
ret.append(el*2)
return ret
la = [5,2,6,3]
res = returnola(la)
print("la :", la)
print("res:", res)
Modify#
Modify the input. by modifying, we typically mean changing data inside existing memory regions, limiting as much as possible the creation of new ones.
Does not return anything!
Does not print anything!
Does not create new memory regions (or limits the creation to the bare needed)
Example:
%%tutor
def modifanta(lst):
"""MODIFIES lst by ordering it in-place
"""
lst.sort()
la = [43434]
la = [7,4,9,8]
modifanta(la)
print("la:", la)
Modify and return#
Modifies the input and returns a pointer to it
Does not print anything!
Does not create new memory regions (or limits the creation to the bare needed)
Note: allows call chaining
%%tutor
def modiret(lst):
"""MODIFY lst by doubling all its elements, and finally RETURNS it
"""
for i in range(len(lst)):
lst[i] = lst[i] * 2
return lst
la = [8,7,5]
res = modiret(la)
print("res :", res) # [16,14,10] RETURNED the modified input
print("la :", la) # [16,14,10] la input was MODIFIED !!
print()
lb = [7,5,6]
modiret(lb).reverse() # NOTE WE CAN CONCATENATE
print("lb :", lb) # [12,10,14] lb input was MODIFIED !!
#modiret(lb).reverse().append(16) # ... but this wouldn't work. Why?
Modify and return a part#
Modify the input and return a part of it
Does not print anything!
%%tutor
def modirip(lst):
"""MODIFY lst by sorting it and removing the greatest element. Finally, RETURN the removed element.
"""
lst.sort()
ret = lst[-1]
lst.pop()
return ret
la = ['b','c','a']
res = modirip(la)
print("res :", res) # 'c' RETURNED a piece of the input
print("la :", la) # ['a','b'] la was MODIFIED!!
Immutable values#
Basic types such integers, float, booleans are immutable, as well as some sequences like strings and tuples : when you are asked to RETURN one of these types, say a string, the only thing you can do is obtaining NEW strings based upon the parameters you receive. Let’s see an example.
Suppose we are asked to implement this function:
Write a function
my_upper
which RETURNS the passed string as uppercase.
We could implement it like this:
%%tutor
external_string = "sailor"
def my_upper(s):
ret = s.upper() # string methods create NEW string
return ret
result = my_upper(external_string)
print(' result:', result)
print('external_string:', external_string)
Notice some things:
the
external_string
didn’t changewe didn’t refer to
external_string
inside the function body: doing so would have defeated the purpose of functions, which is to isolate them from outside world.
Changing the world: fail / 1#
What if we actually did want to change the assignment of external_string
?
You might be tempted to write something like an assignment s =
right inside the function. The following code will not work.
Question
Why? Try to answer before checking execution in Python Tutor.
Answer
s = s.upper()
only MODIFIES the assignment s
of the function call frame, it has no effect on outside world
%%tutor
external_string = "sailor"
def my_upper(s):
s = s.upper()
return s
result = my_upper(external_string)
print(' result:', result)
print('external_string:', external_string)
Changing the world: fail / 2#
Let’s see another temptation. You might try to assign external_string =
right inside the function. The following code again will not work
Question
Why? Try to answer before checking execution in Python Tutor.
Answer
external_string = s.upper()
creates NEW variable external_string
inside frame of function call
%%tutor
external_string = "sailor"
def my_upper(s):
external_string = s.upper()
return external_string
result = my_upper(external_string)
print(' result:', result)
print('external_string:', external_string)
Changing the world: success!#
The proper way to tackle the problem is to create a NEW string inside the function, return it, and then outside the function perform the assignment external_string = result
.
%%tutor
external_string = "sailor"
def my_upper(s):
ret = s.upper()
return ret
result = my_upper(external_string)
external_string = result # reassignes *outside*
print(' result:', result)
print('external_string:', external_string)
Function definition - questions#
For each of the following expressions, try guessing the result it produces (or if it gives error)
def f(): print('car') print(f())
def f(): print('car') print(f())
def f(): return 3 print(f())
def f(): return 3 print(f())
def f() return 3 print(f())
def f(): return 3 print(f()f())
def f(): return 3 print(f()*f())
def f(): pass print(f())
def f(x): return x print( f() )
def f(x): return x print( f(5) )
def f(): print('fire') x = f() print(x)
def f(): return(print('fire')) print(f())
def f(x): return 'x' print(f(5))
def f(x): return x print(f(5))
def etc(): print('etc...') return etc() etc()
def gu(): print('GU') ru() def ru(): print('RU') gu() gu()
Mutable values#
Sequences like lists, sets, dictionaries are mutable objects. When you call a function and pass one of these objects, Python actually gives the function only a reference to the object: a very small pointer which is just an arrow pointing to the memory region where the actual object resides. Since the function only receives a small pointer, calling the function is a fast operation. On the other side, we need to be aware that since no copy of the whole data structure is performed, inside the function it will be like operating on the original memory region which lives outside the function call.
All of this may feel like a bit of a mouthful. Let’s see a practical example in Python Tutor.
Let’s say we need to implement this function:
Write a function which takes a list and MODIFIES it by doubling all of its numbers
Note in the text we used the word MODIFIES, meaning we really want to change the original memory region of the external object we are given.
As simple as it might seem, there are many ways to get this wrong. Let’s see some.
Doubling: fail / 1#
You might be tempted to solve the problem like the following code, but it will not work.
Question
Why? Try to answer before checking execution in Python Tutor.
Answer
element
is created as a NEW variable in the function call frame, doesn’t change original cells
%%tutor
external_numbers = [10,20,30]
def double(lst):
for element in lst:
element = element * 2
double(external_numbers)
Doubling: fail / 2#
You might have another temptation to solve the problem like the following code, but again it will not work.
Question
Why? Try to answer before checking execution in Python Tutor.
Answer
tmp = []
creates a NEW list, but function text tells to MODIFYlst = tmp
Violates IV COMANDMENT: sets the variablelst
in the call frame to point totmp
, but both will be lost after the function call is over
%%tutor
external_numbers = [10,20,30]
def double(lst):
tmp = []
for element in lst:
tmp.append(element * 2)
lst = tmp
double(external_numbers)
Doubling: fail / 3#
You might be tempted to solve the problem also like in the following code, but again it will not work.
Question
Why? Try to answer before checking execution in Python Tutor.
Answer
tmp = []
creates a NEW list, but function text tells to MODIFYexternal_numbers = tmp
creates a NEW associationexternal_numbers
inside function call frame, but it will be lost after the function call is over
%%tutor
external_numbers = [10,20,30]
def double(lst):
tmp = []
for element in lst:
tmp.append(element * 2)
external_numbers = tmp
double(external_numbers)
Doubling: fail / 4#
Let’s see the final temptation, which yet again will not work.
Question
Why? Try to answer before checking execution in Python Tutor.
Answer
external_numbers = [10,20,30]
def double(lst):
tmp = [] # WRONG: we are creating a NEW list, text tells to MODIFY
for element in lst:
tmp.append(element * 2) # WRONG: we are modifying a NEW list
return tmp # WRONG: text didn't ask you to RETURN anything
external_numbers = double(external_numbers) # WRONG: even if external_numbers association will
# actually point to a list of doubled numbers,
# it will be a completely NEW region of memory,
# while we wanted to MODIFY the original one!
%%tutor
external_numbers = [10,20,30]
def double(lst):
tmp = []
for element in lst:
tmp.append(element * 2)
return tmp
external_numbers = double(external_numbers)
Probably you are a bit confused about the previous attempt, which to the untrained eye might look successful. Let’s try to rewrite it with one variable more saved
which will point to exactly the same original memory region of external_numbers
. You will see that at the end saved
will point to [10,20,30]
, showing we didn’t actually MODIFY the original region.
%%tutor
external_numbers = [10,20,30]
saved = external_numbers # we preserve a pointer
def double(lst):
tmp = []
for element in lst:
tmp.append(element * 2)
return tmp
external_numbers = double(external_numbers)
print('external_numbers:', external_numbers) # [20,40,60]
print(' saved:', saved) # [10,20,30]
Doubling success!#
Let’s finally see the right way to do it: we need to consider we want to refer to original cells, so to do it properly we need to access them by index, and we will need a for in
range.
%%tutor
external_numbers = [1, 2, 3, 4, 5]
def double(lst):
for i in range(len(lst)):
lst[i] = lst[i] * 2
double(external_numbers)
Notice that:
when the function call frame is created, we see an arrow to the original data
the
external_list
actually changed, without ever reassigning it (not even outside)we didn’t reassign
lst =
inside the function body, as the IV COMMANDMENT prescribes not to reassign parameterswe didn’t use
return
, as the function text told us nothing about returningwe didn’t referred to
external_list
inside the function body: doing so would have defeated the purpose of functions, which is to isolate them from outside world.
In general, in the case of mutable data data isolation is never tight, as we get pointers to data living outside the function frame. When we manipulate pointers it’s really up to us to take special care.
Optional arguments#
Until now, we’ve only seen functions with so-called positional arguments.
As we’ve seen earlier, the value of every positional argument must be specified when calling a function.
There is another kind of arguments which are optional: they’re known as keyword arguments, or kwargs
, in short.
This allows you for instance to define the following function multiply
, that by default, doesn’t modify the input list:
def multiply(lst, by=1):
for i in range(len(lst)):
lst[i] = lst[i] * by
Since we have by=1
in the function definition, even if we do not specify a value for this argument when calling the function, everything goes fine, as it does have a default value:
some_list = [1, 2, 3]
multiply(some_list)
some_list
[1, 2, 3]
Importantly, when you want to pass a value to these arguments, they must be placed after the positional arguments. Else, you’ll get an error:
multiply(by=2, [1, 2])
Cell In[32], line 1
multiply(by=2, [1, 2])
^
SyntaxError: positional argument follows keyword argument
Important
Basically, positional arguments are like old people on the bus: nobody’s allowed to take their usual seat.
Now, there’s a good reason we’re seeing keyword arguments after the section on mutable values.
There is a very common pitfall in Python that you should be aware of.
Let’s imagine we want to define a function concat
that concatenates two input lists, with the following requirements:
by default, it should return a copy of the first list,
otherwise, the output list should first have the values of the second list, followed by those of the first list,
the first list should never be mutated.
One possible implementation would be:
%%tutor
def concat(lst, other=[]):
other.extend(lst)
return other
input_list = [1, 2, 3]
print(concat(input_list))
print(concat(input_list))
Important
What you’ve seen with Python Tutor happens because a keyword argument is given its default value at the moment the function is defined. So keyword arguments should not be assigned mutable values like lists or dictionaries, because they could actually get mutated on each fuction call!
A very common pattern you can resort to when you actually need to have a default value with a mutable type is as follows:
%%tutor
def concat(lst, other=None):
if other is None:
other = []
other.extend(lst)
return other
input_list = [1, 2, 3]
print(concat(input_list))
print(concat(input_list))
None
is just used as a placeholder here – it could be any other immutable value, actually –, so that other
gets initialised as the empty list on each function call, and not only upon function definition!
Modifying parameters - Questions#
For each of the following expressions, try guessing the result it produces (or if it gives error)
def zam(bal): bal = 4 x = 8 zam(x) print(x)
def zom(y): y = 4 y = 8 zom(y) print(y)
def per(la): la.append('è') per(la) print(la)
def zeb(lst): lst.append('d') la = ['a','b','c'] zeb(la) print(la)
def beware(la): la = ['?','?'] lb = ['d','a','m','n'] beware(lb) print(lb)
def umpa(string): string = "lompa" word = "gnappa" umpa(word) print(word)
def sporty(diz): diz['sneakers'] = 2 cabinet = {'rackets':4, 'balls': 7} sporty(cabinet) print(cabinet)
def numma(lst): lst + [4,5] la = [1,2,3] print(numma(la)) print(la)
def jar(lst): return lst + [4,5] lb = [1,2,3] print(jar(lb)) print(lb)
Exercises - Changing music#
It’s time to better understand what we’re doing when we mess with variables and function calls.
An uncle of ours gave us a dusty album
of songs (for some reason tens of years have passed since he last turned on the radio)
album = [
"Caterina Caselli - Cento giorni",
"Delirium - Jesahel",
"Jan Hammer - Crockett's Theme",
"Sonata Arctica - White Pearl, Black Oceans",
"Lucio Dalla - 4 marzo 1943.mp3",
"The Wellermen - Wellerman",
"Manu Chao - Por el Suelo",
"Intillimani - El Pueblo Unido"
]
Songs are reported with the group, a dash -
and finally the name. Strong with our new knowledge about functions, we decide to put in practice modern software development practices to analyze these mysterious relics of the past.
In the following you will find several exercises which will ask you to develop functions: making something which seems to work is often easy, the true challenge is following exactly what is asked in function text: take particular care about capitalized words, like PRINT, MODIFY, RETURN, and to the desired outputs, trying to understand to which category the various functions belong to.
Exercises must all be solved following this scheme:
album = ...
def func(songs):
# do something with songs, NOT with album
# ....
func(album) # calls to test, external to function body
Warning
Do not write external variable names inside a function
In particular, here:
Do not reassign
album =
Do not call its methods
album.some_method()
A function must be typically seen as an isolated world, which should interact with the outworld ONLY through the given parameters. By explicitly writing album
, you would override such isolation bringing great misfortune.
Exercise - show#
Write a function show
which, given a list songs
, PRINTS the group justified to the right followed by a :
and the song name
HINT: to justify the text, use the string method .rjust(16)
album = [
"Caterina Caselli - Cento giorni",
"Delirium - Jesahel",
"Jan Hammer - Crockett's Theme",
"Sonata Arctica - White Pearl, Black Oceans",
"Lucio Dalla - 4 marzo 1943.mp3",
"The Wellermen - Wellerman",
"Manu Chao - Por el Suelo",
"Intillimani - El Pueblo Unido"
]
Show code cell content
def show(songs):
for song in songs:
parts = song.split(" - ")
print(parts[0].rjust(16) + ":" + " " + parts[1])
res = show(album)
res is None
Caterina Caselli: Cento giorni
Delirium: Jesahel
Jan Hammer: Crockett's Theme
Sonata Arctica: White Pearl, Black Oceans
Lucio Dalla: 4 marzo 1943.mp3
The Wellermen: Wellerman
Manu Chao: Por el Suelo
Intillimani: El Pueblo Unido
True
Exercise - record#
Write a function record
which, given two lists songsA
and songsB
, MODIFIES songsA
overwriting it with the content of songsB
. If songsB
has less elements than songsS
, fill the remaining spaces with None
ASSUME
songsB
has at most the same number of songs ofsongsA
DO NOT reassign
album
(so noalbum =
)
album = [
"Caterina Caselli - Cento giorni",
"Delirium - Jesahel",
"Jan Hammer - Crockett's Theme",
"Sonata Arctica - White Pearl, Black Oceans",
"Lucio Dalla - 4 marzo 1943.mp3",
"The Wellermen - Wellerman",
"Manu Chao - Por el Suelo",
"Intillimani - El Pueblo Unido",
]
Show code cell content
def record(songA, songB):
for i in range(len(songB)):
songA[i] = songB[i]
i += 1
while i < len(songA):
songA[i] = None
i += 1
record(
album,
["Toto Cotugno - L'Italiano vero", "Mia Martini - Minuetto", "Al Bano- Nel sole"],
)
album
["Toto Cotugno - L'Italiano vero",
'Mia Martini - Minuetto',
'Al Bano- Nel sole',
None,
None,
None,
None,
None]
Exercise - great#
Write a function great
which, given a list of songs, MODIFIES the list by uppercasing all the characters, and then RETURNS it
DO NOT reassign
album
(noalbum =
)
album = [
"Caterina Caselli - Cento giorni",
"Delirium - Jesahel",
"Jan Hammer - Crockett's Theme",
"Sonata Arctica - White Pearl, Black Oceans",
"Lucio Dalla - 4 marzo 1943.mp3",
"The Wellermen - Wellerman",
"Manu Chao - Por el Suelo",
"Intillimani - El Pueblo Unido",
]
Show code cell content
def great(songs):
for i in range(len(songs)):
songs[i] = songs[i].upper()
return songs
print(great(album))
print()
album
['CATERINA CASELLI - CENTO GIORNI', 'DELIRIUM - JESAHEL', "JAN HAMMER - CROCKETT'S THEME", 'SONATA ARCTICA - WHITE PEARL, BLACK OCEANS', 'LUCIO DALLA - 4 MARZO 1943.MP3', 'THE WELLERMEN - WELLERMAN', 'MANU CHAO - POR EL SUELO', 'INTILLIMANI - EL PUEBLO UNIDO']
['CATERINA CASELLI - CENTO GIORNI',
'DELIRIUM - JESAHEL',
"JAN HAMMER - CROCKETT'S THEME",
'SONATA ARCTICA - WHITE PEARL, BLACK OCEANS',
'LUCIO DALLA - 4 MARZO 1943.MP3',
'THE WELLERMEN - WELLERMAN',
'MANU CHAO - POR EL SUELO',
'INTILLIMANI - EL PUEBLO UNIDO']
Exercise - shorten#
Write a function shorten
which given a list of songs
and a number n
, MODIFIES songs
so it has only n
songs, then RETURNS a NEW list with all the removed elements.
if
n
is too large, returns an empty list without modifying the albumUSE a parameter name different from
album
DO NOT reassign
album
(so noalbum =
)
album = [
"Caterina Caselli - Cento giorni",
"Delirium - Jesahel",
"Jan Hammer - Crockett's Theme",
"Sonata Arctica - White Pearl, Black Oceans",
"Lucio Dalla - 4 marzo 1943.mp3",
"The Wellermen - Wellerman",
"Manu Chao - Por el Suelo",
"Intillimani - El Pueblo Unido",
]
Show code cell content
def shorten(songs, n):
ret = []
if n >= len(songs):
return ret
for i in range(n):
ret.append(songs.pop())
ret.reverse()
return ret
res1 = shorten(album, 3)
print("returned:\n", res1, "\n")
print("the album is:\n", album, "\n")
res2 = shorten(album, 5)
print("returned:\n", res2, "\n")
print("the album is:\n", album, "\n")
returned:
['The Wellermen - Wellerman', 'Manu Chao - Por el Suelo', 'Intillimani - El Pueblo Unido']
the album is:
['Caterina Caselli - Cento giorni', 'Delirium - Jesahel', "Jan Hammer - Crockett's Theme", 'Sonata Arctica - White Pearl, Black Oceans', 'Lucio Dalla - 4 marzo 1943.mp3']
returned:
[]
the album is:
['Caterina Caselli - Cento giorni', 'Delirium - Jesahel', "Jan Hammer - Crockett's Theme", 'Sonata Arctica - White Pearl, Black Oceans', 'Lucio Dalla - 4 marzo 1943.mp3']
Lambda functions#
Lambda functions are functions which:
have no name
are defined on one line, typically right where they are needed
their body is an expression, thus you need no
return
Let’s create a lambda function which takes a number x
and doubles it:
lambda x: x*2
<function __main__.<lambda>(x)>
As you see, Python created a function object, which gets displayed by Jupyter. Unfortunately, at this point the function object got lost, because that is what happens to any object created by an expression that is not assigned to a variable.
To be able to call the function, we will thus convenient to assign such function object to a variable, say f
:
f = lambda x: x*2
f
<function __main__.<lambda>(x)>
Great, now we have a function we can call as many times as we want:
f(5)
10
f(7)
14
So writing
def f(x):
return x*2
or
f = lambda x: x*2
are completely equivalent forms, the main difference being with def
we can write functions with bodies on multiple lines. Lambdas may appear limited, so why should we use them? Sometimes they allow for very concise code. For example, imagine you have a list of tuples holding animals and their lifespan:
animals = [("dog", 12), ("cat", 14), ("pelican", 30), ("eagle", 25), ("squirrel", 6)]
If you want to sort them by lifespan, you can try the .sort
method but it will not work:
animals.sort()
animals
[('cat', 14), ('dog', 12), ('eagle', 25), ('pelican', 30), ('squirrel', 6)]
Clearly, this is not what we wanted. To get proper ordering, we need to tell Python that when it considers a tuple for comparison, it should extract the lifespan number. To do so, Python provides us with key
parameter, to which we must pass a function that takes as argument the sequence element under consideration (in this case a tuple) and returns a transformation of it which Python will use to perform the comparisons - in this case we want the life expectancy at the 1-th position in the tuple:
animals.sort(key=lambda t: t[1])
animals
[('squirrel', 6), ('dog', 12), ('cat', 14), ('eagle', 25), ('pelican', 30)]
Now we got the ordering we wanted. We could have written the thing as
def myf(t):
return t[1]
animals.sort(key=myf)
animals
[('squirrel', 6), ('dog', 12), ('cat', 14), ('eagle', 25), ('pelican', 30)]
but lambdas clearly save some keyboard typing.
Warning
Note, however, that except in these kinds of cases in which you need to pass a function once to some function or method, it is highly preferable to define functions with a def
.
Notice lambdas can take multiple parameters:
mymul = lambda x,y: x * y
mymul(2,5)
10
Exercise - apply_borders#
✪ Write a function apply_borders
which takes a function f
as parameter and a sequence, and RETURN a tuple holding two elements:
first element is obtained by applying
f
to the first element of the sequencesecond element is obtained by applying
f
to the last element of the sequence
Show code cell content
def apply_borders(f, seq):
return (f(seq[0]), f(seq[-1]))
print(apply_borders(lambda x: x.upper(), ["the", "river", "is", "very", "long"]))
print(apply_borders(lambda x: x[0], ["the", "river", "is", "very", "long"]))
('THE', 'LONG')
('t', 'l')
Exercise - process#
✪✪ Write a lambda expression to be passed as first parameter of the function process
defined down here, so that a call to process generates a list as shown here:
NOTE: process
is already defined, you do not need to change it
def process(f, lista, listb):
orda = list(sorted(lista))
ordb = list(sorted(listb))
ret = []
for i in range(len(lista)):
ret.append(f(orda[i], ordb[i]))
return ret
Show code cell content
f = lambda x,y: x.upper() + y
process(f, ["d", "b", "a", "c", "e", "f"], ["q", "s", "p", "t", "r", "n"])
['An', 'Bp', 'Cq', 'Dr', 'Es', 'Ft']