初步探究Python程序的执行原理


Posted in Python onApril 11, 2015

1. 过程概述

Python先把代码(.py文件)编译成字节码,交给字节码虚拟机,然后虚拟机一条一条执行字节码指令,从而完成程序的执行。
2. 字节码

字节码在Python虚拟机程序里对应的是PyCodeObject对象。
.pyc文件是字节码在磁盘上的表现形式。
3. pyc文件

PyCodeObject对象的创建时机是模块加载的时候,即import。
Python test.py会对test.py进行编译成字节码并解释执行,但是不会生成test.pyc。
如果test.py加载了其他模块,如import util,Python会对util.py进行编译成字节码,生成util.pyc,然后对字节码解释执行。
如果想生成test.pyc,我们可以使用Python内置模块py_compile来编译。
加载模块时,如果同时存在.py和.pyc,Python会尝试使用.pyc,如果.pyc的编译时间早于.py的修改时间,则重新编译.py并更新.pyc。
4. PyCodeObject

Python代码的编译结果就是PyCodeObject对象。

typedef struct {
  PyObject_HEAD
  int co_argcount;    /* 位置参数个数 */
  int co_nlocals;     /* 局部变量个数 */
  int co_stacksize;    /* 栈大小 */
  int co_flags;  
  PyObject *co_code;   /* 字节码指令序列 */
  PyObject *co_consts;  /* 所有常量集合 */
  PyObject *co_names;   /* 所有符号名称集合 */
  PyObject *co_varnames; /* 局部变量名称集合 */
  PyObject *co_freevars; /* 闭包用的的变量名集合 */
  PyObject *co_cellvars; /* 内部嵌套函数引用的变量名集合 */
  /* The rest doesn't count for hash/cmp */
  PyObject *co_filename; /* 代码所在文件名 */
  PyObject *co_name;   /* 模块名|函数名|类名 */
  int co_firstlineno;   /* 代码块在文件中的起始行号 */
  PyObject *co_lnotab;  /* 字节码指令和行号的对应关系 */
  void *co_zombieframe;  /* for optimization only (see frameobject.c) */
} PyCodeObject;
 
typedef struct {
  PyObject_HEAD
  int co_argcount;    /* 位置参数个数 */
  int co_nlocals;     /* 局部变量个数 */
  int co_stacksize;    /* 栈大小 */
  int co_flags;  
  PyObject *co_code;   /* 字节码指令序列 */
  PyObject *co_consts;  /* 所有常量集合 */
  PyObject *co_names;   /* 所有符号名称集合 */
  PyObject *co_varnames; /* 局部变量名称集合 */
  PyObject *co_freevars; /* 闭包用的的变量名集合 */
  PyObject *co_cellvars; /* 内部嵌套函数引用的变量名集合 */
  /* The rest doesn't count for hash/cmp */
  PyObject *co_filename; /* 代码所在文件名 */
  PyObject *co_name;   /* 模块名|函数名|类名 */
  int co_firstlineno;   /* 代码块在文件中的起始行号 */
  PyObject *co_lnotab;  /* 字节码指令和行号的对应关系 */
  void *co_zombieframe;  /* for optimization only (see frameobject.c) */
} PyCodeObject;

5. pyc文件格式

加载模块时,模块对应的PyCodeObject对象被写入.pyc文件,格式如下:

初步探究Python程序的执行原理

6. 分析字节码
6.1 解析PyCodeObject

Python提供了内置函数compile可以编译Python代码和查看PyCodeObject对象,如下:

Python代码[test.py]

s = ”hello”

def func():
  print s

func()
 
s = ”hello”
 
def func():
  print s
 
func()

 

在Python交互式shell里编译代码得到PyCodeObject对象:

初步探究Python程序的执行原理

dir(co)已经列出co的各个域,想查看某个域直接在终端输出即可:

初步探究Python程序的执行原理

test.py的PyCodeObject

