Spiegazione dei generatori Python e del rendimento: risparmia memoria con esempi reali

  1. Home
  2. Blog
  3. Sviluppo software

Sviluppo software

Spiegazione dei generatori Python e del rendimento: Risparmia memoria con esempi reali

I generatori producono valori uno alla volta invece di costruire un intero elenco in memoria. Guarda esattamente come yield mette in pausa una funzione, osserva la differenza di memoria che abbiamo misurato e crea una piccola pipeline di dati.

9 min di lettura
Spiegazione dei generatori Python e del rendimento: risparmia memoria con esempi reali

Punti chiave

  • Una funzione contenente yield restituisce un generatore e il suo corpo non viene eseguito finché non viene richiesto un valore.
  • yield mette in pausa la funzione e mantiene il suo stato locale fino alla richiesta successiva.
  • I generatori utilizzano una quantità piccola e fissa di memoria indipendentemente dal numero di valori che producono.
  • Un generatore può essere consumato solo una volta, che è la trappola più comune.

Immagina di caricare un file di registro da 10 GB in un elenco solo per trovare gli errori. Il tuo programma potrebbe esaurire la memoria prima di trovarne una. I generatori Python risolvono questo problema producendo valori su richiesta, uno alla volta, in modo da poter lavorare su dati enormi o addirittura infiniti quasi senza memoria.

Questa guida mostra passo dopo passo cosa succede realmente quando un generatore è in funzione. Abbiamo eseguito ogni esempio su Python 3.13 e l’output proviene da quell’esecuzione. Aiuta a sentirsi già a proprio agio con le funzioni e i loop, che copre il nostro tutorial Python per principianti.

Cos’è un generatore?

Un generatore è un tipo speciale di iteratore che crei scrivendo una funzione utilizzando yield. Qualsiasi funzione con yield al suo interno diventa automaticamente una funzione generatrice e chiamandola si ottiene un oggetto generatore anziché eseguire il codice.

def count_up(limit):
    print("starting")
    n = 1
    while n <= limit:
        yield n
        n += 1
    print("finished")

gen = count_up(2)
print(gen)

# <generator object count_up at 0x...>

Si noti che “starting” non è stato stampato. La chiamata alla funzione ha creato solo il generatore. All’interno non è ancora partito nulla.

Come funziona la resa, passo dopo passo

Ogni chiamata a next() esegue la funzione finché non raggiunge un yield, restituisce quel valore e si blocca sul posto. La chiamata successiva riprende subito dopo quella riga, con ogni variabile locale intatta.

print(next(gen))
# starting
# 1

print(next(gen))
# 2

next(gen)
# finished
# StopIteration

Seguire attentamente l’output. “starting” appare solo sul primo next(). Nel secondo, l’esecuzione continua da dove si era fermata, quindi n è ancora disponibile e diventa 2. Nel terzo, il ciclo termina, viene stampato “finito”, la funzione cade dalla fine e Python solleva StopIteration per segnalare che non è rimasto nulla.

Raramente chiami next() te stesso. Un ciclo for lo fa per te e gestisce StopIteration in silenzio, così come list(), sum() e funzioni simili:

print(list(count_up(3)))

# starting
# finished
# [1, 2, 3]

rendimento vs rendimento

Aspetto ritorno rendimento
Cosa fa Termina la funzione Mette in pausa la funzione
Valori prodotti Un risultato Una serie, uno alla volta
Stato locale Scartato Conservato tra i valori
Quando il codice viene eseguito Immediatamente quando viene chiamato Solo quando viene richiesto un valore
Memoria per tanti valori Bisogna tenerli tutti Ne tiene uno alla volta

Questo ritardo è noto come valutazione pigra: i valori vengono elaborati solo quando necessario. Un semplice return all’interno di un generatore semplicemente lo termina, il che attiva StopIteration.

La differenza di memoria, misurata

Abbiamo confrontato un elenco di un milione di quadrati con un generatore equivalente, utilizzando sys.getsizeof:

import sys

squares_list = [n * n for n in range(1_000_000)]
squares_gen = (n * n for n in range(1_000_000))

