因为最早用的是 Java 和 C#,写 Python 的时候自然也把 Python 作用域的想的和原有的一致。
Python 的作用域变量遵循在大部分情况下是一致的,但也有例外的情况。
本文着通过遇到的一个作用域的小问题来说说 Python 的作用域
但也有部分例外的情况,比如:
作用域第一版代码如下
- a = 1
- print(a, id(a)) # 打印 1 4465620064
- def func1():
- print(a, id(a))
- func1() # 打印 1 4465620064
作用域第一版对应字节码如下
- 4 0 LOAD_GLOBAL 0 (print)
- 3 LOAD_GLOBAL 1 (a)
- 6 LOAD_GLOBAL 2 (id)
- 9 LOAD_GLOBAL 1 (a)
- 12 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
- 15 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
- 18 POP_TOP
- 19 LOAD_CONST 0 (None)
- 22 RETURN_VALUE
PS: 行 4 表示 代码行数 0 / 3 / 9 ... 不知道是啥,我就先管他叫做条吧 是 load global
PPS: 注意条 3/6 LOAD_GLOBAL 为从全局变量中加载
顺手附上本文需要着重理解的几个指令
- LOAD_GLOBA : Loads the global named co_names[namei] onto the stack.
- LOAD_FAST(var_num) : Pushes a reference to the local co_varnames[var_num] onto the stack.
- STORE_FAST(var_num) : Stores TOS into the local co_varnames[var_num].
这点似乎挺符合我们认知的,那么,再深一点呢?既然这个变量是可以 Load 进来的就可以修改咯?
然而并不是,我们看作用域第二版对应代码如下
- a = 1
- print(a, id(a)) # 打印 1 4465620064
- def func2():
- a = 2
- print(a, id(a))
- func2() # 打印 2 4465620096
一看,WTF, 两个 a 内存值不一样。证明这两个变量是完全两个变量。
作用域第二版对应字节码如下
- 4 0 LOAD_CONST 1 (2)
- 3 STORE_FAST 0 (a)
- 5 6 LOAD_GLOBAL 0 (print)
- 9 LOAD_FAST 0 (a)
- 12 LOAD_GLOBAL 1 (id)
- 15 LOAD_FAST 0 (a)
- 18 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
- 21 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
- 24 POP_TOP
- 25 LOAD_CONST 0 (None)
- 28 RETURN_VALUE
注意行 4 条 3 (STORE_FAST) 以及行 5 条 9/15 (LOAD_FAST)
这说明了这里的 a 并不是 LOAD_GLOBAL 而来,而是从该函数的作用域 LOAD_FAST 而来。
那我们在函数体重修改一下 a 值看看。
- a = 1
- def func3():
- print(a, id(a)) # 注释掉此行不影响结论
- a += 1
- print(a, id(a))
- func3() # 当调用到这里的时候 local variable 'a' referenced before assignment
- # 即 a += 1 => a = a + 1 这里的第二个 a 报错鸟
- 3 0 LOAD_GLOBAL 0 (print)
- 3 LOAD_FAST 0 (a)
- 6 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
- 9 POP_TOP
- 4 10 LOAD_FAST 0 (a)
- 13 LOAD_CONST 1 (1)
- 16 BINARY_ADD
- 17 STORE_FAST 0 (a)
- 5 20 LOAD_GLOBAL 0 (print)
- 23 LOAD_FAST 0 (a)
- 26 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
- 29 POP_TOP
- 30 LOAD_CONST 0 (None)
- 33 RETURN_VALUE
那么,func3 也就自然而言由于没有无法 LOAD_FAST 对应的 a 变量,则报了引用错误。
然后问题来了,a 为基本类型的时候是这样的。如果引用类型呢?我们直接仿照 func3 的实例把 a 改成 list 类型。如下
- a = [1]
- def func4():
- print(a, id(a)) # 这条注不注释掉都一样
- a += 1 # 这里我故意写错 按理来说应该是 a.append(1)
- print(a, id(a))
- func4()
- # 当调用到这里的时候 local variable 'a' referenced before assignment
╮(╯▽╰)╭ 看来事情那么简单,结果变量 a 依旧是无法修改。
可按理来说跟应该报下面的错误呀
- 'int'object is not iterable
- a = [1]
- def func5():
- print(a, id(a))
- a.append(1)
- print(a, id(a))
- func5()
- # [1] 4500243208
- # [1, 1] 4500243208
这下可以修改了。看一下字节码。
- 3 0 LOAD_GLOBAL 0 (print)
- 3 LOAD_GLOBAL 1 (a)
- 6 LOAD_GLOBAL 2 (id)
- 9 LOAD_GLOBAL 1 (a)
- 12 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
- 15 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
- 18 POP_TOP
- 4 19 LOAD_GLOBAL 1 (a)
- 22 LOAD_ATTR 3 (append)
- 25 LOAD_CONST 1 (1)
- 28 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
- 31 POP_TOP
- 5 32 LOAD_GLOBAL 0 (print)
- 35 LOAD_GLOBAL 1 (a)
- 38 LOAD_GLOBAL 2 (id)
- 41 LOAD_GLOBAL 1 (a)
- 44 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
- 47 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
- 50 POP_TOP
- 51 LOAD_CONST 0 (None)
- 54 RETURN_VALUE
从全局拿来 a 变量,执行 append 方法。
看来这是解释器遵循了某种变量查找的法则,似乎就只能从原理上而不是在 CPython 的实现上解释这个问题了。
查找了一些资料,发现 Python 解释器在依据 基于 LEGB 准则 (顺手吐槽一下不是 LGBT)
LEGB 指的变量查找遵循
StackOverFlow 上 martineau 提供了一个不错的例子用来说明
- x = 100
- print("1. Global x:", x)
- class Test(object):
- y = x
- print("2. Enclosed y:", y)
- x = x + 1
- print("3. Enclosed x:", x)
- def method(self):
- print("4. Enclosed self.x", self.x)
- print("5. Global x", x)
- try:
- print(y)
- except NameError as e:
- print("6.", e)
- def method_local_ref(self):
- try:
- print(x)
- except UnboundLocalError as e:
- print("7.", e)
- x = 200 # causing 7 because has same name
- print("8. Local x", x)
- inst = Test()
- inst.method()
- inst.method_local_ref()
我们试着用变量查找准则去解释 第一个例子 的时候,是解释的通的。
第二个例子,发现函数体内的 a 变量已经不是那个 a 变量了。要是按照这个查找原则的话,似乎有点说不通了。
但当解释第三个例子的时候,就完全说不通了。
- a = 1
- def func3():
- print(a, id(a)) # 注释掉此行不影响结论
- a += 1
- print(a, id(a))
- func3() # 当调用到这里的时候 local variable 'a' referenced before assignment
- # 即 a += 1 => a = a + 1 这里的第二个 a 报错鸟
按照我的猜想,这里的代码执行可能有两种情况:
但如果真的和我的想法接近的话,这两种情况都可以执行,除了变量作用域之外还是有一些其他的考量。我把这个叫做本地赋值准则 (拍脑袋起的名称)
一般我们管这种考量叫做 Python 作者就是觉得这种编码方式好你爱写不写 Python 作者对于变量作用域的权衡。
事实上,当解释器编译函数体为字节码的时候,如果是一个赋值操作 (list.append 之流不是赋值操作),则会被限定这个变量认为是一个 local 变量。如果在 local 中找不到,并不向上查找,就报引用错误。
- 这不是 BUG
- 这不是 BUG
- 这不是 BUG
这是一种设计权衡 Python 认为 虽然不强求强制声明类型,但假定被赋值的变量是一个 Local 变量。这样减少避免动态语言比如 JavaScript 动不动就修改掉了全局变量的坑。
这也就解释了第四个例子中赋值操作报错,以及第五个例子 append 为什么可以正常执行。
如果我偏要勉强呢? 可以通过 global 和 nonlocal 来 引入模块级变量 or 上一级变量。
PS: JS 也开始使用 let 进行声明,小箭头函数内部赋值查找变量也是向上查找。
ChangeLog:
来源: https://juejin.im/entry/5a17e698f265da430b7aecf5