PYC文件简介

不说废话,这里说的pyc文件就是 Python 程序编译后得到的字节码文件 (py->pyc).

基本格式

pyc文件一般由3个部分组成:

  • 最开始4个字节是一个Maigc int, 标识此pyc的版本信息, 不同的版本的 Magic 都在 Python/import.c 内定义
  • 接下来四个字节还是个int,是pyc产生的时间(1970.01.01到产生pyc时候的秒数)
  • 接下来是个序列化了的 PyCodeObject(此结构在 Include/code.h 内定义),序列化方法在 Python/marshal.c 内定义

前两个字段的读写很简单,接下来咱们主要看一下 PyCodeObject 的序列化过程, 由于 PyCodeObject 内还有其他很多类型的 PyObject, 所以咱们从一般的 PyObject 的序列化开始看起:

PyObject的序列化

PyObject的序列化在 Python/marshal.c 内实现, 一般是先写入一个 byte 来标识此 PyObject 的类型, 每种 PyObject 对应的类型也在 Python/marshal.c 内定义:

//Python/marshal.c:22
#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'
#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_STRINGREF '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 '>'
之后是 PyObject 的具体数据内容, 变长的对象(str, tuple, list 等)往往还包含了一个 4 bytes 的 len, 比如 PyIntObject 的存储可能是这样的:
‘i’ 4 bytes int
‘I’ 8 bytes int

而 PyStringObject 的存储是这样的:

‘s’ 4 bytes length length bytes content(char[])

PyTupleObject 和 PyListObject 的存储分别是:

‘(‘ 4 bytes length length 个 PyObject
‘[‘ 4 bytes length length 个 PyObject

各种 PyObject 如何序列化,哪些内容被参与了序列化, 可以参看 Python/marshal.c 内的函数 w_object 函数, 接下来咱们着重看下前面提到的 PyCodeObject 的序列化:

PyCodeObject 的序列化

结构体 PyCodeObject 在 Include/code.h 中定义如下:

//Include/code.h
typedef struct {
PyObject_HEAD
int co_argcount; /* #arguments, except *args */
int co_nlocals; /* #local variables */
int co_stacksize; /* #entries needed for evaluation stack */
int co_flags; /* CO_..., see below */
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 doesn't count for hash/cmp */
PyObject *co_filename; /* string (where it was loaded from) */
PyObject *co_name; /* string (name, for reference) */
int co_firstlineno; /* first source line number */
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 */
} PyCodeObject;
 
我们再看 Python/marshal.c 里面的 w_object 函数,从中找出写 PyCodeObject 的部分如下:
//...
else if (PyCode_Check(v)) {
PyCodeObject *co = (PyCodeObject *)v;
w_byte(TYPE_CODE, p);
w_long(co->co_argcount, 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);
}
//...
 
根据上面两段代码,我们很容易就能看到 PyCodeObject 里哪些字段需要参数序列化了,我们就挨个解释下需要序列化的字段们:
  • co_argcount : code需要的位置参数个数,不包括变长参数(*args和**kwargs)
  • co_nlocals : code内所有的局部变量的个数,包括所有参数
  • co_stacksize : code段运行时所需要的最大栈深度
  • co_flags : 一些标识位,也在code.h里定义,注释很清楚,比如 CO_NOFREE(64) 表示此 PyCodeObject 内无 freevars 和 cellvars 等
  • co_code : PyStringObject(‘s’), code对应的字节码(参看 Include/opcode.h 以及此文后续章节)
  • co_consts : 所有常量组成的tuple
  • co_names : code所用的到符号表, tuple类型,元素是字符串
  • co_varnames : code所用到的局部变量名, tuple类型, 元素是 PyStringObject(‘s/t/R’)
  • co_freevars : code所用到的freevar的变量名,tuple类型, 元素是 PyStringObject(‘s/t/R’)
  • co_cellvars : code所用到的cellvar的变量名,tuple类型, 元素是 PyStringObject(‘s/t/R’)
  • co_filename : PyStringObject(‘s’), 此code对应的py文件
  • co_name : 此code的名称
  • co_firstlineno : 此code对应的py文件里的第一行的行号
  • co_lnotab : PyStringObject(‘s’),指令与行号的对应表

在 Python 代码中,每个作用域(或者叫block或者名字空间?)对应一个 PyCodeObject 对象, 所以会出现嵌套: 比如 一个 module 类 定义了 N 个 class, 每个 class 内又定义了 M 个方法. 每个 子作用域 对应的 PyCodeObject 会出现在它的 父作用域 对应的 PyCodeObject 的 co_consts 字段里.

接下来用个例子对上面的某些字段做个说明, 比如下面的python代码

#test.py
import struct def abc(c):
a=1
b=2
return struct.pack("S",a+b+c) var_a="abc"
var_b={var_a:123,"sec_key":abc}
它编译后对应的 pyc 在 magictime 后就放着一个 PyCodeObject 对象, 这个对象的个字段指如下
test.py对应PyCodeObject中各字段的值
字段 注释
co_argcount 0 模块没有参数
co_nlocals 0 模块没有局部变量
co_stacksize 3 栈最大尺寸
co_flags 64 CO_NOFREE
co_code ‘dx00x00d...’ 字节码序列
co_consts (-1, None, another-code-obj...) 所有常量,包括模块里的 function,method
co_names (‘struct’, ‘abc’, ‘var_a’, ‘var_b’) 此作用域内用到的所有符号
co_varnames () 局部变量名(模块没有局部变量)
co_freevars () freevars
co_cellvars () cellvars
co_filename test.py 源码文件名
co_name ‘<module>’ code名字,模块名都是<module>,class是类名,func是函数名
co_firstlineno 3 此code对应作用域的第一行的行号
co_lnotab ‘x0cx02tx05x06x01’ 行号表

这其中的 co_consts 里面的第三个元素是function abc的PyCodeObject, 我们来看下function abc的code的各字段:

function abc 对应PyCodeObject中各字段的值
字段 注释
co_argcount 1 1个参数c
co_nlocals 3 1个参数c, 两个局部变量a,b
co_stacksize 4 栈最大尺寸
co_flags 67 CO_OPTIMIZATION | CO_NEWLOCALS | CO_NOFREE
co_code ‘dx01x00}...’ 字节码序列
co_consts (None, 1, 2, ‘S’) 函数里用到的所有常量
co_names (‘struct’, ‘pack’) 此作用域内用到的所有符号
co_varnames (‘c’, ‘a’, ‘b’) 局部变量名
co_freevars () freevars
co_cellvars () cellvars
co_filename test.py 源码文件名
co_name ‘abc’ code名字,func是函数名
co_firstlineno 5 此code对应作用域的第一行的行号
co_lnotab ‘x00x01x06x01x06x01’ 行号表

图解 test.py 与 test.pyc 的结构关系

co_freevars 与 co_cellvars

这两个字段是给 Closure 准备的(Python里没有真正的Closure),通俗的说就是函数嵌套的时候用的到,比如:

 
def outter(o1,o2):
fc1=o1+o2
fc2=o1*o2
def inner(i):
return (fc1+fc2)*i
对于 outter 函数来说,他的局部变量 fc1 和 fc2 被它内部嵌套的函数所引用,则 fc1 和 fc2 变成它的 cellvars 而不是局部变量 varnames
  • 对于 inner 函数, fc1 和 fc2 既不是局部变量也不是全局变量,他引用自外层函数, 则 fc1 和 fc2 是 inner 的 freevars

PyStringObject 的序列化

通过前面的介绍,你可能会发现PyCodeObject里面的用到str(‘s’)类型的地方很多,什么co_consts啊,co_names,co_varnames,co_freevars,co_cellvars等等都是str的tuple,里面的str重复的也比较多,要是一股脑这么写进去可能会占用很大空间,于是w_object里对PyStringObject的序列化又多加了两种类型:

  • ‘t’ : interned-string, 暂时可能简单理解为 pyc 里回重复出现的 str, 这个类型就是简单的把 str 的类型 ‘s’ 改成 ‘t’ 了,后面还是跟 length(4bytes) 和 content(char[])
  • ‘R’ : 指向 interned-string 的字符串引用, ‘R’后面跟4个 bytea 的引用序号

具体看 Python/marshal.cw_object 的相关实现:

//...
else if (PyString_CheckExact(v)) {
if (p->strings && PyString_CHECK_INTERNED(v)) {
PyObject *o = PyDict_GetItem(p->strings, v);
if (o) {
long w = PyInt_AsLong(o);
w_byte(TYPE_STRINGREF, p);
w_long(w, p);
goto exit;
}
else {
int ok;
o = PyInt_FromSsize_t(PyDict_Size(p->strings));
ok = o &&
PyDict_SetItem(p->strings, v, o) >= 0;
Py_XDECREF(o);
if (!ok) {
p->depth--;
p->error = WFERR_UNMARSHALLABLE;
return;
}
w_byte(TYPE_INTERNED, p);
}
}
else {
w_byte(TYPE_STRING, p);
}
n = PyString_GET_SIZE(v);
if (n > INT_MAX) {
/* huge strings are not supported */
p->depth--;
p->error = WFERR_UNMARSHALLABLE;
return;
}
w_long((long)n, p);
w_string(PyString_AS_STRING(v), (int)n, p);
}
//...

行号对照表

co_lnotab可以看做是(字节码在co_opcode中的index增量(1 byte),对应的源码的行号增量(1-bytes))顺次串成的的字节数组(字符串).

字段 co_code 与 Python的OPCODE

PyCodeObject 的 co_code 字段就是 python opcode 组成的序列, 具体有哪些 opcode可以参看 Include/opcode.h , 这里面有些opcode有参数,有些没有参数, 从opcode.h内的代码段:

#define HAVE_ARGUMENT 90  /* Opcodes from here have an argument: */
//...
#define HAS_ARG(op) ((op) >= HAVE_ARGUMENT)

可以看出,大于等与90的opcode是有参数的, 有参数的opcode的参数是两个 unsigned byte, 第一个是操作数, 第二个目前固定为0x00但是不能省略,举例来说,我要把当前code的co_consts里的第二个常量(index是1)载入到栈顶,则对应的opcode序列为: |LOAD_CONST|0x01|0x00| ,也就是 '0x640x010x00'

其他的opcode大都类似,主要是对函数调用栈以及co_consts, co_varnames, co_freevars, co_cellvars的操作, 还有些BUILD_CLASS, BUILD_MAP, BUILD_LIST, BUILD_TUPLE, BUILD_SLICE, MAKE_FUNCTION, MAKE_CLOSURE 等构建对象的特殊指令.

有参数的 opcode 的参数的参数大多时候是个 index, 比如 LOAD_CONST 1 的 1 就是个 index, 表示把当前 PyCodeObject.co_consts[1] 这个常量载入到栈顶, LOAD_FAST 2 则是把 PyCodeObject.co_varnames[2] 这个局部变量载入到栈顶;而 MAKE_FUNCTION 2 则表示栈顶code-obj对应的 function有两个默认参数.

前面 test.pyc 之外的东西, 比如 class/closure 的创建, 也都逃不过这些指令, 具体每个指令的解释和用法可以参看 : http://docs.python.org/release/2.7/library/dis.html#python-bytecode-instructions

Page(Article) Information / 页面(文章)信息:

最新文章

  1. Web Service和WCF的区别。其实二者不属于一个范畴!!!
  2. 配置本地光盘为yum源
  3. Linux 相关基础笔记
  4. Linux shell用法和技巧(转)
  5. 在jsp中用一数组存储了数据库表中某一字段的值,然后在页面中输出其中的值。
  6. 201521123069 《Java程序设计》 第12周学习总结
  7. sphinx实时索引和高亮显示
  8. Mycat 分片规则详解--固定 hash 分片
  9. CDN边缘节点容器调度实践(上)
  10. 在Asp.Net Core中使用DI的方式使用Hangfire构建后台执行脚本
  11. JS数组映射保存数据-场景
  12. Linux的telent服务
  13. 【Jenkins持续集成】好用的插件集合
  14. D - Balanced Ternary String (贪心)
  15. I.MX6 Manufacturing Tool V2 (MFGTool2) ucl2.xml hacking
  16. ThinkPHP 调用后台方法
  17. 高考结束了,在门头沟有没有想学php建站的。
  18. taro 请求函数封装
  19. 微信小程序 使用腾讯地图SDK详解及实现步骤
  20. java中为什么要使用代理

热门文章

  1. SetForegroundWindow的正确用法
  2. minizlib
  3. [Recompose] Show a Spinner While a Component is Loading using Recompose
  4. 【源代码】Timer和TimerTask源代码剖析
  5. JavaScript实现form表单的多文件上传
  6. [GraphQL] Use GraphQLList with GraphQLObject Types
  7. Http请求工具类(Java原生Form+Json)
  8. CF 559B(Equivalent Strings-构造法)
  9. C++ 如何快速清空vector以及释放vector内存?
  10. [NPM] Use package.json variables in npm scripts