Generatori

Python SELF IT
Livello 13 , Lezione 4
Disponibile

3.1 Introduzione ai generatori

I generatori sono funzioni che ritornano un oggetto iteratore. Questi iteratori generano valori al momento della richiesta, consentendo di elaborare set di dati potenzialmente grandi senza caricarli completamente in memoria.

Ci sono diversi modi per creare generatori, di seguito verranno esaminati i più popolari.

Generatori basati su funzioni

I generatori vengono creati utilizzando la parola chiave yield all'interno di una funzione. Quando una funzione con yield viene invocata, restituisce un oggetto generatore, ma non esegue immediatamente il codice all'interno della funzione. Invece, l'esecuzione viene sospesa sull'espressione yield e ripresa ad ogni chiamata del metodo __next__() dell'oggetto generatore.


def count_up_to(max):
    count = 1
    while count <= max:
        yield count
        count += 1
        
counter = count_up_to(5)
print(next(counter))  # Output: 1
print(next(counter))  # Output: 2
print(next(counter))  # Output: 3
print(next(counter))  # Output: 4
print(next(counter))  # Output: 5

Se all'interno di una funzione c'è un'istruzione yield, Python invece di eseguire tradizionalmente la funzione crea un oggetto generatore che gestisce lo stato di esecuzione della funzione.

Espressioni generatrici

Le espressioni generatrici sono simili alle list comprehension (comprese delle liste), ma vengono create utilizzando parentesi tonde invece delle quadre. Anch'esse restituiscono un oggetto generatore.


squares = (x ** 2 for x in range(10))

print(next(squares))  # Output: 0
print(next(squares))  # Output: 1
print(next(squares))  # Output: 4

Quale metodo ti piace di più?

3.2 Vantaggi dei generatori

Uso efficiente della memoria

I generatori calcolano i valori al volo, consentendo di elaborare grandi dati senza caricarli completamente in memoria. Questo rende i generatori una scelta ideale per lavorare con grandi set di dati o flussi di dati.


def large_range(n):
    for i in range(n):
        yield i
        
for value in large_range(1000000):
    # Elaboriamo i valori uno alla volta
    print(value)

Calcoli pigri

I generatori effettuano calcoli pigri, il che significa che calcolano valori solo quando necessario. Ciò consente di evitare calcoli non necessari e migliora le prestazioni.


def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b
        
fib = fibonacci()
for _ in range(10):
    print(next(fib))

Praticità della sintassi

I generatori forniscono una sintassi comoda per la creazione di iteratori, semplificando la scrittura e la lettura del codice.

3.3 Uso dei generatori

Esempi di utilizzo dei generatori nella libreria standard

Molte funzioni nella libreria standard di Python utilizzano generatori. Ad esempio, la funzione range() restituisce un oggetto generatore, che genera una sequenza di numeri.


for i in range(10):
    print(i)

Sì, il mondo non sarà mai più lo stesso.

Creazione di sequenze infinite

I generatori consentono di creare sequenze infinite, che possono essere utili in diversi scenari, come la generazione di flussi di dati infiniti.


def natural_numbers():
    n = 1
    while True:
        yield n
        n += 1
        
naturals = natural_numbers()
for _ in range(10):
    print(next(naturals))

Uso di send() e close()

Gli oggetti generatore supportano i metodi send() e close(), che consentono di inviare valori indietro nel generatore e terminare la sua esecuzione.


def echo():
    while True:
        received = yield
        print(received)
        
e = echo()
next(e)  # Avviamo il generatore
e.send("Hello, world!")  # Output: Hello, world!
e.close()

3.4 Generatori nella pratica

Generatori ed eccezioni

I generatori possono gestire eccezioni, rendendoli uno strumento potente per scrivere codice più robusto.


def controlled_execution():
    try:
        yield "Start"
        yield "Working"
    except GeneratorExit:
        print("Generator closed")
        
gen = controlled_execution()
print(next(gen))  # Output: Start
print(next(gen))  # Output: Working
gen.close()  # Output: Generator closed

Esploriamo la gestione delle eccezioni nelle prossime lezioni, ma penso che ti sarà utile sapere che i generatori funzionano alla grande con esse.

Generatori nidificati

I generatori possono essere nidificati, consentendo di creare strutture iterative complesse.


def generator1():
    yield from range(3)
    yield from "ABC"
        
for value in generator1():
    print(value)

# Output
0
1
2
A
B
C

Spiegazione:

yield from: Questa costruzione viene usata per delegare parte delle operazioni a un altro generatore, semplificando il codice e migliorando la leggibilità.

Generatori e prestazioni

L'uso dei generatori può notevolmente migliorare le prestazioni dei programmi riducendo l'uso della memoria e aumentando l'efficienza delle iterazioni.

Esempio di confronto tra liste e generatori


import time
import sys

def memory_usage(obj):
    return sys.getsizeof(obj)

n = 10_000_000

# Uso delle liste
start_time = time.time()
list_comp = [x ** 2 for x in range(n)]
list_time = time.time() - start_time
list_memory = memory_usage(list_comp)

# Uso del generatore
start_time = time.time()
gen_comp = (x ** 2 for x in range(n))
gen_result = sum(gen_comp)  # Calcoliamo la somma per comparare i risultati
gen_time = time.time() - start_time
gen_memory = memory_usage(gen_comp)

print(f"Lista:")
print(f"  Tempo: {list_time:.2f} sec")
print(f"  Memoria: {list_memory:,} byte")

print(f"\nGeneratore:")
print(f"  Tempo: {gen_time:.2f} sec")
print(f"  Memoria: {gen_memory:,} byte")

Lista:
  Tempo: 0.62 sec
  Memoria: 89,095,160 byte

Generatore:
  Tempo: 1.13 sec
  Memoria: 200 byte
1
Опрос
Funzioni di sistema,  13 уровень,  4 лекция
недоступен
Funzioni di sistema
Funzioni di sistema
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION