循环中的Lambda

102

考虑以下代码片段:

# directorys == {'login': <object at ...>, 'home': <object at ...>}
for d in directorys:
    self.command["cd " + d] = (lambda : self.root.change_directory(d))

我希望创建一个包含以下两个函数的字典:

# Expected :
self.command == {
    "cd login": lambda: self.root.change_directory("login"),
    "cd home": lambda: self.root.change_directory("home")
}

但是看起来生成的两个 Lambda 函数完全相同:

# Result :
self.command == {
    "cd login": lambda: self.root.change_directory("login"),
    "cd home": lambda: self.root.change_directory("login")   # <- Why login ?
}

我真的不明白为什么。你有什么建议吗?

4个回答

123

你需要为每个创建的函数绑定d。一种方法是将其作为具有默认值的参数传递:

lambda d=d: self.root.change_directory(d)

现在函数中的d使用了参数,尽管它们有相同的名称,在函数创建时会评估该参数的默认值。为了帮助你理解这个过程:

lambda bound_d=d: self.root.change_directory(bound_d)

请记住默认值是如何工作的,例如对于列表和字典等可变对象,因为您正在绑定一个对象。

这种具有默认值参数的习惯用法非常普遍,但如果您检查函数参数并基于它们的存在确定要执行什么操作,则可能会失败。您可以使用另一个闭包避免参数:

(lambda d=d: lambda: self.root.change_directory(d))()
# or
(lambda d: lambda: self.root.change_directory(d))(d)

5
我没有意识到在lambda函数中可以使用默认值。将带有默认参数的lambda函数作为关键字参数传递,看起来很奇怪:command = lambda path = path: selected(path) - ArtOfWarfare
是的,这是正确的。但是如果函数的签名被声明了,我该怎么办?比如tkinter的bind或trace函数的情况。 - Nikolai Ehrhardt

33

这是由于d绑定的位置。Lambda函数都指向变量d而不是当前值,所以当你在下一次迭代中更新d时,此更新会在所有函数中看到。

举个简单的例子:

funcs = []
for x in [1,2,3]:
  funcs.append(lambda: x)

for f in funcs:
  print f()

# output:
3
3
3

您可以通过添加一个额外的函数来解决这个问题,就像这样:

def makeFunc(x):
  return lambda: x

funcs = []
for x in [1,2,3]:
  funcs.append(makeFunc(x))

for f in funcs:
  print f()

# output:
1
2
3

您还可以在lambda表达式内部修复作用域

lambda bound_x=x: bound_x

然而,一般来说这并不是一个好的做法,因为你已经改变了函数的签名。


在 Python 中,使用 def 在 for 循环中定义闭包时是否会出现相同的问题,即 for 循环不会拥有自己的作用域? - bob
@robbie_c 一个有趣的行为来支持你的第一个例子:如果在创建和附加各种函数的循环之后删除x,调用f()将抛出一个NameError,指出x不存在。 - Guimoute

17

或者,你可以使用functools.partial代替lambda。在我看来,这种方式语法更加简洁。

替换为:

for d in directorys:
    self.command["cd " + d] = (lambda d=d: self.root.change_directory(d))

它将会是:

for d in directorys:
    self.command["cd " + d] = partial(self.root.change_directory, d)

或者,这里有另一个简单的例子:

numbers = [1, 2, 3]

lambdas = [lambda: print(number) 
           for number in numbers]
lambdas_with_binding = [lambda number=number: print(number) 
                        for number in numbers]
partials = [partial(print, number) 
            for number in numbers]

for function in lambdas:
    function()
# 3 3 3
for function in lambdas_with_binding:
    function()
# 1 2 3
for function in partials:
    function()
# 1 2 3

3
这是一种更清晰的方法,我会在机会出现时推荐它。lambda x=x技巧不仅不直观,而且利用了另一个上下文中的经典陷阱。使用functools.partial绑定参数是显式的,并且也是eta-reduced(使其更DRY)。哦,还有那个默认参数可以被覆盖,但不应该 - 这是一个潜在的陷阱。 - Karl Knechtel

3

我遇到了相同的问题。所选解决方案对我很有帮助,但我认为有必要添加一个精度来使问题的代码正常运行:在循环外定义lambda函数。顺便说一下,默认值是不必要的。

foo = lambda d: lambda : self.root.change_directory(d)
for d in directorys:
    self.command["cd " + d] = (foo(d))

在我看来,这是最干净和最好的答案。它让我想起了lambda演算中的所有内容,lambda x. lambda y. ... - Jonathan Mugan
@JonathanMugan 它怎么比 self.command["cd " + d] = lambda d=d: self.root.change_directory(d) 更简洁? - Guimoute

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