函数机制,python虚拟机运行原理
分类:高并发

Python源码剖析笔记3-Python执行原理初探

Python的函数机制是很重要的部分,很多时候用python写脚本,就是几个函数简单解决问题,不需要像java那样必须弄个class什么的。

     近期为了面试想要了解下python的运行原理方面的东西,奈何关于python没有找到一本类似于深入理解Java虚拟机方面的书籍,找到了一本《python源码剖析》电子书,但是觉得相对来说最近还是不打算用大布头时间研究这本书,只能先找来几篇相关的博客来阅读,记录如下:

Python源码剖析笔记3-Python执行原理初探

本文简书地址:

之前写了几篇源码剖析笔记,然而慢慢觉得没有从一个宏观的角度理解python执行原理的话,从底向上分析未免太容易让人疑惑,不如先从宏观上对python执行原理有了一个基本了解,再慢慢探究细节,这样也许会好很多。这也是最近这么久没有更新了笔记了,一直在看源码剖析书籍和源码,希望能够从一个宏观层面理清python执行原理。人说读书从薄读厚,再从厚读薄方是理解了真意,希望能够达到这个境地吧,加了个油。

1 函数对象PyFunctionObject

PyFunctionObject对象的定义如下:

typedef struct {
    PyObject_HEAD
    PyObject *func_code;    /* A code object */
    PyObject *func_globals; /* A dictionary (other mappings won't do) */
    PyObject *func_defaults;    /* NULL or a tuple */
    PyObject *func_closure; /* NULL or a tuple of cell objects */
    PyObject *func_doc;     /* The __doc__ attribute, can be anything */
    PyObject *func_name;    /* The __name__ attribute, a string object */
    PyObject *func_dict;    /* The __dict__ attribute, a dict or NULL */
    PyObject *func_weakreflist; /* List of weak references */
    PyObject *func_module;  /* The __module__ attribute, can be anything */
} PyFunctionObject;

先说一下PyFunctionObject中几个重要的变量,func_code, func_globals。其中func_code是函数对象对应的PyCodeObject,而func_globals则是函数的global名字空间,其实这个值是从上一层PyFrameObject传递而来。func_defaults是存储函数默认值的,后面分析函数参数的时候会提到,func_closure与闭包相关,后面也会提到。

###func.py
def f():
  print "Function"

f()

如上面例子func.py,该文件编译后对应2个PyCodeObject对象,一个是func.py本身,一个是函数f。而PyFunctionObject则是在执行字节码def f():时通过MAKE_FUNCTION指令生成。创建PyFunctionObject对象时,会将函数f对应的PyCodeObject对象和当前PyFrameObject对象传入作为参数,最终也就是赋值给PyFunctionObject中的func_code和func_globals字段了。在调用函数时,会将PyFunctionObject对象传入到fast_function函数中,最终根据PyFunctionObject对象的func_code和func_globals字段构建新的栈帧对象PyFrameObject,然后调用PyEval_EvalFrameEx在新的栈帧中执行函数字节码。其中PyEval_EvalFrameEx函数在之前的Python执行原理中有提到过,当时提到的PyEval_EvalCodeEx函数其实也是创建了新的栈帧对象PyFrameObject然后执行PyEval_EvalFrameEx函数。

    一、过程概述

1 Python运行环境初始化

在看怎么执行之前,先要简单的说明一下python的运行时环境初始化。python中有一个解释器状态对象PyInterpreterState用于模拟进程(后面简称进程对象),另外有一个线程状态对象PyThreadState模拟线程(后面简称线程对象)。python中的PyInterpreterState结构通过一个链表链接起来,用于模拟操作系统多进程。进程对象中有一个指针指向线程集合,线程对象则有一个指针指向其对应的进程对象,这样线程和进程就关联了起来。当然,还少不了一个当前运行线程对象_PyThreadState_Current用来维护当前运行的线程。

2 函数调用栈帧

函数调用通过栈帧来建立关联,每个被调用函数的栈帧PyFrameObject会通过f_back指针指向调用函数。而local,global以及builtin名字空间,local名字空间针对新的栈帧是全新的,而global名字空间则是由创建PyFrameObject时从PyFunctionObject传递过来。builtin名字空间则是共享调用者栈帧的(如果该栈帧是初始栈帧,则会先获取builtin字典用于设置PyFrameObject的f_builtins字段)。

