Función lambda en comprensiones de listas

150

¿Por qué la salida de las siguientes dos comprensiones de lista es diferente, aunque fla lambdafunción sea la misma?

f = lambda x: x*x
[f(x) for x in range(10)]

y

[lambda x: x*x for x in range(10)]

Eso sí, ambos type(f)y type(lambda x: x*x)devuelven el mismo tipo.

usuario763191
fuente
[lambda x: x*x for x in range(10)]es más rápido que el primero, ya que no llama a una función de bucle externo, f repetidamente.
riza
@Selinap: ... no, en lugar de eso, estás creando una función completamente nueva cada vez que realizas el ciclo. ... y la sobrecarga de crear esta nueva función, luego llamar es un poco más lento (en mi sistema de todos modos).
Gerrat
@Gerrat: Incluso con gastos generales, aún es más rápido. Pero, por supuesto, [x*x for x in range(10)]es mejor.
riza
34
Acabo de entrar aquí para obtener acceso a google foobar :)
Gal Margalit

Respuestas:

268

El primero crea una sola función lambda y la llama diez veces.

El segundo no llama a la función. Crea 10 funciones lambda diferentes. Pone a todos en una lista. Para que sea equivalente al primero que necesita:

[(lambda x: x*x)(x) for x in range(10)]

O mejor aún:

