Python虚拟机之while循环控制结构(三)

时间:2022-09-09 12:59:01

Python虚拟机中的while循环控制结构

Python虚拟机之if控制流(一)Python虚拟机之for循环控制流(二)两个章节中,我们介绍了if和for两个控制结构在Python虚拟机中的实现,但是这里还差一个while循环控制结构。在这里,我们不单单要考虑循环本身的指令跳跃动作,还要考虑另外两个与循环相关的指令跳跃语义:continue和break

下面,让我们看一下demo4.py这个程序,并用dis模块解释其对应的字节码

# cat demo4.py 
i = 0
while i < 10:
    i += 1
    if i > 5:
        continue
    if i == 20:
        break
    print(i)
# python2.5
……
>>> source = open("demo4.py").read()
>>> co = compile(source, "demo4.py", "exec")
>>> import dis
>>> dis.dis(co)
  1           0 LOAD_CONST               0 (0)
              3 STORE_NAME               0 (i)

  2           6 SETUP_LOOP              71 (to 80)
        >>    9 LOAD_NAME                0 (i)
             12 LOAD_CONST               1 (10)
             15 COMPARE_OP               0 (<)
             18 JUMP_IF_FALSE           57 (to 78)
             21 POP_TOP             

  3          22 LOAD_NAME                0 (i)
             25 LOAD_CONST               2 (1)
             28 INPLACE_ADD         
             29 STORE_NAME               0 (i)

  4          32 LOAD_NAME                0 (i)
             35 LOAD_CONST               3 (5)
             38 COMPARE_OP               4 (>)
             41 JUMP_IF_FALSE            7 (to 51)
             44 POP_TOP             

  5          45 JUMP_ABSOLUTE            9
             48 JUMP_FORWARD             1 (to 52)
        >>   51 POP_TOP             

  6     >>   52 LOAD_NAME                0 (i)
             55 LOAD_CONST               4 (20)
             58 COMPARE_OP               2 (==)
             61 JUMP_IF_FALSE            5 (to 69)
             64 POP_TOP             

  7          65 BREAK_LOOP          
             66 JUMP_FORWARD             1 (to 70)
        >>   69 POP_TOP             

  8     >>   70 LOAD_NAME                0 (i)
             73 PRINT_ITEM          
             74 PRINT_NEWLINE       
             75 JUMP_ABSOLUTE            9
        >>   78 POP_TOP             
             79 POP_BLOCK           
        >>   80 LOAD_CONST               5 (None)
             83 RETURN_VALUE   

  

在执行"15   COMPARE_OP   0"之前,我们先来看一下运行时栈和名字空间的情况,字节码指令分别在执行"3   STORE_NAME   0"时在名字空间上将符号i和PyIntObject对象0进行映射,在执行"9   LOAD_NAME   0"和"12   LOAD_CONST   1   (10)"分别0和10压入运行时栈:

Python虚拟机之while循环控制结构(三)

图1-2执行"15   COMPARE_OP   0"之前运行时栈和名字空间的情况

 接着,"15   COMPARE_OP   0"指令将执行<比较操作,并将比较结果压入运行时栈中。这里,我们先来一个思维的跳跃,直接考虑循环结束时的情况,当某个时刻i的值开始大于或等于10,"15   COMPARE_OP   0"指令运行后的运行时栈和local名字空间如图1-2

Python虚拟机之while循环控制结构(三)

图1-2循环结束时虚拟机的状态

 

虚拟机在执行紧接着的"18   JUMP_IF_FALSE   57"时,会发生指令跳跃的动作,跳跃的距离是57到偏移78的位置,根据dis模块的分析结果,这个距离正好是倒数第四条"78   POP_TOP"处,显然这条指令将Py_False弹出运行时栈,随后的虚拟机通过执行POP_BLOCK指令销毁PyTryBlock对象

循环的正常运转

另一方面,如果"15   COMPARE_OP   0"指令处的判断结果为Py_True,那么虚拟机将进入while循环。这里我们不考虑continue和break的情况(实际上break永远不会发生),只考虑循环正常运转时的情况。在循环正常运转时,i的值小于10,且不等于5,那么Python虚拟机接下来的动作及状态转换如图1-3所示:

Python虚拟机之while循环控制结构(三)

图1-3循环运转过程中Python虚拟机的状态转换

将i的值递增之后,Python虚拟机通过PRINT_ITEM、PRINT_NEWLINE指令将i值输出到标准输出,然后通过执行指令"75   JUMP_ABSOLUTE   9"进行指令回退,回退的位置是距离字节码开始处的9个字节,正好是"while i < 10:"下面的"9   LOAD_NAME   0"指令。Python虚拟机又开始了新一轮的循环,继续比较i和10的大小,如此反复,在每一次TRUE分支中,都会使i的值递增1,直到i的值等于10。这时程序的执行流程就会转入False分支退出循环,然后Python虚拟机才会执行while循环控制结构之后的字节码指令序列

循环流程改变指令之continue

在demo4.py的while循环中,但i大于或等于5时,意味着"41   JUMP_IF_FALSE   7"指令的判断操作的结果为Py_True,那么这里的指令跳跃动作将不会发生,所以虚拟机将执行接下来的"44 POP_TOP"和"45   JUMP_ABSOLUTE  9"指令。在执行JUMP_ABSOLUTE指令时,虚拟机的流程跳转到"9   LOAD_NAME   0"指令处,这完全符合了continue的语义

循环流程改变指令之break

虽然我们在demo4.py的循环中设置了一条break语句,但实际上这条语句永远不会执行,因为在满足执行break语句的条件时(即i == 20),循环在i大于或等于10时便结束了。虽然break语句无法执行,但我们还是可以根据它执行时动作,看看break是如何跳出当前循环的

虚拟机在执行"61   JUMP_IF_FALSE   5"指令时,如果其中的判断操作的结果为Py_True,就意味着i == 20这个条件满足了,程序流程就进入了对break的执行。Python虚拟机首先会执行"64 POP_TOP"指令,将位于运行时栈栈顶的Py_True弹出,然后执行"65   BREAK_LOOP"指令结束循环

ceval.c

case BREAK_LOOP:
	why = WHY_BREAK;
	goto fast_block_end;

  

Python虚拟机将结束状态why设置为why_break,然后进入fast_block_end。fast_block_end是一段比较复杂的代码块,其中还有关于异常机制的代码,这里我们只截取break相关的代码:

ceval.c

#define JUMPTO(x) (next_instr = first_instr + (x))			 

fast_block_end:
	while (why != WHY_NOT && f->f_iblock > 0)
	{
		//取得与当前while循环对应的PyTryBlock
		PyTryBlock *b = PyFrame_BlockPop(f);
		……
		//将运行时栈恢复到while循环之前的状态
		while (STACK_LEVEL() > b->b_level)
		{
			v = POP();
			Py_XDECREF(v);
		}
		//处理break语义动作
		if (b->b_type == SETUP_LOOP && why == WHY_BREAK)
		{
			why = WHY_NOT;
			JUMPTO(b->b_handler);
			break;
		}
		……
	}

  

Python虚拟机首先获得了之前通过SETUP_LOOP指令申请得到的,与当前while循环对应的PyTryBlock结构,然后根据其中存储的运行时栈信息将运行时栈恢复到while循环之后的状态。最后Python虚拟机将why设置为WHY_NOT,表示退出状态没有任何错误,再通过JUMPTO宏,将虚拟机中下一条指令的指示器next_instr设置为距离code开始位置b->b_handler个字节的指令

这个b_handler是在执行SETUP_LOOP指令时设置的,参考SETUP_LOOP的指令代码和PyFrame_BlockSetup的代码可以看到,这个值会被设置为INSTR_OFFSET() + oparg。注意到这里的oparg是指令"6   SETUP_LOOP 71"的指令参数,即71。INSTR_OFFSET()宏对应的代码为((int)(next_instr - first_instr)),因为在执行SETUP_LOOP指令时,next_instr已经指向了下一条待执行的字节码指令,即"9   LOAD_NAME   0",很显然,这里的b_handler的值为INSTR_OFFSET() + oparg = 9 + 71 = 80。这也意味着break语句将导致Python虚拟机的流程跳转到前面所示的指令序列的倒数第2条指令"80   LOAD_CONST   3"处。确实,它已经跳出while循环所对应的指令序列了

值的注意的是,这里虽然使用了why这个用于栈帧(PyFrameObject)结束时的结束状态,但是实际上并没有结束当前活动的栈帧,而仅仅是利用其实现了break语义。可以看到,最后why又被设置为正常状态的WHY_NOT,而虚拟机仍然在当前栈帧中运行