Le funzioni sono uno strumento che ci permette di raggruppare un insieme di istruzioni che eseguono un compito specifico. Le funzioni accettano in input 0 o più argomenti (o parametri), li elaborano, e restituiscono in output un risultato.
Una volta definita una funzione, è possibile eseguirla (operazione cui ci si riferisce spesso con la locuzione chiamata di funzione), passando argomenti diversi a seconda della situazione. Questo ci permette di rendere il codice più ordinato ed evitare ripetizioni.
Definire funzioni
Come abbiamo già accennato in alcuni esempi nelle lezioni precedenti, la sintassi per definire funzioni è molto semplice. Ad esempio possiamo definire una funzione che ritorna True
se un numero è pari o False
se è dispari:
def is_even(n):
if n%2 == 0:
return True
else:
return False
Possiamo notare che:
- la funzione è introdotta dalla parola chiave
def
; - dopo il
def
appare il nome della funzione, in questo casois_even
; - dopo il nome della funzione viene specificata tra parentesi tonde la lista dei parametri accettati dalla funzione;
- dopo la lista dei parametri ci sono i due punti (
:
) che introducono un blocco di codice indentato; - il blocco di codice può contenere diverse istruzioni e 0 o più
return
.
Quando chiamiamo questa funzione possiamo passare un numero qualsiasi che verrà assegnato a n
. Il corpo della funzione viene poi eseguito e, a seconda del valore di n
, la funzione ritorna True
se il numero è pari o False
se è dispari:
>>> def is_even(n):
... # se il resto di n/2 è 0, n è pari
... if n%2 == 0:
... return True
... else:
... return False
...
>>> is_even(4)
True
>>> is_even(5)
False
>>> is_even(-7)
False
È anche possibile documentare una funzione usando una docstring, cioè una stringa (in genere racchiusa tra """..."""
) che si trova come prima istruzione all'interno di una funzione:
def is_even(n):
"""Return True if n is even, False otherwise."""
if n%2 == 0:
return True
else:
return False
La funzione builtin help()
è in grado di estrarre e mostrare questa stringa automaticamente:
>>> help(is_even)
Help on function is_even in module __main__:
is_even(n)
Return True if n is even, False otherwise.
Passaggio di argomenti
Prima di vedere più in dettaglio come definire una funzione, è utile approfondire il passaggio di argomenti. Quando una funzione viene chiamata, è possibile passare 0 o più argomenti. Questi argomenti possono essere passati per posizione o per nome:
>>> def calc_rect_area(width, height):
... """Return the area of the rectangle."""
... return width * height
...
>>> calc_rect_area(3, 5)
15
>>> calc_rect_area(width=3, height=5)
15
>>> calc_rect_area(height=5, width=3)
15
>>> calc_rect_area(3, height=5)
15
Nella prima chiamata entrambi gli argomenti vengono passati per posizione, quindi il primo valore (3
) viene assegnato al primo parametro della funzione (cioè width
) e il secondo valore (5
) viene assegnato al secondo parametro (cioè height
).
Nella seconda e terza chiamata gli argomenti vengono passati per nome, usando width=3
e height=5
, ottenendo il medesimo risultato. Quando gli argomenti vengono passati per nome, l'ordine non è importante.
Nella quarta e ultima chiamata, possiamo vedere che è anche possibile passare alcuni argomenti per posizione e altri per nome, a patto che gli argomenti passati per posizione precedano quelli passati per nome.
Esistono infine altre due forme per passare argomenti:
>>> size = (3, 5)
>>> calc_rect_area(*size)
15
>>> size = {'width': 3, 'height': 5}
>>> calc_rect_area(**size)
15
Nel primo esempio gli argomenti sono contenuti in una sequenza (una tupla in questo caso). Ponendo una *
di fronte all'argomento durante la chiamata, ogni elemento della sequenza viene passato separatamente e associato al parametro corrispondente della funzione. Il funzionamento del secondo esempio è simile, ma invece di una sequenza abbiamo un dizionario, che richiede due **
di fronte all'argomento durante la chiamata per poter associare ogni elemento al parametro corrispondente. In entrambi i casi il numero (e il nome) dei valori passati alla funzione deve corrispondere al numero di parametri accettati dalla funzione.
Definizione di parametri nelle funzioni
Ora che conosciamo i diversi modi per passare argomenti, vediamo diversi esempi che mostrano come definire i parametri di una funzione.
>>> def say_hello():
... print('Hello World!')
...
>>> say_hello()
Hello World!
In questo primo esempio abbiamo definito una funzione con 0 parametri, che quindi non è in grado di accettare nessun argomento.
>>> def say_hello(name):
... print('Hello {}!'.format(name))
...
>>> say_hello()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: say_hello() missing 1 required positional argument: 'name'
>>> say_hello('Python')
Hello Python!
>>> say_hello(name='Python')
Hello Python!
In questo esempio abbiamo aggiunto un parametro name
alla funzione, che viene usato nel messaggio stampato dalla funzione print()
. Se proviamo a chiamare la funzione con 0 argomenti o con più di un argomento, riceviamo un TypeError
, perché il numero di argomenti passati deve corrispondere a quello dei parametri definiti. Possiamo vedere come sia possibile chiamare la funzione passando un singolo argomento, sia per posizione che per nome.
>>> def say_hello(name='World'):
... print('Hello {}!'.format(name))
...
>>> say_hello()
Hello World!
>>> say_hello('Python')
Hello Python!
>>> say_hello(name='Python')
Hello Python!
In questo esempio abbiamo invece aggiunto un valore di default per il name
, usando name='World'
. Questo rende l'argomento corrispondente a name
opzionale: se non viene passato, il valore di name
sarà 'World'
, altrimenti sarà il valore passato come argomento.
>>> def greet(greeting, *, name):
... print('{} {}!'.format(greeting, name))
...
>>> greet('Hello', 'Python')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: greet() takes 1 positional argument but 2 were given
>>> greet('Hello', name='Python')
Hello Python!
>>> greet(greeting='Hello', name='Python')
Hello Python!
Se vogliamo fare in modo che una funzioni accetti solo argomenti passati per nome, possiamo usare una singola *
seguita da virgola. Tutti gli argomenti che appaiono dopo la *
dovranno essere passati per nome.
>>> def say_hello(*names):
... print('Hello {}!'.format(', '.join(names)))
...
>>> say_hello('Python')
Hello Python!
>>> say_hello('Python', 'PyPy', 'Jython', 'IronPython')
Hello Python, PyPy, Jython, IronPython!
La *
immediatamente prima del nome di un parametro (ad esempio *names
) ha invece un significato diverso: permette alla funzione di accettare un numero variabile di argomenti posizionali. In seguito alla chiamata, la variabile names
si riferisce a una tupla che contiene tutti gli argomenti. In questo esempio potete vedere che la funzione può essere chiamata sia con 1 che con 4 argomenti posizionali.
>>> def make_tag(element, **attrs):
... attrs = ' '.join(['{}="{}"'.format(k, v) for k, v in attrs.items()])
... return '<{} {}>'.format(element, attrs)
...
>>> make_tag('div', id='header')
'<div id="header">'
>>> make_tag('a', href='https://www.python.org/', title='Visit Python.org')
'<a href="https://www.python.org/" title="Visit Python.org">'
>>> make_tag('img', src='logo.png', alt='Python logo')
'<img src="logo.png" alt="Python logo">'
È anche possibile definire una funzione che accetta un numero variabile di argomenti passati per nome (anche noti come keyword argument): basta aggiungere **
immediatamente prima del nome di un parametro (ad esempio **attrs
). In questo esempio la funzione accetta un argomento element
seguito da un numero variabile di argomenti passati per nome, che vengono salvati in un dizionario a cui fa riferimento la variabile attrs
. In questo esempio la funzione crea una stringa combinando i nomi degli attributi e i loro valori e la usa in combinazione con il nome dell'elemento per creare tag HTML.
Ritorno di valori
La parola chiave return
viene usata per restituire un valore al chiamante, che può assegnarlo a una variabile o utilizzarlo per altre operazioni:
>>> def square(n):
... return n**2
...
>>> x = square(5)
>>> x
25
>>> square(square(5))
625
>>> square(3) + square(4) == square(5)
True
Una funzione può contenere 0 o più return
, e una volta che un return
viene eseguito, la funzione termina immediatamente. Questo vuol dire che solo uno dei return
viene eseguito ad ogni chiamata:
>>> def abs(n):
... if n < 0:
... return -n # eseguito se n è negativo
... return n # eseguito se n è positivo
...
>>> abs(-5)
5
>>> abs(5)
5
return
è in genere seguito dal valore di ritorno, ma è anche possibile omettere il valore e usare return
per terminare la funzione: in questo caso None
viene ritornato automaticamente. Se si raggiunge il termine della funzione senza incontrare neanche un return
, None
viene restituito automaticamente:
>>> def print_twice(text):
... if not text:
... # termina immediatamente se text è una stringa vuota
... return
... print(text)
... print(text)
... # ritorna None automaticamente al termine della funzione
...
>>> # stampa 2 volte e ritorna None al termine della funzione
>>> res = print_twice('Python')
Python
Python
>>> print(res)
None
>>> # entra nell'if e ritorna None prima di stampare
>>> res = print_twice('')
>>> print(res)
None
Nel caso sia necessario ritornare più valori, è possibile fare:
>>> def midpoint(x1, y1, x2, y2):
... """Return the midpoint between (x1; y1) and (x2; y2)."""
... xm = (x1 + x2) / 2
... ym = (y1 + y2) / 2
... return xm, ym
...
>>> x, y = midpoint(2, 4, 8, 12)
>>> x
5.0
>>> y
8.0
In questo caso il valore ritornato è sempre uno: una singola tupla di 2 elementi. Python supporta un'operazione chiamata unpacking, che ci permette di assegnare contemporaneamente diversi valori a più variabili, permettendo quindi operazioni come la seguente:
x, y = midpoint(2, 4, 8, 12)
In tal modo, possiamo assegnare il primo valore della tupla a x
e il secondo a y
.
Scope delle variabili
Tutti i parametri e le variabili create all'interno di una funzione, sono locali alla funzione, cioè possono essere usate solo da codice che si trova all'interno della funzione. Se proviamo ad accedere a queste variabili dall'esterno della funzione otteniamo un NameError
:
>>> def calc_circle_area(r):
... pi = 3.14
... return pi * r**2
...
>>> calc_circle_area(5)
78.5
>>> r
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'r' is not defined
>>> pi
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'pi' is not defined
Le funzioni possono però accedere in lettura a valori globali, cioè definiti fuori dalla funzione:
>>> pi = 3.14
>>> def calc_circle_area(r):
... return pi * r**2
...
>>> calc_circle_area(5)
78.5
Python segue una semplice regola di risoluzione dei nomi:
- prima verifica se il nome esiste nel namespace locale;
- se non esiste lo cerca nel namespace globale;
- se non esiste neanche nel namespace globale, lo cerca tra gli oggetti builtin.
Se un nome non è presente neanche tra gli oggetti builtin, Python restituisce un NameError
.
Ad esempio:
>>> pi = 3.14
>>> def print_circle_area(r):
... print(pi * r**2)
...
>>> print_circle_area(5)
78.5
La riga print(pi * r**2)
contiene 3 nomi:
r
è una variabile locale alla funzione: Python la trova subito nel namespace locale;pi
è una variabile globale definita fuori dalla funzione: Python non la trova nel namespace locale ma la trova in quello globale;print
è una funzione builtin: Python non la trova nel namespace locale né in quello globale, ma la trova tra gli oggetti builtin.
È anche importante notare che variabili diverse in namespace diversi possono riferirsi allo stesso oggetto, ad esempio:
>>> lista = [1, 2, 3, 4, 5]
>>> def add_elem(seq, elem):
... seq.append(elem)
...
>>> lista
[1, 2, 3, 4, 5]
>>> add_elem(lista, 6)
>>> lista
[1, 2, 3, 4, 5, 6]
Sia la variabile globale lista
che la variabile locale seq
fanno riferimento alla stessa lista. Questo vuol dire che le modifiche fatte alla lista dalla funzione add_elem()
saranno visibili anche all'esterno della funzione. Questo ovviamente può accadere solo quando vengono passati a una funzione oggetti mutabili e quando la funzione modifica l'oggetto che è stato passato.