python是一门动态解释型语言。为了理解"动态"和"解释",前几天都在看《Python源码剖析》,以下是自己的一些总结。

先说解释,除开py2exe等不说(因为我不了解),python脚本需要交由python虚拟机去解释执行,当然大家都知道里面还有一个编译的过程,python虚拟机解释的只是编译后的字节码,而源代码的解释工作由python的编译模块完成。那么字节码是怎么解释执行的呢?我以为会很优雅,但实际上只是一个很长的switch-case,有1711行那么长(Python-2.7.5,参见ceval.c)。python虚拟机简单模拟了x86的堆栈模型,我当初认为她只是设置函数调用时堆栈的参数,然后通过内联汇编直接调用函数。也搞不懂当时是怎么陷入这种思维的了,可能是认为这样速度更快吧。但实际上不是的,那一段switch-case是真当自己是cpu了,对每一个指令执行固定的操作,执行的环境就是堆栈。例如:

def func(arg):
    print("hello world %s" % arg)

if __name__ == '__main__':
    func('.')

如果说每个函数都有一个属于自己的栈帧,那么在进入print的时候,python虚拟机总共创建了3个栈帧。但可惜,这并不是c代码,而是python代码,想一下,最底层,也就是print函数内所执行的操作实现上是用c编写的,再细想,其实创建栈帧,在栈帧中传递参数也是一个个用c编写好的函数,所以这里实际上真正栈帧的数量比3要大得多。python表面上的3次函数调用实际上会转换成多个c函数调用,这就是解释型语言慢的一大原因了。

print最后是怎么执行的呢?恩,有一个字节码叫PRINT_ITEM,然后就会调用python内置的打印函数。那func又是如何调用的呢?恩,有一个字节码叫CALL_FUNCTION,专门对付非内置的python函数。必须说明的是,非内置函数分为两种:c函数和python函数,前者在python内部都被封装成PyCFunctionObject(对象类型是PyCFunction_Type),后者被封装成PyFunctionObject(对象类型是PyFunction_Type),可以想象在CALL_FUNCTION中,前者最后会调用一个c函数,而后者不过是开始新一轮的字节码执行罢了。那怎么结束呢?没关系,有表示控制流的字节码,例如RETURN_VALUE。

然后来说动态。所谓动态是什么呢?我的理解是程序运行时能知道对象的类型,也只有在运行时才能知道对象的类型。例如上面的func函数,arg是什么定义类型的,会传入什么样的参数程序(在func函数的作用域内)一无所知。但是,参数一旦传入,程序能通过参数来找到参数对应的类型,从而判断能否进行print操作。那么,对象本身必须包含对应的类型信息(以下称元信息),源代码实现是这样的:

typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

    /* Methods to implement standard operations */
   ...
} PyTypeObject;

#define PyObject_HEAD                   \
    _PyObject_HEAD_EXTRA                \
    Py_ssize_t ob_refcnt;               \
    struct _typeobject *ob_type;

typedef struct _object {
    PyObject_HEAD
} PyObject;

首先,‘对象’在python内部的表示就是PyObject结构类型(或者说所有对象的基类都是PyObject),而‘对象元信息‘的表示为PyTypeObject结构类型。可以看到PyObject内有指向PyTypeObject的指针ob_type,在对象建立之前,对象元信息必然已经存在(不然怎么知道那个对象需要多大的内在空间呢),当对象创建时,就会把元信息结构体的地址赋值给ob_type。恩,“必然已经存在”,怎么个必然法啊?内置对象当然简单,毕竟已经用c写在代码里了。但如果是用户自定义的呢?当然是从编译完的字节码里来啊。好厉害呢,编译完内存里就有这个对象的存在了。别傻了,编译完只会得到一堆字节码罢了。无论是'对象',还是'对象元信息'都是c结构体,最终这些结构体都要以传统的二进制存放在内存里。传统的c程序是放在代码段,但现在我们只能放在堆区了。所以,应该想的是,字节码的运行过程中是怎样得到对象的元信息。

这里暂停一下,自定义对象是包括函数的,别忘了一切都是对象。函数不像普通的类有基类、数据属性、函数属性等东西,所以函数对象和类对象是分开的表示。而在python的实现中,创建一个函数和创建一个普通的类是不同的操作。创建函数对应的字节码是MAKE_FUNCTION,创建的是一个PyFunctionObject对象,而它的对象元信息也是写死的PyFunction_Type,注意这是一个结构体,而不是一个结构体类型。创建的过程基本上就是把那一个函数编译得到的字节码对象(是的,字节码在内部也被封装成一个对象)的地址赋值给PyFunction_Type里的func_code罢了。好吧,创建了之后函数对象还能放在栈上吗?不行,因为栈的空间是会被复用的,所以必须把放到另外的地方。那个地方叫运行时栈上(Python源码剖析是这么叫的),存放的是map类型的信息(python叫dict类型),例如上而的func创建之后在运行时栈就会有一个映射["func" : <func obj>]。以后就可以通过"func"这个字符串来找到对应的函数对象了。

而创建一个类就不是那么简单了。例如:

class A:
    name = "Class A"
    def ChangeName(self, new_name):
        self.name = new_name
    def PrintName(self):
        print("self.name: %s, class.name: %s" % (self.name, A.name))

就结果而言,当得到类A的元信息后,运行时栈会有一个映射["A" : <class_A>],其中class_A对应的类型依然是PyTypeObject。class_A中会包含一个dict(就是PyTypeObject里的tp_dict),"name", "ChangeName", "PrintName"会映射到相应的对象上。class_A也会包含用于创建实例的方法,创建实例最主要就是分配内存,那么,要分配多大的内存呢?或者说占用内存的是什么?是属于实例自身属性,也就是也以'self.'开头的变量。属于类的全局属性包含在一个dict里,和这种方法类似,属于实例的属性也包含在一个dict里面,所以无论是怎样的自定义类型,一个指针的大小就够了。当然,指针指向的空间在用到的时候也是要分配的,python使用的策略是在第一次用到的时候才真正分配,也就是类似:

if (dict == NULL)
    dict = PyDict_New();
PyDict_SetItem(dict, name, value);

 现在转过头去,稍微看一下class_A是怎样得到的。其实也是字节码运行的结果,所以逐行代码分析的话不如看代码或者《Python源码剖析》。大致流程是:

1. MAKE_FUNCTION,把'class A:'对应的字节码组织成一个func obj;

2. CALL_FUNCTION,执行上面的func obj,执行完后,运行时栈从栈顶开始依次是类A名字、基类列表及属性映射列表。

3. BUILD_CLASS,将上面所说的三样东西传递给build_class,将会生成一个A对应的PyTypeObject,也就是class_A,其中最关键的函数是type_new,可以想象,这里面涉及到为class_A分配内存,设置各个字段的值,例如将属性映射表放到tp_dict里之类的;

4. STORE_NAME,将"A"和class_A建立映射。

断断续续写了两三天,写得可能也不够清楚明了,如有疑问不妨自己去看一下《Python源码剖析》,毕竟我也只是挑了自己感兴趣的部分看了一下(并且对看不懂的作了自动性过滤:))。大家中秋节快乐。

 posted on 2013-09-19 02:51  万事屋madao  阅读(1629)  评论(0编辑  收藏  举报