Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

lambda in for loop only takes last value [duplicate]

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.

like image 870
R4PH43L Avatar asked Nov 29 '15 14:11

R4PH43L


People also ask

How do you do a for loop in Python lambda?

Since a for loop is a statement (as is print , in Python 2. x), you cannot include it in a lambda expression. Instead, you need to use the write method on sys. stdout along with the join method.

Can lambda take only one input?

The function allows multiple input arguments. expression : What the function will do in a single expression. Lambda only accepts one expression, but it can generate multiple outputs.

Can lambda function return more than one value?

The characteristics of lambda functions are:Lambda functions are syntactically restricted to return a single expression. You can use them as an anonymous function inside other functions. The lambda functions do not need a return statement, they always return a single expression.

Can lambda take multiple arguments?

A lambda function can have as many arguments as you need to use, but the body must be one single expression.


2 Answers

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.

like image 147
Terry Jan Reedy Avatar answered Sep 19 '22 08:09

Terry Jan Reedy


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.

like image 41
6502 Avatar answered Sep 18 '22 08:09

6502