Python set: deduplicare record ricevuti da pagine API

by theArchitect
SHARE
Python set: deduplicare record ricevuti da pagine API
© Guida-HTML5.it

Per evitare di elaborare più volte lo stesso record restituito da pagine API sovrapposte, conserva gli identificativi già visti in un set e controlla ogni nuovo ID prima di processarlo. L’appartenenza a un set è in media O(1), quindi questa soluzione è più efficiente di una ricerca ripetuta in una lista. Il pattern seguente mantiene l’ordine originale, valida gli ID e segnala i record non validi.

Sintassi rapida (TL;DR):

def unique_by_id(records):
    seen = set()
    for record in records:
        record_id = record["id"]
        if record_id not in seen:
            seen.add(record_id)
            yield record

Perché usare un set per deduplicare le pagine di un’API

Le API paginate possono restituire record ripetuti: per esempio, quando i dati cambiano tra una richiesta e la successiva, oppure quando l’endpoint usa paginazione basata su offset. Se il client elabora ogni elemento senza controlli, può duplicare notifiche, scritture su database o attività successive. Il problema è particolarmente rilevante durante importazioni e sincronizzazioni.

Una lista di ID già elaborati funziona, ma verificare ogni nuovo elemento con id in lista richiede una scansione lineare. Con n record, il costo complessivo può arrivare a O(n²). Un set usa una tabella hash: il controllo e l’inserimento hanno costo medio O(1), per un costo medio complessivo O(n). Il set non conserva l’ordine; il generatore, invece, restituisce i record nell’ordine in cui arrivano.

Questa tecnica deduplica solo all’interno dell’esecuzione corrente. Se il processo si interrompe e riparte, il set in memoria viene perso: per garantire idempotenza persistente occorre memorizzare gli ID elaborati in un database o in un archivio durevole.

Deduplicazione di record JSON tra pagine API in Python

L’esempio simula pagine di record JSON già decodificate in dizionari Python. La funzione ignora i record duplicati, accetta ID stringa e interi (ma non booleani) e solleva un errore descrittivo quando trova un dato malformato. Puoi eseguirlo direttamente con Python 3.11 o versioni successive.

from collections.abc import Iterable, Iterator
from typing import Any

Record = dict[str, Any]


def iter_unique_records(
    pages: Iterable[Iterable[Record]],
) -> Iterator[Record]:
    """Restituisce i record con ID univoci, nell'ordine di ricezione."""
    seen_ids: set[str | int] = set()

    for page_number, page in enumerate(pages, start=1):
        for record in page:
            if not isinstance(record, dict):
                raise TypeError(
                    f"La pagina {page_number} contiene un record non valido."
                )

            record_id = record.get("id")
            # bool è una sottoclasse di int: va escluso esplicitamente.
            if isinstance(record_id, bool) or not isinstance(
                record_id, (str, int)
            ):
                raise ValueError(
                    f"ID non valido nella pagina {page_number}: "
                    f"{record_id!r}"
                )

            if record_id in seen_ids:
                continue

            seen_ids.add(record_id)
            yield record


if __name__ == "__main__":
    pages: list[list[Record]] = [
        [
            {"id": "evt-101", "type": "created"},
            {"id": "evt-102", "type": "updated"},
        ],
        [
            {"id": "evt-102", "type": "updated"},
            {"id": "evt-103", "type": "deleted"},
        ],
    ]

    for item in iter_unique_records(pages):
        print(f"{item['id']}: {item['type']}")

Output in Console dell'esecuzione

evt-101: created
evt-102: updated
evt-103: deleted

Come funziona il set di ID già elaborati

1. Type hints e generatore per elaborare record in streaming

Iterable[Iterable[Record]] descrive una sequenza di pagine, ciascuna composta da record. La funzione restituisce un Iterator[Record]: grazie a yield, consegna un elemento alla volta invece di accumulare tutti i risultati in una nuova lista. Questo riduce la memoria aggiuntiva usata per i record, anche se il set degli ID cresce con il numero di identificativi distinti.

Il tipo Record è un alias leggibile per dict[str, Any]. In un progetto con uno schema stabile puoi sostituirlo con un TypedDict o con un modello di validazione, così da controllare anche i campi diversi da id.

2. Controllo O(1) e ordine della prima occorrenza

La riga if record_id in seen_ids verifica se l’ID è stato incontrato. Se è già presente, continue salta quel record; altrimenti, add() registra l’ID prima che il record venga restituito. Questo mantiene la prima occorrenza e ignora le successive. La complessità del lookup è media O(1), ma il caso peggiore teorico può essere O(n); inoltre, hash e uguaglianza di oggetti personalizzati possono influire sul costo.

3. Validazione degli ID e valori booleani

Un set richiede elementi hashable: un ID come una lista causerebbe TypeError: unhashable type: 'list'. L’esempio limita gli ID a stringhe e interi, evitando il problema prima del lookup. In Python, bool è una sottoclasse di int: senza il controllo esplicito, True e 1 sono trattati come uguali nei set. La funzione rifiuta quindi i booleani con un ValueError esplicativo.

Confronto tra set, lista e database per deduplicare record API

La scelta dipende dal volume dei dati e dal fatto che la deduplicazione debba sopravvivere al riavvio del processo.

Metodo / Approccio Sintassi / Caratteristica Performance / Complessità Quando sceglierlo
Set in memoria record_id in seen_ids Lookup medio O(1); memoria O(u), con u ID univoci Importazioni o processi temporanei in una singola esecuzione
Lista in memoria record_id in seen_ids Lookup O(n); scansione cumulativa fino a O(n²) Sequenze molto piccole in cui conta la semplicità
Indice univoco persistente Vincolo UNIQUE su una colonna ID Dipende dal database e dall’indice; persiste ai riavvii Worker, sincronizzazioni ripetibili e processi distribuiti

Errori comuni da evitare e best practice con i set Python

  • TypeError: unhashable type: un ID composto da lista o dizionario non può essere aggiunto al set. Valida il formato oppure trasformalo in una chiave immutabile ben definita.
  • Stato perso al riavvio: un set locale non offre deduplicazione persistente né coordinamento tra più processi. Usa una chiave univoca nel database quando la correttezza deve valere anche tra esecuzioni.
  • ID con tipo incoerente: il valore intero 42 e la stringa "42" sono distinti. Normalizza gli ID solo se il contratto dell’API stabilisce che rappresentano la stessa identità.
  • Testare i casi limite: con pytest, verifica pagine vuote, ID duplicati, ID mancanti e valori non validi. Controlla anche che il primo record duplicato sia quello restituito.

Domande frequenti sulla deduplicazione con set in Python

Un set Python mantiene l’ordine dei record API?

No: un set è una raccolta non ordinata e non deve essere usato per determinare l’ordine di output. Nel pattern mostrato, il set serve soltanto a controllare gli ID; i record vengono restituiti dal generatore nell’ordine di ricezione.

Quanto è veloce controllare se un ID è già in un set?

Il controllo id in seen_ids ha complessità media O(1), mentre la stessa verifica su una lista è O(n). Il costo medio di deduplicazione di n record è quindi O(n), con memoria O(u) per u ID distinti.

Perché nel set True e 1 risultano duplicati?

In Python, bool deriva da int e True == 1; hanno anche lo stesso hash. Se il dominio distingue questi valori, validali separatamente prima di inserirli in un set, come fa l’esempio rifiutando gli ID booleani.

Documentazione ufficiale sui set e sui tipi Python

SHARE