DirectButtons extraArgs relies on a __name__ argument

Bug #800861 reported by Jean-André Santoni on 2011-06-22
6
This bug affects 1 person
Affects Status Importance Assigned to Milestone
Panda3D
Undecided
David Rose

Bug Description

I was first trying to use this code:

            bubu[key] = DirectButton(
                text = (key, "Join", "Join", "Full"),
                command = lambda: self.commandAndDestroy( lambda: self.command(key) ),
                #extraArgs = [ functools.partial(self.command, key) ],
                #command = self.commandAndDestroy,
                #extraArgs = [ funcs[key] ],
                scale = scale,
                text_font = font,
                text_fg = (.1875,.15625,.125,1),
                text_shadow = (.5,.46484375,.40625,1),
                text_align = TextNode.ALeft,
                rolloverSound = hover_snd,
                clickSound = clicked_snd,
                pressEffect = 0,
                parent = self.frame
            )

But for an unknown reason, using lambda was the source of a very strange behaviour: whatver button I was clicking, the command was invoked with the last 'key' argument. Like if I had clicked on the last button!

sjoerd_ on IRC told me to use functools.partial instead of lambda. Wich caused this exception:

Traceback (most recent call last):
  File "main.py", line 1038, in <module>
    run()
  File "/usr/share/panda3d/direct/showbase/ShowBase.py", line 2531, in run
    self.taskMgr.run()
  File "/usr/share/panda3d/direct/task/Task.py", line 496, in run
    self.step()
  File "/usr/share/panda3d/direct/task/Task.py", line 454, in step
    self.mgr.poll()
  File "/usr/share/panda3d/direct/showbase/EventManager.py", line 61, in eventLoopTask
    self.doEvents()
  File "/usr/share/panda3d/direct/showbase/EventManager.py", line 55, in doEvents
    processFunc(self.eventQueue.dequeueEvent())
  File "/usr/share/panda3d/direct/showbase/EventManager.py", line 122, in processEvent
    messenger.send(eventName, paramList)
  File "/usr/share/panda3d/direct/showbase/Messenger.py", line 325, in send
    self.__dispatch(acceptorDict, event, sentArgs, foundWatch)
  File "/usr/share/panda3d/direct/showbase/Messenger.py", line 410, in __dispatch
    method (*(extraArgs + sentArgs))
  File "/usr/share/panda3d/direct/gui/DirectButton.py", line 103, in commandFunc
    apply(self['command'], self['extraArgs'])
  File "/home/kivutar/code/tethical/client/GUI.py", line 201, in commandAndDestroy
    Func(command),
  File "/usr/share/panda3d/direct/interval/FunctionInterval.py", line 308, in __init__
    FunctionInterval.__init__(self, function, **kw)
  File "/usr/share/panda3d/direct/interval/FunctionInterval.py", line 62, in __init__
    name = self.makeUniqueName(function)
  File "/usr/share/panda3d/direct/interval/FunctionInterval.py", line 80, in makeUniqueName
    name = 'Func-%s-%d' % (func.__name__, FunctionInterval.functionIntervalNum)
AttributeError: 'functools.partial' object has no attribute '__name__'

So I added a __name__ argument to my functools.partial:

            funcs[key] = functools.partial(self.command, key)
            funcs[key].__name__ = str(key)
            bubu[key] = DirectButton(
                text = (key, "Join", "Join", "Full"),
                #command = lambda: self.commandAndDestroy( lambda: self.command(key) ),
                #extraArgs = [ functools.partial(self.command, key) ],
                command = self.commandAndDestroy,
                extraArgs = [ funcs[key] ],
                scale = scale,
                text_font = font,
                text_fg = (.1875,.15625,.125,1),
                text_shadow = (.5,.46484375,.40625,1),
                text_align = TextNode.ALeft,
                rolloverSound = hover_snd,
                clickSound = clicked_snd,
                pressEffect = 0,
                parent = self.frame
            )

And it fixed my strange bug.
The ideal would be to not rely on this __name__ argument to fix functools compatibility, and find why lambda caused this strange bug.

David Rose (droklaunchpad) wrote :

Unlike some other languages, Python's lambda syntax doesn't perform a full closure on its parameters automatically. This means that when you use a syntax like "lambda: func(key)", the variable reference "key" does not retain the value of the variable at the time you bound it, but rather looks up the variable "key" from the *calling* namespace. This is almost never what you intended. To force Python to bind in a variable reference, you pass it in as a default parameter, like this: "lambda key = key: func(key)". This is a bit of a hairy kludge, but it's the Python way.

You can also use use Panda's Functor class to do this for you. "from direct.showbase.PythonUtil import Functor", then pass "Functor(func, key)", instead of a lambda object, to the command parameter.

I've never seen functools.partial before, but it apparently is meant to do the same thing as our Functor class. Unfortunately, it doesn't define a __name__ member for some reason (unlike almost every other kind of Python object), so it causes a crash in the FunctionInterval code which assumes the function object you pass does have a __name__ member. I've just committed a workaround to FunctionInterval to allow it to work without this.

Changed in panda3d:
status: New → Fix Committed
assignee: nobody → David Rose (droklaunchpad)
rdb (rdb) on 2012-02-13
Changed in panda3d:
status: Fix Committed → Fix Released
To post a comment you must log in.
This report contains Public information  Edit
Everyone can see this information.

Other bug subscribers