4: LE lambda

Funzioni al volo

Tutti i linguaggi di programmazione offrono la possibilità di dichiarare delle funzioni, cioè dei blocchi di codice separati dal programma principale, che vengono richiamati da esso all'occorrenza. Sappiamo che in Python la dichiarazione di una funzione si effettua mediante la parola chiave def. Ad esempio, in un programma potremmo scrivere:


def quadrato(x):
    return x ** 2    
.  .  .
a = quadrato(5)

Python offre però una sintassi alternativa, che ci permette di dichiarare semplici funzioni nel corpo stesso del programma principale, in maniera più compatta. Le funzioni così dichiarate sono dette lambda (il nome deriva dal lambda calculus, un sistema formale introdotto nella logica matematica da Alonso Church).

Apriamo IDLE e scriviamo:


>>> lambda x : x ** 2

<function <lambda> at 0x035B8A50>

Notiamo che IDLE ha riconosciuto la parola chiave lambda colorandola di arancione, e ci ha risposto dicendo di aver immagazzinato una function <lambda> in una certa locazione di memoria. Quindi ora abbiamo una funzione, ma cosa fa questa funzione e come facciamo a chiamarla? Continuate così (vi ricordo che in IDLE per copiare una riga già scritta su quella corrente basta spostarsi su di essa e premere <INVIO>).


>>> (lambda x : x ** 2)(5)
25

>>> (lambda x : x ** 2)(12)
144

Usando un po' di intuizione dovreste cominciare a capire: la prima x (a sinistra dei due punti) è il parametro formale della funzione, mentre l'espressione x ** 2 a destra è il valore restituito, cioè il quadrato di x. Quindi


>>> lambda x : x ** 2

significa "prendi un numero x e restituisci x ** 2".

La chiamata alla funzione richiede invece una sintassi un po' più complicata: notiamo che, a differenza delle funzioni definite con il def, le lambda non hanno un nome con cui possono essere chiamate. Quindi per eseguire una lambda occorre scrivere tutta la definizione tra parentesi tonde, e poi scrivere di seguito (sempre tra parentesi, come è richiesto da tutte le funzioni) i parametri reali da passare alla funzione. In pratica nella prima riga abbiamo chiesto a Python di calcolare il quadrato di 5 e nella terza il quadrato di 12.

Per mezzo delle lambda Python lascia volutamente la possibilità di dichiarare quelle che nel linguaggio informatico sono dette anonymous functions, cioè "funzioni senza nome" (ne riparleremo più avanti). Tuttavia, dato che chiamare una funzione in questo modo è senz'altro complicato, possiamo dare un nome alle lambda semplicemente assegnando la loro definizione ad una variabile, in questo modo:


>>> quadrato = lambda x : x ** 2
>>> quadrato(5)

25

>>> quadrato(12)

144

Così la chiamata ad una lambda diventa del tutto uguale a quella di una funzione "normale" come quella mostrata nell'esempio iniziale. L'unica differenza è che la lambda viene dichiarata "al volo" senza bisogno di una definizione def staccata dal programma principale.

Una lambda è una funzione anonima definita dalla sintassi

lambda argomenti : espressione

cioè la parola chiave lambda, la lista degli argomenti, i due punti ed un'espressione calcolata a partire dagli argomenti, che sarà il valore restituito dalla funzione.

Notiamo che:

ESERCIZIO 4.1: Digitate queste righe di testo in IDLE:

>>> func1 = lambda x, y : x + y
>>> func2 = lambda x, y : x + " e " + y + " si amano follemente"
>>> func3 = lambda x : x <= 10
e cercate di capire cosa fa ogni funzione. Chiamatele alcune volte con dei parametri appropriati osservando ogni volta il risultato restituito
ESERCIZIO 4.2: Ora definite voi: Fate qualche prova chiamando le funzioni che avete definito con vari parametri.

Lambda ed espressioni condizionali

L'impossibilità di usare le istruzioni if ... else ... nelle lambda può essere aggirata per mezzo delle espressioni condizionali, un costrutto sintattico di Python (in altri linguaggi chiamato operatore ternario) che permette di scrivere in forma compatta delle semplici espressioni basate su una condizione. Scriviamo in IDLE:


>>> a = 5
>>> "numero piccolo" if a < 100 else "numero grande"
    
'numero piccolo'

>>> a = 2000000     
>>> "numero piccolo" if a < 100 else "numero grande"

'numero grande'

Un'espressione del tipo

espressione1 if condizione else espressione2

è detta espressione condizionale: espressione1 ed espressione2 sono due espressioni (che possono restituire qualsiasi valore) mentre condizione è un'espressione booleana: l'espressione condizionale restituisce espressione1 se condizione è vera, espressione2 se è falsa.

Altri esempi:


>>> 1 if 10 > 5 else 2

1

>>> x = 6
>>> y = 8
>>> x - y if x > y else x + y

14

Questa costruzione ha due vantaggi:

Le espressioni condizionali sono largamente usate nelle lambda, proprio perchè permettono di simulare un blocco if ... else ... che, come abbiamo detto, non si può usare:


>>>func = lambda x : "numero piccolo" if x < 100 else "numero grande"
>>>func(10)

'numero piccolo'

>>>func(1000000)

'numero grande'
ESERCIZIO 4.3: Definite:

Callable objects

Prima di continuare penso sia necessario soffermarsi un attimo sulla sintassi che usiamo quando chiamiamo una funzione. Se proviamo a scrivere il nome di una funzione di Python senza parentesi ed argomenti vediamo che non otteniamo nessun errore:


>>> print

<built-in function print>

Questo perchè il nome di una funzione (senza parentesi) è per Python una variabile come un'altra: può essere una built-in (cioè una funzione predefinita, come la print dell'esempio), una funzione definita con def oppure una lambda a cui abbiamo assegnato un nome. Quando chiamiamo la funzione Python vede le parentesi tonde della chiamata come un qualsiasi altro operatore, cioè un simbolo (come +, -, * ecc.) che lega tra loro due o più variabili o costanti (nel nostro caso la funzione e i suoi argomenti) e restituisce un valore (il risultato della funzione). Quindi possiamo dire che le funzioni sono genericamente oggetti per i quali è definito l'operatore (): questi sono genericamente detti callable (cioè oggetti chiamabili). Proviamo a fare qualche esperimento in IDLE:


>>> a = 2
>>> b = 3
>>> a + b                # operatore + tra due int: OK

5

>>> a(b)                 # operatore (): errore

Traceback (most recent call last):
  File "<pyshell#17>", line 1, in 
    a(b)
TypeError: 'int' object is not callable

Abbiamo assegnato alle variabili a e b due numeri interi, ed abbiamo usato l'operatore di addizione che ha restituito la loro somma. Quando abbiamo provato ad usare le parentesi Python ci ha risposto che un int non è callable e ci ha dato un errore. Ora continuiamo così:


>>> a = lambda x : x ** 2    # ora a e' callable
>>> a + b                    # operatore +: errore

Traceback (most recent call last):
  File "<pyshell#19>", line 1, in 
    a + b
TypeError: unsupported operand type(s) for +: 'function' and 'int'

>>> a(b)                     # operatore (): OK

9

Ora invece a contiene un callable (una lambda): l'operatore + provoca un errore mentre le () restituisono il risultato della chiamata.

Queste considerazioni saranno molto utili per affrontare il prossimo paragrafo.

QUANDO USARE LE lambda

Dopo aver imparato ad usare le lambda potrebbe facilmente venirvi in mente una domanda: ma a cosa servono? La risposta non è semplicissima: praticamente tutto quello che si può fare con una lambda potrebbe essere fatto anche con una normale funzione, e quindi, tranne qualche semplice caso in cui possiamo abbreviare un po' il nostro codice con qualche funzione definita "al volo", non si vedono grandi applicazioni.

In effetti le funzioni anonime come le lambda mostrano la loro utilità soprattutto in situazioni abbastanza complicate e sofisticate, che probabilmente non sono alla portata di un principiante. Il loro uso classico è quello di implementare le cosiddette high order functions, cioè funzioni che prendono come parametri (oppure restituiscono) altre funzioni. Per capire di che cosa si tratta facciamo subito un esempio, questa volta usando l'editor:

NUOVO PROGRAMMA: fai_qualcosa.py

import math

def fai_qualcosa(f, x):
    return f(x)

print(fai_qualcosa(math.sqrt, 4))

Osserviamo che nella riga #4 "chiamiamo" il primo parametro f dandogli come argomento il secondo. Questo è perfettamente lecito, perchè come ho detto sopra per Python le parentesi tonde sono un operatore come un altro, che può essere applicato a due variabili. Ma naturalmente la nostra fai_qualcosa dovrà essere chiamata con un callable come primo argomento: osservate la chiamata nella #6: il primo argomento è il nome della funzione math.sqrt senza parentesi. Se lanciamo il programma esso stamperà 2 (la radice quadrata di 4).

Se ora volessimo passare alla funzione fai_qualcosa una funzione definita da noi, le lambda diventerebbero utilissime: modificate il programma così:


def fai_qualcosa(f, x):
    return f(x)		 		 
		 
print(fai_qualcosa(lambda x : 2 * x, 8))
print(fai_qualcosa(lambda x : x + 1, 8)

In pratica potremmo dire noi stessi alla funzione fai_qualcosa "cosa deve fare" con il secondo argomento, passandole una funzione come primo argomento. L'alternativa sarebbe quella di scrivere un gran numero di funzioni diverse e di scegliere quale chiamare concatenando un gran numero di if; casi come questo sono abbastanza frequenti nella programmazione di interfacce grafiche, nelle quali l'utente può scegliere tra molte azioni diverse ed il programma deve interpretare ed eseguire la sua scelta.

Infine vorrei farvi notare che alcune high order functions sono già predefinite in Python. I casi più semplici sono quelli delle funzioni max, min, sort, che hanno tutte un parametro opzionale key che ci permette di definire la funzione che compara due elementi. Immaginiamo di avere la seguente lista che contiene i dati di alcune persone sotto forma di dizionario:


persone = [
	{"Nome":"Luigi", "Cognome":"Bianchi", "Eta":35},
	{"Nome":"Pietro", "Cognome":"Verdi", "Eta":38},
	{"Nome":"Laura", "Cognome":"Rossi", "Eta":32},
	{"Nome":"Giuseppe", "Cognome":"Neri", "Eta":41}
	]

Se cerchiamo di ordinarla otteniamo un errore, perchè Python non permette di confrontare tra loro due dizionari.


>>> persone.sort()

Traceback (most recent call last):
  File "<pyshell#1>:", line 1, in 
    persone.sort()
TypeError: '<' not supported between instances of 'dict' and 'dict'

Ma possiamo dire alla sort di confrontare tra loro i cognomi delle persone usando il parametro opzionale key (una funzione che restituisca il valore che dobbiamo confrontare):


>>> persone.sort(key=lambda x : x["Cognome"])
>>> persone

[{'Nome': 'Luigi', 'Cognome': 'Bianchi', 'Eta': 35},
{'Nome': 'Giuseppe', 'Cognome': 'Neri', 'Eta': 41},
{'Nome': 'Laura', 'Cognome': 'Rossi', 'Eta': 32},
{'Nome': 'Pietro', 'Cognome': 'Verdi', 'Eta': 38}]

Ecco un'altra applicazione del parametro key:


>>> nomi = ["Andrea", "Ada", "Massimo", "Luigi", "Valentina"]
>>> nomi.sort(key=len)
>>> nomi
['Ada', 'Luigi', 'Andrea', 'Massimo', 'Valentina']

Questa volta abbiamo ordinato una lista di stringhe per lunghezza. Non abbiamo nemmeno avuto bisogno di una lambda perchè avevamo già bell'e pronta la built-in len.

ESERCIZIO 4.4 Scrivete due istruzioni che restituiscano la persona più giovane e la più vecchia nella lista persone (anche le funzioni max e min hanno il parametro opzionale key).

Fine della lezione