Pregunta Python List Comprehension vs. Mapa


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


536
2017-08-07 23:43


origen


Respuestas:


map puede ser microscópicamente más rápido en algunos casos (cuando NO está haciendo una lambda para este propósito, pero usando la misma función en el mapa y en un listcomp). Las listas de comprensión 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 invierte 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

502
2017-08-07 23:45



Casos

  • Caso común: Casi siempre, querrás usar una lista de comprensión en pitón porque será más obvio lo que está haciendo con los programadores noveles que leen su 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 con los programadores de Python, ya que las listas de comprensión son el estándar de facto en python para la iteración; son esperado.
  • Caso menos común: Sin embargo, si Ya tiene una función definida, a menudo es razonable usar map, aunque se considera "antiponético". Por ejemplo, map(sum, myLists) es más elegante / cortante que [sum(x) for x in myLists]. Obtienes la elegancia de no tener que inventar una variable ficticia (p. sum(x) for x... o sum(_) for _... o sum(readableName) for readableName...) que debe escribir dos veces, solo para repetir. El mismo argumento vale para filter y reduce y cualquier cosa del itertools módulo: si ya tiene una función a mano, puede seguir adelante y realizar alguna programación funcional. Esto mejora la legibilidad en algunas situaciones y la pierde en otras (por ejemplo, programadores noveles, argumentos múltiples) ... pero la legibilidad de su código depende en gran medida de sus comentarios de todos modos.
  • Casi nunca: Es posible que desee utilizar el map funcionar como una función abstracta pura mientras se realiza la programación funcional, donde está mapeando mapo currying map, o beneficiarse de hablar de map como una función. En Haskell, por ejemplo, una interfaz 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 te obliga a utilizar el estilo de generador para hablar de iteración; no puedes generalizarlo fácilmente (Esto a veces es bueno y otras malo). Probablemente se te ocurran ejemplos raros de pitón donde map(f, *lists) es algo razonable de hacer El ejemplo más cercano que puedo encontrar sería sumEach = partial(map,sum), que es un trazador de líneas que es más o menos equivalente a:

def sumEach(myLists):
    return [sum(_) for _ in myLists]
  • Solo usando un for-lazo: También puede, por supuesto, simplemente usar un for-loop. Aunque no es tan elegante desde el punto de vista de la programación funcional, algunas veces las variables no locales hacen que el código sea más claro en lenguajes de programación imperativos como python, porque las personas están muy acostumbradas a leer el código de esa manera. Los bucles for son, generalmente, los más eficientes cuando simplemente se está realizando una operación compleja que no está compilando una lista como lista-las comprensiones y el mapa están optimizados para (por ejemplo, sumar, 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 algún raro holocausto patológico de recolección de basura).

"Pythonism"

No me gusta la palabra "pitonica" porque no encuentro que la pitonica siempre sea elegante a mis ojos. Sin embargo, map y filter y funciones similares (como la muy útil itertools módulo) probablemente se consideran antiponónicos en términos de estilo.

pereza

En términos de eficiencia, como la mayoría de los constructos de programación funcional, MAPA PUEDE SER PEREZOSO, y de hecho es flojo en python. Eso significa que puedes 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 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 listas de comprensión también son intrínsecamente flojas, pero Python ha elegido implementarlos como no perezosos. Sin embargo, python sí admite la comprensión de la lista perezosa 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 en una expresión del 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 flojas, por lo que puede requerir más memoria (a menos que use comprensiones de generador). Los corchetes [...] a menudo hacen las cosas obvias, especialmente cuando están en un lío entre paréntesis. Por otro lado, a veces terminas siendo prolijo como mecanografiar [x for x in.... Siempre y cuando mantenga cortas las variables de los iteradores, las listas de comprensión suelen ser 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 flojo:

% 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 de generador en python2 o python3) evitarán calcular sus valores hasta el último momento necesario. Por lo general, esto generalmente superará cualquier sobrecarga de 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 pitón solo se pueden evaluar por orden x[0], x[1], x[2], ....

