【问题标题】:Closures: What is a good use case example? Why not a functor? And is it worth the negatives?闭包:什么是好的用例示例?为什么不是函子?它值得负面吗?
【发布时间】:2012-12-26 19:43:17
【问题描述】:

我最近涉足 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 中时它是多么糟糕等等?为什么不使用函子?或者重构代码以避免,考虑到它们有多危险?澄清一下,我不是那种口吐白沫的OO类型。我是否低估了它们的用途,夸大了它们的危险,或两者兼而有之?

编辑:也许我应该区分三件事:只读取一次的闭包(这是我的示例显示的,几乎每个人都在讨论),一般读取的闭包和写入的闭包。如果您使用外部函数的局部变量在另一个函数内部定义一个函数,那么这几乎不会再次困扰您。我无法以任何我能想到的方式访问该空间中的变量,因此您无法更改它。这是非常安全的,并且是一种方便的(可能不仅仅是仿函数)生成函数的方法。 另一方面,如果您在类方法或主线程内创建一个闭包,它会在每次调用时读取可以从其他地方访问的变量。所以它可以改变。我认为这是危险的,因为关闭的变量不会出现在函数头中。您可以在代码的第 1 页上说一个长闭包,它关闭了主线程变量 x,然后出于不相关的原因修改 x。然后重新使用闭包,得到你不理解的奇怪行为,这可能很难调试。 如果您真的写入封闭变量,那么正如我使用 Ruby 的示例所示,您确实有可能造成混乱并导致意外行为。

Edit2:我给出了第三次使用闭包的奇怪行为示例,写入非局部变量。这是第二次使用(在可以修改其封闭变量的范围内定义闭包)的奇怪(不那么糟糕)行为的示例:

>>> fs = [(lambda n: i + n) for i in range(10)]
>>> fs[4](5)
14

【问题讨论】:

  • 我不懂 Ruby,所以我不知道您关于内存泄漏的说法是否现实。这在 Python 中不是问题,也许您可​​以澄清这一点?
  • 同意,特别是我不知道非常危险从何而来。
  • 我认为这主要归结为 Python 不是 C++,Ruby 不是 C++,JavaScript 不是 C++,Perl 不是 C++,...如果你尝试在任何地方都使用仿函数,那么人们会想知道为什么你要尝试用 Python 编写 C++。我可能会说仿函数是没有 GC 和闭包的语言的杂项或实现细节,这是更动态的语言(通常)不需要的杂项。当您不需要所有这些机器时,为什么要定义一个全新的类?
  • 这并不是真正的内存泄漏 - 该变量只会在返回的函数保持在作用域内时保留,并且没有理由相信它会比需要的时间长。
  • 您似乎以“关闭是不好的”的心态提出了这个问题,并试图找到方法来证明这一点。是的,闭包有不好的用途,但这并不意味着它们天生就不好。

标签: python ruby closures functor


【解决方案1】:

可读性。您的 Python 示例显示了与函子相比,闭包版本更加明显和易于阅读。

我们还巧妙地避免创建一个除了像函数一样什么都不做的类——这有冗余的味道。

如果没有别的,当我们某事时,it makes sense to describe it as an action, not an object.

另外说明,在 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

【讨论】:

  • 我认为这是一个品味问题。我个人同意,但我遇到过很多人觉得函数返回函数的想法很难理解,因为当调用外部函数和内部函数时,他们很难理解。
  • @BrenBarn 鉴于 Python 的动态特性,函数是一阶对象的想法是一个非常核心的想法,我不认为像函数一样的类是一个更简单的概念。并不是说我不明白你的观点,但我认为这对他们来说并不重要。
  • 我想人们可以问问它是否真的更具可读性。毕竟:这不仅仅是一个函数,它还有状态。此外,如果您以后需要检查有关状态的任何内容,使用对象会更容易。
  • @NirFriedman 你是在暗示所有函数都没有状态吗?如果您需要从另一个上下文检查状态,那么您的示例可能已经超出了闭包的范围,是的,但这并不会使它们变坏。请记住,这不是 Java,我们不需要担心与实现它的方式保持一致,如果我们发现需要,我们可以在以后更改它。另请参阅我的编辑。
  • 拉蒂,我更多的意思是传统意义上的函数没有状态,并不是Python中函数的具体实现不能有状态。看起来如果我们要随身携带状态,为什么不明确说明我们正在随身携带呢?与功能版本相比,我的类版本只是多行代码。总的来说,在其他情况下使用闭包让我更加烦恼,请参阅我对原始问题的编辑。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-12-21
  • 2018-12-05
  • 2017-06-06
  • 1970-01-01
  • 2010-09-08
  • 2016-06-24
  • 1970-01-01
相关资源
最近更新 更多