Skip to content

Tutto per WordPress, lo sviluppo web — e non solo

🐍 Programmazione funzionale in Python

🐍 Programmazione funzionale in Python

Qualsiasi sviluppatore Python prima o poi sente dire: «prova a scrivere in stile funzionale, il tuo codice sarà più pulito». Ma quando apri la documentazione, vedi map, filter e reduce con le lambda e non capisci da dove cominciare.

Il problema non è che la programmazione funzionale sia difficile. Il problema è che la maggior parte delle guide o si addentra nei boschi accademici di Haskell o si limita alla sintassi senza spiegare il «perché». Python, essendo un linguaggio multi-paradigma, fornisce esattamente tanti strumenti funzionali quanti servono per le attività quotidiane, senza estremismi.

In questo articolo, un'analisi pratica delle funzionalità funzionali di Python: dalle list comprehension alle funzioni lambda, con esempi concreti e scenari in cui ogni strumento semplifica davvero il codice.

💡 Panoramica rapida:

  • Analisi delle list comprehension, perché sono migliori dei cicli e quando usarle
  • Padroneggiare i generatori: yield, risparmio di memoria ed espressioni generatrici
  • Esplorare map + filter + reduce: esempi pratici per sostituire i cicli
  • Imparare a scrivere funzioni lambda e capire dove sono appropriate e dove danneggiano la leggibilità

Programmazione funzionale, di cosa stiamo parlando

La programmazione funzionale è un approccio in cui un programma è costruito a partire da funzioni matematiche: prendono dati immutabili in input e restituiscono un risultato senza modificare nulla all'esterno. Nessun effetto collaterale, nessuna modifica dello stato globale.

Nei linguaggi funzionali puri come Haskell, le parti «pure» e «impure» (che interagiscono con il mondo esterno) del programma sono rigorosamente separate. Questo approccio consente la verifica formale della correttezza del codice: il compilatore garantisce l'assenza di effetti collaterali imprevisti.

Python non è stato progettato come linguaggio funzionale. È multi-paradigma: orientato agli oggetti, procedurale e, sì, anche funzionale. Scrivere in Python in uno stile puramente funzionale è come piantare chiodi con un microscopio: tecnicamente possibile, ma scomodo e innaturale. Tuttavia, prendere singoli strumenti funzionali e inserirli nel codice abituale è uno schema funzionante e utile.

List comprehension, il principale strumento funzionale di Python

Il pane quotidiano della programmazione funzionale è lavorare con le liste. Selezionare elementi per condizione, trasformare ogni elemento, costruire una nuova lista da una esistente, tutto questo in Python è coperto dalle list comprehension.

Prima, ciclo procedurale:

1def filter_odd(li):
2 result = []
3 for i in li:
4 if i % 2 == 1:
5 result.append(i)
6 return result
7
8print(filter_odd([2, 4, 6, 7, 8, 1, 19, 200, 42, 31]))

Dopo, list comprehension:

1li = [2, 4, 6, 7, 8, 1, 19, 200, 42, 31]
2odd_numbers = [x for x in li if x % 2 == 1]
3print(odd_numbers) # [7, 1, 19, 31]

Quattro righe di codice procedurale compresse in una. La leggibilità non ne ha risentito: la sintassi [expression for element in iterator if condition] è intuitivamente chiara, «prendi x da li se x è dispari».

E non si tratta solo di zucchero sintattico. La list comprehension è più veloce di un ciclo equivalente con .append() perché viene eseguita a livello C, non a livello di interprete Python.

Cos'è un iteratore

La list comprehension si basa sul concetto di iteratore, un oggetto che restituisce l'elemento successivo di una sequenza quando viene richiesto next(). Qualsiasi oggetto che implementa il metodo __iter__ è detto iterabile. Lista, stringa, tupla, dizionario, set, sono tutti iterabili.

1spam_iter = iter("foobar")
2result = "".join([c.upper() for c in spam_iter])
3print(result) # FOOBAR

Qui iter() restituisce un iteratore di stringa e la list comprehension lo attraversa, applicando .upper() a ogni carattere.

Generatori, sequenze lazy

Un iteratore può essere creato non solo da una collezione esistente, ma anche usando una funzione generatrice. Invece di return, usa yield: la funzione «si addormenta», ricordando il suo stato, e alla successiva chiamata next() riprende dallo stesso punto.

1def gen(max_val):
2 i = 1
3 while i < max_val:
4 yield i
5 i += 1
6
7g = gen(1000)
8for _ in range(12):
9 print(next(g))

Questo codice stamperà i numeri da 1 a 12. La funzione gen non crea in memoria una lista di mille elementi, genera i valori uno per uno, su richiesta. Per sequenze di milioni di record (righe di un file di log, flusso da database), la differenza nel consumo di memoria è di ordini di grandezza.

Espressioni generatrici

Sintassi compatta: come le list comprehension, ma con parentesi tonde invece delle quadre.

1g = (c.upper() for c in "foobar")

Questa non è una tupla, è un generatore. La regola è semplice: parentesi quadre → lista (eager, tutta in memoria), parentesi tonde → generatore (lazy, un elemento alla volta).

I generatori sono un ponte tra stile procedurale e funzionale in Python: forniscono valutazione lazy senza addentrarsi nella teoria delle monadi.

Map, filter e reduce: tre pilastri dell'elaborazione funzionale

Tre funzioni built-in che nei linguaggi funzionali sono il fondamento di tutto. Python le implementa a modo suo ed è importante conoscere le sfumature delle versioni specifiche di Python.

Map, applicare una funzione a ogni elemento

