lambda in for loop only takes last value

醉酒当歌 提交于 2019-11-26 01:54:47

问题


Problemset:

Context Menu should show filter variables dynamically and execute a function with parameters defined inside the callback. Generic descriptions show properly, but function call is always executed with last set option.

What I have tried:

#!/usr/bin/env python

import Tkinter as tk
import ttk
from TkTreectrl import MultiListbox

class SomeClass(ttk.Frame):
    def __init__(self, *args, **kwargs):
        ttk.Frame.__init__(self, *args, **kwargs)
        self.pack(expand=True, fill=tk.BOTH)

        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)

        self.View=MultiListbox(self)

        __columns=(\"Date\",\"Time\",\"Type\",\"File\",\"Line\",\"-\",\"Function\",\"Message\")
        self.View.configure(columns=__columns, expandcolumns=(0,0,0,0,0,0,0,1))

        self.View.bind(\"\", self.cell_context)
        self.View.grid(row=0, column=0, sticky=tk.NW+tk.SE)

        self.__recordset          = []
        self.__recordset_filtered = False

        #Some dummy values
        self.__recordset.append([\"Date\", \"Time\", \"INFO\", \"File\", \"12\", \"-\", \"Function\", \"Message Info\"])
        self.__recordset.append([\"Date\", \"Time\", \"DEBUG\", \"File\", \"12\", \"-\", \"Function\", \"Message Info\"])
        self.__recordset.append([\"Date\", \"Time\", \"WARNING\", \"File\", \"12\", \"-\", \"Function\", \"Message Info\"])

        self.__refresh()

    def cleanView(self):
        self.View.delete(0, tk.END)

    def __refresh(self):
        self.cleanView()
        for row in self.__recordset:
            self.View.insert(tk.END, *row)

    def filter_records(self, column, value):
        print(\"Filter Log Recordset by {column} and {value}\".format(**locals()))
        # Filter functionality works as expected
        # [...]

    def cell_context(self, event):
        __cMenu=tk.Menu(self, tearoff=0)

        if self.__recordset_filtered:
            __cMenu.add_command(label=\"Show all\", command=lambda: filter_records(0, \"\"))

        else:
            column=2
            options=[\"INFO\", \"WARNING\", \"DEBUG\"]

            for i in range(len(options)):
                option=options[i]
                __cMenu.add_command(label=\"{}\".format(option), command=lambda: self.filter_records(column, option))
            # Also tried using for option in options here with same result as now
        __cMenu.post(event.x_root, event.y_root)

if __name__==\"__main__\":
    root=tk.Tk()
    app=SomeClass(root)
    root.mainloop()

The current output i get is:

Filter Log Recordset by 2 and DEBUG

No matter which of the three options i choose. I assume it has sth to do with the garbage collection that only the last option remains but i cannot figure out how to avoid this.

Any help is recommended.


回答1:


Please read about minimal examples. Without reading your code, I believe you have run into a well known issue addressed in previous questions and answers that needs 2 lines to illustrate. Names in function bodies are evaluated when the function is executed.

funcs = [lambda: i for i in range(3)]
for f in funcs: print(f())

prints '2' 3 times because the 3 functions are identical and the 'i' in each is not evaluated until the call, when i == 2. However,

funcs = [lambda i=i:i for i in range(3)]
for f in funcs: print(f())

makes three different functions, each with a different captured value, so 0, 1, and 2 are printed. In your statement

__cMenu.add_command(label="{}".format(option),
    command=lambda: self.filter_records(column, option))

add option=option before : to capture the different values of option. You might want to rewrite as

lambda opt=option: self.filter_records(column, opt)

to differentiate the loop variable from the function parameter. If column changed within the loop, it would need the same treatment.




回答2:


Closures in Python capture variables, not values. For example consider:

def f():
    x = 1
    g = lambda : x
    x = 2
    return g()

What do you expect the result of calling f() to be? The correct answer is 2, because the lambda f captured the variable x, not its value 1 at the time of creation.

Now if for example we write:

L = [(lambda : i) for i in range(10)]

we created a list of 10 different lambdas, but all of them captured the same variable i, thus calling f[3]() the result will be 9 because the value of variable i at the end of the iteration was 9 (in Python a comprehension doesn't create a new binding for each iteration; it just keeps updating the same binding).

A "trick" that can be seen often in Python when capturing the value is the desired semantic is to use default arguments. In Python, differently from say C++, default value expressions are evaluated at function definition time (i.e. when the lambda is created) and not when the function is invoked. So in code like:

L = [(lambda j=i: j) for i in range(10)]

we're declaring a parameter j and setting as default the current value of i at the time the lambda was created. This means that when calling e.g. L[3]() the result will be 3 this time because of the default value of the "hidden" parameter (calling L[3](42) will return 42 of course).

More often you see the sightly more confusing form

lambda i=i: ...

where the "hidden" parameter has the same name as the variable of which we want to capture the value of.



来源:https://stackoverflow.com/questions/33983980/lambda-in-for-loop-only-takes-last-value

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!