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

21 投票
4 回答
10227 浏览
提问于 2025-04-17 02:58

这段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变量这样的处理呢?

更新:我发现这个StackOverflow的帖子很相关(答案比问题更重要): 读取/写入Python闭包

4 个回答

4

示例

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

每当你在一个函数里面给一个变量赋值时,这个变量就是这个函数的局部变量。比如说,start += 1 这行代码是给 start 赋了一个新值,所以 start 是局部变量。因为局部变量 start 存在,当你第一次尝试访问它时,函数不会去全局范围内找 start,这就是你看到的错误原因。

在 Python 3.x 版本中,如果你使用 nonlocal 这个关键词,你的代码示例就能正常工作:

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

而在 Python 2.x 版本中,你可以通过使用 global 这个关键词来解决类似的问题,但在这里不行,因为 start 不是一个全局变量。

在这种情况下,你可以做你建议的那样(x = start),或者使用一个可变的变量来修改并返回一个内部值。

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

在Python 2.x中,有两种“更好”或更符合Python风格的方法来解决这个问题,而不是仅仅使用一个容器来绕过没有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中都有效,并且利用了这样一个事实:在这种情况下,你已经有一个你知道名字的对象,可以引用它的属性——不需要为了这个目的再创建一个新的容器。

撰写回答