全局和闭包变量
修改全局变量的问题
这篇我们讲一个跟全局变量有关的内容。我们看这段程序。我们有一个全局变量 count,然后定义了一个函数 f,f 里面就是 print count,然后运行 f。大家都很容易想出来这段程序的结果,就打印出来一个 0。
count = 0
def f():
print(count)
f()
好,现在问题来了。如果我们在这个函数里面做 count 等于 count 加 1,再运行程序我们就会发现它报错了。它说这个 count 还没被赋值就使用了。这时候你肯定很懵,对吧?我定义了 count 呀。
count = 0
def f():
count = count + 1
f()
print(count)
然后你说,行,那我看一下,在 f 里面 count 到底是什么?于是你在进入 f 这个函数之后,跟刚才一样,立刻打印了 count。你发现竟然还是不好使。刚才还能成功打印的 count,现在也找不着了。
count = 0
def f():
print(count)
count = count + 1
f()
print(count)
用 global 声明全局变量
这个时候,如果你去搜索一下,或者你问一下别人,可能就会有人告诉你说你使用 count 的时候,要先定义它是一个 global variable。然后你就学着网上的样子,在函数最前面加了一个 global count。这个时候再运行程序,你就发现它竟然按照你想象的方式运行了。但是内心深处还是有疑问的,对吧?为什么我刚才 print count print 好好的,现在就要定义这个 global count 呢?
count = 0
def f():
global count
print(count)
count = count + 1
f()
print(count)
我们就先来看一下刚才那几个程序的字节码。首先,最开始的这个程序,我们可以看到,在函数 f 里,当它尝试读取 count 的时候,它使用的是 LOAD_GLOBAL读取全局变量:先查模块的全局字典,找不到再查内置名字(print、len 这些)。,也就是读取全局变量。
import dis
s = """count = 0
def f():
print(count)
f()
"""
dis.dis(s)
然而,当我们在函数里加入了 count 等于 count 加 1 这行代码之后,读取 count 的指令就不再是 LOAD_GLOBAL 了:第四行 print 里的 count 变成了 LOAD_FAST_CHECK读取局部变量,但先检查这个槽位有没有被赋过值;没有就抛 UnboundLocalError。3.12 起用在“读取出现在赋值之前”的局部变量上,正是我们这个例子。,第五行 count 加 1 里的 count 是 LOAD_FAST读取局部变量:按下标直接从函数的局部变量数组里取,不查任何字典,所以叫 fast。只有编译期确定是局部变量的名字才会用它。。两条都是在读局部变量,只是前者多了一步“有没有赋过值”的检查,而这时候 count 还没赋过值,于是报 UnboundLocalError。那我们之前定义的 count 是一个全局变量,在读取局部变量的时候,自然就找不到它了。在我们写了 global count 这行程序之后,我们再读取 count 的时候,它就又变成了 LOAD_GLOBAL读取全局变量:先查模块的全局字典,找不到再查内置名字(print、len 这些)。 了,这样,它自然也就正常工作了。
import dis
s = """count = 0
def f():
global count
print(count)
count = count + 1
f()
"""
dis.dis(s)
import dis
s = """count = 0
def f():
print(count)
count = count + 1
f()
"""
dis.dis(s)
赋值如何决定作用域
那为什么 Python 在这种情况下会使用 LOAD_FAST,而不是 LOAD_GLOBAL 来读取 count 的值呢?因为它在编译的时候发现了一个对 count 的 assignment。当你在一个函数里尝试对一个变量赋值的时候,Python 就会默认这个变量是一个局部变量,无论你赋值的位置是在哪。也就是说,即使我们是在第四行先 print the count,然后再在第五行对 count 进行了赋值,Python 依然会在编译的时候认为 count 这个东西是一个局部变量。于是在打印的时候,它也把它当成局部变量,在局部变量的范围里面去寻找这个 count。
所以说,当你的函数里存在对全局变量的赋值的时候,你就必须要显式地声明这个变量是一个全局变量。反之,当你这个函数里只使用了这个变量,而没有对这个变量赋值的时候,Python 就会机智地在全局变量里去寻找这个变量。
用 nonlocal 修改闭包变量
事实上,不光是全局变量如此,Python 的闭包也有这样的属性。我们看,我们在函数 g 里面定义了 count 等于 0,然后在 g 函数内部定义的 f 里面对 count 进行赋值,这样的代码依然是会报错的。
def g():
count = 0
def f():
count = count + 1
f()
g()
或者我们可以换一种形式,在 g 函数里面定义的 count 等于 0,然后在 f 函数里面尝试把 count 变成 1,你会发现,在运行完 f 函数之后,g 里面的 count 依然是 0。这是因为在 f 函数里面,你做 count 等于 1 的时候,它改变的是 f 内部的局部变量,而不是通过闭包机制改变 g 里面的这个局部变量。
def g():
count = 0
def f():
count = 1
f()
print(count)
g()
那在这种情况下,如果我们把 count 声明成一个全局变量,你会发现它依然是报错的。因为我们需要的 count,它不是一个全局变量,它是一个在闭包外层的变量。所以正确的声明方式是用 nonlocal 来声明这个变量不是一个局部变量。
def g():
count = 0
def f():
global count
count = count + 1
f()
print(count)
g()
def g():
count = 0
def f():
nonlocal count
count = count + 1
f()
print(count)
g()
修改对象算不算赋值
那 Python 的这个特性呢,说实话,有一点点反人类,不是非常的直观。当然,它这么设计,肯定也有它的想法,对吧?那我听过一个对于这种说法的总结叫做“全局变量可读不可写”。这个说法呢,非常好记,但是有一个让人容易混淆的地方。
我们看这段程序。我们定义了一个全局变量 dictionary d,然后在函数 f 里面对 d 进行一个改变,这段代码是完全可以正常工作的。
d = {}
def f():
d["a"] = 1
f()
print(d)
那你认为在 f 函数里面,这个操作算是对 d 的写入吗?这件事呢,就多少有点含糊。因为我们确实是改变了 d。但是我们改变 d 的方式本质上,从字节码来看,是首先读取了全局变量 d,然后再用 subscript 下标赋值去改变了全局变量 d 里的内容。所以,从不同的角度看,你可以认为它是写,也可以认为它不是写。
因此,我觉得一个更不容易混淆的方法还是要去看这个变量在这个 scope 作用域之内有没有被赋值,也就是它有没有出现在等号的前面。只要它被赋值了,它就会被默认成是一个局部变量。如果它没有被赋值,它又不是一个输入的 argument,Python 就会认为它是一个全局变量。同时,一定要注意,这跟这个赋值语句出现在哪里,出现的顺序是没有关系的。即便你对这个变量的读取,是出现在赋值之前的,Python 也会认为它是一个局部变量。
到这里可能你会有个疑问,刚刚不是说赋值会被认为是一个局部变量吗?那上面那段代码应该报错才对,这是因为变量 d 引用了一个字典对象,d["a"]是修改 d 指向的对象,不是给变量 d 赋值,所以 Python 不会把 d 判定成局部变量。