print(sys.getsizeof(squares_list), sys.getsizeof(squares_gen))

# 8448728 200

La struttura stessa dell’elenco occupava circa 8,4 MB, mentre il generatore occupava solo 200 byte sul nostro sistema. Il divario reale è ancora più grande, poiché getsizeof non conta gli oggetti interi all’interno della lista. Anche la dimensione del generatore rimane la stessa per un miliardo di elementi, poiché memorizza solo lo stato corrente e non i risultati.

C’è un compromesso, però. Un elenco può essere riutilizzato, indicizzato e misurato con len(). Un generatore non offre nessuno di questi. Se è necessario un accesso casuale o si esegue il loop più volte su una piccola quantità di dati, un elenco è più semplice. Consulta la nostra guida su Elenco Python vs tuple vs set per ulteriori informazioni su questi tipi.

Espressioni del generatore

Per i casi semplici, non è necessaria una funzione. Un’espressione del generatore assomiglia a una comprensione di elenco con parentesi tonde al posto di quelle quadre:

squares = [n * n for n in range(5)]   # builds a list now
squares = (n * n for n in range(5))   # produces values on demand

print(sum(n * n for n in range(5)))

# 30

Quando un’espressione del generatore è l’unico argomento di una funzione, come in sum(), puoi tralasciare le parentesi extra. Si tratta di un’abitudine ordinata per i totali su dati di grandi dimensioni, poiché non viene creato alcun elenco temporaneo.

Costruisci una pipeline di dati

È qui che i generatori brillano davvero. Piccoli generatori possono essere concatenati in modo che ogni elemento scorra attraverso ogni passaggio, uno alla volta:

def read_lines(lines):
    for line in lines:
        yield line.strip()

def only_errors(lines):
    for line in lines:
        if line.startswith("ERROR"):
            yield line

def messages(lines):
    for line in lines:
        yield line.split(" ", 1)[1]

log = ["INFO started\n", "ERROR db down\n",
       "ERROR disk full\n", "INFO stopped\n"]

print(list(messages(only_errors(read_lines(log)))))

# ['db down', 'disk full']

Ogni passaggio svolge un piccolo lavoro e può essere testato da solo. Nell’uso reale, scambia l’elenco di esempio con un file aperto, perché gli oggetti file possono essere letti riga per riga, come mostra la nostra guida alla gestione dei file Python. Anche un registro di più gigabyte passerebbe quindi con pochissima memoria.

Suggerimento: Una pipeline non funziona finché qualcosa non la consuma. Costruire la catena è immediato. L’elaborazione avviene solo quando si esegue il loop sul generatore finale o lo si avvolge in una chiamata come list().

Sequenze infinite

Una lista non può essere infinita, ma un generatore sì, perché calcola sempre e solo il valore successivo. Usa itertools.islice per prenderne quanti ne hai bisogno:

import itertools

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

print(list(itertools.islice(fibonacci(), 8)))

# [0, 1, 1, 2, 3, 5, 8, 13]

Fai attenzione, però: la chiamata list(fibonacci()) senza limite verrà eseguita fino a quando la tua memoria non sarà esaurita. Lo stesso modello si adatta ai generatori di ID, ai cicli di polling e ai risultati API impaginati, in cui si recupera la pagina successiva solo quando si esaurisce quella corrente.

resa da e lotti

yield from passa a un altro iterabile, salvandoti un ciclo nidificato:

def chain_all(*groups):
    for group in groups:
        yield from group

print(list(chain_all([1, 2], (3, 4))))

# [1, 2, 3, 4]

Uno schema molto pratico è quello di suddividere i dati in batch di dimensioni fisse, ad esempio per inserire un migliaio di righe alla volta in un database:

def batches(items, size):
    batch = []
    for item in items:
        batch.append(item)
        if len(batch) == size:
            yield batch
            batch = []
    if batch:
        yield batch

print(list(batches(range(7), 3)))

# [[0, 1, 2], [3, 4, 5], [6]]

L’ultimo if batch assicura che un gruppo rimanente più piccolo non vada perso. Python 3.12 e versioni successive includono anche itertools.batched, che svolge un lavoro simile.

