Lambda function behavior with and without keyword arguments

2024/10/10 12:22:21

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

Answer

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 iyield lambda: i# now, set i to different values successivelyfor i in range(100): yieldg = 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              16 LOAD_CONST               1 (<code object <lambda> at 0xb72ea650, file "<stdin>", line 2>)9 MAKE_CLOSURE             012 YIELD_VALUE13 POP_TOP3          14 SETUP_LOOP              25 (to 42)17 LOAD_GLOBAL              0 (range)20 LOAD_CONST               2 (100)23 CALL_FUNCTION            126 GET_ITER>>   27 FOR_ITER                11 (to 41)30 STORE_DEREF              0 (i)33 LOAD_CONST               0 (None)36 YIELD_VALUE37 POP_TOP38 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            112 GET_ITER>>   13 FOR_ITER                11 (to 27)16 STORE_FAST               0 (i)19 LOAD_CONST               0 (None)22 YIELD_VALUE23 POP_TOP24 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.

https://en.xdnf.cn/q/69897.html

Related Q&A

How to get type annotation within python function scope?

For example: def test():a: intb: strprint(__annotations__) test()This function call raises a NameError: name __annotations__ is not defined error. What I want is to get the type annotation within the f…

General explanation of how epoll works?

Im doing a technical write-up on switching from a database-polling (via synchronous stored procedure call) to a message queue (via pub/sub). Id like to be able to explain how polling a database is vas…

How to return cost, grad as tuple for scipys fmin_cg function

How can I make scipys fmin_cg use one function that returns cost and gradient as a tuple? The problem with having f for cost and fprime for gradient, is that I might have to perform an operation twice…

n-gram name analysis in non-english languages (CJK, etc)

Im working on deduping a database of people. For a first pass, Im following a basic 2-step process to avoid an O(n^2) operation over the whole database, as described in the literature. First, I "b…

How to retrieve all the attributes of LDAP database

I am using ldap module of python to connect to ldap server. I am able to query the database but I dont know how to retrieve the fields present in the database, so that I can notify the user in advance …

Why would this dataset implementation run out of memory?

I follow this instruction and write the following code to create a Dataset for images(COCO2014 training set)from pathlib import Path import tensorflow as tfdef image_dataset(filepath, image_size, batch…

Paramiko: Creating a PKey from a public key string

Im trying to use the SSH protocol at a low level (i.e. I dont want to start a shell or anything, I just want to pass data). Thus, I am using Paramikos Transport class directly.Ive got the server side d…

Appending to the end of a file in a concurrent environment

What steps need to be taken to ensure that "full" lines are always correctly appended to the end of a file if multiple of the following (example) program are running concurrently.#!/usr/bin/e…

Cython Pickling in Package not found as Error

Im having trouble pickling a Cython class, but only when its defined inside a package. This problem was noted previously online, but they didnt state how it was resolved. There are two components here:…

How can I process images faster with Python?

Id trying to write a script that will detect an RGB value on the screen then click the x,y values. I know how to perform the click but I need to process the image a lot faster than my code below curren…