List comprehensions

Deze pagina bevat uitvoerbare code.

Een lus die een nieuwe lijst verzamelt

In week 4 van Programmeren 1 schreef je lussen met het lusrecept. Veel van die lussen doen hetzelfde: ze lopen een reeks langs en verzamelen een nieuwe lijst. Deze functie houdt de even getallen uit een lijst over:

def only_evens(L):
    """Geeft een lijst met de even getallen uit L."""
    result = []

    for x in L:
        if x % 2 == 0:
            result.append(x)

    return result


assert only_evens([0, 1, 2, 3, 4]) == [0, 2, 4]
assert only_evens([]) == []

Met het lusrecept erbij:

Vraag

Bij only_evens

1. Wat verzamel je, en wat is de startwaarde?

een nieuwe lijst result, startwaarde []

2. Wat loop je langs?

de elementen van L

3. Wat gebeurt er per stap met wat je verzamelt?

is x even, dan komt x erbij

4. Wanneer is het klaar?

als L op is

5. Wat geef je terug, en waar staat die regel?

result, na de lus

Bij zo’n lus zijn vraag 1, 4 en 5 altijd hetzelfde. Je begint met een lege lijst, de for stopt vanzelf als de reeks op is, en na de lus geef je de lijst terug. Alleen vraag 2 en 3 zeggen iets over dít probleem. Voor precies dit recept heeft Python een kortere schrijfwijze.

De list comprehension

def only_evens(L):
    """Geeft een lijst met de even getallen uit L."""
    return [x for x in L if x % 2 == 0]


assert only_evens([0, 1, 2, 3, 4]) == [0, 2, 4]
assert only_evens([]) == []

Dit heet een list comprehension: een lijst die je in één uitdrukking opbouwt uit een andere reeks. De term is Engels en blijft dat.

Lees hem in drie delen:

Deel

Bij only_evens

In het lusrecept

wat er in de nieuwe lijst komt

x

vraag 3: wat erbij komt

wat je langsloopt

for x in L

vraag 2

welke elementen meedoen

if x % 2 == 0

vraag 3: óf het erbij komt

De vierkante haken zeggen dat het resultaat een lijst is. De lege startlijst, de append en de return na de lus staan er niet meer: die doet de list comprehension voor je. Het derde deel heet ook wel het filter. Als schema:

[ wat erin komt   for element in reeks   if conditie ]

Beide versies van only_evens zijn goed en geven dezelfde lijst terug. Wanneer je beter de lus houdt, zie je in het tweede college van deze week.

Zonder filter

Het filter mag weg. Dan komt er voor elk element iets in de nieuwe lijst. Wat erin komt hoeft niet het element zelf te zijn; het mag ook iets zijn wat je ermee uitrekent. De lengte van elk woord:

words = ["de", "vuurtoren", "draait"]
[len(word) for word in words]
[2, 9, 6]

Langs range en langs een string

Wat je langsloopt, mag elke reeks zijn waar een for langs kan. range levert getallen. De eerste vijf veelvouden van 5:

[x * 5 for x in range(1, 6)]
[5, 10, 15, 20, 25]

Een string levert tekens. De klinkers in een woord:

[c for c in "vuurtoren" if c in "aeiou"]
['u', 'u', 'o', 'e']

Soms heb je het element zelf niet nodig, alleen het aantal keer. Noem het dan _, zoals in week 4 van Programmeren 1: de naam voor een variabele die je bewust niet gebruikt. Een lijst met vier nullen:

[0 for _ in range(4)]
[0, 0, 0, 0]

Positie en element tegelijk: enumerate

In week 4 van Programmeren 1 liep je soms langs de index in plaats van langs de elementen, met range(len(...)):

words = ["spam", "taart", "spam"]

for ix in range(len(words)):
    print(ix, words[ix])
0 spam
1 taart
2 spam

Vaak heb je allebei nodig: de positie en het element. enumerate geeft je ze tegelijk, als paren:

list(enumerate(words))
[(0, 'spam'), (1, 'taart'), (2, 'spam')]

Zo’n paar pak je uit zoals de paren van .items() in week 1. words[ix] hoeft dan niet meer, want het element komt al mee:

for i, word in enumerate(words):
    print(i, word)
0 spam
1 taart
2 spam

In een list comprehension werkt het net zo. Op welke posities staat "spam"?

[i for i, word in enumerate(words) if word == "spam"]
[0, 2]

enumerate begint bij 0 te tellen, net als een index. Wil je bij 1 beginnen, bijvoorbeeld voor regelnummers, geef dan 1 mee als tweede argument:

