Python虚拟机中的一般表达式(三)
其他一般表达式
在前两章: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获得对象的字符串表示形式,然后将字符串输出
Python虚拟机中的一般表达式(三)的更多相关文章
- Python虚拟机中的一般表达式(二)
复杂内建对象的创建 在上一章Python虚拟机中的一般表达式(一)中,我们看到了Python是如何创建一个空的字典对象和列表对象,那么如果创建一个非空的字典对象和列表对象,Python的行为又是如何呢 ...
- Python虚拟机中的一般表达式(一)
在Python虚拟机框架这一章中,我们通过PyEval_EvalFrameEx看到了Python虚拟机的整体框架.而这章开始,我们将了解Python虚拟机是如何完成对Python的一般表达式的执行,这 ...
- 《python解释器源码剖析》第10章--python虚拟机中的一般表达式
10.0 序 上一章中,我们通过PyEval_EvalFrameEx看到了python虚拟机的整体框架,那么这一章我们将深入到PyEval_EvalFrameEx的各个细节当中,深入剖析python的 ...
- (Python学习9)Python虚拟机中的一般表达式
1.准备工作 执行.py程序时,Python解释器对PyCodeObject的co_code存储的字节码进行解释执行,同时co_consts存储了常量,co_names存储了变量名称.用compile ...
- 《python解释器源码剖析》第11章--python虚拟机中的控制流
11.0 序 在上一章中,我们剖析了python虚拟机中的一般表达式的实现.在剖析一遍表达式是我们的流程都是从上往下顺序执行的,在执行的过程中没有任何变化.但是显然这是不够的,因为怎么能没有流程控制呢 ...
- python虚拟机中的异常流控制
异常:对程序运行中的非正常情况进行抽象.并且提供相应的语法结构和语义元素,使得程序员能够通过这些语法结构和语义元素来方便地描述异常发生时的行为. 1.Python中的异常机制: 1.1Python虚拟 ...
- 《python解释器源码剖析》第13章--python虚拟机中的类机制
13.0 序 这一章我们就来看看python中类是怎么实现的,我们知道C不是一个面向对象语言,而python却是一个面向对象的语言,那么在python的底层,是如何使用C来支持python实现面向对象 ...
- 《python解释器源码剖析》第12章--python虚拟机中的函数机制
12.0 序 函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作.当然在调用函数时,会干什么来着.对,要在运行时栈中创建栈帧,用于函数的执行. 在python ...
- 《python源代码剖析》笔记 python虚拟机中的函数机制
本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.Python虚拟机在运行函数调用时会动态地创建新的 PyFrameObject对象, 这 ...
随机推荐
- C#中的XML文档注释-推荐的文档注释标记
文档注释是为了方便自己和他人更好地理解代码所实现的功能.下面记录了一些常用的文档注释标记: <C> 用法: <c>text</c> 将说明中的文本标记为代码.例如: ...
- (办公)ssm发送邮件
1.添加jar包 <!-- Javamail API --> <dependency> <groupId>javax.mail</groupId> &l ...
- zTree的重点
今天学习了zTree插件,记录一下使用步骤: 1 下载,把下载好的目录整个放在项目中 文件夹目录: js:一般引这jquery.ztree.all.js和jquery.ztree.core.js cs ...
- 个人博客 attack.cf
新开了个emlog搭的博客 地址:attack.cf 主要分享一下网络安全方面的东西和一些精品资源 欢迎来访
- [20190611]记录一下github的基本用法
本文记录如何使用github创建项目并上传代码,因为有一段时间没用github了,中途又重装了系统,今天重新使用一下. 然后特地做简要记录: 1. 创建SSH Key SSH Key指一般在C:\Us ...
- linux系统基本结构-《循序渐进linux》
1.linux控制台 linux系统由桌面控制台(X -Window视窗)和字符控制台组成.字符控制台是linux的核心,默认linux下有6个字符控制台. 字符控制台--〉X-Window下:ctr ...
- LR中变量、参数的使用介绍
Action(){ char * url = "www.baidu.com"; char arr_url[1024]; //将url变量的值复制给p_url1参数 lr_save_ ...
- Python+selenium之测试报告(1)
一.下载HTMLTestRunner.py HTMLTestRunner 是 Python 标准库的 unittest 模块的一个扩展.它生成易于使用的 HTML 测试报告.HTMLTestRunne ...
- last命令
last——列出目前与过去登入系统的用户信息 命令所在路径:/usr/bin/last 示例1: $ last
- codeforce Gym 100500I Hall of Fame (水)
题意:统计一些串中,字母的出现频率,不分大小写,找出现频率最高5个字符(相同频率优先取字典序大的),把他们的对应的值加起来判断以下是否大于62. 没出现的不算. #include<cstdio& ...