这里可以回顾一下C语言中的函数调用的栈帧关系。如下面的代码,对应的栈帧结构如图所示。在调用函数时,会先把函数参数会压入当前函数的栈帧中,每个函数都有自己的栈帧,由于esp会变化,所以其他函数会通过ebp来索引函数参数。

图片 1

图1 c语言函数栈帧

//函数调用栈帧测试代码func.c
int bar(int c, int d)
{
    int e = c + d;
    return e;
}

int foo(int a, int b)
{
    return bar(a, b);
}

int main(void)
{
    foo(2, 3);
    return 0;
}

那么python中是如何来模拟函数参数传递的呢?从C语言函数调用过程可以知道,函数调用前,函数参数会先压入到调用函数的栈帧中,而被调用函数则根据ebp来取参数。这里先回顾下PyFrameObject对象的结构,函数调用与PyFrameObject有着千丝万缕的联系。

typedef struct _frame {
    PyObject_VAR_HEAD
    struct _frame *f_back;  /* previous frame, or NULL */
    PyCodeObject *f_code;   /* code segment */
    PyObject *f_builtins;   /* builtin symbol table (PyDictObject) */
    PyObject *f_globals;    /* global symbol table (PyDictObject) */
    PyObject *f_locals;     /* local symbol table (any mapping) */
    PyObject **f_valuestack;    /* points after the last local */
    PyObject **f_stacktop;
    PyObject *f_trace;      /* Trace function */

    PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;

    PyThreadState *f_tstate;
    int f_lasti;        /* Last instruction if called */

    int f_lineno;       /* Current line number */
    int f_iblock;       /* index in f_blockstack */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
    PyObject *f_localsplus[1];  /* locals+stack, dynamically sized */
} PyFrameObject;

PyFrameObject对象中,f_valuestack指向运行时栈的栈底,而f_stacktop则是指向栈顶,在往运行时栈中压入函数参数时,f_stacktop会变化,这两个变量有点类似C里面的ebp和esp。f_localsplus则是指向局部变量+Cell对象+Free对象+运行时栈,其内存布局如图2所示。其中cell对象和free对象在闭包中用到,后面再看,这里主要说说局部变量和运行时栈。python在调用函数之前,会先将函数对象,函数参数压入到当前栈帧的运行时栈中,而在执行函数时,会新建一个PyFrameObject栈帧对象,然后将函数参数拷贝到新栈帧的存储局部变量的那块空间中(也就是f_localsplus执行的那块内存),接着才会调用PyEval_EvalFrameEx执行被调用函数的代码。

图片 2

图2 f_localsplus内存布局

看下面的func2.py,通过这个例子可以来看一下函数调用流程。例子代码和对应字节码如下。

#func2.py
def f(name, age):
    age += 5
    print '%s is %s old' % (name, age)
f('ssj', 18)

##字节码
In [1]: import dis

In [2]: source = open('func2.py').read()

In [3]: co = compile(source, 'func2.py', 'exec')

In [4]: dis.dis(co)
  1           0 LOAD_CONST               0 (<code object f at 0x10776faf8, file "func2.py", line 1>)
              3 MAKE_FUNCTION            0
              6 STORE_NAME               0 (f)

  4           9 LOAD_NAME                0 (f)
             12 LOAD_CONST               1 ('ssj')
             15 LOAD_CONST               2 (18)
             18 CALL_FUNCTION            2
             21 POP_TOP             
             22 LOAD_CONST               3 (None)
             25 RETURN_VALUE 

In [5]: dis.dis(co.co_consts[0])
  2           0 LOAD_FAST                1 (age)
              3 LOAD_CONST               1 (5)
              6 INPLACE_ADD         
              7 STORE_FAST               1 (age)
              ......

可以看到def f(name, age):跟之前说过的一样,字节码就是通过MAKE_FUNCTION指令创建PyFunctionObject对象并存储到local名字空间,对应的符号为函数名f,如果函数有默认参数,在MAKE_FUNCTION指令中还会设置默认参数到func_defaults字段。而准备调用函数时,则是先讲函数对象和函数参数执行函数时,会将函数参数压栈,然后才通过CALL_FUNCTION指令调用函数。在调用PyEval_EvalFrameEx执行函数代码前,创建新的栈帧后,会先将函数参数拷贝到f_localsplus指向的那片局部变量空间中,然后才真正执行函数f调用代码。执行函数f时,会将age参数压入栈然后加上5,然后存储到f_localsplus的第二个字段(第一个字段为name字符串"ssj")。函数参数位置变化如下图所示。

