【问题标题】:Read/Write Python Closures读/写 Python 闭包
【发布时间】:2011-01-01 20:05:58
【问题描述】:

闭包是一个非常有用的语言特性。它们让我们可以做一些聪明的事情,否则会需要大量代码,并且通常使我们能够编写更优雅、更清晰的代码。在 Python 2.x 中,闭包变量名不能被反弹;也就是说,在另一个词法范围内定义的函数不能对其本地范围之外的变量执行类似some_var = 'changed!' 的操作。有人可以解释为什么会这样吗?在某些情况下,我想创建一个在外部范围内重新绑定变量的闭包,但这是不可能的。我意识到在几乎所有情况下(如果不是全部),这种行为都可以通过类来实现,但它通常不那么干净或优雅。为什么我不能用闭包来做到这一点?

这里是一个重新绑定闭包的例子:

def counter():
    count = 0
    def c():
        count += 1
        return count
    return c

这是你调用它时的当前行为:

>>> c()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in c
UnboundLocalError: local variable 'count' referenced before assignment

我希望它做的是:

>>> c()
1
>>> c()
2
>>> c()
3

【问题讨论】:

  • 好吧,在这种情况下,itertools.count(1).next。根据我的经验,要找到真正的 Python 使用闭包“不如 Perl/JS/Scheme 干净或优雅”的实际案例并不容易。
  • 你真正要找的是所谓的“发电机”
  • 我选择这个代码示例是因为它很简单,而不是因为它是我真正想在生产中使用的。我会尝试记住一个好的生产示例并对其进行编辑。显然,我选择的示例代码让人们望而却步。
  • @fuzzy-lollipop 这显然是错误的。我知道生成器是什么,并且我经常使用它们。发电机太棒了。然而,生成器不是读/写闭包。
  • 协程看起来可能就是这样。

标签: python language-design closures readonly


【解决方案1】:

扩展伊格纳西奥的回答:

def counter():
    count = 0
    def c():
        nonlocal count
        count += 1
        return count
    return c

x = counter()
print([x(),x(),x()])

在 Python 3 中给出 [1,2,3]; counter() 的调用会给出独立的计数器。其他解决方案 - 尤其是使用 itertools/yield 更为惯用。

【讨论】:

  • 我意识到这不是我能用的最好的例子,它只是看起来最简单的例子。这正是我一直在寻找的(以及他们多年前没有添加“非本地”的原因)。反例只是一段用来说明问题的一次性代码。感谢您明确表示。
  • @northtree nonlocal 语句仅影响范围,它不会影响线程安全。如果多个线程同时执行语句count += 1,将导致错误的结果;它需要在锁中执行。但是,这与问题正交。这取决于用例是否应该在 c 中存在锁定或者是调用者的责任。
【解决方案2】:

你可以这样做,它的工作方式或多或少是一样的:

class counter(object):
    def __init__(self, count=0):
        self.count = count
    def __call__(self):
        self.count += 1
        return self.count    

或者,有点小技巧:

def counter():
    count = [0]
    def incr(n):
        n[0] += 1
        return n[0]
    return lambda: incr(count)

我会选择第一个解决方案。

编辑:这就是我不阅读大型文字博客的结果。

无论如何,Python 闭包相当有限的原因是“因为 Guido 喜欢它”。 Python 是在 90 年代初设计的,正处于 OO 的鼎盛时期。闭包在人们想要的语言特性列表中相当低。随着一流函数、闭包和其他东西等函数式想法进入主流流行,Python 等语言不得不加入它们,因此它们的使用可能有点尴尬,因为这不是该语言的设计目的。

&lt;rant on="Python scoping"&gt;

此外,Python (2.x) 有一些奇怪的(在我看来)关于作用域的想法,这会干扰闭包的合理实现等。这总是困扰着我:

new = [x for x in old]

在我们使用它的范围内定义名称x,因为它(在我看来)是一个概念上更小的范围。 (虽然 Python 获得了一致性分数,但使用 for 循环执行相同的操作具有相同的行为。避免这种情况的唯一方法是使用 map。)

不管怎样,&lt;/rant&gt;

