Comprehensions — un ciclo su una riga
List, dictionary e set comprehension, la differenza tra un if filtrante e un if/else che sceglie, e quando una comprehension è la scelta sbagliata.
Come vuoi imparare?
Il problema che stiamo risolvendo
Una forma di codice ha continuato a riapparire negli ultimi sei capitoli:
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]Le quattro righe sopra e la singola riga sotto fanno esattamente la stessa cosa.
Tre di quelle quattro righe non sono il lavoro, sono la contabilità — creare una lista vuota, scorrere la sorgente, fare l'append ogni volta. L'unica vera decisione è mark * 2. Tutto il resto è identico ogni volta.
Una comprehension elimina quella ripetizione. Non è una capacità nuova — è ciò che sapevi già scrivere, in meno parole. Ed è anche da lì che viene il suo limite, ed è l'argomento dell'ultima sezione di questo capitolo: accorciare ciò che diventa più chiaro una volta accorciato, non tutto.
Alla fine di questo capitolo sarai in grado di
- Scrivere e leggere una list comprehension
- Filtrare con
if, e scegliere conif/else - Scrivere dictionary e set comprehension
- Dire quando una comprehension è la scelta sbagliata
- Scomporre una comprehension illeggibile tornando a un ciclo
Prerequisiti: Dati annidati — una lista di dizionari.
La forma
[ result for item in source ]Qui result è ciò che vuoi costruire, item è una cosa a ogni giro, e source è la lista (o altra collezione) da cui provengono le cose.
Letta ad alta voce è una frase normale: "`mark 2 per ogni mark in marks`"*.
Un modo per tenerla a mente: leggi prima la parte for, poi l'inizio. Sapere da dove arrivano le cose rende facile vedere cosa si sta costruendo.
Filtrare
Un if alla fine tiene solo le cose che corrispondono:
marks = [72, 45, 90, 33]
passed = [mark for mark in marks if mark >= 40]
print(passed)[72, 45, 90]Qui la prima parte è solo mark — niente calcolo, preso com'è. Filtrare e trasformare si possono fare insieme:
names = [" rafi ", "AHMED", " dia"]
clean = [name.strip().lower() for name in names if name.strip()]
print(clean)['rafi', 'ahmed', 'dia']La condizione è name.strip() senza nessun confronto. Ricorda la regola sulla verità del capitolo otto: una stringa vuota è falsa. Quindi chi ha digitato solo spazi viene scartato.
if alla fine versus if all'inizio
Due cose diverse, e la posizione è la differenza:
marks = [72, 33, 90]
labels = ["pass" if m >= 40 else "fail" for m in marks]
print(labels)['pass', 'fail', 'pass']Un if alla fine filtra — alcune cose non raggiungono mai il risultato, quindi il risultato può essere più corto.
Un if ... else all'inizio sceglie — ogni cosa produce qualcosa, quindi la lunghezza non cambia. È la condizione su una riga del capitolo quindici, dentro una comprehension.
La lunghezza cambia? Quella domanda è il modo più semplice per distinguere le due forme.
Anche dizionari e insiemi
La stessa forma, parentesi diverse:
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}Parentesi graffe e due punti fanno una dictionary comprehension. marks.items() restituisce una tupla a ogni giro e name, mark la spacchetta, esattamente come in un ciclo.
Le stesse parentesi graffe senza i due punti fanno un insieme:
orders = [
{"item": "pen"},
{"item": "bag"},
{"item": "pen"},
]
items = {order["item"] for order in orders}
print(sorted(items))['bag', 'pen']La domanda "quanti diversi" del capitolo sedici in una riga. pen arriva due volte e compare una volta.
Numeri in stringhe — che hai già visto
Questa riga è comparsa nel capitolo tredici con la promessa che la spiegazione stava arrivando:
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']Ora può essere letta per intero. str(m) for m in marks è una comprehension senza le parentesi quadre — quella forma si chiama generator expression, e invece di costruire tutta la lista d'un colpo passa a join un elemento alla volta. Le parentesi si possono togliere quando è l'unico argomento di una funzione, e con grandi quantità di dati risparmia memoria.
Quando non scriverne una
Essere capaci di scrivere una comprehension non significa che tu debba. Guarda questa:
rows = [["1", "2"], ["3", "x"]]
numbers = [int(c) for row in rows for c in row if c.isdigit()]
print(numbers)[1, 2, 3]La riga è corretta. Ma ha due for e un if, e devi ricordarti l'ordine: i for si leggono dal più esterno al più interno, come sarebbero scritti i cicli annidati. A distanza di sei mesi, leggere quella riga richiede sforzo.
Come ciclo:
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]Più righe, meno pensieri. L'obiettivo del codice non è essere corto ma essere chiaro — e la maggior parte delle volte accorciare lo rende più chiaro, ma non sempre.
Come regola pratica: un for e al massimo un if — una comprehension. Più di quello — un ciclo. E non mettere mai un print o un append dentro una comprehension; è una cosa per costruire un valore, non per fare lavoro.
Un esempio completo
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.0Gli stessi dati del capitolo diciassette, con la maggior parte di ciò che lì era un ciclo ora in una riga.
Tre cose che vale la pena notare.
totals e sum(totals) sono righe separate. Il totale avrebbe potuto essere sum(order["quantity"] * order["price"] for order in orders), ma allora i singoli valori di riga non esisterebbero. Un nome intermedio vale spesso più di un trucco su una riga.
Lo stesso calcolo compare due volte dentro big — una volta nella condizione, e servirebbe di nuovo nel risultato. Qui il risultato prende solo il nome, quindi nulla è perso; ma quando ti servono entrambi, quello è un buon motivo per lasciare la comprehension e scrivere un ciclo, dove il valore può essere calcolato una volta e dato un nome.
by_item ha perso un record. Quattro ordini, tre coppie nel risultato — pen compare due volte, e per la regola del capitolo quindici l'ultimo ha sostituito silenziosamente il primo. Qui entrambi i pen hanno lo stesso prezzo quindi nessun danno è stato fatto; con prezzi diversi sarebbe stato silenziosamente sbagliato. Una comprehension non crea quel problema, ma essere corta le dà meno chance di essere notata.
Quando si rompe
SyntaxError per aver messo if e else alla fine [m for m in marks if m >= 40 else 0] non è valido. L'if filtrante va alla fine e non ha else; l'if ... else che sceglie va davanti: [m if m >= 40 else 0 for m in marks].
Il risultato è pieno di None Qualcosa dentro la comprehension non restituisce niente — come [items.append(x) for x in other]. append restituisce None, quindi il risultato è una lista di None. Una comprehension costruisce valori; per fare lavoro, scrivi un ciclo normale.
NameError — la variabile della comprehension non esiste fuori Dopo [m * 2 for m in marks] non puoi usare m. Nel capitolo undici la variabile di un ciclo for sopravviveva al ciclo; quella di una comprehension no — vive solo dentro.
Una dictionary comprehension ha prodotto meno coppie del previsto La stessa chiave è stata prodotta più di una volta e l'ultima ha sostituito la prima. Confronta con len().
Non riesco a leggere la riga Questa è ragione sufficiente. Scomponila in un ciclo — la differenza di velocità raramente importa per i principianti, e la prossima persona a leggerla, probabilmente tu, sarà grata.
Passo 4 di 6 — Prevedi
Verifica cosa hai capito
C'è un if alla fine. Cosa viene stampato?
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]
Questa volta l'if è davanti, con un else. Cosa viene stampato?
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 viene chiamato dentro una comprehension. Cosa stampano le due righe?
source = ["pen", "bag"]
target = []
result = [target.append(item) for item in source]
print(target)
print(result)- A['pen', 'bag'] [None, None]
- BUn `TypeError`
- C['pen', 'bag'] ['pen', 'bag']
- D[] ['pen', 'bag']
Per rispondere serve un account
Accedi per verificare le tue risposte
Le domande sono qui sopra, e ragionarci a mente è la parte che conta. Accedi per vedere le risposte, le spiegazioni e i suggerimenti a tre livelli.
Tocca a te
Scrivi un file chiamato clean.py con una lista di nomi disordinati — alcuni con spazi extra, alcuni in maiuscolo, e almeno uno completamente vuoto.
Usando le comprehensions, costruisci:
- Una lista di nomi puliti — senza spazi extra, in minuscolo, con i vuoti scartati
- Una lista della lunghezza di ogni nome
- Una lista dei nomi più lunghi di cinque caratteri
- Un dizionario di
{nome: lunghezza} - Un insieme delle prime lettere
Poi due esperimenti:
- Riscrivi il numero 1 come un normale ciclo
for, e controlla che i due risultati siano uguali con== - Scrivi una comprehension con due
fore unif, poi scomponila in un ciclo. Qual è più facile da leggere?
Quell'ultima domanda non ha una risposta "giusta", e questo è il punto. Imparare a farsela è la vera lezione di questo capitolo.
Passo 6 di 6
Sfida — il quiz del capitolo
Dieci domande dalla più facile alla più difficile. Le ultime sono difficili di proposito.
Accedi per fare il quiz