Functions 1 - introduction

Introduction

A function is some 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.

WARNING: this tutorial is not really complete

For more info see:

What to do

  • unzip exercises in a folder, you should get something like this:

functions
   fun1-intro.ipynb
   fun1-intro-sol.ipynb
   fun2-errors-and-testing.ipynb
   fun2-errors-and-testing-sol.ipynb
   fun3-strings.ipynb
   fun3-strings-sol.ipynb
   fun4-lists.ipynb
   fun4-lists-sol.ipynb
   fun5-tuples.ipynb
   fun5-tuples-sol.ipynb
   fun6-sets.ipynb
   fun6-sets-sol.ipynb
   fun7-dictionaries.ipynb
   fun7-dictionaries-sol.ipynb
   fun8-chal.ipynb
   jupman.py

WARNING: to correctly visualize the notebook, it MUST be in an unzipped folder !

  • open Jupyter Notebook from that folder. Two things should open, first a console and then browser. The browser should show a file list: navigate the list and open the notebook functions/fun1-intro.ipynb

  • Go on reading that notebook, and follow instuctions inside.

Shortcut keys:

  • to execute Python code inside a Jupyter cell, press Control + Enter

  • to execute Python code inside a Jupyter cell AND select next cell, press Shift + Enter

  • to execute Python code inside a Jupyter cell AND a create a new cell aftwerwards, press Alt + Enter

  • If the notebooks look stuck, try to select Kernel -> Restart

Why functions?

We may need functions for a lot of reasons, including:

  1. 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;

  2. Decompose a complex task: make the code easier to write and understand by splitting the whole program in several easier functions;

Function definition - questions

For each of the following expressions, try guessing the result it produces (or if it gives error)

  1. def f():
    print('car')
    print(f())
    
  2. def f():
        print('car')
    print(f())
    
  3. def f():
    return 3
    print(f())
    
  4. def f():
        return 3
    print(f())
    
  5. def f()
        return 3
    print(f())
    
  6. def f():
        return 3
    print(f()f())
    
  7. def f():
        return 3
    print(f()*f())
    
  8. def f():
        pass
    print(f())
    
  9. def f(x):
        return x
    print( f() )
    
  10. def f(x):
        return x
    print( f(5) )
    
  11. def f():
        print('fire')
    x = f()
    print(x)
    
  12. def f():
        return(print('fire'))
    print(f())
    
  13. def f(x):
        return 'x'
    print(f(5))
    
  14. def f(x):
        return x
    print(f(5))
    
  15. def etc():
        print('etc...')
        return etc()
    etc()
    
  16. def gu():
        print('GU')
        ru()
    def ru():
        print('RU')
        gu()
    gu()
    

Different function kinds

You can roughly find 5 different function kinds in the wild:

  1. 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

  2. RETURNS a value, either as NEW memory region or a pointer to an existing memory region

  3. MODIFIES the input

  4. MODIFIES the input and RETURNS it (allows for call chaining)

  5. 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:

[2]:
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)
jupman.pytut()
The first two elements are 8 5
[2]:

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:

[3]:
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)
jupman.pytut()
la : [5, 2, 6, 3]
res: [10, 4, 12, 6]
[3]:

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:

[4]:
def modifanta(lst):
    """MODIFIIES lst by ordering it in-place
    """
    lst.sort()
    la = [43434]


la = [7,4,9,8]

modifanta(la)

print("la:", la)
jupman.pytut()
la: [4, 7, 8, 9]
[4]:

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

[5]:
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?

jupman.pytut()
res : [16, 14, 10]
la  : [16, 14, 10]

lb  : [12, 10, 14]
[5]:

MODIFY AND RETURN A PART

MODIFY the input and RETURN a part of it

  • DOES NOT print anything!

[6]:
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!!
jupman.pytut()
res   : c
la    : ['a', 'b']
[6]:

Remember the commandments

III COMMANDMENT

You shall never ever reassign function parameters

Never perform any of these assignments, as you risk losing the parameter passed during function call:

[7]:
def sin(my_int):
    my_int = 666            # you lost the 5 passed from external call!
    print(my_int)           # prints 666

x = 5
sin(x)
666

Same reasoning can be applied to all other types:

[8]:
def evil(my_string):
    my_string = "666"
[9]:
def disgrace(my_list):
    my_list = [666]
[10]:
def delirium(my_dict):
    my_dict = {"evil":666}

For the sole case when you have composite parameters like lists or dictionaries, you can write like below IF AND ONLY IF the function description requires to MODIFY the internal elements of the parameter (like for example sorting a list in-place or changing the field of a dictionary).

[11]:
# MODIFY my_list in some way
def allowed(my_list):
    my_list[2] = 9

outside = [8,5,7]
allowed(outside)
print(outside)
[8, 5, 9]

On the other hand, if the function requires to RETURN a NEW object, you shall not fall into the temptation of modifying the input:

[12]:

# RETURN a NEW sorted list
def pain(my_list):
    my_list.sort()    # BAD, you are modifying the input list instead of creating a new one!
    return my_list
[13]:
# RETURN a NEW list
def crisis(my_list):
    my_list[0] = 5    # BAD, as above
    return my_list
[14]:
# RETURN a NEW dictionary
def torment(my_dict):
    my_dict['a'] = 6  # BAD, you are modifying the input dictionary instead of creating a new one!
    return my_dict
[15]:
# RETURN a NEW class instance
def desperation(my_instance):
    my_instance.my_field = 6  # BAD, you are modifying the input object
                              #      instead of creating a new one!
    return my_instance

IV COMMANDMENT

You shall never ever reassign values to function calls or methods

WRONG:

my_function() = 666
my_function() = 'evil'
my_function() = [666]

CORRECT:

x = 5
y = my_fun()
z = []
z[0] = 7
d = dict()
d["a"] = 6

Function calls like my_function() return calculations results and store them in a box in memory which is only created for the purposes of the call, and Python will not allow us to reuse it like it were a variabile.

Whenever you see name() in the left part, it cannot be followed by the equality sign = (but it can be followed by two equals sign == if you are doing a comparison).

V COMMANDMENT

You shall never ever redefine system functions

Python has several system defined functions. For example list is a Python type: as such, you can use it for example as a function to convert some type to a list:

[16]:
list("ciao")
[16]:
['c', 'i', 'a', 'o']

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 = ['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

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:

[17]:
# Run this cell to have Python Tutor working
import jupman;
[18]:
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)

jupman.pytut()
         result: SAILOR
external_string: sailor
[18]:

Notice some things:

  • the external_string didn’t change

  • we didn’t write s = inside the function body, as the IV COMMANDMENT prescribes not to reassign parameters

  • we 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.

Show answer
[19]:
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)

jupman.pytut()
         result: SAILOR
external_string: sailor
[19]:

Changing the world: fail / 2

Let’s see another temptation. You might try to assign external_string = right inside the function. The follwing code again will not work

QUESTION: Why? Try to answer before checking execution in Python Tutor.

Show answer
[20]:
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)

jupman.pytut()
         result: SAILOR
external_string: sailor
[20]:

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.

[21]:
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)

jupman.pytut()
         result: SAILOR
external_string: SAILOR
[21]:

global keyword

If we really wanted to modify external_string association inside the function, we could still do it with global keyword, but typically it’s best to avoid using it.

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.

Show answer
[22]:
external_numbers = [10,20,30]

def double(lst):
    for element in lst:
        element = element * 2

double(external_numbers)

jupman.pytut()
[22]:

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.

Show answer
[23]:
external_numbers = [10,20,30]

def double(lst):
    tmp = []
    for element in lst:
        tmp.append(element * 2)

    lst = tmp

double(external_numbers)

jupman.pytut()
[23]:

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.

Show answer
[24]:
external_numbers = [10,20,30]

def double(lst):
    tmp = []
    for element in lst:
        tmp.append(element * 2)

    external_numbers = tmp

double(external_numbers)

jupman.pytut()
[24]:

Doubling: fail / 4

Lastly, you might fall into this other temptation tempted, but yet again it will not work.

QUESTION: Why? Try to answer before checking execution in Python Tutor.

Show answer
[25]:
external_numbers = [10,20,30]

def double(lst):
    tmp = []
    for element in lst:
        tmp.append(element * 2)

    return tmp

external_numbers = double(external_numbers)

jupman.pytut()
[25]:

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.

[26]:
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]

jupman.pytut()
external_numbers: [20, 40, 60]
           saved: [10, 20, 30]
[26]:

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.

[27]:

external_numbers = [1,2,3,4,5]

def double(lst):
    for i in range(len(lst)):
        lst[i] = lst[i] * 2

double(external_numbers)

jupman.pytut()
[27]:

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 parameters

  • we didn’t use return, as the function text told us nothing about returning

  • we 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.

Modifying parameters - Questions

For each of the following expressions, try guessing the result it produces (or if it gives error)

  1. def zam(bal):
        bal = 4
    x = 8
    zam(x)
    print(x)
    
  2. def zom(y):
        y = 4
    y = 8
    zom(y)
    print(y)
    
  3. def per(la):
        la.append('è')
    per(la)
    print(la)
    
  4. def zeb(lst):
        lst.append('d')
    la = ['a','b','c']
    zeb(la)
    print(la)
    
  5. def beware(la):
        la = ['?','?']
    lb = ['d','a','m','n']
    beware(lb)
    print(lb)
    
  6. def umpa(string):
        string = "lompa"
    word = "gnappa"
    umpa(word)
    print(word)
    
  7. def sporty(diz):
        diz['sneakers'] = 2
    cabinet = {'rackets':4,
               'balls': 7}
    sporty(cabinet)
    print(cabinet)
    
  8. def numma(lst):
        lst + [4,5]
    la = [1,2,3]
    print(numma(la))
    print(la)
    
  9. 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)