【讨论】:

  • 所有非常好的信息,我很欣赏你的洞察力。我也同意你对 python 范围的评估。不过,问题是“为什么我不能?”,而不是“我该怎么做?”。我想知道为什么语言是这样设计的。
  • 很棒的技巧。在 python 2.7 中使用它并喜欢它(虽然我不喜欢我必须这样做)。
【解决方案3】:

3.x 中的nonlocal 应该解决这个问题。

【讨论】:

  • 这真是个好消息。我会仔细看看的。这是否意味着“非本地”信号解释器为这个函数创建一个 thunk?你知道这是怎么回事吗?
  • 我不知道所有细节,但nonlocal 应该向编译器表明它需要遍历作用域才能找到名称。
  • 对。 AFAIK,唯一可行的方法是在 thunk 中获得 GC 之前,基本上将包含函数的整个范围打包。
【解决方案4】:

我会使用生成器:

>>> def counter():
    count = 0
    while True:
        count += 1
        yield(count)
        
>>> c = counter()
>>> c.next()
1
>>> c.next()
2
>>> c.next()
3

编辑:我相信您问题的最终答案是PEP-3104

在大多数支持嵌套的语言中 范围,代码可以引用或重新绑定 (分配给)最近的任何名称 封闭范围。目前,Python 代码可以引用任何名称 封闭范围,但它只能 在两个范围内重新绑定名称:本地 范围(通过简单的分配)或 模块全局范围(使用全局 声明)。

这个限制已经提出了很多 Python-Dev 邮件列表上的次数 和其他地方,并导致扩展 讨论和许多方法建议 以消除此限制。这个PEP 总结了各种备选方案 被建议的,一起 优点和缺点 每个人都提到了。

2.1版本之前,Python的处理方式 范围类似于标准 C:在一个文件中只有两个 范围、全球和本地级别。在 C,这是自然的结果 函数定义的事实 不能嵌套。但在 Python 中, 虽然通常定义函数 在顶层,一个函数 定义可以在任何地方执行。 这给了 Python 句法 没有嵌套范围的外观 语义,并产生 令人惊讶的不一致 对一些程序员来说——例如,一个 递归函数 顶层将停止工作时 移动到另一个函数内部,因为 递归函数自己的名字 将不再在其中可见 身体的范围。这违反了 一个函数应该的直觉 放置时表现一致 不同的上下文。

【讨论】:

  • +1 我太专注于所需的使用语法,而忘记了生成器。一直很好。
  • 这是一个很好的答案,但这不是我提出的问题的答案。我想知道为什么 python 是这样设计的,而不是如何解决它。我可以用一个类,用生成器(它只是实例化一个类,所以它相当于同一件事),或者可能是其他方式来做到这一点。但我不关心反例,我想知道为什么语言是这样设计的。
  • Python 2.x 认为如果它不在当前范围内,则您正在声明一个新变量。这就是 Python 3.0 引入 nonlocal 关键字来解决此问题的原因。
  • 再一次,“python 认为你在声明一个新变量”是什么,而不是为什么。我从错误消息中得到了这么多。 :-) 不过,我很欣赏这种洞察力。
【解决方案5】:

函数也可以有属性,所以这也可以:

def counter():
    def c():
        while True:
            yield c.count
            c.count += 1
    c.count = 0
    return c

但是,在这个具体示例中,我将使用 jbochi 建议的生成器。

至于为什么,我不能肯定地说,但我想这不是一个明确的设计选择,而是 Python 有时奇怪的作用域规则的残余(尤其是有些奇怪其范围规则的演变)。

【讨论】:

  • +1 获得了一种我以前不知道的方法(以及同意 Python 愚蠢的范围规则)。
  • 这是一个有趣的想法,但实际上它只是一个类声明。事实上,它与 javascript 样式类非常相似。但感谢您的洞察力;我不知道你可以为函数添加任意属性。
  • 是的,它基本上是一个类。而且我发现,当您有使用函数属性的冲动时,您应该 (a) 使用生成器(如本例所示),或 (b) 使用类。 但是我认为值得指出的是函数属性确实存在并且可以用来解决你的问题(尽管我同意它们有点丑) .
  • 您的程序中存在语义错误:count 属性应该应用于内部函数 c 而不是外部函数(返回内部函数的单独实例)
