Funzioni in Python: def, return, argomenti, *args e **kwargs, lambda e annotazioni di tipo
Una funzione in Python è un blocco di codice con un nome, che si dichiara con la parola def, si richiama per nome con le parentesi e restituisce un risultato con return; senza return la funzione restituisce None. Gli argomenti si passano per posizione o per nome, *args raccoglie i valori posizionali in più in una tupla, **kwargs i valori con nome in un dizionario, mentre lambda crea una brevissima funzione anonima da una sola espressione.
Una funzione in Python è un blocco di codice con un nome che si può richiamare quante volte si vuole con dati diversi. Si dichiara con la parola def, riceve i valori tramite i parametri e restituisce il risultato con return. Le funzioni sono il modo principale per trasformare un lungo script in una serie di passaggi chiari e verificabili.
Come dichiarare una funzione: def in Python
La dichiarazione di una funzione inizia con def, seguono il nome, i parametri tra parentesi e i due punti. Il corpo si scrive con un rientro di quattro spazi: il rientro in Python non è un fatto estetico, ma parte della sintassi.
def greet(name):
"""Restituisce un saluto per l'utente."""
return f"Ciao, {name}!"
print(greet("Anna")) # Ciao, Anna!
La stringa tra triple virgolette subito dopo l'intestazione è la docstring, cioè la documentazione della funzione. La mostra help(greet) e i suggerimenti dell'editor. Secondo la convenzione PEP 8 i nomi delle funzioni si scrivono in snake_case: add_expense, get_rate.
La riga def greet(name): si chiama firma della funzione: nome più elenco dei parametri (e le annotazioni, se ci sono). Dalla firma si capisce come chiamare la funzione senza leggerne il corpo.
Che cosa fa return in Python
return termina la funzione e restituisce un valore a chi l'ha chiamata. Il codice che segue un return già eseguito non viene più eseguito. Se return manca o non ha un valore, la funzione restituisce None.
def area(w, h):
w * h # il risultato viene calcolato e poi buttato via
print(area(3, 4)) # None
def area(w, h):
return w * h
print(area(3, 4)) # 12
Il return dimenticato è uno degli errori più frequenti dei principianti: la funzione «funziona», ma restituisce None, e l'errore emerge una riga più sotto. Si possono restituire anche più valori: in realtà è una sola tupla, che si scompatta comodamente:
def min_max(numbers):
return min(numbers), max(numbers)
low, high = min_max([4, 1, 9])
print(low, high) # 1 9
Un return anticipato aiuta a evitare annidamenti inutili: prima si gestiscono i casi particolari, poi la logica principale.
def discount(price, is_member):
if price <= 0:
return 0
if not is_member:
return price
return price * 0.9
Argomenti di una funzione in Python: posizionali e con nome
I parametri sono i nomi nella dichiarazione della funzione, gli argomenti sono i valori che si passano alla chiamata. Gli argomenti si possono passare in ordine (posizionali) oppure per nome (con nome). Gli argomenti con nome rendono la chiamata autodescrittiva e fanno perdere importanza all'ordine.
def add_expense(title, amount, category="altro"):
return {"title": title, "amount": amount, "category": category}
add_expense("Caffè", 250) # posizionale
add_expense(amount=250, title="Caffè") # per nome
add_expense("Taxi", 600, category="trasporti") # misto
Un parametro con valore predefinito è facoltativo. Regole: i parametri con valore predefinito vanno dopo quelli obbligatori, e nella chiamata gli argomenti posizionali precedono quelli con nome. Altrimenti Python genera un SyntaxError.
Trappola: valore predefinito mutabile
Il valore predefinito viene calcolato una sola volta, nel momento in cui viene eseguita la riga def, e non a ogni chiamata. Se si tratta di una lista o di un dizionario, sarà condiviso da tutte le chiamate.
def add_item(item, acc=[]):
acc.append(item)
return acc
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] — la lista «ricorda» la chiamata precedente
La soluzione corretta è usare None come valore predefinito e creare un oggetto nuovo all'interno della funzione:
def add_item(item, acc=None):
if acc is None:
acc = []
acc.append(item)
return acc
*args e **kwargs: un numero qualsiasi di argomenti
L'asterisco davanti a un parametro raccoglie tutti gli argomenti posizionali in più in una tupla (tuple); due asterischi raccolgono tutti gli argomenti con nome in più in un dizionario (dict). I nomi args e kwargs sono solo una convenzione: contano gli asterischi.
def total(*args):
return sum(args)
print(total(100, 250, 40)) # 390
def make_user(name, **kwargs):
user = {"name": name}
user.update(kwargs)
return user
print(make_user("Anna", city="Milano", age=30))
# {'name': 'Anna', 'city': 'Milano', 'age': 30}
Gli stessi asterischi funzionano anche al contrario: nella chiamata scompattano una sequenza o un dizionario in argomenti:
values = [3, 4]
print(pow(*values)) # 81
opts = {"sep": " | ", "end": "\n"}
print("a", "b", **opts) # a | b
Solo posizionali e solo con nome
La barra / e l'asterisco * nell'elenco dei parametri funzionano come barriere: tutto ciò che sta prima di / si passa solo in ordine (la sintassi è comparsa in Python 3.8, PEP 570), tutto ciò che sta dopo * solo per nome.
def convert(amount, /, *, rate, currency="USD"):
return round(amount / rate, 2), currency
convert(100, rate=90.5) # (1.1, 'USD')
convert(amount=100, rate=90.5) # TypeError: amount si passa solo in posizione
convert(100, 90.5) # TypeError: rate solo per nome
I parametri accessibili solo per nome proteggono da confusione in chiamate come convert(100, 90.5, "EUR"), dove non è chiaro che cosa sia cosa.
Ambito delle variabili: la regola LEGB
Python cerca il nome di una variabile in questo ordine: Local (dentro la funzione) → Enclosing (nella funzione esterna) → Global (a livello di modulo) → Built-in (i nomi integrati come len). Leggere una variabile globale dentro una funzione è possibile, ma per assegnarle un nuovo valore serve la dichiarazione global.
limit = 1000
def check(amount):
return amount > limit # lettura della globale — consentita
def raise_limit():
global limit
limit = 2000 # senza global si creerebbe una variabile locale
In pratica global conviene evitarlo: una funzione che modifica lo stato esterno si testa male. È più affidabile ricevere il valore come argomento e restituire il risultato.
Funzioni annidate e closure
Una funzione si può dichiarare dentro un'altra. La funzione interna vede le variabili di quella esterna anche dopo che quest'ultima è terminata: è la cosiddetta closure. Per modificare una variabile di questo tipo si usa nonlocal.
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c(), c(), c()) # 1 2 3
Le closure stanno alla base dei decoratori e delle fabbriche di funzioni.
La funzione è un oggetto
In Python una funzione è un normale oggetto: si può mettere in una variabile, passare a un'altra funzione, restituire da una funzione, conservare in un dizionario. Ha degli attributi: __name__, __doc__, __defaults__, __annotations__.
def double(x):
return x * 2
f = double
print(f(5), f.__name__) # 10 double
print(list(map(double, [1, 2, 3]))) # [2, 4, 6]
Lambda in Python: funzioni anonime
La funzione lambda è una funzione breve senza nome, il cui corpo è una sola espressione. Il risultato dell'espressione viene restituito automaticamente: return non si scrive.
square = lambda x: x ** 2
print(square(4)) # 16
expenses = [("Caffè", 250), ("Taxi", 600), ("Pranzo", 450)]
expenses.sort(key=lambda e: e[1], reverse=True)
print(expenses) # [('Taxi', 600), ('Pranzo', 450), ('Caffè', 250)]
print(list(filter(lambda n: n % 2 == 0, range(10)))) # [0, 2, 4, 6, 8]
Le lambda in Python sono adatte come argomento breve per sorted, max, map, filter. Assegnare una lambda a una variabile, come square qui sopra, non è consigliato dalla PEP 8: se la funzione deve avere un nome, scrivete def — avrà un __name__ normale nel traceback e uno spazio per la docstring.
| def | lambda | |
|---|---|---|
| Nome | presente | assente (<lambda>) |
| Corpo | qualsiasi numero di istruzioni | una sola espressione |
| return | esplicito | implicito |
| Docstring, annotazioni | sì | no |
| Dove è adatta | ovunque | key o callback brevi |
Annotazioni di tipo: suggerimenti, non controlli
Le annotazioni descrivono i tipi attesi per i parametri e per il risultato. Python stesso non le verifica durante l'esecuzione: una funzione con x: int accetta tranquillamente una stringa. Le controllano strumenti esterni — mypy, pyright, ty — e l'editor, che evidenzia l'errore prima dell'avvio.
def add_expense(title: str, amount: float, category: str = "altro") -> dict[str, object]:
return {"title": title, "amount": amount, "category": category}
def area(w: float, h: float) -> float:
w * h # mypy: Missing return statement
Il controllo si lancia senza installazione con uvx mypy .. L'esempio sopra mostra l'utilità delle annotazioni: un return dimenticato con -> float viene individuato subito dall'analizzatore.
Dalla versione 3.12 (PEP 695) le funzioni generiche si scrivono senza TypeVar: il parametro di tipo si indica tra parentesi quadre, mentre un alias di tipo si dichiara con la parola type.
def first[T](xs: list[T]) -> T:
return xs[0]
type Money = int | float
In Python 3.14 (PEP 649) le annotazioni sono diventate pigre: vengono valutate al primo accesso, quindi è possibile far riferimento a una classe dichiarata più sotto senza virgolette, e from __future__ import annotations non serve più.
«Non passato» o «passato come None»: sentinella in 3.15
Il valore predefinito None non permette di distinguere «l'argomento non è stato passato» da «è stato passato None di proposito». Per anni il problema è stato risolto con un segnaposto fatto in casa:
_MISSING = object()
def get_rate(currency, default=_MISSING):
rates = {"USD": 90.5}
if currency in rates:
return rates[currency]
if default is _MISSING:
raise KeyError(currency)
return default
In Python 3.15 è comparsa la funzione integrata sentinel (PEP 661): MISSING = sentinel("MISSING") crea un marcatore unico con una rappresentazione leggibile, che alla copia resta sé stesso, si serializza con pickle e può comparire in un'annotazione del tipo float | MISSING. Nelle versioni precedenti funziona ancora il trucco con object().
Errori tipici con le funzioni
- Dimenticato return — la funzione restituisce
None. - Dimenticate le parentesi —
print(greet)stampa l'oggetto funzione, non il risultato della chiamata. - Valore predefinito mutabile — una lista comune a tutte le chiamate.
- Numero errato di argomenti —
TypeError: greet() missing 1 required positional argument: 'name'. - Nome uguale a quello di una funzione integrata — una funzione chiamata
listosumnasconde quella integrata.
Conclusioni
Le funzioni in Python si dichiarano con def, restituiscono il risultato con return e ricevono argomenti per posizione o per nome. *args e **kwargs danno flessibilità, / e * il rigore delle chiamate, lambda la brevità per le funzioni usa e getta, e le annotazioni di tipo insieme a mypy intercettano gli errori prima dell'esecuzione. Nel corso di Python questo argomento si affronta in pratica: il tracker delle spese si divide in funzioni con annotazioni, verificate con mypy.
Domande frequenti
In che cosa un parametro si differenzia da un argomento?
Il parametro è il nome nella dichiarazione della funzione, l'argomento è il valore concreto passato alla chiamata. In def greet(name) name è un parametro, in greet("Anna") la stringa "Anna" è un argomento.
Si può chiamare una funzione prima di averla dichiarata?
A livello di modulo no: la riga def deve essere eseguita prima della chiamata, altrimenti si ottiene NameError. All'interno di un'altra funzione, invece, si può chiamare una funzione dichiarata più sotto, purché la chiamata avvenga dopo la sua dichiarazione.
Quanti valori può restituire una funzione?
Formalmente uno solo, ma quel valore può essere una tupla: return a, b restituisce una tupla di due elementi, che si scompatta con x, y = f().
Le annotazioni di tipo sono obbligatorie?
No, Python funziona anche senza. Nel codice letto da altre persone, però, le annotazioni semplificano la manutenzione e permettono a mypy o pyright di trovare gli errori prima dell'esecuzione.
Si può usare if dentro una lambda?
Solo un'espressione condizionale: lambda x: "pari" if x % 2 == 0 else "dispari". Istruzioni, cicli e assegnazioni non sono ammessi nella lambda.
Questo argomento è trattato in modo approfondito, con pratica, nel corso «Python 3.15: от новичка до профи».
Vai al corso