Storia della questione
L'elaborazione pigra (lazy evaluation) è una tecnica di programmazione in cui i calcoli sono ritardati fino a quando il risultato è necessario per il codice. Nel linguaggio Python, questa paradigma ha guadagnato popolarità grazie ai generatori e a funzioni speciali della libreria standard, come itertools. Originariamente, tali tecniche provenivano da linguaggi funzionali, ma Python fornisce i propri strumenti nativi e di terze parti per l'elaborazione pigra.
Problema
L'elaborazione eager (affamata) tradizionale richiede il caricamento e il calcolo di tutti i dati contemporaneamente (ad esempio, quando si utilizzano le comprensioni delle liste), il che può portare a un elevato consumo di memoria e a una diminuzione delle prestazioni quando si lavora con sequenze grandi o infinite. L'elaborazione pigra consente di "caricare" e elaborare gli elementi solo secondo necessità, evitando spese inutili di risorse.
Soluzione
In Python, l'elaborazione pigra è realizzata non solo tramite generatori (yield), ma anche attraverso funzioni pigre speciali nei moduli itertools, funzioni standard come map, filter, e anche oggetti di espressioni generatore (generator expressions). Ad esempio, la funzione map() restituisce un iteratore pigro, che calcola i valori solo quando richiesto:
# Esempio di elaborazione pigra: elevamento al quadrato di ciascun numero squares = map(lambda x: x ** 2, range(10**10)) # non utilizza memoria per una lista print(next(squares)) # 0 print(next(squares)) # 1
Caratteristiche chiave:
La funzione map() in Python implementa sempre l'elaborazione pigra? Come sapere quali funzioni standard sono pigre?
No, a partire da Python 3, le funzioni map(), filter(), zip() restituiscono iteratori, quindi implementano l'elaborazione pigra. In Python 2 queste funzioni restituivano liste. Per scoprire se un oggetto è pigro, è necessario verificare il suo tipo o esaminare la documentazione:
result = map(lambda x: x+1, range(5)) print(type(result)) # 'map' — è un iteratore
Funzionerà l'elaborazione pigra quando si utilizza un'espressione generatore all'interno della funzione sum()?
La funzione sum() richiede di attraversare completamente l'iteratore. L'espressione generatore di per sé è pigra, ma sum() alla fine consuma comunque l'intera sequenza:
s = sum(x**2 for x in range(1000000)) # il generatore viene esaurito completamente
È possibile applicare l'elaborazione pigra a liste e tuple normali, ad esempio tramite map/lambda?
Sì, è possibile, ma le liste e le tuple sono comunque caricate in memoria. Map restituirà un iteratore pigro su di esse, ma i dati originali sono comunque completamente in memoria. Per una catena pigra completa, è preferibile lavorare con generatori ad ogni passo:
def gen(): for i in range(1, 100): yield i squares = map(lambda x: x**2, gen()) # tutto è pigro
Un sviluppatore scrive una routine per un grande file di log utilizzando un'espressione generatore per filtrare le righe, ma accidentalmente lo converte subito in una lista:
with open('biglog.txt') as f: important_lines = [line for line in f if 'ERROR' in line] # carica l'intero file
Vantaggi:
Svantaggi:
Un altro team utilizza un approccio pigro con un'espressione generatore e elabora le righe man mano che arrivano:
with open('biglog.txt') as f: for line in (l for l in f if 'ERROR' in l): process(line)
Vantaggi:
Svantaggi: