Comprehensions — een lus op één regel
Lijst-, dictionary- en setcomprehensions, het verschil tussen een filterend if en een kiend if/else, en wanneer een comprehension de verkeerde keuze is.
Hoe wil je leren?
Het probleem dat we oplossen
Eén vorm van code is de afgelopen zes hoofdstukken herhaaldelijk teruggekomen:
marks = [72, 45, 90, 33]
doubled = []
for mark in marks:
doubled.append(mark * 2)
print(doubled)
doubled = [mark * 2 for mark in marks]
print(doubled)[144, 90, 180, 66]
[144, 90, 180, 66]De vier regels hierboven en de ene regel hieronder doen precies hetzelfde.
Drie van die vier regels zijn niet het werk, ze zijn de boekhouding — een lege lijst maken, over de bron lopen, elke keer toevoegen. De enige echte keuze is mark * 2. Alles anders is elke keer hetzelfde.
Een comprehension haalt die herhaling weg. Het is geen nieuwe mogelijkheid — het is wat je al kon schrijven, in minder woorden. En dat is ook waar de limiet vandaan komt, en dat is het onderwerp van de laatste sectie van dit hoofdstuk: verkort wat duidelijker wordt als het korter is, niet alles.
Aan het einde van dit hoofdstuk kun je
- Een list comprehension schrijven en lezen
- Filteren met
if, en kiezen metif/else - Dictionary comprehensions en set comprehensions schrijven
- Zeggen wanneer een comprehension de verkeerde keuze is
- Een onleesbare comprehension ontbinden in een lus
Voorkennis: Geneste data — een lijst van dictionaries.
De vorm
[ result for item in source ]Hier is result wat je wilt bouwen, item is één ding elke ronde, en source is de lijst (of andere collectie) waar de dingen vandaan komen.
Hardop gelezen is het een gewone zin: "`mark 2 voor elke mark in marks`"*.
Een manier om het te onthouden: lees het for-deel eerst, dan het begin. Weten waar dingen vandaan komen maakt duidelijk wat er gebouwd wordt.
Filteren
Een if aan het einde houdt alleen de dingen die voldoen:
marks = [72, 45, 90, 33]
passed = [mark for mark in marks if mark >= 40]
print(passed)[72, 45, 90]Hier is het eerste deel gewoon mark — geen berekening, zoals het is. Filteren en transformeren kan tegelijk:
names = [" rafi ", "AHMED", " dia"]
clean = [name.strip().lower() for name in names if name.strip()]
print(clean)['rafi', 'ahmed', 'dia']De voorwaarde is name.strip() zonder enige vergelijking. Herinner hoofdstuk acht zijn truthiness-regel: een lege string is false. Dus iemand die alleen spaties intypte, wordt weggelaten.
if aan het einde versus if aan het begin
Twee verschillende dingen, en de positie is het verschil:
marks = [72, 33, 90]
labels = ["pass" if m >= 40 else "fail" for m in marks]
print(labels)['pass', 'fail', 'pass']Een if aan het einde filtert — sommige dingen bereiken het resultaat nooit, dus het resultaat kan korter zijn.
Een if ... else aan het begin kiest — elk ding levert iets op, dus de lengte blijft onveranderd. Het is de eenregelige voorwaarde uit hoofdstuk vijftien, in een comprehension.
Verandert de lengte? Die vraag is de makkelijkste manier om de twee te onderscheiden.
Dictionaries en sets ook
Dezelfde vorm, andere haakjes:
marks = {"rafi": 72, "ahmed": 45, "bilal": 90}
passed = {name: mark for name, mark in marks.items() if mark >= 50}
print(passed)
doubled = {name: mark * 2 for name, mark in marks.items()}
print(doubled){'rafi': 72, 'bilal': 90}
{'rafi': 144, 'ahmed': 90, 'bilal': 180}Accolades en een dubbele punt maken een dictionary comprehension. marks.items() levert elke ronde een tuple en name, mark pakt hem uit, precies zoals in een lus.
Dezelfde accolades zonder dubbele punt maken een set:
orders = [
{"item": "pen"},
{"item": "bag"},
{"item": "pen"},
]
items = {order["item"] for order in orders}
print(sorted(items))['bag', 'pen']De "hoeveel verschillende"-vraag uit hoofdstuk zestien in één regel. pen komt twee keer voor en blijft één keer.
Getallen omzetten naar strings — wat je al gezien hebt
Deze regel verscheen in hoofdstuk dertien met de belofte dat de uitleg zou komen:
marks = [72, 45, 90]
print(", ".join(str(m) for m in marks))
print([str(m) for m in marks])72, 45, 90
['72', '45', '90']Nu kan het volledig gelezen worden. str(m) for m in marks is een comprehension zonder de vierkante haakjes — die vorm heet een generator expression, en in plaats van de hele lijst ineens te bouwen, geeft hij join één item per keer. De haakjes mogen weg wanneer het het enig argument van een functie is, en bij grote data bespaart het geheugen.
Wanneer je er geen hoeft te schrijven
Dat je een comprehension kunt schrijven, betekent niet dat je dat ook moet doen. Kijk hier:
rows = [["1", "2"], ["3", "x"]]
numbers = [int(c) for row in rows for c in row if c.isdigit()]
print(numbers)[1, 2, 3]De regel klopt. Maar het heeft twee fors en een if, en je moet de volgorde onthouden: de fors lezen van buitenaf naar binnenaf, zoals geneste lussen geschreven zouden worden. Zes maanden later is die regel werk om te lezen.
Als een lus:
rows = [["1", "2"], ["3", "x"]]
numbers = []
for row in rows:
for c in row:
if c.isdigit():
numbers.append(int(c))
print(numbers)[1, 2, 3]Meer regels, minder nadenken. Het doel van code is niet kort te zijn maar duidelijk te zijn — en meestal maakt inkorten het duidelijker, maar niet altijd.
Als werkregel: één for en hoogstens één if — een comprehension. Meer dan dat — een lus. En zet nooit een print of een append in een comprehension; het is bedoeld om een waarde te bouwen, niet om werk te doen.
Een compleet voorbeeld
views.py:
# One list of records in, three different views out
orders = [
{"customer": "rafi", "item": "pen", "quantity": 3, "price": 15.0},
{"customer": "ahmed", "item": "bag", "quantity": 1, "price": 850.0},
{"customer": "rafi", "item": "ink", "quantity": 2, "price": 120.0},
{"customer": "dia", "item": "pen", "quantity": 5, "price": 15.0},
]
totals = [order["quantity"] * order["price"] for order in orders]
print("Totals :", totals)
big = [order["item"] for order in orders if order["quantity"] * order["price"] > 100]
print("Over 100 :", big)
items = {order["item"] for order in orders}
print("Distinct :", sorted(items))
by_item = {order["item"]: order["price"] for order in orders}
print("Price list:", by_item)
print("Grand :", sum(totals))Totals : [45.0, 850.0, 240.0, 75.0]
Over 100 : ['bag', 'ink']
Distinct : ['bag', 'ink', 'pen']
Price list: {'pen': 15.0, 'bag': 850.0, 'ink': 120.0}
Grand : 1210.0Deze data is dezelfde als hoofdstuk zeventien, met het meeste wat daar een lus was nu één regel.
Drie dingen die opvallen.
totals en sum(totals) zijn losse regels. Het totaal had sum(order["quantity"] * order["price"] for order in orders) kunnen zijn, maar dan bestonden de individuele regelwaarden niet. Een tussenvariabele is vaak meer waard dan een one-liner.
Dezelfde berekening verschijnt tweemaal in big — één keer in de voorwaarde, en hij zou weer nodig zijn in het resultaat. Hier pakt het resultaat alleen de naam, dus is niets verloren; maar wanneer je beide nodig hebt, is dat een goed reden om de comprehension laten staan en een lus te schrijven, waar de waarde één keer berekend kan worden en een naam krijgt.
by_item verloor een record. Vier bestellingen, drie paren in het resultaat — pen komt twee keer voor, en volgens hoofdstuk vijftien zijn regel vervangt de laatste de eerste stilzwijgend. Hier hebben beide pens dezelfde prijs dus ging er niets mis; met verschillende prijzen zou het stilzwijgend fout zijn. Een comprehension creëert dat probleem niet, maar omdat het kort is, is de kans kleiner dat het opvalt.
Wanneer het breekt
SyntaxError door if en else aan het einde te zetten [m for m in marks if m >= 40 else 0] is niet geldig. Het filterende if gaat aan het einde en heeft geen else; het if ... else dat kiest gaat aan het begin: [m if m >= 40 else 0 for m in marks].
Het resultaat zit vol None Iets binnenin de comprehension levert niets op — zoals [items.append(x) for x in other]. append geeft None terug, dus het resultaat is een lijst van None. Een comprehension bouwt waarden; om werk te doen, schrijf een gewone lus.
NameError — de variabele van de comprehension is er buiten niet meer Na [m * 2 for m in marks] kun je m niet gebruiken. In hoofdstuk elf overleefde een for lus zijn variabele de lus; die van een comprehension niet — die leeft alleen binnenin.
Een dictionary comprehension produceerde minder paren dan verwacht Dezelfde key werd vaker geproduceerd en de laatste vervangt de eerste. Vergelijk met len().
Ik kan de regel niet lezen Dat is reden genoeg. Breek het op in een lus — het snelheidsverschil telt zelden voor beginners, en de volgende die het leest, waarschijnlijk jij, zal dankbaar zijn.
Stap 4 van 6 — Voorspellen
Test je begrip
Er is een if aan het einde. Wat wordt geprint?
marks = [72, 45, 90, 33]
passed = [mark for mark in marks if mark >= 40]
print(passed)- A[72, 45, 90, 33]
- B[True, True, True, False]
- C[33]
- D[72, 45, 90]
Deze keer zit de if aan het begin, met een else. Wat wordt geprint?
marks = [72, 33, 90]
labels = ["pass" if m >= 40 else "fail" for m in marks]
print(labels)- A['pass', 'fail', 'pass']
- B['pass', 'pass']
- C[72, 90]
- D['pass', 'fail']
append wordt binnenin een comprehension aangeroepen. Wat printen de twee regels?
source = ["pen", "bag"]
target = []
result = [target.append(item) for item in source]
print(target)
print(result)- A['pen', 'bag'] [None, None]
- BEen `TypeError`
- C['pen', 'bag'] ['pen', 'bag']
- D[] ['pen', 'bag']
Voor antwoorden heb je een account nodig
Log in om je antwoorden te controleren
De vragen staan hierboven, en ze in je hoofd uitwerken is waar het om draait. Log in om de antwoorden, de uitleg en de hints in drie niveaus te zien.
Jouw beurt
Schrijf een bestand genaamd clean.py met een lijst van onordelijke namen — sommige met extra spaties, sommige met een hoofdletter, en minimaal één die volledig leeg is.
Met comprehensions bouw je:
- Een lijst van schone namen — getrimd, lowercase, waarbij lege namen weggelaten zijn
- Een lijst van de lengte van elke naam
- Een lijst van de namen langer dan vijf karakters
- Een dictionary van
{naam: lengte} - Een set van de eerste letters
En twee experimenten:
- Schrijf opdracht 1 opnieuw als een gewone
forlus, en controleer of de twee resultaten gelijk zijn met== - Schrijf een comprehension met twee
fors en eenif, en ontbind hem in een lus. Welke is makkelijker te lezen?
Die laatste vraag heeft geen "goed" antwoord, en dat is het punt. Leren die vraag stellen is de echte les van dit hoofdstuk.
Stap 6 van 6
Uitdaging — de hoofdstukquiz
Tien vragen van makkelijk naar moeilijk. De laatste zijn met opzet lastig.
Log in om de quiz te maken