Lista de comprensión vs mapa

¿Hay alguna razón para preferir usar map() sobre la comprensión de lista o viceversa? ¿Alguno de ellos es generalmente más eficiente o se considera generalmente más python que el otro?

map puede ser microscópicamente más rápido en algunos casos (cuando NO está creando un lambda para este propósito, sino utilizando la misma función en el mapa y una lista de componentes). La lista de comprensión puede ser más rápida en otros casos y la mayoría (no todos) de los pitones los consideran más directos y claros.

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 invierte completamente cuando el mapa necesita un 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 

Casos

  • Caso común : Casi siempre, querrá usar una lista de comprensión en python porque será más obvio lo que está haciendo a los progtwigdores principiantes que leen su código. (Esto no se aplica a otros idiomas, donde pueden aplicarse otros modismos). Incluso será más obvio lo que está haciendo con los progtwigdores de Python, ya que las comprensiones de listas son el estándar de facto en python para la iteración; ellos son esperados
  • Caso menos común : sin embargo, si ya tiene una función definida , a menudo es razonable usar el map , aunque se considera “no-lípico”. Por ejemplo, map(sum, myLists) es más elegante / terso que [sum(x) for x in myLists] . Obtiene la elegancia de no tener que crear una variable ficticia (por ejemplo, sum(x) for x... o sum(_) for _... o sum(readableName) for readableName... ) que debe escribir Dos veces, sólo para iterar. El mismo argumento es válido para filter y reduce y cualquier cosa del módulo itertools : si ya tiene una función a mano, puede seguir adelante y hacer algo de progtwigción funcional. Esto gana legibilidad en algunas situaciones, y lo pierde en otras (por ejemplo, progtwigdores novatos, múltiples argumentos) … pero la legibilidad de su código depende en gran medida de sus comentarios.
  • Casi nunca : es posible que desee utilizar la función de map como una función pura y abstracta mientras realiza la progtwigción funcional, donde está mapeando el map , o estudiando el map , o de lo contrario se beneficia de hablar del map como una función. En Haskell, por ejemplo, una interfaz de functor llamada fmap 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 de generador para hablar de iteración; No puedes generalizarlo fácilmente. (Esto a veces es bueno y a veces malo). Es probable que pueda encontrar ejemplos raros de python en los que el map(f, *lists) sea ​​una cosa razonable. El ejemplo más cercano que puedo encontrar sería sumEach = partial(map,sum) , que es una sumEach = partial(map,sum) de línea que es aproximadamente equivalente a:
 def sumEach(myLists): return [sum(_) for _ in myLists] 
  • Solo usando un for loop : Por supuesto, también puede usar un for-loop. Aunque no son tan elegantes desde el punto de vista de la progtwigción funcional, a veces las variables no locales hacen que el código sea más claro en los lenguajes de progtwigción imperativos como python, porque la gente está muy acostumbrada a leer el código de esa manera. Los bucles for también son, por lo general, los más eficientes cuando simplemente está realizando una operación compleja que no está construyendo una lista, como la lista de comprensión y el mapa están optimizados para (por ejemplo, sumr 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, a menos que un raro hipo patológico de recolección de basura).

“Pitonismo”

No me gusta la palabra “pythonic” porque no encuentro que pythonic sea siempre elegante en mis ojos. Sin embargo, el map y el filter y funciones similares (como el módulo de itertools muy útil) probablemente se consideren antípticos en términos de estilo.

pereza

En términos de eficiencia, como la mayoría de las construcciones de progtwigción funcionales, MAP PUEDE SER PERZOSO , y de hecho es perezoso en python. Eso significa que puedes hacer esto (en python3 ) y tu computadora no se quedará sin memoria y perderá todos tus datos no guardados:

 >>> map(str, range(10**100))  

Intenta hacer eso 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 intrínsecamente perezosas, pero python ha elegido implementarlas como no perezosas . Sin embargo, python admite la comprensión de listas perezosas en forma de expresiones generadoras, de la siguiente manera:

 >>> (str(n) for n in range(10**100))  at 0xacbdef> 

