• PYC文件简介


    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 的结构关系



    ../_images/pyc_format_example_0.png

    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 / 页面(文章)信息:

    服务项目 技术咨询 微信图书 微信视频 微信代码 定制开发 其他福利
    服务入口 QQ群有问必答
    查看详情
    一本书解决90%问题
    查看详情
    微信开发视频
    小程序开发视频
    免费代码
    ¥1888阿里云代金券
    查看详情
    营销工具
    微信特异功能
  • 相关阅读:
    【BZOJ1015】星球大战starwar
    【BZOJ1878】HH的项链
    【BZOJ1012】最大数maxnumber
    【BZOJ3767】A+B Problem加强版
    【BZOJ1406】密码箱
    【BZOJ1067】降雨量
    【BZOJ1305】dance跳舞
    【BZOJ1509】逃学的小孩
    【BZOJ1103】大都市meg
    【BZOJ3262】陌上花开
  • 原文地址:https://www.cnblogs.com/txw1958/p/pyc_introduce.html
Copyright © 2020-2023  润新知