Python闭包未按预期工作

12
当我运行以下脚本时,两个lambda都会在同一个文件junk.txt上运行os.startfile()。我希望每个lambda使用它创建时设置的值"f"。有没有办法让它按照我的期望工作?
import os


def main():
    files = [r'C:\_local\test.txt', r'C:\_local\junk.txt']
    funcs = []
    for f in files:
        funcs.append(lambda: os.startfile(f))
    print funcs
    funcs[0]()
    funcs[1]()


if __name__ == '__main__':
    main()

类似于在Python中使用lambda表达式在循环内部生成函数。 - Rodrigue
2个回答

24

有一种方法是这样做:

def main():
    files = [r'C:\_local\test.txt', r'C:\_local\junk.txt']
    funcs = []
    for f in files:
        # create a new lambda and store the current `f` as default to `path`
        funcs.append(lambda path=f: os.stat(path))
    print funcs

    # calling the lambda without a parameter uses the default value
    funcs[0]() 
    funcs[1]()
否则f在函数调用时被查找,因此您会得到当前(循环后)的值。
我喜欢的更好的方式:
def make_statfunc(f):
    return lambda: os.stat(f)

for f in files:
    # pass the current f to another function
    funcs.append(make_statfunc(f))

甚至可以在 Python 2.5+ 中:

from functools import partial
for f in files:
    # create a partially applied function
    funcs.append(partial(os.stat, f))

8
这是因为默认参数在函数定义时就被绑定了。 - user395760
啊,太棒了。生成的lambda实际上被用作PyQt的事件处理程序,因此它们不能有任何输入参数,而且我目前被困在Python 2.4中,所以我无法使用functools,但第二个解决方案——将其传递给另一个函数——非常完美。谢谢。 - user297250
@Brendan,如果你定义了qt slots,那么应该能够带有默认输入参数,对吧?如果采用第二种解决方案,你可能还想要应用装饰器“PyQt4.QtCore.pyqtSlot”。 - Neil G

5
重要的是要理解,当一个变量成为闭包的一部分时,它本身被包含在内,而不是其值。
这意味着在循环中创建的所有闭包都使用同一个变量"f",在循环结束时,该变量将包含循环内使用的最后一个值。
然而,由于语言的定义方式,在Python 2.x中,这些被捕获的变量是“只读”的:任何赋值都会使变量成为局部变量,除非它被声明为“global”(Python 3.x添加了“nonlocal”关键字,允许写入外部作用域的局部变量)。
如Jochen Ritzel在他的回答中所说,避免此变量捕获并取代值捕获的常见习惯用法是编写
lambda f=f: os.startfile(f)

这段代码有效是因为默认参数值在函数创建时被计算,f不是外部变量而是一个函数参数,它将具有所需的默认值(因此此lambda只是一个带有参数默认值的函数,不再关闭任何词法变量)。

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