最近我开始学习Python。以前,我主要用C++和Matlab编写数值和数据分析代码。我看到了很多关于Python、Ruby和闭包的讨论。几乎所有的例子都长得像这样:
>>> def makeAdder(y):
... def myAdder(x):
... return x + y
... return myAdder
...
>>> f = makeAdder(10)
>>> f(5)
15
我知道这在某种程度上可能是有用的。但是,现实情况下,在像这种“只读”情况下,可以很容易地通过一个对象(一个函数对象)来模拟行为:
>>> class MyAdder(object):
... def __init__(self,y):
... self.y = y
... def __call__(self,x):
... return self.y + x
...
>>> f = MyAdder(5)
>>> f(10)
15
这种对象在编码时不需要占用更多的空间,而且更加灵活。跟踪和调试后续代码也更加容易。
在这种情况下,我们只读取了非局部变量。但是我们也可以对它进行写操作:在Ruby中自然可以,在Python中使用nonlocal关键字。当然,该对象也支持此功能。但是使用该对象,您将数据捆绑在一起,因此您确切地知道发生了什么。闭包可能以完全不透明的方式携带变量,这可能导致难以调试的代码。以下是一个非常奇怪的例子:
irb(main):001:0> def new_counter
irb(main):002:1> x = 0
irb(main):003:1> lambda { x +=1 }
irb(main):004:1> end
=> nil
irb(main):005:0> counter_a = new_counter
=> #<Proc:0x00007f85c6421cd0@(irb):3>
irb(main):006:0> counter_a.call
=> 1
irb(main):007:0> counter_a.call
=> 2
对我来说,这种行为很难理解。它还可能导致内存泄漏。这给了你很大的自由度,可以让你自寻烦恼。尤其是在Ruby中,你不需要显式地启用它(与Python不同),因为在Ruby中,主要代码中到处都有块,它们可以访问所有内容。如果外部变量由于闭包而发生更改,如果你传递该闭包,你可以将一个变量无限期地更改,并且超出了它所在的范围。相比之下,对象始终安全地携带其数据。
为什么会听到很多关于闭包的好处以及它们应该被潜在地包含在Java中,当它们没有完全在Python中时,它们是如何失败的等等的谈论?为什么不使用函数对象?或者重构代码以避免使用它们,因为它们非常危险?只是为了澄清,我不是那些口吐白沫的面向对象类型。我是否低估了它们的用途,过高地评估了它们的危险性,还是两者都是?
编辑:也许我应该区分三个东西:只读一次的闭包(这是我的示例显示的,并且几乎每个人都会讨论),一般读取的闭包和写入的闭包。如果你在另一个函数内定义一个函数,并使用外部函数的本地变量,那么我几乎想不出这将对你产生任何影响。该空间中的变量无法以任何方式被访问,因此您无法更改它。这非常安全,并且是生成函数的方便方法(可能比函数对象更方便)。
另一方面,如果你在类方法或主线程内创建闭包,它将每次调用时读取可以从其他地方访问的变量。因此它可能会发生变化。我认为这很危险,因为闭合变量不出现在函数头中。你可以在代码第一页上拥有一个长的闭包,它关闭了一个主线程变量x,然后出于无关原因修改x。然后重新使用闭包并获得你不理解的奇怪行为,这可能很难调试。
如果你实际上写入封闭变量,那么正如我在Ruby示例中所示,你真的有可能制造混乱并导致意外行为。
编辑2:我给出了第三种用法(写入非本地变量)的闭包奇怪行为的示例。下面是第二种用法的奇怪(不那么糟糕)行为的示例(在可以修改其封闭变量的范围内定义闭包):
>>> fs = [(lambda n: i + n) for i in range(10)]
>>> fs[4](5)
14