C'è un motivo per preferire l'uso della map()
comprensione over list o viceversa? Uno dei due è generalmente più efficiente o generalmente considerato più pitonico dell'altro?
C'è un motivo per preferire l'uso della map()
comprensione over list o viceversa? Uno dei due è generalmente più efficiente o generalmente considerato più pitonico dell'altro?
Risposte:
map
può essere microscopicamente più veloce in alcuni casi (quando NON stai realizzando un lambda per lo scopo, ma usando la stessa funzione in map e in un listcomp). La comprensione dell'elenco può essere più rapida in altri casi e la maggior parte (non tutti) dei pitonisti li considera più diretti e chiari.
Un esempio del piccolo vantaggio di velocità della mappa quando si utilizza esattamente la stessa funzione:
$ python -mtimeit -s'xs=range(10)' 'map(hex, xs)'
100000 loops, best of 3: 4.86 usec per loop
$ python -mtimeit -s'xs=range(10)' '[hex(x) for x in xs]'
100000 loops, best of 3: 5.58 usec per loop
Un esempio di come il confronto delle prestazioni viene completamente invertito quando la mappa necessita di una lambda:
$ python -mtimeit -s'xs=range(10)' 'map(lambda x: x+2, xs)'
100000 loops, best of 3: 4.24 usec per loop
$ python -mtimeit -s'xs=range(10)' '[x+2 for x in xs]'
100000 loops, best of 3: 2.32 usec per loop
map(operator.attrgetter('foo'), objs)
più facile da leggere di [o.foo for o in objs]
?!
o
qui, e i tuoi esempi mostrano perché.
str()
esempio, però.
casi
map
, sebbene sia considerata "non pitonica". Ad esempio, map(sum, myLists)
è più elegante / conciso di [sum(x) for x in myLists]
. Si guadagna l'eleganza di non dover recuperare una variabile dummy (ad esempio, sum(x) for x...
o sum(_) for _...
o sum(readableName) for readableName...
), che è necessario digitare due volte, solo per iterare. Lo stesso argomento vale per filter
e reduce
qualsiasi cosa del itertools
modulo: se hai già una funzione a portata di mano, potresti andare avanti e fare un po 'di programmazione funzionale. Ciò aumenta la leggibilità in alcune situazioni e la perde in altre (ad es. Programmatori principianti, argomenti multipli) ... ma la leggibilità del codice dipende in ogni caso dai commenti.map
funzione come una pura funzione astratta mentre esegui una programmazione funzionale, dove stai mappando map
o curry map
, o altrimenti trarrai vantaggio dal parlare map
come una funzione. In Haskell, ad esempio, un'interfaccia di funzione chiamata fmap
generalizza il mapping su qualsiasi struttura di dati. Questo è molto raro in Python perché la grammatica di Python ti costringe a usare lo stile del generatore per parlare di iterazione; non puoi generalizzare facilmente. (Questo a volte è buono e talvolta cattivo.) Probabilmente puoi trovare rari esempi di pitone dove map(f, *lists)
è una cosa ragionevole da fare. L'esempio più vicino che posso trovare sarebbe sumEach = partial(map,sum)
, che è un one-liner che è molto approssimativamente equivalente a:def sumEach(myLists):
return [sum(_) for _ in myLists]
for
-loop : puoi anche ovviamente usare un for-loop. Sebbene non siano così eleganti dal punto di vista della programmazione funzionale, a volte variabili non locali rendono il codice più chiaro nei linguaggi di programmazione imperativa come Python, perché le persone sono molto abituate a leggere il codice in quel modo. I for-loop sono anche, generalmente, i più efficienti quando stai semplicemente facendo qualsiasi operazione complessa che non sta costruendo un elenco come la comprensione delle liste e la mappa per cui sono ottimizzati (ad esempio sommando o facendo un albero, ecc.) - almeno efficiente in termini di memoria (non necessariamente in termini di tempo, dove mi aspetterei nella peggiore delle ipotesi un fattore costante, salvo qualche raro singhiozzo patologico di raccolta dei rifiuti)."Pythonism"
Non mi piace la parola "pitone" perché non trovo che pitone sia sempre elegante ai miei occhi. Tuttavia, map
e filter
funzioni simili (come il itertools
modulo molto utile ) sono probabilmente considerate non ritoniche in termini di stile.
Pigrizia
In termini di efficienza, come la maggior parte dei costrutti di programmazione funzionale, MAP PU BE ESSERE LUSSUOSO , e in effetti è pigro in pitone. Ciò significa che puoi farlo (in python3 ) e il tuo computer non esaurirà la memoria e perderà tutti i tuoi dati non salvati:
>>> map(str, range(10**100))
<map object at 0x2201d50>
Prova a farlo con una comprensione dell'elenco:
>>> [str(n) for n in range(10**100)]
# DO NOT TRY THIS AT HOME OR YOU WILL BE SAD #
Nota che anche le comprensioni degli elenchi sono intrinsecamente pigre, ma Python ha scelto di implementarle come non pigre . Tuttavia, python supporta la comprensione dell'elenco pigro sotto forma di espressioni generatrici, come segue:
>>> (str(n) for n in range(10**100))
<generator object <genexpr> at 0xacbdef>
Fondamentalmente puoi pensare alla [...]
sintassi come passare un'espressione di generatore al costruttore della lista, come list(x for x in range(5))
.
Breve esempio inventato
from operator import neg
print({x:x**2 for x in map(neg,range(5))})
print({x:x**2 for x in [-y for y in range(5)]})
print({x:x**2 for x in (-y for y in range(5))})
Le comprensioni dell'elenco sono non pigre, quindi potrebbe richiedere più memoria (a meno che non si utilizzino le comprensioni del generatore). Le parentesi quadre [...]
spesso rendono le cose ovvie, specialmente quando si trovano in un pasticcio di parentesi. D'altra parte, a volte finisci per essere prolisso come scrivere [x for x in...
. Finché si mantengono brevi le variabili dell'iteratore, le comprensioni degli elenchi sono generalmente più chiare se non si rientra il codice. Ma puoi sempre indentare il tuo codice.
print(
{x:x**2 for x in (-y for y in range(5))}
)
o rompere le cose:
rangeNeg5 = (-y for y in range(5))
print(
{x:x**2 for x in rangeNeg5}
)
Confronto di efficienza per python3
map
è ora pigro:
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=map(f,xs)'
1000000 loops, best of 3: 0.336 usec per loop ^^^^^^^^^
Pertanto, se non map
userete tutti i vostri dati, o non saprete in anticipo di quanti dati avete bisogno, in python3 (ed espressioni del generatore in python2 o python3) eviterete di calcolarne i valori fino all'ultimo momento necessario. Di solito questo supererà di gran lunga qualsiasi sovraccarico dall'uso map
. Il rovescio della medaglia è che questo è molto limitato in Python rispetto alla maggior parte dei linguaggi funzionali: ottieni questo vantaggio solo se accedi ai tuoi dati da sinistra a destra "in ordine", perché le espressioni del generatore python possono essere valutate solo nell'ordine x[0], x[1], x[2], ...
.
Tuttavia, diciamo che abbiamo una funzione pre-creata f
che vorremmo map
e ignoriamo la pigrizia map
forzando immediatamente la valutazione list(...)
. Otteniamo risultati molto interessanti:
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=list(map(f,xs))'
10000 loops, best of 3: 165/124/135 usec per loop ^^^^^^^^^^^^^^^
for list(<map object>)
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=[f(x) for x in xs]'
10000 loops, best of 3: 181/118/123 usec per loop ^^^^^^^^^^^^^^^^^^
for list(<generator>), probably optimized
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=list(f(x) for x in xs)'
1000 loops, best of 3: 215/150/150 usec per loop ^^^^^^^^^^^^^^^^^^^^^^
for list(<generator>)
I risultati sono nella forma AAA / BBB / CCC dove A è stato eseguito con una workstation Intel del 2010 circa con Python 3.?.?, E B e C sono stati eseguiti con una workstation AMD del 2013 circa con Python 3.2.1, con hardware estremamente diverso. Il risultato sembra essere che la comprensione di mappe ed elenchi sia paragonabile nelle prestazioni, che è fortemente influenzata da altri fattori casuali. L'unica cosa che possiamo dire sembra essere che, stranamente, mentre ci aspettiamo che la comprensione dell'elenco [...]
funzioni meglio delle espressioni del generatore (...)
, map
è ANCHE più efficiente delle espressioni del generatore (sempre supponendo che tutti i valori vengano valutati / utilizzati).
È importante rendersi conto che questi test assumono una funzione molto semplice (la funzione identità); tuttavia questo va bene perché se la funzione fosse complicata, il sovraccarico delle prestazioni sarebbe trascurabile rispetto ad altri fattori nel programma. (Potrebbe essere comunque interessante testare con altre cose semplici come f=lambda x:x+x
)
Se sei abile a leggere l'assembly di Python, puoi usare il dis
modulo per vedere se è proprio quello che succede dietro le quinte:
>>> listComp = compile('[f(x) for x in xs]', 'listComp', 'eval')
>>> dis.dis(listComp)
1 0 LOAD_CONST 0 (<code object <listcomp> at 0x2511a48, file "listComp", line 1>)
3 MAKE_FUNCTION 0
6 LOAD_NAME 0 (xs)
9 GET_ITER
10 CALL_FUNCTION 1
13 RETURN_VALUE
>>> listComp.co_consts
(<code object <listcomp> at 0x2511a48, file "listComp", line 1>,)
>>> dis.dis(listComp.co_consts[0])
1 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 18 (to 27)
9 STORE_FAST 1 (x)
12 LOAD_GLOBAL 0 (f)
15 LOAD_FAST 1 (x)
18 CALL_FUNCTION 1
21 LIST_APPEND 2
24 JUMP_ABSOLUTE 6
>> 27 RETURN_VALUE
>>> listComp2 = compile('list(f(x) for x in xs)', 'listComp2', 'eval')
>>> dis.dis(listComp2)
1 0 LOAD_NAME 0 (list)
3 LOAD_CONST 0 (<code object <genexpr> at 0x255bc68, file "listComp2", line 1>)
6 MAKE_FUNCTION 0
9 LOAD_NAME 1 (xs)
12 GET_ITER
13 CALL_FUNCTION 1
16 CALL_FUNCTION 1
19 RETURN_VALUE
>>> listComp2.co_consts
(<code object <genexpr> at 0x255bc68, file "listComp2", line 1>,)
>>> dis.dis(listComp2.co_consts[0])
1 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 17 (to 23)
6 STORE_FAST 1 (x)
9 LOAD_GLOBAL 0 (f)
12 LOAD_FAST 1 (x)
15 CALL_FUNCTION 1
18 YIELD_VALUE
19 POP_TOP
20 JUMP_ABSOLUTE 3
>> 23 LOAD_CONST 0 (None)
26 RETURN_VALUE
>>> evalledMap = compile('list(map(f,xs))', 'evalledMap', 'eval')
>>> dis.dis(evalledMap)
1 0 LOAD_NAME 0 (list)
3 LOAD_NAME 1 (map)
6 LOAD_NAME 2 (f)
9 LOAD_NAME 3 (xs)
12 CALL_FUNCTION 2
15 CALL_FUNCTION 1
18 RETURN_VALUE
Sembra che sia meglio usare la [...]
sintassi di list(...)
. Purtroppo la map
classe è un po 'opaca da disassemblare, ma possiamo farcela con il nostro test di velocità.
map
e filter
insieme alla libreria standard itertools
hanno uno stile intrinsecamente cattivo. A meno che GvR in realtà non dica che sono stati un terribile errore o esclusivamente per le prestazioni, l'unica conclusione naturale se questo è ciò che dice "Pythonicness" è di dimenticarsene come stupido ;-)
map
/ filter
fosse una grande idea per Python 3 , e solo una ribellione di altri Pythonistas li teneva nello spazio dei nomi incorporato (mentre reduce
veniva spostato in functools
). Personalmente non sono d' accordo ( map
e sto filter
bene con funzioni predefinite, in particolare integrate, ma non le uso mai se lambda
fosse necessario), ma GvR non le ha praticamente definite Pythonic per anni.
itertools
? La parte che cito da questa risposta è l'affermazione principale che mi confonde. Non so se nel suo mondo ideale, map
e filter
si sposterebbe itertools
(o functools
) o andrebbe del tutto, ma qualunque sia il caso, una volta che si dice che non itertools
è Pitonico nella sua interezza, allora non so davvero cosa sia "Pythonic" dovrebbe significare, ma non credo che possa essere qualcosa di simile a "ciò che GvR raccomanda alle persone di usare".
map
/ filter
, no itertools
. La programmazione funzionale è perfettamente Pythonic ( itertools
, functools
e operator
sono stati tutti progettati specificamente con la programmazione funzionale in mente, e io uso idiomi funzionali in Python tutto il tempo), e itertools
fornisce funzionalità che sarebbe un dolore per attuare da soli, E 'specificamente map
e filter
di essere ridondante con le espressioni del generatore ciò li fece odiare da Guido. itertools
è sempre andato bene.
map
e filter
invece della comprensione dell'elenco.Un motivo oggettivo per cui dovresti preferirli anche se non sono "Pythonic" è questo:
richiedono funzioni / lambdas come argomenti, che introducono un nuovo ambito .
Sono stato morso da questo più di una volta:
for x, y in somePoints:
# (several lines of code here)
squared = [x ** 2 for x in numbers]
# Oops, x was silently overwritten!
ma se invece avessi detto:
for x, y in somePoints:
# (several lines of code here)
squared = map(lambda x: x ** 2, numbers)
allora tutto sarebbe andato bene.
Si potrebbe dire che ero sciocco per aver usato lo stesso nome di variabile nello stesso ambito.
Non lo ero. Inizialmente il codice andava bene, i due x
non erano nello stesso ambito.
È stato solo dopo che mi sono trasferito il blocco interno in un'altra sezione del codice è emerso il problema (leggi: problema durante la manutenzione, non in fase di sviluppo) e non me lo aspettavo.
Sì, se non commetti questo errore, la comprensione delle liste è più elegante.
Ma per esperienza personale (e dal vedere gli altri commettere lo stesso errore) l'ho visto accadere abbastanza volte che penso che non valga la pena soffrire quando questi bug si insinuano nel tuo codice.
Usa map
e filter
. Impediscono bug impercettibili e difficili da diagnosticare.
Non dimenticare di considerare l'utilizzo imap
e ifilter
(in itertools
) se sono appropriati per la tua situazione!
map
e / o filter
. Semmai, la traduzione più diretta e logica per evitare il tuo problema non è map(lambda x: x ** 2, numbers)
ma piuttosto un'espressione del generatore list(x ** 2 for x in numbers)
che non perde, come ha già sottolineato JeromeJ. Guarda Mehrdad, non prendere un voto così personale, non sono assolutamente d'accordo con il tuo ragionamento qui.
In realtà, la map
comprensione dell'elenco si comporta in modo abbastanza diverso nel linguaggio Python 3. Dai un'occhiata al seguente programma Python 3:
def square(x):
return x*x
squares = map(square, [1, 2, 3])
print(list(squares))
print(list(squares))
Potresti aspettarti che stampi la riga "[1, 4, 9]" due volte, ma invece stampa "[1, 4, 9]" seguito da "[]". La prima volta che lo guardi squares
sembra comportarsi come una sequenza di tre elementi, ma la seconda volta come uno vuoto.
Nel linguaggio Python 2 map
restituisce un vecchio elenco semplice, proprio come fanno le comprensioni degli elenchi in entrambe le lingue. Il punto cruciale è che il valore restituito map
in Python 3 (e imap
in Python 2) non è un elenco - è un iteratore!
Gli elementi vengono consumati quando si scorre su un iteratore, diversamente da quando si scorre su un elenco. Questo è il motivo per cui squares
appare vuoto nell'ultima print(list(squares))
riga.
Riassumere:
map
produrre una struttura di dati, non un iteratore. Ma forse gli iteratori pigri sono più facili delle strutture di dati pigri. Cibo per la mente. Grazie @MnZrK
Trovo che le comprensioni degli elenchi siano generalmente più espressive di ciò che sto cercando di fare rispetto a map
- entrambi lo fanno, ma il primo salva il carico mentale di cercare di capire quale potrebbe essere lambda
un'espressione complessa .
C'è anche un'intervista là fuori da qualche parte (non riesco a trovarla in maniera sprovvista) in cui Guido elenca lambda
le funzioni funzionali come la cosa di cui più si rammarica di accettare in Python, quindi potresti argomentare che sono non-Pythonic in virtù di quella.
const
parola chiave in C ++ è un grande trionfo su queste linee.
lambda
sono stati resi così scadenti (nessuna dichiarazione ..) che sono difficili da usare e comunque limitati.
Ecco un possibile caso:
map(lambda op1,op2: op1*op2, list1, list2)
contro:
[op1*op2 for op1,op2 in zip(list1,list2)]
Immagino che zip () sia uno svantaggio sfortunato e inutile che devi indulgere se insisti nell'usare la comprensione dell'elenco anziché la mappa. Sarebbe bello se qualcuno chiarisse questo in modo affermativo o negativo.
zip
pigro usandoitertools.izip
map(operator.mul, list1, list2)
. È su queste semplicissime espressioni sul lato sinistro che la comprensione diventa goffa.
Se hai intenzione di scrivere un codice asincrono, parallelo o distribuito, probabilmente preferirai map
una comprensione dell'elenco - poiché la maggior parte dei pacchetti asincroni, paralleli o distribuiti fornisce una map
funzione per sovraccaricare Python map
. Quindi passando la map
funzione appropriata al resto del codice, potrebbe non essere necessario modificare il codice seriale originale per farlo funzionare in parallelo (ecc.).
Quindi, dato che Python 3 map()
è un iteratore, devi tenere a mente ciò di cui hai bisogno: un iteratore o un list
oggetto.
Come già accennato @AlexMartelli , map()
è più veloce della comprensione dell'elenco solo se non si utilizza la lambda
funzione.
Ti presenterò alcuni paragoni di tempo.
Python 3.5.2 e CPython
ho usato il taccuino di Giove e in particolare il %timeit
comando magico incorporato
Misure : s == 1000 ms == 1000 * 1000 µs = 1000 * 1000 * 1000 ns
Impostare:
x_list = [(i, i+1, i+2, i*2, i-9) for i in range(1000)]
i_list = list(range(1000))
Funzione integrata:
%timeit map(sum, x_list) # creating iterator object
# Output: The slowest run took 9.91 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 277 ns per loop
%timeit list(map(sum, x_list)) # creating list with map
# Output: 1000 loops, best of 3: 214 µs per loop
%timeit [sum(x) for x in x_list] # creating list with list comprehension
# Output: 1000 loops, best of 3: 290 µs per loop
lambda
funzione:
%timeit map(lambda i: i+1, i_list)
# Output: The slowest run took 8.64 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 325 ns per loop
%timeit list(map(lambda i: i+1, i_list))
# Output: 1000 loops, best of 3: 183 µs per loop
%timeit [i+1 for i in i_list]
# Output: 10000 loops, best of 3: 84.2 µs per loop
Esiste anche un'espressione del generatore, vedi PEP-0289 . Quindi ho pensato che sarebbe stato utile aggiungerlo al confronto
%timeit (sum(i) for i in x_list)
# Output: The slowest run took 6.66 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 495 ns per loop
%timeit list((sum(x) for x in x_list))
# Output: 1000 loops, best of 3: 319 µs per loop
%timeit (i+1 for i in i_list)
# Output: The slowest run took 6.83 times longer than the fastest.
# This could mean that an intermediate result is being cached.
# 1000000 loops, best of 3: 506 ns per loop
%timeit list((i+1 for i in i_list))
# Output: 10000 loops, best of 3: 125 µs per loop
list
oggetto:Usa la comprensione dell'elenco se è una funzione personalizzata, usa list(map())
se c'è una funzione integrata
list
oggetti, hai solo bisogno di uno iterabile:Usa sempre map()
!
Ho eseguito un test rapido confrontando tre metodi per invocare il metodo di un oggetto. La differenza di tempo, in questo caso, è trascurabile ed è una questione della funzione in questione (vedere la risposta di @Alex Martelli ). Qui, ho esaminato i seguenti metodi:
# map_lambda
list(map(lambda x: x.add(), vals))
# map_operator
from operator import methodcaller
list(map(methodcaller("add"), vals))
# map_comprehension
[x.add() for x in vals]
Ho esaminato gli elenchi (memorizzati nella variabile vals
) di numeri interi (Python int
) e in virgola mobile (Python float
) per aumentare le dimensioni dell'elenco. È considerata la seguente classe fittizia DummyNum
:
class DummyNum(object):
"""Dummy class"""
__slots__ = 'n',
def __init__(self, n):
self.n = n
def add(self):
self.n += 5
In particolare, il add
metodo. L' __slots__
attributo è una semplice ottimizzazione in Python per definire la memoria totale richiesta dalla classe (attributi), riducendo la dimensione della memoria. Ecco i grafici risultanti.
Come affermato in precedenza, la tecnica utilizzata fa una differenza minima e dovresti codificare in un modo che sia più leggibile per te o nella circostanza particolare. In questo caso, la comprensione dell'elenco ( map_comprehension
tecnica) è più rapida per entrambi i tipi di aggiunte in un oggetto, in particolare con elenchi più brevi.
Visita questo pastebin per la fonte utilizzata per generare la trama e i dati.
map
è più veloce solo se la funzione viene chiamata nello stesso identico modo (cioè [*map(f, vals)]
vs. [f(x) for x in vals]
). Quindi list(map(methodcaller("add"), vals))
è più veloce di [methodcaller("add")(x) for x in vals]
. map
potrebbe non essere più veloce quando la controparte ciclica utilizza un diverso metodo di chiamata che può evitare un certo sovraccarico (ad esempio, x.add()
evita il methodcaller
sovraccarico dell'espressione o lambda). Per questo specifico test case, [*map(DummyNum.add, vals)]
sarebbe più veloce (perché DummyNum.add(x)
e x.add()
sostanzialmente ha le stesse prestazioni).
list()
chiamate esplicite sono leggermente più lente della comprensione dell'elenco. Per un confronto equo devi scrivere [*map(...)]
.
list()
chiamate aumentassero le spese generali. Avrei dovuto passare più tempo a leggere le risposte. Eseguirò nuovamente questi test per un confronto equo, per quanto trascurabili possano essere le differenze.
Ritengo che il modo più Pythonic sia usare una comprensione della lista invece di map
e filter
. Il motivo è che la comprensione dell'elenco è più chiara di map
e filter
.
In [1]: odd_cubes = [x ** 3 for x in range(10) if x % 2 == 1] # using a list comprehension
In [2]: odd_cubes_alt = list(map(lambda x: x ** 3, filter(lambda x: x % 2 == 1, range(10)))) # using map and filter
In [3]: odd_cubes == odd_cubes_alt
Out[3]: True
Come vedi, una comprensione non richiede lambda
espressioni extra come map
necessità. Inoltre, una comprensione consente anche di filtrare facilmente, mentre map
richiede filter
di consentire il filtraggio.
Ho provato il codice di @ alex-martelli ma ho riscontrato alcune discrepanze
python -mtimeit -s "xs=range(123456)" "map(hex, xs)"
1000000 loops, best of 5: 218 nsec per loop
python -mtimeit -s "xs=range(123456)" "[hex(x) for x in xs]"
10 loops, best of 5: 19.4 msec per loop
map impiega lo stesso tempo anche per intervalli molto ampi, mentre l'utilizzo della comprensione dell'elenco richiede molto tempo, come è evidente dal mio codice. Quindi, a parte il fatto di essere considerato "non pitonico", non ho affrontato alcun problema di prestazioni relativo all'utilizzo della mappa.
map
restituisce un elenco. In Python 3 map
viene valutato pigramente, quindi la semplice chiamata map
non calcola nessuno dei nuovi elementi dell'elenco, quindi perché si ottengono tempi così brevi.