2009-06-02 30 views
62

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.Alcance de las funciones lambda y sus parámetros?

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

La salida de este código es:

mi 
mi 
mi 
do 
re 
mi 

que esperaba:

do 
re 
mi 
do 
re 
mi 

¿Por qué se utiliza un repetidor en mal estado ¿cosas claras?

He intentado usar un deepcopy:

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.

+2

El título de su pregunta es algo engañoso. – lispmachine

+1

¿Por qué usar lambdas si los encuentras confusos? ¿Por qué no usar def para definir funciones? ¿De qué se trata tu problema que hace que lambdas sea tan importante? –

+0

@ S.Lott La función anidada dará como resultado el mismo problema (tal vez más claramente visible) – lispmachine

Respuesta

56

El problema aquí es la variable m (una referencia) tomada del ámbito circundante. Solo los parámetros se mantienen en el alcance de lambda.

Para solucionar esto usted tiene que crear otra posibilidad de 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 alcance aledañas para encontrar m, pero esta vez es callback_factory alcance que 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() 
+1

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

+0

+1 para functools – Dacav

0

En primer lugar, lo que está viendo no es un problema, y ​​no relacionada con 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ás viendo estos resultados.

sintaxis lambda, en su ejemplo no es necesario, y que más bien estaría utilizando una simple llamada de función:

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

Una vez más, usted debe ser muy preciso sobre qué parámetros lambda que está utilizando y el lugar exacto de su el alcance comienza y termina

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

El problema terminología puede ser debido a el hecho de que, en Python, el valor de un nombre es una referencia a un objeto. Por lo tanto, siempre pasa el valor (no copia implícita), y ese valor es siempre es una referencia. [...] Ahora si usted desea acuñar un nombre para eso, como "por referencia de objeto", "por cero valor copiado", o lo que sea, sea mi invitado. Tratando de volver a utilizar terminología que se aplica más generalmente a las lenguas donde "las variables son cajas" a un lenguaje donde "variables son post-it etiquetas" es, en mi humilde opinión, es más probable confundir de ayudar.

0

Se está capturando la variable m, por lo que su expresión lambda siempre ve su valor "actual".

Si necesita capturar efectivamente el valor en un momento determinado, escribir una función toma el valor que desea como parámetro y devuelve una expresión lambda. En ese momento, el lambda capturará el valor del parámetro, que no va a cambiar cuando se llama 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 
0

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

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

Cuando se crea un lambda, que no tiene una copia de las variables en el ámbito de inclusión 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 utilizándolo como el argumento predeterminado de un parámetro opcional. Por lo general, utilizar un parámetro del mismo nombre por lo que no tiene que cambiar el cuerpo del código:

for m in ('do', 're', 'mi'): 
    funcList.append(lambda m=m: callback(m)) 
+0

+1 uso excesivo interesante de los parámetros por defecto – lispmachine

+0

quiero decir * parámetros opcionales * con valores por defecto – lispmachine

+6

¡Buena solución! Aunque es complicado, creo que el significado original es más claro que con otras sintaxis. – Quantum7

4

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

Cuando se define una lambda (o una función, ya que este es el mismo comportamiento exacto), no evalúa la expresión lambda antes de 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, pensar dinámico: nada se evalúa antes de la interpretación, es por eso que su código utiliza el último valor de m.

Cuando se busca m en la ejecución lambda, m se toma del ámbito superior, lo que significa que, como señalaron otros; se puede eludir este problema mediante la adición de otro ámbito:

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

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

Aquí, cuando el lambda se llama, se ve en el alcance lambda' definición 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 claridad :)

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

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

+0

Cuando dice "en el sentido clásico", quiere decir, "como C tiene". Muchos idiomas, incluido Python, implementan variables de manera diferente a C. –

1

No relacionado directamente con el problema en cuestión, pero una valiosa sabiduría: Python Objects por Fredrik Lundh.

+0

No relacionado directamente con su respuesta, pero una búsqueda de gatitos: http://www.google.com/search?q=kitten – Singletoned

+0

@Singletoned: si el OP se cruzó el artículo al cual les proporcioné un enlace, en primer lugar no harían la pregunta; por eso está indirectamente relacionado. Estoy seguro de que estarás encantado de explicarme cómo los gatitos están indirectamente relacionados con mi respuesta (a través de un enfoque holístico, supongo;) – tzot

0

Como nota al pie, map, aunque despreciado por alguna conocida figura de Python, obliga a una construcción que evita esta trampa.

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

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

0

la soluiton 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 enlazar el valor actual de i-j en el

cada vez que el exterior lambda se llama hace que una instancia del interior lambda con j vinculado al valor actual de i como i valor de

Cuestiones relacionadas