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