Geneste data — een lijst van dictionaries
De vorm waarin echte data binnenkomt: dictionaries in lijsten, lijsten in dictionaries. Erin pakken, groeperen, en waarom copy() hier niet genoeg is.
Hoe wil je leren?
Het probleem dat we oplossen
De laatste hoofdstukken behandelden vier containers — lijsten, tuples, dictionaries en sets. Elk voorbeeld gebruikte er één van, maar slechts één niveau diep.
Echte data komt niet zo binnen. Een lijst met bestellingen ziet er zo uit:
customer item quantity price
rafi pen 3 15.0
ahmed bag 1 850.0Er zitten hier twee niveaus in. Buitenin een collectie — het aantal bestellingen is niet bekend, dus een lijst. Binnenin is elke bestelling een record waarvan elk deel een naam heeft — dus een dictionary.
Wat terugkomt van een API, wat je krijgt bij het lezen van een CSV, wat een database teruggeeft: bijna altijd deze vorm, een lijst van dictionaries. Dit hoofdstuk introduceert geen nieuwe container. Het gaat over de containers die je al hebt in elkaar stoppen, en over de fouten herkennen die daaruit volgen.
Aan het einde van dit hoofdstuk kun je
- Een lijst van dictionaries binnengaan en erdoorheen lopen
- Een diepe
KeyErrorofIndexErrorlezen en zien op welk niveau het misgaat - Een lijst van dictionaries groeperen en samenvatten
- Onvolledige records afhandelen met de standaardwaarde van
.get() - Uitleggen waarom
copy()niet genoeg is voor geneste data
Voorkennis: Sets — een verzameling zonder duplicaten.
De vorm herkennen
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.0De regel orders[0]["customer"] is de kernvaardigheid van dit hoofdstuk, en je leest hem van links naar rechts, één stap per keer:
orders— een lijstorders[0]— zijn eerste item, een dictionaryorders[0]["customer"]— decustomer-waarde van die dictionary
Twijfel je? Stop dan en print de middelste stap. print(orders[0]) schrijven om te zien dat het een dictionary is, is een gewoonte die de helft van de problemen met geneste data wegneemt.
Erdoorheen lopen met een lus
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 geeft elke ronde een dictionary, en daarbinnen pak je dingen op naam. Nergens rekenen met indexen.
Aanhalingstekens in een f-string. f"{order['customer']}" gebruikt binnenin enkele aanhalingstekens, omdat de f-string zelf met dubbele tekens is geschreven. Gebruik dezelfde soort binnen en buiten, dan kan Python niet zien waar de tekst eindigt. Houd één soort binnenin en de andere buiten.Onvolledige records
In echte data heeft niet elke record elke key:
orders = [{"customer": "rafi"}]
print(orders[0]["discount"])KeyError: 'discount'De KeyError zegt dat de key ontbreekt, maar niet welke record hem mist. Als dat bij één bestelling van twaalfduizend gebeurt, is het lastig te vinden. Lopen met enumerate geeft je het nummer van elke record, zodat je degene zonder key kunt printen:
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'}Nu weet je dat het probleem in record 2 zit (het tellen begint bij nul), en hoe die record eruitziet.
Als het ontbreken van een key normaal is, .get():
orders = [{"customer": "rafi"}]
print(orders[0].get("discount", 0))0De regel uit hoofdstuk vijftien geldt hier ook: vierkante haken als hij er zou moeten zijn, .get() als het ontbreken normaal is. De verleiding is sterker bij geneste data, want .get() kan elke fout stilleggen — en dan wordt onvolledige data stilzwijgend nul en gaat het je berekeningen in.
De andere vorm — lijsten in een dictionary
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) - eraserDe regel by_customer["rafi"].append("bottle") zijn twee stappen in één: by_customer["rafi"] haalt een lijst tevoorschijn, en daarna voegt append eraan toe.
Deze vorm beantwoordt een probleem uit hoofdstuk vijftien, waar een key maar één keer kon bestaan en één persoon dus geen meerdere records kon hebben. Maak van de waarde een lijst en dan kan het wel.
Groeperen — het nuttigste patroon
Een lijst van dictionaries omzetten in "wie kocht wat":
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']}De drie middelste regels zijn de hele techniek: als de key ontbreekt, zet daar eerst een lege lijst neer en voeg daarna gerust toe. Zonder dat gooit de allereerste bestelling een KeyError.
Hetzelfde past in één regel:
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, []) zegt "geef me de waarde van deze key, en als hij er geen heeft zet je deze lege lijst daar neer en geef je mij die" — en aan wat terugkomt kun je toevoegen. Het verschil met .get() doet ertoe: .get() zet niets in de dictionary, setdefault wel.
De vorm van drie regels leest makkelijker, de vorm van één regel schrijft sneller. Je komt beide tegen in echte code.
Nog dieper
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] zijn vier stappen: dictionary → lijst → dictionary → lijst. Van links naar rechts gelezen is het niet ingewikkeld, alleen lang.
Voorbij drie niveaus: stop en denk na. De keten breken en de middelste stap een naam geven maakt de code leesbaar:
first_person = shop["staff"][0]
print(first_person["shifts"][1])copy() is niet genoeg voor geneste data
Hoofdstuk twaalf liet zien dat b = a twee namen op één lijst achterlaat, en dat copy() de oplossing is. Bij geneste data beschermt copy() slechts één niveau:
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 is gemaakt met copy() — een nieuwe buiten-dictionary, maar de binnenlijst is dezelfde lijst. Dus a["items"].append("pen") veranderde ook de oorspronkelijke template.
b is gemaakt met deepcopy(), die ook alles van binnen kopieert. Wijzigingen in b komen nergens anders terecht.
copy is een module, dus die heeft import copy nodig voordat je hem gebruikt — modules komen uitgebreid aan bod in hoofdstuk tweeëntwintig.
Wanneer heb jedeepcopynodig? Als je een kopie maakt van een geneste structuur om die te veranderen, en de oorspronkelijke ongemoeid wilt laten. Weet datdeepcopylangzaam is, en dat je hem meestal niet nodig hebt — de meeste code bouwt nieuwe data in plaats van bestaande data te wijzigen.
Een compleet voorbeeld
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 : ahmedDit kleine programma gebruikt alle vier containers van de laatste hoofdstukken, elk op de plek die erbij past.
Een lijst voor de bestellingen, want het aantal is onbekend en de volgorde telt.
Dictionaries voor elke bestelling, want elk deel heeft een naam nodig; en voor spend, want dat telt op bij naam.
Tuples voor elk paar in pairs. En hier zit een truc: het bedrag komt als eerste in de tuple, want Python sorteert tuples op hun eerste waarde. Een gewone .sort(reverse=True) ordent dus op bedrag en heeft geen key= nodig.
Een set voor distinct, want de vraag was "hoeveel verschillende", en pen komt twee keer voor.
En spend.get(customer, 0.0) is het telpatroon van hoofdstuk vijftien in een ander jasje — een bedrag optellen in plaats van er één optellen.
Wanneer het misgaat
KeyError: 'discount' — maar in welke record? Loop met for i, order in enumerate(orders): zodat i bekend is als het misgaat — zoals in het voorbeeld hierboven. Of zet print(order) in de lus; de laatste die geprint wordt is de schuldige.
TypeError: string indices must be integers Bijna altijd betekent dit dat je dacht een dictionary te hebben, terwijl je een string hebt. Als orders een lijst met namen is in plaats van een lijst van dictionaries, geeft order["customer"] precies dit. Check met print(type(order)).
TypeError: list indices must be integers or slices, not str De tegenovergestelde fout — je hebt een lijst en vraagt op naam. Meestal mist er een [0]: orders[0]["customer"], niet orders["customer"].
AttributeError: 'list' object has no attribute 'get' Dezelfde soort fout in een andere vorm. Het ding waarop je .get() aanriep is een lijst, geen dictionary.
Ik veranderde een kopie en de oorspronkelijke veranderde ook copy() is shallow — binnenlijsten en dictionaries blijven gedeeld. import copy en gebruik copy.deepcopy(...).
Een KeyError bij de allereerste ronde tijdens het groeperen grouped[key].append(...) draaide voordat de key bestond. Maak hem eerst aan met if key not in grouped: en een lege lijst, of gebruik setdefault.
Stap 4 van 6 — Voorspellen
Test je begrip
Twee niveaus diep. Wat wordt er geprint?
orders = [
{"customer": "rafi", "item": "pen"},
{"customer": "ahmed", "item": "bag"},
]
print(orders[1]["item"])- Apen
- Bbag
- Cahmed
- DEen `KeyError`
Een poging om te groeperen. Wat gebeurt er bij de allereerste ronde?
orders = [
{"customer": "rafi", "item": "pen"},
]
grouped = {}
for order in orders:
grouped[order["customer"]].append(order["item"])
print(grouped)- AEen `AttributeError` — een dictionary heeft geen `append`
- B`KeyError: 'rafi'` — de key bestaat nog niet, dus er is niets om aan toe te voegen
- CHet print `{}`
- DHet werkt en print `{'rafi': ['pen']}`
Er is een copy() gemaakt en twee wijzigingen aangebracht. Wat gebeurde er met de oorspronkelijke?
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': []}
Voor antwoorden heb je een account nodig
Log in om je antwoorden te controleren
De vragen staan hierboven, en ze in je hoofd uitwerken is waar het om draait. Log in om de antwoorden, de uitleg en de hints in drie niveaus te zien.
Jouw beurt
Schrijf een bestand genaamd library.py met een lijst van minstens zes boeken, elk een dictionary met een titel, een auteur, een jaar en een aantal exemplaren.
Daarna:
- Print elk boek in uitgelijnde kolommen
- Bereken het totale aantal exemplaren en het gemiddelde jaar
- Groepeer op auteur tot
{"author": ["title", "title"]}en print hoeveel boeken elk heeft - Tel de verschillende auteurs met een set
- Print de titel van het boek met de meeste exemplaren
Breek het daarna op de manier waarop echte data dat doet:
- Verwijder de key
"year"helemaal uit één boek en draai het programma. Welke regel stopte het, en vertelde het bericht je welk boek de schuldige was? - Gebruik nu
.get("year", 0)op die regel en draai het opnieuw. Wat is het gemiddelde jaar nu, en klopt het?
Die laatste vraag is de echte les. .get() stopte de fout, maar een ontbrekend jaar als nul behandelen en meenemen in het gemiddelde is een verkeerd antwoord dat stilzwijgend tot stand komt. Een fout weghalen is niet hetzelfde als een probleem oplossen.
Stap 6 van 6
Uitdaging — de hoofdstukquiz
Tien vragen van makkelijk naar moeilijk. De laatste zijn met opzet lastig.
Log in om de quiz te maken