楔子
在介绍 if 语句的时候,我们看到了最基本的控制流,其核心就是跳转。但是 if 只能向前跳转,而接下来介绍的 for、while 循环,指令是可以回退的,也就是向后跳转。
for 控制流
我们看一个简单的 for 循环的字节码。
import dis
code_string = """
lst = [1, 2]
for item in lst:
print(item)
"""
dis.dis(compile(code_string, "<file>", "exec"))
反编译之后,字节码指令如下。
// 加载常量 1,压入运行时栈
0 LOAD_CONST 0 (1)
// 加载常量 2,压入运行时栈
2 LOAD_CONST 1 (2)
// 将运行时栈的元素弹出,构建长度为 2 的列表,并压入栈中
4 BUILD_LIST 2
// 将上一步构建的列表从栈顶弹出,并用符号 lst 与之绑定
// 到此 lst = [1, 2] 便完成了
6 STORE_NAME 0 (lst)
// 从全局名字空间中加载 lst
8 LOAD_NAME 0 (lst)
// 获取对应的迭代器,即 iter(lst)
10 GET_ITER
// 开始 for 循环,将里面的元素依次迭代出来
// 如果迭代结束,向前跳转 12 个偏移量,来到偏移量为 26 的指令
>> 12 FOR_ITER 12 (to 26)
// 到这里说明上一步迭代出元素了
// 用符号 item 和迭代出的元素进行绑定
14 STORE_NAME 1 (item)
// 对应 print(item)
16 LOAD_NAME 2 (print)
18 LOAD_NAME 1 (item)
20 CALL_FUNCTION 1
22 POP_TOP
// 到此,一次遍历就完成了,那么跳转到偏移量为 12 的指令,进行下一轮循环
// 注意:上面的 FOR_ITER 指令和这里的 JUMP_ABSOLUTE 指令的参数都是 12
// 但它们有着不同,FOR_ITER 指令的参数 12 表示从当前位置向前跳转 12 个偏移量
// 而 JUMP_ABSOLUTE 指令的参数 12 表示跳转到偏移量为 12 个位置(或者说从开头跳转 12 个偏移量)
24 JUMP_ABSOLUTE 12
>> 26 LOAD_CONST 2 (None)
28 RETURN_VALUE
我们直接从 10 GET_ITER 开始看起,首先 for 循环遍历的对象必须是可迭代对象,然后会调用它的 __iter__ 方法,得到迭代器。再不断地调用迭代器的 __next__ 方法,一步一步将里面的值全部迭代出来,当出现 StopIteration 异常时,for 循环捕捉,最后退出。
另外,我们说 Python 里面是先有值,后有变量,for 循环也不例外。循环的时候,先将迭代器中的元素迭代出来,然后再让变量 item 指向。因此包含 10 个元素的迭代器,需要迭代 11 次才能结束。因为 for 循环事先是不知道迭代 10 次就能结束的,它需要再迭代一次,发现没有元素可以迭代、并捕获抛出的 StopIteration 之后,才能结束。
for 循环遍历可迭代对象时,会先拿到对应的迭代器,那如果遍历的就是一个迭代器呢?答案是依旧调用 __iter__,只不过由于本身就是一个迭代器,所以返回的还是其本身。
将元素迭代出来之后,就开始执行 for 循环体的逻辑了。
执行完一轮循环之后,通过 JUMP_ABSOLUTE 跳转到字节码偏移量为 12、也就是 FOR_ITER 的位置开始下一次循环。这里我们发现它没有跳到 GET_ITER 那里,所以可以得出结论,for 循环在遍历的时候只会创建一次迭代器。
下面来看指令对应的具体逻辑:
case TARGET(GET_ITER): {
// 获取栈顶元素,即上一步压入的列表指针
PyObject *iterable = TOP();
// 调用 PyObject_GetIter,获取对应的迭代器
// 这个函数在介绍迭代器的时候已经说过了
// 等价于 iter = type(iterable).__iter__(iterable)
PyObject *iter = PyObject_GetIter(iterable);
Py_DECREF(iterable);
// 将迭代器 iter 设置为栈顶元素
SET_TOP(iter);
if (iter == NULL)
goto error;
// 指令预测,解释器认为下一条指令大概率是 FOR_ITER 或 CALL_FUNCTION
PREDICT(FOR_ITER);
PREDICT(CALL_FUNCTION);
DISPATCH();
}
当创建完迭代器之后,就正式进入 for 循环了。所以从 FOR_ITER 开始,进入了虚拟机层面上的 for 循环。
源代码中的 for 循环,在虚拟机层面也一定对应着一个相应的循环控制结构。因为无论进行怎样的变换,都不可能在虚拟机层面利用顺序结构来实现源码层面上的循环结构,这也可以看作是程序的拓扑不变性。
因此源代码是宏观的,虚拟机执行字节码是微观的,尽管两者的层级不同,但本质上等价的,是程序从一种形式到另一种形式的等价转换。
我们来看一下 FOR_ITER 指令对应的具体实现:
case TARGET(FOR_ITER): {
PREDICTED(FOR_ITER);
// 从栈顶获取迭代器对象(指针)
PyObject *iter = TOP();
// 调用迭代器类型对象的 tp_iternext,将迭代器内的元素迭代出来
PyObject *next = (*iter->ob_type->tp_iternext)(iter);
// 如果 next != NULL,说明迭代到元素了,那么压入运行时栈
if (next != NULL) {
PUSH(next);
PREDICT(STORE_FAST);
PREDICT(UNPACK_SEQUENCE);
DISPATCH();
}
// 否则说明迭代出现异常
if (_PyErr_Occurred(tstate)) {
// 如果异常还不是 StopIteration,那么跳转到 error 标签
if (!_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) {
goto error;
}
else if (tstate->c_tracefunc != NULL) {
call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj, tstate, f);
}
// 否则说明是 StopIteration,那么证明迭代完毕,将异常清空
_PyErr_Clear(tstate);
}
// 迭代结束了,但运行时栈里面还有一个迭代器对象
// 那么要将它弹出,因此这里执行了 STACK_SHRINK(1)
STACK_SHRINK(1);
Py_DECREF(iter);
// 跳转到 for 循环结束后的下一条指令
// 当前的指令为:12 FOR_ITER 12 (to 26)
// 所以会通过 JUMPBY 实现一个相对跳转
// 从当前位置向前跳转 12 个偏移量,来到偏移量为 26 的指令
JUMPBY(oparg);
PREDICT(POP_BLOCK);
DISPATCH();
}
在执行 FOR_ITER 的时候,如果迭代器没有耗尽,那么会迭代出元素,压入运行时栈,然后调用 DISPATCH() 去执行下一条指令。当一轮循环结束后,还要进行指令回退,从字节码中也看到了,for 循环遍历一次之后,会再次跳转到 FOR_ITER,而跳转所使用的指令就是 JUMP_ABSOLUTE,从名字也能看出这个指令会使用绝对跳转。
case TARGET(JUMP_ABSOLUTE): {
PREDICTED(JUMP_ABSOLUTE);
// 跳转到偏移量为 oparg 的指令
JUMPTO(oparg);
#if FAST_LOOPS
FAST_DISPATCH();
#else
DISPATCH();
#endif
}
之前介绍过 JUMPTO 和 JUMPBY 两个宏,
#define JUMPTO(x) (next_instr = first_instr + (x) / sizeof(_Py_CODEUNIT))
#define JUMPBY(x) (next_instr += (x) / sizeof(_Py_CODEUNIT))
这两个宏都表示跳转 x 个偏移量,但 JUMPTO 是从头开始跳转,所以只要 x 固定,那么跳转位置就始终是固定的。而 JUMPBY 表示从当前位置开始跳转,所以位置不同,跳转的结果也不同。
然后天下没有不散的宴席,随着迭代的进行,for 循环总有退出的那一刻,而这个退出的动作只能落在 FOR_ITER 的身上。在 FOR_ITER 指令执行的过程中,如果遇到了 StopIteration,就意味着迭代结束了。这个结果将导致虚拟机会将迭代器从运行时栈中弹出,同时执行一个 JUMPBY 动作,向前跳跃,在字节码的层面是向下,也就是偏移量增大的方向。
while 控制流
看完了 for,再来看看 while,并且我们还要分析两个关键字:break、continue。
import dis
code_string = """
a = 0
while a < 10:
a += 1
if a == 5:
continue
if a == 7:
break
print(a)
"""
dis.dis(compile(code_string, "<file>", "exec"))
看一下它的指令:
// a = 0
0 LOAD_CONST 0 (0)
2 STORE_NAME 0 (a)
// 比较 a < 10
>> 4 LOAD_NAME 0 (a)
6 LOAD_CONST 1 (10)
8 COMPARE_OP 0 (<)
// 如果 a < 10 为假,说明循环结束
// 跳转到偏移量为 50 的指令,内部会使用绝对跳转
10 POP_JUMP_IF_FALSE 50
// 到这里说明 while 条件成立,进入循环体
// 执行 a += 1
12 LOAD_NAME 0 (a)
14 LOAD_CONST 2 (1)
16 INPLACE_ADD
18 STORE_NAME 0 (a)
// 比较 a == 5
20 LOAD_NAME 0 (a)
22 LOAD_CONST 3 (5)
24 COMPARE_OP 2 (==)
// 如果 a == 5 为假,跳转到偏移量为 30 的指令
26 POP_JUMP_IF_FALSE 30
// 否则说明 a == 5 为真,执行 continue
// 由于 continue 是立即进入下一轮循环
// 所以直接跳转到偏移量为 4 的指令,即 while 循环的开始位置
// 所以在虚拟机的层面,continue 就是一个跳转指令
28 JUMP_ABSOLUTE 4
// 比较 a == 7
>> 30 LOAD_NAME 0 (a)
32 LOAD_CONST 4 (7)
34 COMPARE_OP 2 (==)
// 如果 a == 7 为假,跳转到偏移量为 40 的指令
36 POP_JUMP_IF_FALSE 40
// 否则说明 a == 7 为真,执行 break
// 因此直接跳转到偏移量为 50 的位置,即 while 循环结束后的下一条指令
38 JUMP_ABSOLUTE 50
// print(a)
>> 40 LOAD_NAME 1 (print)
42 LOAD_NAME 0 (a)
44 CALL_FUNCTION 1
46 POP_TOP
// 到这里说明一轮循环结束了,那么跳转到偏移量为 4 的位置,即 while 循环的开始位置
48 JUMP_ABSOLUTE 4
// 隐式的 return None
>> 50 LOAD_CONST 5 (None)
52 RETURN_VALUE
有了 for 循环,再看 while 循环就简单多了,整体逻辑和 for 高度相似,当然里面还结合了 if。
刚才说了,尽管源代码和字节码的层级不同,但本质上是等价的,是程序从一种形式到另一种形式的等价转换。在源码中能看到的,在字节码当中也能看到。比如源代码中的 continue 会跳转到循环所在位置,那么在字节码中自然也会对应一个跳转指令。
小结
以上我们就探讨了 Python 的两种循环,总的来说没什么难度,本质上还是跳转。只不过有时会通过 JUMPTO 进行绝对跳转,有时会通过 JUMPBY 进行相对跳转。
欢迎大家关注我的公众号:古明地觉的编程教室。
如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。