23. Funzioni di prima classe e tecniche funzionali
Nei capitoli precedenti abbiamo imparato a definire e chiamare le funzioni, lavorare con parametri e argomenti e comprendere lo scope delle variabili. Ora esploreremo una caratteristica potente che distingue Python: le funzioni sono oggetti di prima classe. Questo significa che le funzioni possono essere trattate come qualsiasi altro valore—memorizzate in variabili, passate come argomenti ad altre funzioni e restituite dalle funzioni.
Questa capacità apre tecniche di programmazione eleganti che rendono il codice più flessibile, riutilizzabile ed espressivo. Vedremo come sfruttare le funzioni di prima classe tramite esempi pratici, comprendere le closure (funzioni che “ricordano” il loro ambiente), usare le espressioni lambda per definizioni di funzioni concise e applicare funzioni integrate come map(), filter(), any() e all() per lavorare con collezioni in modo efficiente.
23.1) Le funzioni come oggetti di prima classe
23.1.1) Cosa significa "di prima classe"
In Python, le funzioni sono oggetti di prima classe, il che significa che possono essere:
- Assegnate a variabili
- Memorizzate in strutture dati (liste, dizionari, ecc.)
- Passate come argomenti ad altre funzioni
- Restituite come valori da altre funzioni
Questo è diverso da alcuni linguaggi di programmazione in cui le funzioni hanno uno status speciale e non possono essere manipolate come valori regolari. In Python, una funzione è semplicemente un altro tipo di oggetto, simile a interi, stringhe o liste.
Vediamo questo in azione:
# Definisci una funzione semplice
def greet(name):
return f"Hello, {name}!"
# Assegna la funzione a una variabile
say_hello = greet
# Chiama la funzione tramite la nuova variabile
message = say_hello("Alice")
print(message) # Output: Hello, Alice!
# Verifica che entrambi i nomi si riferiscano alla stessa funzione
print(greet) # Output: <function greet at 0x...>
print(say_hello) # Output: <function greet at 0x...>
print(greet is say_hello) # Output: TrueNota che quando scriviamo say_hello = greet, non stiamo chiamando la funzione (nessuna parentesi). Stiamo creando un nuovo nome che si riferisce allo stesso oggetto funzione. Sia greet sia say_hello ora puntano alla stessa funzione, cosa che possiamo verificare usando l’operatore is.
23.1.2) Memorizzare funzioni in strutture dati
Dato che le funzioni sono oggetti, possiamo memorizzarle in liste, dizionari o qualsiasi altra collezione:
# Calcolatrice con operazioni memorizzate in un dizionario
def add(x, y):
return x + y
def subtract(x, y):
return x - y
def multiply(x, y):
return x * y
def divide(x, y):
return x / y
# Memorizza le funzioni in un dizionario
operations = {
'+': add,
'-': subtract,
'*': multiply,
'/': divide
}
# Usa il dizionario per eseguire calcoli
num1 = 10
num2 = 5
operator = '*'
result = operations[operator](num1, num2)
print(f"{num1} {operator} {num2} = {result}") # Output: 10 * 5 = 50Questo schema è estremamente utile per costruire sistemi flessibili. Invece di scrivere lunghe catene di istruzioni if-elif per scegliere quale funzione chiamare, possiamo cercare la funzione appropriata in un dizionario e chiamarla direttamente.
23.2) Passare funzioni come argomenti
23.2.1) Il concetto di base
Uno degli usi più potenti delle funzioni di prima classe è passarle come argomenti ad altre funzioni. Questo ci permette di scrivere codice flessibile e riutilizzabile che può funzionare con comportamenti diversi.
Ecco un esempio semplice:
# Funzione che applica un'altra funzione a un valore
def apply_operation(value, operation):
"""Applica la funzione operation ricevuta come parametro al valore."""
return operation(value)
# Operazioni diverse
def double(x):
return x * 2
def square(x):
return x * x
def negate(x):
return -x
# Usa la stessa funzione apply_operation con operazioni diverse
number = 5
print(apply_operation(number, double)) # Output: 10
print(apply_operation(number, square)) # Output: 25
print(apply_operation(number, negate)) # Output: -5La funzione apply_operation non sa né le interessa quale operazione specifica stia eseguendo. Si limita a chiamare qualunque funzione le venga passata. Questa separazione delle responsabilità rende il codice più modulare e più facile da estendere.
23.2.2) Elaborare collezioni con funzioni personalizzate
Uno schema comune è elaborare ogni elemento di una collezione usando una funzione passata come argomento:
# Elabora ogni elemento in una lista usando una funzione data
def process_list(items, processor):
"""Applica la funzione processor a ciascun elemento della lista."""
results = []
for item in items:
results.append(processor(item))
return results
# Funzioni di elaborazione diverse
def uppercase(text):
return text.upper()
def add_exclamation(text):
return text + "!"
def get_length(text):
return len(text)
# Elabora la stessa lista in modi diversi
words = ["hello", "world", "python"]
print(process_list(words, uppercase)) # Output: ['HELLO', 'WORLD', 'PYTHON']
print(process_list(words, add_exclamation)) # Output: ['hello!', 'world!', 'python!']
print(process_list(words, get_length)) # Output: [5, 5, 6]Questo schema è così utile che Python fornisce funzioni integrate come map() e filter() che funzionano in questo modo (le esploreremo nella Sezione 23.6).
23.2.3) Ordinare fornendo una funzione key (breve introduzione)
La funzione sorted() di Python accetta un parametro key—una funzione che determina come confrontare gli elementi:
# Ordina gli studenti in base a criteri diversi
students = [
{"name": "Alice", "grade": 85, "age": 20},
{"name": "Bob", "grade": 92, "age": 19},
{"name": "Charlie", "grade": 78, "age": 21},
{"name": "Diana", "grade": 95, "age": 20}
]
# Funzione per estrarre il voto
def get_grade(student):
return student["grade"]
# Funzione per estrarre il nome
def get_name(student):
return student["name"]
# Ordina per voto (crescente)
by_grade = sorted(students, key=get_grade)
print("Sorted by grade:")
for student in by_grade:
print(f" {student['name']}: {student['grade']}")
# Output:
# Charlie: 78
# Alice: 85
# Bob: 92
# Diana: 95
# Ordina per nome (alfabetico)
by_name = sorted(students, key=get_name)
print("\nSorted by name:")
for student in by_name:
print(f" {student['name']}: {student['grade']}")
# Output:
# Alice: 85
# Bob: 92
# Charlie: 78
# Diana: 95La funzione key viene chiamata una volta per ciascun elemento e il suo valore di ritorno viene usato per il confronto. Questo è molto più flessibile rispetto al dover scrivere una logica di ordinamento personalizzata.
Questo schema di passare funzioni per personalizzare il comportamento è estremamente comune in Python. Esploreremo tecniche di ordinamento più avanzate nel Capitolo 38.
23.3) Restituire funzioni dalle funzioni
23.3.1) Funzioni che creano funzioni
Così come possiamo passare funzioni come argomenti, possiamo anche restituire funzioni da altre funzioni. Questo ci consente di creare funzioni specializzate in modo dinamico:
# Funzione che crea e restituisce una nuova funzione
def create_multiplier(factor):
"""Crea una funzione che moltiplica per il fattore dato."""
def multiplier(x):
return x * factor
return multiplier
# Crea funzioni moltiplicatrici specializzate
double = create_multiplier(2)
triple = create_multiplier(3)
times_ten = create_multiplier(10)
# Usa le funzioni create
print(double(5)) # Output: 10
print(triple(5)) # Output: 15
print(times_ten(5)) # Output: 50Cosa sta succedendo qui? La funzione create_multiplier definisce una funzione interna chiamata multiplier e la restituisce. Ogni volta che chiamiamo create_multiplier con un fattore diverso, otteniamo una nuova funzione che “ricorda” quel fattore specifico. Questo è il nostro primo assaggio delle closure, che esploreremo in profondità nella prossima sezione.
23.3.2) Creare validatori personalizzati
Restituire funzioni è particolarmente utile per creare funzioni di validazione o di elaborazione personalizzate:
# Crea validatori di intervallo in modo dinamico
def create_range_validator(min_value, max_value):
"""Crea una funzione che valida se un numero è nell'intervallo."""
def validator(number):
return min_value <= number <= max_value
return validator
# Crea validatori specifici
is_valid_age = create_range_validator(0, 120)
is_valid_percentage = create_range_validator(0, 100)
is_room_temperature = create_range_validator(15, 30)
# Usa i validatori
age = 25
print(f"Is {age} a valid age? {is_valid_age(age)}") # Output: True
temp = 22
print(f"Is {temp}°C room temperature? {is_room_temperature(temp)}") # Output: True
score = 150
print(f"Is {score} a valid percentage? {is_valid_percentage(score)}") # Output: False23.4) Comprendere le closure: funzioni che ricordano
23.4.1) Che cos'è una closure?
Una closure è una funzione che “ricorda” variabili dallo scope in cui è stata creata, anche dopo che quello scope ha terminato l’esecuzione. Negli esempi della Sezione 23.3, abbiamo già usato closure senza nominarle esplicitamente.
Esaminiamo come funzionano le closure:
def create_counter(start=0):
"""Crea una funzione contatore che ricorda il proprio conteggio."""
count = start # Questa variabile viene "catturata" dalla closure
def counter():
nonlocal count # Accede alla variabile catturata
count += 1
return count
return counter
# Crea due contatori indipendenti
counter1 = create_counter(0)
counter2 = create_counter(100)
# Ogni contatore mantiene il proprio conteggio
print(counter1()) # Output: 1
print(counter1()) # Output: 2
print(counter1()) # Output: 3
print(counter2()) # Output: 101
print(counter2()) # Output: 102
print(counter1()) # Output: 4 (counter1 is independent of counter2)La funzione interna counter forma una closure sulla variabile count. Anche se create_counter ha terminato l’esecuzione, la funzione counter restituita ha ancora accesso a count. Ogni chiamata a create_counter crea una nuova closure indipendente con la propria variabile count.
23.4.2) Come le closure catturano le variabili
Quando una funzione viene definita all’interno di un’altra funzione, può accedere alle variabili dello scope della funzione esterna. Queste variabili vengono “catturate” e rimangono accessibili anche dopo che la funzione esterna ha effettuato il return:
Quando Python crea la funzione interna, non salva solo il codice della funzione—salva anche i riferimenti a qualsiasi variabile della funzione esterna che la funzione interna utilizza. Questo processo si chiama “cattura” delle variabili.
def create_greeter(greeting):
"""Crea una funzione di saluto con un saluto personalizzato."""
def greet(name):
return f"{greeting}, {name}!"
return greet
# Crea salutatori diversi
say_hello = create_greeter("Hello")
say_hi = create_greeter("Hi")
say_bonjour = create_greeter("Bonjour")
# Ogni salutatore ricorda il proprio saluto specifico
print(say_hello("Alice")) # Output: Hello, Alice!
print(say_hi("Bob")) # Output: Hi, Bob!
print(say_bonjour("Claire")) # Output: Bonjour, Claire!Il parametro greeting viene catturato dalla closure. Ogni funzione salutatore ha il proprio valore greeting catturato che usa ogni volta che viene chiamata.
23.4.3) Uso pratico: funzioni di configurazione
Le closure sono ottime per creare funzioni con comportamento preconfigurato:
# Crea calcolatori di prezzo con aliquote fiscali diverse
def create_price_calculator(tax_rate):
"""Crea un calcolatore che applica una specifica aliquota fiscale."""
def calculate_total(price):
tax = price * tax_rate
return price + tax
return calculate_total
# Crea calcolatori per regioni diverse
us_calculator = create_price_calculator(0.07) # tasse al 7%
uk_calculator = create_price_calculator(0.20) # IVA al 20%
japan_calculator = create_price_calculator(0.10) # imposta sui consumi al 10%
# Calcola i prezzi in regioni diverse
item_price = 100
print(f"US total: ${us_calculator(item_price):.2f}") # Output: US total: $107.00
print(f"UK total: £{uk_calculator(item_price):.2f}") # Output: UK total: £120.00
print(f"Japan total: ¥{japan_calculator(item_price):.2f}") # Output: Japan total: ¥110.0023.4.4) Quando usare le closure
Le closure sono particolarmente utili quando devi:
- Creare funzioni con comportamento preconfigurato
- Mantenere stato tra chiamate di funzione senza usare classi
- Implementare funzioni di callback che devono ricordare il contesto
- Creare factory di funzioni che producono funzioni specializzate
23.5) Usare lambda per brevi funzioni anonime
23.5.1) Cosa sono le espressioni lambda?
Una espressione lambda crea una piccola funzione anonima—una funzione senza nome. Le espressioni lambda sono utili quando serve una funzione semplice per un breve periodo e non vuoi definirla formalmente con def.
La sintassi è:
lambda parameters: expressionLa lambda prende parametri (come una normale funzione) e restituisce il risultato della valutazione dell’espressione. Ecco un esempio semplice:
# Funzione normale
def add(x, y):
return x + y
# Espressione lambda equivalente
add_lambda = lambda x, y: x + y
# Entrambe funzionano allo stesso modo
print(add(3, 5)) # Output: 8
print(add_lambda(3, 5)) # Output: 8Le espressioni lambda sono limitate a una singola espressione—non possono contenere istruzioni come if, for o più righe di codice. Questa limitazione le mantiene semplici e focalizzate.
23.5.2) Espressioni lambda come argomenti
Le espressioni lambda brillano quando devi passare una funzione semplice come argomento e non vuoi definire una funzione separata con un nome:
# Ordina gli studenti per voto usando lambda
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 92},
{"name": "Charlie", "grade": 78},
{"name": "Diana", "grade": 95}
]
# Invece di definire una funzione separata:
# def get_grade(student):
# return student["grade"]
# sorted_students = sorted(students, key=get_grade)
# Possiamo usare direttamente una lambda:
sorted_students = sorted(students, key=lambda student: student["grade"])
print("Students sorted by grade:")
for student in sorted_students:
print(f" {student['name']}: {student['grade']}")
# Output:
# Charlie: 78
# Alice: 85
# Bob: 92
# Diana: 95Questo è più conciso quando la funzione è semplice e viene usata una sola volta. La lambda lambda student: student["grade"] è equivalente a una funzione che prende uno studente e restituisce il suo voto.
23.5.3) Lambda con più parametri
Le espressioni lambda possono accettare più parametri, proprio come le funzioni normali:
# Operazioni di calcolatrice usando lambda
operations = {
'add': lambda x, y: x + y,
'subtract': lambda x, y: x - y,
'multiply': lambda x, y: x * y,
'divide': lambda x, y: x / y if y != 0 else "Error"
}
# Usa le espressioni lambda
print(operations['add'](10, 5)) # Output: 15
print(operations['multiply'](10, 5)) # Output: 50
print(operations['divide'](10, 0)) # Output: ErrorNota come possiamo usare un’espressione condizionale (x / y if y != 0 else "Error") all’interno di una lambda, ma non possiamo usare un’istruzione if (che richiederebbe più righe).
23.5.4) Quando usare lambda vs funzioni con nome
Usa le espressioni lambda quando:
- La funzione è molto semplice (una sola espressione)
- La funzione viene usata una sola volta o in un contesto molto localizzato
- Definire una funzione con nome aggiungerebbe verbosità non necessaria
Usa una funzione con nome quando:
- La funzione è complessa o richiede più istruzioni
- La funzione verrà riutilizzata in più punti
- La funzione ha bisogno di un nome descrittivo per chiarezza
- La funzione ha bisogno di una docstring
23.5.5) Limitazioni delle lambda e alternative
Le espressioni lambda hanno limitazioni importanti:
# ❌ Questo non funzionerà - lambda non può contenere istruzioni
# bad_lambda = lambda x:
# if x > 0:
# return x
# else:
# return -x
# ✅ Usa invece un'espressione condizionale
absolute_value = lambda x: x if x > 0 else -x
print(absolute_value(-5)) # Output: 5
print(absolute_value(3)) # Output: 3
# ✅ Per più operazioni, usa una funzione normale
def process_and_double(x):
print(f"Processing: {x}")
return x * 2
result = process_and_double(5) # Output: Processing: 5
print(result) # Output: 10Le espressioni lambda sono strumenti per situazioni specifiche. Quando rendono il codice più chiaro e conciso, usale. Quando rendono il codice più difficile da capire, usa invece una normale funzione con nome.
23.6) Usare map() e filter() con funzioni semplici
23.6.1) La funzione map()
La funzione map() applica una data function a ciascun elemento di un iterabile (come una lista, una tupla o una stringa) e restituisce un iteratore contenente i risultati. È un modo per trasformare ogni elemento di una collezione senza scrivere un ciclo esplicito.
map(function, iterable, *iterables)Parametri:
function(obbligatorio): Una funzione che prende uno o più argomenti, li elabora e restituisce un valore. La funzione viene chiamata una volta per ciascun elemento negli iterabili.iterable(obbligatorio): Una sequenza (lista, tupla, stringa, ecc.) i cui elementi verranno passati allafunction.*iterables(opzionale): Iterabili aggiuntivi per unafunctioncon più argomenti.
Se vengono forniti più iterabili, la funzione deve accettare quel numero di argomenti.
map() si fermerà quando l’iterabile più corto sarà esaurito.
Restituisce:
Un oggetto map (iteratore) contenente i risultati restituiti dalla function per ciascun elemento di input.
Importante: l’oggetto map è un iteratore, non una sequenza come una list.
# Raddoppia ogni numero in una lista
numbers = [1, 2, 3, 4, 5]
def double(x):
return x * 2
# Applica double a ciascun numero
doubled = map(double, numbers)
result = list(doubled) # Converti l'oggetto map (iteratore) in lista
print(result) # Output: [2, 4, 6, 8, 10]23.6.2) Usare map() con Lambda
Le espressioni lambda funzionano perfettamente con map() per trasformazioni semplici:
# Converti temperature da Celsius a Fahrenheit
celsius_temps = [0, 10, 20, 30, 40]
fahrenheit_temps = list(map(lambda c: (c * 9/5) + 32, celsius_temps))
print(fahrenheit_temps) # Output: [32.0, 50.0, 68.0, 86.0, 104.0]23.6.3) La funzione filter()
La funzione filter() applica una data function a ciascun elemento di un iterable e restituisce un iteratore contenente solo gli elementi per cui la funzione restituisce True. È un modo per selezionare elementi da una collezione senza scrivere un ciclo esplicito.
filter(function, iterable)Parametri:
function: Una funzione che prende un argomento, lo valuta e restituisceTrueoFalse. La funzione viene chiamata una volta per ciascun elemento nell’iterable.iterable: Una sequenza (lista, tupla, stringa, ecc.) i cui elementi verranno testati dallafunction.
Restituisce:
Un oggetto filter (iteratore) contenente solo gli elementi per cui la function ha restituito True.
Importante: l’oggetto filter è un iteratore, non una sequenza come una lista.
Esempio:
# Mantieni solo i numeri pari
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def is_even(x):
return x % 2 == 0
# Applica is_even a ciascun numero, mantieni solo quelli che restituiscono True
even_numbers = filter(is_even, numbers)
result = list(even_numbers) # Converti l'oggetto filter in lista
print(result) # Output: [2, 4, 6, 8, 10]23.6.4) Usare filter() con Lambda
Le espressioni lambda sono comunemente usate con filter() per un filtraggio conciso:
# Filtra gli studenti che hanno superato (grade >= 60)
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 55},
{"name": "Charlie", "grade": 92},
{"name": "Diana", "grade": 48},
{"name": "Eve", "grade": 73}
]
passed = list(filter(lambda s: s["grade"] >= 60, students))
print("Students who passed:")
for student in passed:
print(f" {student['name']}: {student['grade']}")
# Output:
# Alice: 85
# Charlie: 92
# Eve: 7323.6.5) Combinare map() e filter()
Puoi concatenare operazioni map() e filter() per eseguire trasformazioni complesse:
# Ottieni i quadrati dei numeri pari
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# Prima filtra i numeri pari, poi elevane al quadrato i valori
even_numbers = filter(lambda x: x % 2 == 0, numbers)
squared = map(lambda x: x ** 2, even_numbers)
result = list(squared)
print(result) # Output: [4, 16, 36, 64, 100]Confronto visivo: map() vs filter()
Differenze chiave:
map(): Applica una funzione per trasformare ogni elemento → l’output ha la stessa lunghezzafilter(): Testa ogni elemento e mantiene solo quelli che superano il test → l’output ha lunghezza uguale o più corta
In questo capitolo abbiamo esplorato le potenti funzionalità di programmazione funzionale di Python. Abbiamo imparato che le funzioni sono oggetti di prima classe e possono essere passate in giro come qualsiasi altro valore, abilitando schemi di codice flessibili e riutilizzabili. Abbiamo scoperto come le funzioni possono restituire altre funzioni, creando closure che ricordano il loro ambiente. Abbiamo esplorato le espressioni lambda per definizioni di funzione concise e abbiamo usato map() e filter() per elaborare collezioni in modo elegante.
Questi concetti costituiscono le fondamenta per tecniche avanzate di programmazione Python. Nel Capitolo 38, costruiremo su questa conoscenza per padroneggiare i decorator, una delle funzionalità più eleganti di Python.