Python and Tkinter lambda function

2020-01-24 02:54发布

问题:

(As the 'homework' tag indicates, this is part of a big project in Computer Science.)

I am writing a Jeopardy! simulation in Python with tkinter, and I'm having a big problem regarding the use of the lambda function in buttons. Assume root = Tk() and categories is a list.

# Variable to keep the buttons
root._buttons = {}

# Display headers on top of page
for i in range(5):
    # Get category name for display in main window
    name = categories[i]
    b = Label(root, text=fill(name.upper(), 10), width=18, height=3,\
        bg="darkblue", fg="white", font=("Helvetica bold", "", 11))
    b.grid(row=0, column=i)

    # Create list of buttons in that variable (root._buttons)
    btnlist = [None]*5

    # Display individual questions
    for j in range(5):

        # Make a button for the question
        b = Button(root, text="$" + str(200 * (j+1)), width=8, height=1,
            bg="darkblue", fg="orange", font=("Impact", "", 30))
        b.cat = name
        b.value = 200 * (j + 1)
        b.sel = lambda: select(b.cat, b.value)

        # Add callback event to button
        print(b.cat, b.value, b.sel)
        b.config(command=b.sel)

        # Add button to window
        b.grid(row=j+1, column=i)

        # Append to list
        btnlist[j] = b

    root._buttons[categories[i]] = btnlist

For all of the code, see my little Code Viewer (under construction!)

It's at lambda: select(b.cat, b.value) where the problem seems to occur, because when I click any button on the board, it always goes to the one last button on the board. I've tried other approaches, unfortunately all using lambda, and I have not seen any approach that does not involve lambda.

回答1:

Change

lambda: select(b.cat, b.value)

to

lambda b = b: select(b.cat, b.value)

In your original code, b is not a local variable of the lambda; it is found in enclosing scope. Once the for-loop is completed, b retains it last value. That is why the lambda functions all use the last button.

If you define the lambda to take one argument with a default value, the default value is determined (and fixed) at the time the lambda is defined. Now b is a local variable of the lambda, and when the lambda is called with no arguments, Python sets b to the default value which happily is set to various different buttons as desired.



回答2:

It would let you be more expressive if you replaced the lambda expression with a function factory. (presuming that you're going to call this multiple times). That way you can do assignments, add more complicated logic, etc later on without having to deal with the limitations of lambda.

For example:

def button_factory(b):
    def bsel():
        """ button associated with question"""
        return select(b.cat, b.value)
    return bsel

Given an input b, button_factory returns a function callable with () that returns exactly what you want. The only difference is that you can do assignments, etc.

Even though it may take up more lines of code initially, it gives you greater flexibility later on. (for example, you could attach a counter to bsel and be able to count how many times a particular question was selected, etc).

It also aids introspection, as you could make each docstring clearly identify which question it is associated with, etc.