¿Por qué map devuelve un objeto de mapa en lugar de una lista en Python 3?

82

Estoy interesado en la comprensión del nuevo lenguaje de diseño de Python 3.x .

Disfruto, en Python 2.7, la función map:

Python 2.7.12
In[2]: map(lambda x: x+1, [1,2,3])
Out[2]: [2, 3, 4]

Sin embargo, en Python 3.x las cosas han cambiado:

Python 3.5.1
In[2]: map(lambda x: x+1, [1,2,3])
Out[2]: <map at 0x4218390>

Entiendo el cómo, pero no pude encontrar una referencia al por qué. ¿Por qué los diseñadores de lenguajes tomaron esta decisión, que, en mi opinión, presenta un gran dolor? ¿Fue esto para luchar con los desarrolladores al ceñirse a las listas de comprensión?

OMI, la lista se puede pensar naturalmente como Functors ; y de alguna manera se ha pensado que pienso de esta manera:

fmap :: (a -> b) -> f a -> f b
NoIdeaCómo arreglar esto
fuente
2
La razón fundamental debe ser la misma de por qué utilizamos generadores en lugar de listas por comprensión. Al utilizar la evaluación diferida, no necesitamos guardar grandes cosas en la memoria. Verifique la respuesta aceptada aquí: stackoverflow.com/questions/1303347/…
Moberg
8
¿Podría explicar por qué esto le produce "tanto dolor"?
RemcoGerlich
3
Creo que es porque los años de uso mostraron que los usos más comunes de mapsimplemente iteraron sobre el resultado. Crear una lista cuando no la necesita es ineficaz, por lo que los desarrolladores decidieron hacerla mapfloja. Hay mucho que ganar aquí para el rendimiento y no mucho que perder (si necesita una lista, solo pida una ... list(map(...))).
mgilson
3
Ok, me parece interesante que en lugar de mantener el patrón Functor y ofrecer una versión perezosa de List, de alguna manera tomaron la decisión de forzar una evaluación perezosa de una lista cada vez que se mapea. Hubiera preferido tener el derecho de hacer mi propia elección, también conocido como Generador -> mapa -> Generador o Lista -> mapa -> Lista (depende de mí para decidir)
NoIdeaHowToFixThis
4
@NoIdeaHowToFixThis, en realidad depende de usted, si necesita la lista completa, simplemente
transfórmela

Respuestas:

37

Creo que la razón por la cual un mapa todavía existe en absoluto cuando las expresiones generadoras también existen, es que puede tomar múltiples argumentos iterador que están todos en bucle una y pasados a la función:

>>> list(map(min, [1,2,3,4], [0,10,0,10]))
[0,2,0,4]

Eso es un poco más fácil que usar zip:

>>> list(min(x, y) for x, y in zip([1,2,3,4], [0,10,0,10]))

De lo contrario, simplemente no agrega nada sobre las expresiones del generador.

RemcoGerlich
fuente
1
Creo que si agregamos el deseo de enfatizar que las listas por comprensión son más pitónicas y los diseñadores del lenguaje quisieron enfatizar eso, creo que esta es la respuesta más acertada. @vishes_shell de alguna manera no se enfoca lo suficiente en el diseño del lenguaje.
NoIdeaHowToFixThis
2
Produce resultados diferentes en Python 2 y 3 si las dos listas no tienen la misma longitud . Pruebe c = list(map(max, [1,2,3,4], [0,10,0,10, 99]))en python 2 y en python 3.
cdarke
1
Aquí hay una referencia del plan original para eliminar el mapa por completo de python3: artima.com/weblogs/viewpost.jsp?thread=98196
Bernhard
Hmm, qué extraño cuando envuelvo el mapa en la lista, obtengo una lista de listas de 1 elemento.
awiebe
24

Debido a que devuelve un iterador, omite almacenar la lista de tamaño completo en la memoria. Para que pueda repetirlo fácilmente en el futuro sin causar ningún dolor en la memoria. Es posible que ni siquiera necesite una lista completa, sino una parte de ella, hasta que se alcance su condición.

Puede encontrar útiles estos documentos , los iteradores son increíbles.