list(enumerate(["eerste", "tweede"], 1))
[(1, 'eerste'), (2, 'tweede')]

Twee lijsten naast elkaar: zip

Soms horen twee lijsten bij elkaar, element voor element. Hier de namen van drie spelers en hun scores:

names = ["Anne", "Bram", "Cas"]
scores = [7, 9, 6]

zip legt ze naast elkaar, zoals een rits: het eerste element van de ene lijst bij het eerste van de andere, het tweede bij het tweede, enzovoort. Ook dat levert paren op:

list(zip(names, scores))
[('Anne', 7), ('Bram', 9), ('Cas', 6)]

Die paren pak je weer uit, in een lus of in een list comprehension:

for name, score in zip(names, scores):
    print(name, score)
Anne 7
Bram 9
Cas 6
[[name, score] for name, score in zip(names, scores)]
[['Anne', 7], ['Bram', 9], ['Cas', 6]]

Zonder zip had je range(len(names)) nodig en twee keer indexeren: [[names[i], scores[i]] for i in range(len(names))]. Met zip zie je meteen wat bij elkaar hoort. Zijn de lijsten niet even lang, dan stopt zip bij de kortste.

Per element een keuze: a if c else b

Soms wil je voor elk element een van twee waarden in de nieuwe lijst, afhankelijk van een conditie. Met een if-statement schrijf je dat zo:

temperature = -3

if temperature > 0:
    label = "dooi"
else:
    label = "vorst"

label
'vorst'

Python heeft er ook een uitdrukking voor, de conditionele expressie:

label = "dooi" if temperature > 0 else "vorst"
label
'vorst'

Lees hem als: "dooi" als temperature > 0, en anders "vorst". Het deel met else is verplicht, want een uitdrukking levert altijd een waarde op.

Omdat het een uitdrukking is, past hij op de plek van wat erin komt:

temperatures = [-3, 4, 0, 12]
["dooi" if t > 0 else "vorst" for t in temperatures]
['vorst', 'dooi', 'vorst', 'dooi']

Er kan nu op twee plekken een if staan, en die doen iets anders:

Voorbeeld

Wat het doet

Nieuwe lijst

filter, achteraan

[t for t in temperatures if t > 0]

laat elementen weg

[4, 12], korter

conditionele expressie, vooraan

["dooi" if t > 0 else "vorst" for t in temperatures]

kiest per element een waarde

even lang als temperatures

Een filter heeft geen else, een conditionele expressie wel.

Opdrachten

Opdracht 1

Voorspel eerst voor elke regel in de cel hieronder welke lijst er wordt afgedrukt:

Regel

Jouw voorspelling

1

2

3

4

Voer daarna de cel uit en vergelijk de uitvoer met je voorspelling.

print([x * x for x in range(4)])
print([word for word in ["de", "vuurtoren", "is", "hoog"] if len(word) > 2])
print([i for i, c in enumerate("banaan") if c == "a"])
print(["kort" if len(w) < 4 else "lang" for w in ["de", "toren", "is"]])

Opdracht 2

Deze lus zoekt de posities van de woorden die niet "spam" zijn:

words = ["spam", "taart", "spam", "thee"]
positions = []

for ix in range(len(words)):
    if words[ix] != "spam":
        positions.append(ix)

Maak dezelfde lijst positions met een list comprehension en enumerate.

words = ["spam", "taart", "spam", "thee"]

# jouw oplossing
assert positions == [1, 3]

Opdracht 3

Nu andersom. Deze list comprehension geeft de spelers met een score onder de 8 een punt erbij:

names = ["Anne", "Bram", "Cas"]
scores = [7, 9, 6]
bonus = [[name, score + 1] for name, score in zip(names, scores) if score < 8]

Maak dezelfde lijst met een lus, in een variabele result. Beantwoord eerst de vragen van het lusrecept.

names = ["Anne", "Bram", "Cas"]
scores = [7, 9, 6]

# jouw oplossing
assert result == [["Anne", 8], ["Cas", 7]]

Opdracht 4

Maak met een list comprehension een lijst results waarin voor elk cijfer in grades het woord "voldoende" staat als het cijfer minstens 5.5 is, en anders "onvoldoende".

grades = [7.5, 4.0, 5.5, 3.2]

# jouw oplossing
assert results == ["voldoende", "onvoldoende", "voldoende", "onvoldoende"]

Tot slot

Een list comprehension verzamelt een lijst. In het tweede college van deze week verzamel je op dezelfde manier een set, een dictionary en een lijst van lijsten, en zie je wanneer je beter de lus houdt.