Dati annidati — una lista di dizionari
La forma in cui arrivano i dati veri: dizionari dentro le liste, liste dentro i dizionari. Come si entra dentro, il raggruppamento, e perché copy() qui non basta.
Come vuoi imparare?
Il problema che stiamo risolvendo
I capitoli scorsi hanno presentato quattro contenitori — liste, tuple, dizionari, insiemi. Ogni esempio ne usava uno solo, a un solo livello.
I dati veri non arrivano così. Una lista di ordini assomiglia a questa:
customer item quantity price
rafi pen 3 15.0
ahmed bag 1 850.0Qui ci sono due livelli. Fuori una collezione — il numero degli ordini non è noto, quindi una lista. Dentro, ogni ordine è un record in cui ogni parte ha un nome — quindi un dizionario.
Ciò che torna da un'API, ciò che ottieni leggendo un CSV, ciò che restituisce un database: quasi sempre questa forma, una lista di dizionari. Il capitolo di oggi non introduce nessun contenitore nuovo. Parla del mettere i contenitori che hai già dentro gli altri, e del riconoscere gli errori che ne derivano.
Alla fine di questo capitolo sarai in grado di
- Entrare in una lista di dizionari e percorrerla
- Leggere un
KeyErroroIndexErrorprofondo e capire a quale livello è andato storto - Raggruppare e riassumere una lista di dizionari
- Gestire i record incompleti con il default di
.get() - Spiegare perché
copy()non basta per i dati annidati
Prerequisiti: Insiemi — una collezione senza duplicati.
Riconoscere la forma
orders = [
{"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
{"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
]
print(len(orders))
print(orders[0])
print(orders[0]["customer"])
print(orders[1]["price"])2
{'customer': 'rafi', 'item': 'pen', 'quantity': 3, 'price': 15.0}
rafi
850.0La riga orders[0]["customer"] è la competenza fondamentale di questo capitolo, e si legge da sinistra a destra, un passo alla volta:
orders— una listaorders[0]— il suo primo elemento, che è un dizionarioorders[0]["customer"]— il valorecustomerdi quel dizionario
In caso di dubbio, fermati e stampa il passaggio intermedio. Scrivere print(orders[0]) per vedere che è un dizionario è un'abitudine che elimina metà dei guai con i dati annidati.
Percorrerla con un ciclo
orders = [
{"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
{"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
]
for order in orders:
total = order["quantity"] * order["price"]
print(f"{order['customer']:<8} {order['item']:<6} {total:>8.2f}")rafi pen 45.00
ahmed bag 850.00for order in orders ti dà un dizionario a ogni giro, e al suo interno raggiungi le cose per nome. Nessun calcolo sugli indici, da nessuna parte.
Virgolette dentro un f-string. f"{order['customer']}" usa le virgolette singole dentro perché la f-string stessa è scritta con le virgolette doppie. Se usi lo stesso tipo dentro e fuori, Python non riesce a capire dove finisce il testo. Tienine uno dentro e l'altro fuori.Record incompleti
Nei dati veri, non ogni record ha ogni chiave:
orders = [{"customer": "rafi"}]
print(orders[0]["discount"])KeyError: 'discount'Il KeyError dice che la chiave manca, ma non quale record non ce l'ha. Quando succede in un ordine su dodicimila, può essere difficilissimo da trovare. Percorrere la lista con enumerate ti dà il numero di ogni record, così puoi stampare quello che non ha la chiave:
orders = [
{"customer": "rafi", "discount": 5},
{"customer": "ahmed", "discount": 0},
{"customer": "dia"},
]
for i, order in enumerate(orders):
if "discount" not in order:
print("record", i, "has no discount:", order)record 2 has no discount: {'customer': 'dia'}Ora sai che il problema è nel record 2 (il conteggio parte da zero), e com'è fatto quel record.
Quando il fatto che una chiave manchi è normale, .get():
orders = [{"customer": "rafi"}]
print(orders[0].get("discount", 0))0Vale anche qui la regola del capitolo quindici: parentesi quadre quando la chiave ci deve essere, .get() quando la sua assenza è normale. La tentazione è più forte con i dati annidati, perché .get() può zittire ogni errore — e allora i dati incompleti diventano in silenzio zero e finiscono nei tuoi calcoli.
L'altra forma — liste dentro un dizionario
by_customer = {
"rafi": ["pen", "ink"],
"ahmed": ["bag"],
}
by_customer["rafi"].append("bottle")
by_customer["dia"] = ["eraser"]
print(by_customer)
for customer, items in by_customer.items():
print(f"{customer}: {len(items)} item(s) - {', '.join(items)}"){'rafi': ['pen', 'ink', 'bottle'], 'ahmed': ['bag'], 'dia': ['eraser']}
rafi: 3 item(s) - pen, ink, bottle
ahmed: 1 item(s) - bag
dia: 1 item(s) - eraserLa riga by_customer["rafi"].append("bottle") sono due passaggi in uno: by_customer["rafi"] estrae una lista, e poi append aggiunge a quella lista.
Questa forma risponde a un problema del capitolo quindici, dove una chiave poteva esistere una sola volta e quindi una persona non poteva avere più record. Rendi il valore una lista e ora può.
Raggruppare — il pattern più utile
Trasformare una lista di dizionari in "chi ha comprato cosa":
orders = [
{"customer": "rafi", "item": "pen"},
{"customer": "ahmed", "item": "bag"},
{"customer": "rafi", "item": "ink"},
]
grouped = {}
for order in orders:
customer = order["customer"]
if customer not in grouped:
grouped[customer] = []
grouped[customer].append(order["item"])
print(grouped){'rafi': ['pen', 'ink'], 'ahmed': ['bag']}Le tre righe centrali sono tutta la tecnica: se la chiave manca, metti prima lì una lista vuota, poi aggiungi senza preoccuparti. Senza quella, già il primo ordine solleverebbe un KeyError.
La stessa cosa sta in una riga:
orders = [
{"customer": "rafi", "item": "pen"},
{"customer": "rafi", "item": "ink"},
]
grouped = {}
for order in orders:
grouped.setdefault(order["customer"], []).append(order["item"])
print(grouped){'rafi': ['pen', 'ink']}setdefault(key, []) dice "dammi il valore di questa chiave, e se non ce n'è uno metti questa lista vuota lì e dammi quella" — e a quello che torna viene fatto l'append. La differenza con .get() conta: .get() non mette nulla nel dizionario, setdefault sì.
La forma su tre righe si legge più facilmente, quella su una riga si scrive più velocemente. Ne incontrerai entrambe nel codice vero.
Ancora più in profondità
shop = {
"name": "Corner Store",
"staff": [
{"name": "rafi", "shifts": ["mon", "tue"]},
{"name": "dia", "shifts": ["wed"]},
],
}
print(shop["staff"][0]["shifts"][1])
print(len(shop["staff"]))
for person in shop["staff"]:
print(f"{person['name']}: {len(person['shifts'])} shift(s)")tue
2
rafi: 2 shift(s)
dia: 1 shift(s)shop["staff"][0]["shifts"][1] sono quattro passaggi: dizionario → lista → dizionario → lista. Letta da sinistra a destra non è complicata, solo lunga.
Oltre tre livelli, fermati e pensa. Spezzare la catena e dare un nome al passaggio intermedio rende il codice leggibile:
first_person = shop["staff"][0]
print(first_person["shifts"][1])copy() non basta per i dati annidati
Il capitolo dodici ha mostrato che b = a lascia due nomi sulla stessa lista, e che la correzione è copy(). Con i dati annidati, copy() protegge solo un livello:
import copy
template = {"name": "", "items": []}
a = template.copy()
b = copy.deepcopy(template)
a["items"].append("pen")
b["items"].append("bag")
print(template)
print(a)
print(b){'name': '', 'items': ['pen']}
{'name': '', 'items': ['pen']}
{'name': '', 'items': ['bag']}a è stato creato con copy() — un nuovo dizionario esterno, ma la lista interna è la stessa lista. Quindi a["items"].append("pen") ha cambiato anche l'template originale.
b è stato creato con deepcopy(), che copia anche tutto ciò che sta dentro. Quindi le modifiche a b non si diffondono da nessuna parte.
copy è un modulo, quindi richiede import copy prima dell'uso — i moduli per intero arrivano nel capitolo ventidue.
Quando servedeepcopy? Quando stai prendendo copia di una struttura annidata per modificarla, e vuoi lasciare intatto l'originale. Tieni presente chedeepcopyè lento, e la maggior parte delle volte non serve — la maggior parte del codice costruisce dati nuovi invece di modificare quelli esistenti.
Un esempio completo
orders.py:
# The shape data actually arrives in: a list of records
orders = [
{"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
{"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
{"customer": "rafi", "item": "ink", "quantity": 2, "price": 120.0},
{"customer": "dia", "item": "pen", "quantity": 5, "price": 15.0},
]
print("Customer Item Qty Total")
grand_total = 0.0
for order in orders:
total = order["quantity"] * order["price"]
grand_total = grand_total + total
print(f"{order['customer']:<9} {order['item']:<6} {order['quantity']:>3} {total:>10.2f}")
print(f"{'':<20}{grand_total:>10.2f}")
print()
# Group the spend by customer: the key may not exist yet, so .get supplies a start
spend = {}
for order in orders:
customer = order["customer"]
spend[customer] = spend.get(customer, 0.0) + order["quantity"] * order["price"]
# Sort by amount by putting the amount first in a tuple
pairs = []
for customer, amount in spend.items():
pairs.append((amount, customer))
pairs.sort(reverse=True)
print("Spend per customer, highest first:")
for amount, customer in pairs:
print(f" {customer:<8} {amount:>8.2f}")
distinct = set()
for order in orders:
distinct.add(order["item"])
print()
print("Distinct items:", sorted(distinct))
print("Best customer :", pairs[0][1])Customer Item Qty Total
rafi pen 3 45.00
ahmed bag 1 850.00
rafi ink 2 240.00
dia pen 5 75.00
1210.00
Spend per customer, highest first:
ahmed 850.00
rafi 285.00
dia 75.00
Distinct items: ['bag', 'ink', 'pen']
Best customer : ahmedQuesto piccolo programma usa tutti e quattro i contenitori dei capitoli scorsi, ognuno nel posto che gli spetta.
Una lista per gli ordini, perché il numero è sconosciuto e l'ordine conta.
Dizionari per ogni ordine, perché ogni parte ha bisogno di un nome; e per spend, perché accumula per nome.
Tuple per ogni coppia in pairs. E qui c'è un trucco: l'importo è messo per primo nella tupla, perché Python ordina le tuple in base al loro primo valore. Così un semplice .sort(reverse=True) ordina per importo e non serve nessun key=.
Un insieme per distinct, perché la domanda era "quanti diversi", e pen compare due volte.
E spend.get(customer, 0.0) è il pattern di conteggio del capitolo quindici sotto altra forma — si somma un importo invece di aggiungere uno.
Quando qualcosa va storto
KeyError: 'discount' — ma in quale record? Percorri la lista con for i, order in enumerate(orders): così i è noto quando fallisce — come nell'esempio sopra. Oppure metti print(order) dentro il ciclo; l'ultimo stampato è quello colpevole.
TypeError: string indices must be integers Quasi sempre significa che pensavi di avere un dizionario e hai una stringa. Se orders è una lista di nomi anziché una lista di dizionari, order["customer"] dà esattamente questo. Controlla con print(type(order)).
TypeError: list indices must be integers or slices, not str L'errore opposto — hai una lista e la interroghi per nome. Di solito manca un [0]: orders[0]["customer"], non orders["customer"].
AttributeError: 'list' object has no attribute 'get' Lo stesso tipo di errore in un'altra forma. La cosa su cui hai chiamato .get() è una lista, non un dizionario.
Ho cambiato una copia e l'originale è cambiato anche lui copy() è superficiale — le liste e i dizionari interni restano condivisi. Fai import copy e usa copy.deepcopy(...).
Un KeyError già al primo giro durante il raggruppamento grouped[key].append(...) è partito prima che la chiave esistesse. Creala con if key not in grouped: e una lista vuota, oppure usa setdefault.
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
Due livelli di profondità. Cosa viene stampato?
orders = [
{"customer": "rafi", "item": "pen"},
{"customer": "ahmed", "item": "bag"},
]
print(orders[1]["item"])- Apen
- Bbag
- Cahmed
- DUn `KeyError`
Un tentativo di raggruppare. Cosa succede al primo giro?
orders = [
{"customer": "rafi", "item": "pen"},
]
grouped = {}
for order in orders:
grouped[order["customer"]].append(order["item"])
print(grouped)- AUn `AttributeError` — un dizionario non ha `append`
- B`KeyError: 'rafi'` — la chiave non esiste ancora, quindi non c'è nulla a cui fare l'append
- CStampa `{}`
- DFunziona e stampa `{'rafi': ['pen']}`
Viene presa una copy() e fatte due modifiche. Cosa è successo all'originale?
import copy
template = {"name": "", "items": []}
a = template.copy()
a["name"] = "rafi"
a["items"].append("pen")
print(template)- A{'name': '', 'items': ['pen']}
- B{'name': '', 'items': []}
- C{'name': 'rafi', 'items': ['pen']}
- D{'name': 'rafi', 'items': []}
Per rispondere serve un account
Accedi per verificare le tue risposte
Le domande sono qui sopra, e ragionarci a mente è la parte che conta. Accedi per vedere le risposte, le spiegazioni e i suggerimenti a tre livelli.
Tocca a te
Scrivi un file chiamato library.py che contiene una lista di almeno sei libri, ognuno un dizionario con un titolo, un autore, un anno e un numero di copie.
Poi:
- Stampa ogni libro in colonne allineate
- Calcola il totale delle copie e l'anno medio
- Raggruppa per autore in
{"author": ["title", "title"]}e stampa quanti libri ha ognuno - Conta gli autori distinti con un insieme
- Stampa il titolo del libro con più copie
Poi rompilo come fanno i dati veri:
- Elimina del tutto la chiave
"year"da un libro ed esegui il programma. Quale riga lo ha fermato, e il messaggio ti ha detto quale libro era il colpevole? - Ora usa
.get("year", 0)su quella riga ed eseguilo di nuovo. Qual è l'anno medio adesso, e è vero?
Quell'ultima domanda è la vera lezione. .get() ha fermato l'errore, ma trattare un anno mancante come zero e metterlo nella media è una risposta sbagliata arrivata in silenzio. Togliere un errore non è lo stesso che risolvere un problema.
Passo 6 di 6
Sfida — il quiz del capitolo
Dieci domande dalla più facile alla più difficile. Le ultime sono difficili di proposito.
Accedi per fare il quiz