图片 3

图3 函数参数位置变化

1、python先把代码(.py文件)编译成字节码,交给字节码虚拟机,然后虚拟机会从编译得到的PyCodeObject对象中一条一条执行字节码指令,并在当前的上下文环境中执行这条字节码指令,从而完成程序的执行。Python虚拟机实际上是在模拟操作中执行文件的过程。PyCodeObject对象中包含了字节码指令以及程序的所有静态信息,但没有包含程序运行时的动态信息——执行环境(PyFrameObject)

1.1 进程线程初始化

python中调用PyInitialize()函数来完成运行环境初始化。在初始化函数中,会创建进程对象interp以及线程对象并在进程对象和线程对象建立关联,并设置当前运行线程对象为刚创建的线程对象。接下来是类型系统初始化,包括int,str,bool,list等类型初始化,这里留到后面再慢慢分析。然后,就是另外一个大头,那就是系统模块初始化。进程对象interp中有一个modules变量用于维护所有的模块对象,modules变量为字典对象,其中维护(name, module)对应关系,在python中对应着sys.modules。

3 函数执行时名字空间

还是看第一节中给的例子func.py,其对应的字节码如下,其实定义函数def f():就是用函数对应的PyCodeObject和栈帧对应的f_globals构建PyFunctionObject对象,然后通过STORE_NAME指令将PyFunctionObject对象与函数名f关联并存储到local名字空间。函数f对应的PyCodeObject可以通过co.co_consts[0]获取并查看。

In [1]: source = open('func.py').read()

In [2]: import dis

In [3]: co = dis.dis(source, 'func.py', 'exec')
 1           0 LOAD_CONST               0 (<code object f at 0x1107688a0, file "func.py", line 1>)
              3 MAKE_FUNCTION            0
              6 STORE_NAME               0 (f)

  4           9 LOAD_NAME                0 (f)
             12 CALL_FUNCTION            0
             15 POP_TOP             
             16 LOAD_CONST               1 (None)
             19 RETURN_VALUE  

In [10]: dis.dis(co.co_consts[0])
  2           0 LOAD_CONST               1 ('Function')
              3 PRINT_ITEM          
              4 PRINT_NEWLINE       
              5 LOAD_CONST               0 (None)
              8 RETURN_VALUE        

之前有说过python中分为local,global,builtin名字空间,函数执行时的名字空间略有不同。其global名字空间我们可以看到是通过PyFunctionObject从上一层栈帧传递来的,而local名字空间则是赋值为NULL,也就是函数中并没有用到local名字空间,那么问题来了,函数中的那些局部变量是怎么访问到的呢?那其实在函数中局部变量是通过LOAD_FAST指令(这个指令下一节会分析)来访问的,也就是说它访问的是f_localsplus的内存空间,不需要动态查找f_locals这个PyDictObject,静态的方法可以提供效率。

2、字节码在python虚拟机程序里对应的是PyCodeObject对象;

1.2 模块初始化

系统模块初始化过程会初始化 __builtin__, sys, __main__, site等模块。在python中,模块对象是以PyModuleObject结构体存在的,除了通用的对象头部,其中就只有一个字典字段md_dict。模块对象中的md_dict字段存储的内容是我们很熟悉的,比如__name__, __doc__等属性,以及模块中的方法等。

__builtin__模块初始化中,md_dict中存储的内容就包括内置函数以及系统类型对象,如len,dir,getattr等函数以及int,str,list等类型对象。正因为如此,我们才能在代码中直接用len函数,因为根据LEGB规则,我们能够在__builtin__模块中找到len这个符号。几乎同样的过程创建sys模块以及__main__模块。创建完成后,进程对象interp->builtins会被设置为__builtin__模块的md_dict字段,即模块对象中的那个字典字段。而interp->sysdict则是被设置为sys模块的md_dict字段。

sys模块初始化后,其中包括前面提到过的modules以及path,version,stdin,stdout,maxint等属性,exit,getrefcount,_getframe等函数。注意这里是设置了基本的sys.path(即python安装目录的lib路径等),第三方模块的路径是在site模块初始化的时候添加的。

需要说明的是,__main__模块是个特殊的模块,在我们写第一个python程序时,其中的__name__ == "__main__"中的__main__指的就是这个模块名字。当我们用python xxx.py运行python程序时,该源文件就可以当作是名为__main__的模块了,而如果是通过其他模块导入,则其名字就是源文件本身的名字,至于为什么,这个在后面运行一个python程序的例子中会详细说明。其中还有一点要说明的是,在创建__main__模块的时候,会在模块的字典中插入("__builtins__", __builtin__ module)对应关系。在后面可以看到这个模块特别重要,因为在运行时栈帧对象PyFrameObject的f_buitins字段就会被设置为__builtin__模块,而栈帧对象的locals和globals字段初始会被设置为__main__模块的字典。

另外,site模块初始化主要用来初始化python第三方模块搜索路径,我们经常用的sys.path就是这个模块设置的了。它不仅将site-packages路径加到sys.path中,还会把site-packages目录下面的.pth文件中的所有路径加入到sys.path中。

下面是一些验证代码,可以看到sys.modules中果然有了__builtin__, sys, __main__等模块。此外,系统的类型对象都已经位于__builtin__模块字典中。

In [13]: import sys

In [14]: sys.modules['__builtin__'].__dict__['int']
Out[14]: int

In [15]: sys.modules['__builtin__'].__dict__['len']
Out[15]: 

In [16]: sys.modules['__builtin__'].__dict__['__name__']
Out[16]: '__builtin__'

In [17]: sys.modules['__builtin__'].__dict__['__doc__']
Out[17]: "Built-in functions, exceptions, and other objects.nnNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices."

In [18]: sys.modules['sys']
Out[18]: 

In [19]: sys.modules['__main__']
Out[19]: 

好了,基本工作已经准备妥当,接下来可以运行python程序了。有两种方式,一种是在命令行下面的交互,另外一种是以python xxx.py的方式运行。在说明这两种方式前,需要先介绍下python程序运行相关的几个结构。

4 函数参数

Python中函数参数分为位置参数,键参数以及扩展位置参数和扩展键参数。位置参数就是之前我们例子中的参数,而键参数则是在调用函数指定参数的值。而扩展位置参数和扩展键参数格式则是类似*lst**kwargs。位置参数还能设置默认值,如果有默认值,默认值是在MAKE_FUNCTION指令赋值给func_defaults的。

下面的例子可以看到这几种参数的用法。扩展位置参数在python内部是通过一个元组对象存储的,不管最终传递了几个参数。而扩展键参数在python内部则是通过一个字典对象存储的。对于像 def f(a, b, *lst):这样的函数,如果调用函数时参数为f(1,2,3,4),其实在PyCodeObject对象中的co_argcount=2, co_nlocals=3。co_argcount是位置参数的个数,而co_nlocals是局部变量数目,包括位置参数在内。

##params1.py 位置参数和键参数
def f(a, b):
    print a, b
f(1, 2)
f(b=2, a=1)

##params2.py 位置参数,扩展位置参数,扩展键参数
def f(value, *lst, **kwargs):
    print value # -1
    print lst  # (1,2)
    print kwargs # {'a':3, 'b':4}
f(-1, 1, 2, a=3, b=4)

##params3.py 位置参数默认值
def f(lst = []):
    lst.append(3) 
    print lst 
f() #打印[3]
f() #打印[3,3]

最后还要提到的一点的是,函数参数默认值是在定义函数时设置的。如例子中的params3.py所示,如果指定了参数默认值,而调用函数时又没有覆盖默认值,则容易出现问题。要解决这个问题,可以在函数f中加个判断if lst: lst = []

     .pyc文件是字节码在磁盘上的表现形式。

1.3 Python运行相关数据结构

python运行相关数据结构主要由PyCodeObject,PyFrameObject以及PyFunctionObject。其中PyCodeObject是python字节码的存储结构,编译后的pyc文件就是以PyCodeObject结构序列化后存储的,运行时加载并反序列化为PyCodeObject对象。PyFrameObject是对栈帧的模拟,当进入到一个新的函数时,都会有PyFrameObject对象用于模拟栈帧操作。PyFunctionObject则是函数对象,一个函数对应一个PyCodeObject,在执行def test():语句的时候会创建PyFunctionObject对象。可以这样认为,PyCodeObject是一种静态的结构,python源文件确定,那么编译后的PyCodeObject对象也是不变的;而PyFrameObject和PyFunctionObject是动态结构,其中的内容会在运行时动态变化。

