1

Code:

def createLetters(frame, startX, startY, width, height, spacing):

    alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", 
                "J", "K", "L", "M", "N", "O", "P", "Q", "R", 
                "S", "T", "U", "V", "W", "X", "Y", "Z"]

    def letterAction(letter):
        letter.destroy()

    for i in range(0, 26):

        if (i >= 9 and i <= 17):
            y = startY +  height + 2 * spacing
            x = startX + ((width + spacing) * (i - 9))

        elif (i >= 17):
            y = startY + 2 * height + 3 * spacing
            x = (width + spacing) / 2 + startX + ((width + spacing) * (i - 18))

        elif (i <= 8):
            y = startY + spacing
            x = startX + ((width + spacing) * i)

        exec(alphabet[i] + " = Button(" + frame + ", text = '" + alphabet[i] + "', command = letterAction(" + alphabet[i] + "))")
        exec(alphabet[i] + ".place(x = " + str(x) + ", y = " + str(y) + ", width = " + str(width) + ", height = " + str(height) + ")")

Error:

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Python33\lib\tkinter\__init__.py", line 1442, in __call__
    return self.func(*args)
  File "E:\Hangman\hangmanTk.py", line 106, in playScreen
    createLetters("playFrame", 175, 250, 50, 50, 0)
  File "E:\Hangman\hangmanTk.py", line 95, in createLetters
    exec(alphabet[i] + " = Button(" + frame + ", text = '" + alphabet[i] + "', command = letterAction(" + alphabet[i] + "))")
  File "<string>", line 1, in <module>
NameError: name 'A' is not defined

I'm attempting to create multiple tkinter buttons with a loop. I can create the buttons fine but i can't seem to create callbacks for them. When ever i try, it tells me that the variables that i use for the buttons are not defined. I tried adding "exec("global " + alphabet[i])" above where i defined the buttons but that didn't change anything.

6
  • import string; alphabet = list(string.ascii_uppercase) would be a lot less painful Commented Jul 10, 2013 at 20:36
  • For that matter, there's no reason to have a list instead of a string in the first place, since all he needs to do is index it. (And looping over enumerate would make it even simpler.) Commented Jul 10, 2013 at 20:39
  • What does the enumerate function do with strings? I can't find any examples of it being used with a string. Commented Jul 10, 2013 at 21:14
  • @guney98: The same thing it does with any other iterable. It's easier to show than to explain, so just fire up your interpreter and type this: print(list(enumerate('abc'))) Commented Jul 10, 2013 at 21:32
  • place is a very odd choice. Is there a specific reason you're using it instead of grid? Commented Jul 10, 2013 at 23:15

2 Answers 2

2

Using exec is almost always the wrong way to do it, no matter what "it" is.

And creating variables dynamically is almost always the wrong thing to do.

And your problems getting this to work are a perfect illustration of why.


Just create a dict mapping names to buttons:

buttons = {}

# ...

letter = alphabet[i]
buttons[letter] = Button(frame, text = letter, command = letterAction(letter))
buttons[letter].place(x = x, y = y, width = width, height = height)

If you really want to dump the dict into locals() (or, similarly, self.__dict__ or globals() or …), that's trivial. But you don't. The only place you need to use the variable is in your letterAction function. So:

def createLetters(frame, startX, startY, width, height, spacing):

    alphabet = string.ascii_uppercase
    buttons = {}

    def letterAction(letter):
        buttons[letter].destroy()

    for i, letter in enumerate(alphabet):

        if (i >= 9 and i <= 17):
            y = startY +  height + 2 * spacing
            x = startX + ((width + spacing) * (i - 9))

        elif (i >= 17):
            y = startY + 2 * height + 3 * spacing
            x = (width + spacing) / 2 + startX + ((width + spacing) * (i - 18))

        elif (i <= 8):
            y = startY + spacing
            x = startX + ((width + spacing) * i)

        buttons[letter] = Button(frame, text = letter, command = letterAction(letter))
        buttons[letter].place(x = x, y = y, width = width, height = height)

But notice that this does the wrong thing. command = letterAction(letter)—whether you run it directly, or via exec—is going to call letterAction(letter) now, destroying the button before you even create it, and returning None, which you'll then set as the command.

You need lambda: letterAction(letter) or partial(letterAction, letter) to fix this.

Plus, you can't write code to pass the button variable itself to letter, either now or later, because the variable doesn't exist yet. You have to pass the letter, as a string, as I did above.


But really, if you think about it, you don't need these button variables at all—whether in a dict or otherwise. You just need a way to bind each button as its own callback's target, right? There are a number of ways to do this, but the obvious one is a class, either inheriting or delegating to Button (or, in this case, neither, since you don't need to use it as a button, or even remember it, after creation).

While we're at it, let's remove some extraneous parens and such that just make things harder to read and fix the problem that 17 seems to belong in two different groups…

class SelfDestructiveButton(object):
    def __init__(self, frame, letter, x, y, width, height):
        self.button = Button(frame, text=letter, command=self.command)
        self.button.place(x=x, y=y, width=width, height=height)
    def command(self):
        self.button.destroy()

def createLetters(frame, startX, startY, width, height, spacing):
    for i, letter in enumerate(string.ascii_uppercase):
        if 9 <= i <= 17:
            y = startY +  height + 2 * spacing
            x = startX + ((width + spacing) * (i - 9))
        elif i > 17:
            y = startY + 2 * height + 3 * spacing
            x = (width + spacing) / 2 + startX + ((width + spacing) * (i - 18))
        else:
            y = startY + spacing
            x = startX + ((width + spacing) * i)
        SelfDestructiveButton(frame, letter, x, y, width, height)

It might be even clearer with if 'J' <= letter <= 'R', because it's the letters rather than the numbers that you're going to see while debugging it.

Sign up to request clarification or add additional context in comments.

Comments

0

The string in your first call to exec evaluates to:

"A = Button(<frame>, text = 'A', command = letterAction(A))"

You are therefore referencing A (the name) before having defined it. I'm guessing you forgot the single quotes around the second alphabet[i]:

exec(alphabet[i] + " = Button(" + frame + ", text = '" + alphabet[i] + "', command = letterAction('" + alphabet[i] + "'))")

Note this would call letterAction('A'), i.e. 'A'.destroy(), which will throw an AttributeError since strings have no destroy() method. What is letterAction supposed to achieve?

3 Comments

Maybe the buttons are intended to destroy themselves? In this case, A (the button itself) is no yet created, and letterAction should return the destroy method, not invoke it.
@tobias_k Or perhaps use functools.partial() on the letterAction function. Either way A needs to be defined first though.
There's no way to make this work without using the button name _as a string_—either lambda: letterAction('A') or partial(letterAction, 'A'). Which means that letterAction has to look it up in locals(). (Of course it's really not a local, but a closure variable… but locals()[letter] will work.)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.