咱们平常会写各类各样的python脚本,在运行的时候只须要输入python xxx.py
程序就执行了。那么问题就来了,一个py文件是如何被python变成一系列的机器指令并执行的呢?python
python的执行原理能够用两个词来囊括:虚拟机、字节码数组
首先在python中有一个很是关键的东西,这个东西被称为解释器(interpreter),当咱们在命令行中输入python时,就是为了激活这个解释器。固然若是后面还跟上了py文件,那么解释器会马上被激活,而后执行py文件里面的代码。然而在真正开始执行以前,解释器实际上还要完成一个很是复杂的工做--编译py文件闭包
没错,python虽然是解释型语言,但也是有编译的过程的。不管执行哪个py文件,首先都是对源代码进行编译,编译成一组python的字节码(byte code),而后将编译的字节码交给python的虚拟机(virtual machine),而后由虚拟机按照顺序一条一条地执行字节码,从而完成对python执行动做。关于虚拟机和解释器的区别,我的以为在python中能够认为解释器 = 编译器 + 虚拟机。不要误会这里的编译器,这只是编译成python中的字节码,可不是C语言中直接编译成机器码的解释器。app
那么这个python的编译器和虚拟机藏身于什么地方呢?咱们打开python的安装目录,会看到一个python.exe,点击的时候确实能启动一个终端,可是这个文件大小还不到100K,不可能容纳一个解释器加一个虚拟机。可是事实上你会发现,下面还有一个python37.dll,没错,编译器、虚拟机都藏身于python37.dll当中。ide
咱们来看一个简单的例子,来看看一个py文件被编译以后应该产生一些什么结果函数
class A: pass def foo(): pass a = A() foo()
首先咱们知道,python执行这个文件首先要进行的动做就是编译,编译的结果是字节码。然而除了字节码以外,还应该包含一些其余的信息,这些结果也python运行的时候所必须的。lua
在编译过程当中,像常量值、字符串这些源代码当中的静态信息都会被python编译器收集起来,这些静态信息都会体如今编译以后的结果里面。在python运行期间,这些源文件提供的静态信息都会被存储在一个运行时的对象当中,当python运行结束时,这个运行时对象中所包含的信息还会被存储在一种文件中。这个对象和文件就是咱们接下来要探讨的重点:PyCodeObject对象和pyc文件spa
咱们知道编译的结果是一个pyc文件,可是里面的内容是PyCodeObject对象,对于python编译器来讲,PyCodeObject对象才是其真正的编译结果,而pyc文件是这个对象在硬盘上表现形式。所以它们其实是python对源代码编译以后的两种不一样的存在形式。命令行
在程序运行期间,编译结果存在于内存的PyCodeObject对象当中,而python结束运行以后,编译结果又被保存到了pyc文件当中。当下一次运行的时候,python会根据pyc文件中记录的编译结果直接创建内存中的PyCodeObject对象,而不须要再度从新编译了。debug
关于python是如何编译py文件的,这个咱们不作介绍,由于这还涉及分词、创建语法树等等,咱们的重点是编译以后的结果。要完全理解python虚拟机的运行时行为,就必须完全理解PyCodeObject对象。
/* Bytecode object */ typedef struct { PyObject_HEAD int co_argcount; /* #arguments, except *args */ int co_kwonlyargcount; /* #keyword only arguments */ int co_nlocals; /* #local variables */ int co_stacksize; /* #entries needed for evaluation stack */ int co_flags; /* CO_..., see below */ int co_firstlineno; /* first source line number */ PyObject *co_code; /* instruction opcodes */ PyObject *co_consts; /* list (constants used) */ PyObject *co_names; /* list of strings (names used) */ PyObject *co_varnames; /* tuple of strings (local variable names) */ PyObject *co_freevars; /* tuple of strings (free variable names) */ PyObject *co_cellvars; /* tuple of strings (cell variable names) */ /* The rest aren't used in either hash or comparisons, except for co_name, used in both. This is done to preserve the name and line number for tracebacks and debuggers; otherwise, constant de-duplication would collapse identical functions/lambdas defined on different lines. */ Py_ssize_t *co_cell2arg; /* Maps cell vars which are arguments. */ PyObject *co_filename; /* unicode (where it was loaded from) */ PyObject *co_name; /* unicode (name, for reference) */ PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) See Objects/lnotab_notes.txt for details. */ void *co_zombieframe; /* for optimization only (see frameobject.c) */ PyObject *co_weakreflist; /* to support weakrefs to code objects */ /* Scratch space for extra data relating to the code object. Type is a void* to keep the format private in codeobject.c to force people to go through the proper APIs. */ void *co_extra; } PyCodeObject;
这里面的每个域表明什么,能够暂时无需理会,咱们后面会一一介绍,可是里面有一个co_code能够提早剧透一下,这个域存放的是编译所生成的字节码指令序列。
python编译器在对python源代码进行编译的时候,对于代码中的每个block,都会建立一个PyCodeObject与之对应,但如何肯定多少代码才算是一个block呢?事实上,python有一个简单而清晰的规则:当进入一个新的名字空间,或者说做用域时,咱们就算是进入了一个新的block了。
回顾以前建立的py文件,在编译完以后会有三个PyCodeObject对象,一个是对应整个py文件的,一个是对应class A的,一个是对应def foo的。
在这里,咱们开始说起python中一个相当重要的概念--命名空间(name space)。命名空间是符号的上下文环境,符号的含义取决于命名空间。更具体的说,一个变量名对应的变量值什么,在python中是不肯定的,须要命名空间来决定。
对于某个符号,好比说a,在某个命名空间中,它多是一个PyLongObject对象;而在另外一个命名空间中,它多是一个PyListObject对象。可是在一个命名空间中,一个符号只能有一种含义。并且命名空间能够一层套一层的造成一条命名空间链
,python虚拟机在执行的时候,会有很大一部分时间消耗在从命名空间链
中肯定一个符号所对应的对象是什么。这也侧面说明了,为何python在建立变量的时候不须要指定类型、以及python为何比较慢。
若是如今命名空间还不是很了解,没关系,随着剖析的深刻,你必定会对命名空间和python在命名空间链上的行为有愈来愈深入的理解。总之如今须要记住的是:一个code block对应一个命名空间、同时也对应一个PyCodeObject对象。在python中,类、函数、module都对应着一个独自的命名空间,所以都会有一个PyCodeObject与之对应。
每个PyCodeObject对象中都包含了每个code block中全部代码通过编译后获得的byte code序列。前面咱们说到,python会将字节码序列和PyCodeObject对象一块儿存储在pyc文件中。但不幸的是,事实并不老是这样。有时,当咱们运行一个简单的程序时并无产生pyc文件,所以咱们猜想:有些python程序只是临时完成一些琐碎的工做,这样的程序仅仅只会运行一次,而后就不会再使用了,所以也就没有保存至pyc文件的必要。
若是咱们在代码中加上了一个import abc这样语句,再执行你就会发现python为其生成了pyc文件,这就说明import会触发pyc的生成。实际上,在运行过程当中,若是碰到import abc这样的语句,那么python会在设定好的path中寻找abc.pyc或者abc.dll文件,若是没有这些文件,而是只发现了abc.py,那么python会先将abc.py编译成PyCodeObject,而后建立pyc文件,并将PyCodeObject写到pyc文件里面去。接下来,再对abc.pyc进行import动做,对,并非编译成PyCodeObject对象以后直接使用,而是先写到pyc里面去,而后将pyc文件的PyCodeObject对象从新在内存中复制出来。
关于python的import机制,咱们后面章节会剖析,这里只是用来完成pyc文件的触发。固然获得pyc文件有不少方法,好比使用py_compile模块。
a.py
class A: a = 1
b.py
import a
执行b.py的时候,会发现建立了a.pyc。另外关于pyc文件的建立位置,会在当前文件的同级目录下的__pycache__
目录中建立,名字就叫作,py文件名.cpython-版本号.pyc
咱们使用notepad++打开,以二进制的方式查看一下,发现了一堆看上去毫无心义的数字。
所以python如何解释这些字节流就相当重要了,这也是咱们关心的pyc文件的格式。
要了解pyc文件的格式,就必须清楚PyCodeObject对象中的每个域都表明什么含义,这一点是不管如何也绕不过去的。
回顾一下,PyCodeObject结构体的定义:
PyObject_HEAD:真的是一切皆对象,字节码也是一个对象,那么天然要求PyObject这些头部信息
co_argcount:位置参数个数
import inspect frame = None def foo(a, b, c, d): global frame frame = inspect.currentframe() foo(1, 2, 3, 4) # frame是这个函数的栈帧,这个栈帧先不用管 # 总之咱们再frame.f_code就能够拿到字节码,这个字节码就是底层的PyCodeObject对象 # 咱们再调用一个co_argcount就能够拿到位置参数的个数 print(frame.f_code.co_argcount) # 4
co_kwonlyargcount:只能用关键字参数的个数
def foo(a, b, *, c, d): global frame frame = inspect.currentframe() foo(1, 2, c=3, d=4) print(frame.f_code.co_argcount) # 2 print(frame.f_code.co_kwonlyargcount) # 2 # 咱们注意到c和d只能使用关键字参数传递,因此是2个 # 若是定义的时候不加*,即便你在在调用的时候都是使用关键字参数传递,也没有用 # 都是属于位置参数。 # co_kwonlyargcount是只能用关键字参数传递的个数
co_nlocals:代码块中局部变量的个数,也包括参数
import inspect frame = None def foo(a, b, *, c): name = "xxx" age = 16 gender = "f" c = 33 global frame frame = inspect.currentframe() foo(1, 2, c=3) print(frame.f_code.co_nlocals) # 6 """ 参数有三个,加上局部变量3个,一共6个 下面的c=33中的c和参数的c总体是1个变量 """
co_stacksize:执行该段代码块须要的栈空间
import inspect frame = None def foo1(a, b): global frame frame = inspect.currentframe() foo1(1, 222) print(frame.f_code.co_stacksize) # 2
co_flags:用于mask,没什么用,这个能够不用管
co_firstlineno:代码块在对应文件的起始行
若是foo被函数调用呢?
每一个函数都有本身独自的命名空间,以及PyCodeObject对象,因此即使是经过bar调用的,co_firstlineno仍是自身的代码块的起始行
co_code:代码块编译成字节码的指令序列,以PyBytesObject的形式存在
import inspect frame = None def bar(): foo(1, 2) def foo(a, b): global frame frame = inspect.currentframe() bar() print(frame.f_code.co_code) # b't\x00\xa0\x01\xa1\x00a\x02d\x00S\x00'
co_consts:常量池,PyTupleObject对象,保存代码块中的全部常量。
import inspect frame = None def foo(a, b): name = "satori" global frame age = 16 frame = inspect.currentframe() foo(1, 2) print(frame.f_code.co_consts) # (None, 'satori', 16)
co_names:PyTupleObject对象,保存代码块中的全部符号
import inspect frame = None def foo(a, b): name = "satori" global frame age = 16 frame = inspect.currentframe() foo(1, 2) print(frame.f_code.co_names) # ('inspect', 'currentframe', 'frame')
co_varnames:代码块中出现的局部变量名
import inspect frame = None def foo(a, b): name = "satori" global frame age = 16 frame = inspect.currentframe() foo(1, 2) print(frame.f_code.co_varnames) # ('a', 'b', 'name', 'age')
co_freevars:python实现闭包所须要的东西。
import inspect frame = None def foo(a, b): name = "satori" global frame age = 16 frame = inspect.currentframe() foo(1, 2) print(frame.f_code.co_freevars) # ()
import inspect frame = None def foo(): name = "satori" age = 16 def inner(): global frame name age frame = inspect.currentframe() return inner foo()() print(frame.f_code.co_freevars) # ('age', 'name')
co_cellvars:内部嵌套函数所引用的外部函数的变量
import inspect frame = None def foo(): global frame name = "satori" age = 16 frame = inspect.currentframe() def inner(): name age return inner foo() # 注意到:这里是foo(),不是foo()(),co_freevars须要的是内部函数的栈帧,因此咱们要调用两次 # 但这里co_cellvars须要的外部函数foo的栈帧,所以咱们只须要调用一次便可,由于global frame是在外部函数当中的 print(frame.f_code.co_cellvars) # ('age', 'name')
co_filename:代码块所在的文件名
import inspect frame = None def foo(): global frame name = "satori" age = 16 frame = inspect.currentframe() def inner(): name age return inner foo() print(frame.f_code.co_filename) # C:/Users/satori/Desktop/love_minami/a.py
co_name:代码块的名字,一般是函数名或者类名
import inspect frame = None def foo(): global frame name = "satori" age = 16 frame = inspect.currentframe() def inner(): name age return inner foo() print(frame.f_code.co_name) # foo
co_lnotab:字节码指令与python源代码的行号之间的对应关系,以PyByteObject的形式存在
import inspect frame = None def foo(): global frame name = "satori" age = 16 frame = inspect.currentframe() def inner(): name age return inner foo() print(frame.f_code.co_lnotab) # b'\x00\x03\x04\x01\x04\x01\x08\x02\x0e\x04'
事实上,python不会直接记录这些信息,而是会记录增量值。好比说:
目前PyCodeObject中的全部属性咱们就介绍完了,事实上,还有那么两三个属性咱们没有介绍到,由于基本不用,而且经过frame.f_code去获取也根本获取不到。
事实上咱们已经介绍了一种方法去获取相应的PyCodeObject对象,可是还有没有其余的方法呢?
__code__
def foo(): name = "satori" age = 16 code = foo.__code__ # 能够看到,函数自己就提供了获取PyCodeObject对象的接口,直接调用__code__便可 # 此时拿到的就是frame.f_code print(code.co_varnames) # ('name', 'age')
compile
在介绍compile以前,先介绍一下eval和exec。
eval:传入一个字符串,而后把字符串里面的内容拿出来。
a = 1 # 因此eval("a")就等价于a print(eval("a")) # 1 print(eval("1 + 1 + 1")) # 3 # 注意:eval是有返回值的,返回值就是字符串里面内容。 # 或者说eval是能够做为右值的,好比a = eval("xxx") # 因此eval里面毫不能够出现诸如赋值之类的,好比 print(eval("a = 3")),那么这个语句等价于print(a = 3),这样显然会出现语法错误的 # 所以eval里面把字符串剥掉以后就是一个普通的值,不能够出现诸如if、def等语句 try: eval("xxx") except NameError as e: print(e) # name 'xxx' is not defined
exec:传入一个字符串,把字符串里面的内容当成语句来执行,这个是没有返回值,或者说返回值是None
exec("a = 1") # 等价于把a = 1这个字符串里面的内容当成语句来执行 print(a) # 1 statement = """a = 123 if a == 123: print("a等于123") else: print("a不等于123") """ exec(statement) # a等于123 # 注意:'a等于123'并非exec返回的,而是把上面那坨字符串当成普通代码执行的时候print出来的 # 这即是exec的做用。 # 那么它和eval的区别就显而易见的,eval是要求字符串里面的内容可以当成一个值来打印,返回值就是里面的值 # 而exec则是直接执行里面的内容 # 举个例子 print(eval("1 + 1")) # 2 print(exec("1 + 1")) # None exec("a = 1 + 1") print(a) # 2 try: eval("a = 1 + 1") except SyntaxError as e: print(e) # invalid syntax (<string>, line 1)`compile:至关于将二者组合起来`
compile则是拿到一个PyCodeObject对象
statement = "a, b = 1, 2" co = compile(statement, "hanser", "exec") print(co.co_name) # <module> print(co.co_filename) # hanser
前面咱们提到,python经过import module进行加载时,若是没有找到相应的pyc或者dll文件,就会在py文件的基础上自动建立pyc文件。因此想要了解pyc文件是怎么建立的,只须要了解PyCodeObject是如何写入的便可。关于写入pyc文件,主要写入三个内容:
magic number
这是python定义的一个整数值,不一样版本的python会定义不一样的magic number,这个值是为了保证python可以加载正确的pyc。好比python3.7不会加载3.6版本的pyc,由于python在加载这个pyc文件的时候会首先检测该pyc的magic number,如何和自身的magic number不一致,则拒绝加载。
pyc的建立时间
这个很好理解,由于编译完以后要是把源代码修改了怎么办呢?所以会判断源代码的最后修改时间和pyc文件的建立时间,若是pyc文件的建立时间比源代码修改时间要早,说明在生成pyc以后,源代码被修改了,那么会从新编译新的pyc,而反之则会直接加载pyc。
PyCodeObject对象
这个不用说了,确定是要存储的。
文件对象:
//位置:Python/marshal.c //FILE是一个文件句柄,能够把WFILE当作是FILE的包装 typedef struct { FILE *fp; //文件句柄 //下面的字段在写入信息的时候会看到 int error; int depth; PyObject *str; char *ptr; char *end; char *buf; _Py_hashtable_t *hashtable; int version; } WFILE;
写入magic number和时间:
写入magic number和时间都是调用了PyMarshal_WriteLongToFile
,咱们来看看长什么样子
void PyMarshal_WriteLongToFile(long x, FILE *fp, int version) { //声明char型的数组,元素个数为4个 char buf[4]; //声明一个WFILE类型变量wf WFILE wf; //内存初始化 memset(&wf, 0, sizeof(wf)); //设置fp,文件句柄 wf.fp = fp; //将buf数组的指针赋值给wf.ptr和wf.buf wf.ptr = wf.buf = buf; //至关于buf的最后一个元素的指针 wf.end = wf.ptr + sizeof(buf); //写错误 wf.error = WFERR_OK; //写入版本信息 wf.version = version; //调用w_long将x也就是版本信息或者时间写到wf里面去 w_long(x, &wf); //刷到磁盘上 w_flush(&wf); } //因此咱们看到这一步只是初始化一个WFILE对象,真正写入则是调用w_long static void w_long(long x, WFILE *p) { w_byte((char)( x & 0xff), p); w_byte((char)((x>> 8) & 0xff), p); w_byte((char)((x>>16) & 0xff), p); w_byte((char)((x>>24) & 0xff), p); } //w_long则是将要写入的x一个字节一个字节写到文件里面去。
写入PyCodeObject对象:
写入PyCodeObject对象则是调用了PyMarshal_WriteObjectToFile
,咱们也来看看长什么样子
void PyMarshal_WriteObjectToFile(PyObject *x, FILE *fp, int version) { char buf[BUFSIZ]; WFILE wf; memset(&wf, 0, sizeof(wf)); wf.fp = fp; wf.ptr = wf.buf = buf; wf.end = wf.ptr + sizeof(buf); wf.error = WFERR_OK; wf.version = version; if (w_init_refs(&wf, version)) return; /* caller mush check PyErr_Occurred() */ w_object(x, &wf); w_clear_refs(&wf); w_flush(&wf); } //能够看到,和PyMarshal_WriteLongToFile基本是相似的 //只不过PyMarshal_WriteLongToFile调用的是w_long,而PyMarshal_WriteObjectToFile调用的是w_object static void w_object(PyObject *v, WFILE *p) { char flag = '\0'; p->depth++; if (p->depth > MAX_MARSHAL_STACK_DEPTH) { p->error = WFERR_NESTEDTOODEEP; } else if (v == NULL) { w_byte(TYPE_NULL, p); } else if (v == Py_None) { w_byte(TYPE_NONE, p); } else if (v == PyExc_StopIteration) { w_byte(TYPE_STOPITER, p); } else if (v == Py_Ellipsis) { w_byte(TYPE_ELLIPSIS, p); } else if (v == Py_False) { w_byte(TYPE_FALSE, p); } else if (v == Py_True) { w_byte(TYPE_TRUE, p); } else if (!w_ref(v, &flag, p)) w_complex_object(v, flag, p); p->depth--; }
能够看到本质上仍是调用了w_byte,可是在这里面咱们并无看到诸如:list、tuple之类的数据的存储过程,注意最后的w_complex_object,关键来了
//源代码很长 static void w_complex_object(PyObject *v, char flag, WFILE *p) { Py_ssize_t i, n; if (PyLong_CheckExact(v)) { long x = PyLong_AsLong(v); if ((x == -1) && PyErr_Occurred()) { PyLongObject *ob = (PyLongObject *)v; PyErr_Clear(); w_PyLong(ob, flag, p); } else { #if SIZEOF_LONG > 4 long y = Py_ARITHMETIC_RIGHT_SHIFT(long, x, 31); if (y && y != -1) { /* Too large for TYPE_INT */ w_PyLong((PyLongObject*)v, flag, p); } else #endif { W_TYPE(TYPE_INT, p); w_long(x, p); } } } else if (PyFloat_CheckExact(v)) { if (p->version > 1) { unsigned char buf[8]; if (_PyFloat_Pack8(PyFloat_AsDouble(v), buf, 1) < 0) { p->error = WFERR_UNMARSHALLABLE; return; } W_TYPE(TYPE_BINARY_FLOAT, p); w_string((char*)buf, 8, p); } else { char *buf = PyOS_double_to_string(PyFloat_AS_DOUBLE(v), 'g', 17, 0, NULL); if (!buf) { p->error = WFERR_NOMEMORY; return; } n = strlen(buf); W_TYPE(TYPE_FLOAT, p); w_byte((int)n, p); w_string(buf, n, p); PyMem_Free(buf); } } else if (PyComplex_CheckExact(v)) { if (p->version > 1) { unsigned char buf[8]; if (_PyFloat_Pack8(PyComplex_RealAsDouble(v), buf, 1) < 0) { p->error = WFERR_UNMARSHALLABLE; return; } W_TYPE(TYPE_BINARY_COMPLEX, p); w_string((char*)buf, 8, p); if (_PyFloat_Pack8(PyComplex_ImagAsDouble(v), buf, 1) < 0) { p->error = WFERR_UNMARSHALLABLE; return; } w_string((char*)buf, 8, p); } else { char *buf; W_TYPE(TYPE_COMPLEX, p); buf = PyOS_double_to_string(PyComplex_RealAsDouble(v), 'g', 17, 0, NULL); if (!buf) { p->error = WFERR_NOMEMORY; return; } n = strlen(buf); w_byte((int)n, p); w_string(buf, n, p); PyMem_Free(buf); buf = PyOS_double_to_string(PyComplex_ImagAsDouble(v), 'g', 17, 0, NULL); if (!buf) { p->error = WFERR_NOMEMORY; return; } n = strlen(buf); w_byte((int)n, p); w_string(buf, n, p); PyMem_Free(buf); } } else if (PyBytes_CheckExact(v)) { W_TYPE(TYPE_STRING, p); w_pstring(PyBytes_AS_STRING(v), PyBytes_GET_SIZE(v), p); } else if (PyUnicode_CheckExact(v)) { if (p->version >= 4 && PyUnicode_IS_ASCII(v)) { int is_short = PyUnicode_GET_LENGTH(v) < 256; if (is_short) { if (PyUnicode_CHECK_INTERNED(v)) W_TYPE(TYPE_SHORT_ASCII_INTERNED, p); else W_TYPE(TYPE_SHORT_ASCII, p); w_short_pstring((char *) PyUnicode_1BYTE_DATA(v), PyUnicode_GET_LENGTH(v), p); } else { if (PyUnicode_CHECK_INTERNED(v)) W_TYPE(TYPE_ASCII_INTERNED, p); else W_TYPE(TYPE_ASCII, p); w_pstring((char *) PyUnicode_1BYTE_DATA(v), PyUnicode_GET_LENGTH(v), p); } } else { PyObject *utf8; utf8 = PyUnicode_AsEncodedString(v, "utf8", "surrogatepass"); if (utf8 == NULL) { p->depth--; p->error = WFERR_UNMARSHALLABLE; return; } if (p->version >= 3 && PyUnicode_CHECK_INTERNED(v)) W_TYPE(TYPE_INTERNED, p); else W_TYPE(TYPE_UNICODE, p); w_pstring(PyBytes_AS_STRING(utf8), PyBytes_GET_SIZE(utf8), p); Py_DECREF(utf8); } } else if (PyTuple_CheckExact(v)) { n = PyTuple_Size(v); if (p->version >= 4 && n < 256) { W_TYPE(TYPE_SMALL_TUPLE, p); w_byte((unsigned char)n, p); } else { W_TYPE(TYPE_TUPLE, p); W_SIZE(n, p); } for (i = 0; i < n; i++) { w_object(PyTuple_GET_ITEM(v, i), p); } } else if (PyList_CheckExact(v)) { W_TYPE(TYPE_LIST, p); n = PyList_GET_SIZE(v); W_SIZE(n, p); for (i = 0; i < n; i++) { w_object(PyList_GET_ITEM(v, i), p); } } else if (PyDict_CheckExact(v)) { Py_ssize_t pos; PyObject *key, *value; W_TYPE(TYPE_DICT, p); /* This one is NULL object terminated! */ pos = 0; while (PyDict_Next(v, &pos, &key, &value)) { w_object(key, p); w_object(value, p); } w_object((PyObject *)NULL, p); } else if (PyAnySet_CheckExact(v)) { PyObject *value, *it; if (PyObject_TypeCheck(v, &PySet_Type)) W_TYPE(TYPE_SET, p); else W_TYPE(TYPE_FROZENSET, p); n = PyObject_Size(v); if (n == -1) { p->depth--; p->error = WFERR_UNMARSHALLABLE; return; } W_SIZE(n, p); it = PyObject_GetIter(v); if (it == NULL) { p->depth--; p->error = WFERR_UNMARSHALLABLE; return; } while ((value = PyIter_Next(it)) != NULL) { w_object(value, p); Py_DECREF(value); } Py_DECREF(it); if (PyErr_Occurred()) { p->depth--; p->error = WFERR_UNMARSHALLABLE; return; } } else if (PyCode_Check(v)) { PyCodeObject *co = (PyCodeObject *)v; W_TYPE(TYPE_CODE, p); w_long(co->co_argcount, p); w_long(co->co_kwonlyargcount, p); w_long(co->co_nlocals, p); w_long(co->co_stacksize, p); w_long(co->co_flags, p); w_object(co->co_code, p); w_object(co->co_consts, p); w_object(co->co_names, p); w_object(co->co_varnames, p); w_object(co->co_freevars, p); w_object(co->co_cellvars, p); w_object(co->co_filename, p); w_object(co->co_name, p); w_long(co->co_firstlineno, p); w_object(co->co_lnotab, p); } else if (PyObject_CheckBuffer(v)) { /* Write unknown bytes-like objects as a bytes object */ Py_buffer view; if (PyObject_GetBuffer(v, &view, PyBUF_SIMPLE) != 0) { w_byte(TYPE_UNKNOWN, p); p->depth--; p->error = WFERR_UNMARSHALLABLE; return; } W_TYPE(TYPE_STRING, p); w_pstring(view.buf, view.len, p); PyBuffer_Release(&view); } else { W_TYPE(TYPE_UNKNOWN, p); p->error = WFERR_UNMARSHALLABLE; } }
源代码很长,这里就不一一分析了。虽然长,可是逻辑很简单,就是对应不一样的对象、执行不一样的写的动做。然而其最终目的都是经过w_byte写到pyc文件中。换句话说,python在往pyc写入list对象时,只是将list中包含的数值或者字符串等对象写到了pyc文件中。同时这也意味着,python在加载pyc文件时,必须基于这些数值或字符串从新构造出list对象。
对于PyCodeObject对象,很显然,w_object会遍历PyCodeObject中的全部域,将这些域依次写入
PyCodeObject *co = (PyCodeObject *)v; W_TYPE(TYPE_CODE, p); w_long(co->co_argcount, p); w_long(co->co_kwonlyargcount, p); w_long(co->co_nlocals, p); w_long(co->co_stacksize, p); w_long(co->co_flags, p); w_object(co->co_code, p); w_object(co->co_consts, p); w_object(co->co_names, p); w_object(co->co_varnames, p); w_object(co->co_freevars, p); w_object(co->co_cellvars, p); w_object(co->co_filename, p); w_object(co->co_name, p); w_long(co->co_firstlineno, p); w_object(co->co_lnotab, p);
可是当面对一个PyListObject对象时,会有什么变化呢?没错,会和PyCodeObject同样,w_object仍是会遍历,而后将PyListObject对象中的每个元素依次写入到pyc文件中。
//能够看到PyTupleObject、PyListObject、PyDictObject都是采用了相同的姿式 //注意里面的W_TYPE else if (PyTuple_CheckExact(v)) { n = PyTuple_Size(v); if (p->version >= 4 && n < 256) { W_TYPE(TYPE_SMALL_TUPLE, p); w_byte((unsigned char)n, p); } else { W_TYPE(TYPE_TUPLE, p); W_SIZE(n, p); } for (i = 0; i < n; i++) { w_object(PyTuple_GET_ITEM(v, i), p); } } else if (PyList_CheckExact(v)) { W_TYPE(TYPE_LIST, p); n = PyList_GET_SIZE(v); W_SIZE(n, p); for (i = 0; i < n; i++) { w_object(PyList_GET_ITEM(v, i), p); } } else if (PyDict_CheckExact(v)) { Py_ssize_t pos; PyObject *key, *value; W_TYPE(TYPE_DICT, p); /* This one is NULL object terminated! */ pos = 0; while (PyDict_Next(v, &pos, &key, &value)) { w_object(key, p); w_object(value, p); } w_object((PyObject *)NULL, p); }
咱们看到不管对于哪个对象,在写入以前,都会先调用W_TYPE写一个相似于类型的东西,是的,诸如TYPE_LIST、TYPE_TUPLE、TYPE_DICT这样的标识,对于pyc文件的加载起着相当重要的做用。
以前说过,python仅仅将数值和字符串写入到pyc文件。当PyCodeObject写入到pyc以后,全部的数据就变成了了字节流,类型信息就丢失了。然鹅若是没有类型信息,那么当python再次加载pyc文件的时候,就没办法知道字节流中隐藏的结构和蕴含的信息,因此python必须往pyc文件写入一个标识,这些标识正是python定义的类型信息,若是python在pyc中发现了这样的标识,则预示着上一个对象结束,新的对象开始,而且也知道新对象是什么样的对象,从而也知道该执行什么样的加载动做。这些标识也是能够看到的
//marshal.c #define TYPE_NULL '0' #define TYPE_NONE 'N' #define TYPE_FALSE 'F' #define TYPE_TRUE 'T' #define TYPE_STOPITER 'S' #define TYPE_ELLIPSIS '.' #define TYPE_INT 'i' /* TYPE_INT64 is not generated anymore. Supported for backward compatibility only. */ #define TYPE_INT64 'I' #define TYPE_FLOAT 'f' #define TYPE_BINARY_FLOAT 'g' #define TYPE_COMPLEX 'x' #define TYPE_BINARY_COMPLEX 'y' #define TYPE_LONG 'l' #define TYPE_STRING 's' #define TYPE_INTERNED 't' #define TYPE_REF 'r' #define TYPE_TUPLE '(' #define TYPE_LIST '[' #define TYPE_DICT '{' #define TYPE_CODE 'c' #define TYPE_UNICODE 'u' #define TYPE_UNKNOWN '?' #define TYPE_SET '<' #define TYPE_FROZENSET '>'
到了这里能够看到,其实python对于PyCodeObject对象的导出其实是不复杂的,实际上无论什么对象,最后都为归结为两种简单的形式,一种是数值写入,一种是字符串写入。上面都是对数值的写入,比较简单,仅仅须要按照字节一次写入pyc便可。然而在写入字符串的时候,python设计了一种比较复杂的机制,有兴趣能够本身阅读源码,这里再也不介绍。
# a.py class A: pass def foo(): pass a = A() foo()
咱们以前说对于这样的一个py文件,会建立三个PyCodeObject对象,可是写到pyc文件里面的只有一个PyCodeObject,这难道不就意味着有两个PyCodeObject丢失了吗?其实很明显,有两个PyCodeObject对象是位于另外一个PyCodeObject对象当中的。所以其实foo和A对应的PyCodeObject对象是位于a.py这个PyCodeObject对象当中的,准确的说是位于co_consts当中
在将一个PyCodeObject对象写入到pyc文件当中时,若是碰到了包含的另外一个PyCodeObject对象,那么就会递归地执行写入PyCodeObject对象的操做。如此下去,最终全部的PyCodeObject对象都会写入到pyc文件当中,所以pyc文件当中的PyCodeObject对象也是以一种嵌套的关系联系在一块儿的。
关于python的字节码,是后面章节剖析虚拟机的重点,如今先来看一下。咱们知道python执行源代码以前会对其进行编译获得字节码序列,python虚拟机会根据这些字节码序列来进行一系列的操做,从而完成对程序的执行。
在python中一共定义了130条指令
#define POP_TOP 1 #define ROT_TWO 2 #define ROT_THREE 3 #define DUP_TOP 4 #define DUP_TOP_TWO 5 #define NOP 9 #define UNARY_POSITIVE 10 #define UNARY_NEGATIVE 11 #define UNARY_NOT 12 #define UNARY_INVERT 15 #define BINARY_MATRIX_MULTIPLY 16 #define INPLACE_MATRIX_MULTIPLY 17 #define BINARY_POWER 19 #define BINARY_MULTIPLY 20 #define BINARY_MODULO 22 #define BINARY_ADD 23 #define BINARY_SUBTRACT 24 #define BINARY_SUBSCR 25 #define BINARY_FLOOR_DIVIDE 26 #define BINARY_TRUE_DIVIDE 27 #define INPLACE_FLOOR_DIVIDE 28 ... ...
若是使用过dis模块的小伙伴确定很熟悉,固然这里咱们只是先看一下, 后面章节会介绍。
结尾的图片里面的妹子叫hanser,我的很是喜欢的一个up主(开婴儿车的老司机),唱歌超好听的,能够去B站关注一下她,能给你带来不少欢乐哦。
连接(hanser哔哩哔哩我的主页):https://space.bilibili.com/11073?from=search&seid=542967129544922757