¿Hay alguna razón para preferir el uso de la map()
comprensión de la lista o viceversa? ¿Alguno de ellos es generalmente más eficiente o generalmente considerado más pitónico que el otro?
¿Hay alguna razón para preferir el uso de la map()
comprensión de la lista o viceversa? ¿Alguno de ellos es generalmente más eficiente o generalmente considerado más pitónico que el otro?
Respuestas:
map
puede ser microscópicamente más rápido en algunos casos (cuando NO está haciendo una lambda para ese propósito, sino que usa la misma función en map y listcomp). Las comprensiones de listas pueden ser más rápidas en otros casos y la mayoría (no todos) los pitonistas las consideran más directas y claras.
Un ejemplo de la pequeña ventaja de velocidad del mapa cuando se usa exactamente la misma función:
$ 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 ejemplo de cómo la comparación de rendimiento se revierte por completo cuando el mapa necesita 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)
más fácil de leer que [o.foo for o in objs]
?
o
aquí, y sus ejemplos muestran por qué.
str()
ejemplo.
Casos
map
, aunque se considera 'no pitónica'. Por ejemplo, map(sum, myLists)
es más elegante / conciso que [sum(x) for x in myLists]
. Obtiene la elegancia de no tener que componer una variable ficticia (por ejemplo, sum(x) for x...
or sum(_) for _...
o sum(readableName) for readableName...
) que debe escribir dos veces, solo para iterar. El mismo argumento es válido para filter
y reduce
todo lo relacionado con el itertools
módulo: si ya tiene una función a mano, puede seguir adelante y hacer una programación funcional. Esto aumenta la legibilidad en algunas situaciones y la pierde en otras (por ejemplo, programadores novatos, múltiples argumentos) ... pero la legibilidad de su código depende en gran medida de sus comentarios de todos modos.map
función como una función abstracta pura mientras realiza la programación funcional, donde está mapeando map
o cursando map
, o de lo contrario se beneficia de hablar map
como una función. En Haskell, por ejemplo, una interfaz de functor llamadafmap
generaliza el mapeo sobre cualquier estructura de datos. Esto es muy poco común en python porque la gramática de python lo obliga a usar el estilo generador para hablar sobre la iteración; no puedes generalizarlo fácilmente. (Esto a veces es bueno y a veces malo). Probablemente se te ocurran ejemplos raros de Python donde map(f, *lists)
sea algo razonable. El ejemplo más cercano que se me ocurre sería sumEach = partial(map,sum)
, que es una línea que es más o menos equivalente a:def sumEach(myLists):
return [sum(_) for _ in myLists]
for
bucle : También puede, por supuesto, usar un bucle for. Si bien no es tan elegante desde el punto de vista de la programación funcional, a veces las variables no locales aclaran el código en lenguajes de programación imperativos como python, porque las personas están muy acostumbradas a leer código de esa manera. Los bucles for también son, por lo general, los más eficientes cuando simplemente realiza una operación compleja que no es construir una lista, como la comprensión de listas y el mapa para los que están optimizados (por ejemplo, sumar o hacer un árbol, etc.), al menos eficiente en términos de memoria (no necesariamente en términos de tiempo, donde esperaría en el peor de los casos un factor constante, salvo algunos hipo patológicos raros de recolección de basura)."Pitonismo"
No me gusta la palabra "pitón" porque no encuentro que pitón sea siempre elegante en mis ojos. Sin embargo,map
y filter
funciones similares (como el itertools
módulo muy útil ) probablemente se consideren poco pitónicas en términos de estilo.
pereza
En términos de eficiencia, al igual que la mayoría de las construcciones de programación funcional, MAP PUEDE SER PEREZOSO , y de hecho es perezoso en python. Eso significa que puede hacer esto (en python3 ) y su computadora no se quedará sin memoria y perderá todos sus datos no guardados:
>>> map(str, range(10**100))
<map object at 0x2201d50>
Intenta hacerlo con una lista de comprensión:
>>> [str(n) for n in range(10**100)]
# DO NOT TRY THIS AT HOME OR YOU WILL BE SAD #
Tenga en cuenta que las comprensiones de listas también son inherentemente perezosas, pero Python ha elegido implementarlas como no perezosas . Sin embargo, python admite comprensiones de listas perezosas en forma de expresiones generadoras, de la siguiente manera:
>>> (str(n) for n in range(10**100))
<generator object <genexpr> at 0xacbdef>
Básicamente puedes pensar en el [...]
sintaxis como pasar una expresión de generador al constructor de la lista, como list(x for x in range(5))
.
Breve ejemplo 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))})
Las comprensiones de listas no son perezosas, por lo que puede requerir más memoria (a menos que use comprensiones de generador). Los corchetes a [...]
menudo hacen que las cosas sean obvias, especialmente cuando están entre paréntesis. Por otro lado, a veces terminas siendo detallado como escribir[x for x in...
. Siempre y cuando mantenga las variables de su iterador cortas, las comprensiones de listas generalmente son más claras si no sangra su código. Pero siempre puedes sangrar tu código.
print(
{x:x**2 for x in (-y for y in range(5))}
)
o romper las cosas:
rangeNeg5 = (-y for y in range(5))
print(
{x:x**2 for x in rangeNeg5}
)
Comparación de eficiencia para python3
map
ahora es vago:
% python3 -mtimeit -s 'xs=range(1000)' 'f=lambda x:x' 'z=map(f,xs)'
1000000 loops, best of 3: 0.336 usec per loop ^^^^^^^^^
Por lo tanto, si no va a utilizar todos sus datos, o no sabe de antemano cuántos datos necesita, map
en python3 (y las expresiones generadoras en python2 o python3) evitará calcular sus valores hasta el último momento necesario. Por lo general, esto generalmente superará cualquier sobrecarga del uso map
. La desventaja es que esto es muy limitado en Python en comparación con la mayoría de los lenguajes funcionales: solo obtienes este beneficio si accedes a tus datos de izquierda a derecha "en orden", porque las expresiones del generador de Python solo pueden evaluarse en el orden x[0], x[1], x[2], ...
.
Sin embargo, digamos que tenemos una función prefabricada que f
nos gustaría map
, e ignoramos la pereza de map
forzar inmediatamente la evaluación con list(...)
. Obtenemos algunos resultados muy interesantes:
% 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>)
Los resultados están en la forma AAA / BBB / CCC donde A se realizó con una estación de trabajo Intel de alrededor de 2010 con python 3.?.?, Y B y C se realizaron con una estación de trabajo AMD de alrededor de 2013 con python 3.2.1, Con hardware extremadamente diferente. El resultado parece ser que las comprensiones de mapas y listas son comparables en rendimiento, lo que se ve más afectado por otros factores aleatorios. Lo único que podemos decir parece ser que, curiosamente, mientras esperamos que las comprensiones de listas [...]
funcionen mejor que las expresiones generadoras(...)
,map
TAMBIÉN es más eficiente que las expresiones generadoras (suponiendo nuevamente que todos los valores se evalúen / usen).
Es importante darse cuenta de que estas pruebas asumen una función muy simple (la función de identidad); sin embargo, esto está bien porque si la función fuera complicada, la sobrecarga de rendimiento sería insignificante en comparación con otros factores en el programa. (Todavía puede ser interesante probar con otras cosas simples comof=lambda x:x+x
)
Si eres hábil para leer el ensamblaje de Python, puedes usar el dis
módulo para ver si eso es lo que está sucediendo detrás de escena:
>>> 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 es mejor usar la [...]
sintaxis que list(...)
. Lamentablemente, la map
clase es un poco opaca para el desmontaje, pero podemos hacer las cosas con nuestra prueba de velocidad.
map
y filter
junto con la biblioteca estándar itertools
son inherentemente mal estilo. A menos que GvR realmente diga que fueron un terrible error o únicamente por el rendimiento, la única conclusión natural si eso es lo que dice "Pythonicness" es olvidarse de eso como estúpido ;-)
map
/ filter
era una gran idea para Python 3 , y solo una rebelión de otros Pythonistas los mantuvo en el espacio de nombres incorporado (mientras reduce
se movía a functools
). Personalmente, no estoy de acuerdo ( map
y estoy de acuerdo filter
con las funciones predefinidas, particularmente incorporadas, simplemente nunca las use si lambda
fuera necesario), pero GvR básicamente las ha llamado no Pythonic durante años.
itertools
? La parte que cito de esta respuesta es la afirmación principal que me confunde. No sé si en su mundo ideal, map
y me filter
mudaría a itertools
(o functools
) o iría por completo, pero cualquiera que sea el caso, una vez que uno diga que no itertools
es Phythonic en su totalidad, entonces realmente no sé qué es "Pythonic" se supone que significa, pero no creo que pueda ser algo similar a "lo que GvR recomienda que la gente use".
map
/ filter
, no itertools
. La programación funcional es perfectamente Pythonic ( itertools
, functools
y operator
fueron diseñados específicamente con la programación funcional en mente, y lo uso modismos funcionales en Python todo el tiempo), y itertools
proporciona características que sería un dolor de aplicar a sí mismo, es específicamente map
y filter
ser redundante con las expresiones generadoras eso hizo que Guido los odiara. itertools
siempre ha estado bien
map
yfilter
lugar de enumerar las comprensiones.Una razón objetiva por la que debería preferirlos aunque no sean "Pythonic" es esta:
requieren funciones / lambdas como argumentos, que introducen un nuevo alcance .
He sido mordido por esto más de una vez:
for x, y in somePoints:
# (several lines of code here)
squared = [x ** 2 for x in numbers]
# Oops, x was silently overwritten!
pero si en cambio hubiera dicho:
for x, y in somePoints:
# (several lines of code here)
squared = map(lambda x: x ** 2, numbers)
entonces todo hubiera estado bien.
Se podría decir que estaba siendo tonto por usar el mismo nombre de variable en el mismo ámbito.
Yo no estaba El código estaba bien originalmente: los dos x
s no estaban en el mismo alcance.
Fue solo después de que me mudé el bloque interno a una sección diferente del código que surgió el problema (léase: problema durante el mantenimiento, no el desarrollo), y no lo esperaba.
Sí, si nunca comete este error, entonces las comprensiones de listas son más elegantes.
Pero por experiencia personal (y al ver a otros cometer el mismo error) lo he visto suceder tantas veces que creo que no vale la pena el dolor que tienes que pasar cuando estos errores se introducen en tu código.
Uso map
y filter
. Previenen sutiles errores difíciles de diagnosticar relacionados con el alcance.
¡No olvides considerar usar imap
y ifilter
(in itertools
) si son apropiados para tu situación!
map
y / o filter
. En todo caso, la traducción más directa y lógica para evitar su problema no es hacerlo, map(lambda x: x ** 2, numbers)
sino más bien a una expresión generadora list(x ** 2 for x in numbers)
que no se filtre, como ya señaló JeromeJ. Mira, Mehrdad, no tomes un voto a favor tan personalmente, simplemente estoy totalmente en desacuerdo con tu razonamiento aquí.
En realidad, map
y las comprensiones de listas se comportan de manera bastante diferente en el lenguaje Python 3. Eche un vistazo al siguiente programa de Python 3:
def square(x):
return x*x
squares = map(square, [1, 2, 3])
print(list(squares))
print(list(squares))
Puede esperar que imprima la línea "[1, 4, 9]" dos veces, pero en su lugar imprime "[1, 4, 9]" seguido de "[]". La primera vez que mirassquares
parece comportarse como una secuencia de tres elementos, pero la segunda como vacía.
En el lenguaje Python 2, se map
devuelve una lista antigua simple, tal como lo hacen las comprensiones de listas en ambos idiomas. El quid es que el valor de retorno de map
Python 3 (y imap
en Python 2) no es una lista, ¡es un iterador!
Los elementos se consumen cuando itera sobre un iterador a diferencia de cuando itera sobre una lista. Es por eso que se squares
ve vacío en la última print(list(squares))
línea.
Para resumir:
map
producir una estructura de datos, no un iterador. Pero quizás los iteradores perezosos son más fáciles que las estructuras de datos perezosas. Comida para el pensamiento. Gracias @MnZrK
Encuentro que las comprensiones de listas son generalmente más expresivas de lo que estoy tratando de hacer que map
, ambas lo hacen, pero la primera ahorra la carga mental de tratar de entender lo que podría ser un complejolambda
expresión .
También hay una entrevista en algún lugar (no puedo encontrarlo de manera informal) donde Guido enumera lambda
las funciones funcionales como lo que más lamenta de aceptar en Python, por lo que podría argumentar que no son Pythonic en virtud de eso.
const
palabra clave en C ++ es un gran triunfo en este sentido.
lambda
, se han hecho tan lamentables (sin declaraciones ...) que son difíciles de usar y de todos modos limitados.
Aquí hay un caso posible:
map(lambda op1,op2: op1*op2, list1, list2)
versus:
[op1*op2 for op1,op2 in zip(list1,list2)]
Supongo que el zip () es una sobrecarga desafortunada e innecesaria que debes disfrutar si insistes en usar listas de comprensión en lugar del mapa. Sería genial si alguien aclara esto ya sea afirmativa o negativamente.
zip
perezoso usandoitertools.izip
map(operator.mul, list1, list2)
. Es en estas simples expresiones del lado izquierdo que las comprensiones se vuelven torpes.
Si planea escribir cualquier código asincrónico, paralelo o distribuido, probablemente preferirá map
una comprensión de la lista, ya que la mayoría de los paquetes asincrónicos, paralelos o distribuidos proporcionan una map
función para sobrecargar Python map
. Luego, al pasar la map
función adecuada al resto de su código, es posible que no tenga que modificar su código de serie original para que se ejecute en paralelo (etc.).
Entonces, dado que Python 3 map()
es un iterador, debe tener en cuenta lo que necesita: un iterador u list
objeto.
Como @AlexMartelli ya mencionó , map()
es más rápido que la comprensión de la lista solo si no usa la lambda
función.
Te presentaré algunas comparaciones de tiempo.
Python 3.5.2 y CPython
que he usado Júpiter portátil y especialmente %timeit
comando integrado mágicas
mediciones : s == 1000 ms == 1000 * 1000 µs = 1000 * 1000 * 1000 ns
Preparar:
x_list = [(i, i+1, i+2, i*2, i-9) for i in range(1000)]
i_list = list(range(1000))
Función 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
función:
%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
También existe la expresión generador, ver PEP-0289 . Entonces pensé que sería útil agregarlo a la comparación
%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 la comprensión de la lista si es una función personalizada, use list(map())
úsela si hay una función incorporada
list
objeto, solo necesita uno iterable:Siempre uso map()
!
Realicé una prueba rápida comparando tres métodos para invocar el método de un objeto. La diferencia horaria, en este caso, es insignificante y depende de la función en cuestión (ver la respuesta de @Alex Martelli ). Aquí, miré los siguientes 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]
Miré las listas (almacenadas en la variable vals
) de números enteros (Python int
) y números de coma flotante (Python float
) para aumentar el tamaño de las listas. Se considera la siguiente clase ficticia DummyNum
:
class DummyNum(object):
"""Dummy class"""
__slots__ = 'n',
def __init__(self, n):
self.n = n
def add(self):
self.n += 5
Específicamente, el add
método. El __slots__
atributo es una optimización simple en Python para definir la memoria total que necesita la clase (atributos), reduciendo el tamaño de la memoria. Aquí están las parcelas resultantes.
Como se indicó anteriormente, la técnica utilizada hace una diferencia mínima y debe codificar de la manera más legible para usted, o en la circunstancia particular. En este caso, la lista de comprensión (map_comprehension
técnica) es más rápida para ambos tipos de adiciones en un objeto, especialmente con listas más cortas.
Visite este pastebin para conocer la fuente utilizada para generar el diagrama y los datos.
map
es más rápido solo si la función se llama exactamente de la misma manera (es decir, [*map(f, vals)]
vs. [f(x) for x in vals]
). Entonces list(map(methodcaller("add"), vals))
es más rápido que [methodcaller("add")(x) for x in vals]
. map
Es posible que no sea más rápido cuando la contraparte en bucle utiliza un método de llamada diferente que puede evitar algunos gastos generales (por ejemplo, x.add()
evita la methodcaller
sobrecarga de la expresión o lambda). Para este caso de prueba específico, [*map(DummyNum.add, vals)]
sería más rápido (porque DummyNum.add(x)
y x.add()
básicamente tendría el mismo rendimiento).
list()
llamadas explícitas son un poco más lentas que las listas de comprensión. Para una comparación justa necesitas escribir [*map(...)]
.
list()
llamadas aumentaban los gastos generales. Debería haber pasado más tiempo leyendo las respuestas. Volveré a ejecutar estas pruebas para una comparación justa, por insignificantes que sean las diferencias.
Considero que la forma más pitónica es utilizar una lista de comprensión en lugar de map
y filter
. La razón es que las comprensiones de listas son más claras que map
y 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 puede ver, una comprensión no requiere lambda
expresiones adicionales como map
necesidades. Además, una comprensión también permite filtrar fácilmente, mientras que map
requiere filter
permitir el filtrado.
Probé el código por @ alex-martelli pero encontré algunas discrepancias
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
El mapa toma la misma cantidad de tiempo incluso para rangos muy grandes, mientras que el uso de la comprensión de la lista lleva mucho tiempo, como lo demuestra mi código. Por lo tanto, aparte de ser considerado "antipático", no he enfrentado ningún problema de rendimiento relacionado con el uso del mapa.
map
devuelve una lista. En Python 3, map
se evalúa de manera perezosa, por lo que simplemente llamar map
no calcula ninguno de los nuevos elementos de la lista, por lo tanto, por qué tiene tiempos tan cortos.