Comprehension di dizionari e set in Python
Padroneggia le comprehension di dizionari, set ed espressioni generatrici in Python con sintassi chiara, esempi reali e insidie comuni.
Le comprehension di dizionari, le comprehension di set e le espressioni generatrici estendono la sintassi compatta della list comprehension ad altre strutture dati. Questo capitolo spiega ciascuna forma in dettaglio: sintassi, filtraggio, annidamento, pattern del mondo reale e quando evitarle.
Comprehension di dizionari
Una comprehension di dizionario costruisce un nuovo dict a partire da qualsiasi iterabile in una singola espressione. Invece di scrivere un ciclo for che chiama d[key] = value ad ogni iterazione, si descrive il mapping in modo conciso tra parentesi graffe.
Sintassi
new_dict = {key_expr: value_expr for item in iterable}Aggiungere un filtro if opzionale dopo l'iterabile:
new_dict = {key_expr: value_expr for item in iterable if condition}| Parte | Ruolo |
|---|---|
key_expr | Espressione che produce ogni chiave |
value_expr | Espressione che produce ogni valore |
item | Variabile di ciclo — assume ogni valore da iterable in sequenza |
if condition | Filtro opzionale — salta gli elementi dove condition è False |
Esempio di base: costruire una mappa dei quadrati
squares = {x: x ** 2 for x in range(1, 6)}
print(squares)
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}Il ciclo for equivalente:
squares = {}
for x in range(1, 6):
squares[x] = x ** 2Entrambi producono lo stesso risultato; la forma con comprehension è più concisa ed esprime l'intento a colpo d'occhio.
Filtraggio delle voci
Mantenere solo le coppie che soddisfano una condizione:
prices = {"apple": 1.20, "banana": 0.50, "orange": 0.80, "grape": 2.50}
expensive = {item: price for item, price in prices.items() if price >= 1.00}
print(expensive)
# {'apple': 1.2, 'grape': 2.5}Trasformazione dei valori
Applicare una funzione o un'espressione aritmetica a ogni valore:
prices = {"apple": 1.20, "banana": 0.50, "orange": 0.80}
# Apply a 10% discount and round to 2 decimal places
discounted = {item: round(price * 0.9, 2) for item, price in prices.items()}
print(discounted)
# {'apple': 1.08, 'banana': 0.45, 'orange': 0.72}Scambio di chiavi e valori
Invertire un dizionario (funziona correttamente quando tutti i valori sono univoci):
capitals = {"France": "Paris", "Germany": "Berlin", "Japan": "Tokyo"}
inverted = {city: country for country, city in capitals.items()}
print(inverted)
# {'Paris': 'France', 'Berlin': 'Germany', 'Tokyo': 'Japan'}Se i valori non sono univoci, le voci successive sovrascrivono quelle precedenti. Usare questo approccio solo quando si è certi che i valori formino una mappatura uno a uno.
Costruire da due iterabili con zip()
zip() accoppia elementi di due sequenze, rendendo facile costruire un dizionario da liste separate di chiavi e valori:
keys = ["name", "age", "city"]
values = ["Alice", 30, "Berlin"]
profile = {k: v for k, v in zip(keys, values)}
print(profile)
# {'name': 'Alice', 'age': 30, 'city': 'Berlin'}Questo è equivalente a dict(zip(keys, values)), ma la forma con comprehension permette di aggiungere facilmente un filtro o trasformare i valori allo stesso tempo.
Normalizzazione delle chiavi
Un'operazione comune nel mondo reale è la pulizia delle chiavi di un dizionario — ad esempio, convertire tutte le chiavi in minuscolo quando si uniscono dati da fonti diverse:
raw = {"Name": "Alice", "AGE": 30, "City": "Berlin"}
normalised = {k.lower(): v for k, v in raw.items()}
print(normalised)
# {'name': 'Alice', 'age': 30, 'city': 'Berlin'}Espressione di valore condizionale (ternario nel valore)
È possibile usare un'espressione ternaria nella posizione del valore per scegliere tra due valori per elemento:
scores = {"Alice": 95, "Bob": 62, "Carol": 78, "Dave": 45}
grades = {name: "pass" if score >= 70 else "fail" for name, score in scores.items()}
print(grades)
# {'Alice': 'pass', 'Bob': 'fail', 'Carol': 'pass', 'Dave': 'fail'}Comprehension di dizionari annidate
È possibile annidare una comprehension per gestire dati su più livelli. Ad esempio, estrarre un campo da una lista di dizionari annidati:
students = [
{"name": "Alice", "score": 95, "grade": "A"},
{"name": "Bob", "score": 82, "grade": "B"},
{"name": "Carol", "score": 91, "grade": "A"},
]
# Build a {name: score} mapping from the list
name_to_score = {s["name"]: s["score"] for s in students}
print(name_to_score)
# {'Alice': 95, 'Bob': 82, 'Carol': 91}Mantenere l'annidamento superficiale. Se la logica diventa difficile da seguire, estrarre una funzione di supporto o usare un semplice ciclo for.
Comprehension di set
Una comprehension di set costruisce un set in una singola espressione. Poiché i set contengono solo elementi univoci, i duplicati vengono rimossi automaticamente.
Sintassi
new_set = {expression for item in iterable}
new_set = {expression for item in iterable if condition}L'unica differenza visiva rispetto a una comprehension di dizionario è l'assenza dei due punti — c'è una sola espressione, non una coppia key: value.
Esempio di base: quadrati univoci
numbers = [1, 2, 2, 3, 3, 3, 4]
unique_squares = {x ** 2 for x in numbers}
print(unique_squares)
# {1, 4, 9, 16}L'ordine dell'output non è garantito — i set non sono ordinati, quindi non fare affidamento su un ordine di visualizzazione specifico.
Deduplicazione durante la trasformazione
Un pattern comune è normalizzare le stringhe e deduplicarle in un unico passaggio:
tags = ["Python", "python", "PYTHON", "Data", "data", "DATA"]
unique_tags = {tag.lower() for tag in tags}
print(unique_tags)
# {'python', 'data'}Filtraggio con una condizione
words = ["cat", "elephant", "dog", "rhinoceros", "ant"]
long_words = {w for w in words if len(w) > 4}
print(long_words)
# {'elephant', 'rhinoceros'}Trovare elementi comuni
Le comprehension di set si combinano naturalmente con le operazioni sugli insiemi:
list_a = [1, 2, 3, 4, 5, 5, 6]
list_b = [4, 5, 6, 7, 8]
set_a = {x for x in list_a}
set_b = {x for x in list_b}
common = set_a & set_b
print(common)
# {4, 5, 6}Per una conversione semplice senza trasformazione, set(list_a) è più diretto. Usare una comprehension di set quando è necessario filtrare o trasformare durante la conversione.
Espressioni generatrici
Un'espressione generatrice ha l'aspetto di una list comprehension ma utilizza le parentesi tonde invece di quelle quadre. Invece di costruire l'intera collezione in memoria in una volta sola, produce un valore alla volta, su richiesta (in modo pigro).
Sintassi
gen = (expression for item in iterable)
gen = (expression for item in iterable if condition)Perché usare un'espressione generatrice?
| Scenario | List comprehension | Espressione generatrice |
|---|---|---|
| È necessario iterare il risultato più volte | Sì | No — i generatori si esauriscono dopo un singolo passaggio |
È necessario l'accesso casuale per indice (result[3]) | Sì | No |
Il risultato viene passato a sum(), max(), any(), ecc. | Funziona ma alloca una lista | Preferita — trasmette i valori senza costruire una lista |
| Sequenza molto grande o infinita | Può esaurire la memoria | Efficiente — un valore alla volta |
Esempio di base
# List comprehension — builds the entire list in memory
squares_list = [x ** 2 for x in range(1, 6)]
print(squares_list) # [1, 4, 9, 16, 25]
# Generator expression — yields values one at a time
squares_gen = (x ** 2 for x in range(1, 6))
print(squares_gen) # <generator object <genexpr> at 0x...>
print(list(squares_gen)) # [1, 4, 9, 16, 25]L'oggetto generatore in sé non è la lista — lo si consuma iterandolo o passandolo a una funzione.
Passaggio diretto a funzioni integrate
Quando si passa un'espressione generatrice come unico argomento a una funzione, è possibile omettere la coppia extra di parentesi:
total = sum(x ** 2 for x in range(1, 1001))
print(total) # 333833500
maximum = max(len(word) for word in ["apple", "banana", "kiwi"])
print(maximum) # 6
any_negative = any(x < 0 for x in [1, -2, 3])
print(any_negative) # TrueVantaggio in termini di memoria
Per grandi quantità di dati, un'espressione generatrice evita di caricare tutto in RAM:
# Simulate a large log file as a list of strings
log_lines = [f"ERROR line {i}" if i % 100 == 0 else f"INFO line {i}" for i in range(1_000_000)]
# Generator scans lines without building an intermediate list
error_count = sum(1 for line in log_lines if line.startswith("ERROR"))
print(error_count) # 10000I generatori si esauriscono dopo un singolo passaggio
Questa è l'insidia più comune:
gen = (x * 2 for x in range(5))
print(list(gen)) # [0, 2, 4, 6, 8]
print(list(gen)) # [] — the generator is now emptySe è necessario iterare il risultato più di una volta, usare una list comprehension oppure ricreare il generatore.
Concatenazione di espressioni generatrici
Le espressioni generatrici possono essere composte senza creare liste intermedie. Ogni stadio preleva i valori dallo stadio precedente:
numbers = range(1, 11)
# Stage 1: filter even numbers
evens = (x for x in numbers if x % 2 == 0)
# Stage 2: square them
even_squares = (x ** 2 for x in evens)
print(list(even_squares)) # [4, 16, 36, 64, 100]Non viene creata alcuna lista intermedia — i valori scorrono attraverso la pipeline un valore alla volta.
Scegliere la forma giusta
| Hai bisogno di… | Usa |
|---|---|
| Una lista di valori trasformati/filtrati | [expr for item in it] |
| Un dizionario da coppie di valori | {k: v for item in it} |
| Una collezione senza duplicati | {expr for item in it} |
| Iterazione singola efficiente in memoria | (expr for item in it) |
| Logica complessa e multi-istruzione per elemento | Ciclo for semplice |
Insidie comuni
Comprehension di dizionario vs. comprehension di set. Entrambe usano le parentesi graffe {}. La differenza sta nel fatto che si scriva key: value (dict) o una singola expression (set). Un {} vuoto crea sempre un dict vuoto, non un set vuoto — usare set() per un set vuoto.
d = {} # empty dict
s = set() # empty set — NOT {}Chiavi sovrascritte. Se l'espressione della chiave produce duplicati, i valori successivi sovrascrivono silenziosamente quelli precedenti:
data = [("a", 1), ("b", 2), ("a", 99)]
d = {k: v for k, v in data}
print(d) # {'a': 99, 'b': 2} — first 'a' is goneScope delle variabili. In Python 3, la variabile di ciclo in qualsiasi comprehension è locale alla comprehension e non si propaga nell'ambito circostante:
x = "original"
result = {x: x.upper() for x in ["a", "b", "c"]}
print(x) # 'original' — comprehension's x did not overwrite thisLimite di leggibilità. Se si ha bisogno di più di una condizione if o l'espressione è lunga, un ciclo for semplice con nomi di variabili descrittivi è solitamente più chiaro:
# Hard to read
result = {k: v for k, v in data.items() if k.startswith("user_") if v is not None}
# Easier to read
result = {}
for k, v in data.items():
if k.startswith("user_") and v is not None:
result[k] = vArgomenti correlati
- List Comprehension — le basi: sintassi, filtraggio, cicli annidati e quando usare un semplice ciclo
for - Dizionari Python — concetti base sui dizionari, creazione e accesso
- Set Python — creazione di set, operazioni e casi d'uso
- Iterare sui dizionari — tutte le tecniche per iterare sui dizionari
- Iteratori Python — come funziona il protocollo degli iteratori di Python sotto il cofano