Python虚拟机之for循环控制流(二)

Python虚拟机中的for循环控制流

Python虚拟机之if控制流(一)这一章中,我们了解if控制流的字节码实现,在if控制结构中,虽然Python虚拟机会在不同的分支摇摆,但大体还是向前执行,但是在for循环控制结构中,我们将会看到一种新的指令跳跃方式,即指令回退。在if控制流章节中,我们看到了指令跳跃时,通常跳跃的距离都是当前指令与目标指令之间的距离。如果按照这种逻辑,进行回退时,这个跳跃是否是负数呢?别急,我们下面一点一点来剖析for循环控制流的实现

# cat demo3.py 
lst = [1, 2]
for i in lst:
    print(i)
# python2.5
……
>>> source = open("demo3.py").read()
>>> co = compile(source, "demo3.py", "exec")
>>> import dis
>>> dis.dis(co)
  1           0 LOAD_CONST               0 (1)
              3 LOAD_CONST               1 (2)
              6 BUILD_LIST               2
              9 STORE_NAME               0 (lst)

  2          12 SETUP_LOOP              19 (to 34)
             15 LOAD_NAME                0 (lst)
             18 GET_ITER            
        >>   19 FOR_ITER                11 (to 33)
             22 STORE_NAME               1 (i)

  3          25 LOAD_NAME                1 (i)
             28 PRINT_ITEM          
             29 PRINT_NEWLINE       
             30 JUMP_ABSOLUTE           19
        >>   33 POP_BLOCK           
        >>   34 LOAD_CONST               2 (None)
             37 RETURN_VALUE  

  

第一条指令这里不再介绍,就是一条创建一个列表对象,如果有疑问的同学可以看Python虚拟机中的一般表达式(二)这一章节关于列表对象创建的介绍,我们主要看"12   SETUP_LOOP   19"这条指令,它在Python虚拟机中为for循环控制结构起着至关重要的作用

ceval.c

case SETUP_LOOP:
case SETUP_EXCEPT:
case SETUP_FINALLY:
	PyFrame_BlockSetup(f, opcode, INSTR_OFFSET() + oparg,
					   STACK_LEVEL());
	continue;

  

在SETUP_LOOP指令的实现中,仅仅简单调用了一个PyFrame_BlockSetup函数,那么,我们来看一下这个函数的实现

frameobject.c

void
PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level)
{
	PyTryBlock *b;
	if (f->f_iblock >= CO_MAXBLOCKS)
		Py_FatalError("XXX block stack overflow");
	b = &f->f_blockstack[f->f_iblock++];
	b->b_type = type;
	b->b_level = level;
	b->b_handler = handler;
}

  

在这里,我们第一次使用PyFrameObject中的f_blockstack

frameobject.h

typedef struct _frame {
    ……
    int f_iblock;		/* index in f_blockstack */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
    ……
} PyFrameObject; 

  

CO_MAXBLOCKS在Python2.5中被定义为20,f_iblock在调用PyFrame_New时被初始化为0,而那个至关重要的PyTryBlock定义如下:

frameobject.h

typedef struct {
    int b_type;			/* what kind of block this is */
    int b_handler;		/* where to jump to find handler */
    int b_level;		/* value stack level to pop to */
} PyTryBlock;

  

显然,PyFrameObject对象中的f_blockstack是一个PyTryBlock结构的数组,而SETUP_LOOP指令所做的就是从这个数组中获得一块PyTryBlock结构,并在这个结构中存放一些Python虚拟机当前的状态信息。比如当前执行的字节码指令,当前运行时栈的深度等等

PyTryBlock结构中有一个b_type域,这意味着实际上存在着几种不同用途的PyTryBlock对象。从PyFrame_BlockSetup中可以看到,这个b_type实际上被设置为当前Python虚拟机正在执行的字节码指令,以字节码指令作为区分PyTryBlock的不同用途。从上面的SETUP_LOOP的实现我们可以知道,除了循环,异常机制也会用到PyTryBlock

list迭代器

在SETUP_LOOP指令从PyFrameObject的f_blockstack中申请了一块PyTryBlock结构的空间之后,Python虚拟机通过"15   LOAD_NAME 0"指令,将刚创建的PyListObject对象压入运行时栈,再执行"18 GET_ITER"指令来获得PyListObject对象的迭代器

ceval.c

case GET_ITER:
	//从运行时栈获得PyListObject对象
	v = TOP();
	x = PyObject_GetIter(v);
	Py_DECREF(v);
	if (x != NULL)
	{
		//将PyListObject对象的iterator压入运行时栈
		SET_TOP(x);
		PREDICT(FOR_ITER);
		continue;
	}
	STACKADJ(-1);
	break;

  

在GET_ITER的指令代码中,Python虚拟机首先会获得位于运行时栈的栈顶的PyListObject对象,然后通过PyObject_GetIter获得PyListObject对象的迭代器

object.h

typedef PyObject *(*getiterfunc) (PyObject *);

  

abstract.c

PyObject *
PyObject_GetIter(PyObject *o)
{
	PyTypeObject *t = o->ob_type;
	getiterfunc f = NULL;
	if (PyType_HasFeature(t, Py_TPFLAGS_HAVE_ITER))
		f = t->tp_iter;//获得类型对象中的tp_iter操作
	if (f == NULL) {
		if (PySequence_Check(o))
			return PySeqIter_New(o);
		return type_error("'%.200s' object is not iterable", o);
	}
	else {
		//通过tp_iter操作获得iterator
		PyObject *res = (*f)(o);
		if (res != NULL && !PyIter_Check(res)) {
			PyErr_Format(PyExc_TypeError,
				     "iter() returned non-iterator "
				     "of type '%.100s'",
				     res->ob_type->tp_name);
			Py_DECREF(res);
			res = NULL;
		}
		return res;
	}
}

  

显然,PyObject_GetIter是通过调用对象对应的类型对象中的tp_iter操作来获得与对象关联的迭代器,在Python中,不光PyListObject对象是PyObject对象,就连listiterobject迭代器对象也是PyObject对象

listobject.c

typedef struct {
	PyObject_HEAD
	long it_index;
	PyListObject *it_seq; /* Set to NULL when iterator is exhausted */
} listiterobject;

  

既然迭代器是是PyObject对象,那么显然,它也一定拥有类型对象。迭代器listiterobject对象所对应的类型对象为PyListIter_Type

listobject.c

PyTypeObject PyListIter_Type = {
	PyObject_HEAD_INIT(&PyType_Type)
	0,					/* ob_size */
	"listiterator",				/* tp_name */
	sizeof(listiterobject),			/* tp_basicsize */
	0,					/* tp_itemsize */
	/* methods */
	(destructor)listiter_dealloc,		/* tp_dealloc */
	0,					/* tp_print */
	0,					/* tp_getattr */
	0,					/* tp_setattr */
	0,					/* tp_compare */
	0,					/* tp_repr */
	0,					/* tp_as_number */
	0,					/* tp_as_sequence */
	0,					/* tp_as_mapping */
	0,					/* tp_hash */
	0,					/* tp_call */
	0,					/* tp_str */
	PyObject_GenericGetAttr,		/* tp_getattro */
	0,					/* tp_setattro */
	0,					/* tp_as_buffer */
	Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,/* tp_flags */
	0,					/* tp_doc */
	(traverseproc)listiter_traverse,	/* tp_traverse */
	0,					/* tp_clear */
	0,					/* tp_richcompare */
	0,					/* tp_weaklistoffset */
	PyObject_SelfIter,			/* tp_iter */
	(iternextfunc)listiter_next,		/* tp_iternext */
	listiter_methods,			/* tp_methods */
	0,					/* tp_members */
};

  

PyListObject对象的类型对象PyList_Type中,tp_iter域被设置为list_iter。这个list_iter正是PyObject_GetIter中所获得的那个f,也正是创建迭代器对象的函数:

listobject.c

static PyObject *
list_iter(PyObject *seq)
{
	listiterobject *it;

	if (!PyList_Check(seq)) {
		PyErr_BadInternalCall();
		return NULL;
	}
	it = PyObject_GC_New(listiterobject, &PyListIter_Type);
	if (it == NULL)
		return NULL;
	it->it_index = 0;
	Py_INCREF(seq);
	it->it_seq = (PyListObject *)seq;
	_PyObject_GC_TRACK(it);
	return (PyObject *)it;
}

  

PyListObject对象的迭代器只是对原来的PyListObject对象做了一层简单的包装,在迭代器中,维护了当前访问的元素在PyListObject对象中的序号:it_index。通过这个序号,listiterobject对象就可以实现对PyListObject的遍历

 GET_ITER指令在获得了PyListObject对象的迭代器之后,通过SET_TOP宏强制将这个迭代器对象设置为运行时栈的栈顶元素:

图1-1创建迭代器时运行时栈的变化

在指令"18   GET_ITER"完成之后,Python虚拟机开始了FOR_ITER指令的预测动作,这样做是为了提高效率

迭代控制

迭代器是一个对容器实现遍历的工具,遍历,就是循环的另一种说法。从"19   FOR_ITER   11"指令开始,我们开始进入Python虚拟机的for循环:

ceval.c

case FOR_ITER:
	//从运行时栈的栈顶获得iterator对象
	v = TOP();
	//通过iterator对象获得集合中的下一个元素对象
	x = (*v->ob_type->tp_iternext)(v);
	if (x != NULL)
	{
		//将获得的元素对象压入运行时栈
		PUSH(x);
		PREDICT(STORE_FAST);
		PREDICT(UNPACK_SEQUENCE);
		continue;
	}
	//x == NULL,意味着iterator的迭代已经结束
	if (PyErr_Occurred())
	{
		if (!PyErr_ExceptionMatches(PyExc_StopIteration))
			break;
		PyErr_Clear();
	}
	/* iterator ended normally */
	x = v = POP();
	Py_DECREF(v);
	JUMPBY(oparg);
	continue;

  

FOR_ITER的指令代码首先从运行时栈中获得PyListObject对象的迭代器,然后调用tp_iternext开始进行迭代。迭代器的tp_iternext操作总是返回与迭代器关联的容器对象的下一个元素,如果当前已经抵达了容器对象的结束位置,那么tp_iternext将返回NULL,这个结果代表遍历结束

FOR_ITER的指令代码会检查tp_iternext的返回结果,如果得到的是一个有效的元素,即x != NULL,那么将获得的元素压入到运行时栈中,并开始一系列字节码预测的动作。在demo3.py编译后的FOR_ITER指令中,PREDICT(STORE_FAST)和PREDICT(UNPACK_SEQUENCE)这两个预测动作都会失败,所以在continue处,Python虚拟机会重新进入对下一条字节码指令——"22   STORE_NAME 1"的执行过程

对于PyListObject对象的迭代器,其获取下一个元素的操作如下:

listobject.c

static PyObject *
listiter_next(listiterobject *it)
{
	PyListObject *seq;
	PyObject *item;

	assert(it != NULL);
	//seq是PyListObject对象
	seq = it->it_seq;
	if (seq == NULL)
		return NULL;
	assert(PyList_Check(seq));

	if (it->it_index < PyList_GET_SIZE(seq)) {
		//获得序号为it_index的元素对象
		item = PyList_GET_ITEM(seq, it->it_index);
		//调整it_index,使其指向下一个元素对象
		++it->it_index;
		Py_INCREF(item);
		return item;
	}
	//迭代结束
	Py_DECREF(seq);
	it->it_seq = NULL;
	return NULL;
}

  

在获取当前it_index对应的PyListObject对象的元素后,将it_index递增,为下一个迭代动作做好准备。在我们的例子中,这里会返回一个PyIntObject对象1

图1-2展示了直到"22   STORE_NAME   1"之前,运行时栈及local名字空间的变化情况

 

图1-2 迭代过程中虚拟机的状态变化

 

 在这之后,Python虚拟机将沿着字节码的顺序一条一条执行下去,从而完成输出的动作。按照demo3.py中Python源代码所定义的行为,应该获得PyListObject对象中的下一个元素,并继续进行输出操作。直观上,这里需要一个向后回退的指令,这个指令正是"30   JUMP_ABSOLUTE   19"

ceval.c

case JUMP_ABSOLUTE:
	JUMPTO(oparg);
	continue;

  

前面我们提到,如果for循环沿用JUMPBY的逻辑,那么参数必须是一个负数,因为涉及到一个指令回退的过程。但这里Python采用的是另一种做法,不再使用相对距离进行跳跃,而是使用基于字节码指令序列开始位置的绝对距离跳跃,而完成这一动作的,正是JUMP_ABSOLUTE指令

JUMP_ABSOLUTE指令的行为是强制设定next_instr的值,将next_instr设定到距离f->f_code->co_code开始地址的某一特定偏移的位置。这个偏移的量由JUMP_ABSOLUTE的指令参数决定。所以,这条参数就成了for循环中指令回退动作最关键的一点,在我们编译后的字节码指令中,JUMP_ABSOLUTE的指令参数是19,那么这个19是代表什么呢?

我们把眼光放回编译后的字节码指令开始初,字节码指令的第二列代表偏移,那么这个19即为偏移位为19的位置,于是我们到达了"19   FOR_ITER   11"这条指令,那么Python虚拟机下一步动作就是执行FOR_ITER,即通过PyListObject对象中的迭代器获取下一个元素,然后继续向前,打印元素

可见,在JUMP_ABSOLUTE指令处,Python虚拟机实现了字节码的向后回退动作。而Python虚拟机也在FOR_ITER指令和JUMP_ABSOLUTE指令之间成功地构造处了一个循环结构,正是这个循环结构对应着demo3.py中的那个for循环结构

终止迭代

可能你会好奇,为什么"19   FOR_ITER   11"中的字节码FOR_ITER也会有参数,其实,这个11是和终止迭代有关。一个列表不管再长,也有迭代完毕的一天,在FOR_ITER检查到PyListObject对象的迭代器获得的下一个元素为NULL时,就意味着迭代结束了。这个结果将导致Python虚拟机会将迭代器从运行时栈中弹出,同时执行一个JUMPBY的动作,向前飞跃

ceval.c

#define JUMPBY(x) (next_instr += (x))

  

向前飞跃的距离即为FOR_ITER的参数,即为11。我们从FOR_ITER后的STORE_NAME前进11个字节,正好达到POP_BLOCK

ceval.c

case POP_BLOCK:
{
	PyTryBlock *b = PyFrame_BlockPop(f);
	while (STACK_LEVEL() > b->b_level)
	{
		v = POP();
		Py_DECREF(v);
	}
}

  

frameobject.c

PyTryBlock *
PyFrame_BlockPop(PyFrameObject *f)
{
	PyTryBlock *b;
	if (f->f_iblock <= 0)
		Py_FatalError("XXX block stack underflow");
	//向f_blockstack中归还PyTryBlock
	b = &f->f_blockstack[--f->f_iblock];
	return b;
}

  

还记得在SETUP_LOOP处,Python虚拟机从f->f_blockstack中申请了一个PyTryBlock结构吗?Python虚拟机会将一些状态信息保存到所获得的PyTryBlock结构中,在执行POP_BLOCK指令时,实际上就是把PyTryBlock归还给f->f_blockstack。同时,Python虚拟机抽取在SETUP_LOOP指令处保存在PyTryBlock中的信息,并根据其中存储的SETUP_LOOP指令、运行时栈的深度信息将运行时栈恢复到SETUP_LOOP之前的状态,从而完成整个for循环结构。

在执行SETUP_LOOP指令时,Python虚拟机保存了许多信息,而在执行POP_BLOCK指令时,却只能使用栈深度信息来恢复运行时栈啊,为什么会有这种不对称呢?这是因为,PyTryBlock并不是专门为for循环准备的,Python中还会有一些机制用到这个结构,为了避免代码过于复杂,Python不管三七二十一,在PyFrame_BlockSetup中一股脑将所有机制可能用到的参数全部放在PyTryBlock结构中,各个机制需要什么参数,取用需要的参数即可,对于其他参数,可以不用理会

最后,让我们修改一下GET_ITER、FOR_ITER、JUMP_ABSOLUTE的实现,来实时观察一下for循环的执行流程

ceval.c

case GET_ITER:
	v = TOP();
	x = PyObject_GetIter(v);
	Py_DECREF(v);
	if (x != NULL) {
		SET_TOP(x);
		printf("[GET_ITER]:Get iterator...\n");
		PREDICT(FOR_ITER);
		continue;
	}
	STACKADJ(-1);
	break;
……
case FOR_ITER:
	v = TOP();
	x = (*v->ob_type->tp_iternext)(v);
	if (x != NULL) {
		PUSH(x);
		if (PyInt_CheckExact(x)) {
			register long i;
			i = PyInt_AS_LONG(x);
			printf("[FOR_ITER]:Get next item -> %ld...\n", i);
		}
		PREDICT(STORE_FAST);
		PREDICT(UNPACK_SEQUENCE);
		continue;
	}
	else {
		printf("[FOR_ITER]:Get next item -> NULL...\n");
	}
	if (PyErr_Occurred()) {
		if (!PyErr_ExceptionMatches(PyExc_StopIteration))
			break;
		PyErr_Clear();
	}
	x = v = POP();
	Py_DECREF(v);
	JUMPBY(oparg);
	continue;
……
case JUMP_ABSOLUTE:
	JUMPTO(oparg);
	if (*next_instr == FOR_ITER){
		printf("[JUMP_ABSOLUTE]:Go back to FOR_ITER...\n");
	}
	continue;

    

重新编译Python,然后进入Python的命令行模式

>>> lst = [6, 5, 4, 3, 2, 1]
>>> for i in lst:
...     pass
... 
[GET_ITER]:Get iterator...
[FOR_ITER]:Get next item -> 6...
[JUMP_ABSOLUTE]:Go back to FOR_ITER...
[FOR_ITER]:Get next item -> 5...
[JUMP_ABSOLUTE]:Go back to FOR_ITER...
[FOR_ITER]:Get next item -> 4...
[JUMP_ABSOLUTE]:Go back to FOR_ITER...
[FOR_ITER]:Get next item -> 3...
[JUMP_ABSOLUTE]:Go back to FOR_ITER...
[FOR_ITER]:Get next item -> 2...
[JUMP_ABSOLUTE]:Go back to FOR_ITER...
[FOR_ITER]:Get next item -> 1...
[JUMP_ABSOLUTE]:Go back to FOR_ITER...
[FOR_ITER]:Get next item -> NULL...
>>> 

  

  

posted @ 2018-08-18 17:34  北洛  阅读(1174)  评论(0编辑  收藏  举报