invia, chiudi e pulisci

I generatori possono anche ricevere valori. send() riprende il generatore e il valore diventa il risultato dell’espressione yield. Devi chiamare next() una volta per raggiungere il primo yield:

def running_total():
    total = 0
    while True:
        value = yield total
        total += value

t = running_total()
next(t)
print(t.send(10), t.send(5))

# 10 15

La maggior parte delle persone ne ha raramente bisogno. Una preoccupazione più comune è la pulizia. Se ti fermi prima, un blocco finally viene comunque eseguito quando il generatore è chiuso:

def tracked():
    try:
        yield 1
        yield 2
    finally:
        print("cleanup ran")

tr = tracked()
next(tr)
tr.close()

# cleanup ran

Pytest utilizza proprio questa idea: un dispositivo con yield esegue successivamente la pulizia, come dimostra il nostro tutorial pytest. Allo stesso modo, il decoratore contextmanager della libreria standard trasforma un generatore in un blocco with.

Generatori nelle classi

Scrivere un metodo __iter__ come generatore è il modo più semplice per rendere iterabile la tua classe. Un vantaggio è che ogni ciclo riceve un nuovo generatore, quindi l’oggetto può essere ripetuto ancora e ancora:

class Countdown:
    def __init__(self, start):
        self.start = start

    def __iter__(self):
        n = self.start
        while n > 0:
            yield n
            n -= 1

c = Countdown(3)
print(list(c), list(c))

# [3, 2, 1] [3, 2, 1]

Quando utilizzare un generatore

Usa un generatore quando Utilizza un elenco quando
I dati sono grandi o le loro dimensioni sono sconosciute I dati sono piccoli e si adattano alla memoria
Elabori gli articoli una volta, in ordine Fai il loop più volte
Potresti fermarti presto Hai bisogno di len(), indicizzazione o affettatura
Stai leggendo uno stream o dei file È necessario ordinare o invertire i dati
Stai costruendo una pipeline La semplicità conta più della memoria

Un generatore non è sempre complessivamente più veloce. Se si finisce per utilizzare tutti i valori, il lavoro totale è simile e la comprensione di un elenco può essere anche più rapida per dati di piccole dimensioni poiché comporta un minor sovraccarico per elemento. I vantaggi principali sono l’utilizzo della memoria e l’ottenimento dei primi risultati in tempi più rapidi.

Errori comuni

Riutilizzare un generatore esaurito

Questo cattura quasi tutti ad un certo punto. Un generatore ricorda che è finito:

g = count_up(2)
print(list(g), list(g))

# [1, 2] []

La seconda chiamata ottiene un elenco vuoto senza alcun errore. Chiama di nuovo la funzione per ottenere un nuovo generatore o usa l’approccio di classe mostrato sopra.

Previsto che il codice venga eseguito immediatamente

def noisy():
    print("body runs")
    yield 1

n = noisy()        # nothing is printed
print(next(n))     # body runs, then 1

I controlli degli argomenti nella parte superiore di un generatore non vengono eseguiti fino al primo next(), quindi gli errori possono apparire lontani dalla chiamata che li ha causati.

Altre trappole

  • Chiamare len() o indicizzare. I generatori non supportano nessuno dei due e rilanciano a TypeError.
  • Chiudere un file troppo presto. Restituire un generatore dall’interno di un blocco with può significare che il file si chiude prima che venga letta qualsiasi riga. Metti il ​​yield all’interno del blocco with.
  • Racchiudere tutto in list(). Ciò elimina il beneficio della memoria.
  • Utilizzarne uno per dati di piccole dimensioni. Un elenco semplice è spesso più chiaro.

Considerazioni finali

Un generatore è una funzione che può mettere in pausa. Quell’idea ti offre un basso utilizzo della memoria, sequenze infinite e pipeline pulite. Per metterlo in pratica, trova un punto nel tuo codice che costruisca un lungo elenco solo per ripeterlo una volta e trasformalo in un generatore. Quindi prova a concatenare due piccoli generatori su un vero file di registro.

Domande frequenti

