Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python: lambda function behavior with and without keyword arguments?

I am using lambda functions for GUI programming with tkinter. Recently I got stuck when implementing buttons that open files:

self.file=""
button = Button(conf_f, text="Tools opt.",
        command=lambda: tktb.helpers.openfile(self.file))

As you see, I want to define a file path that can be updated, and that is not known when creating the GUI. The issue I had is that earlier my code was :

button = Button(conf_f, text="Tools opt.",
        command=lambda f=self.file: tktb.helpers.openfile(f))

The lambda function had a keyword argument to pass the file path. In this case, the parameter f was not updated when self.file was.

I got the keyword argument from a code snippet and I use it everywhere. Obviously I shouldn't...

This is still not clear to me... Could someone explain me the difference between the two lambda forms and when to use one an another?

Thank you!

PS: The following comment led me to the solution but I'd like a little more explanations: lambda working oddly with tkinter

like image 452
Plouff Avatar asked Jul 16 '13 07:07

Plouff


1 Answers

I'll try to explain it more in depth.

If you do

i = 0
f = lambda: i

you create a function (lambda is essentially a function) which accesses its enclosing scope's i variable.

Internally, it does so by having a so-called closure which contains the i. It is, loosely spoken, a kind of pointer to the real variable which can hold different values at different points of time.

def a():
    # first, yield a function to access i
    yield lambda: i
    # now, set i to different values successively
    for i in range(100): yield

g = a() # create generator
f = next(g) # get the function
f() # -> error as i is not set yet
next(g)
f() # -> 0
next(g)
f() # -> 1
# and so on
f.func_closure # -> an object stemming from the local scope of a()
f.func_closure[0].cell_contents # -> the current value of this variable

Here, all values of i are - at their time - stored in that said closure. If the function f() needs them. it gets them from there.

You can see that difference on the disassembly listings:

These said functions a() and f() disassemble like this:

>>> dis.dis(a)
  2           0 LOAD_CLOSURE             0 (i)
              3 BUILD_TUPLE              1
              6 LOAD_CONST               1 (<code object <lambda> at 0xb72ea650, file "<stdin>", line 2>)
              9 MAKE_CLOSURE             0
             12 YIELD_VALUE
             13 POP_TOP

  3          14 SETUP_LOOP              25 (to 42)
             17 LOAD_GLOBAL              0 (range)
             20 LOAD_CONST               2 (100)
             23 CALL_FUNCTION            1
             26 GET_ITER
        >>   27 FOR_ITER                11 (to 41)
             30 STORE_DEREF              0 (i)
             33 LOAD_CONST               0 (None)
             36 YIELD_VALUE
             37 POP_TOP
             38 JUMP_ABSOLUTE           27
        >>   41 POP_BLOCK
        >>   42 LOAD_CONST               0 (None)
             45 RETURN_VALUE
>>> dis.dis(f)
  2           0 LOAD_DEREF               0 (i)
              3 RETURN_VALUE

Compare that to a function b() which looks like

>>> def b():
...   for i in range(100): yield
>>> dis.dis(b)
  2           0 SETUP_LOOP              25 (to 28)
              3 LOAD_GLOBAL              0 (range)
              6 LOAD_CONST               1 (100)
              9 CALL_FUNCTION            1
             12 GET_ITER
        >>   13 FOR_ITER                11 (to 27)
             16 STORE_FAST               0 (i)
             19 LOAD_CONST               0 (None)
             22 YIELD_VALUE
             23 POP_TOP
             24 JUMP_ABSOLUTE           13
        >>   27 POP_BLOCK
        >>   28 LOAD_CONST               0 (None)
             31 RETURN_VALUE

The main difference in the loop is

        >>   13 FOR_ITER                11 (to 27)
             16 STORE_FAST               0 (i)

in b() vs.

        >>   27 FOR_ITER                11 (to 41)
             30 STORE_DEREF              0 (i)

in a(): the STORE_DEREF stores in a cell object (closure), while STORE_FAST uses a "normal" variable, which (probably) works a little bit faster.

The lambda as well makes a difference:

>>> dis.dis(lambda: i)
  1           0 LOAD_GLOBAL              0 (i)
              3 RETURN_VALUE

Here you have a LOAD_GLOBAL, while the one above uses LOAD_DEREF. The latter, as well, is for the closure.

I completely forgot about lambda i=i: i.

If you have the value as a default parameter, it finds its way into the function via a completely different path: the current value of i gets passed to the just created function via a default parameter:

>>> i = 42
>>> f = lambda i=i: i
>>> dis.dis(f)
  1           0 LOAD_FAST                0 (i)
              3 RETURN_VALUE

This way the function gets called as f(). It detects that there is a missing argument and fills the respective parameter with the default value. All this happens before the function is called; from within the function you just see that the value is taken and returned.

And there is yet another way to accomplish your task: Just use the lambda as if it would take a value: lambda i: i. If you call this, it complains about a missing argument.

But you can cope with that with the use of functools.partial:

ff = [functools.partial(lambda i: i, x) for x in range(100)]
ff[12]()
ff[54]()

This wrapper gets a callable and a number of arguments to be passed. The resulting object is a callable which calls the original callable with these arguments plus any arguments you give to it. It can be used here to keep locked to the value intended.

like image 112
glglgl Avatar answered Nov 03 '22 06:11

glglgl