Dict en set comprehensions¶
In het eerste college van deze week verzamelde je een lijst in één uitdrukking. Met andere haken verzamel je op dezelfde manier een set of een dictionary, de datatypes uit week 1. Daarna bouw je een lijst van lijsten, en zie je wanneer een lus de betere keuze blijft.
Een set verzamelen¶
words = ["de", "vuurtoren", "van", "ameland", "draait"]
{word[0] for word in words}
{'a', 'd', 'v'}
Met accolades in plaats van vierkante haken wordt het resultaat een set: een
set comprehension. Verder zijn de delen dezelfde als bij een list
comprehension. En zoals in elke set komen dubbele waarden er maar één keer in:
"de" en "draait" leveren samen één "d" op. Een set heeft geen volgorde,
dus de letters kunnen bij jou in een andere volgorde staan.
Een dictionary verzamelen¶
{word: len(word) for word in words}
{'de': 2, 'vuurtoren': 9, 'van': 3, 'ameland': 7, 'draait': 6}
Staat er bij wat erin komt een paar sleutel: waarde, dan wordt het resultaat
een dictionary: een dict comprehension. Hier is elk woord de sleutel en zijn
lengte de waarde.
Een dict comprehension kan ook een dictionary langslopen, met .items() zoals
in week 1. Deze houdt uit een telling alleen de woorden over die vaker dan één
keer voorkomen:
counts = {"de": 3, "vuurtoren": 2, "van": 1, "licht": 1}
{word: count for word, count in counts.items() if count > 1}
{'de': 3, 'vuurtoren': 2}
De drie vormen naast elkaar. Samen heten ze comprehensions:
Haken |
Wat erin komt |
Resultaat |
|---|---|---|
|
een waarde |
lijst |
|
een waarde |
set |
|
een paar |
dictionary |
Let op: {} zonder iets erin is een lege dictionary, geen lege set. Dat kende
je al uit week 1.
Tellen met sum¶
Een getal is geen verzameling, maar een telling kan toch in één uitdrukking.
Hoeveel woorden hebben meer dan drie letters? Maak een lijst met een 1 voor elk
woord dat meetelt, en tel die lijst op met sum:
sum([1 for word in words if len(word) > 3])
3
Op dezelfde manier is dit het totale aantal letters:
sum([len(word) for word in words])
27
Een lijst van lijsten¶
Wat erin komt, mag zelf een lijst zijn. Een raster van drie breed en twee hoog, gevuld met punten:
[["."] * 3 for _ in range(2)]
[['.', '.', '.'], ['.', '.', '.']]
Het rijnummer heb je hier niet nodig, dus heet het _.
Wat erin komt, mag ook zelf een comprehension zijn. Dat heet een geneste comprehension. Dit is de tafel van vermenigvuldiging tot en met 4:
n = 4
[[x * y for x in range(1, n + 1)] for y in range(1, n + 1)]
[[1, 2, 3, 4], [2, 4, 6, 8], [3, 6, 9, 12], [4, 8, 12, 16]]
Lees hem van buiten naar binnen. De buitenste comprehension loopt de rijen y
langs. Voor elke rij komt er één lijst in, en die lijst is de binnenste
comprehension, over de kolommen x. Het is dezelfde geneste lus als in week 5
van Programmeren 1:
table = []
for y in range(1, n + 1):
row = []
for x in range(1, n + 1):
row.append(x * y)
table.append(row)
table
[[1, 2, 3, 4], [2, 4, 6, 8], [3, 6, 9, 12], [4, 8, 12, 16]]
Je kunt de vormen ook combineren. Deze dict comprehension koppelt elk woord aan
de lijst van posities waarop het staat. De waarde is dus een list comprehension
met enumerate:
sentence = ["de", "kat", "en", "de", "hond"]
{word: [i for i, w in enumerate(sentence) if w == word] for word in set(sentence)}
{'hond': [4], 'de': [0, 3], 'en': [2], 'kat': [1]}
De woorden komen uit een set, en een set heeft geen volgorde. De sleutels van deze dictionary kunnen bij jou dus in een andere volgorde staan.
Wanneer je een lus houdt¶
Een comprehension bouwt een nieuwe lijst, set of dictionary op, en per element komt er hoogstens één ding bij. Past je probleem daar niet in, dan is een lus de betere keuze. Vier gevallen.
Per stap iets bijwerken¶
In week 1 telde je hoe vaak elk woord voorkomt. Per stap tel je bij één woord
één op. Als dict comprehension lukt dat ook, met de lijstmethode count, die
telt hoe vaak een waarde in een lijst staat:
words = ["spam", "spam", "taart", "spam"]
{word: words.count(word) for word in set(words)}
{'taart': 1, 'spam': 3}
De uitkomst klopt; alleen de volgorde van de sleutels kan bij jou anders zijn,
omdat ze uit een set komen. Maar er gebeurt iets anders dan je ziet. Voor elk verschillend
woord loopt words.count(word) de hele lijst opnieuw langs. De lus uit week 1
loopt de lijst één keer langs en werkt per stap één telling bij. Bij een tekst
van duizenden woorden merk je dat verschil. Een lus die per stap iets bijwerkt
wat er al was, blijft een lus.
Iets onthouden tussen de stappen¶
most_frequent uit de basisopgave van week 1 onthoudt per stap twee dingen: het
beste woord tot nu toe, en hoe vaak dat voorkwam. Een comprehension kan niets
onthouden tussen de stappen. Hij kan alleen per element iets aan het resultaat
toevoegen. Het beste tot nu toe zoek je dus met een lus.
Iets doen in plaats van iets verzamelen¶
Wil je elk woord afdrukken, dan heb je geen nieuwe lijst nodig. Een list
comprehension met print erin werkt wel, maar hij maakt een lijst vol None
die niemand gebruikt. Schrijf dan een lus.
Leesbaarheid¶
Een comprehension is korter, maar niet altijd beter te lezen. Deze vervangt in
de woorden van meer dan drie letters elke klinker door een *:
words = ["de", "vuurtoren", "van", "ameland"]
[[c if c not in "aeiou" else "*" for c in word] for word in words if len(word) > 3]
[['v', '*', '*', 'r', 't', '*', 'r', '*', 'n'],
['*', 'm', '*', 'l', '*', 'n', 'd']]
Hetzelfde met een lus:
result = []
for word in words:
if len(word) > 3:
row = []
for c in word:
if c in "aeiou":
row.append("*")
else:
row.append(c)
result.append(row)
result
[['v', '*', '*', 'r', 't', '*', 'r', '*', 'n'],
['*', 'm', '*', 'l', '*', 'n', 'd']]
Allebei goed, en ze geven dezelfde lijst. Maar de comprehension moet je twee
keer lezen: welke if laat woorden weg, en welke kiest een teken? In de lus doet
elke regel één ding. Moet je een comprehension twee keer lezen voordat je ziet
wat hij doet, schrijf dan de lus.
Samengevat:
Kies een comprehension als |
Kies een lus als |
|---|---|
je een nieuwe lijst, set of dictionary opbouwt |
je per stap iets bijwerkt wat er al was |
er per element hoogstens één ding bij komt |
je iets onthoudt tussen de stappen |
hij in één keer leest |
je per stap iets doet, zoals afdrukken |
Opdrachten¶
Opdracht 1¶
Voorspel eerst voor elke regel in de cel hieronder wat er wordt afgedrukt:
Regel |
Jouw voorspelling |
|---|---|
1 |
|
2 |
|
3 |
|
4 |
Voer daarna de cel uit en vergelijk de uitvoer met je voorspelling. Bij de set kan de volgorde bij jou anders zijn.
print({len(w) for w in ["de", "kat", "en", "hond"]})
print({w: w[0] for w in ["ja", "nee"]})
print([[x + y for x in range(3)] for y in range(2)])
print(sum([1 for c in "vuurtoren" if c == "r"]))
Opdracht 2¶
In week 1 zag je de dictionary zodiac_years, met de jaren per dier van de
Chinese dierenriem. Maak met een dict comprehension een dictionary
first_year, die elk dier koppelt aan het eerste jaar in zijn lijst.
Dier |
Eerste jaar |
|---|---|
|
|
|
|
|
|
zodiac_years = {
"rabbit": [1999, 1987, 1975],
"ox": [1997, 1985, 1973],
"dragon": [2000, 1988],
}
# jouw oplossing
assert first_year == {"rabbit": 1999, "ox": 1997, "dragon": 2000}
Opdracht 3¶
Hieronder staan drie vragen over de lijst words. Beslis per vraag of je hem
met een comprehension beantwoordt of met een lus, en schrijf de code.
Wat is de lengte van elk woord, als dictionary van woord naar lengte? Noem het resultaat
lengths.Welke woorden hebben meer dan vier letters, zonder dubbele? Noem het resultaat
long_words.Wat is het langste woord? Bij gelijke lengte telt het eerste. Noem het resultaat
longest.
Zet bij elke vraag in een commentaarregel waarom je deze vorm kiest. Kijk je keuze na met de tekst onder de testcel.
words = ["de", "toren", "draait", "de", "lamp", "draait"]
# jouw oplossing
assert lengths == {"de": 2, "toren": 5, "draait": 6, "lamp": 4}
assert long_words == {"toren", "draait"}
assert longest == "draait"
Je keuze klopt als je bij 1 en 2 een comprehension kiest, omdat je een nieuwe verzameling opbouwt, en bij 3 een lus, omdat je het langste woord tot nu toe moet onthouden.
Tot slot¶
In het werkcollege gebruik je deze vormen samen. Je maakt een register bij een tekst: bij elk woord de regels waarop het staat.