Lista de comprensión vs mapa

733

¿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?

TimothyAWiseman
fuente
8
Tenga en cuenta que PyLint advierte si usa el mapa en lugar de la comprensión de la lista, vea el mensaje W0141 .
lumbric
2
@lumbric, no estoy seguro, pero solo si se usa lambda en el mapa.
0xc0de

Respuestas:

662

mappuede 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
Alex Martelli
fuente
39
Sí, de hecho, nuestra guía de estilo interna de Python en el trabajo recomienda explícitamente listcomps contra el mapa y el filtro (sin mencionar el pequeño pero medible mapa de mejora del rendimiento que puede proporcionar en algunos casos ;-).
Alex Martelli
46
No es un problema con los infinitos puntos de estilo de Alex, pero a veces map me parece más fácil de leer: data = map (str, some_list_of_objects). Algunos otros ... operator.attrgetter, operator.itemgetter, etc.
Gregg Lind
57
map(operator.attrgetter('foo'), objs)más fácil de leer que [o.foo for o in objs]?
Alex Martelli
52
@ Alex: prefiero no introducir nombres innecesarios, como oaquí, y sus ejemplos muestran por qué.
Reid Barton el
29
Sin embargo, creo que @GreggLind tiene un punto, con su str()ejemplo.
Eric O Lebigot
474

Casos

  • Caso común : casi siempre, querrás usar una comprensión de lista en Python porque será más obvio lo que estás haciendo a los programadores novatos que leen tu código. (Esto no se aplica a otros idiomas, donde pueden aplicarse otras expresiones idiomáticas). Incluso será más obvio lo que está haciendo a los programadores de Python, ya que las comprensiones de listas son el estándar de facto en Python para la iteración; se esperan .
  • Caso menos común : sin embargo, si ya tiene una función definida , a menudo es razonable usarla 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 filtery reducetodo lo relacionado con el itertoolsmó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.
  • Casi nunca : es posible que desee utilizar la mapfunción como una función abstracta pura mientras realiza la programación funcional, donde está mapeando mapo cursando map, o de lo contrario se beneficia de hablar mapcomo 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]
  • Simplemente usando un forbucle : 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 filterfunciones similares (como el itertoolsmó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, mapen 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 fnos gustaría map, e ignoramos la pereza de mapforzar 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 dismó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 mapclase es un poco opaca para el desmontaje, pero podemos hacer las cosas con nuestra prueba de velocidad.

ninjagecko
fuente
55
"el muy útil módulo itertools [probablemente] se considere poco pitónico en términos de estilo". Hmm Tampoco me gusta el término "Pythonic", por lo que en cierto sentido no me importa lo que significa, pero no creo que sea justo para quienes lo usan, decir eso de acuerdo con las construcciones de "Pythonicness" mapy filterjunto con la biblioteca estándar itertoolsson 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 ;-)
Steve Jessop
44
@SteveJessop: En realidad, Guido pensó que abandonar map/ filterera una gran idea para Python 3 , y solo una rebelión de otros Pythonistas los mantuvo en el espacio de nombres incorporado (mientras reducese movía a functools). Personalmente, no estoy de acuerdo ( mapy estoy de acuerdo filtercon las funciones predefinidas, particularmente incorporadas, simplemente nunca las use si lambdafuera necesario), pero GvR básicamente las ha llamado no Pythonic durante años.
ShadowRanger
@ShadowRanger: cierto, pero ¿GvR alguna vez planeó eliminarlo itertools? La parte que cito de esta respuesta es la afirmación principal que me confunde. No sé si en su mundo ideal, mapy me filtermudaría a itertools(o functools) o iría por completo, pero cualquiera que sea el caso, una vez que uno diga que no itertoolses 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".
Steve Jessop
2
@SteveJessop: solo me dirigía a map/ filter, no itertools. La programación funcional es perfectamente Pythonic ( itertools, functoolsy operatorfueron diseñados específicamente con la programación funcional en mente, y lo uso modismos funcionales en Python todo el tiempo), y itertoolsproporciona características que sería un dolor de aplicar a sí mismo, es específicamente mapy filterser redundante con las expresiones generadoras eso hizo que Guido los odiara. itertoolssiempre ha estado bien
ShadowRanger
1
Podría marcar esta respuesta como favorita si hubiera alguna manera. Bien explicado.
NelsonGon
95

Python 2: debes usar mapyfilter 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 xs 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.

Conclusión:

Uso mapy filter. Previenen sutiles errores difíciles de diagnosticar relacionados con el alcance.

