闭包:有哪些好的使用案例?为什么不用函数对象?它是否值得负面影响?

10

最近我开始学习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

1
我认为这主要归结于Python不是C++,Ruby不是C++,JavaScript不是C++,Perl也不是C++...如果你试图在任何地方都使用函数对象,那么人们会想知道为什么你要在Python中编写C++。我可能会说,对于没有垃圾回收和闭包的语言来说,函数对象是一个补救措施或实现细节,而更动态的语言(通常)不需要这样的补救措施。当你不需要所有这些机器时,为什么要定义一个全新的类呢? - mu is too short
1
这并不是真正的内存泄漏 - 只有在返回的函数仍然在作用域中时,该变量才会被保留,而且没有理由相信它会比需要的时间更长。 - Gareth Latty
4
您似乎在以“闭包是不好的”为思维方式提出这个问题,并试图找到方法来证明这一点。是的,闭包存在一些不良的使用情况,但这并不意味着它们本质上是不好的。 - Gareth Latty
我并不认为它们一定是不好的,我只是在比较潜在的上行空间和下行风险。我发布的一些代码在非常简单的示例中具有非常奇怪的行为。在大型复杂的代码中发现类似性质的错误将不会很有趣。正如我所说,当闭包在函数中定义时,它更加安全。否则,你可能会遇到奇怪的问题。而且,由于另一个结构可以提供相同的功能,所以它略微冗长的事实似乎并不那么糟糕。 - Nir Friedman
一等函数、lambda表达式(匿名函数)和闭包在可用的地方都被广泛使用。每个人都会告诉你它们是一个很棒的东西。光这一点就足以证明它们带来的好处大于坏处。你可以使用任何工具做愚蠢的事情,只需要用它来做好事即可。在C语言中,宏可能会做出可怕的事情并阻碍语言的良好工具支持。但有时它们是正确的工具。我敢打赌,闭包有更好的平衡... - ziggystar
显示剩余4条评论
1个回答

10

易读性。你的Python示例显示闭包版本与函数对象相比更容易理解和阅读。

我们还避免了创建一个仅充当函数的类——这看起来有些冗余。

即使没有其他原因,在执行某些操作时,用动作描述它比用对象更合理。

另外,这些结构在Python中被广泛应用于修饰器等方面,其效果非常好,是非常有用的功能。

编辑:关于状态的一点说明,记住函数在Python中并不特殊,它们仍然是对象:

>>> def makeAdder(y):
...     def myAdder(x):
...         return x + myAdder.y
...     myAdder.y = y
...     return myAdder
... 
>>> f = makeAdder(10)
>>> f(5)
15
>>> f.y
10

1
我认为这是一个品味问题。我个人同意,但我遇到过很多人觉得函数返回函数的想法很难理解,因为他们很难理清外部和内部函数何时被调用。 - BrenBarn
4
鉴于Python的动态特性,函数作为一等对象的概念是非常核心的,我认为类像函数一样运作并不比这更容易理解。虽然我能理解你的观点,但我认为这并不能反驳它们的优势。 - Gareth Latty
1
我认为可以问一下它是否真的更易读。毕竟:这不仅是一个函数,它还有状态。此外,如果您以后需要检查状态的任何内容,使用对象会更容易。 - Nir Friedman
@NirFriedman 你是在暗示所有函数都没有状态吗?如果需要从另一个上下文检查状态,那么也许你的示例已经超出了闭包的范围,但这并不意味着它们不好。记住这不是Java,我们不需要担心实现的一致性,如果我们发现需要,以后可以更改。请参见我的编辑。 - Gareth Latty
Latty,我的意思是传统意义上的函数没有状态,而不是Python中函数的具体实现不能有状态。 如果我们要携带状态,为什么不明确地表明我们正在携带它呢?与函数式版本相比,我的类版本只多了一行代码。 总的来说,我更担心在其他情况下使用闭包,详见我对原问题的编辑。 - Nir Friedman
@NirFriedman 有没有什么理由,就因为其他语言中的函数不提供这个功能,我们就应该在Python中避免它?除此之外,你已经不再谈论一般的闭包了。是的,有些情况下闭包并不是最好的选择。你似乎正在改变你的问题,直到没有好的用例留下来。 - Gareth Latty

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