Básicamente, puede pensar que la [...] syntax pasa una expresión generadora al constructor de listas, como la 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 la lista no son perezosas, por lo que pueden requerir más memoria (a menos que use las comprensiones del generador). Los corchetes a menudo [...] hacen que las cosas sean obvias, especialmente cuando están en un lío de paréntesis. Por otro lado, a veces terminas siendo verboso como escribir [x for x in... Mientras mantengas cortas las variables de tu iterador, la comprensión de las listas suele ser más clara si no se sangra el 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 es ahora perezoso:

 % 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 si no sabe de antemano la cantidad de datos que necesita, mapee en python3 (y las expresiones generadoras en python2 o python3) evitará calcular sus valores hasta el último momento necesario. Por lo general, esto usualmente será mayor que cualquier sobrecarga por el uso del map . La desventaja es que esto es muy limitado en python en comparación con la mayoría de los lenguajes funcionales: solo obtiene este beneficio si accede a sus datos de izquierda a derecha “en orden”, porque las expresiones del generador de python solo se pueden evaluar en el orden x[0], x[1], x[2], ...

Sin embargo, digamos que tenemos una función pre-hecha f nos gustaría map , e ignoramos la pereza del map forzando inmediatamente la evaluación con la list(...) . Conseguimos 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() % 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(), 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() 

Los resultados se encuentran en la forma AAA / BBB / CCC donde A se realizó con una estación de trabajo Intel alrededor de 2010 con python 3, y B y C se realizaron con una estación de trabajo AMD alrededor de 2013 con python 3.2.1, Con hardware extremadamente diferente. El resultado parece ser que las interpretaciones de los mapas y las listas son comparables en cuanto al rendimiento, que está más fuertemente afectado por otros factores aleatorios. Lo único que podemos decir es que, por extraño que parezca, aunque esperamos que las comprensiones de listas funcionen mejor que las expresiones generadoras (...) , el map TAMBIÉN es más eficiente que las expresiones generadoras (nuevamente, suponiendo que todos los valores son evaluado / utilizado).

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 despreciable en comparación con otros factores en el progtwig. (Puede ser interesante probar con otras cosas simples como f=lambda x:x+x )

Si tiene habilidad para leer el ensamblaje de python, puede usar el módulo dis para ver si eso es realmente 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 ( 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 ( 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 ( 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 ( 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 [...] syntax que list(...) . Lamentablemente, la clase de map es un poco opaca al desensamblaje, pero podemos hacerla con nuestra prueba de velocidad.

Debe usar el map y el filter lugar de las listas de comprensión.

Una razón objetiva por la que deberías preferirlos aunque no sean “Pythonic” es esta:
Requieren funciones / lambdas como argumentos, que introducen un nuevo scope .

Me he 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 por el contrario 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: las dos x no estaban en el mismo ámbito.
Fue solo después de mover el bloque interno a una sección diferente del código que surgió el problema (lea: problema durante el mantenimiento, no desarrollo), y no lo esperaba.

Sí, si nunca comete este error , las listas de comprensión son más elegantes.
Pero por experiencia personal (y por ver a otros cometer el mismo error), he visto que esto sucede tantas veces que creo que no vale la pena el dolor que tiene que atravesar cuando estos errores ingresan en su código.

Conclusión:

Usa el map y el filter . Previenen errores sutiles difíciles de diagnosticar relacionados con el scope.

Nota al margen:

¡No olvide considerar el uso de imap y ifilter (en itertools ) si son apropiados para su situación!

En realidad, las comprensiones de map y listas se comportan de manera muy diferente en el lenguaje Python 3. Echa un vistazo al siguiente progtwig 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 miras los squares parece que se comporta como una secuencia de tres elementos, pero la segunda vez como un vacío.

En el map idiomas de Python 2, se muestra una lista antigua, tal como lo hacen las comprensiones de listas en ambos idiomas. El problema es que el valor de retorno del map en Python 3 (e imap en Python 2) no es una lista, ¡es un iterador!

Los elementos se consumen cuando se itera en un iterador, a diferencia de cuando se itera en una lista. Esta es la razón por la que los squares aparecen vacíos en la última línea de print(list(squares)) .

Para resumir:

  • Cuando se trata de iteradores, debe recordar que son de estado y que mutan a medida que los atraviesa.
  • Las listas son más predecibles ya que solo cambian cuando las mates explícitamente; son contenedores .
  • Y una ventaja adicional: los números, las cadenas y las tuplas son aún más predecibles ya que no pueden cambiar en absoluto; son valores

Encuentro que las comprensiones de listas son generalmente más expresivas de lo que estoy tratando de hacer que de map , ambas lo logran, pero la primera guarda la carga mental de tratar de entender lo que podría ser una expresión lambda compleja.

También hay una entrevista en algún lugar (no puedo encontrarlo de antemano) donde Guido enumera lambda s y las funciones funcionales como la cosa que más lamenta de aceptar en Python, por lo que podría argumentar que son antipythonicos. virtud de eso

Si planea escribir cualquier código asíncrono, paralelo o distribuido, es probable que prefiera map sobre una lista de comprensión, ya que la mayoría de los paquetes asíncronos, paralelos o distribuidos proporcionan una función de map para sobrecargar el map de Python. Luego, al pasar la función de map apropiada al rest de su código, es posible que no tenga que modificar su código de serie original para que se ejecute en paralelo (etc.).

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 un gasto desafortunado e innecesario que debe realizar si insiste en usar las listas de comprensión en lugar del mapa. Sería genial si alguien aclara esto de manera afirmativa o negativa.

Entonces, como Python 3, map() es un iterador, debes tener en cuenta lo que necesitas: un iterador o un objeto de list .

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

Les presentaré algunas comparaciones de tiempo.

Python 3.5.2 y CPython
He usado el cuaderno de Júpiter y especialmente el comando mágico incorporado %timeit
Medidas : 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 

función lambda

 %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 una expresión del generador, ver PEP-0289 . Así que 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 objeto de list :

Use la comprensión de lista si es una función personalizada, use list(map()) si hay una función incorporada

No necesita un objeto de list , solo necesita uno iterable:

Siempre usa map() !

Considero que la forma más Pythonic es usar una lista de comprensión en lugar de un map y filter . La razón es que las listas de comprensión son más claras que el map y el 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 usted ve, una comprensión no requiere expresiones lambda adicionales como las necesidades del map . Además, una comprensión también permite filtrar fácilmente, mientras que el map requiere un filter para permitir el filtrado.