0x00 前言
因为最早用的是 Java 和 C#,写 Python 的时候自然也把 Python 作用域的想的和原有的一致。
Python 的作用域变量遵循在大部分情况下是一致的,但也有例外的情况。
本文着通过遇到的一个作用域的小问题来说说 Python 的作用域
0x01 作用域的几个实例
但也有部分例外的情况,比如:
1.1 第一个例子
作用域第一版代码如下
1 | 复制代码a = 1 |
作用域第一版对应字节码如下
1 | 复制代码 4 0 LOAD_GLOBAL 0 (print) |
PS: 行 4 表示 代码行数 0 / 3 / 9 … 不知道是啥,我就先管他叫做条吧 是 load global
PPS: 注意条 3/6 LOAD_GLOBAL 为从全局变量中加载
顺手附上本文需要着重理解的几个指令
1 | 复制代码LOAD_GLOBA : Loads the global named co_names[namei] onto the stack. |
这点似乎挺符合我们认知的,那么,再深一点呢?既然这个变量是可以 Load 进来的就可以修改咯?
1.2 第二个例子
然而并不是,我们看作用域第二版对应代码如下
1 | 复制代码a = 1 |
一看,WTF, 两个 a 内存值不一样。证明这两个变量是完全两个变量。
作用域第二版对应字节码如下
1 | 复制代码 4 0 LOAD_CONST 1 (2) |
注意行 4 条 3 (STORE_FAST) 以及行 5 条 9/15 (LOAD_FAST)
这说明了这里的 a 并不是 LOAD_GLOBAL 而来,而是从该函数的作用域 LOAD_FAST 而来。
1.3 第三个例子
那我们在函数体重修改一下 a 值看看。
1 | 复制代码a = 1 |
1 | 复制代码 3 0 LOAD_GLOBAL 0 (print) |
那么,func3 也就自然而言由于没有无法 LOAD_FAST 对应的 a 变量,则报了引用错误。
然后问题来了,a 为基本类型的时候是这样的。如果引用类型呢?我们直接仿照 func3 的实例把 a 改成 list 类型。如下
1.4 第四个例子
1 | 复制代码a = [1] |
╮(╯▽╰)╭ 看来事情那么简单,结果变量 a 依旧是无法修改。
可按理来说跟应该报下面的错误呀
1 | 复制代码'int' object is not iterable |
1.5 第五个例子
1 | 复制代码a = [1] |
这下可以修改了。看一下字节码。
1 | 复制代码 3 0 LOAD_GLOBAL 0 (print) |
从全局拿来 a 变量,执行 append 方法。
0x02 作用域准则以及本地赋值准则
2.1 作用域准则
看来这是解释器遵循了某种变量查找的法则,似乎就只能从原理上而不是在 CPython 的实现上解释这个问题了。
查找了一些资料,发现 Python 解释器在依据 基于 LEGB 准则 (顺手吐槽一下不是 LGBT)
LEGB 指的变量查找遵循
- Local
- Enclosing-function locals
- Global
- Built-In
StackOverFlow 上 martineau 提供了一个不错的例子用来说明
1 | 复制代码x = 100 |
我们试着用变量查找准则去解释 第一个例子 的时候,是解释的通的。
第二个例子,发现函数体内的 a 变量已经不是那个 a 变量了。要是按照这个查找原则的话,似乎有点说不通了。
但当解释第三个例子的时候,就完全说不通了。
1 | 复制代码a = 1 |
按照我的猜想,这里的代码执行可能有两种情况:
- 当代码执行到第三行的时候可能是向从 local 找 a, 发现没有,再找 Enclosing-function 发现没有,最后应该在 Global 里面找到才是。注释掉第三行的时候也是同理。
- 当代码执行到第三行的时候可能是向下从 local 找 a, 发现有,然后代码执行,结束。
但如果真的和我的想法接近的话,这两种情况都可以执行,除了变量作用域之外还是有一些其他的考量。我把这个叫做本地赋值准则 (拍脑袋起的名称)
一般我们管这种考量叫做 Python 作者就是觉得这种编码方式好你爱写不写 Python 作者对于变量作用域的权衡。
事实上,当解释器编译函数体为字节码的时候,如果是一个赋值操作 (list.append 之流不是赋值操作),则会被限定这个变量认为是一个 local 变量。如果在 local 中找不到,并不向上查找,就报引用错误。
1 | 复制代码这不是 BUG |
这是一种设计权衡 Python 认为 虽然不强求强制声明类型,但假定被赋值的变量是一个 Local 变量。这样减少避免动态语言比如 JavaScript 动不动就修改掉了全局变量的坑。
这也就解释了第四个例子中赋值操作报错,以及第五个例子 append 为什么可以正常执行。
如果我偏要勉强呢? 可以通过 global 和 nonlocal 来 引入模块级变量 or 上一级变量。
PS: JS 也开始使用 let 进行声明,小箭头函数内部赋值查找变量也是向上查找。
0xEE 参考链接
ChangeLog:
- 2017-11-20 从原有笔记中抽取本文整理而成
本文转载自: 掘金