初步探究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转码问题的解决方法
Oct 07 Python
Python写的英文字符大小写转换代码示例
Mar 06 Python
python3使用pyqt5制作一个超简单浏览器的实例
Oct 19 Python
Python编程实现粒子群算法(PSO)详解
Nov 13 Python
如何在python中使用selenium的示例
Dec 26 Python
在双python下设置python3为默认的方法
Oct 31 Python
在Qt中正确的设置窗体的背景图片的几种方法总结
Jun 19 Python
python使用yield压平嵌套字典的超简单方法
Nov 02 Python
解决python 读取 log日志的编码问题
Dec 24 Python
python序列类型种类详解
Feb 26 Python
python 实现分组求和与分组累加求和代码
May 18 Python
基于python requests selenium爬取excel vba过程解析
Aug 12 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下判断数组中是否存在相同的值array_unique
2008/03/25 PHP
功能齐全的PHP发送邮件类代码附详细说明
2008/07/10 PHP
PHP 第一节 php简介
2012/04/28 PHP
php制作中间带自己定义图片二维码的方法
2014/01/27 PHP
PHP中数组的分组排序实例
2014/06/01 PHP
php发送与接收流文件的方法
2015/02/11 PHP
Laravel手动分页实现方法详解
2016/10/09 PHP
thinkPHP自定义类实现方法详解
2016/11/30 PHP
PHP实现的简单异常处理类示例
2017/05/04 PHP
[原创]PHP global全局变量经典应用与注意事项分析【附$GLOBALS用法对比】
2019/07/12 PHP
IE和Mozilla的兼容性汇总event
2007/08/12 Javascript
jquery 框架使用教程 AJAX篇
2009/10/11 Javascript
JS面向对象基础讲解(工厂模式、构造函数模式、原型模式、混合模式、动态原型模式)
2014/08/16 Javascript
使用AngularJS实现可伸缩的页面切换的方法
2015/06/19 Javascript
js复制内容到剪贴板代码,js复制代码的简单实例
2016/10/27 Javascript
详解angular2采用自定义指令(Directive)方式加载jquery插件
2017/02/09 Javascript
jQuery插件HighCharts实现的2D面积图效果示例【附demo源码下载】
2017/03/15 Javascript
jQuery实现checkbox即点即改批量删除及中间遇到的坑
2017/11/11 jQuery
vue 权限认证token的实现方法
2018/07/17 Javascript
详解如何在vscode里面调试js和node.js的方法步骤
2018/12/24 Javascript
JS计算两个数组的交集、差集、并集、补集(多种实现方式)
2019/05/21 Javascript
微信接入之获取用户头像的方法步骤
2019/09/23 Javascript
JavaScript单线程和任务队列原理解析
2020/02/04 Javascript
如何基于js判断浏览器版本
2020/02/20 Javascript
原生js实现html手机端城市列表索引选择城市
2020/06/24 Javascript
[09:37]2018DOTA2国际邀请赛寻真——不懈追梦的Team Serenity
2018/08/13 DOTA
numpy自动生成数组详解
2017/12/15 Python
学习和使用python的13个理由
2019/07/30 Python
python 实现控制鼠标键盘
2020/11/27 Python
Oracle中delete,truncate和drop的区别
2016/05/05 面试题
怎样写好自我鉴定
2013/12/04 职场文书
端午节活动总结
2014/08/26 职场文书
教师节座谈会主持词
2015/07/03 职场文书
创业计划书之川味火锅店
2019/09/02 职场文书
react 项目中引入图片的几种方式
2021/06/02 Javascript
spring cloud gateway中如何读取请求参数
2021/07/15 Java/Android