为什么这个闭包不能修改外部作用域的变量?
这段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 个回答
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()
每当你在一个函数里面给一个变量赋值时,这个变量就是这个函数的局部变量。比如说,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
在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中都有效,并且利用了这样一个事实:在这种情况下,你已经有一个你知道名字的对象,可以引用它的属性——不需要为了这个目的再创建一个新的容器。