Python: lambda function behavior with and without keyword arguments? Python: lambda function behavior with and without keyword arguments? tkinter tkinter

Python: lambda function behavior with and without keyword arguments?


I'll try to explain it more in depth.

If you do

i = 0f = 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): yieldg = a() # create generatorf = next(g) # get the functionf() # -> error as i is not set yetnext(g)f() # -> 0next(g)f() # -> 1# and so onf.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.