python 程序执行原理

 Python是一门解释性语言,它的具体工作流程如下:
 
 - 编译,形成.pyc或.pyo后缀的语言
 - 放入解释器,解释器执行字节流(opecode)
 
和java字节码一样,他们都是基于栈进行解释的。

分析pyc文件

  • 一个 pyc 文件包含了三部分信息:Python 的 magic number、pyc 文件创建的时间信息,以及 PyCodeObject 对象。

前四个字节为magic,这个和python版本相关
然后四个字节为编译时间,后面为code_type(PycodeObject)。
而在3.4及以后则在编译时间之后添加一个filesize。

magic number

Python 定义的一个整数值。一般来说,不同版本的 Python 实现都会定义不同的 magic number,这个值是用来保证 Python 兼容性的。比如要限制由低版本编译的 pyc 文件不能让高版本的 Python 程序来执行,只需要检查 magic number 不同就可以了。由于不同版本的 Python 定义的字节码指令可能会不同,如果不做检查,执行的时候就可能出错。

所以python还有一个解释器版本的问题。

下面所示的代码可以来创建 pyc 文件,使用方法

python generate_pyc.py module_name

为什么 pyc 文件也称作字节码文件?因为这些文件存储的都是一些二进制的字节数据,而不是能让人直观查看的文本数据。

Python 标准库提供了用来生成代码对应字节码的工具 dis。dis 提供一个名为 dis 的方法,这个方法接收一个 code 对象,然后会输出 code 对象里的字节码指令信息。

s = open('demo.py').read()
co = compile(s, 'demo.py', 'exec')
import dis
dis.dis(co)

执行上面这段代码可以输出 demo.py 编译后的字节码指令

 1           0 LOAD_CONST               0 (-1)
              3 LOAD_CONST               1 (None)
              6 IMPORT_NAME              0 (foo)
              9 STORE_NAME               0 (foo)

  3          12 LOAD_CONST               2 (1)
             15 LOAD_CONST               3 (u'python')
             18 BUILD_LIST               2
             21 STORE_NAME               1 (a)

  4          24 LOAD_CONST               4 (u'a string')
             27 STORE_NAME               1 (a)

  6          30 LOAD_CONST               5 ()
             33 MAKE_FUNCTION            0
             36 STORE_NAME               2 (func)

 11          39 LOAD_NAME                1 (a)
             42 PRINT_ITEM          
             43 PRINT_NEWLINE       

 13          44 LOAD_NAME                3 (__name__)
             47 LOAD_CONST               6 (u'__main__')
             50 COMPARE_OP               2 (==)
             53 POP_JUMP_IF_FALSE       82

 14          56 LOAD_NAME                2 (func)
             59 CALL_FUNCTION            0
             62 POP_TOP             

 15          63 LOAD_NAME                0 (foo)
             66 LOAD_ATTR                4 (add)
             69 LOAD_CONST               2 (1)
             72 LOAD_CONST               7 (2)
             75 CALL_FUNCTION            2
             78 POP_TOP             
             79 JUMP_FORWARD             0 (to 82)
        >>   82 LOAD_CONST               1 (None)
             85 RETURN_VALUE

Python 虚拟机

demo.py 被编译后,接下来的工作就交由 Python 虚拟机来执行字节码指令了。Python 虚拟机会从编译得到的 PyCodeObject 对象中依次读入每一条字节码指令,并在当前的上下文环境中执行这条字节码指令。我们的程序就是通过这样循环往复的过程才得以执行。

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;

一个被解析的pyc文件如下:

magic 03f30d0a
moddate aa813e59 (Mon Jun 12 19:57:30 2017)
code
   argcount 0
   nlocals 0
   stacksize 1
   flags 0040
   code 640000474864010053      
   consts
      'hello world!'
      None
   names ()
   varnames ()
   freevars ()
   cellvars ()
   filename 'C:\\Users\\Administrator\\Desktop\\test3.py'
   name '<module>'
   firstlineno 1
   lnotab 

在 Python 的世界中,一切都是对象,函数也是对象,类型也是对象,类也是对象(类属于自定义的类型,在 Python 2.2 之前,int, dict 这些内置类型与类是存在不同的,在之后才统一起来,全部继承自 object),甚至连编译出来的字节码也是对象,.pyc 文件是字节码对象(PyCodeObject)在硬盘上的表现形式。在运行期间,编译结果也就是 PyCodeObject 对象,只会存在于内存中,而当这个模块的 Python 代码执行完后,就会将编译结果保存到了 pyc 文件中,这样下次就不用编译,直接加载到内存中。

这个 PyCodeObject 对象包含了 Python 源代码中的字符串,常量值,以及通过语法解析后编译生成的字节码指令。PyCodeObject 对象还会存储这些字节码指令与原始代码行号的对应关系,这样当出现异常时,就能指明位于哪一行的代码。

python基于栈的运行机制,python的运行是单纯模拟cpu运行的机制
看一下它的堆结构

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;    /* 运行时栈顶 */
    …….

opcode又称为操作码,是将python源代码进行编译之后的结果,python虚拟机无法直接执行human-readable的源代码,因此python编译器第一步先将源代码进行编译,以此得到opcode。例如在执行python程序时一般会先生成一个pyc文件,pyc文件就是编译后的结果,其中含有opcode序列。

结果输出内容如下,其中LOAD_CONST,STORE_FAST,BINARY_ADD即是我们提到的opcode,python是基于栈的语言,LOAD_CONST是将常量进行压栈
python代码在编译完成后在内存中的对象称为PyCodeObject,PyCodeObject的C定义
我们关心co_consts和co_names两个列表,第一个存放了所有的常量,第二存放了所有的变量,因此有下面的结论。
LOAD_CONST 0 表示将co_consts中的第0个(下标0)放入栈中。
STORE_FAST 0 表示将栈顶元素赋值给co_names中存放的第0个元素。

python的目标不是一个性能高效的语言,出于脚本动态类型的原因虚拟机做了大量计算来判断一个变量的当前类型,并且整个python虚拟机是基于栈逻辑的,频繁的压栈出栈操作也需要大量计算。动态类型变化导致python的性能优化非常困难

对比优化前后的结果可以发现,优化后明显减少了指令的数量

Python代码[test.py]

s = ”hello”

def func():
    print s

func()

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’

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’

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

opcode oparg opcode opcode oparg …

1 byte 2 bytes 1 byte 1 byte 2 bytes

执行字节码

Python虚拟机的原理就是模拟可执行程序再X86机器上的运行
Python虚拟机的原理就是模拟上述行为。当发生函数调用时,创建新的栈帧,对应Python的实现就是PyFrameObject对象。

猜你喜欢

转载自blog.csdn.net/zhc_24/article/details/82119543
今日推荐