5 闭包和装饰器

之前提到过,PyCodeObject中有两个字段与闭包相关,分别是co_cellvars和co_freevars。其中co_cellvars通常是一个元组,里面保存的是嵌套作用域中使用的变量名集合,而co_freevars也通常是一个元组,里面保存的是外层作用域中的变量名集合。如下面这个闭包的例子,有三个PyCodeObject对象,closure.py本身,函数get_func以及inner_func分布对应一个PyCodeObject。其中get_func的PyCodeObject中的co_cellvars值是元组('value',),同时,inner_func的PyCodeObject的co_freevars存储的内容也是变量名value。

#closure.py 闭包
def get_func():
    value = "inner"
    def inner_func():
         print value
    return inner_func
show_value = get_func()
show_value()

可以看下get_func和inner_func的PyFrameObject的内存布局,就可以大致了解闭包的机制了。其实就是在外层函数的局部变量中存储内层嵌套函数inner_func的PyFunctionObject对象,而PyFunctionObject中的func_closure字段是一个存储PyCellObject的元组对象。在执行inner_func时,会先将func_closure中存储的PyCellObject对象拷贝到inner_func的PyFrameObject的free对象中,也就是cell对象后面那块存储空间,这样在inner_func中通过freevars引用到value了(注意,这个freevars不是inner_func的PyCodeObject中的co_freevars,而是PyFrameObject中的对应的内存区域,虽然他们的内容是一致的)。

图片 4

图5 闭包机制图示

装饰器是基于闭包实现的,可以对一个函数,方法,类进行加工,实现一些额外功能,在实际编码中会经常用到,比如检查用户是否登录,检查输入参数等,就可以用到装饰器来减少冗余代码。下面是一个装饰器的例子:

#decorator.py 装饰器
def wrapper(fn):
    def _wrapper():
        print 'wrapper '
        fn()
    return _wrapper

@wrapper
def func():
    print 'real func'

if __name__ == "__main__":
    func()  #输出'wrapper' 'real func'

更多装饰器介绍,参见vamei的这篇文章 Python深入05 装饰器

3、从整体上看:OS中执行程序离不开两个概念:进程和线程。python中模拟了这两个概念,模拟进程和线程的分别是PyInterpreterStatePyTreadState。即:每个PyThreadState都对应着一个帧栈,python虚拟机在多个线程上切换。当python虚拟机开始执行时,它会先进行一些初始化操作,最后进入PyEval_EvalFramEx函数,它的作用是不断读取编译好的字节码,并一条一条执行,类似CPU执行指令的过程。函数内部主要是一个switch结构,根据字节码的不同执行不同的代码。

PyCodeObject对象

python程序文件在执行前需要编译成PyCodeObject对象,每一个CodeBlock都会是一个PyCodeObject对象,在Python中,类,函数,模块都是一个Code Block,也就是说编译后都有一个单独的PyCodeObject对象,因此,一个python文件编译后可能会有多个PyCodeObject对象,比如下面的示例程序编译后就会存在2个PyCodeObject对象,一个对应test.py整个文件,一个对应函数test。关于PyCodeObject对象的解析,可以参见我之前的文章Python pyc格式解析,这里就不赘述了。

#示例代码test.py
def test():
    print "hello world"

if __name__ == "__main__":
    test()

6 参考资料

  • 《python源码剖析》 主要例子和原理都是参照本书
  • Python快速教程
  • 宋劲松 《Linux C语言一站式编程》

 

PyFrameObject对象

