【问题标题】:Why doesn't this closure modify the variable in the enclosing scope?为什么这个闭包不修改封闭范围内的变量?
【发布时间】:2011-11-24 01:14:39
【问题描述】:

这点 Python 不行:

def make_incrementer(start):
    def closure():
        # I know I could write 'x = start' and use x - that's not my point though (:
        while True:
            yield start
            start += 1
    return closure

x = make_incrementer(100)
iter = x()
print iter.next()    # Exception: UnboundLocalError: local variable 'start' referenced before assignment

我知道如何解决该错误,但请耐心等待:

这段代码运行良好:

def test(start):
    def closure():
        return start
    return closure

x = test(999)
print x()    # prints 999

为什么我可以读取闭包内的start 变量但不能写入它? 什么语言规则导致了start 变量的这种处理?

更新:我发现这篇 SO 帖子相关(答案比问题更重要):Read/Write Python Closures

【问题讨论】:

  • 您在评论中提到的“重新绑定到局部变量”解决方案将比每次访问容器项执行得更好。它也更 Pythonic。请参阅我对替代方案的回答,这些替代方案也比仅针对副作用使用容器更具 Pythonic。
  • 这确实与 Read/Write Python Closures 完全相同

标签: python closures generator


【解决方案1】:

每当您在函数内部分配变量时,它将是该函数的局部变量。 start += 1 行正在为start 分配一个新值,因此start 是一个局部变量。由于存在局部变量 start,因此当您第一次尝试访问它时,该函数不会尝试在全局范围内查找 start,因此您会看到错误。

在 3.x 中,如果您使用 nonlocal 关键字,您的代码示例将起作用:

def make_incrementer(start):
    def closure():
        nonlocal start
        while True:
            yield start
            start += 1
    return closure

在 2.x 上,您通常可以通过使用 global 关键字来解决类似问题,但这在这里不起作用,因为 start 不是全局变量。

在这种情况下,您可以执行您建议的操作 (x = start),也可以使用可变变量来修改并生成内部值。

def make_incrementer(start):
    start = [start]
    def closure():
        while True:
            yield start[0]
            start[0] += 1
    return closure

【讨论】:

  • 所以第二段代码有效,因为它没有分配给start,因此 Python 将范围遍历到我真正想要的那个,我接受它......
【解决方案2】:

在 Python 2.x 上有两种“更好”/更 Pythonic 的方法可以做到这一点,而不是使用容器来解决缺少 nonlocal 关键字的问题。

您在代码的注释中提到的一个 - 绑定到局部变量。还有另一种方法:

使用默认参数

def make_incrementer(start):
    def closure(start = start):
        while True:
            yield start
            start += 1
    return closure

x = make_incrementer(100)
iter = x()
print iter.next()

这具有局部变量的所有优点,而无需额外的代码行。它也发生在x = make_incrememter(100) 行而不是iter = x() 行上,这可能会或可能不会取决于具体情况。

您还可以使用“不实际分配给引用的变量”方法,以比使用容器更优雅的方式:

使用函数属性

def make_incrementer(start):
    def closure():
        # You can still do x = closure.start if you want to rebind to local scope
        while True:
            yield closure.start
            closure.start += 1
    closure.start = start
    return closure

x = make_incrementer(100)
iter = x()
print iter.next()    

这适用于所有最新版本的 Python,并利用了这样一个事实,即在这种情况下,您已经拥有一个知道名称的对象,您可以在其上引用属性——无需为此目的创建新容器.

【讨论】:

  • 很好的解释,除了顶部的rebinding to a local variable -- 你实际上只是在绑定。
  • @EthanFurman 你说得对,我的意思是“也绑定”而不是“重新绑定”。
【解决方案3】:

Example

def make_incrementer(start):
    def closure():
        # I know I could write 'x = start' and use x - that's not my point though (:
        while True:
            yield start[0]
            start[0] += 1
    return closure

x = make_incrementer([100])
iter = x()
print iter.next()

【讨论】:

  • 嗯,有趣的解决问题的方法;投赞成票。
  • 闭包变量不是只读的,否则您将无法修改它们。引用也不正确——实例__dict__s 没有关闭。
  • 闭包变量不像全局变量那样是只读的(没有global 语句)——问题是Python对任何赋值变量的假设,而不是关于闭包变量的任何假设他们自己。 F.J 的回答修改了一个闭包变量,不是吗?它只是不重新绑定闭包名称,这并不完全相同。
  • @agf,我错了。我对此并不满意,但你们是对的。
【解决方案4】:

在 Python 3.x 中,您可以使用 nonlocal 关键字重新绑定不在本地范围内的名称。在 2.x 中,您唯一的选择是 modifying(或 mutating)闭包变量,将实例变量添加到内部函数,或(如您不想这样做)创建局部变量...

# modifying  --> call like x = make_incrementer([100])
def make_incrementer(start):
    def closure():
        # I know I could write 'x = start' and use x - that's not my point though (:
        while True:
            yield start[0]
            start[0] += 1
    return closure

# adding instance variables  --> call like x = make_incrementer(100)
def make_incrementer(start):
    def closure():
        while True:
            yield closure.start
            closure.start += 1
    closure.start = start
    return closure

# creating local variable  --> call like x = make_incrementer(100)
def make_incrementer(start):
    def closure(start=start):
        while True:
            yield start
            start += 1
    return closure

【讨论】:

  • @agf:谢谢,修正了我的答案。此外,当我写它时,我的答案比其他人有更好的解释(他们已经编辑了他们的答案;)
猜你喜欢
  • 1970-01-01
  • 2019-08-02
  • 1970-01-01
  • 1970-01-01
  • 2016-11-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多