List comprehensions¶
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 |
|---|---|
1. Wat verzamel je, en wat is de startwaarde? |
een nieuwe lijst |
2. Wat loop je langs? |
de elementen van |
3. Wat gebeurt er per stap met wat je verzamelt? |
is |
4. Wanneer is het klaar? |
als |
5. Wat geef je terug, en waar staat die regel? |
|
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 |
In het lusrecept |
|---|---|---|
wat er in de nieuwe lijst komt |
|
vraag 3: wat erbij komt |
wat je langsloopt |
|
vraag 2 |
welke elementen meedoen |
|
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 |
|
laat elementen weg |
|
conditionele expressie, vooraan |
|
kiest per element een waarde |
even lang als |
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.