Sin embargo, digamos que tenemos una función prefabricada f a nosotros nos gustaria map, e ignoramos la pereza de map al forzar inmediatamente la evaluación con list(...). Obtenemos 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>)

En los resultados están en la forma AAA / BBB / CCC donde se ejecutó A en una estación de trabajo Intel circa-2010 con python 3.?.?, Y B y C se realizaron con una estación de trabajo AMD circa-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, que se ve muy afectado por otros factores aleatorios. Lo único que podemos decir parece ser que, curiosamente, mientras esperamos listas de comprensión [...] para funcionar mejor que las expresiones del generador (...), map TAMBIÉN es más eficiente que las expresiones del generador (asumiendo de nuevo que todos los valores son evaluados / usados).

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 como f=lambda x:x+x)

Si eres hábil para leer el montaje de Python, puedes usar dis módulo para ver si eso es realmente lo que sucede 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 [...] sintaxis que list(...). Tristemente map la clase es un poco opaca al desmontaje, pero podemos cumplir con nuestra prueba de velocidad.


359
2018-06-20 05:41



Deberías usar map y filter en lugar de listas de comprensión.

Un objetivo por lo que debes preferirlos a pesar de que no son "Ptónicos" es esto:
Requieren funciones / lambdas como argumentos, que introduce un nuevo alcance.

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 en cambio yo hubiera dicho:

for x, y in somePoints:
    # (several lines of code here)
    squared = map(lambda x: x ** 2, numbers)

entonces todo habría 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 movido el bloque interno a una sección diferente del código que surgió el problema (léase: problema durante el mantenimiento, no desarrollo), y no lo esperaba.

Sí, si nunca cometes este error luego enumera las comprensiones son más elegantes.
Pero a partir de la experiencia personal (y de ver a los demás cometer el mismo error) lo he visto suceder lo suficiente que creo que no vale la pena el dolor que tienes que atravesar cuando estos errores se infiltran en tu código.

Conclusión:

Utilizar map y filter. Evitan errores sutiles difíciles de diagnosticar relacionados con el alcance.

Nota al margen:

No olvides considerar usar imap y ifilter (en itertools) si son apropiados para su situación!


76
2017-11-20 22:28



Actualmente, map y las comprensiones de la lista se comportan de manera bastante diferente en el lenguaje de 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 miras squares parece comportarse como una secuencia de tres elementos, pero la segunda vez como una vacía.

En el lenguaje Python 2 map devuelve una lista simple y antigua, al igual que las listas de comprensión en ambos idiomas. El quid es que el valor de retorno de map en 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. Esta es la razón por squares parece vacío en el último print(list(squares)) línea.

Para resumir:

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

33
2017-10-01 13:09



Encuentro que las listas de comprensión son generalmente más expresivas de lo que intento hacer que map - Ambos lo hacen, pero el primero ahorra la carga mental de tratar de entender lo que podría ser un complejo lambdaexpresión.

También hay una entrevista por ahí en algún lugar (no puedo encontrarlo de forma informal) donde Guido enumera lambday las funciones funcionales como lo que más lamenta de aceptar en Python, por lo que podrías argumentar que no son pitonicas en virtud de eso.


15
2017-08-07 23:59



Aquí hay un posible caso:

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.


12
2017-11-02 08:42



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


12
2018-06-08 17:03



Otra razón para utilizar la comprensión de lista sobre map () y filter () es que Psyco no puede compilar estas funciones.

Ver http://psyco.sourceforge.net/


6
2018-02-21 21:27



Entonces desde Python 3, map() es un iterador, debe tener en cuenta lo que necesita: un iterador o list objeto.

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

Les presentaré algunas comparaciones de tiempo.

Python 3.5.2 y CPython
he usado Cuaderno de Júpiter y especialmente %timeit comando mágico incorporado
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 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 list objeto:

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

No necesitas list objeto, solo necesitas uno iterativo:

Siempre usa map()!


5
2017-12-03 14:18