Nota al margen:

¡No olvides considerar usar imapy ifilter(in itertools) si son apropiados para tu situación!

usuario541686
fuente
77
Gracias por señalar esto. No se me había ocurrido explícitamente que la comprensión de la lista tenía el mismo alcance y podría ser un problema. Dicho esto, creo que algunas de las otras respuestas dejan en claro que la comprensión de la lista debería ser el enfoque predeterminado la mayor parte del tiempo, pero que esto es algo para recordar. Este también es un buen recordatorio general para mantener pequeñas las funciones (y, por lo tanto, el alcance) y realizar pruebas unitarias exhaustivas y usar declaraciones de afirmación.
TimothyAWiseman
13
@wim: Esto solo se trataba de Python 2, aunque se aplica a Python 3 si desea seguir siendo compatible con versiones anteriores. Lo sabía y había estado usando Python por un tiempo (sí, más que solo unos pocos meses), y sin embargo me sucedió. He visto a otros que son más inteligentes que yo caer en la misma trampa. Si eres tan brillante y / o experimentado que esto no es un problema para ti, entonces estoy feliz por ti, no creo que la mayoría de las personas sean como tú. Si lo fueran, no habría tanta necesidad de solucionarlo en Python 3.
user541686
12
Lo siento, pero escribiste esto a fines de 2012, mucho después de que Python 3 está en la escena, y la respuesta dice que estás recomendando un estilo de codificación de Python que de otro modo sería impopular solo porque te mordió un error al cortar y ... pegar código Nunca afirmé ser brillante o experimentado, simplemente no estoy de acuerdo en que el reclamo en negrita esté justificado por sus razones.
wim
8
@wim: ¿Eh? Python 2 todavía se usa en muchos lugares, el hecho de que Python 3 exista no cambia eso. Y cuando dices "no es exactamente un error sutil para cualquiera que haya usado Python por más de unos pocos meses", esa oración literalmente significa "esto solo concierne a desarrolladores inexpertos" (claramente no a ti). Y para que conste, claramente no leíste la respuesta porque dije en negrita que estaba moviendo , no copiando, el código. Los errores de copiar y pegar son bastante uniformes en todos los idiomas. Este tipo de error es más exclusivo de Python debido a su alcance; es más sutil y fácil de olvidar y perder.
user541686
3
Todavía no es una razón lógica para cambiar mapy / 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í.
wim
46

