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.
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.
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.
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.
A lambda function can have as many arguments as you need to use, but the body must be one single expression.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With