co.co_argcount  0
co.co_nlocals   0
co.co_names    (‘s', 'func')
co.co_varnames  (‘s', 'func')
co.co_consts   (‘hello', <code object func at 0x2aaeeec57110, file ”test.py”, line 3>, None)
co.co_code    'd\x00\x00Z\x00\x00d\x01\x00\x84\x00\x00Z\x01\x00e\x01\x00\x83\x00\x00\x01d\x02\x00S'
 
co.co_argcount  0
co.co_nlocals   0
co.co_names    (‘s', 'func')
co.co_varnames  (‘s', 'func')
co.co_consts   (‘hello', <code object func at 0x2aaeeec57110, file ”test.py”, line 3>, None)
co.co_code    'd\x00\x00Z\x00\x00d\x01\x00\x84\x00\x00Z\x01\x00e\x01\x00\x83\x00\x00\x01d\x02\x00S'

Python解释器会为函数也生成的字节码PyCodeObject对象,见上面的co_consts[1]

func的PyCodeObject

func.co_argcount  0
func.co_nlocals  0
func.co_names   (‘s',)
func.co_varnames  ()
func.co_consts   (None,)
func.co_code    ‘t\x00\x00GHd\x00\x00S'
 
func.co_argcount  0
func.co_nlocals  0
func.co_names   (‘s',)
func.co_varnames  ()
func.co_consts   (None,)
func.co_code    ‘t\x00\x00GHd\x00\x00S'

 

co_code是指令序列,是一串二进制流,它的格式和解析方法见6.2。
6.2 解析指令序列

指令序列co_code的格式

初步探究Python程序的执行原理

Python内置的dis模块可以解析co_code,如下图:

test.py的指令序列

初步探究Python程序的执行原理

func函数的指令序列

初步探究Python程序的执行原理

第一列表示以下几个指令在py文件中的行号;
第二列是该指令在指令序列co_code里的偏移量;
第三列是指令opcode的名称,分为有操作数和无操作数两种,opcode在指令序列中是一个字节的整数;
第四列是操作数oparg,在指令序列中占两个字节,基本都是co_consts或者co_names的下标;
第五列带括号的是操作数说明。
7. 执行字节码

Python虚拟机的原理就是模拟可执行程序再X86机器上的运行,X86的运行时栈帧如下图:

初步探究Python程序的执行原理

假如test.py用C语言来实现,会是下面这个样子:

const char *s = “hello”;

void func() {
  printf(“%s\n”, s);
}

int main() {
  func();
  return 0;
}
 
const char *s = “hello”;
 
void func() {
  printf(“%s\n”, s);
}
 
int main() {
  func();
  return 0;
}

 

Python虚拟机的原理就是模拟上述行为。当发生函数调用时,创建新的栈帧,对应Python的实现就是PyFrameObject对象。
7.1 PyFrameObject

typedef struct _frame {
  PyObject_VAR_HEAD
  struct _frame *f_back;  /* 调用者的帧 */
  PyCodeObject *f_code;   /* 帧对应的字节码对象 */
  PyObject *f_builtins;   /* 内置名字空间 */
  PyObject *f_globals;   /* 全局名字空间 */
  PyObject *f_locals;    /* 本地名字空间 */
  PyObject **f_valuestack; /* 运行时栈底 */
  PyObject **f_stacktop;  /* 运行时栈顶 */
  …….
}
 
typedef struct _frame {
  PyObject_VAR_HEAD
  struct _frame *f_back;  /* 调用者的帧 */
  PyCodeObject *f_code;   /* 帧对应的字节码对象 */
  PyObject *f_builtins;   /* 内置名字空间 */
  PyObject *f_globals;   /* 全局名字空间 */
  PyObject *f_locals;    /* 本地名字空间 */
  PyObject **f_valuestack; /* 运行时栈底 */
  PyObject **f_stacktop;  /* 运行时栈顶 */
  …….
}

 

那么对应Python的运行时栈就是这样子:

初步探究Python程序的执行原理

7.2 执行指令

执行test.py的字节码时,会先创建一个栈帧,以下用f表示当前栈帧,执行过程注释如下:

test.py的符号名集合和常量集合

co.co_names  (‘s', 'func')
co.co_consts (‘hello', <code object func at 0x2aaeeec57110, file ”test.py”, line 3>, None)
 
co.co_names  (‘s', 'func')
co.co_consts (‘hello', <code object func at 0x2aaeeec57110, file ”test.py”, line 3>, None)

 

test.py的指令序列

初步探究Python程序的执行原理

上面的CALL_FUNCTION指令执行时,会创建新的栈帧,并执行func的字节码指令,以下用f表示当前栈帧,func的字节码执行过程如下:

func函数的符号名集合和常量集合

func.co_names    (‘s',)
func.co_consts   (None,)
 
func.co_names    (‘s',)
func.co_consts   (None,)

func函数的指令序列

初步探究Python程序的执行原理

7.3 查看栈帧

如果你想查看当前栈帧,Python提供了sys._getframe()方法可以获取当前栈帧,你只需要在代码里加入代码如下:

def func():
  import sys
  frame = sys._getframe()
  print frame.f_locals
  print frame.f_globals
  print frame.f_back.f_locals
  #你可以打印frame的各个域
  print s

 

Python 相关文章推荐
python的Crypto模块实现AES加密实例代码
Jan 22 Python
python 执行shell命令并将结果保存的实例
May 11 Python
利用Python写一个爬妹子的爬虫
Jun 08 Python
对python csv模块配置分隔符和引用符详解
Dec 12 Python
浅谈Python 多进程默认不能共享全局变量的问题
Jan 11 Python
Python之时间和日期使用小结
Feb 14 Python
翻转数列python实现,求前n项和,并能输出整个数列的案例
May 03 Python
Python实现动态循环输出文字功能
May 07 Python
超级实用的8个Python列表技巧
Aug 24 Python
Python通过yagmail实现发送邮件代码解析
Oct 27 Python
解决python3.x安装numpy成功但import出错的问题
Nov 17 Python
Django-silk性能测试工具安装及使用解析
Nov 28 Python
Python与shell的3种交互方式介绍
Apr 11 #Python
Python函数参数类型*、**的区别
Apr 11 #Python
Python中的多重装饰器
Apr 11 #Python
Python中的各种装饰器详解
Apr 11 #Python
将Django使用的数据库从MySQL迁移到PostgreSQL的教程
Apr 11 #Python
Python返回真假值(True or False)小技巧
Apr 10 #Python
Python选择排序、冒泡排序、合并排序代码实例
Apr 10 #Python
You might like
PHP高手需要要掌握的知识点
2014/08/21 PHP
php生成过去100年下拉列表的方法
2015/07/20 PHP
详解php语言最牛掰的Laravel框架
2017/11/20 PHP
Apply an AutoFormat to an Excel Spreadsheet
2007/06/12 Javascript
再次分享18个非常棒的jQuery表格插件
2011/04/10 Javascript
javascript判断ie浏览器6/7版本加载不同样式表的实现代码
2011/12/26 Javascript
Jquery 例外被抛出且未被接住原因介绍
2013/09/04 Javascript
Angularjs 基础入门
2014/12/26 Javascript
jQuery使用load()方法载入另外一个网页文件内的指定标签内容到div标签的方法
2015/03/25 Javascript
jquery实现的伪分页效果代码
2015/10/29 Javascript
JavaScript html5 canvas绘制时钟效果(二)
2016/03/27 Javascript
jQuery的ready方法实现原理分析
2016/10/26 Javascript
js实现图片360度旋转
2017/01/22 Javascript
Javarscript中模块(module)、加载(load)与捆绑(bundle)详解
2017/05/28 Javascript
在Js页面通过POST传递参数跳转到新页面详解
2017/08/25 Javascript
浅谈Vue SPA 首屏加载优化实践
2017/12/15 Javascript
详解JS模块导入导出
2017/12/20 Javascript
使用validate.js实现表单数据提交前的验证方法
2018/09/04 Javascript
layui 数据表格 点击分页按钮 监听事件的实例
2019/09/02 Javascript
vue中的计算属性和侦听属性
2020/11/06 Javascript
Python 错误和异常小结
2013/10/09 Python
Python实现微信中找回好友、群聊用户撤回的消息功能示例
2019/08/23 Python
关于numpy数组轴的使用详解
2019/12/05 Python
使用JS+CSS3技术:让你的名字动起来
2013/04/27 HTML / CSS
HTML5 placeholder属性详解
2016/06/22 HTML / CSS
新闻学专业应届生求职信
2013/11/08 职场文书
财务学生的职业生涯发展
2014/02/11 职场文书
节约用水倡议书
2014/04/16 职场文书
红色故事演讲稿
2014/05/22 职场文书
诚实守信演讲稿
2014/09/01 职场文书
党性锻炼的心得体会
2014/09/03 职场文书
2014年出纳工作总结与计划
2014/12/09 职场文书
2014年小学美术工作总结
2014/12/20 职场文书
2015年化工厂工作总结
2015/05/04 职场文书
建筑工程挂靠协议书
2016/03/23 职场文书
python cv2图像质量压缩的算法示例
2021/06/04 Python