为什么这个闭包不能修改封闭作用域中的变量?

21

这段 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风格。 - agf
这确实是阅读/编写Python闭包的精确副本。 - agf
4个回答

37
无论何时你在函数内部给一个变量赋值,它都将成为该函数的局部变量。代码行 start += 1start 赋了一个新值,因此 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 遍历了作用域,找到了我实际想要的那个变量,这样理解对吗? - jwd

11

在Python 2.x中,有两种更好/更符合Python规范的方法来实现此操作,而不是只是使用一个容器来解决缺少非本地关键字的问题。

你在代码的评论中提到了其中一种——绑定到本地变量。还有另一种方法:

使用默认参数

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,并利用了在此情况下,您已经有一个您知道名称并可以引用属性的对象--没有必要为此创建新容器。


1
除了顶部的“重新绑定到本地变量”之外,您的解释非常好--实际上,您只是进行绑定。 - Ethan Furman
@EthanFurman 你说得对,我真正的意思是“也绑定”,而不是“重新绑定”。 - agf

4

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()

1
闭包变量并非只读,否则您将无法修改它们。引用也是不正确的——实例__dict__并未关闭。 - Ethan Furman
@agf,在3.x.x版本中,并且需要声明一个语句。 - Joe
你误解了。start[0] += 1 修改的是闭包变量,只是没有重新绑定名称,这是 Python 对赋值和本地变量的假设所排除的操作。如果它真的是只读的,你(或 F.J)就无法这样做。 - agf
@agf,你不能向该数组添加或删除元素。你只能修改它的内容。 - Joe
1
@agf,我错了。虽然我不太高兴,但你们是正确的。 - Joe
显示剩余6条评论

3
在Python 3.x中,您可以使用“ nonlocal”关键字来重新绑定非本地作用域中的名称。在2.x中,您唯一的选择是修改闭包变量,向内部函数添加实例变量或(您不想这样做时)创建局部变量...
# 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:谢谢,我修正了我的答案。此外,当我写下这个答案时,我的解释比其他人更好(他们后来编辑了他们的答案 ;))。 - Ethan Furman

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