[x*x for x in range(10)]
Winston Ewert
fuente
13
O map(lambda x: x*x, range(10)), que probablemente era lo que el OP quería decir en primer lugar.
Daniel Roseman
sí, lambda x: x * x .. (x) parece un principio.
staticor
[lambda x: x * x para x en el rango (10)] es básicamente una función en Haskell
moshe beeri
@DanielRoseman, o para ser precisos list(map(lambda x: x*x, range(10)))te dará[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
rrlamichhane
108

Esta pregunta toca una parte muy apestosa de la sintaxis de Python "famosa" y "obvia": lo que tiene prioridad, la lambda o la comprensión de la lista.

No creo que el objetivo del OP sea generar una lista de cuadrados de 0 a 9. Si ese fuera el caso, podríamos dar aún más soluciones:

squares = []
for x in range(10): squares.append(x*x)
  • Esta es la buena forma de sintaxis imperativa.

Pero no es el punto. El punto es W (hy) TF ¿es esta expresión ambigua tan contraintuitiva? Y tengo un caso idiota para ti al final, así que no descartes mi respuesta demasiado pronto (la tuve en una entrevista de trabajo).

Entonces, la comprensión del OP devolvió una lista de lambdas:

[(lambda x: x*x) for x in range(10)]

Por supuesto, esto es solo 10 copias diferentes de la función de cuadratura, ver:

>>> [lambda x: x*x for _ in range(3)]
[<function <lambda> at 0x00000000023AD438>, <function <lambda> at 0x00000000023AD4A8>, <function <lambda> at 0x00000000023AD3C8>]

Tenga en cuenta las direcciones de memoria de las lambdas: ¡todas son diferentes!

Por supuesto, podría tener una versión más "óptima" (jaja) de esta expresión:

>>> [lambda x: x*x] * 3
[<function <lambda> at 0x00000000023AD2E8>, <function <lambda> at 0x00000000023AD2E8>, <function <lambda> at 0x00000000023AD2E8>]

¿Ver? 3 veces la misma lambda.

Tenga en cuenta que utilicé _comofor variable. No tiene nada que ver con el xen el lambda(se eclipsa léxico!). ¿Consíguelo?

Estoy dejando de lado la discusión, por qué la precedencia de sintaxis no es así, que todo significaba:

[lambda x: (x*x for x in range(10))]

que podría ser: [[0, 1, 4, ..., 81]]o [(0, 1, 4, ..., 81)], o lo que me parece más lógico , esto sería unlist elemento de 1: un generatorretorno de los valores. Simplemente no es el caso, el lenguaje no funciona de esta manera.

PERO qué, si ...

¿Qué pasa si NO eclipsas el for variable Y la usas en tu lambdas ???

Bueno, entonces pasa una mierda. Mira este:

[lambda x: x * i for i in range(4)]

esto significa, por supuesto:

[(lambda x: x * i) for i in range(4)]

PERO NO SIGNIFICA:

[(lambda x: x * 0), (lambda x: x * 1), ... (lambda x: x * 3)]

¡Esto es una locura!

Las lambdas en la lista de comprensión son un cierre sobre el alcance de esta comprensión. Un cierre léxico , por lo que se refieren a lai referencia vía, y no a su valor cuando fueron evaluados!

Entonces, esta expresión:

[(lambda x: x * i) for i in range(4)]

ES aproximadamente EQUIVALENTE a:

[(lambda x: x * 3), (lambda x: x * 3), ... (lambda x: x * 3)]

Estoy seguro de que podríamos ver más aquí usando un descompilador de Python (con lo que quiero decir, por ejemplo, el dismódulo), pero para la discusión independiente de Python-VM esto es suficiente. Esto en cuanto a la pregunta de la entrevista de trabajo.

Ahora, ¿cómo hacer un listlambdas multiplicador, que realmente se multiplica por enteros consecutivos? Bueno, de manera similar a la respuesta aceptada, necesitamos romper el vínculo directo ienvolviéndolo en otro lambda, que se llama dentro la expresión de comprensión de la lista:

Antes de:

>>> a = [(lambda x: x * i) for i in (1, 2)]
>>> a[1](1)
2
>>> a[0](1)
2

Después:

>>> a = [(lambda y: (lambda x: y * x))(i) for i in (1, 2)]
>>> a[1](1)
2
>>> a[0](1)
1

(También tenía la variable lambda externa = i, pero decidí que esta es la solución más clara: presentéy para que todos podamos ver qué bruja es cuál).

Editar 2019-08-30:

Siguiendo una sugerencia de @josoler, que también está presente en una respuesta de @sheridp: el valor de la "variable de bucle" de comprensión de la lista se puede "incrustar" dentro de un objeto; la clave es acceder a él en el momento adecuado. La sección "Después" anterior lo hace envolviéndolo en otro lambday llamándolo inmediatamente con el valor actual de i. Otra forma (un poco más fácil de leer, ya que no produce ningún efecto 'WAT') es almacenar el valor identro de un partialobjeto y hacer que el "interno" (original) lo lambdatome como argumento (aprobado por elpartial objeto en el hora de la llamada), es decir:

Después de 2:

>>> from functools import partial
>>> a = [partial(lambda y, x: y * x, i) for i in (1, 2)]
>>> a[0](2), a[1](2)
(2, 4)

Genial, ¡pero todavía hay un pequeño giro para ti! Digamos que no queremos facilitar el uso del lector de código y pasar el factor por nombre (como argumento de palabra clave parapartial ). Hagamos un cambio de nombre:

Después de 2.5:

>>> a = [partial(lambda coef, x: coef * x, coef=i) for i in (1, 2)]
>>> a[0](1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: <lambda>() got multiple values for argument 'coef'

WAT?

>>> a[0]()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: <lambda>() missing 1 required positional argument: 'x'

Espera ... ¿Estamos cambiando el número de argumentos por 1 y pasando de "demasiados" a "muy pocos"?

Bueno, no es un WAT real, cuando pasamos coefde partialesta manera, se convierte en un argumento de palabra clave, por lo que debe venir después del posicionalx argumento , así:

Después de 3:

>>> a = [partial(lambda x, coef: coef * x, coef=i) for i in (1, 2)]
>>> a[0](2), a[1](2)
(2, 4)

Preferiría la última versión sobre la lambda anidada, pero a cada una su propia ...

Tomasz Gandor
fuente
22
esa es una pregunta de entrevista de trabajo cruel e inusual.
szeitlin
1
Si mi colega no preguntara, probablemente nunca buscaría esta respuesta
piggybox
8
Guau. Acabo de ser mordido mal por este comportamiento absurdo. ¡Gracias por tu publicación!
Ant
1
Excelente respuesta También me encontré con este problema también. Por un lado, apunta a una limitación de Python, pero por otro lado también podría ser un indicador de olor de código. Estoy usando esta solución para un proyecto de juguete, pero podría ser una señal para reestructurar en un entorno de producción.
ahota
2
En aras de la claridad y la integridad, podría escribir la última lista de comprensión como:[partial(lambda i, x: i * x, i) for i in (1, 2)]
josoler
19

La gran diferencia es que el primer ejemplo en realidad invoca la lambda f(x), mientras que el segundo ejemplo no.

Su primer ejemplo es equivalente a [(lambda x: x*x)(x) for x in range(10)]mientras que su segundo ejemplo es equivalente a [f for x in range(10)].

Gabe
fuente
11

El primero

f = lambda x: x*x
[f(x) for x in range(10)]

se ejecuta f()para cada valor en el rango, por lo que lo hace f(x)para cada valor

el segundo

[lambda x: x*x for x in range(10)]

ejecuta el lambda para cada valor en la lista, por lo que genera todas esas funciones.

zellio
fuente
11

La gente dio buenas respuestas pero olvidó mencionar la parte más importante en mi opinión: en el segundo ejemplo, la Xcomprensión de la lista NO es la misma que la Xde la lambdafunción, no tienen ninguna relación. Entonces, el segundo ejemplo es en realidad el mismo que:

[Lambda X: X*X for I in range(10)]

Las iteraciones internas en range(10) solo son responsables de crear 10 funciones lambda similares en una lista (10 funciones separadas pero totalmente similares, devolviendo la potencia 2 de cada entrada).

Por otro lado, el primer ejemplo funciona totalmente diferente, porque la X de las iteraciones SI interactúa con los resultados, para cada iteración el valor es, X*Xpor lo tanto, el resultado sería[0,1,4,9,16,25, 36, 49, 64 ,81]

Seid Tidhar
fuente
Éste es un punto importante. Te voté y lo expliqué en mi respuesta.
Tomasz Gandor
6

Las otras respuestas son correctas, pero si está tratando de hacer una lista de funciones, cada una con un parámetro diferente, que se pueda ejecutar más tarde , el siguiente código lo hará:

import functools
a = [functools.partial(lambda x: x*x, x) for x in range(10)]

b = []
for i in a:
    b.append(i())

In [26]: b
Out[26]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Si bien el ejemplo está ideado, lo encontré útil cuando quería una lista de funciones en las que cada una imprime algo diferente, es decir

import functools
a = [functools.partial(lambda x: print(x), x) for x in range(10)]

for i in a:
    i()
sheridp
fuente