Alcance de las funciones lambda y sus parámetros

89

Necesito una función de devolución de llamada que sea casi exactamente igual para una serie de eventos de interfaz gráfica de usuario. La función se comportará de forma ligeramente diferente según el evento que la haya llamado. Me parece un caso simple, pero no puedo entender este comportamiento extraño de las funciones lambda.

Entonces tengo el siguiente código simplificado a continuación:

def callback(msg):
    print msg

#creating a list of function handles with an iterator
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(lambda: callback(m))
for f in funcList:
    f()

#create one at a time
funcList=[]
funcList.append(lambda: callback('do'))
funcList.append(lambda: callback('re'))
funcList.append(lambda: callback('mi'))
for f in funcList:
    f()

El resultado de este código es:

mi
mi
mi
do
re
mi

Esperaba:

do
re
mi
do
re
mi

¿Por qué el uso de un iterador arruinó las cosas?

Intenté usar una copia profunda:

import copy
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(lambda: callback(copy.deepcopy(m)))
for f in funcList:
    f()

Pero esto tiene el mismo problema.

Agartland
fuente
3
El título de su pregunta es algo engañoso.
lispmachine
1
¿Por qué usar lambdas si las encuentra confusas? ¿Por qué no usar def para definir funciones? ¿Qué tiene tu problema que hace que las lambdas sean tan importantes?
S.Lott
La función anidada @ S.Lott resultará en el mismo problema (tal vez más claramente visible)
lispmachine
1
@agartland: ¿Eres yo? Yo también estaba trabajando en eventos de GUI, y escribí la siguiente prueba casi idéntica antes de encontrar esta página durante la investigación de antecedentes: pastebin.com/M5jjHjFT
imallett
5
Consulte ¿Por qué las lambdas definidas en un bucle con valores diferentes devuelven el mismo resultado? en las Preguntas frecuentes de programación oficiales para Python. Explica el problema bastante bien y ofrece una solución.
abarnert

Respuestas:

79

El problema aquí es que la mvariable (una referencia) se toma del alcance circundante. Solo los parámetros se mantienen en el ámbito lambda.

Para resolver esto, debe crear otro alcance para lambda:

def callback(msg):
    print msg

def callback_factory(m):
    return lambda: callback(m)

funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(callback_factory(m))
for f in funcList:
    f()

En el ejemplo anterior, lambda también usa el alcance circundante para buscar m, pero esta vez su callback_factoryalcance se crea una vez por cada callback_factory llamada.

O con functools.partial :

from functools import partial

def callback(msg):
    print msg

funcList=[partial(callback, m) for m in ('do', 're', 'mi')]
for f in funcList:
    f()
lispmachine
fuente
2
Esta explicación es un poco engañosa. El problema es el cambio de valor de m en la iteración, no el alcance.
Ixx
El comentario anterior es cierto como lo señaló @abarnert en el comentario sobre la pregunta donde también se proporciona un enlace que explica el fenonimon y la solución. El método de fábrica ofrece el mismo efecto que el argumento del método de fábrica tiene el efecto de crear una nueva variable con alcance local a la lambda. Sin embargo, la solición dada no funciona sintácticamente ya que no hay argumentos para la lambda, y la solución lambda en lamda a continuación también ofrece el mismo efecto sin crear un nuevo método persistente para crear una lambda
Mark Parris
132

Cuando se crea una lambda, no hace una copia de las variables en el ámbito adjunto que utiliza. Mantiene una referencia al entorno para que luego pueda buscar el valor de la variable. Solo hay uno m. Se le asigna cada vez que pasa por el bucle. Después del ciclo, la variable mtiene valor 'mi'. Entonces, cuando ejecute la función que creó más tarde, buscará el valor de men el entorno que la creó, que para entonces tendrá valor 'mi'.

Una solución común e idiomática a este problema es capturar el valor de men el momento en que se crea el lambda usándolo como el argumento predeterminado de un parámetro opcional. Por lo general, usa un parámetro con el mismo nombre para no tener que cambiar el cuerpo del código:

for m in ('do', 're', 'mi'):
    funcList.append(lambda m=m: callback(m))
newacct
fuente
Me
6
¡Buena solución! Aunque es complicado, creo que el significado original es más claro que con otras sintaxis.
Quantum7
3
No hay nada de hackeo o engañoso en esto; es exactamente la misma solución que sugieren las preguntas frecuentes oficiales de Python. Vea aquí .
abarnert
3
@abernert, "hackish and tricky" no es necesariamente incompatible con "ser la solución que sugiere la FAQ oficial de Python". Gracias por la referencia.
Don Hatch
1
reutilizar el mismo nombre de variable no es claro para alguien que no esté familiarizado con este concepto. La ilustración sería mejor si fuera lambda n = m. Sí, tendrías que cambiar tu parámetro de devolución de llamada, pero creo que el cuerpo del bucle for podría permanecer igual.
Nick
6

Python usa referencias, por supuesto, pero no importa en este contexto.

Cuando define una lambda (o una función, ya que este es exactamente el mismo comportamiento), no evalúa la expresión lambda antes del tiempo de ejecución:

# defining that function is perfectly fine
def broken():
    print undefined_var

broken() # but calling it will raise a NameError

Incluso más sorprendente que su ejemplo lambda:

i = 'bar'
def foo():
    print i

