带有和不带有关键字参数的Lambda函数行为差异

7

我正在使用lambda函数与tkinter进行GUI编程。最近,在实现打开文件的按钮时遇到了困难:

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

您好,我希望定义一个可以更新的文件路径,但在创建GUI时这个路径是未知的。 我之前遇到的问题是代码如下:

button = Button(conf_f, text="Tools opt.",
        command=lambda f=self.file: tktb.helpers.openfile(f))
lambda函数有一个关键字参数,可以传递文件路径。在这种情况下,当 self.file 更新时,参数f没有被更新。
我从代码片段中得到了这个关键字参数,并且我在很多地方都使用它。显然我不应该这样做……
我还是不太清楚……能有人解释一下这两种lambda形式的区别以及何时使用哪一种吗?
注:下面的评论带领我找到了解决方案,但我想要更多的解释:lambda working oddly with tkinter

你还需要什么更详细的解释吗?你链接到的问题的被接受答案已经相当简明地解释了这个区别。 - BrenBarn
事实上,我想了解为什么参数值可以根据编码风格更新或不更新。 - Plouff
1个回答

13
我会尝试更深入地解释一下。
如果您这样做
i = 0
f = lambda: i

你创建一个函数(λ本质上是一个函数),该函数访问其封闭作用域的i变量。

内部,它通过拥有所谓的闭包来实现这一点,其中包含i。粗略地说,它是指向真实变量的指针,在不同时间点可以保存不同的值。

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

在这里,所有i的值 - 在它们的时间内 - 都存储在该闭包中。如果函数f()需要它们,它会从那里获取。

您可以在反汇编列表中看到这种差异:

这些所谓的函数a()f()的反汇编如下:

>>> 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

将其与函数b()进行比较,它看起来像:

>>> 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

循环中的主要区别在于

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

b()中对比。

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

a() 函数中: STORE_DEREF 存储在一个 cell 对象(闭包)中,而 STORE_FAST 使用一个“普通”的变量,这可能会稍微快一些。

lambda 表达式也有所不同:

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

这里您使用了LOAD_GLOBAL,而上面的代码使用了LOAD_DEREF。同样,后者用于闭包。

我完全忘记了 lambda i=i: i

如果您将值作为默认参数,它会通过完全不同的路径进入函数:当前的i值通过默认参数传递给刚创建的函数:

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

这样,函数将被调用为f()。它检测到缺少参数并使用默认值填充相应的参数。所有这些都发生在函数被调用之前;在函数内部,您只看到值被获取并返回。

还有另一种方法可以完成您的任务:只需像使用接受值的lambda一样使用它:lambda i: i。如果您调用它,它会抱怨缺少参数。

但是,您可以通过使用functools.partial来处理这个问题:

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

这个包装器获取一个可调用对象和一些要传递的参数。结果对象是一个可调用对象,它使用这些参数加上你提供给它的任何参数来调用原始的可调用对象。在此可用于保持锁定到所需值。


所以,如果我想看到差异,我需要运行:1/ dis.dis(lambda: i) 和 2/ dis.dis(lambda i=i: i)。我会得到一个LOAD_GLOBAL和一个LOAD_DEREF(希望我是正确的...)。我现在不能做这个,但我明天会尝试。非常感谢您的答复!我还需要进一步阅读有关函数闭包的内容。我从未听说过这个... - Plouff
@Plouff 大致上是这样的,是的。我刚刚加入了一些关于这个问题的想法。 - glglgl
非常感谢您的回答。我不能说现在一切都清楚了,因为我不太习惯这种事情。但我认为我理解了最重要的部分。我还注意到,正是tkinter绑定让我困惑了。由于您需要将“事件”作为参数传递,因此需要在lambda中使用参数。之后,每次我需要lambda函数时,我都使用kwd参数。而那是不正确的... - Plouff
@Plouff 这不是一个容易的主题;最好记住访问“外部”变量意味着访问执行时的值,而不是定义时的值。 - glglgl
在我的案例中,我会尝试记住反命题:即使在Lambda函数中,默认关键字参数的默认值也会在定义时存储。我之前认为存储的是指针而不是值。 - Plouff

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接