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
42e 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
- Documentazione ufficiale Python — riferimento completo al linguaggio e alla libreria standard.
- Tipi set e frozenset — operazioni, appartenenza e requisiti degli elementi.
- Iterator e Iterable — interfacce astratte per annotare gli input e l’output del generatore.
