36
votes

I am trying to create buttons in tkinter within a for loop. And with each loop pass the i count value out as an argument in the command value. So when the function is called from the command value I can tell which button was pressed and act accordingly. The problem is, lets say len is 3, it will create 3 buttons with titles "Game 1" through "Game 3" but when any of the buttons are pressed the printed value is always 2, the last iteration. So it appears the buttons are being made as separate entities, but the i value in the command arguments seem to be all the same. Here is the code:

def createGameURLs(self):
    self.button = []
    for i in range(3):
        self.button.append(Button(self, text='Game '+str(i+1),command=lambda:self.open_this(i)))
        self.button[i].grid(column=4, row=i+1, sticky=W)
def open_this(self, myNum):
    print(myNum)

Is there a way to get the current i value, upon each iteration, to stick with that particular button?

3
A big thanks to both of you LukaD and BrenBarn, I have been fighting with that for several days now believe it or not. Both ways worked perfectly. I went with the i=i fix for now, but I'm definitely going to read up on the functools. I appreciate both answers. - Marcel
If BrenBarns solution works for you then you should mark it as your accepted answer. - lukad

3 Answers

97
votes

Change your lambda to lambda i=i: self.open_this(i).

This may look magical, but here's what's happening. When you use that lambda to define your function, the open_this call doesn't get the value of the variable i at the time you define the function. Instead, it makes a closure, which is sort of like a note to itself saying "I should look for what the value of the variable i is at the time that I am called". Of course, the function is called after the loop is over, so at that time i will always be equal to the last value from the loop.

Using the i=i trick causes your function to store the current value of i at the time your lambda is defined, instead of waiting to look up the value of i later.

7
votes

This is how closures work in python. I ran into this problem myself once. You could use functools.partial for this.

for i in range(3):
    self.button.append(Button(self, text='Game '+str(i+1), command=partial(self.open_this, i)))
1
votes

Simply attach your buttons scope within a lambda function like this:

btn["command"] = lambda btn=btn: click(btn) where click(btn) is the function that passes in the button itself. This will create a binding scope from the button to the function itself.

Features:

  • Customize gridsize
  • Responsive resizing
  • Toggle active state

#Python2
#from Tkinter import *
#import Tkinter as tkinter
#Python3
from tkinter import *
import tkinter

root = Tk()
frame=Frame(root)
Grid.rowconfigure(root, 0, weight=1)
Grid.columnconfigure(root, 0, weight=1)
frame.grid(row=0, column=0, sticky=N+S+E+W)
grid=Frame(frame)
grid.grid(sticky=N+S+E+W, column=0, row=7, columnspan=2)
Grid.rowconfigure(frame, 7, weight=1)
Grid.columnconfigure(frame, 0, weight=1)

active="red"
default_color="white"

def main(height=5,width=5):
  for x in range(width):
    for y in range(height):
      btn = tkinter.Button(frame, bg=default_color)
      btn.grid(column=x, row=y, sticky=N+S+E+W)
      btn["command"] = lambda btn=btn: click(btn)

  for x in range(width):
    Grid.columnconfigure(frame, x, weight=1)

  for y in range(height):
    Grid.rowconfigure(frame, y, weight=1)

  return frame

def click(button):
  if(button["bg"] == active):
    button["bg"] = default_color
  else:
    button["bg"] = active

w= main(10,10)
tkinter.mainloop()

enter image description here enter image description here

enter image description here