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...
>>>
Python虚拟机之for循环控制流(二)的更多相关文章
- Python虚拟机之while循环控制结构(三)
Python虚拟机中的while循环控制结构 在Python虚拟机之if控制流(一)和Python虚拟机之for循环控制流(二)两个章节中,我们介绍了if和for两个控制结构在Python虚拟机中的实 ...
- Python虚拟机之异常控制流(四)
Python虚拟机中的异常控制流 先前,我们分别介绍了Python虚拟机之if控制流(一).Python虚拟机之for循环控制流(二)和Python虚拟机之while循环控制结构(三).这一章,我们来 ...
- 《python解释器源码剖析》第11章--python虚拟机中的控制流
11.0 序 在上一章中,我们剖析了python虚拟机中的一般表达式的实现.在剖析一遍表达式是我们的流程都是从上往下顺序执行的,在执行的过程中没有任何变化.但是显然这是不够的,因为怎么能没有流程控制呢 ...
- Python虚拟机之if控制流(一)
Python虚拟机中的if控制流 在所有的编程语言中,if控制流是最简单也是最常用的控制流语句.下面,我们来分析一下在Python虚拟机中对if控制流的实现 # cat demo.py a = 1 i ...
- Python虚拟机之异常控制流(五)
Python中的异常控制语义结构 在Python虚拟机之异常控制流(四)这一章中,我们考察了Python的异常在虚拟机中的级别上是什么东西,抛出异常这个动作在虚拟机的级别上对应的行为,最后,我们还剖析 ...
- Python之虚拟机操作:利用VIX二次开发,实现自己的pyvix(系列一)成果展示和python实例
在日常工作中,需要使用python脚本去自动化控制VMware虚拟机,现有的pyvix功能较少,而且不适合个人编程习惯,故萌发了开发一个berlin版本pyvix的想法,暂且叫其OpenPyVix.O ...
- Python虚拟机类机制之填充tp_dict(二)
填充tp_dict 在Python虚拟机类机制之对象模型(一)这一章中,我们介绍了Python的内置类型type如果要完成到class对象的转变,有一个重要的步骤就是填充tp_dict对象,这是一个极 ...
- Python虚拟机函数机制之名字空间(二)
函数执行时的名字空间 在Python虚拟机函数机制之无参调用(一)这一章中,我们对Python中的函数调用机制有个大概的了解,在此基础上,我们再来看一些细节上的问题.在执行MAKE_FUNCTION指 ...
- Python虚拟机中的一般表达式(二)
复杂内建对象的创建 在上一章Python虚拟机中的一般表达式(一)中,我们看到了Python是如何创建一个空的字典对象和列表对象,那么如果创建一个非空的字典对象和列表对象,Python的行为又是如何呢 ...
随机推荐
- 提升Java代码质量(一)
博主双12入手了一本"Effective Java第二版",本系列文章将初步梳理书中内容,我也查了些资料,我会针对知识点做一点展开,方便以后复习回顾; Item1.考虑用静态工厂代 ...
- Beta_版本发布
学号 姓名 201731041215 王阳 201731062302 鲜雨珂 201731062128 邓捷 201731062305 周蓉 201731062131 龙继平 201731062304 ...
- log4cpp安装使用
1. 主页:http://log4cpp.sourceforge.net“Log4cpp is library of C++ classes for flexible logging to files ...
- 中国区 Azure 服务和定价模式概述
由世纪互联运营的 Microsoft Azure 是第一个在中国正式商用,符合中国政府相关法规要求的国际化公有云服务.本文剖析了由世纪互联运营的 Microsoft Azure 的运营模式.采购模式. ...
- C#中Image类与byte[]之间的转换
//将image转化为二进制 public byte[] GetByteImage(Image img) { byte[] bt = null; if (!img.Equals(null)) { us ...
- codeforce Gym 100570B ShortestPath Query (最短路SPFA)
题意:询问单源最短路径,每条边有一个颜色,要求路径上相邻边的颜色不能相同,无重边且边权为正. 题解:因为路径的合法性和边的颜色有关, 所以在做spfa的时候,把边丢到队列中去,松弛的时候注意判断一下颜 ...
- CDOJ 490 UESTC 490 Swap Game(思路,逆序对)
题意:有两种颜色的小球形成环,求最小交互次数使球相连. 题解:先解决另一个简单的问题,如果是一个链,把红球标记为1,蓝球标记为0,要排成升序需要多少次交换呢?答案是逆序对总数,原因是一次交互最多消除一 ...
- [C++讨论课] 课堂记录(一)
今天第一次参加c++讨论课,记录下了各组同学的展示的问题或者解决方法,也有一些知识点上的内容,供以后复习参考. 1.常量指针和指针常量问题 常量指针:指向常量的指针,例如const int *p = ...
- Java变量、Java对象初始化顺序
局部变量与成员变量: 局部变量分为: 行参:在方法签名中定义的局部变量,随方法的结束而凋亡. 方法内的局部变量:必须在方法内对其显示初始化,从初始化后开始生效,随方法的结束而凋亡. 代码块内的局部变量 ...
- MySQL基础教程——创建数据库并插入数据
本节将介绍 MySQL 新建数据库,新建表,插入数据以及基本数据类型的相关知识.本节实验将创建一个名为 mysql_shiyan 的数据库,其中有两张表 employee和 department. 1 ...