Comprensione delle liste senza [] in Python


86

Iscrizione a un elenco:

>>> ''.join([ str(_) for _ in xrange(10) ])
'0123456789'

join deve prendere un iterabile.

Apparentemente, joinl'argomento di è [ str(_) for _ in xrange(10) ], ed è una comprensione di liste .

Guarda questo:

>>>''.join( str(_) for _ in xrange(10) )
'0123456789'

Ora, joinl'argomento di è solo str(_) for _ in xrange(10)no [], ma il risultato è lo stesso.

Perché? Fa str(_) for _ in xrange(10)anche produrre un elenco o un iterabile?


1
Immagino che joinmolto probabilmente sia scritto in C e quindi sia molto più veloce di una comprensione di liste ... Tempo di prova!
Joel Cornett

A quanto pare, ho letto la tua domanda completamente sbagliata. Sembra che stia restituendo un generatore per me ...
Joel Cornett

18
Solo una nota: _non ha un significato speciale, è un nome di variabile normale. Viene spesso utilizzato come nome usa e getta, ma non è così (stai usando la variabile). Eviterei di usarlo in un codice (almeno in questo modo).
rplnt

Risposte:


69
>>>''.join( str(_) for _ in xrange(10) )

Questa è chiamata espressione del generatore ed è spiegata in PEP 289 .

La principale differenza tra le espressioni del generatore e le comprensioni dell'elenco è che le prime non creano l'elenco in memoria.

Nota che c'è un terzo modo per scrivere l'espressione:

''.join(map(str, xrange(10)))

1
Per quanto ne so, un generatore può essere prodotto tramite un'espressione simile a una tupla, come ( str(_) for _ in xrange(10) ). Ma ero confuso sul fatto che, perché ()può essere omesso join, il che significa che il codice dovrebbe essere come `` '' .join ((str (_) for _ in xrange (10))), giusto?
Alcott

2
@Alcott La mia comprensione delle tuple è che sono effettivamente definite dall'elenco di espressioni separate da virgole e non dalle parentesi; le parentesi sono lì solo per raggruppare visivamente i valori in un assegnamento o per raggruppare effettivamente i valori se la tupla andasse in qualche altro elenco separato da virgole, come una chiamata di funzione. Questo è spesso dimostrato eseguendo codice come tup = 1, 2, 3; print(tup). Con questo in mente, l'uso forcome parte di un'espressione crea il generatore e le parentesi sono lì solo per distinguerlo da un ciclo scritto in modo errato.
Eric Ed Lohmar

133

Gli altri intervistati hanno risposto correttamente dicendo che hai scoperto un'espressione generatrice (che ha una notazione simile a quella delle liste di comprensione ma senza le parentesi quadre circostanti).

In generale, i genexps (come sono affettuosamente chiamati) sono più efficienti dalla memoria e più veloci delle comprensioni di elenchi.

TUTTAVIA, nel caso di ''.join()una lista di comprensione è sia più veloce che più efficiente in termini di memoria. Il motivo è che il join deve eseguire due passaggi sui dati, quindi in realtà ha bisogno di un elenco reale. Se gliene dai uno, può iniziare immediatamente il suo lavoro. Se invece gli dai un genexp, non può iniziare a funzionare finché non costruisce un nuovo elenco in memoria eseguendo il genexp fino all'esaurimento:

~ $ python -m timeit '"".join(str(n) for n in xrange(1000))'
1000 loops, best of 3: 335 usec per loop
~ $ python -m timeit '"".join([str(n) for n in xrange(1000)])'
1000 loops, best of 3: 288 usec per loop

Lo stesso risultato vale quando si confronta itertools.imap con map :

~ $ python -m timeit -s'from itertools import imap' '"".join(imap(str, xrange(1000)))'
1000 loops, best of 3: 220 usec per loop
~ $ python -m timeit '"".join(map(str, xrange(1000)))'
1000 loops, best of 3: 212 usec per loop

4
@lazyr Il tuo secondo tempismo sta facendo troppo lavoro. Non avvolgere un genexp attorno a un listcomp, usa semplicemente un genexp direttamente. Non c'è da stupirsi che tu abbia tempi strani.
Raymond Hettinger

11
Puoi spiegare perché sono ''.join()necessari 2 passaggi sull'iteratore per costruire una stringa?
ovgolovin

28
@ovgolovin Immagino che il primo passaggio consista nel sommare le lunghezze delle stringhe in modo da poter allocare la quantità corretta di memoria per la stringa concatenata, mentre il secondo passaggio consiste nel copiare le singole stringhe nello spazio allocato.
Lauritz V. Thaulow

20
@lazyr Questa ipotesi è corretta. Questo è esattamente ciò che fa str.join :-)
Raymond Hettinger