python程序的字节码指令以及一些静态信息比如常量等都存储在PyCodeObject中,运行时显然不可能只是操作PyCodeObject对象,因为有很多内容是运行时动态改变的,比如下面这个代码test2.py,虽然1和2处的字节码指令相同,但是它们执行结果显然是不同的,这些信息显然不能在PyCodeObject中存储,这些信息其实需要通过PyFrameObject也就是栈帧对象来获取。PyFrameObject对象中有locals,globals,builtins三个字段对应local,global,builtin三个名字空间,即我们常说的LGB规则,当然加上闭包,就是LEGB规则。一个模块对应的文件定义一个global作用域,一个函数定义一个local作用域,python自身定义了一个顶级作用域builtin作用域,这三个作用域分别对应PyFrameObject对象的三个字段,这样就可以找到对应的名字引用。比如test2.py中的1处的i引用的是函数test的局部变量i,对应内容是字符串“hello world”,而2处的i引用的是模块的local作用域的名字i,对应内容是整数123(注意模块的local作用域和global作用域是一样的)。需要注意的是,函数中局部变量的访问并不需要去访问locals名字空间,因为函数的局部变量总是不变的,在编译时就能确定局部变量使用的内存位置。

#示例代码test2.py
i = 123                                                                                                                                                       

def test():
  i = 'hello world'
  print i #1

test()
print i #2

     二、关于.pyc文件

PyFunctionObject对象

PyFunctionObject是函数对象,在创建函数的指令MAKE_FUNCTION中构建。PyFunctionObject中有个func_code字段指向该函数对应的PyCodeObject对象,另外还有func_globals指向global名字空间,注意到这里并没有使用local名字空间。调用函数时,会创建新的栈帧对象PyFrameObject来执行函数,函数调用关系通过栈帧对象PyFrameObject中的f_back字段进行关联。最终执行函数调用时,PyFunctionObject对象的影响已经消失,真正起作用的是PyFunctionObject的PyCodeObject对象和global名字空间,因为在创建函数栈帧时会将这两个参数传给PyFrameObject对象。

     PyCodeObject对象的创建时机是模块加载的时候,即import。

1.4 Python程序运行过程浅析

说完几个基本对象,现在回到之前的话题,开始准备执行python程序。两种方式交互式和直接python xxx.py虽然有所不同,但最终归于一处,就是启动虚拟机执行python字节码。这里以python xxx.py方式为例,在运行python程序之前,需要对源文件编译成字节码,创建PyCodeObject对象。这个是通过PyAST_Compile函数实现的,至于具体编译流程,这就要参看《编译原理》那本龙书了,这里暂时当做黑盒好了,因为单就编译这部分而言,一时半会也说不清楚(好吧,其实是我也没有学好编译原理)。编译后得到PyCodeObject对象,然后调用PyEval_EvalCode(co, globals, locals)函数创建PyFrameObject对象并执行字节码了。注意到参数里面的co是PyCodeObject对象,而由于运行PyEval_EvalCode时创建的栈帧对象是Python创建的第一个PyFrameObject对象,所以f_back为NULL,而且它的globals和locals就是__main__模块的字典对象。如果我们不是直接运行,而是导入一个模块的话,则还会将python源码编译后得到的PyCodeObject对象保存到pyc文件中,下次加载模块时如果这个模块没有改动过就可以直接从pyc文件中读取内容而不需要再次编译了。

执行字节码的过程就是模拟CPU执行指令的过程一样,先指向PyFrameObject的f_code字段对应的PyCodeObject对象的co_code字段,这就是字节码存储的位置,然后取出第一条指令,接着第二条指令…依次执行完所有的指令。python中指令长度为1个字节或者3个字节,其中无参数的指令长度是1个字节,有参数的指令长度是3个字节(指令1字节+参数2字节)。

python虚拟机的进程,线程,栈帧对象等关系如下图所示:

图片 5

1、执行 python test.py 会对test.py进行编译成字节码并解释执行,但不会生成test.pyc
2、如果test.py中加载了其他模块,如import urllib2,那么python会对urllib2.py进行编译成字节码,生成urllib2.pyc,然后对字节码解释执行。
3、如果想生成test.pyc,我们可以使用python内置模块py_compile来编译。
也可以执行命令 python -m test.py 这样,就生成了test.pyc
4、加载模块时,如果同时存在.py和.pyc,python会使用.pyc运行,如果.pyc的编译时间早于.py的时间,则重新编译.py,并更新.pyc文件。

2 Python程序运行实例说明

程序猿学习一门新的语言往往都是从hello world开始的,一来就跟世界打个招呼,因为接下来就要去面对程序语言未知的世界了。我学习python也是从这里开始的,只是以前并不去深究它的执行原理,这回是逃不过去了。看看下面的栗子。

