0

I've provided the minimum buggy code I could reproduce.

I'm using python 3.

I have a function printValue that outputs the parameter it receives. I also have a list of values.

Using tkinter, I created a frame and am generating a table within the frame based on values. The trouble arises when I have to add a button that outputs the row number. Each button is outputting the value 5 when they should output the corresponding row number.

import tkinter as tk

def printValue(param):
    print(param)

Values=[[1],[2],[3],[4],[5]]
mainGuiWindow = tk.Tk()

frame = tk.Frame(mainGuiWindow)
frame.pack()

def refreshFrame():
    global frame
    global Values
    frame.destroy()
    frame = tk.Frame(mainGuiWindow)
    frame.pack()
    tk.Label(frame,text="Sl. No").grid(row=0,column=0)
    tk.Label(frame,text="Value").grid(row=0,column=1)
    tk.Label(frame,text="Print Entry").grid(row=0,column=2)

    numberOfEntries = 0
    for entry in Values:
        numberOfEntries += 1
        tk.Label(frame,text=numberOfEntries).grid(row=numberOfEntries,column=0)
        tk.Label(frame,text=entry[0]).grid(row=numberOfEntries,column=1)
        tk.Button(frame, text="Print",command=lambda: printValue(numberOfEntries)).grid(row=numberOfEntries,column=2)

refreshFrame()

mainGuiWindow.mainloop() 

Problem seems to be that the function parameter in button is binded to the variable 'numberOfEntries'

How do I pass the parameter in the button such that the function prints the correct value? I don't want to modify the function definition. I need to change the way it is called by the button.

Abhay Aravinda
  • 878
  • 6
  • 17
  • @AbhayAravinda Read [Python and Tkinter lambda function](https://stackoverflow.com/a/11005426/7414759) – stovfl Mar 16 '20 at 18:08

1 Answers1

1

This is because of how your frame is "refreshed". You are defining the page here, and when you set up all of the buttons and everything, you go through the entire for loop before everything gets rendered. This will stick the command to be the last value in your loop, 5. Effectively, it will do this:

def f(n):
    return n

def list_of_functions():
    x = []
    for i in range(5):
        x.append(lambda: f(i))
    return x

for func in list_of_functions():
    func()

4
4
4
4
4

This is a behavior of functions in general, because it's taking the value of numberOfEntries that you last defined, not the one that you think it was defined with. To show this with the equivalent "normal" function:

# Note that there's no argument here, because
# you haven't given your lambda an argument
def f():
    # does a global lookup for value g
    return g

def create_funcs():
    funcs = []
    for g in range(5):
        funcs.append(f)
    return funcs

for func in create_funcs():
    func()

4
4
4
4
4

The workaround here is to physically bind that value to the function at definition time. This can be accomplished with functools.partial. To show the simple example first:

from functools import partial 

def f(n):
    return n

def list_of_functions():
    x = []
    for i in range(5):
        x.append(partial(f, i))
    return x

for func in list_of_functions():
    func()

0
1
2
3
4

Or, you can add an input argument to your lambda and bind a default to it:


def f(n):
    return n

def list_of_functions():
    x = []
    for i in range(5):
        # note that now we have bound an input argument to our lambda
        x.append(lambda i=i: f(i))
    return x

for func in list_of_functions():
    func()
0
1
2
3
4

Now, each call changes appropriately. To show how to implement this in your code:

from functools import partial


def refreshFrame():
    global frame
    global Values
    frame.destroy()
    frame = tk.Frame(mainGuiWindow)
    frame.pack()
    tk.Label(frame,text="Sl. No").grid(row=0,column=0)
    tk.Label(frame,text="Value").grid(row=0,column=1)
    tk.Label(frame,text="Print Entry").grid(row=0,column=2)

    # I've changed this to be a bit more idiomatic
    for n, (entry, *_) in enumerate(Values, start=1):
        tk.Label(frame,text=n).grid(row=n, column=0)
        tk.Label(frame,text=entry).grid(row=n, column=1)

        # this *looks* like a lambda, and still returns a function
        # but binds the value of `n` to the function, rather than relying
        # on references to a variable that you've changed
        tk.Button(frame, text="Print",command=partial(printValue, n)).grid(row=n, column=2)
C.Nivs
  • 12,353
  • 2
  • 19
  • 44