Python & AI Tutorials Logo
Programmazione Python

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:

python
# 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: True

Nota 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:

python
# 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 = 50

Questo 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:

python
# 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: -5

La 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:

python
# 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:

python
# 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: 95

La 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:

python
# 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: 50

Cosa 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:

python
# 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: False

23.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:

python
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.

python
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:

python
# 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.00

23.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 è:

python
lambda parameters: expression

La lambda prende parametri (come una normale funzione) e restituisce il risultato della valutazione dell’espressione. Ecco un esempio semplice:

python
# 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: 8

Le 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:

python
# 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: 95

Questo è 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:

python
# 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: Error

Nota 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:

python
# ❌ 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: 10

Le 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.

python
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 alla function.
  • *iterables (opzionale): Iterabili aggiuntivi per una function con 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.

python
# 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:

python
# 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.

python
filter(function, iterable)

Parametri:

  • function: Una funzione che prende un argomento, lo valuta e restituisce True o False. La funzione viene chiamata una volta per ciascun elemento nell’iterable.
  • iterable: Una sequenza (lista, tupla, stringa, ecc.) i cui elementi verranno testati dalla function.

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:

python
# 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:

python
# 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: 73

23.6.5) Combinare map() e filter()

Puoi concatenare operazioni map() e filter() per eseguire trasformazioni complesse:

python
# 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()

filter() - Mantiene ALCUNI elementi

Input: [1, 2, 3, 4, 5]

Test: is_even(x)

Output: [2, 4](uguale o più corta)

map() - Trasforma TUTTI gli elementi

Input: [1, 2, 3, 4, 5]

Applica: double(x) = x * 2

Output: [2, 4, 6, 8, 10](stessa lunghezza)

Differenze chiave:

  • map(): Applica una funzione per trasformare ogni elemento → l’output ha la stessa lunghezza
  • filter(): 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.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai