楔子

在介绍 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♪(・ω・)ノ。