En realidad, mapy 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 mapdevuelve una lista antigua simple, tal como lo hacen las comprensiones de listas en ambos idiomas. El quid es que el valor de retorno de mapPython 3 (y imapen 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 squaresve vacío en la última print(list(squares))línea.

Para resumir:

  • Cuando se trata de iteradores, debe recordar que tienen estado y que mutan a medida que los atraviesa.
  • Las listas son más predecibles ya que solo cambian cuando las mutas explícitamente; son contenedores .
  • Y una ventaja: los números, las cadenas y las tuplas son aún más predecibles ya que no pueden cambiar en absoluto; Son valores .
raek
fuente
Este es probablemente el mejor argumento para comprender las listas. El mapa de pitones no es el mapa funcional sino el hijastro pelirrojo lisiado de una implementación funcional. Muy triste, porque realmente no me gustan las comprensiones.
semiomante
@semiomant Diría que el mapa perezoso (como en python3) es más 'funcional' que el mapa entusiasta (como en python2). Por ejemplo, el mapa en Haskell es vago (bueno, todo en Haskell es vago ...). De todos modos, el mapa diferido es mejor para encadenar mapas: si tiene un mapa aplicado al mapa aplicado al mapa, tiene una lista para cada una de las llamadas de mapa intermedias en python2, mientras que en python3 solo tiene una lista resultante, por lo que es más eficiente en memoria .
MnZrK
Supongo que lo que quiero es mapproducir 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
semiomante
Desea decir que el mapa devuelve un iterable, no un iterador.
user541686
16

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 lambdalas 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.

Dan
fuente
99
Sí, suspiro, pero la intención original de Guido a lambda quitar por completo en Python 3 consiguió un aluvión de presión en contra de ella, por lo que volvió en él a pesar de mi soporte robusto - ah bueno, lambda conjetura es demasiado útil en muchas SIMPLE casos, el único El problema es cuando excede los límites de SIMPLE o se le asigna un nombre (en cuyo último caso es un duplicado tonto de def! -).
Alex Martelli
1
La entrevista en la que está pensando es esta: amk.ca/python/writing/gvr-interview , donde Guido dice "A veces he sido demasiado rápido en aceptar contribuciones, y luego me di cuenta de que era un error. Un ejemplo sería Algunas de las características de programación funcional, como las funciones lambda. lambda es una palabra clave que le permite crear una pequeña función anónima; las funciones integradas como mapear, filtrar y reducir ejecutan una función sobre un tipo de secuencia, como una lista. "
J. Taylor
3
@Alex, no tengo tus años de experiencia, pero he visto una comprensión de listas mucho más complicada que las lambdas. Por supuesto, abusar de las características del lenguaje es siempre una tentación difícil de resistir. Es interesante que las comprensiones de listas (empíricamente) parezcan más propensas al abuso que las lambdas, aunque no estoy seguro de por qué debería ser así. También señalaré que "cojear" no siempre es algo malo. Reducir el alcance de "cosas que esta línea podría estar haciendo" a veces puede facilitarle la lectura. Por ejemplo, la constpalabra clave en C ++ es un gran triunfo en este sentido.
Stuart Berg
> guido. Lo cual es otra evidencia de que Guido está loco. Por supuesto lambda, se han hecho tan lamentables (sin declaraciones ...) que son difíciles de usar y de todos modos limitados.
javadba
16

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.

Andz
fuente
"[op1 * op2 de op1, op2 en zip (list1, list2)]" | s / form / for / Y una lista equivalente sin zip: (menos legible) [list1 [i] * list2 [i] para i in range (len (list1))]
débil
2
Debería ser "para" no "de" en su segunda cita de código, @andz, y también en el comentario de @ weakish. Pensé que había descubierto un nuevo enfoque sintáctico para comprender las listas ... Maldición.
physicsmichael
44
para agregar un comentario muy tardío, puede hacer zipperezoso usandoitertools.izip
tacaswell
55
Creo que aún prefiero map(operator.mul, list1, list2). Es en estas simples expresiones del lado izquierdo que las comprensiones se vuelven torpes.
Yann Vernier
1
No me había dado cuenta de que el mapa podría tomar varios iterables como entradas para su función y, por lo tanto, podría evitar un zip.
bli
16

Si planea escribir cualquier código asincrónico, paralelo o distribuido, probablemente preferirá mapuna comprensión de la lista, ya que la mayoría de los paquetes asincrónicos, paralelos o distribuidos proporcionan una mapfunción para sobrecargar Python map. Luego, al pasar la mapfunció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.).

Mike McKerns
fuente
9

Entonces, dado que Python 3 map()es un iterador, debe tener en cuenta lo que necesita: un iterador u listobjeto.

Como @AlexMartelli ya mencionó , map()es más rápido que la comprensión de la lista solo si no usa la lambdafunción.

Te presentaré algunas comparaciones de tiempo.

Python 3.5.2 y CPython
que he usado Júpiter portátil y especialmente %timeitcomando 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

Necesitas 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

No necesitas list objeto, solo necesita uno iterable:

Siempre uso map()!

vishes_shell
fuente
1

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 addmé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.

Rendimiento de la asignación de métodos de objetos Python

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.

craymichael
fuente
1
Como ya se explicó en otras respuestas, mapes 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]. mapEs 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 methodcallersobrecarga 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).
GZ0
1
Por cierto, las list()llamadas explícitas son un poco más lentas que las listas de comprensión. Para una comparación justa necesitas escribir [*map(...)].
GZ0
@ GZ0 gracias por los excelentes comentarios! Todo tiene sentido, y no sabía que las 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.
craymichael
0

Considero que la forma más pitónica es utilizar una lista de comprensión en lugar de mapy filter. La razón es que las comprensiones de listas son más claras que mapy 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 lambdaexpresiones adicionales como mapnecesidades. Además, una comprensión también permite filtrar fácilmente, mientras que maprequiere filterpermitir el filtrado.

lmiguelvargasf
fuente
0

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.

Mohit Raj
fuente
3
Esta es una pregunta muy antigua, y la respuesta a la que te refieres probablemente se escribió en referencia a Python 2, donde mapdevuelve una lista. En Python 3, mapse evalúa de manera perezosa, por lo que simplemente llamar mapno calcula ninguno de los nuevos elementos de la lista, por lo tanto, por qué tiene tiempos tan cortos.
kaya3
Creo que está utilizando Python 3.x Cuando le hice esta pregunta, Python 3 se había lanzado recientemente y Python 2.x era el estándar.
TimothyAWiseman