Nei capitoli precedenti abbiamo imparato a:
Ora dobbiamo affrontare un problema fondamentale.
Immagina di avere un programma di diverse centinaia di righe e di accorgerti che lo stesso procedimento compare in cinque punti diversi.
Potremmo copiare il codice cinque volte.
Ma cosa succede se dobbiamo modificarlo?
Dovremmo ricordarci di modificare tutte le copie.
Le funzioni risolvono proprio questo tipo di problema.
Una funzione permette di:
Alla fine del capitolo saprai utilizzare:
def;return;* e **;global e nonlocal;map(), filter() e sorted();functools;Consideriamo questo programma:
price = 10
quantity = 3
total = price * quantity
print(f"Totale: {total:.2f} €")
Supponiamo di dover eseguire lo stesso calcolo per dieci prodotti.
Potremmo ripetere:
price1 = 10
quantity1 = 3
total1 = price1 * quantity1
price2 = 5
quantity2 = 4
total2 = price2 * quantity2
Ma stiamo duplicando la logica.
Una funzione ci permette di descrivere il comportamento una sola volta:
def calculate_total(price, quantity):
return price * quantity
Poi possiamo utilizzarla:
total1 = calculate_total(10, 3)
total2 = calculate_total(5, 4)
La funzione diventa un piccolo componente riutilizzabile.
Possiamo rappresentare il concetto così:
dati
↓
┌───────────────┐
│ funzione │
└───────────────┘
↓
risultato
defLa sintassi fondamentale è:
def nome_funzione():
istruzioni
Esempio:
def say_hello():
print("Hello, Python!")
Attenzione: definire una funzione non significa eseguirla.
La funzione viene eseguita quando la chiamiamo:
say_hello()
Possiamo chiamarla più volte:
say_hello()
say_hello()
say_hello()
Output:
Hello, Python!
Hello, Python!
Hello, Python!
Come per if, for e while, l'indentazione definisce il corpo della
funzione:
def say_hello():
print("Hello")
print("Welcome")
Entrambe le istruzioni appartengono alla funzione.
Questa invece:
def say_hello():
print("Hello")
print("Outside")
contiene una sola istruzione nella funzione.
L'indentazione non è quindi decorativa: definisce la struttura del programma.
Definisci una funzione:
show_message()
che visualizzi:
Benvenuto nel corso di Python!
Chiama la funzione tre volte.
Definisci:
show_separator()
che visualizzi una riga di =.
Una funzione può ricevere dati.
def greet(name):
print(f"Ciao {name}!")
name è un parametro.
Quando chiamiamo:
greet("Anna")
"Anna" è un argomento.
Possiamo utilizzare valori diversi:
greet("Anna")
greet("Luca")
greet("Marco")
Una distinzione terminologica utile:
parametro → variabile definita nella funzione
argomento → valore passato durante la chiamata
Una funzione può avere più parametri:
def introduce(name, age):
print(f"{name} ha {age} anni")
Chiamata:
introduce("Anna", 25)
Possiamo anche avere tre o più parametri:
def create_product(name, price, quantity):
print(f"{name}: {price:.2f} € × {quantity}")
Nella chiamata:
introduce("Anna", 25)
i valori vengono associati ai parametri in base alla posizione:
name ← "Anna"
age ← 25
Per questo:
introduce(25, "Anna")
non produce lo stesso significato.
La posizione degli argomenti conta.
Possiamo specificare esplicitamente il nome del parametro:
introduce(name="Anna", age=25)
In questo caso l'ordine può essere cambiato:
introduce(age=25, name="Anna")
Questo rende le chiamate più leggibili, soprattutto quando una funzione possiede molti parametri.
Possiamo anche mescolare posizionali e keyword, ma gli argomenti posizionali devono precedere quelli keyword:
introduce("Anna", age=25)
è valido.
Questo invece non è valido:
introduce(name="Anna", 25)
Definisci:
def add(a, b):
...
che calcoli la somma.
Crea:
def show_product(name, price, quantity):
...
e chiamala utilizzando keyword arguments.
Scrivi una funzione che riceva:
nome
città
e produca una frase descrittiva.
return: restituire un risultatoUna funzione può restituire un valore utilizzando return.
def add(a, b):
return a + b
Ora:
result = add(3, 5)
print(result)
produce:
8
La funzione non deve necessariamente stampare il risultato.
È spesso preferibile che una funzione calcoli e restituisca, lasciando al chiamante la decisione su cosa fare del risultato.
Confrontiamo:
def add(a, b):
print(a + b)
con:
def add(a, b):
return a + b
La seconda è più riutilizzabile:
result = add(3, 5)
double = add(3, 5) * 2
text = f"Risultato: {add(3, 5)}"
return interrompe la funzioneQuando Python incontra return, la funzione termina immediatamente.
def test():
print("A")
return
print("B")
Chiamando:
test()
otteniamo:
A
B non viene eseguito.
Una funzione può restituire più valori:
def statistics(numbers):
minimum = min(numbers)
maximum = max(numbers)
average = sum(numbers) / len(numbers)
return minimum, maximum, average
Possiamo utilizzare tuple unpacking:
minimum, maximum, average = statistics([10, 20, 30])
In realtà la funzione restituisce una tupla:
(minimum, maximum, average)
returnSe una funzione non contiene return, oppure raggiunge la fine senza
eseguire un return, il valore restituito è:
None
Esempio:
def hello():
print("Hello")
result = hello()
print(result)
Output:
Hello
None
None rappresenta l'assenza di un valore.
returnScrivi:
def square(number):
...
che restituisca il quadrato.
Scrivi una funzione che riceva prezzo e quantità e restituisca il totale.
Scrivi una funzione che restituisca minimo, massimo e media di una lista di numeri.
Possiamo assegnare un valore predefinito a un parametro:
def greet(name, greeting="Ciao"):
return f"{greeting}, {name}!"
Possiamo chiamarla:
print(greet("Anna"))
ottenendo:
Ciao, Anna!
Oppure:
print(greet("Anna", "Buongiorno"))
ottenendo:
Buongiorno, Anna!
I parametri con valore predefinito devono seguire i parametri obbligatori:
def greet(name, greeting="Ciao"):
...
è valido.
Questo:
def greet(greeting="Ciao", name):
...
non è valido.
Questo è un errore concettuale molto importante.
Evitiamo:
def add_item(item, items=[]):
items.append(item)
return items
Il valore predefinito [] viene creato una sola volta, quando la
funzione viene definita.
Quindi le chiamate successive condividono quella stessa lista.
È preferibile:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
Ora ogni chiamata che non fornisce items crea una nuova lista.
Regola pratica: per parametri mutabili come liste, dizionari e set, usare spesso
Nonecome valore predefinito e creare l'oggetto dentro la funzione.
Crea:
def power(number, exponent=2):
...
Crea una funzione:
def connect(host="localhost", port=8080):
...
che restituisca una stringa di connessione.
Spiega perché è rischioso usare:
items=[]
come default.
*args: un numero variabile di argomentiPossiamo creare funzioni che ricevono un numero variabile di argomenti posizionali:
def total(*numbers):
return sum(numbers)
Possiamo chiamare:
print(total(1, 2))
print(total(1, 2, 3, 4))
print(total())
Dentro la funzione:
numbers
è una tupla.
Quindi:
total(1, 2, 3)
equivale concettualmente a:
numbers = (1, 2, 3)
**kwargs: argomenti keyword variabili**kwargs raccoglie un numero variabile di argomenti nominati in un
dizionario.
def show_info(**info):
for key, value in info.items():
print(f"{key}: {value}")
Chiamata:
show_info(
name="Anna",
age=25,
city="Torino"
)
Dentro la funzione:
info
è un dizionario.
*args e **kwargsPossiamo scrivere:
def function(required, *args, option=True, **kwargs):
...
L'ordine generale dei parametri è regolato dalla sintassi delle funzioni Python.
Per esempio:
def example(a, b=10, *args, option=True, **kwargs):
...
Qui abbiamo:
a;b;args;option;kwargs.Non è necessario usare sempre tutti questi strumenti. Una funzione con troppi meccanismi può diventare difficile da capire.
* nella chiamataSe abbiamo:
numbers = [1, 2, 3]
e:
def add(a, b, c):
return a + b + c
possiamo scrivere:
result = add(*numbers)
Python espande la lista:
add(1, 2, 3)
Lo stesso vale per tuple e altri iterabili compatibili.
** nella chiamataDato:
def introduce(name, age):
return f"{name} ha {age} anni"
e:
person = {
"name": "Anna",
"age": 25
}
possiamo scrivere:
print(introduce(**person))
Il dizionario viene espanso in:
introduce(name="Anna", age=25)
Le chiavi devono corrispondere ai parametri della funzione.
Scrivi una funzione:
def multiply_all(*numbers):
...
che moltiplichi tutti i valori.
Scrivi:
def print_profile(**profile):
...
che visualizzi tutte le informazioni.
Usa * per passare una lista a una funzione che richiede tre parametri.
Usa ** per passare un dizionario a una funzione.
Python permette di dichiarare parametri che possono essere passati solo per posizione.
def divide(a, b, /):
return a / b
Questa chiamata è valida:
divide(10, 2)
Questa non è valida:
divide(a=10, b=2)
I parametri prima di / sono positional-only.
Questo può essere utile quando vogliamo controllare esplicitamente l'API di una funzione.
Possiamo invece richiedere che alcuni parametri siano passati solo per nome.
def create_user(name, *, admin=False):
...
Questa chiamata è valida:
create_user("Anna", admin=True)
Questa no:
create_user("Anna", True)
I parametri dopo * sono keyword-only.
Questa tecnica rende le chiamate più esplicite.
Possiamo combinare le due tecniche:
def example(a, b, /, c, *, option=False):
...
Qui:
a e b sono positional-only;c può essere posizionale o keyword;option è keyword-only.La firma di una funzione può quindi comunicare molto precisamente come deve essere utilizzata.
Consideriamo:
def calculate():
total = 100
print(total)
calculate()
La variabile:
total
è locale alla funzione.
Non possiamo normalmente utilizzarla fuori:
calculate()
print(total)
Questo produce NameError.
La variabile esiste nello scope locale della funzione.
Possiamo avere:
total = 100
def show_total():
print(total)
La funzione può leggere la variabile globale.
Ma se assegniamo:
total = 100
def change_total():
total = 200
la funzione crea una nuova variabile locale total.
La variabile globale rimane:
100
Questo è un punto fondamentale:
Un'assegnazione dentro una funzione crea normalmente una variabile locale.
globalSe abbiamo davvero bisogno di modificare una variabile globale, possiamo
usare global:
counter = 0
def increment():
global counter
counter += 1
Dopo:
increment()
print(counter)
otteniamo:
1
Tuttavia, l'uso frequente di global è generalmente un segnale che il
programma potrebbe essere progettato meglio.
È spesso preferibile passare dati come parametri e restituire risultati.
Meglio:
def increment(counter):
return counter + 1
counter = increment(counter)
che affidarsi allo stato globale.
nonlocalnonlocal viene utilizzato nelle funzioni annidate quando vogliamo
modificare una variabile appartenente allo scope della funzione esterna.
Esempio:
def counter():
value = 0
def increment():
nonlocal value
value += 1
return value
return increment
Possiamo creare:
next_number = counter()
print(next_number())
print(next_number())
print(next_number())
Output:
1
2
3
Abbiamo creato una closure: la funzione interna conserva l'accesso
alla variabile value della funzione esterna.
Python cerca i nomi seguendo una regola chiamata LEGB:
L → Local
E → Enclosing
G → Global
B → Built-in
In pratica:
Esempio:
name = "global"
def outer():
name = "enclosing"
def inner():
name = "local"
print(name)
inner()
outer()
Viene stampato:
local
perché il nome viene trovato immediatamente nello scope locale di
inner().
Spiega perché questo programma stampa 10:
x = 10
def test():
x = 20
print(x)
test()
print(x)
Riscrivi un programma che utilizzi una funzione con parametro invece di una variabile globale.
Crea una closure che mantenga un contatore.
Una funzione dovrebbe spiegare chiaramente cosa fa.
Python permette di inserire una docstring come prima istruzione della funzione:
def square(number):
"""Restituisce il quadrato di number."""
return number ** 2
Possiamo leggere la docstring:
print(square.__doc__)
oppure utilizzare:
help(square)
Una docstring è particolarmente importante nelle funzioni che fanno parte di un progetto o di una libreria.
Per funzioni più importanti possiamo documentare parametri e risultato:
def calculate_total(price, quantity):
"""
Calcola il costo totale di un prodotto.
Args:
price: prezzo unitario.
quantity: quantità acquistata.
Returns:
Il costo totale.
"""
return price * quantity
La sintassi esatta della documentazione può seguire convenzioni diverse. L'importante è mantenere coerenza nel progetto.
Possiamo documentare i tipi attesi:
def add(a: int, b: int) -> int:
return a + b
Possiamo usare type hints anche per strutture più complesse:
def average(numbers: list[float]) -> float:
return sum(numbers) / len(numbers)
Con Python moderno, list[int] e sintassi simili sono preferibili alle
vecchie forme del modulo typing quando compatibili con la versione
utilizzata.
Ricordiamo:
I type hints descrivono le intenzioni del programmatore, ma Python non li applica automaticamente come vincoli runtime.
I type hints possono essere utilizzati da:
Per esempio, una funzione:
def greet(name: str) -> str:
return f"Ciao {name}"
comunica chiaramente:
input → str
output → str
Questo diventa molto utile nei progetti grandi.
Aggiungi una docstring a:
def multiply(a, b):
return a * b
Aggiungi type hints.
Scrivi una funzione calculate_discount() completa di docstring e type
hints.
In Python le funzioni sono first-class objects.
Significa che possiamo:
Esempio:
def square(x):
return x ** 2
operation = square
print(operation(5))
operation ora fa riferimento alla stessa funzione.
Possiamo scrivere:
def apply_operation(operation, value):
return operation(value)
Poi:
def square(x):
return x ** 2
print(apply_operation(square, 5))
Risultato:
25
La funzione apply_operation() riceve un comportamento come dato.
Queste sono chiamate funzioni di ordine superiore.
Una funzione è di ordine superiore quando:
Esempio:
def repeat(operation, value, times):
result = value
for _ in range(times):
result = operation(result)
return result
Possiamo usare:
def double(x):
return x * 2
print(repeat(double, 1, 4))
Risultato:
16
La funzione è diventata un piccolo meccanismo riutilizzabile.
Una lambda permette di creare una piccola funzione senza darle necessariamente un nome.
square = lambda x: x ** 2
Equivale concettualmente a:
def square(x):
return x ** 2
Le lambda sono particolarmente utili quando passiamo una funzione a un'altra funzione.
Esempio:
numbers = [1, 2, 3, 4]
squares = list(map(lambda x: x ** 2, numbers))
sorted() e keyAbbiamo già incontrato key nel capitolo sulle strutture dati.
Esempio:
names = ["Anna", "Luca", "Alessandro", "Mia"]
ordered = sorted(names, key=lambda name: len(name))
La funzione passata a key stabilisce il valore utilizzato per
l'ordinamento.
Possiamo utilizzare una funzione normale:
def by_length(name):
return len(name)
ordered = sorted(names, key=by_length)
map()map() applica una funzione a ogni elemento:
numbers = [1, 2, 3, 4]
doubled = map(lambda x: x * 2, numbers)
print(list(doubled))
Output:
[2, 4, 6, 8]
map() restituisce un iteratore.
filter()filter() conserva gli elementi per i quali la funzione restituisce un
valore truthy:
numbers = [1, 2, 3, 4, 5, 6]
even = filter(lambda x: x % 2 == 0, numbers)
print(list(even))
Risultato:
[2, 4, 6]
map()/filter()?Spesso possiamo scegliere tra più stili.
Con map():
squares = list(map(lambda x: x ** 2, numbers))
Con comprehension:
squares = [x ** 2 for x in numbers]
La seconda forma è spesso più leggibile in Python.
Analogamente:
even = list(filter(lambda x: x % 2 == 0, numbers))
può diventare:
even = [x for x in numbers if x % 2 == 0]
La scelta dipende dal contesto e dalla chiarezza.
Crea una funzione double() e passala a una funzione apply().
Ordina una lista di parole per lunghezza utilizzando una lambda.
Trasforma una lista di numeri con map().
Filtra i numeri pari con filter().
Riscrivi gli esercizi 25 e 26 usando comprehension.
Una closure si verifica quando una funzione interna conserva un riferimento a variabili dello scope esterno anche dopo che la funzione esterna ha terminato la propria esecuzione.
Esempio:
def make_multiplier(factor):
def multiply(value):
return value * factor
return multiply
Creiamo:
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10))
print(triple(10))
Output:
20
30
La funzione multiply() conserva il valore di factor.
Una closure permette di creare una funzione configurata.
double = make_multiplier(2)
significa:
crea una funzione che moltiplica per 2.
Mentre:
triple = make_multiplier(3)
crea:
una funzione che moltiplica per 3.
Le closure sono alla base di alcuni pattern funzionali e sono importanti per comprendere i decoratori.
Un decoratore è una funzione che riceve una funzione e ne restituisce una nuova con comportamento aggiuntivo.
Esempio:
def log_call(function):
def wrapper():
print("Funzione in esecuzione")
result = function()
print("Funzione terminata")
return result
return wrapper
Possiamo applicarlo manualmente:
def hello():
print("Hello")
hello = log_call(hello)
hello()
Output:
Funzione in esecuzione
Hello
Funzione terminata
La sintassi più elegante utilizza @:
@log_call
def hello():
print("Hello")
Ora:
hello()
ha lo stesso comportamento.
functools.wrapsUn decoratore reale dovrebbe generalmente utilizzare functools.wraps:
from functools import wraps
def log_call(function):
@wraps(function)
def wrapper():
print("Funzione in esecuzione")
result = function()
print("Funzione terminata")
return result
return wrapper
wraps() aiuta a preservare metadati della funzione originale, come
nome e docstring.
Un decoratore generico deve poter funzionare con funzioni aventi argomenti diversi.
Possiamo usare *args e **kwargs:
from functools import wraps
def log_call(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"Chiamata a {function.__name__}")
result = function(*args, **kwargs)
print("Chiamata terminata")
return result
return wrapper
Ora:
@log_call
def add(a, b):
return a + b
print(add(3, 5))
Il decoratore funziona anche con una funzione che riceve parametri.
I decoratori saranno approfonditi quando avremo studiato moduli, classi ed eccezioni.
Una funzione ricorsiva è una funzione che richiama se stessa.
Esempio classico: fattoriale.
def factorial(n):
if n <= 1:
return 1
return n * factorial(n - 1)
Calcoliamo:
print(factorial(5))
La sequenza concettuale è:
factorial(5)
5 × factorial(4)
5 × 4 × factorial(3)
5 × 4 × 3 × factorial(2)
5 × 4 × 3 × 2 × factorial(1)
e factorial(1) restituisce 1.
Ogni ricorsione ben progettata deve avere almeno:
Nel nostro esempio:
if n <= 1:
return 1
è il caso base.
Mentre:
return n * factorial(n - 1)
è il passo ricorsivo.
Senza caso base avremmo una ricorsione infinita.
Ogni chiamata di funzione utilizza una porzione dello stack di esecuzione.
Una ricorsione troppo profonda può quindi produrre:
RecursionError
Python non è progettato per gestire ricorsioni enormemente profonde.
Per molti problemi che possono essere risolti facilmente con un ciclo, un approccio iterativo è preferibile.
Possiamo scrivere:
def recursive_sum(numbers):
if not numbers:
return 0
return numbers[0] + recursive_sum(numbers[1:])
Funziona, ma non è necessariamente una buona soluzione pratica.
Per una lista, il semplice:
sum(numbers)
è migliore.
Questo esempio serve a comprendere la ricorsione, non a suggerire di sostituire le funzioni built-in con codice ricorsivo.
Implementa ricorsivamente il fattoriale.
Implementa una funzione ricorsiva che calcoli la somma dei numeri da 1
a n.
Individua il caso base e il passo ricorsivo della funzione:
def countdown(n):
if n == 0:
return
print(n)
countdown(n - 1)
Una funzione è detta pura quando, in termini pratici:
Esempio:
def add(a, b):
return a + b
La funzione è pura.
Un esempio meno puro:
total = 0
def add_to_total(value):
global total
total += value
La funzione modifica uno stato esterno.
Le funzioni pure sono generalmente più facili da:
Non significa che un programma debba contenere esclusivamente funzioni pure. I programmi reali devono anche interagire con file, rete, database, utenti e sistemi esterni.
Consideriamo:
def process_order():
# legge input
# valida dati
# calcola prezzo
# salva database
# invia email
# stampa ricevuta
...
La funzione è probabilmente troppo grande.
Potremmo separare:
read_order()
validate_order()
calculate_total()
save_order()
send_confirmation()
print_receipt()
Ogni funzione ha una responsabilità più precisa.
Questa idea viene spesso espressa come:
Una funzione dovrebbe avere una responsabilità chiara.
Non significa che ogni funzione debba contenere una sola riga. Significa che il suo scopo dovrebbe essere facilmente identificabile.
Possiamo costruire comportamenti complessi componendo funzioni semplici.
def calculate_subtotal(price, quantity):
return price * quantity
def calculate_discount(total, percentage):
return total * percentage / 100
def calculate_final_price(price, quantity, discount):
subtotal = calculate_subtotal(price, quantity)
discount_value = calculate_discount(subtotal, discount)
return subtotal - discount_value
Ora:
final_price = calculate_final_price(100, 2, 10)
print(final_price)
Le singole funzioni sono facili da verificare separatamente.
Le funzioni sono anche un buon posto per centralizzare regole.
def is_valid_price(price):
return price >= 0
Possiamo utilizzare:
if is_valid_price(price):
...
Invece di ripetere:
if price >= 0:
in molti punti.
Questo diventa ancora più importante quando una regola cambia.
NoneQuando una funzione può non trovare un risultato, None è spesso una
scelta naturale:
def find_user(users, name):
for user in users:
if user["name"] == name:
return user
return None
Il chiamante può controllare:
user = find_user(users, "Anna")
if user is not None:
print(user)
Per confrontare con None è preferibile usare:
is None
is not None
piuttosto che == None.
Possiamo vedere una funzione come una piccola interfaccia:
INPUT
↓
┌─────────────────┐
│ funzione │
└─────────────────┘
↓
OUTPUT
Il chiamante non deve necessariamente conoscere come funziona internamente.
Per esempio:
total = calculate_final_price(100, 2, 10)
Chi utilizza la funzione deve sapere:
Non deve necessariamente conoscere tutti i calcoli interni.
Questa separazione tra interfaccia e implementazione sarà fondamentale quando arriveremo alla programmazione a oggetti.
Dividi un programma che:
in almeno tre funzioni.
Scrivi una funzione find_product() che restituisca il prodotto oppure
None.
Spiega perché è utile separare il calcolo dalla visualizzazione.
functoolsIl modulo functools contiene strumenti utili per lavorare con
funzioni.
Uno dei più importanti è:
from functools import reduce
reduce() combina progressivamente gli elementi di un iterabile.
Esempio:
from functools import reduce
numbers = [1, 2, 3, 4]
product = reduce(lambda a, b: a * b, numbers)
print(product)
Risultato:
24
In Python, però, reduce() non dovrebbe essere utilizzato
automaticamente quando esiste una soluzione più leggibile.
Per esempio:
sum(numbers)
è molto più chiaro per una somma.
Per un prodotto, math.prod() è spesso una scelta migliore:
from math import prod
product = prod(numbers)
reduce() è uno strumento importante da conoscere, ma la leggibilità
deve guidare la scelta.
functools.partialpartial() permette di creare una nuova funzione con alcuni argomenti
già impostati.
from functools import partial
def multiply(a, b):
return a * b
double = partial(multiply, 2)
print(double(10))
Risultato:
20
Abbiamo trasformato:
multiply(2, 10)
in:
double(10)
È utile quando vogliamo creare una versione specializzata di una funzione generale.
Una funzione può essere definita all'interno di un'altra:
def outer():
def inner():
print("Inner")
inner()
La funzione interna è disponibile all'interno dello scope della funzione esterna.
Le funzioni nidificate sono utilizzate, tra gli altri casi, per:
Non è necessario usarle sempre.
passQuando dobbiamo definire una funzione ma non abbiamo ancora deciso cosa farà, possiamo usare:
def future_function():
pass
pass non fa nulla.
È utile come segnaposto temporaneo.
È però preferibile non lasciare a lungo funzioni vuote nel codice di produzione.
Una funzione può lasciare propagare un'eccezione:
def divide(a, b):
return a / b
Se:
divide(10, 0)
si verifica ZeroDivisionError.
Non dobbiamo necessariamente gestire l'errore dentro la funzione.
A seconda del progetto, può essere più corretto lasciare che il chiamante decida come gestirlo.
Approfondiremo questo tema nel capitolo dedicato alle eccezioni.
Costruiamo un piccolo modulo logico per un negozio.
def calculate_subtotal(price, quantity):
return price * quantity
def calculate_discount(subtotal, percentage):
return subtotal * percentage / 100
def calculate_total(price, quantity, discount_percentage=0):
subtotal = calculate_subtotal(price, quantity)
discount = calculate_discount(subtotal, discount_percentage)
return subtotal - discount
Utilizzo:
price = 12.50
quantity = 4
total = calculate_total(
price,
quantity,
discount_percentage=10
)
print(f"Totale: {total:.2f} €")
Risultato:
Totale: 45.00 €
Abbiamo separato:
calcolo del subtotale
↓
calcolo dello sconto
↓
calcolo del totale
Ogni funzione ha una responsabilità precisa.
Consideriamo:
students = [
{"name": "Anna", "grades": [7, 8, 6, 9]},
{"name": "Luca", "grades": [5, 6, 6, 7]},
{"name": "Marco", "grades": [9, 8, 10, 9]}
]
Possiamo creare:
def calculate_average(grades):
return sum(grades) / len(grades)
Poi:
def student_average(student):
return calculate_average(student["grades"])
E:
for student in students:
average = student_average(student)
print(f"{student['name']}: {average:.2f}")
Separando le funzioni, possiamo riutilizzare il calcolo della media in molti punti.
Possiamo utilizzare max() con key:
best_student = max(
students,
key=student_average
)
Ora:
print(best_student["name"])
produce:
Marco
Questa soluzione mostra quanto sia potente la combinazione:
funzioni
+
key
+
funzioni built-in
Le funzioni permettono di incapsulare operazioni sulle strutture dati.
Esempio:
def get_low_stock(products, threshold=10):
return [
product
for product in products
if product["stock"] <= threshold
]
Utilizzo:
low_stock = get_low_stock(products)
La funzione nasconde i dettagli dell'algoritmo e offre un'interfaccia semplice.
Scrivi:
def get_total_value(products):
...
che calcoli il valore totale del magazzino.
Scrivi:
def get_categories(products):
...
che restituisca un set delle categorie.
Scrivi:
def get_expensive_products(products, minimum_price):
...
che restituisca i prodotti sopra una determinata soglia.
Definire:
def hello():
print("Hello")
e scrivere:
hello
non esegue la funzione.
Serve:
hello()
print() e returnQuesto:
def add(a, b):
print(a + b)
non restituisce il risultato.
Questo:
def add(a, b):
return a + b
lo restituisce.
def add(a, b):
return a + b
add(5)
produce TypeError.
Evitare:
def add_item(item, items=[]):
...
Una funzione che contiene centinaia di righe è spesso difficile da testare e mantenere.
Le dipendenze globali rendono più difficile capire da dove arrivano i dati e prevedere gli effetti di una funzione.
A cosa serve def?
Qual è la differenza tra parametro e argomento?
A cosa serve return?
Che cosa restituisce una funzione senza return?
Che cos'è un keyword argument?
A cosa servono *args e **kwargs?
Che cosa fa * durante una chiamata?
Che cosa fa ** durante una chiamata?
Che cosa significa scope locale?
A cosa servono global e nonlocal?
Che cos'è una docstring?
Che cosa sono i type hints?
Che cosa significa che una funzione è un first-class object?
Che cos'è una funzione di ordine superiore?
Quando è utile una lambda?
Che cos'è una closure?
Che cos'è un decoratore?
Quali sono i due elementi fondamentali della ricorsione?
Perché i default mutabili possono essere problematici?
Perché è utile dividere un programma in funzioni piccole?
Crea le funzioni:
add()
subtract()
multiply()
divide()
e una funzione:
calculate()
che scelga l'operazione.
Crea funzioni per:
Scrivi:
def get_even(numbers):
...
e:
def get_greater_than(numbers, threshold):
...
Dato:
products = [
{"name": "Milk", "price": 1.50, "stock": 20},
{"name": "Bread", "price": 2.00, "stock": 8},
{"name": "Eggs", "price": 3.20, "stock": 15}
]
crea funzioni per:
È arrivato il momento di mettere insieme tutto ciò che abbiamo studiato.
Costruisci un piccolo sistema di gestione ordini utilizzando funzioni.
products = [
{
"name": "Milk",
"price": 1.50,
"stock": 20
},
{
"name": "Bread",
"price": 2.00,
"stock": 8
},
{
"name": "Eggs",
"price": 3.20,
"stock": 15
},
{
"name": "Apples",
"price": 2.50,
"stock": 30
}
]
Un ordine è rappresentato da:
order = [
{"product": "Milk", "quantity": 3},
{"product": "Eggs", "quantity": 2},
{"product": "Apples", "quantity": 5}
]
Crea almeno queste funzioni:
find_product(products, name)
Restituisce il prodotto oppure None.
calculate_line_total(product, quantity)
Calcola il totale di una riga dell'ordine.
calculate_order_total(products, order)
Calcola il totale dell'ordine.
get_low_stock(products, threshold=10)
Restituisce i prodotti con scorta bassa.
get_order_summary(products, order)
Restituisce una struttura contenente le informazioni dell'ordine.
key;========================================
RIEPILOGO ORDINE
========================================
Milk × 3 4.50 €
Eggs × 2 6.40 €
Apples × 5 12.50 €
Totale ordine: 23.40 €
Prodotti con scorta bassa:
- Bread
========================================
Una possibile soluzione è:
def find_product(products: list[dict], name: str) -> dict | None:
"""Restituisce il prodotto con il nome indicato, oppure None."""
for product in products:
if product["name"].lower() == name.lower():
return product
return None
def calculate_line_total(product: dict, quantity: int) -> float:
"""Calcola il totale di una riga dell'ordine."""
return product["price"] * quantity
def calculate_order_total(
products: list[dict],
order: list[dict]
) -> float:
"""Calcola il totale complessivo dell'ordine."""
total = 0.0
for line in order:
product = find_product(products, line["product"])
if product is None:
continue
total += calculate_line_total(
product,
line["quantity"]
)
return total
def get_low_stock(
products: list[dict],
threshold: int = 10
) -> list[dict]:
"""Restituisce i prodotti con scorta minore o uguale alla soglia."""
return [
product
for product in products
if product["stock"] <= threshold
]
def get_order_summary(
products: list[dict],
order: list[dict]
) -> list[dict]:
"""Costruisce il riepilogo delle righe valide dell'ordine."""
summary = []
for line in order:
product = find_product(products, line["product"])
if product is None:
continue
quantity = line["quantity"]
summary.append({
"name": product["name"],
"quantity": quantity,
"total": calculate_line_total(product, quantity)
})
return summary
Possiamo utilizzare le funzioni:
products = [
{"name": "Milk", "price": 1.50, "stock": 20},
{"name": "Bread", "price": 2.00, "stock": 8},
{"name": "Eggs", "price": 3.20, "stock": 15},
{"name": "Apples", "price": 2.50, "stock": 30}
]
order = [
{"product": "Milk", "quantity": 3},
{"product": "Eggs", "quantity": 2},
{"product": "Apples", "quantity": 5}
]
summary = get_order_summary(products, order)
total = calculate_order_total(products, order)
low_stock = get_low_stock(products)
print("========================================")
print(" RIEPILOGO ORDINE")
print("========================================")
print()
for line in summary:
print(
f"{line['name']:10} × "
f"{line['quantity']:<3} "
f"{line['total']:7.2f} €"
)
print()
print(f"Totale ordine: {total:.2f} €")
print()
print("Prodotti con scorta bassa:")
for product in low_stock:
print(f"- {product['name']}")
print()
print("========================================")
La soluzione precedente funziona, ma possiamo riflettere su alcuni aspetti progettuali.
La funzione:
find_product()
conosce la struttura dei prodotti.
La funzione:
calculate_line_total()
si occupa soltanto del calcolo della riga.
La funzione:
calculate_order_total()
coordina il calcolo.
La funzione:
get_order_summary()
prepara i dati da presentare.
Abbiamo quindi una struttura:
programma
│
┌────────┴────────┐
↓ ↓
gestione ordine gestione inventario
│ │
┌───────┴───────┐ │
↓ ↓ ↓
find_product line_total get_low_stock
│
↓
calculate_order_total
Questa suddivisione sarà sempre più importante nei capitoli successivi.
Un buon principio progettuale è limitare le responsabilità.
Per esempio, calculate_order_total() non dovrebbe necessariamente
stampare:
print("Totale:", total)
È meglio:
return total
In questo modo il chiamante può decidere cosa fare:
total = calculate_order_total(products, order)
print(f"Totale: {total:.2f} €")
oppure:
save_total_to_file(total)
oppure:
send_total_by_email(total)
La funzione di calcolo non deve conoscere il sistema di visualizzazione o di salvataggio.
Questa separazione è una delle basi della progettazione del software.
In questo capitolo abbiamo imparato a utilizzare le funzioni per organizzare e riutilizzare il codice.
Abbiamo visto che:
def definisce una funzione;return restituisce un risultato e termina la funzione;return restituisce None;*args raccoglie argomenti posizionali;**kwargs raccoglie argomenti keyword;* può espandere un iterabile durante una chiamata;** può espandere un dizionario durante una chiamata;/ permette di definire parametri positional-only;* nella firma permette di definire parametri keyword-only;global permette di riferirsi a una variabile globale;nonlocal permette di modificare una variabile di uno scope esterno
non globale;lambda permette di creare piccole funzioni anonime;map(), filter() e sorted() possono ricevere funzioni;functools offre strumenti avanzati per lavorare con funzioni;Il modello mentale costruito nei primi quattro capitoli è ora:
CAPITOLO 1
DATI
│
├── variabili
├── tipi
├── stringhe
└── operatori
CAPITOLO 2
CONTROLLO
│
├── if
├── for
├── while
└── condizioni
CAPITOLO 3
STRUTTURE DATI
│
├── list
├── tuple
├── set
└── dict
CAPITOLO 4
FUNZIONI
│
├── parametri
├── return
├── scope
├── funzioni come valori
└── composizione
Ora abbiamo gli strumenti fondamentali per iniziare a costruire programmi strutturati.
Nel prossimo capitolo entreremo nel mondo della programmazione a oggetti, imparando a rappresentare nel codice entità, stato e comportamento attraverso classi e oggetti.