【解决方案6】:

官方Python tutorial 以及Python execution model 中对这种行为进行了相当彻底的解释。特别是从教程中:

Python 的一个特殊之处在于——如果 没有全局声明生效—— 名称的分配总是进入 最里面的范围。

但是,这并没有说明为什么它会以这种方式行事。

更多信息来自PEP 3104,它试图在 Python 3.0 中解决这种情况。
在那里,您可以看到它是这种方式,因为在某个时间点,它被视为最佳解决方案,而不是引入经典的静态嵌套作用域(参见 Re: Scoping (was Re: Lambda binding solved?))。

也就是说,我也有自己的解释。
Python 将命名空间实现为字典;当一个变量的查找在内部失败时,它会在外部尝试,依此类推,直到到达内置。
然而,绑定变量是完全不同的东西,因为你需要指定一个特定的命名空间——它总是最里面的(除非你设置了“全局”标志,这意味着它总是全局命名空间)。
最终,用于查找和绑定变量的不同算法是闭包在 Python 中只读的原因。
但是,再一次,这只是我的猜测:-)

【讨论】:

  • 有趣的想法。从我的角度来看,当你有一个被分配的左值时,最合乎逻辑的事情是首先在符号表中查找它。如果存在,则使用它,如果不存在,则在最内层范围内创建它。但也许这只是我......?
  • 我完全同意你的观点,我已经被 Python 中的类似假设咬了好几次。出于这个原因,我发现用字典来推理非常有用:一旦你以这种方式看到记忆模型,事情就会变得更加清晰。
  • 我知道已经晚了,但我想补充一点,pep 3104 解决了这个想法,why not use it。另外,我确实陷入了这个 SO 问题,因为我正在阅读 PEP 并想要 classic static nested scope 的定义/解释任何提示?
  • +1 :) 这是我找到的唯一答案,试图解释 Python 2.x 中缺少完整的一流闭包
【解决方案7】:

这并不是说它们是只读的,而是你意识到的范围更严格。如果您不能在 Python 3+ 中使用nonlocal,那么您至少可以使用显式范围。 Python 2.6.1,在模块级别有明确的范围:

>>> def counter():
...     sys.modules[__name__].count = 0
...     def c():
...         sys.modules[__name__].count += 1
...         return sys.modules[__name__].count
...     sys.modules[__name__].c = c
...     
>>> counter()
>>> c()
1
>>> c()
2
>>> c()
3

需要做更多的工作才能使 count 变量的范围更受限制,而不是使用伪全局模块变量(仍然是 Python 2.6.1):

>>> def counter():
...     class c():
...         def __init__(self):
...             self.count = 0
...     cinstance = c()
...     def iter():
...         cinstance.count += 1
...         return cinstance.count
...     return iter
... 
>>> c = counter()
>>> c()
1
>>> c()
2
>>> c()
3
>>> d = counter()
>>> d()
1
>>> c()
4
>>> d()
2

【讨论】:

  • 事实证明,无法影响非本地非全局范围内的事物与只读相同。模块级别的显式作用域只是为您提供另一种全局,它不会为您每次执行 counter() 函数提供一个新实例。
  • 承认。请参阅上面我的新附加示例。我正在尝试说出您在最初的问题中所说的想要实现的目标。
  • 我现在想到我的新示例,使用内部cinstance 来保持count 的状态与问题中的第一个示例几乎相同,除了count 现在是在实例内部维护。 counter() 的每次调用都有一个单独的实例,这是您最初想要的。令我困惑的是,为什么内部实例变量cinstance 在作用域方面的处理方式与您的第一个示例中的原始count 不同;你可以自己测试一下,上面的代码不会cinstance上产生UnboundLocalError
【解决方案8】:

扩展 sdcvvc 将参数传递给闭包的答案。

def counter():
    count = 0
    def c(delta=1):
        nonlocal count
        count += delta
        return count
    return c

x = counter()
print([x(), x(100), x(-99)])

线程安全版本:

import threading

def counter():
    count = 0
    _lock = threading.Lock()
    def c(delta=1):
        nonlocal count
        with _lock:
            count += delta
            return count
    return c

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-01-15
    • 2013-05-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多