Objeto que representa un flujo de datos. Las llamadas repetidas al __next__()método del iterador (o pasarlo a la función incorporada next()) devuelven elementos sucesivos en la secuencia. Cuando no hay más datos disponibles, se genera una StopIterationexcepción. En este punto, el objeto iterador se agota y cualquier llamada adicional a su __next__()método simplemente se activa StopIterationnuevamente. Se requiere __iter__()que los iteradores tengan un método que devuelva el objeto iterador en sí, por lo que cada iterador también es iterable y puede usarse en la mayoría de los lugares donde se aceptan otros iterables. Una excepción notable es el código que intenta múltiples pases de iteración. Un objeto contenedor (como a list) produce un nuevo iterador cada vez que lo pasa aliter()función o utilizarlo en un bucle for. Intentar esto con un iterador solo devolverá el mismo objeto de iterador agotado que se usó en la pasada de iteración anterior, haciéndolo aparecer como un contenedor vacío.

vishes_shell
fuente
14

Guido responde a esta pregunta aquí : " ya que crear una lista sería un desperdicio ".

También dice que la transformación correcta es usar un forbucle regular .

La conversión map()de 2 a 3 podría no ser solo un simple caso de pegarse una list( )ronda. Guido también dice:

"Si las secuencias de entrada no tienen la misma longitud, map()se detendrá en la terminación de la más corta de las secuencias. Para una compatibilidad total con map()Python 2.x, también envuelva las secuencias itertools.zip_longest(), por ejemplo

map(func, *sequences)

se convierte en

list(map(func, itertools.zip_longest(*sequences)))

"

cdarke
fuente
3
El comentario de Guido se map()invoca por los efectos secundarios de la función , no por su uso como functor.
abukaj
4
La transformación con zip_longestestá mal. usted tiene que utilizar itertools.starmappara que sea equivalente: list(starmap(func, zip_longest(*sequences))). Esto se debe a que zip_longestproduce tuplas, por lo funcque recibiría un único nargumento -uple en lugar de nargumentos distintos como es el caso al llamar map(func, *sequences).
Bakuriu
12

En Python 3, muchas funciones (no solo mappero zip, rangey otras) devuelven un iterador en lugar de la lista completa. Es posible que desee un iterador (por ejemplo, para evitar mantener la lista completa en la memoria) o puede querer una lista (por ejemplo, para poder indexar).

Sin embargo, creo que la razón clave del cambio en Python 3 es que, si bien es trivial convertir un iterador en una lista utilizando list(some_iterator)el equivalente inverso, iter(some_list)no se logra el resultado deseado porque la lista completa ya se ha creado y se ha guardado en la memoria.

Por ejemplo, en Python 3 list(range(n))funciona bien ya que hay un bajo costo para construir el rangeobjeto y luego convertirlo en una lista. Sin embargo, en Python 2 iter(range(n))no se guarda memoria porque la lista completa se construye range()antes de que se construya el iterador.

Por lo tanto, en Python 2, se requieren funciones separadas para crear un iterador en lugar de una lista, como imapfor map(aunque no son del todo equivalentes ), xrangefor range, izipfor zip. Por el contrario, Python 3 solo requiere una única función, ya que una list()llamada crea la lista completa si es necesario.

Chris_Rands
fuente
AFAIK en Python 2.7 también funciona desde itertoolsiteradores de retorno. Además, no vería los iteradores como listas diferidas, ya que las listas se pueden iterar varias veces y se puede acceder a ellas de forma aleatoria.
abukaj
@abukaj ok gracias, he editado mi respuesta para tratar de ser más claro
Chris_Rands
@IgorRivin ¿a qué te refieres? Los mapobjetos de Python 3 tienen un next()método. Los rangeobjetos de rango de Python 3 no son estrictamente iteradores, lo sé
Chris_Rands
@Chris_Rands en mi Python 3.6.2 de distribución Anaconda, al hacer foo = map(lambda x: x, [1, 2, 3])devuelve un objeto de mapa foo. hacer foo.next()vuelve con un error:'map' object has no attribute 'next'
Igor Rivin
1
@IgorRivin: Los métodos que comienzan y terminan con __están reservados para Python; sin esa reserva, tiene el problema de distinguir las cosas para las que nextes solo un método (no son realmente iteradores) y las cosas que son iteradores. En la práctica, debe omitir los métodos y simplemente usar la next()función (por ejemplo next(foo)), que funciona correctamente en todas las versiones de Python a partir de la 2.6. Es la misma forma en que lo usa len(foo), aunque foo.__len__()funcionaría bien; Los métodos dunder generalmente no están destinados a ser llamados directamente, sino implícitamente como parte de alguna otra operación.
ShadowRanger