4
A volte mi manca davvero la possibilità di "aggiungere ai preferiti" una risposta specifica su SO.
Aria

5

Il tuo secondo esempio utilizza un'espressione generatrice anziché una comprensione di elenchi. La differenza è che con la comprensione della lista, una lista è completamente costruita e passata a .join(). Con l'espressione del generatore, gli elementi vengono generati uno per uno e consumati da .join(). Quest'ultimo utilizza meno memoria ed è generalmente più veloce.

Come accade, il costruttore della lista consumerà felicemente qualsiasi iterabile, inclusa un'espressione del generatore. Così:

[str(n) for n in xrange(10)]

è solo "zucchero sintattico" per:

list(str(n) for n in xrange(10))

In altre parole, una lista di comprensione è solo un'espressione generatrice che viene trasformata in una lista.


2
Sei sicuro che siano equivalenti sotto il cofano? Timeit dice:: [str(x) for x in xrange(1000)]262 usec list(str(x) for x in xrange(1000)),: 304 usec.
Lauritz V. Thaulow

2
@lazyr Hai ragione. La comprensione delle liste è più veloce. E questo è il motivo per cui le comprensioni delle liste perdono in Python 2.x. Questo è ciò che ha scritto GVR: "" Questo era un artefatto dell'implementazione originale di list list; è stato per anni uno dei "piccoli sporchi segreti" di Python. È iniziato come un compromesso intenzionale per rendere la comprensione delle liste incredibilmente veloce e, sebbene non fosse una trappola comune per i principianti, di tanto in tanto pungeva le
ovgolovin

3
@ovgolovin Il motivo per cui listcomp è più veloce è perché join deve creare un elenco prima di poter iniziare a lavorare. La "perdita" a cui ti riferisci non è un problema di velocità - significa solo che la variabile di induzione del ciclo è esposta al di fuori della listacomp.
Raymond Hettinger

1
@RaymondHettinger Allora cosa significano queste parole "È iniziato come un compromesso intenzionale per rendere le comprensioni delle liste incredibilmente veloci "? Come ho capito, c'è una connessione tra la loro perdita e problemi di velocità. GVR ha anche scritto: "Per le espressioni del generatore non abbiamo potuto farlo. Le espressioni del generatore sono implementate usando i generatori, la cui esecuzione richiede un frame di esecuzione separato. Pertanto, le espressioni del generatore (specialmente se ripetono su una breve sequenza) erano meno efficienti delle comprensioni di lista . "
ovgolovin

4
@ovgolovin Hai fatto un salto sbagliato da un dettaglio di implementazione di listcomp al motivo per cui str.join si comporta in questo modo. Una delle prime righe nel codice str.join è seq = PySequence_Fast(orig, "");e questa è l'unica ragione per cui gli iteratori vengono eseguiti più lentamente delle liste o delle tuple quando si chiama str.join (). Sei il benvenuto ad avviare una chat se vuoi discuterne ulteriormente (sono l'autore di PEP 289, il creatore del codice operativo LIST_APPEND e quello che ha ottimizzato il costruttore list (), quindi ne ho alcuni familiarità con il problema).
Raymond Hettinger


4

Se è tra parentesi, ma non parentesi, è tecnicamente un'espressione generatrice. Le espressioni del generatore sono state introdotte per la prima volta in Python 2.4.

http://wiki.python.org/moin/Generators

La parte dopo l'unione ( str(_) for _ in xrange(10) )è, di per sé, un'espressione generatrice. Potresti fare qualcosa come:

mylist = (str(_) for _ in xrange(10))
''.join(mylist)

e significa esattamente la stessa cosa che hai scritto nel secondo caso sopra.

I generatori hanno alcune proprietà molto interessanti, non ultimo il fatto che non finiscono per allocare un intero elenco quando non ne hai bisogno. Invece, una funzione come join "pompa" gli elementi fuori dall'espressione del generatore uno alla volta, facendo il suo lavoro sulle minuscole parti intermedie.

Nei tuoi esempi particolari, l'elenco e il generatore probabilmente non si comportano in modo molto diverso, ma in generale preferisco usare le espressioni del generatore (e anche le funzioni del generatore) ogni volta che posso, soprattutto perché è estremamente raro che un generatore sia più lento di un elenco completo materializzazione.


1

Questo è un generatore, piuttosto che una comprensione di elenchi. Anche i generatori sono iterabili, ma invece di creare prima l'intero elenco e poi passarlo all'unione, passa ogni valore in xrange uno per uno, il che può essere molto più efficiente.


0

L'argomento della seconda joinchiamata è un'espressione generatrice. Produce un iterabile.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.