Attraversare strutture JSON annidate con comprensioni e generatori Python

by theArchitect
SHARE
Attraversare strutture JSON annidate con comprensioni e generatori Python
© Guida-HTML5.it

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 isinstance per 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 next per 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.

Approfondisci con risorse ufficiali

SHARE