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.
python
lexical-closures
Agartland
fuente
fuente
Respuestas:
El problema aquí es que la
m
variable (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 sucallback_factory
alcance se crea una vez por cadacallback_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()
fuente
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 variablem
tiene valor'mi'
. Entonces, cuando ejecute la función que creó más tarde, buscará el valor dem
en 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
m
en 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))
fuente
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 :)
fuente
No está directamente relacionado con el tema en cuestión, pero es una valiosa sabiduría: Python Objects de Fredrik Lundh.
fuente
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))
fuente
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
lambda
se utiliza para vincular el valor actual dei
aj
en elcada vez que el exterior
lambda
se llama hace que una instancia de la interiorlambda
conj
unido al valor actual dei
comoi
valor 'sfuente
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
m
es 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:
fuente
La variable
m
se 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:
fuente
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 :)
fuente
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 i
actúa como la fábrica en otras respuestas.fuente