lambda in for loop only takes last value

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.

Answers:

Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.

Method 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.

Method 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 L[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.

Method 3

I know I am late, but I found a messy workaround which gets the job done (tested in Python 3.7)

If you use a double lambda (like I said, very messy) you can preserve the value, like so:

Step 1: Create the nested lambda statement:

send_param = lambda val: lambda: print(val)

Step 2: Use the lambda statement:

send_param(i)

The send_param method returns the inner most lambda (lambda: print(val)) without executing the statement, until you call the result of send_param which takes no arguments, for example:

a = send_param(i)
a()

Only the second line will execute the print statement.


All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x