8

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?

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

2
  • What more explanation do you need? The accepted answer to the question you linked to explains the difference pretty succinctly.
    – BrenBarn
    Commented Jul 16, 2013 at 7:55
  • In fact, I'd like to understand why the argument value can be updated or not depending of the coding style.
    – Plouff
    Commented Jul 16, 2013 at 8:00

1 Answer 1

13

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.

5
  • So if I want to see the difference I need to run: 1/ dis.dis(lambda: i) and 2/ dis.dis(lambda i=i: i). I'll get one LOAD_GLOBAL and one LOAD_DEREF (hope I am right...). I can't do this now but I'll try tomorrow. Thank you very much for your answer! I also need further reading on function closures. I never heard anything about that...
    – Plouff
    Commented Jul 16, 2013 at 15:52
  • @Plouff Roughly spoken, yes. I just added some more thoughts about that.
    – glglgl
    Commented Jul 16, 2013 at 16:17
  • Thank you so much for your answer. I can't say everything is clear now since I am not so used to this kind of thing. But I think I understood the most important parts. I also noticed that it was the tkinter bindings that mess up my mind. Since you need to pass the event as argument, you need argument in lambda. After that, each time I needed lambda functions, I used kwd arguments. And that was not correct...
    – Plouff
    Commented Jul 17, 2013 at 7:43
  • @Plouff It is not an easy subject; the best is to remember that access to an "outermore" variable means accessing the value at the time of execution, not at the time of definition.
    – glglgl
    Commented Jul 17, 2013 at 8:28
  • In my case I'll try to remember the contrapositive statement: default values of keyword arguments are stored at definition even in lambda functions. I thought a pointer was stored instead of a value.
    – Plouff
    Commented Jul 17, 2013 at 8:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.