Cosa fa yield in Python?

yield produce un valore e mette in pausa la funzione, mantenendo le sue variabili locali. La funzione riprende dallo stesso punto quando viene richiesto il valore successivo.

Qual è la differenza tra un generatore e una lista?

Un elenco memorizza tutti i suoi valori in memoria contemporaneamente. Un generatore calcola ogni valore su richiesta, utilizza poca memoria e solitamente può essere utilizzato una sola volta.

Qual è la differenza tra un iteratore e un generatore?

Ogni generatore è un iteratore. Un generatore è semplicemente il modo più semplice per scriverne uno, poiché Python crea i metodi __iter__ e __next__ per te.

Puoi eseguire il loop su un generatore due volte?

No. Una volta esaurito, non fornisce più nulla, senza generare errori. Crea un nuovo generatore chiamando nuovamente la funzione o memorizza i risultati in un elenco.

I generatori sono più veloci delle liste?

Non sempre. Usano molta meno memoria e forniscono il primo risultato prima, ma la comprensione di un elenco può essere leggermente più veloce quando si consumano interamente piccole quantità di dati.

Continua a imparare

Lavora con Stack Quora

Hai bisogno di aiuto con test, WordPress o SEO?

Aiutiamo i team a fornire software migliore e a far crescere i loro siti. Raccontaci a cosa stai lavorando e ti suggeriremo chiaramente il prossimo passo.

Continua a leggere

Altre guide su test, intelligenza artificiale e sviluppo moderno

Ti è piaciuto questo articolo? Scegli un argomento e passa direttamente ai nostri tutorial più recenti.

Intelligenza artificiale

Spiegazione delle allucinazioni dell'intelligenza artificiale: perché i modelli linguistici inventano le cose e come ridurle

Spiegazione delle allucinazioni dell'intelligenza artificiale: cosa sono, perché i modelli linguistici le producono, tipi comuni, come ridurle con b…

Leggi l'articolo →
DevOps

Spiegazione delle variabili d'ambiente: file .env, segreti e configurazione eseguiti correttamente

Variabili d'ambiente spiegate con esempi Python che abbiamo eseguito: lettura e impostazione, perché ogni valore è una stringa, file .env, ereditari…

Leggi l'articolo →
Sviluppo software

Spiegazione della limitazione della velocità API: algoritmi, intestazioni e gestione degli errori 429

La limitazione della velocità dell'API spiegata con una simulazione che abbiamo eseguito: bucket di token, finestra fissa, finestra scorrevole, risp…

Leggi l'articolo →
Automazione dei test

Derisione in Python: unittest.mock, patch e gli errori che fanno mentire i test

Mocking in Python con unittest.mock, testato: Mock e return_value, side_effect, patch dove viene utilizzato un nome, specifiche e autospec, date moc…

Leggi l'articolo →
Test del software

Spiegazione della copertura del codice: copertura di linea o di filiale e cosa significa realmente il 100%.

Copertura del codice spiegata con una vera esecuzione di cover.py: copertura di linea, ramo e condizione, lettura di un rapporto, perché la copertur…

Leggi l'articolo →
Database e SQL

DELETE vs TRUNCATE vs DROP in SQL: differenze, rollback e quando utilizzarli

DELETE vs TRUNCATE vs DROP spiegato con SQL testato: cosa rimuove, rollback, trigger, reimpostazione dell'identità, velocità e come differiscono Pos…

Leggi l'articolo →
Database e SQL

Spiegazione delle visualizzazioni SQL: crea, utilizza ed evita le insidie, con esempi

Viste SQL spiegate con le query che abbiamo eseguito: creazione di una vista, risultati in tempo reale, nascondere colonne, prestazioni, viste aggio…

Leggi l'articolo →
Sviluppo software

Interfaccia TypeScript e tipo: differenze reali, testate con il compilatore

Interfaccia TypeScript vs tipo, verificata con il vero compilatore: cosa possono fare entrambi, cosa può fare solo uno, fusione delle dichiarazioni,…

Leggi l'articolo →
Sviluppo software

Spiegazione di Python *args e **kwargs: funzioni flessibili con esempi chiari

