其他一般表达式
在前两章:Python虚拟机中的一般表达式(一)、Python虚拟机中的一般表达式(二)中,我们介绍了Python虚拟机是怎样执行创建一个整数值对象、字符串对象、字典对象和列表对象。现在,我们再来学习变量赋值、变量运算和print操作,Python是如何执行的
还是和以前一样,我们看一下normal.py对应的PyCodeObject所对应的符号表和常量
# cat normal.py
a = 5
b = a
c = a + b
print(c)
# python2.5
……
>>> source = open("normal.py").read()
>>> co = compile(source, "normal.py", "exec")
>>> co.co_names
('a', 'b', 'c')
>>> co.co_consts
(5, None)
以及normal.py所对应的字节码指令
>>> import dis
>>> dis.dis(co)
1 0 LOAD_CONST 0 (5)
3 STORE_NAME 0 (a) 2 6 LOAD_NAME 0 (a)
9 STORE_NAME 1 (b) 3 12 LOAD_NAME 0 (a)
15 LOAD_NAME 1 (b)
18 BINARY_ADD
19 STORE_NAME 2 (c) 4 22 LOAD_NAME 2 (c)
25 PRINT_ITEM
26 PRINT_NEWLINE
27 LOAD_CONST 1 (None)
30 RETURN_VALUE
>>>
第一行a = 5所对应的字节码指令这里不再叙述,我们开始看第二行的b = a
b = a
//分析结果
2 6 LOAD_NAME 0 (a)
9 STORE_NAME 1 (b)
上面我们看到了一个新的指令:LOAD_NAME,这里,我们再看一下LOAD_NAME指令的内容,看看它到底做了什么,是如何配合STORE_NAME,完成赋值语句的
ceval.c
case LOAD_NAME:
w = GETITEM(names, oparg); if ((v = f->f_locals) == NULL) {
PyErr_Format(PyExc_SystemError,
"no locals when loading %s",
PyObject_REPR(w));
break;
}
if (PyDict_CheckExact(v)) {
//[1]:在local名字空间中查找变量名对应的变量值
x = PyDict_GetItem(v, w);
Py_XINCREF(x);
}
else {
x = PyObject_GetItem(v, w);
if (x == NULL && PyErr_Occurred()) {
if (!PyErr_ExceptionMatches(PyExc_KeyError))
break;
PyErr_Clear();
}
}
if (x == NULL) {
//[2]:在global名字空间中查找变量名对应的变量值
x = PyDict_GetItem(f->f_globals, w);
if (x == NULL) {
//[3]:在builtin名字空间中查找变量名对应的变量值
x = PyDict_GetItem(f->f_builtins, w);
if (x == NULL) {
//[4]:查找变量名失败,抛出异常
format_exc_check_arg(
PyExc_NameError,
NAME_ERROR_MSG ,w);
break;
}
}
Py_INCREF(x);
}
PUSH(x);
continue;
LOAD_NAME其实看上去内容很多,实际上很简单,结合上面代码的注释[1]、[2]、[3]处,我们可以知道,LOAD_NAME无非就是在local、global和builtin三个名字空间中查找一个变量名所对应的值,如果直到builtin名字空间也找不到,就抛出异常。而找到变量名对应的值之后,再压入运行时栈。最后配合STORE_NAME,完成赋值语句。
而Python的官方文档也描述了变量的搜索会沿着局部作用域(local名字空间)、全局作用域(global名字空间)和内建作用域(builtin名字空间)依次上溯,直至搜索成功或全部搜完3个作用域
让我们稍微修改一下LOAD_NAME的代码,然后重新编译安装Python
case LOAD_NAME:
w = GETITEM(names, oparg);
if ((v = f->f_locals) == NULL) { PyErr_Format(PyExc_SystemError,
"no locals when loading %s",
PyObject_REPR(w));
break;
}
if (PyDict_CheckExact(v)) {
x = PyDict_GetItem(v, w);
//[1]
if(strcmp(PyString_AsString(w),"PythonVM")==0){
printf("[LOAD NAME]:Search PyObject %s in local name space...%s\n",PyString_AsString(w),x==NULL? "False": "Success");
}
Py_XINCREF(x);
}
else {
x = PyObject_GetItem(v, w);
if (x == NULL && PyErr_Occurred()) {
if (!PyErr_ExceptionMatches(PyExc_KeyError))
break;
PyErr_Clear();
}
}
if (x == NULL) {
x = PyDict_GetItem(f->f_globals, w);
//[2]
if(strcmp(PyString_AsString(w),"PythonVM")==0){
printf("[LOAD NAME]:Search PyObject %s in global name space...%s\n", PyString_AsString(w), x==NULL? "False": "Success");
}
if (x == NULL) {
x = PyDict_GetItem(f->f_builtins, w);
//[3]
if(strcmp(PyString_AsString(w),"PythonVM")==0){
printf("[LOAD NAME]:Search PyObject %s in builtin name space...%s\n", PyString_AsString(w), x==NULL? "False": "Success");
}
if (x == NULL) {
//[4]
if(strcmp(PyString_AsString(w),"PythonVM")==0){
printf("[LOAD NAME]:Search PyObject %s faild\n", PyString_AsString(w));
}
format_exc_check_arg(
PyExc_NameError,
NAME_ERROR_MSG ,w);
break;
}
}
Py_INCREF(x);
}
PUSH(x);
continue;
我们在[1]、[2]、[3]、[4]处加入代码,尝试搜索符号的过程
# python2.5
……
>>> print PythonVM
[LOAD NAME]:Search PyObject PythonVM in local name space...False
[LOAD NAME]:Search PyObject PythonVM in global name space...False
[LOAD NAME]:Search PyObject PythonVM in builtin name space...False
[LOAD NAME]:Search PyObject PythonVM faild
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'PythonVM' is not defined
我们未曾定义PythonVM这个变量,在打印PythonVM这个符号时会先获取PythonVM对应的值,也就是会执行LOAD_NAME这条指令,可以看到,搜索变量时确实是按着local、global、builtin这三个名字空间搜索
数值运算
前面我们已经介绍了赋值操作所对应的字节码时如何执行的,这一小节,我们在基于之前的内容,了解数值运算
c = a + b
//分析结果
3 12 LOAD_NAME 0 (a)
15 LOAD_NAME 1 (b)
18 BINARY_ADD
19 STORE_NAME 2 (c)
LOAD_NAME将a和b的值读取,并压入运行时栈,,然后通过BINARY_ADD进行加法运算,根据后面的STORE_NAME,可以猜测在BINARY_ADD中,已经把运算结果压入运行时栈,最后再STORE_NAME弹出其结果,建立符号和值的关系。现在,我们来看一下BINARY_ADD
case BINARY_ADD:
w = POP();
v = TOP();
if (PyInt_CheckExact(v) && PyInt_CheckExact(w)) {
//[1]:内置PyIntObject对象相加的快速通道
register long a, b, i;
a = PyInt_AS_LONG(v);
b = PyInt_AS_LONG(w);
i = a + b;
//[2]:如果其结果溢出,转向慢速通道
if ((i^a) < 0 && (i^b) < 0)
{
goto slow_add;
}
x = PyInt_FromLong(i);
}
//[3]:PyStringObject对象相加的快速通道
else if (PyString_CheckExact(v) &&
PyString_CheckExact(w)) {
x = string_concatenate(v, w, f, next_instr);
/* string_concatenate consumed the ref to v */
goto skip_decref_vx;
}
else {
//[4]:一般对象相加的慢速通道
slow_add:
x = PyNumber_Add(v, w);
}
Py_DECREF(v);
skip_decref_vx:
Py_DECREF(w);
SET_TOP(x);
if (x != NULL) continue;
break;
从上面的代码[1]、[2]、[3]、[4]处可以看到,如果对象是int对象,则将其值取出,然后相加再检测是否溢出,如果溢出则走对象相加的慢速通道,如果没有溢出则返回,如果是PyStringObject对象相加,则根据相加结果创建新的PyStringObject对象返回
如果参与运算的对象是这两种快速通道之外的情况,那只能走慢速通道PyNumber_Add完成加法运算。在PyNumber_Add中,Python虚拟机会进行大量的类型判断,寻找与对象相对应的加法操作函数等额外工作,速度会比前两种加速机制慢上很多。一般来说,Python虚拟机在PyNumber_Add中首先检查参与运算的对象的类型对象,检查PyNumberMethods中的nb_add能否完成v和w上的加法运算,如果不能,还会检查PySequenceMethods中的sq_concat能否完成,如果都不能,则会抛出异常
这里需要注意一点的是,虽然Python虚拟机为PyIntObject对象准备了快速通道,但是如果计算结果溢出,Python虚拟机会放弃快速通道的计算结果,转向慢速通道。为了验证之前所说,我们再次修改BINARY_ADD的代码,在[1]、[2]、[3]处加上监测代码:
case BINARY_ADD:
w = POP();
v = TOP();
if (PyInt_CheckExact(v) && PyInt_CheckExact(w))
{
/* INLINE: int + int */
register long a, b, i;
a = PyInt_AS_LONG(v);
b = PyInt_AS_LONG(w);
i = a + b;
if ((i ^ a) < 0 && (i ^ b) < 0)
{
//[1]
printf("[BINARY_ADD]:%ld + %ld in quick channel...overflow\n", a, b);
goto slow_add;
}
//[2]
printf("[BINARY_ADD]:%ld + %ld in quick channel...success\n", a, b);
x = PyInt_FromLong(i);
}
else if (PyString_CheckExact(v) &&
PyString_CheckExact(w))
{
x = string_concatenate(v, w, f, next_instr);
/* string_concatenate consumed the ref to v */
goto skip_decref_vx;
}
else
{
slow_add:
//[3]
if (PyInt_CheckExact(v) && PyInt_CheckExact(w))
{
register long a, b;
a = PyInt_AS_LONG(v);
b = PyInt_AS_LONG(w);
printf("[BINARY_ADD]:%ld + %ld switch to slow channel\n", a, b);
}
x = PyNumber_Add(v, w);
}
Py_DECREF(v);
skip_decref_vx:
Py_DECREF(w);
SET_TOP(x);
if (x != NULL)
continue;
break;
然后编译安装Python,测试一下BINARY_ADD的行为:
# python2.5
……
>>> a = 1
>>> b = 2
>>> a + b
[BINARY_ADD]:1 + 2 in quick channel...success
3
>>> c = 9223372036854775807
>>> d = c + c
[BINARY_ADD]:9223372036854775807 + 9223372036854775807 in quick channel...overflow
[BINARY_ADD]:9223372036854775807 + 9223372036854775807 switch to slow channel
>>> type(d)
<type 'long'>
信息输出
最后来看一下print的动作,在前面的normal.py中,最后我们打印了c这个对象,我们来看一下对应的字节码:
print(c)
//分析结果
4 22 LOAD_NAME 2 (c)
25 PRINT_ITEM
26 PRINT_NEWLINE
在打印对象之前,一定要获取它的值,所以第一条字节码指令是LOAD_NAME,将c的值从名字空间取出,然后压入运行时栈,最后通过PRINT_ITEM完成打印操作
case PRINT_ITEM:
v = POP();
if (stream == NULL || stream == Py_None)
{
w = PySys_GetObject("stdout");
if (w == NULL)
{
PyErr_SetString(PyExc_RuntimeError,
"lost sys.stdout");
err = -1;
}
}
Py_XINCREF(w);
if (w != NULL && PyFile_SoftSpace(w, 0))
err = PyFile_WriteString(" ", w);
if (err == 0)
err = PyFile_WriteObject(v, w, Py_PRINT_RAW);
………//省略部分代价
Py_XDECREF(w);
Py_DECREF(v);
Py_XDECREF(stream);
stream = NULL;
if (err == 0)
continue;
break;
Python在打印时会判断一个名为stream的对象是否为NULL,如果为NULL则将w设置为标准输出流。那么,stream是什么呢?它实际上是定义在PyEval_EvalFrameEx中的一个PyObject对象
register PyObject *stream = NULL;
如果输出的时候,是通过如下的Python代码:
# cat demo3.py
f = open("test", "w")
print >> f, 1
# python2.5
……
>>> source = open("demo3.py").read()
>>> co = compile(source, "demo3.py", "exec")
>>> import dis
>>> dis.dis(co)
1 0 LOAD_NAME 0 (open)
3 LOAD_CONST 0 ('test')
6 LOAD_CONST 1 ('w')
9 CALL_FUNCTION 2
12 STORE_NAME 1 (f) 2 15 LOAD_NAME 1 (f)
18 DUP_TOP
19 LOAD_CONST 2 (1)
22 ROT_TWO
23 PRINT_ITEM_TO
24 PRINT_NEWLINE_TO
25 LOAD_CONST 3 (None)
28 RETURN_VALUE
>>>
那么在执行PRINT_ITEM之前,将会执行PRINT_NEWLINE_TO这条指令
case PRINT_ITEM_TO:
w = stream = POP();
/* fall through to PRINT_ITEM */
case PRINT_ITEM:
……
可以看到,在执行PRINT_NEWLINE_TO时就给stream赋值了,同时也赋值给w。所以实际上stream是作为一个判断条件来使用的,真正使用的输出目标是w。要多次使用这一个stream的原因是变量w在别的字节码指令中可能还会用到,所以无法通过判断w是否为NULL来确定是否需要输出到标准输出流,可以看到,在PRINT_ITEM最后,又将stream设置为NULL,为下次输出时的判断做准备
在获得输出的目标和待输出的对象后,PRINT_ITEM将通过PyFile_WriteObject->PyObject_Print->internal_print的调用序列最终调用v->ob_type->tp_print等待输出对象自身所携带的输出函数进行输出。如果对象没有定义tp_print,它就会调用tp_str或tp_repr获得对象的字符串表示形式,然后将字符串输出