Introduzione
Le comprensioni di lista e i generatori sono strumenti molto utili quando si lavora con dati organizzati in strutture annidate, come oggetti JSON, risposte provenienti da API o configurazioni applicative. In questi casi, le informazioni interessanti non si trovano sempre allo stesso livello: un prodotto può essere contenuto in una categoria, una categoria in un catalogo e così via.
Una comprensione di lista è adatta quando vogliamo ottenere rapidamente una nuova lista partendo da una sequenza esistente. Un generatore, invece, produce i risultati uno alla volta, soltanto quando servono. Questa differenza è importante quando attraversiamo molti elementi oppure quando vogliamo evitare di creare strutture temporanee in memoria.
In questo tutorial costruiremo un piccolo attraversatore ricorsivo per visitare dati JSON annidati. Useremo:
- comprensioni di lista per estrarre valori da strutture note;
- generatori per visitare ricorsivamente dizionari e liste;
- filtri e trasformazioni applicati durante l´attraversamento;
- un esempio pratico con prodotti organizzati in categorie e sottocategorie.
Codice completo
from collections.abc import Iterator
from typing import Any
catalogo = {
"nome": "Negozio online",
"categorie": [
{
"nome": "Informatica",
"prodotti": [
{"nome": "Tastiera", "prezzo": 49.90, "disponibile": True},
{"nome": "Mouse", "prezzo": 24.50, "disponibile": False},
],
},
{
"nome": "Audio",
"prodotti": [
{"nome": "Cuffie", "prezzo": 79.90, "disponibile": True},
{"nome": "Microfono", "prezzo": 129.00, "disponibile": True},
],
},
],
}
def visita_nodi(dato: Any, percorso: tuple[str, ...] = ()) -> Iterator[tuple[tuple[str, ...], Any]]:
"""
Produce ogni nodo della struttura insieme al suo percorso.
Il percorso indica la posizione del nodo nel JSON.
"""
yield percorso, dato
if isinstance(dato, dict):
for chiave, valore in dato.items():
yield from visita_nodi(valore, percorso + (str(chiave),))
elif isinstance(dato, list):
for indice, elemento in enumerate(dato):
yield from visita_nodi(elemento, percorso + (str(indice),))
def estrai_prodotti(dato: Any) -> Iterator[dict[str, Any]]:
"""
Restituisce soltanto i dizionari che rappresentano prodotti.
"""
for percorso, nodo in visita_nodi(dato):
if (
isinstance(nodo, dict)
and "nome" in nodo
and "prezzo" in nodo
and "disponibile" in nodo
):
yield nodo
def prodotti_disponibili_sotto(prezzo_massimo: float) -> Iterator[dict[str, Any]]:
"""
Filtra i prodotti disponibili sotto una determinata soglia.
"""
prodotti = estrai_prodotti(catalogo)
return (
prodotto
for prodotto in prodotti
if prodotto["disponibile"]
and prodotto["prezzo"] <= prezzo_massimo
)
# Comprensione di lista: otteniamo tutti i nomi dei prodotti.
nomi_prodotti = [
prodotto["nome"]
for prodotto in estrai_prodotti(catalogo)
]
print("Tutti i prodotti:", nomi_prodotti)
# Generatore: i prodotti vengono elaborati uno alla volta.
prodotti_economici = prodotti_disponibili_sotto(80)
for prodotto in prodotti_economici:
print(f´{prodotto["nome"]}: {prodotto["prezzo"]:.2f} euro´)
# Comprensione di lista con trasformazione e condizione.
etichette = [
f´{p["nome"]} - {p["prezzo"]:.2f} euro´
for p in estrai_prodotti(catalogo)
if p["disponibile"]
]
print("Prodotti disponibili:", etichette)
# Ricerca di un singolo elemento senza visitare inutilmente il resto.
primo_prodotto_costoso = next(
(
prodotto
for prodotto in estrai_prodotti(catalogo)
if prodotto["prezzo"] > 100
),
None,
)
print("Primo prodotto costoso:", primo_prodotto_costoso) Spiegazione
Attraversamento ricorsivo
La funzione visita_nodi riceve un valore qualsiasi: un dizionario, una lista oppure un dato semplice come una stringa o un numero. Prima restituisce il nodo corrente tramite yield, poi controlla il suo tipo.
Se il nodo è un dizionario, la funzione visita ogni valore associato alle chiavi. Se invece è una lista, visita ogni elemento. L´istruzione yield from delega alla chiamata ricorsiva la produzione dei risultati. In questo modo l´intera struttura viene attraversata senza costruire una lista contenente tutti i nodi.
Il parametro percorso conserva la posizione del nodo. Per esempio, un prodotto potrebbe avere un percorso simile a ("categorie", "1", "prodotti", "0"). Questo dato è utile per diagnosticare errori nei JSON o indicare precisamente dove si trova un valore.
Selezionare i prodotti
La funzione estrai_prodotti usa il generatore precedente e applica un controllo strutturale. Invece di basarsi sul percorso, verifica la presenza delle chiavi nome, prezzo e disponibile. È una strategia più flessibile, perché continua a funzionare anche se la struttura delle categorie cambia.
Il risultato è un generatore di dizionari. Ogni prodotto viene restituito soltanto quando il programma lo richiede, ad esempio durante un ciclo for.
Comprensione di lista e generatore
La variabile nomi_prodotti usa una comprensione di lista. In una sola espressione estrae il campo nome da ogni prodotto. È leggibile e appropriata perché l´obiettivo è ottenere una lista concreta da riutilizzare.
La variabile prodotti_economici, invece, contiene un´espressione generatrice. La sintassi è simile a quella di una comprensione di lista, ma le parentesi tonde indicano che i prodotti saranno filtrati e prodotti in modo differito.
Infine, next combinato con un generatore permette di fermarsi al primo elemento compatibile. Il valore None viene restituito se non esiste alcun prodotto costoso.
Best practice
- Scegli una lista quando serve riutilizzare i risultati. Una lista può essere indicizzata, ordinata e percorsa più volte.
- Scegli un generatore per elaborazioni progressive. È ideale quando basta un solo passaggio sui dati.
- Mantieni i predicati leggibili. Se una condizione diventa lunga, estraila in una funzione con un nome descrittivo.
- Usa
isinstanceper distinguere dizionari e liste. Evita di presumere che ogni nodo abbia lo stesso tipo. - Proteggi l´accesso alle chiavi. In dati esterni o non validati, usa
dict.get()oppure una fase di validazione. - Non abusare della ricorsione. Per JSON estremamente profondi può essere preferibile un algoritmo iterativo basato su una pila.
- Usa
nextper ricerche anticipate. In questo modo l´attraversamento si interrompe appena trova il risultato.
Riepilogo
Le comprensioni di lista rendono concise le estrazioni da strutture già note, mentre i generatori permettono di attraversare dati annidati in modo progressivo ed efficiente. Combinando ricorsione, yield, yield from e condizioni, possiamo creare funzioni riutilizzabili per analizzare JSON complessi senza duplicare la logica.
Il modello mostrato può essere esteso per cercare chiavi specifiche, calcolare statistiche, validare record o trasformare ogni nodo in un formato più semplice. La regola principale è separare l´attraversamento dalla logica applicativa: una funzione visita la struttura, mentre altre decidono quali elementi selezionare o trasformare.
