Weird lambda behavior

Dave Angel davea at ieee.org
Wed Apr 22 10:45:45 EDT 2009


Rüdiger Ranft wrote:
> Hi all,
>
> I want to generate some methods in a class using setattr and lambda.
> Within each generated function a name parameter to the function is
> replaced by a string constant, to keep trail which function was called.
> The problem I have is, that the substituted name parameter is not
> replaced by the correct name of the function, but by the last name the
> for loop has seen.
>
> import unittest
>
> class WidgetDummy:
>     '''This class records all calls to methods to an outer list'''
>
>     def __init__( self, name, calls ):
>         '''name is the name of the object, which gets included into a
>         call record. calls is the list where the calls are appended.'''
>         self.name = name
>         self.calls = calls
>         for fn in ( 'Clear', 'Append', 'foobar' ):
>             func = lambda *y,**z: self.__callFn__( fn, y, z )
>             setattr( self, fn, func )
>
>     def __callFn__( self, fnName, *args ):
>         '''Add the function call to the call list'''
>         self.calls.append( ( self.name, fnName, args ) )
>
> class Testcase( unittest.TestCase ):
>     def testFoo( self ):
>         calls = []
>         wd = WidgetDummy( 'foo', calls )
>         wd.Append( 23 )
>         self.assertEqual( [ ( 'foo', 'Append', ( 23, {} ) ) ], calls )
>
> unittest.main()
>
>   
This is a common problem on this list.  I was possibly the last one to 
ask what was essentially the same question, though it was on the 
wxPython list, 3/15/09 - "Using lambda function within event binding"

Anyway, the answer is that when (free) variables are used inside any 
nested function (a def inside a def, or a lambda), these variables are 
not copied, they are scoped from the nesting environment.  So even 
though the __init__ may have ended, all these lambda functions still 
retain a reference to the same 'fn' variable, which is kept alive till 
all the lambda functions are gone.  Lookup closures if you want to 
understand more, or use dis() on the function.

The simplest solution is a hack, but a common one.  Use default 
variables, which have a lifetime that matches the lambda, but are 
initialized when the lambda is being created.  To do this, you'd add an 
extra optional argument to the lambda, and use that as the first 
argument to the call.   Unfortunately, although this is what I used, I 
don't know if you can do it when you're already using the *y syntax.  In 
the line below, I assumed you could change to exactly one non-keyword 
argument.

     func = lambda y, unlikelydummyname=fn, **z: self.__callFn__( fn, y, **z )


Incidentally, in your example, I believe you needed the *y and **z in 
the actual parameters to __callFn__().   You had omitted the asterisks.  
Further, you had no keyword arguments in the formal parameter list.  So 
I'm not sure where you were really headed.  But perhaps the above will 
get you started.





More information about the Python-list mailing list