Generate Tkinter Buttons dynamically

I want to generate n amount of Tkinter Buttons which do different things. I have this code:

import Tkinter as tk

for i in range(boardWidth):
    newButton = tk.Button(root, text=str(i+1),
                    command=lambda: Board.playColumn(i+1, Board.getCurrentPlayer()))
    Board.boardButtons.append(newButton)

If boardWidth is 5, though I get buttons labelled 1 to 5, when clicked they all do Board.playColumn(5, Board.getCurrentPlayer()).

I need the first button to do Board.playColumn(1, Board.getCurrentPlayer()), the second to do Board.playColumn(2, Board.getCurrentPlayer()) and so on.


Solution 1:

I think the problem is that the lambda is picking up the final value of i after the for loop ends. This should fix that (untested):

import Tkinter as tk

for i in range(boardWidth):
    newButton = tk.Button(root, text=str(i+1),
                    command=lambda j=i+1: Board.playColumn(j, Board.getCurrentPlayer()))
    Board.boardButtons.append(newButton)

Update

BTW, this worked by adding an argument to the lambda function with a default value calculated from the value of i at the time each one is created in the loop rather than referring back to the final value of i through a closure when the expression within it executes later.

Solution 2:

Your problem is that you create lots of lambda objects in the same namespace, and those lambdas make reference to names in the outer scope. That means they don't become closures and they don't store references to the objects until later... When it happens, all lambdas will refer to the last value of i.

Try using a callback factory to fix that:

import Tkinter as tk

def callbackFactory(b, n):
    def _callback():
        return b.playColumn(n, b.getCurrentPlayer())
    return _callback

for i in range(boardWidth):
    newButton = tk.Button(root, text=str(i+1), 
        command=callbackFactory(Board, i+1))
    Board.boardButtons.append(newButton)

Another idea is to store the current value of i as a default argument value in the lambda object, instead of relying on closure behavior to store the reference:

for i in range(boardWidth):
    newButton = tk.Button(root, text=str(i+1), 
        command=lambda x=i: Board.playColumn(x+1, Board.getCurrentPlayer()))
    Board.boardButtons.append(newButton)