Existe um motivo para preferir usar o map()
excesso de compreensão da lista ou vice-versa? Um deles é geralmente mais eficiente ou considerado geralmente mais pitônico que o outro?
Existe um motivo para preferir usar o map()
excesso de compreensão da lista ou vice-versa? Um deles é geralmente mais eficiente ou considerado geralmente mais pitônico que o outro?
Respostas:
map
pode ser microscopicamente mais rápido em alguns casos (quando você NÃO está criando um lambda para esse fim, mas usando a mesma função no mapa e em um listcomp). Em outros casos, a compreensão da lista pode ser mais rápida e a maioria (não todos) os pythonistas os consideram mais diretos e claros.
Um exemplo da pequena vantagem de velocidade do mapa ao usar exatamente a mesma função:
$ 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
Um exemplo de como a comparação de desempenho é completamente revertida quando o mapa precisa de um 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)
mais fácil de ler do que [o.foo for o in objs]
?!
o
aqui, e seus exemplos mostram o porquê.
str()
exemplo dele .
Estojos
map
, embora seja considerada 'não-tônica'. Por exemplo, map(sum, myLists)
é mais elegante / conciso que [sum(x) for x in myLists]
. Você ganha a elegância de não precisar criar uma variável fictícia (por exemplo, sum(x) for x...
ou sum(_) for _...
ou sum(readableName) for readableName...
) que você deve digitar duas vezes, apenas para iterar. O mesmo argumento vale para filter
e reduce
e nada do itertools
módulo: se você já tem uma função útil, você poderia ir em frente e fazer alguma programação funcional. Isso aumenta a legibilidade em algumas situações e a perde em outras (por exemplo, programadores iniciantes, vários argumentos) ... mas a legibilidade do seu código depende muito dos seus comentários.map
função como uma função abstrata pura enquanto faz a programação funcional, onde está mapeando map
ou fazendo curry map
, ou se beneficiando de falar map
como uma função. Em Haskell, por exemplo, uma interface de functor chamada fmap
generaliza o mapeamento sobre qualquer estrutura de dados. Isso é muito incomum no python porque a gramática do python obriga a usar o estilo de gerador para falar sobre iteração; você não pode generalizar facilmente. (Isso às vezes é bom e às vezes ruim.) Você provavelmente pode criar exemplos raros de python, onde map(f, *lists)
é uma coisa razoável a se fazer. O exemplo mais próximo que eu posso sugerir seria sumEach = partial(map,sum)
, que é uma linha que é aproximadamente equivalente a:def sumEach(myLists):
return [sum(_) for _ in myLists]
for
loop : Você também pode, é claro, usar um loop for. Embora não sejam tão elegantes do ponto de vista da programação funcional, às vezes variáveis não locais tornam o código mais claro em linguagens de programação imperativas, como python, porque as pessoas estão muito acostumadas a ler código dessa maneira. Os loops for também são, geralmente, os mais eficientes quando você está apenas realizando uma operação complexa que não está construindo uma lista, como compreensão de lista e mapa, que são otimizados (por exemplo, somar ou criar uma árvore etc.) - pelo menos eficiente em termos de memória (não necessariamente em termos de tempo, onde eu esperaria, na pior das hipóteses, um fator constante, exceto alguns raros soluços patológicos na coleta de lixo)."Pitthonismo"
Não gosto da palavra "pitônico" porque não acho que o pitônico seja sempre elegante aos meus olhos. No entanto, map
e filter
funções similares (como o itertools
módulo muito útil ) provavelmente são consideradas não-sintônicas em termos de estilo.
Preguiça
Em termos de eficiência, como a maioria das construções de programação funcional, o MAP CAN BE LAZY e de fato é preguiçoso em python. Isso significa que você pode fazer isso (em python3 ) e o computador não ficará sem memória e perderá todos os dados não salvos:
>>> map(str, range(10**100))
<map object at 0x2201d50>
Tente fazer isso com uma compreensão da lista:
>>> [str(n) for n in range(10**100)]
# DO NOT TRY THIS AT HOME OR YOU WILL BE SAD #
Observe que as compreensões de lista também são inerentemente preguiçosas, mas o python optou por implementá-las como não preguiçosas . No entanto, o python suporta compreensões de lista lenta na forma de expressões geradoras, da seguinte maneira:
>>> (str(n) for n in range(10**100))
<generator object <genexpr> at 0xacbdef>
Você pode basicamente pensar na [...]
sintaxe como passando uma expressão geradora para o construtor da lista, como list(x for x in range(5))
.
Breve exemplo artificial
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))})
As compreensões de lista não são preguiçosas, portanto, pode exigir mais memória (a menos que você use compreensões de gerador). Os colchetes [...]
geralmente tornam as coisas óbvias, especialmente quando estão entre parênteses. Por outro lado, às vezes você acaba sendo verboso como digitar [x for x in...
. Contanto que você mantenha as variáveis do iterador curtas, a compreensão da lista geralmente será mais clara se você não recuar o código. Mas você sempre pode recuar seu código.
print(
{x:x**2 for x in (-y for y in range(5))}
)
ou acabar com as coisas:
rangeNeg5 = (-y for y in range(5))
print(
{x:x**2 for x in rangeNeg5}
)
Comparação de eficiência para python3
map
agora é preguiçoso:
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=map(f,xs)'
1000000 loops, best of 3: 0.336 usec per loop ^^^^^^^^^
Portanto, se você não usará todos os seus dados ou não souber antecipadamente quantos dados precisa, map
em python3 (e expressões geradoras em python2 ou python3) evitará o cálculo de seus valores até o último momento necessário. Geralmente, isso geralmente supera qualquer sobrecarga de uso map
. A desvantagem é que isso é muito limitado no python, em oposição à maioria das linguagens funcionais: você só obtém esse benefício se acessar os dados da esquerda para a direita "em ordem", porque as expressões do gerador python só podem ser avaliadas na ordem x[0], x[1], x[2], ...
.
No entanto, digamos que temos uma função pré-criada f
que gostaríamos de map
, e ignoramos a preguiça de map
forçar imediatamente a avaliação list(...)
. Temos alguns resultados muito interessantes:
% 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>)
Os resultados estão no formato AAA / BBB / CCC, onde A foi realizado em uma estação de trabalho Intel por volta de 2010 com python 3.?.? E B e C foram executados em uma estação de trabalho AMD por volta de 2013 com python 3.2.1, com hardware extremamente diferente. O resultado parece ser que as compreensões de mapas e listas são comparáveis em desempenho, o que é mais fortemente afetado por outros fatores aleatórios. A única coisa que podemos dizer parece ser que, estranhamente, enquanto esperamos que as compreensões de lista tenham um [...]
desempenho melhor que as expressões geradoras (...)
, map
também é mais eficiente que as expressões geradoras (novamente assumindo que todos os valores são avaliados / usados).
É importante perceber que esses testes assumem uma função muito simples (a função de identidade); no entanto, isso é bom porque se a função fosse complicada, a sobrecarga de desempenho seria insignificante em comparação com outros fatores no programa. (Ainda pode ser interessante testar com outras coisas simples, como f=lambda x:x+x
)
Se você é habilidoso em ler assembly python, pode usar o dis
módulo para ver se é isso o que está acontecendo nos bastidores:
>>> 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
Parece que é melhor usar [...]
sintaxe do que list(...)
. Infelizmente, a map
classe é um pouco opaca para desmontar, mas podemos fazer o devido com nosso teste de velocidade.
map
e filter
juntamente com a biblioteca padrão, itertools
são inerentemente de estilo ruim. A menos que GvR realmente diz que eles eram ou um erro terrível ou unicamente para o desempenho, a conclusão natural se é isso que "Pythonicness" diz é esquecê-lo tão estúpido ;-)
map
/ filter
era uma ótima idéia para o Python 3 , e apenas uma rebelião de outros pythonistas os manteve no espaço de nome interno (enquanto reduce
foi movido para functools
). Pessoalmente, não concordo ( map
e estou filter
bem com funções predefinidas, especialmente embutidas, nunca as use se lambda
for necessário), mas o GvR basicamente as chamou de Pythonic por anos.
itertools
? A parte que cito dessa resposta é a principal alegação que me confunde. Eu não sei se ele está no mundo ideal dele map
e filter
se mudaria para itertools
(ou functools
) ou iria completamente, mas qualquer que seja o caso, uma vez que se diga que itertools
é não-Pitônico na sua totalidade, então realmente não sei o que é "Pitônico". deveria significar, mas não acho que possa ser algo parecido com "o que o GvR recomenda que as pessoas usem".
map
/ filter
, não itertools
. A programação funcional é perfeitamente Pythonic ( itertools
, functools
e operator
foram todos projetados especificamente com programação funcional em mente, e eu uso expressões funcionais em Python o tempo todo), e itertools
fornece recursos que seria uma dor de implementar si mesmo, é especificamente map
e filter
ser redundante com gerador de expressões isso fez Guido odiá-los. itertools
sempre foi bom.
map
e filter
não a compreensão da lista.Uma razão objetiva pela qual você deve preferi-los, mesmo que não sejam "pitonicos", é o seguinte:
eles requerem funções / lambdas como argumentos, o que introduz um novo escopo .
Fui mordido por isso mais de uma vez:
for x, y in somePoints:
# (several lines of code here)
squared = [x ** 2 for x in numbers]
# Oops, x was silently overwritten!
mas se eu tivesse dito:
for x, y in somePoints:
# (several lines of code here)
squared = map(lambda x: x ** 2, numbers)
então tudo ficaria bem.
Você poderia dizer que eu estava sendo bobo por usar o mesmo nome de variável no mesmo escopo.
Eu não estava. O código estava bom originalmente - os dois x
s não estavam no mesmo escopo.
Foi somente depois que eu mudei o bloco interno para uma seção diferente do código que o problema surgiu (leia-se: problema durante a manutenção, não no desenvolvimento), e eu não esperava isso.
Sim, se você nunca cometer esse erro , a compreensão da lista será mais elegante.
Mas, por experiência pessoal (e ao ver os outros cometerem o mesmo erro), já vi isso acontecer várias vezes e acho que não vale a pena a dor que você passa quando esses bugs entram no seu código.
Use map
e filter
. Eles evitam erros sutis, difíceis de diagnosticar, relacionados ao escopo.
Não se esqueça de considerar o uso imap
e ifilter
(in itertools
) se eles forem apropriados para sua situação!
map
e / ou filter
. De qualquer forma, a tradução mais direta e lógica para evitar seu problema não é, map(lambda x: x ** 2, numbers)
mas sim uma expressão geradora list(x ** 2 for x in numbers)
que não vaza, como JeromeJ já apontou. Olhe Mehrdad, não tome uma votação tão pessoal, apenas discordo totalmente do seu raciocínio aqui.
Na verdade, as map
compreensões de lista se comportam de maneira bastante diferente na linguagem Python 3. Dê uma olhada no seguinte programa Python 3:
def square(x):
return x*x
squares = map(square, [1, 2, 3])
print(list(squares))
print(list(squares))
Você pode esperar que imprima a linha "[1, 4, 9]" duas vezes, mas, em vez disso, imprime "[1, 4, 9]" seguido de "[]". A primeira vez que você olha squares
parece se comportar como uma sequência de três elementos, mas a segunda vez como um vazio.
Na linguagem Python 2, map
retorna uma lista antiga simples, assim como as compreensões de lista nos dois idiomas. O ponto crucial é que o valor de retorno map
no Python 3 (e imap
no Python 2) não é uma lista - é um iterador!
Os elementos são consumidos quando você itera sobre um iterador, diferente da iteração sobre uma lista. É por isso que squares
parece vazio na última print(list(squares))
linha.
Para resumir:
map
produzir uma estrutura de dados, não um iterador. Mas talvez os iteradores preguiçosos sejam mais fáceis do que as estruturas de dados preguiçosas. Alimento para o pensamento. Obrigado @MnZrK
Acho que as compreensões de lista são geralmente mais expressivas do que estou tentando fazer do que map
- elas fazem isso, mas a primeira economiza a carga mental de tentar entender o que poderia ser uma lambda
expressão complexa .
Também há uma entrevista em algum lugar (não consigo encontrar de imediato) onde Guido lista se lambda
e as funções funcionais como a coisa que ele mais se arrepende de aceitar em Python, para que você possa argumentar que eles não são pitonistas em virtude por essa.
const
palavra - chave em C ++ é um grande triunfo nesse sentido.
lambda
que foram feitos tão coxos (sem declarações ...) que são difíceis de usar e limitados de qualquer maneira.
Aqui está um caso possível:
map(lambda op1,op2: op1*op2, list1, list2)
versus:
[op1*op2 for op1,op2 in zip(list1,list2)]
Eu acho que o zip () é uma sobrecarga infeliz e desnecessária que você precisa se dedicar se insistir em usar a compreensão da lista em vez do mapa. Seria ótimo se alguém esclarecesse isso afirmativamente ou negativamente.
zip
preguiçoso usandoitertools.izip
map(operator.mul, list1, list2)
. É nessas expressões simples do lado esquerdo que as compreensões se tornam desajeitadas.
Se você planeja escrever qualquer código assíncrono, paralelo ou distribuído, provavelmente preferirá map
a compreensão de uma lista - já que a maioria dos pacotes assíncronos, paralelos ou distribuídos fornece uma map
função para sobrecarregar os python map
. Em seguida, passando a map
função apropriada para o restante do seu código, talvez você não precise modificar o código de série original para que ele seja executado em paralelo (etc).
Portanto, como o Python 3 map()
é um iterador, você precisa ter em mente o que precisa: um iterador ou list
objeto.
Como o @AlexMartelli já mencionou , map()
é mais rápido que a compreensão da lista apenas se você não usar a lambda
função.
Vou apresentar algumas comparações de tempo.
Python 3.5.2 e CPython
Eu usei o notebook Jupiter e, especialmente, o %timeit
comando mágico interno
Medidas : s == 1000 ms == 1000 * 1000 µs = 1000 * 1000 * 1000 ns
Configuração:
x_list = [(i, i+1, i+2, i*2, i-9) for i in range(1000)]
i_list = list(range(1000))
Função incorporada:
%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
função:
%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
Também existe expressão de gerador, consulte PEP-0289 . Então eu pensei que seria útil adicioná-lo à comparação
%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
objeto:Use a compreensão da lista se for uma função personalizada, use list(map())
se houver uma função interna
list
objeto, apenas um iterável:Sempre use map()
!
Fiz um teste rápido comparando três métodos para invocar o método de um objeto. A diferença de horário, neste caso, é insignificante e é uma questão da função em questão (consulte a resposta de @Alex Martelli ). Aqui, olhei para os seguintes métodos:
# 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]
Eu olhei para listas (armazenadas na variável vals
) de números inteiros (Python int
) e números de ponto flutuante (Python float
) para aumentar o tamanho da lista. A seguinte classe fictícia DummyNum
é considerada:
class DummyNum(object):
"""Dummy class"""
__slots__ = 'n',
def __init__(self, n):
self.n = n
def add(self):
self.n += 5
Especificamente, o add
método O __slots__
atributo é uma otimização simples no Python para definir a memória total necessária à classe (atributos), reduzindo o tamanho da memória. Aqui estão os gráficos resultantes.
Como declarado anteriormente, a técnica usada faz uma diferença mínima e você deve codificar da maneira que for mais legível para você ou na circunstância específica. Nesse caso, a compreensão da lista ( map_comprehension
técnica) é mais rápida para os dois tipos de adições em um objeto, especialmente em listas mais curtas.
Visite este pastebin para a fonte usada para gerar o gráfico e os dados.
map
é mais rápido apenas se a função for chamada exatamente da mesma maneira (ou seja, [*map(f, vals)]
vs. [f(x) for x in vals]
). Então list(map(methodcaller("add"), vals))
é mais rápido que [methodcaller("add")(x) for x in vals]
. map
pode não ser mais rápido quando a contraparte em loop usa um método de chamada diferente que pode evitar sobrecarga (por exemplo, x.add()
evita a methodcaller
sobrecarga da expressão ou lambda). Para este caso de teste específico, [*map(DummyNum.add, vals)]
seria mais rápido (porque DummyNum.add(x)
e x.add()
tem basicamente o mesmo desempenho).
list()
chamadas explícitas são um pouco mais lentas que as compreensões de lista. Para uma comparação justa, você precisa escrever [*map(...)]
.
list()
chamadas aumentavam a sobrecarga. Deveria ter passado mais tempo lendo as respostas. Vou repetir esses testes para uma comparação justa, por mais insignificantes que sejam as diferenças.
Considero que a maneira mais pitônica é usar uma compreensão de lista em vez de map
e filter
. A razão é que as compreensões da lista são mais claras que 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
Como você vê, uma compreensão não requer lambda
expressões extras conforme as map
necessidades. Além disso, uma compreensão também permite filtrar facilmente, enquanto map
requer filter
permitir a filtragem.
Eu tentei o código por @ alex-martelli, mas encontrei algumas discrepâncias
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
O mapa leva a mesma quantidade de tempo, mesmo para intervalos muito grandes, enquanto o uso da compreensão da lista leva muito tempo, como é evidente no meu código. Portanto, além de ser considerado "antitônico", não enfrentei nenhum problema de desempenho relacionado ao uso do mapa.
map
retorna uma lista. No Python 3, map
é avaliado preguiçosamente; portanto, simplesmente chamar map
não computa nenhum dos novos elementos da lista, portanto, por que você obtém tempos tão curtos.