Lijsten, dictionaries, sets, lussen en comprehensions
Wanneer je welke van Pythons vier containers gebruikt, lussen en voorwaarden, comprehensions, en woorden tellen in rommelige tickets om een top vijf te vinden die elke keer hetzelfde uitkomt.
Hoe wil je leren?
Het probleem dat we oplossen
Het hoofd van de support-afdeling wil weten waar klanten deze week het meest over klaagden in de tickets. De tickets zijn rommelige tekst — sommige in hoofdletters, sommige vol leestekens, sommige die twee keer hetzelfde zeggen. Een collega schreef in vijf minuten een script en stuurde een lijst: bovenaan the, daarna refund, en dan Refund en refund. als aparte regels.
De tweede run bracht een ander probleem. Een kleine verandering in de volgorde van de tickets veranderde het vijfde woord van de top vijf, terwijl het aantal van geen enkel woord was veranderd. Niemand die het rapport las, kon zien welke versie klopte.
Het antwoord is geen speciale bibliotheek. Het zijn Python's vier basiscontainers — lijsten, tuples, dictionaries en sets — en lussen en voorwaarden die je goed gebruikt. Een groot deel van het dagelijkse werk van een AI-engineer bestaat precies uit dit: tekst opschonen, tellen, filteren en sorteren, op een manier die bij dezelfde invoer altijd hetzelfde resultaat geeft.
Aan het einde van dit hoofdstuk kun je
- Kiezen tussen een lijst, een tuple, een dictionary en een set voor een taak, en zeggen waarom
- Nette lussen schrijven met
for,while,if/elif/else,enumerateen.items() - Data filteren en transformeren met list-, dictionary- en set-comprehensions
- Woorden uit rommelige tekst halen en ze tellen met een dictionary en met
Counter - Woorden met gelijke aantallen sorteren volgens een expliciete regel, zodat het resultaat altijd hetzelfde is
Wat je eerst nodig hebt: pathlib, JSON/YAML-config en omgevingsvariabelen — en kunnen werken met een script via uv run.
Vier containers, vier taken
Eerst de vier naast elkaar:
tickets = ["refund", "login", "refund", "billing"]
latest = ("T-1042", "refund", 3)
owners = {"refund": "Maya", "login": "Omar"}
seen = set(tickets)
print(tickets[0], tickets[-1], len(tickets))
print(latest[1])
print(owners["login"])
print(sorted(seen))
print("login" in seen, "shipping" in seen)refund billing 4
refund
Omar
['billing', 'login', 'refund']
True FalseElk van die vier doet een andere belofte, en je kiest op basis van die belofte:
- Lijst (
[...]) — houdt de volgorde, staat dezelfde waarde meer dan één keer toe en kan worden aangepast. Voor "een rij dingen die na elkaar komen": een lijst met tickets, een batch prompts. - Tuple (
(...)) — houdt de volgorde, maar kan na het maken niet meer veranderd worden. Voor een paar waarden die samen één record vormen, zoals("T-1042", "refund", 3)— id, categorie, prioriteit. Hier betekent de positie iets. - Dictionary (
{sleutel: waarde}) — zoek een waarde op via een sleutel. "Wie is eigenaar van welke categorie", "hoe vaak komt elk woord voor" — overal waar je van het ene naar het andere gaat. - Set (
set(...)of{a, b}) — elke waarde één keer, geen gegarandeerde volgorde. Voor als de vraag alleen is "zit het erin" of "hoeveel verschillende waarden zijn er".
sorted(seen) wordt geprint omdat de volgorde van een geprinte set niet gegarandeerd is — die kan bij de ene run er zus uitzien en bij de volgende anders. Als de uitvoer van een programma van de volgorde van een set afhangt, is die uitvoer ook niet stabiel.
inop een lijst controleert elk element één voor één; op een set of dictionary is het antwoord bijna meteen, of er nu tien elementen zijn of tien miljoen. Als je steeds opnieuwif word in stop_wordscontroleert, hoortstop_wordseen set te zijn, niet een lijst. Waarom dat zo is en hoe je het verschil meet, komt later in de cursus.
Lussen: zeg waarover je loopt
Lussen over tuples en dictionaries lezen het duidelijkst met unpacking — waarden gaan direct in benoemde variabelen:
ticket_id, category, priority = ("T-1042", "refund", 3)
print(ticket_id, category, priority)
owners = {"refund": "Maya", "login": "Omar"}
for category, owner in owners.items():
print(f"{category} -> {owner}")
for position, category in enumerate(["refund", "login"], start=1):
print(position, category)T-1042 refund 3
refund -> Maya
login -> Omar
1 refund
2 login.items() geeft elke keer een paar (sleutel, waarde), en enumerate geeft elk element samen met zijn positie — je hoeft niet met range(len(...)) indexen bij te houden. Sinds Python 3.7 onthoudt een dictionary de volgorde waarin de sleutels zijn toegevoegd, dus de lus over owners begint met refund.
Wat gebeurt er als je .items() vergeet?
owners = {"refund": "Maya", "login": "Omar"}
for category, owner in owners:
print(category, owner)Traceback (most recent call last):
File "main.py", line 2, in <module>
for category, owner in owners:
^^^^^^^^^^^^^^^
ValueError: too many values to unpack (expected 2)Als je rechtstreeks over een dictionary loopt, krijg je alleen de sleutels. De eerste sleutel, "refund", is een string van zes tekens, en Python probeert die in twee variabelen te splitsen — zes tekens, twee vakjes. De melding lijkt vreemd tot je je herinnert dat een string ook een reeks is.
if/elif/else stopt bij de eerste voorwaarde die waar is, dus de volgorde van de voorwaarden is de logica:
latencies_ms = [120, 340, 95, 1800, 210, 2600]
for latency in latencies_ms:
if latency >= 2000:
label = "critical"
elif latency >= 1000:
label = "slow"
else:
label = "ok"
print(latency, label)120 ok
340 ok
95 ok
1800 slow
210 ok
2600 criticalSchrijf de voorwaarden in omgekeerde volgorde — eerst>= 1000, dan>= 2000— en er is geen fout, maar2600wordtslow, want de eerste voorwaarde is waar en de keten stopt daar. De takcriticalwordt nooit meer uitgevoerd. Als je op drempelwaarden splitst, zet dan de strengste voorwaarde bovenaan.
for loopt over een bekende verzameling. while loopt zolang een voorwaarde geldt — als je van tevoren niet weet hoe vaak het nodig is. Data uit een API ophalen, pagina voor pagina, is het klassieke voorbeeld. Hier speelt een dictionary de rol van de API:
pages = {1: ["t1", "t2"], 2: ["t3"], 3: []}
page = 1
collected = []
while True:
batch = pages[page]
if not batch:
break
collected.extend(batch)
page += 1
print(collected, "pages read:", page - 1)['t1', 't2', 't3'] pages read: 2Een lege pagina betekent break. if not batch werkt omdat een lege lijst falsy is. Schrijf bij lussen als deze eerst de stopvoorwaarde en daarna de rest — een while True die vergeet te stoppen, loopt eeuwig door, zonder enige fout.
Comprehensions: één collectie uit een andere
"Een lege lijst, een lus, een if, een append" — dit patroon van vier regels komt zo vaak voor dat Python er een vorm van één regel voor heeft:
latencies_ms = [120, 340, 95, 1800, 210, 2600]
slow = [ms for ms in latencies_ms if ms >= 1000]
seconds = [ms / 1000 for ms in latencies_ms]
labels = {ms: ("slow" if ms >= 1000 else "ok") for ms in latencies_ms}
buckets = {ms // 1000 for ms in latencies_ms}
print(slow)
print(seconds)
print(labels)
print(sorted(buckets))[1800, 2600]
[0.12, 0.34, 0.095, 1.8, 0.21, 2.6]
{120: 'ok', 340: 'ok', 95: 'ok', 1800: 'slow', 210: 'ok', 2600: 'slow'}
[0, 1, 2]Hoe lees je het: [resultaat voor element in collectie if voorwaarde]. Het linkerdeel zegt wat je van elk element maakt; de if aan het eind zegt welke je houdt. Dezelfde vorm met {sleutel: waarde for ...} maakt een dictionary, en {waarde for ...} een set — buckets bevat drie verschillende seconde-buckets, elk één keer.
Verwar de twee soorten if niet. De if ms >= 1000 aan het eind is een filter — als die onwaar is, valt het element weg. "slow" if ms >= 1000 else "ok" is een keuze — elk element blijft, en de waarde is een van de twee. Daarom bevat labels alle zes.
Een comprehension is bedoeld om een nieuwe collectie op te bouwen. Als je alleen iets wilt printen of naar een bestand wilt schrijven — als het resultaat toch weggegooid wordt — schrijf dan een gewone for-lus. En een comprehension van drie niveaus past misschien op één regel, maar een lus van vijf regels is voor een collega vaak makkelijker te lezen.
Woorden tellen: met de hand, daarna met Counter
Bijna iedereen begint met een poging om met een dictionary te tellen die er zo uitziet:
words = ["refund", "late", "refund", "login", "refund", "late"]
counts = {}
for word in words:
counts[word] += 1Traceback (most recent call last):
File "main.py", line 5, in <module>
counts[word] += 1
~~~~~~^^^^^^
KeyError: 'refund'counts[word] += 1 betekent counts[word] = counts[word] + 1 — en het lezen van counts[word] aan de rechterkant ziet dat de sleutel "refund" nog niet bestaat. Een woord dat voor de eerste keer voorkomt, heeft een startwaarde nodig. .get(sleutel, 0) levert precies die:
words = ["refund", "late", "refund", "login", "refund", "late"]
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
print(counts){'refund': 3, 'late': 2, 'login': 1}Die taak komt zo vaak voor dat de standaardbibliotheek er een type voor heeft — collections.Counter, een soort dictionary:
from collections import Counter
words = ["refund", "late", "login", "refund", "login", "late", "slow"]
counts = Counter(words)
print(counts)
print(counts.most_common(2))
print(counts["missing"])Counter({'refund': 2, 'late': 2, 'login': 2, 'slow': 1})
[('refund', 2), ('late', 2)]
0Een ontbrekende sleutel uit een Counter geeft 0, niet een KeyError — precies goed voor tellen. Maar kijk naar most_common(2): refund, late en login hebben allemaal een aantal van 2, en de eerste twee zijn teruggekomen. Waarom viel login weg? Omdat most_common bij gelijke aantallen de volgorde houdt waarin de woorden voor het eerst zijn gezien. login kwam voor het eerst als derde in de lijst, dus het verloor. Verander de volgorde van de tickets, en het resultaat verandert ook — het mysterie uit het begin van het hoofdstuk.
Tekst opschonen: welke woorden zijn echt woorden?
split() breekt alleen op witruimte:
text = "Refund not received. REFUND please! Login fails; login page is slow."
print(text.split())
print(text.lower().split())['Refund', 'not', 'received.', 'REFUND', 'please!', 'Login', 'fails;', 'login', 'page', 'is', 'slow.']
['refund', 'not', 'received.', 'refund', 'please!', 'login', 'fails;', 'login', 'page', 'is', 'slow.'].lower() loste het verschil tussen hoofdletters en kleine letters op, maar received., please! en slow. hebben nog steeds leestekens — en bij tellen zijn slow. en slow twee verschillende woorden. Hier helpt een reguliere expressie: in plaats van te zeggen wat je weggooit, zeg je hoe een woord eruitziet:
import re
text = "Refund not received. REFUND please! Login fails; login page is slow. Can't pay."
words = re.findall(r"[a-z']+", text.lower())
print(words)['refund', 'not', 'received', 'refund', 'please', 'login', 'fails', 'login', 'page', 'is', 'slow', "can't", 'pay'][a-z']+ betekent "een of meer kleine Engelse letters of apostrofs". re.findall geeft elke plek terug waar het patroon in de tekst past, en al het andere — punten, komma's, cijfers — valt weg. can't blijft één woord.
Dit patroon heeft een gat: als iemand'invalid token'schrijft, komt het eerste woord eruit als'invalid— met het aanhalingsteken eraan vast. Het volgende voorbeeld maakt het patroon strakker, zodat een apostrof alleen tussen twee letters kan staan. En onthoud dat[a-z]alleen Engelse letters zijn; tekst in andere schriften heeft een ander patroon nodig.
Gelijke aantallen: bepaal zelf de sorteerregel
Om aantallen om te zetten in een ranglijst moet je sorteren. Drie manieren, op een dictionary waarin refund en login allebei een aantal van 3 hebben:
counts = {"refund": 3, "late": 2, "login": 3, "slow": 1}
print(sorted(counts, reverse=True))
print(sorted(counts.items(), key=lambda item: item[1], reverse=True))
print(sorted(counts.items(), key=lambda item: (-item[1], item[0])))['slow', 'refund', 'login', 'late']
[('refund', 3), ('login', 3), ('late', 2), ('slow', 1)]
[('login', 3), ('refund', 3), ('late', 2), ('slow', 1)]De eerste regel is het gevaarlijkst, omdat hij redelijk lijkt. Het sorteren van een dictionary sorteert de sleutels, niet de waarden — dit is dus omgekeerde alfabetische volgorde en heeft niets met de aantallen te maken. slow kwam één keer voor, maar staat toch bovenaan. Geen fout.
De tweede regel sorteert op aantal. Maar welke komt eerst, refund of login? Pythons sorted is "stabiel" — elementen met gelijke sleutels behouden de volgorde die ze in de invoer hadden. refund is eerder in de dictionary gezet, dus die komt eerst. Met andere woorden: het resultaat hangt af van de volgorde waarin de data binnenkwam.
De derde regel benoemt de regel zelf. key geeft een tuple terug, en tuples worden element voor element vergeleken: eerst -item[1] — het negatieve aantal, zodat grotere aantallen eerst komen — en bij een gelijkspel item[0], het woord zelf, alfabetisch. Nu is het resultaat hetzelfde, welke volgorde de invoer ook heeft. reverse=True is niet nodig, want het minteken keert de volgorde van het aantal al om, terwijl de alfabetische volgorde blijft zoals die is.
Waar resultaten vergeleken gaan worden — rapporten, tests, evaluaties — geef een expliciete regel voor gelijke waarden. Anders verschillen het rapport van vandaag en dat van morgen, ook als de data zelf niet is veranderd.
Een compleet voorbeeld
Nu alles samen: de top vijf woorden in de tickets. Stopwoorden — woorden als the en and die in elke tekst voorkomen maar niets zeggen — vallen weg, woorden van twee letters vallen weg, en gelijkspel wordt alfabetisch beslist. top_words.py:
import re
from collections import Counter
STOP_WORDS = {
"a", "and", "for", "i", "is", "it", "my", "not", "of", "on", "the", "to", "was", "with",
}
TICKETS = [
"Refund not received for order 1182. I was charged twice!",
"The app is slow and the login page freezes.",
"Charged twice again. Please refund the second charge.",
"Login fails with 'invalid token' on my phone.",
"Still waiting on my refund, it was promised last week.",
"App slow after the update; login takes a minute.",
]
def tokenize(text: str) -> list[str]:
return re.findall(r"[a-z]+(?:'[a-z]+)?", text.lower())
def top_words(texts: list[str], n: int = 5) -> list[tuple[str, int]]:
counts = Counter(
word
for text in texts
for word in tokenize(text)
if word not in STOP_WORDS and len(word) > 2
)
ranked = sorted(counts.items(), key=lambda item: (-item[1], item[0]))
return ranked[:n]
def print_ranking(ranking: list[tuple[str, int]]) -> None:
if not ranking:
print("no words to rank")
return
width = max(len(word) for word, _ in ranking)
for rank, (word, count) in enumerate(ranking, start=1):
print(f"{rank}. {word:<{width}} {count} {'#' * count}")
if __name__ == "__main__":
print_ranking(top_words(TICKETS))
print_ranking(top_words([]))uv run top_words.py1. login 3 ###
2. refund 3 ###
3. app 2 ##
4. charged 2 ##
5. slow 2 ##
no words to rankDe keuzes achter dit voorbeeld:
Het woordpatroon [a-z]+(?:'[a-z]+)?. Een of meer letters, daarna optioneel een apostrof en nog meer letters. can't blijft één woord, maar het aanhalingsteken rond 'invalid token' wordt niet meer onderdeel van een woord. (?:...) is een groep die niet wordt vastgelegd — met een gewone (...) zou re.findall alleen het deel van de groep teruggeven in plaats van het hele woord.
Stopwoorden zijn een set. Elk woord wordt gecontroleerd met word not in STOP_WORDS; op een set is die controle bijna direct, hoe lang de lijst ook wordt.
Counter wordt rechtstreeks gevoed door een generator. Twee fors en een if vormen één stroom — elk woord van elk ticket dat het filter passeert. Er wordt geen grote tussenlijst met alle woorden gebouwd. De twee fors staan in dezelfde volgorde als gewone geneste lussen: de buitenste eerst.
Een expliciete tie-break. login en refund hebben allebei 3; app, charged, slow en twice hebben allemaal 2. Alfabetische volgorde bepaalde wie de vijfde plek kreeg, dus twice viel weg. De regel is niet perfect — twice viel niet weg omdat het minder belangrijk was — maar hij is vastgelegd en hij is elke keer hetzelfde. Als iemand die het rapport leest ernaar vraagt, is er een antwoord.
Lege invoer bewust afgehandeld. max() op een lege verzameling geeft ValueError: max() iterable argument is empty. Een rustige week zonder tickets is normaal, geen fout — dus print_ranking controleert eerst op een lege ranglijst. De tweede uitvoerregel laat precies dat zien.
Tellen en tonen apart gehouden. top_words geeft een lijst terug en print niets; print_ranking print alleen. Zo kunnen dezelfde aantallen vanuit een test, een API of een dashboard gebruikt worden. {word:<{width}} lijnt elk woord links uit in een ruimte even breed als het langste woord, zodat de kolom met aantallen netjes uitlijnt.
Eén beperking: charged en charge zijn hier twee aparte woorden. Het samenvoegen van de vormen van een woord is een eigen probleem, en AI-systemen zoeken meestal op betekenis in plaats van op het tellen van woorden — die aanpak komt later in de cursus. Maar ook daar zijn het opschonen van tekst, filteren en sorteren in een stabiele volgorde — het werk van dit hoofdstuk — de eerste stappen.
Wanneer het breekt
KeyError: 'refund' counts[word] += 1 voor een sleutel die nog niet in de dictionary staat. Schrijf counts[word] = counts.get(word, 0) + 1, of gebruik collections.Counter.
ValueError: too many values to unpack (expected 2) De code zegt for key, value in d: over een dictionary. Als je rechtstreeks over een dictionary loopt, krijg je alleen sleutels; schrijf d.items() om paren te krijgen.
TypeError: unhashable type: 'list' Een lijst is in een set gestopt of als dictionarysleutel gebruikt. Elementen van een set en sleutels van een dictionary moeten dingen zijn die niet kunnen veranderen. Gebruik in plaats van een lijst een tuple: seen.add(("refund", "late")).
RuntimeError: dictionary changed size during iteration Er zijn sleutels verwijderd uit of toegevoegd aan een dictionary terwijl je erover loopt. Bouw een nieuwe dictionary in plaats van te verwijderen: counts = {word: n for word, n in counts.items() if n >= 2}.
AttributeError: 'tuple' object has no attribute 'append' Een tuple kan na het maken niet meer veranderd worden. Als de waarden echt groeien en krimpen, had het een lijst moeten zijn; als de tuple een record is, bouw je een nieuwe tuple met de nieuwe waarde.
ValueError: max() iterable argument is empty max() of min() op een lege verzameling. Controleer eerst of hij leeg is, of geef een standaardwaarde: max(lengths, default=0).
Stap 4 van 6 — Voorspellen
Test je begrip
Een collega pakt "het meest voorkomende woord" uit een verzameling aantallen. Wat wordt geprint?
counts = {"refund": 5, "slow": 1, "login": 3}
print(sorted(counts, reverse=True)[0])- A`refund`
- B`('refund', 5)`
- C`slow`
- D`5`
label(2600) zou "critical" moeten teruggeven, maar het geeft "slow" terug — zonder fout. Waar zit de bug?
def label(latency_ms):
if latency_ms >= 1000:
return "slow"
elif latency_ms >= 2000:
return "critical"
return "ok"
print(label(2600))- ADe volgorde van de voorwaarden: `>= 1000` is eerst waar en de keten stopt, dus `>= 2000` wordt nooit bereikt — de strengste voorwaarde moet eerst komen
- B`return "ok"` heeft een `else:` nodig ervoor
- CDe tweede voorwaarde moet `if` gebruiken in plaats van `elif`
- D`2600` is een geheel getal, dus het moet met `float()` worden omgezet voordat het met `>=` wordt vergeleken
Een pipeline controleert voor elk van een miljoen woorden of het een van 300 stopwoorden is. In welke container moeten de stopwoorden staan?
- AEen string, met alle woorden samengevoegd tot één lange tekst
- BEen tuple, omdat stopwoorden niet veranderen
- CEen set, omdat een `in`-controle bijna direct is, hoe groot die ook is
- DEen lijst, omdat die alfabetisch gesorteerd kan blijven
Spelen hoort bij Pro
+30 XP wacht op je
Speel dit hoofdstuk met Pro
Lezen is gratis en het hele hoofdstuk is open. De eerste 5 hoofdstukken van elke gratis cursus speel je gratis. Vanaf hoofdstuk 6 horen vragen beantwoorden, XP verdienen en je reeks laten groeien bij Pro.
Ontgrendel spelen met ProJouw beurt
Zoek nu de topwoorden per categorie. Kopieer in een bestand met de naam categories.py de STOP_WORDS en tokenize van het hoofdstuk, en voeg deze tickets toe:
TICKETS = [
("billing", "Charged twice for one order."),
("billing", "Refund for the double charge, please."),
("login", "Login fails on the new phone."),
("billing", "Still no refund after the charge."),
("login", "Password reset email never arrives."),
("login", "Login page shows a blank screen."),
]Schrijf daarna code die:
- Elke categorie haar eigen aantallen heeft — een dictionary waarvan de sleutels categorienamen zijn en waarvan de waarden de
Countervan die categorie zijn (hint:dict.setdefaultenCounter.update) - Stopwoorden en woorden van twee letters of minder niet worden geteld
- Elke categorie de top twee woorden toont, met hogere aantallen eerst en alfabetische volgorde bij een gelijkspel
- Categorieën worden in alfabetische volgorde geprint, ongeacht de volgorde van de tickets
uv run categories.py moet precies dit printen:
billing charge=2, refund=2
login login=2, arrives=1Keer daarna de lijst TICKETS om (TICKETS[::-1]) en draai het opnieuw — verandert de uitvoer? Zo ja, welke voorwaarde is dan niet vervuld? Verwijder tot slot de tie-break en sorteer alleen op aantal: wat is het tweede woord voor login, en blijft het hetzelfde als je de volgorde omkeert?
Stap 6 van 6
Uitdaging — de hoofdstukquiz
Tien vragen van makkelijk naar moeilijk. De laatste zijn met opzet lastig.
Speel met Pro