foo() # bar

i = 'banana'

foo() # you would expect 'bar' here? well it prints 'banana'

En resumen, piense en dinámica: nada se evalúa antes de la interpretación, por eso su código usa el último valor de m.

Cuando busca m en la ejecución lambda, m se toma del ámbito superior, lo que significa que, como otros señalaron; puede evitar ese problema agregando otro alcance:

def factory(x):
    return lambda: callback(x)

for m in ('do', 're', 'mi'):
    funcList.append(factory(m))

Aquí, cuando se llama a lambda, busca en el ámbito de definición de lambda una x. Esta x es una variable local definida en el cuerpo de la fábrica. Debido a esto, el valor utilizado en la ejecución de lambda será el valor que se pasó como parámetro durante la llamada a la fábrica. ¡Y doremi!

Como nota, podría haber definido fábrica como fábrica (m) [reemplazar x por m], el comportamiento es el mismo. Usé un nombre diferente para mayor claridad :)

Es posible que Andrej Bauer tenga problemas de lambda similares. Lo interesante de ese blog son los comentarios, donde aprenderá más sobre el cierre de Python :)

Nicolas Dumazet
fuente
1

No está directamente relacionado con el tema en cuestión, pero es una valiosa sabiduría: Python Objects de Fredrik Lundh.

tzot
fuente
1
No está directamente relacionado con su respuesta, sino una búsqueda de gatitos: google.com/search?q=kitten
Singletoned
@Singletoned: si el OP asimilara el artículo al que proporcioné un enlace, no harían la pregunta en primer lugar; por eso está indirectamente relacionado. Estoy seguro de que estará encantado de explicarme cómo los gatitos se relacionan indirectamente con mi respuesta (a través de un enfoque holístico, supongo;)
tzot
1

Sí, ese es un problema de alcance, se une a la m externa, ya sea que esté usando una función lambda o local. En su lugar, use un functor:

class Func1(object):
    def __init__(self, callback, message):
        self.callback = callback
        self.message = message
    def __call__(self):
        return self.callback(self.message)
funcList.append(Func1(callback, m))
Benoît
fuente
1

la solución a lambda es más lambda

In [0]: funcs = [(lambda j: (lambda: j))(i) for i in ('do', 're', 'mi')]

In [1]: funcs
Out[1]: 
[<function __main__.<lambda>>,
 <function __main__.<lambda>>,
 <function __main__.<lambda>>]

In [2]: [f() for f in funcs]
Out[2]: ['do', 're', 'mi']

el exterior lambdase utiliza para vincular el valor actual de ia j en el

cada vez que el exterior lambdase llama hace que una instancia de la interior lambdacon junido al valor actual de icomo ivalor 's

Aaron Goldman
fuente
0

Primero, lo que está viendo no es un problema y no está relacionado con la llamada por referencia o por valor.

La sintaxis lambda que definió no tiene parámetros y, como tal, el alcance que está viendo con el parámetro mes externo a la función lambda. Es por eso que está viendo estos resultados.

La sintaxis Lambda, en su ejemplo, no es necesaria, y preferiría usar una simple llamada a función:

for m in ('do', 're', 'mi'):
    callback(m)

Nuevamente, debe ser muy preciso acerca de qué parámetros lambda está utilizando y dónde exactamente comienza y termina su alcance.

Como nota al margen, con respecto al paso de parámetros. Los parámetros en Python siempre son referencias a objetos. Para citar a Alex Martelli:

El problema de la terminología puede deberse al hecho de que, en Python, el valor de un nombre es una referencia a un objeto. Entonces, siempre pasa el valor (sin copia implícita), y ese valor siempre es una referencia. [...] Ahora, si quieres acuñar un nombre para eso, como "por referencia de objeto", "por valor no copiado", o lo que sea, sé mi invitado. Intentar reutilizar la terminología que se aplica más generalmente a lenguajes donde "las variables son cajas" a un lenguaje donde "las variables son etiquetas post-it" es, en mi humilde opinión, más probable que confunda que ayude.

Yuval Adam
fuente
0

La variable mse está capturando, por lo que su expresión lambda siempre ve su valor "actual".

Si necesita capturar eficazmente el valor en un momento determinado, escriba una función que tome el valor que desee como parámetro y devuelva una expresión lambda. En ese punto, la lambda capturará el valor del parámetro , que no cambiará cuando llame a la función varias veces:

def callback(msg):
    print msg

def createCallback(msg):
    return lambda: callback(msg)

#creating a list of function handles with an iterator
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(createCallback(m))
for f in funcList:
    f()

Salida:

do
re
mi
Jon Skeet
fuente
0

en realidad, no hay variables en el sentido clásico en Python, solo nombres que han sido vinculados por referencias al objeto aplicable. Incluso las funciones son una especie de objeto en Python, y las lambdas no hacen una excepción a la regla :)

Tom
fuente
Cuando dice "en el sentido clásico", quiere decir "como C tiene". Muchos lenguajes, incluido Python, implementan variables de manera diferente a C.
Ned Batchelder
0

Como nota al margen map, aunque despreciado por alguna figura de Python bien conocida, fuerza una construcción que evita este escollo.

fs = map (lambda i: lambda: callback (i), ['do', 're', 'mi'])

NB: el primero lambda iactúa como la fábrica en otras respuestas.

YvesgereY
fuente