#示例代码test3.py
i = 1
s = 'hello world'

def test():
    k = 5
    print k
    print s

if __name__ == "__main__":
    test()

这个例子代码不多,不过也涉及到python运行原理的方方面面(除了类机制那一块外,类机制那一块还没有理清楚,先不理会)。那么按照之前部分说的,执行python test3.py的时候,会先初始化python进程和线程,然后初始化系统模块以及类型系统等,然后运行python程序test3.py。每次运行python程序都是开启一个python虚拟机,由于是直接运行,需要先编译为字节码格式,得到PyCodeObject对象,然后从字节码对象的第一条指令开始执行。因为是直接运行,所以PyCodeObject也就没有序列化到pyc文件保存了。下面可以看下test3.py的PyCodeObject,使用python的dis模块可以看到字节码指令。

In [1]: source = open('test3.py').read()

In [2]: co = compile(source, 'test3.py', 'exec')

In [3]: co.co_consts
Out[3]: 
(1,
 'hello world',
 ,
 '__main__',
 None)

In [4]: co.co_names
Out[4]: ('i', 's', 'test', '__name__')

In [5]: dis.dis(co) ##模块本身的字节码,下面说的整数,字符串等都是指python中的对象,对应PyIntObject,PyStringObject等。
  1           0 LOAD_CONST               0 (1) # 加载常量表中的第0个常量也就是整数1到栈中。
              3 STORE_NAME               0 (i) # 获取变量名i,出栈刚刚加载的整数1,然后存储变量名和整数1到f->f_locals中,这个字段对应着查找名字时的local名字空间。

  2           6 LOAD_CONST               1 ('hello world') 

              9 STORE_NAME               1 (s)  #同理,获取变量名s,出栈刚刚加载的字符串hello world,并存储变量名和字符串hello world的对应关系到local名字空间。

  4          12 LOAD_CONST               2 ()
             15 MAKE_FUNCTION            0   #出栈刚刚入栈的函数test的PyCodeObject对象,以code object和PyFrameObject的f_globals为参数创建函数对象PyFunctionObject并入栈
             18 STORE_NAME               2 (test)  #获取变量test,并出栈刚入栈的PyFunctionObject对象,并存储到local名字空间。

  9          21 LOAD_NAME                3 (__name__) ##LOAD_NAME会先依次搜索local,global,builtin名字空间,当然我们这里是在local名字空间能找到__name__。
             24 LOAD_CONST               3 ('__main__')
             27 COMPARE_OP               2 (==)  ##比较指令
             30 JUMP_IF_FALSE           11 (to 44) ##如果不相等则直接跳转到44对应的指令处,也就是下面的POP_TOP。因为在COMPARE_OP指令中,会设置栈顶为比较的结果,所以需要出栈这个比较结果。当然我们这里是相等,所以接着往下执行33处的指令,也是POP_TOP。
             33 POP_TOP             

 10          34 LOAD_NAME                2 (test) ##加载函数对象
             37 CALL_FUNCTION            0  ##调用函数
             40 POP_TOP                     ##出栈函数返回值
             41 JUMP_FORWARD             1 (to 45) ##前进1步,注意是下一条指令地址+1,也就是44+1=45
        >>   44 POP_TOP             
        >>   45 LOAD_CONST               4 (None) 
             48 RETURN_VALUE     #返回None


In [6]: dis.dis(co.co_consts[2])  ##查看函数test的字节码
  5           0 LOAD_CONST               1 (5)
              3 STORE_FAST               0 (k) #STORE_FAST与STORE_NAME不同,它是存储到PyFrameObject的f_localsplus中,不是local名字空间。

  6           6 LOAD_FAST                0 (k) #相对应的,LOAD_FAST是从f_localsplus取值
              9 PRINT_ITEM          
             10 PRINT_NEWLINE         #打印输出 

  7          11 LOAD_GLOBAL              0 (s) #因为函数没有使用local名字空间,所以,这里不是LOAD_NAME,而是LOAD_GLOBAL,不要被名字迷惑,它实际上会依次搜索global,builtin名字空间。
             14 PRINT_ITEM          
             15 PRINT_NEWLINE       
             16 LOAD_CONST               0 (None)
             19 RETURN_VALUE        