[28]:
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 misterious 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

DO NOT WRITE EXTERNAL VARIABLE NAMES INSIDE THE FUNCTION

In particular:

  • 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.

ALWAYS USE A PARAMETER NAME DIFFERENT FROM EXTERNAL VARIABLES

For example, if external data is called album, you can call the parameter songs

Exercise - show

Write a function 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)

>>> res = show(album)  # only prints, implicitly it returns 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
>>> res
None
Show solution
[29]:

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"
]

# write here


Exercise - authors

Write a function which given a list of songs, RETURN a NEW list with only the authors

>>> autors(album)
['Caterina Caselli', 'Delirium', 'Jan Hammer', 'Sonata Arctica', 'Lucio Dalla', 'The Wellermen', 'Manu Chao', 'Intillimani']

>>> 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 solution
[30]:

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"
]

# write here


Exercise - record

Write a function 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 remanining spaces with None

  • ASSUME songsB has at most the same number of songs of songsA

  • DO NOT reassign album (so no album =)

# returns nothing!
>>> record(album, ["Toto Cotugno - L'Italiano vero", "Mia Martini - Minuetto", "Al Bano-Nel sole"])

>>> album   # parameeter was modified
["Toto Cotugno - L'Italiano vero",
 "Mia Martini - Minuetto",
 "Al Bano - Nel sole",
 None,
 None,
 None,
 None,
 None]
Show solution
[31]:

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"
]

# write here


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 (no album =)

Example:

>>> great(album)   # return
['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']

>>> album          # parameter was modified
['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 solution
[32]:

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"
]

# write here


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.

  • USE a parameter name different from album

  • DO NOT reassign album (so no album =)

Example:

>>> shorten(album, 3)  # returns
[   "Sonata Arctica - White Pearl, Black Oceans",
    "Lucio Dalla - 4 marzo 1943.mp3",
    "The Wellermen - Wellerman",
    "Manu Chao - Por el Suelo",
    "Intillimani - El Pueblo Unido"
]
>>> album               # the parameter was modified
[   "Caterina Caselli - Cento giorni",
    "Delirium - Jesahel",
    "Jan Hammer - Crockett's Theme",
]
Show solution
[33]:

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"
]

# write here


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:

[34]:
lambda x: x*2
[34]:
<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:

[35]:
f = lambda x: x*2
[36]:
f
[36]:
<function __main__.<lambda>(x)>

Great, now we have a function we can call as many times as we want:

[37]:
f(5)
[37]:
10
[38]:
f(7)
[38]:
14

So writing

[39]:
def f(x):
    return x*2

or

[40]:
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:

[41]:
animals = [('dog', 12), ('cat', 14), ('pelican', 30), ('eagle', 25), ('squirrel', 6)]

If you want to sort them, you can try the .sort method but it will not work:

[42]:
animals.sort()
[43]:
animals
[43]:
[('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, Pyhton provides us with key parameter, which we must pass a function that takes as argument the list element under consideration (in this case a tuple) and will return a trasformation of it (in this case the number at 1-th position):

[44]:
animals.sort(key=lambda t: t[1])
[45]:
animals
[45]:
[('squirrel', 6), ('dog', 12), ('cat', 14), ('eagle', 25), ('pelican', 30)]

Now we got the ordering we wanted. We could have written the thing as

[46]:
def myf(t):
    return t[1]

animals.sort(key=myf)
animals
[46]:
[('squirrel', 6), ('dog', 12), ('cat', 14), ('eagle', 25), ('pelican', 30)]

but lambdas clearly save some keyboard typing

Notice lambdas can take multiple parameters:

[47]:
mymul = lambda x,y: x * y

mymul(2,5)
[47]:
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 sequence

  • second element is obtained by appling f to the last element of the sequence

Example:

>>> apply_borders(lambda x: x.upper(), ['the', 'river', 'is', 'very', 'long'])
('THE', 'LONG')
>>> apply_borders(lambda x: x[0], ['the', 'river', 'is', 'very', 'long'])
('t', 'l')
Show solution
[48]:
# write here


[49]:
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:

>>> f = PUT_YOUR_LAMBDA_FUNCTION
>>> process(f, ['d','b','a','c','e','f'], ['q','s','p','t','r','n'])
['An', 'Bp', 'Cq', 'Dr', 'Es', 'Ft']

NOTE: process is already defined, you do not need to change it

Show solution
[50]:
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

# write here the f = lambda ...


[51]:
process(f, ['d','b','a','c','e','f'], ['q','s','p','t','r','n'])
[51]:
['An', 'Bp', 'Cq', 'Dr', 'Es', 'Ft']

Continue

Go on with error handling and testing

[ ]: