1

I'm trying to use Python objects as wrappers for Tk canvas items. For instance:

class PlayingCard:
    def __init__(self, item):
        self.item = item

aceOfSpades = PlayingCard(canvas.create_image((coordinates), image = PhotoImage(aceofspades.gif)))
print aceOfSpades.item
>>> <canvas item id>

This way, if I need to manipulate a canvas item, I can reference it by an object:

aceOfSpades.item.move(dx, dy)

So, the problem is that I have lots of objects (52, in fact), each with their own self.item which refers to a canvas image item, and I want to iterate over the objects and create event bindings for their canvas items if the object meets certain conditions. This is my solution (pseudo pycode):

def event_handler(card):
    card.attribute = updated_value

for card in [list of card objects]:
    if card.attribute == test_condition:
        canvas.tag_bind(
            card.item, #this is the item id stored in the PlayingCard.item variable
            <Event Sequence>,
            lambda x: event_handler(card)
            )

The problem is that, after the iteration is complete, all the event bindings pass the same argument to the event handler.

In other words, I wanted to pass the card object to the event handler as an argument, so that event handler would have access to the card object when an event occurs corresponding to the card's object.item canvas item. However, what this code does instead is pass the same argument (i.e. card object) to the event handler regardless of canvas item. In code, this means that if the event sequence is a click, then clicking on any card canvas item calls the function event_handler(<last card object in iteration>), whereas I want clicking on a canvas item to call event_handler(<corresponding card object>).

Am I making sense? I don't understand why this approach doesn't yield the results I want it to.

jayhendren
  • 4,286
  • 2
  • 35
  • 59

1 Answers1

2

Use an optional keyword argument with default value in your lambda function. The default value is bound to the lambda at the time the lambda is defined, so within the lambda, card is a local variable and each lambda will have a different default value for card.

Without the default value, when the lambda function is called, the value of card is looked up using the LEGB rule -- it is looked for in the Local, then Extended, then Global, then Builtin scopes. Since it is not defined in the local scope, it finds it in the extended scope (the scope containing for card in [list of cards]). There, card references the last card in list of cards. That is why all click events affect the same card.

for card in [list of card objects]:
    if card.attribute == test_condition:
        canvas.tag_bind(
            card.item, #this is the item id stored in the PlayingCard.item variable
            <Event Sequence>,
            lambda x, card = card: event_handler(card)
            )
Community
  • 1
  • 1
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Thank you very much! I had found a workaround, but using your solution will make my code much much easier to write and reduce the number of functions. – jayhendren Nov 28 '12 at 19:17