按照我们前面的分析,test3.py这个文件编译后其实对应2个PyCodeObject,一个是本身test3.py这个模块整体的PyCodeObject,另外一个则是函数test对应的PyCodeObject。根据PyCodeObject的结构,我们可以知道test3.py字节码中常量co_consts有5个,分别是整数1,字符串‘hello world’,函数test对应的PyCodeObject对象,字符串__main__,以及模块返回值None对象。恩,从这里可以发现,其实模块也是有返回值的。我们同样可以用dis模块查看函数test的字节码。

关于字节码指令,代码中做了解析。需要注意到函数中局部变量如k的取值用的是LOAD_FAST,即直接从PyFrameObject的f_localsplus字段取,而不是LOAD_NAME那样依次从local,global以及builtin查找,这是函数的特性决定的。函数的运行时栈也是位于f_localsplus对应的那片内存中,只是前面一部分用于存储函数参数和局部变量,而后面那部分才是运行时栈使用,这样逻辑上运行时栈和函数参数以及局部变量是分离的,虽然物理上它们是连在一起的。需要注意的是,python中使用了预测指令机制,比如COMPARE_OP经常跟JUMP_IF_FALSE或JUMP_IF_TRUE成对出现,所以如果COMPARE_OP的下一条指令正好是JUNP_IF_FALSE,则可以直接跳转到对应代码处执行,提高一定效率。

此外,还要知道在运行test3.py的时候,模块的test3.py栈帧对象中的f_locals和f_globals的值是一样的,都是__main__模块的字典。在test3.py的代码后面加上如下代码可以验证这个猜想。

 ... #test3.py的代码

if __name__ == "__main__":
    test()
    print locals() == sys.modules['__main__'].__dict__ # True
    print globals() == sys.modules['__main__'].__dict__ # True
    print globals() == locals() # True

正式因为如此,所以python中函数定义顺序是无关的,不需要跟C语言那样在调用函数前先声明函数。比如下面test4.py是完全正常的代码,函数定义顺序不影响函数调用,因为在执行def语句的时候,会执行MAKE_FUNCTION指令将函数对象加入到local名字空间,而local和global此时对应的是同一个字典,所以也相当于加入了global名字空间,从而在运行函数g的时候是可以找到函数f的。另外也可以注意到,函数声明和实现其实是分离的,声明的字节码指令在模块的PyCodeObject中执行,而实现的字节码指令则是在函数自己的PyCodeObject中。

#test4.py
def g():                                                                                                                                                     
  print 'function g'
  f() 

def f():
  print 'function f'

g()
~      

Python源码剖析笔记3-Python执行原理初探 本文简书地址: 之前写了几篇源...

 

     三、关于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;

 

     四、执行字节码

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

                           图片 6

 

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

const char *s = “hello”;

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

int main() {
    func();
    return 0;
}

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

PyFrameObject对象创建程序运行时的动态信息,即执行环境,相关源码大致如下:

typedef struct _frame{  
    PyObject_VAR_HEAD //"运行时栈"的大小是不确定的  
    struct _frame *f_back; //执行环境链上的前一个frame,很多个PyFrameObject连接起来形成执行环境链表  
    PyCodeObject *f_code; //PyCodeObject 对象,这个frame就是这个PyCodeObject对象的上下文环境  
    PyObject *f_builtins; //builtin名字空间  
    PyObject *f_globals;  //global名字空间  
    PyObject *f_locals;   //local名字空间  
    PyObject **f_valuestack; //"运行时栈"的栈底位置  
    PyObject **f_stacktop;   //"运行时栈"的栈顶位置  
    //...  
    int f_lasti;  //上一条字节码指令在f_code中的偏移位置  
    int f_lineno; //当前字节码对应的源代码行  
    //...  

    //动态内存,维护(局部变量+cell对象集合+free对象集合+运行时栈)所需要的空间  
    PyObject *f_localsplus[1];    
} PyFrameObject; 

每一个 PyFrameObject对象都维护了一个 PyCodeObject对象,这表明每一个 PyFrameObject中的动态内存空间对象都和源代码中的一段Code相对应。

 

 

 

参考博客:

本文由10bet手机官网发布于高并发,转载请注明出处:函数机制,python虚拟机运行原理

上一篇:Linux命令之文本处理,命令行文本处理工具 下一篇:没有了
猜你喜欢
热门排行
精彩图文