Python *args e **kwargs spiegati con il codice che abbiamo eseguito: compressione e decompressione, ordine dei parametri, argomenti solo per parole …

Leggi l'articolo →
Sviluppo software

Funzioni Lambda Python: sintassi, usi reali e quando non usarli

Funzioni lambda Python spiegate con il codice che abbiamo eseguito: sintassi, ordinamento con chiave, mappa e filtro, chiusure, trap dell'associazio…

Leggi l'articolo →
Intelligenza artificiale

Cosa sono gli agenti AI? Come funzionano, con un semplice ciclo di agenti che puoi eseguire

Cosa sono gli agenti IA? Una semplice guida al ciclo dell'agente, agli strumenti, alla memoria e ai guardrail, con un piccolo ciclo Python che abbia…

Leggi l'articolo →
DevOps

Monitoraggio vs osservabilità: la vera differenza, con log, metriche e tracce

Spiegazione del monitoraggio e dell'osservabilità: errori noti e sconosciuti, log, metriche e tracce, una procedura dettagliata sugli incidenti di p…

Leggi l'articolo →
DevOps

Volumi Docker e montaggi di associazione: mantenere i dati del contenitore nel modo giusto

Volumi Docker e montaggi di collegamento spiegati: perché i dati del contenitore scompaiono, volumi denominati, montaggi di collegamento, tmpfs, ese…

Leggi l'articolo →
Automazione dei test

TDD vs BDD: differenze chiave, una caratteristica costruita in entrambi i modi e quando utilizzarli ciascuno

TDD vs BDD spiegato con una funzione di sconto costruita in entrambi i modi: refactoring rosso-verde con il codice che abbiamo eseguito, scenari "Da…

Leggi l'articolo →
Test del software

Test di accettazione da parte dell'utente (UAT): processo, lista di controllo ed esempi reali

Spiegazione del test di accettazione dell'utente: chi lo esegue, in cosa differisce dal QA, un processo in sette fasi, un modello di test case UAT, …

Leggi l'articolo →
Database e SQL

Spiegazione dei valori SQL NULL: IS NULL, COALESCE, NULLIF e le trappole da evitare

Valori SQL NULL spiegati con le query che abbiamo eseguito: logica a tre valori, IS NULL, COALESCE, NULLIF, come aggregazioni, join e ordinamento tr…

Leggi l'articolo →
Database e SQL

Istruzione SQL CASE WHEN: sintassi, esempi reali ed errori comuni

SQL CASE WHEN spiegato con le query che abbiamo eseguito: moduli semplici e cercati, ordine delle condizioni, pivot con SUM, trucchi COUNT, ordiname…

Leggi l'articolo →
Sviluppo software

Cos'è il JWT? Token Web JSON spiegati con un esempio funzionante

Cos'è il JWT? Spiegazione dei token Web JSON: le tre parti, un token creato e manomesso nel codice che abbiamo eseguito, JWT e sessioni, archiviazio…

Leggi l'articolo →
Sviluppo software

Spiegazione del loop di eventi JavaScript: stack di chiamate, microtask e timer con esempi

Il ciclo di eventi JavaScript spiegato con il codice che abbiamo eseguito: stack di chiamate, code di attività e microtask, perché setTimeout 0 non …

Leggi l'articolo →
Sviluppo software

Spiegazione dei generatori Python e del rendimento: risparmia memoria con esempi reali

Generatori Python e rendimento spiegati con il codice che abbiamo eseguito: come vengono messi in pausa e ripresi, misurato il risparmio di memoria,…

Leggi l'articolo →
Intelligenza artificiale

Cosa sono gli incorporamenti? I vettori e la somiglianza del coseno spiegati semplicemente

Cosa sono gli incorporamenti? Una guida in linguaggio semplice ai vettori, alla similarità del coseno e alla ricerca semantica, con un piccolo esemp…

Leggi l'articolo →
DevOps

Distribuzione blu-verde rispetto a quella canaria: differenze, compromessi e come scegliere