1def square(x):
2 return x * x
3
4result = map(square, [1, 2, 3])
5print(list(result)) # [1, 4, 9]

map prende una funzione e una sequenza e restituisce un iteratore con i risultati dell'applicazione della funzione a ogni elemento. In Python 3, map restituisce un iteratore, non una lista, quindi serve list() per vedere il risultato.

Per trasformazioni semplici, la list comprehension spesso si legge meglio:

1[x * x for x in [1, 2, 3]] # same thing, but more familiar

Filter, selezionare elementi per condizione

1def is_upper(c):
2 return c == c.upper()
3
4result = filter(is_upper, "FreedominObscureandoutlandishcOde")
5print(list(result)) # ['F', 'O', 'O']

filter mantiene solo gli elementi per cui la funzione predicato restituisce True. Come map, restituisce un iteratore in Python 3.

Reduce, ridurre una sequenza a un singolo valore

A differenza di map e filter, reduce non risiede nel namespace built-in, ma nel modulo functools. Applica sequenzialmente una funzione agli elementi, accumulando il risultato.

1from functools import reduce
2
3def add(a, b):
4 return a + b
5
6print(reduce(add, range(1, 6))) # 15

Dietro le quinte: ((((1 + 2) + 3) + 4) + 5) = 15. Per sommare numeri in Python esiste la built-in sum(), quindi reduce si usa più spesso per riduzioni non standard: costruire un albero da una lista piatta, unire dizionari annidati, calcolare il massimo comune divisore di una sequenza.

Funzioni lambda, aiutanti anonimi

Quando una funzione serve esattamente una volta e il suo corpo sta in un'espressione, si usa lambda:

1lambda arguments: expression

Lo stesso esempio di filter può essere riscritto senza un def separato:

1result = list(filter(lambda c: c == c.upper(), "FreedominObscureandoutlandishcOde"))

Lambda è un compromesso. Vantaggio: non serve dichiarare una funzione separata per un controllo banale. Svantaggio: la leggibilità cala se l'espressione diventa più complessa di un paio di operazioni. Regola pratica: se la lambda non sta su una riga o inizi ad annidare lambda una dentro l'altra, estrai un normale def.

Dove le lambda sono effettivamente utili

Lo scenario più comune è l'ordinamento per una chiave non standard:

1users = [{"name": "Alice", "age": 31}, {"name": "Bob", "age": 25}]
2users.sort(key=lambda u: u["age"])

O una trasformazione rapida in map/filter quando il corpo è davvero banale:

1squares = list(map(lambda x: x * x, range(10)))

Ma se stai per usare una lambda per una logica con quattro o cinque operazioni, fermati. Il nome di una funzione funge da documentazione e def offre spazio per una docstring.

⁉️🤔 Domande frequenti

Quando una list comprehension è meglio di map?

Una list comprehension come [x*2 for x in data] si legge in modo più naturale di list(map(lambda x: x*2, data)). Un'operazione built-in con una lambda ha due livelli di indirezione, mentre una list comprehension ne ha uno. Ma se la funzione esiste già come def separato, map con essa è conciso. La list comprehension vince in leggibilità per trasformazioni semplici, specialmente con filtraggio tramite [... if ...]. Per logiche complesse con variabili intermedie, un normale ciclo for è imbattibile.

Generatore o lista, quale scegliere?

Se il risultato serve più volte (iterazione, indicizzazione, lunghezza), scegli una lista. Se iteri una volta sola e il volume di dati è grande, un generatore farà risparmiare memoria. Una lista di un milione di interi occupa circa 8 MB, un generatore per lo stesso intervallo meno di un kilobyte. Ma un generatore non può essere «riavvolto» né vi si può accedere per indice. Regola pratica: dati da file o database, generatore; risultato per uso ripetuto, list().

Le lambda danneggiano la leggibilità del codice?

In piccole dosi e per logiche banali, no. key=lambda x: x["price"] si legge all'istante. Ma non appena una lambda smette di essere «un pensiero unico», estraila in una funzione normale. I team con code review limitano le lambda a un'espressione senza annidamenti. Se un collega impiega più di cinque secondi per capirla, riscrivila come def. Il nome funge da documentazione: def by_price(item): return item["price"] è più chiaro di una lambda anonima.

Quanto è più lento il Python funzionale rispetto a quello procedurale?

Per la maggior parte delle attività quotidiane, la differenza è trascurabile, frazioni di microsecondo. La list comprehension è spesso più veloce di un ciclo equivalente perché è implementata in C. Il collo di bottiglia nei programmi Python non è la scelta tra map e un ciclo, ma input/output, richieste di rete e architettura in generale. Scrivi in modo chiaro, profila i punti critici con cProfile e ottimizza solo ciò che rallenta davvero.

Cosa usare nel tuo codice da oggi

Gli strumenti funzionali di Python non sono un dogma, ma leve. Non sostituiscono il codice procedurale e orientato agli oggetti, ma lo completano in punti specifici.

Inizia con le list comprehension, è l'applicazione più comune e sicura. Padroneggia i generatori: appena vedi una lista temporanea usata una volta sola, chiediti: non si dovrebbe sostituire con yield? Usa map e filter dove semplificano davvero una catena di trasformazioni. Riserva le lambda per key= negli ordinamenti e per callback banali.

Lo stile funzionale in Python non riguarda la «purezza», ma l'espressività. Se dopo aver sostituito un ciclo con una list comprehension il codice è diventato più chiaro, hai fatto tutto bene. Prova a riscrivere un metodo problematico in chiave funzionale e confronta il diff: spesso il risultato parla da solo.