Dict en set comprehensions

Deze pagina bevat uitvoerbare code.

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 sleutel: waarde

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

"rabbit"

1999

"ox"

1997

"dragon"

2000

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.

  1. Wat is de lengte van elk woord, als dictionary van woord naar lengte? Noem het resultaat lengths.

  2. Welke woorden hebben meer dan vier letters, zonder dubbele? Noem het resultaat long_words.

  3. 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.