python - sintaxis - Alcance de las funciones lambda y sus parámetros?
lambda python 3 español (10)
Necesito una función de devolución de llamada que sea casi exactamente la misma para una serie de eventos de GUI. La función se comportará de forma ligeramente diferente según el evento que la haya llamado. Parece un caso simple para mí, pero no puedo entender este extraño comportamiento de las funciones lambda.
Así que 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 ha estropeado 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.
Como nota al margen, el map
, aunque despreciado por alguna figura bien conocida de Python, obliga a una construcción que evita esta trampa.
fs = map (lambda i: lambda: callback (i), [''do'', ''re'', ''mi''])
NB: la primera lambda i
actúo como la fábrica en otras respuestas.
Cuando se crea un lambda, no hace una copia de las variables en el alcance adjunto que utiliza. Mantiene una referencia al entorno para que pueda buscar el valor de la variable más adelante. Solo hay un m
. Se asigna a cada vez a través del ciclo. Después del ciclo, 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 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. Usualmente usas un parámetro del mismo nombre para que no tengas que cambiar el cuerpo del código:
for m in (''do'', ''re'', ''mi''):
funcList.append(lambda m=m: callback(m))
El problema aquí es la variable m
(una referencia) tomada del alcance circundante. Solo los parámetros se mantienen en el alcance de lambda.
Para resolver esto, debes 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 usa el alcance de búsqueda para encontrar m
, pero esta vez es el alcance 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()
La variable m
se está capturando, por lo que su expresión lambda siempre ve su valor "actual".
Si necesita capturar efectivamente el valor en un momento determinado, escriba una función que tome el valor que desea 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
No relacionado directamente con el tema en cuestión, pero una pieza invaluable de sabiduría: objetos de Python por Fredrik Lundh.
Primero, lo que está viendo no es un problema, y no está relacionado con la llamada por referencia o el valor por defecto.
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ás viendo estos resultados.
La sintaxis de Lambda, en su ejemplo no es necesaria, y preferiría utilizar una llamada a función simple:
for m in (''do'', ''re'', ''mi''):
callback(m)
Nuevamente, debe ser muy preciso sobre 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 son siempre referencias a objetos. Para citar a Alex Martelli:
El problema terminológico 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 siempre es 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. Tratar de reutilizar la terminología que generalmente se aplica a los idiomas donde "las variables son recuadros" a un idioma donde "las variables son etiquetas post-it" es, en mi humilde opinión, más susceptible de confundir que de ayudar.
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
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 dinámica: nada se evalúa antes de la interpretación, es por eso que su código usa el último valor de m.
Cuando busca m en la ejecución de lambda, m se toma del ámbito superior, lo que significa que, como otros señalaron; puede eludir 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 la lambda, se ve en el alcance de definición de lambda para 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 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. Utilicé un nombre diferente para mayor claridad :)
Es posible que Andrej Bauer tenga problemas lambda similares. Lo que es interesante en ese blog son los comentarios, donde aprenderás más sobre el cierre de Python :)
Sí, ese es un problema de alcance, se une a la m externa, ya sea que esté utilizando una función lambda o local. En cambio, usa 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))
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 :)
la solución para 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 lambda
externo se usa para unir el valor actual de i
a j
en el
cada vez que se llama lambda
externa, lambda
una instancia de la lambda
interna con j
ligado al valor actual de i
como valor de i