Spiegazione della distribuzione blu-verde e canary: come funziona ciascuna, costi, rischi e rollback, bucket utente stabile che abbiamo testato, mig…

Leggi l'articolo →
Automazione dei test

Test instabili: quali sono le cause e come risolverli definitivamente

Test instabili spiegati con demo che abbiamo eseguito: le cause principali, come confermarne e diagnosticarne uno, soluzioni adeguate per tempistich…

Leggi l'articolo →
Test del software

Test di carico e test di stress: differenze, metriche ed esempi k6

Spiegazione del test di carico e dello stress test: cosa dimostra ciascuno, test di picco e di assorbimento, le metriche che contano, perché le medi…

Leggi l'articolo →
Test del software

Caso di test e scenario di test: differenze chiave, esempi e quando utilizzarli

Spiegazione del caso di test e dello scenario di test: definizioni chiare, una tabella affiancata, una funzione di reimpostazione della password pre…

Leggi l'articolo →
Database e SQL

Ottimizzazione delle query SQL: leggi i piani di esecuzione e correggi query lente, con tempistiche

Ottimizzazione delle query SQL testata su un milione di righe: leggi i piani di esecuzione, aggiungi gli indici corretti, correggi filtri non aggior…

Leggi l'articolo →
Database e SQL

SQL UNION vs UNION ALL: differenze, esempi e prestazioni testate

SQL UNION vs UNION ALL spiegati con le query che abbiamo eseguito: duplicati, regole seguite da entrambi, un vero bug di dati, prestazioni misurate,…

Leggi l'articolo →
Sviluppo software

Tutorial sull'API di recupero JavaScript: GET, POST, gestione degli errori e timeout

Un tutorial sull'API JavaScript Fetch testato su un server locale: GET e POST, perché 404 non genera eccezioni, un helper riutilizzabile, stringhe d…

Leggi l'articolo →
Sviluppo software

Gestione dei file Python: lettura, scrittura, CSV, JSON e pathlib con esempi

Gestione dei file Python spiegata con il codice che abbiamo eseguito: modalità aperte, con, lettura di file di grandi dimensioni, CSV, JSON, pathlib…

Leggi l'articolo →
Sviluppo software

Classi e oggetti Python: tutorial OOP con esempi reali

Classi e oggetti Python spiegati con il codice che abbiamo eseguito: __init__, metodi, attributi di classe e istanza, ereditarietà, proprietà, metod…

Leggi l'articolo →
Intelligenza artificiale

Che cos'è un modello linguistico di grandi dimensioni? Come funzionano i LLM, spiegato senza esagerazioni

Cos’è un modello linguistico ampio? LLM spiegati in un linguaggio semplice: token, incorporamenti, attenzione, formazione, finestre di temperatura e…

Leggi l'articolo →
DevOps

Comandi Linux per principianti: la guida essenziale del terminale con esempi

Comandi Linux per principianti, raggruppati per attività: navigazione, file, visualizzazione log, grep, pipe, permessi, processi e archivi, con un e…

Leggi l'articolo →
Automazione dei test

Selenio vs drammaturgo: differenze chiave, codice a confronto e quale scegliere

Selenium vs Playwright a confronto: architettura, attesa, supporto per browser e linguaggio, debug, lo stesso test in entrambi gli strumenti, consig…

Leggi l'articolo →
Sviluppo software

Git merge vs rebase: differenze, esempi e quando usare ciascuno

Git merge vs rebase spiegato con output reale del log, una tabella di confronto, la regola d'oro del rebase e un flusso di lavoro sicuro per i team.

Leggi l'articolo →
Sviluppo software

List comprehension in Python: sintassi, esempi e quando evitarla

List comprehension in Python spiegata: sintassi, filtri, if-else, cicli annidati, tempi misurati e i casi in cui un normale ciclo for è meglio.

Leggi l'articolo →
Database e SQL

SQL JOIN spiegati con esempi (Inner, Left, Right, Full)

Capisci i JOIN SQL passo dopo passo con tabelle e query di esempio: INNER, LEFT, RIGHT, FULL, CROSS e SELF, con errori comuni e consigli sulle prest…

Leggi l'articolo →