¿Alcance de las funciones lambda y sus parámetros?

Necesito una función de callback que sea casi exactamente la misma para una serie de eventos gui. 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 extraño comportamiento 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() 

La salida de este código es:

 mi mi mi do re mi 

Esperaba:

 do re mi do re mi 

¿Por qué usar un iterador ha estropeado las cosas?

He intentado 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.

El problema aquí es la variable m (una referencia) que se toma del ámbito circundante. Sólo los parámetros se mantienen en el ámbito lambda.

Para resolver esto tienes que crear otro ámbito 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 utiliza el scope que rodea para encontrar m , pero esta vez es el scope de callback_factory que se crea una vez por cada callback_factory .

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() 

Cuando se crea un lambda, no hace una copia de las variables en el ámbito de cierre que utiliza. Mantiene una referencia al entorno para que pueda consultar el valor de la variable más adelante. Sólo hay una m . Se asigna a cada momento a través del bucle. Después del bucle, la variable m tiene el valor 'mi' . Entonces, cuando realmente ejecute la función que creó más tarde, buscará el valor de m en el entorno que lo creó, que para entonces tendrá el valor 'mi' .

Una solución común e idiomática para 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 del 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)) 

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

Cuando define un 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 

Aún más sorprendente que tu 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 dynamic: nada se evalúa antes de la interpretación, por eso su código utiliza el último valor de m.

Cuando se busca m en la ejecución lambda, m se toma del scope más alto, lo que significa que, como otros lo señalaron; puedes sortear ese problema agregando otro scope:

 def factory(x): return lambda: callback(x) for m in ('do', 're', 'mi'): funcList.append(factory(m)) 

Aquí, cuando se llama a la lambda, busca en la definición de lambda para un 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 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 la fábrica como fábrica (m) [reemplazar x por m], el comportamiento es el mismo. Utilicé un nombre diferente para mayor claridad 🙂

Es posible que descubra que Andrej Bauer tuvo problemas lambda similares. Lo que es interesante en ese blog son los comentarios, donde aprenderá más sobre el cierre de Python 🙂

No relacionado directamente con el tema en cuestión, pero sin embargo, una valiosa pieza de sabiduría: Python Objects by Fredrik Lundh.

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

La syntax lambda que definió no tiene parámetros, y como tal, el scope que está viendo con el parámetro m es externo a la función lambda. Por eso estás viendo estos resultados.

La syntax de Lambda, en su ejemplo, no es necesaria, y prefiere utilizar una llamada a función simple:

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

Una vez más, debe ser muy preciso acerca de los parámetros lambda que está utilizando y dónde comienza y termina exactamente su scope.

Como nota al margen, sobre el 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. Por lo tanto, siempre pasa el valor (sin copia implícita), y ese valor es siempre una referencia. […] Ahora, si desea acuñar un nombre para eso, como “por referencia de objeto”, “por valor no copiado”, o lo que sea, sea mi invitado. Intentar reutilizar la terminología que generalmente se aplica a idiomas donde “las variables son recuadros” a un idioma donde “las variables son tags post-it” es, en mi humilde opinión, más probabilidades de confundir que de ayudar.

La variable m está siendo capturada, por lo que su expresión lambda siempre ve su valor “actual”.

Si necesita capturar efectivamente el valor en un momento dado, escriba una función toma el valor que desea como parámetro y devuelve una expresión lambda. En ese momento, 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 

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 algún tipo de objeto en Python, y las lambdas no hacen una excepción a la regla 🙂

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

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

NB: la primera lambda i actúa como la fábrica en otras respuestas.

Sí, ese es un problema de scope, se enlaza con la m externa, ya sea que esté utilizando un lambda o una función local. En su lugar, utilice 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)) 

la soluitó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]: [>, >, >] In [2]: [f() for f in funcs] Out[2]: ['do', 're', 'mi'] 

la lambda exterior se utiliza para enlazar el valor actual de i to j en el

cada vez que se llama a la lambda externa, se lambda una instancia de la lambda interna con j vinculada al valor actual de i como el valor de i