Python 编译

目录

1 前言

2 pyc

2.1 运行程序

2.2 PyCodeObject

2.3 产生pyc文件的方法

2.4 在 Python 中访问 PyCodeObject

2.5 创建 pyc 文件

2.5.1. magic number

2.5.2 pyc 创建时间

2.5.3 PyCodeObject

2.5.4 字符串写入pyc文件

2.2.5 pyc文件读出字符串

2.6 字节码

2.7 解析 pyc

2.8 生成pyc文件

2.8.1 生成单个pyc文件(py_compile.compile)

2.8.2 批量生成pyc文件(compileall.compile_dir)

3 pyc和pyo的生成方法

4 pyd

4.1 编译单个pyd

4.2 批量编译pyd文件

4.2.1 误区

4.2.2 批量编译pyd


1 前言

(1)py文件是如何执行的?

  • py代码不会转换成一系列的机器指令,然后让cpu去执行
  • Python的本质和Java/C#是一样的,都是通过虚拟机来执行字节码实现的

(2)Python解释器是什么?

  • 就是平时调用的/usr/bin/python可执行文件
  • 它包含Python编译器和Python虚拟机
  • Python解释器在执行Python程序文件的时候。会首先使用Python编译器对源文件进行编译,编译会产生Python的一组字节码(byte code)(当然还会产生其他内容。。。);然后将编译结果交给Python虚拟机,由虚拟机一条一条顺序执行字节码

(3)Python编译器和虚拟机在什么地方?

  • 没有在python.exe中,而是在python2*.dll动态库中,编译和执行的过程都调用了python2*.dll动态库
  • 对比javaa.java通过javac编译成a.class文件,然后通过java执行a.class中的字节码,得到执行结果

(4)py源文件经过Python编译器后会产生什么?

  • 代码中操作类型的内容会生成字节码
  • 字符串、常量值和字节码等静态信息都会存储为一个PyCodeObject对象(Python运行时的对象);通过也会存储到一个文件中,就是pyc文件
  • PyCodeObject对象和pyc文件之间的关系就是序列化和反序列化的关系

Python是一门解释型的语言,而对比Java这种编译后再解释型的语言似乎就显得不够友好了。其实不然,Python一样可以编译。

Python和Java的解释方式对比

  • Java:源代码 -> 编译成class -> Jvm解释运行
  • Python:源代码 -> Python解释器解释运行

直接解释和编译后解释其实最大的区别就是源代码的安全性。

        如果是直接解释源代码,那么源代码没有安全性可言,也就是任何一个人都可以打开源代码一看究竟,任何人都可以随意修改源代码。

       事实上,Python和Java的解释方式是相同的,只是我们表面上看Python是直接解释源代码,而实际上python解释器只会加载字节码。细心的小伙伴肯定发现了这一点,当我们import某个模块时,总是会在模块所在的目录创建一个__pycache__目录,里面存放着被加载模块的字节码文件。

编译源代码有以下作用:

源代码保护(算法保护)/ 防止用户篡改源代码解释器加载代码速度加快Python的几种文件类型

Python有以下几种类型的文件:

  • py:Python控制台程序的源代码文件
  • pyw:Python带用户界面的源代码文件
  • pyx:Python包源文件
  • pyc:Python字节码文件
  • pyo:Python优化后的字节码文件
  • pyd:Python的库文件(Python版DLL)、在Linux上是so文件

2 pyc

2.1 运行程序

       当在shell中敲入python xx.py运行 Python 程序时,就是激活了 Python 解释器

       Python 解释器并不会立即运行程序,而是会对 Python 程序的源代码进行编译,产生字节码,然后将字节码交给虚拟机一条条顺序执行。

        源文件中的内容可以分为:字符串常量操作

    操作会被编译为字节码指令序列字符串常量在编译的过程中会被收集起来。这些编译后的信息在程序运行时,会作为 运行时对象 PyCodeObject 存储于内存中。运行结束后,PyCodeObject 被放入xx.pyc文件,保存在硬盘中。这样,在下次运行时,可以直接根据.pyc文件的内容,在内存中建立 PyCodeObject ,不需要再进行编译。

2.2 PyCodeObject

        在编译器对源码进行编译时,会为每一个 Code Block 创建一个对应的 PyCodeObject

        什么是 Code Block 呢?规则是:当进入一个新的名字空间,或者新的作用域,就是进入了一个新 Code Block。名字空间是符号的上下文环境,决定了符号的含义。也就是说,决定了变量名对应的变量值是什么。

        名字空间是可以嵌套的,能够形成一个名字空间链,虚拟机在执行字节码时,一个重要的任务就是从链中确定一个符号的对象是什么。

        在 Python 中,函数modules 对应独立的名字空间,所以都有对应的 PyCodeObject

        PyCodeObject 中co_code域保存的就是对操作编译生成的字节码指令序列

2.3 产生pyc文件的方法

        上面提到,Python 程序运行结束后,会在硬盘中以.pyc文件的形式存储 PyCodeObject,但直接运行 Python 程序并不会产生.pyc文件。

        这可能是因为直接运行的 Python 程序,有些只是临时使用一次,所以没有通过.pyc保存编译结果的必要。

        一种常见的,产生pyc文件的方法是import机制。当Python 程序运行时,如果遇到 import abc,会到设定好的path中寻找 abc.pyc 文件,如果没有,只找到abc.py,会先将 abc.py 编译成 CodeObject,然后创建 pyc 文件,将 CodeObject写入,最后才会对 pyc 进行import操作,将 pyc 中的 CodeObject重新复制到内存,并运行。

        另外,Python 标准库中的py_compilecompile可以帮助手动产生 pyc 文件。

        每一个PyCodeObject对象都包含了每一个Code Block中所有Python源代码经过编译后得到的byte code序列,Python会将这些字节码序列和PyCodeObject对象一起存储在pyc文件中。pyc 文件内容是二进制的,要了解pyc文件,首先我们必须清楚PyCodeObject中大部分域所代表的含义。

PyCodeObject中域含义
FieldContent
co_argcountCode Block的位置参数个数,比如说一个函数的位置参数个数
co_nlocalsCode Block中局部变量的个数,包括其位置参数的个数
co_stacksize 执行该段Code Block需要的栈空间
co_flags N/A,表示该域对理解Python虚拟机的行为没太多用处
co_code Code Block编译所得的字节码指令序列,以PyStringObject的形式存在
co_consts PyTuppleObject对象,保存Code Block中所有的常量
co_names PyTuppleObject对象,保存Code Block中所有的符号
co_varnames Code Block中的局部变量名集合
co_freevars Python实现闭包需要用到的东西,为自由变量
co_cellvars Code Block中内部嵌套函数所引用的局部变量名集合
co_filename Code Block所对应的.py文件的完整路径
co_name Code Block的名字,通常是函数名或类名
co_firstlineno Code Block在对应的.py文件中的起始行
co_lnotab 字节码指令与.py文件中的source code行号的对应关系,以PyStringObject的形式存在

2.4 在 Python 中访问 PyCodeObject

        C语言形式的 PyCodeObject 对应 Python 中的 Code对象,Code对象 是对 PyCodeObject 的简单包装。

        因此,可以通过 Code对象 访问 PyCodeObject 的各个域。这就需要使用 内建函数 compile

新建文件test.py:

import sys

a = 1

def b():
    print a
    a = 2
    print a
>>> source = open('/Users/chao/Desktop/test.py').read()
>>> co = compile(source, 'test.py', 'exec')
>>> type(co)
<type 'code'>
>>> dir(co)
['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
>>> print co.co_names
()
>>> print co.co_name
<module>
>>> print co.co_filename
test.py

注:

        dir() 函数不带参数时,返回当前范围内的变量、方法和定义的类型列表;带参数时,返回参数的属性、方法列表。如果参数包含方法__dir__(),该方法将被调用。如果参数不包含__dir__(),该方法将最大限度地收集参数信息。

     dir([object])
  • object -- 对象、变量、类型。

        示例:

>>>dir()   #  获得当前模块的属性列表
['__builtins__', '__doc__', '__name__', '__package__', 'arr', 'myslice']
>>> dir([ ])    # 查看列表的方法
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__delslice__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getslice__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__setslice__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
>>>

2.5 创建 pyc 文件

一个 pyc 文件包含三部分独立的信息

  • magic number
  • pyc 文件的创建时间信息
  • PyCodeObject

import.c:

static void
write_compiled_module(PyCodeObject *co, char *cpathname, struct stat *srcstat)
{
    FILE *fp;
    time_t mtime = srcstat->st_mtime;
#ifdef MS_WINDOWS   /* since Windows uses different permissions  */
    mode_t mode = srcstat->st_mode & ~S_IEXEC;
#else
    mode_t mode = srcstat->st_mode & ~S_IXUSR & ~S_IXGRP & ~S_IXOTH;
#endif

    fp = open_exclusive(cpathname, mode);
    if (fp == NULL) {
        if (Py_VerboseFlag)
            PySys_WriteStderr(
                "# can't create %s\n", cpathname);
        return;
    }
    PyMarshal_WriteLongToFile(pyc_magic, fp, Py_MARSHAL_VERSION);     # 写入`magic number`
    /* First write a 0 for mtime */
    PyMarshal_WriteLongToFile(0L, fp, Py_MARSHAL_VERSION);
    PyMarshal_WriteObjectToFile((PyObject *)co, fp, Py_MARSHAL_VERSION);  # 写入`PyCodeObject`
    if (fflush(fp) != 0 || ferror(fp)) {
        if (Py_VerboseFlag)
            PySys_WriteStderr("# can't write %s\n", cpathname);
        /* Don't keep partial file */
        fclose(fp);
        (void) unlink(cpathname);
        return;
    }
    /* Now write the true mtime */
    fseek(fp, 4L, 0);
    assert(mtime < LONG_MAX);
    PyMarshal_WriteLongToFile((long)mtime, fp, Py_MARSHAL_VERSION);   # 写入 pyc 创建时间
    fflush(fp);
    fclose(fp);
    if (Py_VerboseFlag)
        PySys_WriteStderr("# wrote %s\n", cpathname);
}

下面一一进行说明

2.5.1. magic number

      是 Python 定义的一个整数值,不同版本定义不同,用来确保 Python 的兼容性。Python 在加载 pyc 时首先检查 magic number ,如果与 Python 自身的 magic number 不同,说明创建 pyc 的 Python 版本 与 当前版本不兼容,会拒绝加载。

          为什么会不兼容呢?因为字节码指令发生了变化,有删除或增加。

/* Magic word to reject .pyc files generated by other Python versions.
   It should change for each incompatible change to the bytecode.

   The value of CR and LF is incorporated so if you ever read or write
   a .pyc file in text mode the magic number will be wrong; also, the
   Apple MPW compiler swaps their values, botching string constants.

   The magic numbers must be spaced apart atleast 2 values, as the
   -U interpeter flag will cause MAGIC+1 being used. They have been
   odd numbers for some time now.

   There were a variety of old schemes for setting the magic number.
   The current working scheme is to increment the previous value by
   10.

   Known values:
       Python 1.5:   20121
       Python 1.5.1: 20121
       Python 1.5.2: 20121
       Python 1.6:   50428
       Python 2.0:   50823
       Python 2.0.1: 50823
       Python 2.1:   60202
       Python 2.1.1: 60202
       Python 2.1.2: 60202
       Python 2.2:   60717
       Python 2.3a0: 62011
       Python 2.3a0: 62021
       Python 2.3a0: 62011 (!)
       Python 2.4a0: 62041
       Python 2.4a3: 62051
       Python 2.4b1: 62061
       Python 2.5a0: 62071
       Python 2.5a0: 62081 (ast-branch)
       Python 2.5a0: 62091 (with)
       Python 2.5a0: 62092 (changed WITH_CLEANUP opcode)
       Python 2.5b3: 62101 (fix wrong code: for x, in ...)
       Python 2.5b3: 62111 (fix wrong code: x += yield)
       Python 2.5c1: 62121 (fix wrong lnotab with for loops and
                            storing constants that should have been removed)
       Python 2.5c2: 62131 (fix wrong code: for x, in ... in listcomp/genexp)
       Python 2.6a0: 62151 (peephole optimizations and STORE_MAP opcode)
       Python 2.6a1: 62161 (WITH_CLEANUP optimization)
.
*/
#define MAGIC (62161 | ((long)'\r'<<16) | ((long)'\n'<<24))

/* Magic word as global; note that _PyImport_Init() can change the
   value of this global to accommodate for alterations of how the
   compiler works which are enabled by command line switches. */
static long pyc_magic = MAGIC;

2.5.2 pyc 创建时间

        使得 Python 自动将 pyc 文件与最新的 Python 文件同步。当对 Python 程序进行编译产生 pyc 后,如果后来进行了修改,此时 Python 在尝试加载 pyc 时,会发现 pyc 创建时间早于 Python 程序,于是将重新编译,生成新的 pyc 文件。

2.5.3 PyCodeObject

        编译器会遍历 PyCodeObject 中的所有域,并依次写入 pyc。对于 PyCodeObject 中的每一个对象,同样会进行遍历,并写入类型标志数据(数值/字符串)

        类型标志的三个作用:表明上一个对象的结束新对象的开始确定新对象的类型

marshal.h,类型标志

#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          '>'

PyCodeObject对象的写入:

void
PyMarshal_WriteLongToFile(long x, FILE *fp, int version)
{
    WFILE wf;
    wf.fp = fp;
    wf.error = 0;
    wf.depth = 0;
    wf.strings = NULL;
    wf.version = version;
    w_long(x, &wf);
}

void
PyMarshal_WriteObjectToFile(PyObject *x, FILE *fp, int version)
{
    WFILE wf;
    wf.fp = fp;
    wf.error = 0;
    wf.depth = 0;
    wf.strings = (version > 0) ? PyDict_New() : NULL;
    wf.version = version;
    w_object(x, &wf);
    Py_XDECREF(wf.strings);
}

以上是将内容写入文件调用的函数:

  • w_long()函数就是将数据一个字节一个字节写入文件中
  • w_object()函数的代码很长,但是逻辑很简单,内部其实就是针对不同的类型(intstring等)做不同的操作。但是最源头都是w_longw_string(毕竟所有的字符最后不是数字就是字母)。
  • Python在向pyc文件写入list的时候,只是将list中的数值或字符串写入文件;这也就意味着Python在加载pyc文件的时候,需要根据这些数值或字符串重新组装list
  • 写入每一种类型的数据时,都会先写入一个类型标识,要不然咋知道那一堆二进制字节流是什么意思呢。一个类型标识符的出现,预示着一个对象的开始,上一个对象的结束。

        不论写入多复杂的对象,归结到最后都是数值的写入字符串的写入。数值写入很简单,直接写入即可。字符串的写入相对复杂。

2.5.4 字符串写入pyc文件

typedef struct {
    FILE *fp;
    int error;
    int depth;
    /* If fp == NULL, the following are valid: */
    PyObject *str;
    char *ptr;
    char *end;
    PyObject *strings; /* dict on marshal, list on unmarshal */
    int version;
} WFILE;
  • WFILE结构就是对FILE文件句柄的封装,strings域需要重点关注一下,它在向pyc文件写入时,string会指向PyDictObject对象;而从pyc文件读出时,string则会指向PyListObject对象。
  • 写入pyc文件时,PyMarshal_WriteObjectToFile()函数就创建了PyDictObject对象(wf.strings = (version > 0) ? PyDict_New() : NULL;)。
  • string对象的键值对存储的结构为(PyStringObject, PyIntObject),PyIntObject对象表示的是第几个被写入到pyc文件的intern字符串(类似一个索引,到从pyc文件读取时,即string指向PyListObject对象时会用到)。
/* w_object中处理string的代码 */
else if (PyString_Check(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 = 1;
                    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 = 1;
            return;
        }
        w_long((long)n, p);
        w_string(PyString_AS_STRING(v), (int)n, p);
    }

写入分为三种情况

  • 普通字符串:写入类型标识TYPE_STRINGw_long()写入字符串长度,w_string()写入字符串本身
  • intern字符串首次写入:string字典中加入字符串的键值对,写入类型标识符TYPE_INTERNED,后面和普通字符串一样
  • intern字符串非首次写入:写入类型标识TYPE_STRINGREF,然后w_long()写入string中查找到的key对应的value

2.2.5 pyc文件读出字符串

PyObject *
PyMarshal_ReadObjectFromFile(FILE *fp)
{
    RFILE rf;
    PyObject *result;
    rf.fp = fp;
    rf.strings = PyList_New(0);
    rf.depth = 0;
    rf.ptr = rf.end = NULL;
    result = r_object(&rf);
    Py_DECREF(rf.strings);
    return result;
}
  • 如上所示,pyc文件读取内容时,string被初始化为PyListObject对象
  • 进入r_object()r_objectw_object的逆运算),开始从pyc文件读取数据,创建PyCodeObject对象。
  • 当读取到TYPE_INTERNED标识符时,将标识符后面的字符串进行intern操作,然后将它加到string指向的PyListObject对象中;
  • 当读取到TYPE_STRINGREF标识符时,直接根据写入时存储的索引IDstring中查找已经intern的字符串

2.6 字节码

        源代码编译为 字节码指令 序列,虚拟机根据字节码进行操作,完成程序的执行,opcode.h中定义了当前版本 Python 支持的字节码指令。

       字节码指令 的编码并不是按顺序增长的,中间有跳跃。

    Include目录下的opcode.h定义了字节码指令。

#define STOP_CODE   0
#define POP_TOP     1
#define ROT_TWO     2
#define ROT_THREE   3
#define DUP_TOP     4
#define ROT_FOUR    5
#define NOP     9

#define UNARY_POSITIVE  10
#define UNARY_NEGATIVE  11
#define UNARY_NOT   12
#define UNARY_CONVERT   13

#define UNARY_INVERT    15

#define LIST_APPEND 18
#define BINARY_POWER    19

#define BINARY_MULTIPLY 20
#define BINARY_DIVIDE   21
#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
#define INPLACE_TRUE_DIVIDE 29

#define SLICE       30
/* Also uses 31-33 */

#define STORE_SLICE 40
/* Also uses 41-43 */

#define DELETE_SLICE    50
/* Also uses 51-53 */

#define STORE_MAP   54
#define INPLACE_ADD 55
#define INPLACE_SUBTRACT    56
#define INPLACE_MULTIPLY    57
#define INPLACE_DIVIDE  58
#define INPLACE_MODULO  59
#define STORE_SUBSCR    60
#define DELETE_SUBSCR   61

#define BINARY_LSHIFT   62
#define BINARY_RSHIFT   63
#define BINARY_AND  64
#define BINARY_XOR  65
#define BINARY_OR   66
#define INPLACE_POWER   67
#define GET_ITER    68

#define PRINT_EXPR  70
#define PRINT_ITEM  71
#define PRINT_NEWLINE   72
#define PRINT_ITEM_TO   73
#define PRINT_NEWLINE_TO 74
#define INPLACE_LSHIFT  75
#define INPLACE_RSHIFT  76
#define INPLACE_AND 77
#define INPLACE_XOR 78
#define INPLACE_OR  79
#define BREAK_LOOP  80
#define WITH_CLEANUP    81
#define LOAD_LOCALS 82
#define RETURN_VALUE    83
#define IMPORT_STAR 84
#define EXEC_STMT   85
#define YIELD_VALUE 86
#define POP_BLOCK   87
#define END_FINALLY 88
#define BUILD_CLASS 89

#define HAVE_ARGUMENT   90  /* Opcodes from here have an argument: */

#define STORE_NAME  90  /* Index in name list */
#define DELETE_NAME 91  /* "" */
#define UNPACK_SEQUENCE 92  /* Number of sequence items */
#define FOR_ITER    93

#define STORE_ATTR  95  /* Index in name list */
#define DELETE_ATTR 96  /* "" */
#define STORE_GLOBAL    97  /* "" */
#define DELETE_GLOBAL   98  /* "" */
#define DUP_TOPX    99  /* number of items to duplicate */
#define LOAD_CONST  100 /* Index in const list */
#define LOAD_NAME   101 /* Index in name list */
#define BUILD_TUPLE 102 /* Number of tuple items */
#define BUILD_LIST  103 /* Number of list items */
#define BUILD_MAP   104 /* Always zero for now */
#define LOAD_ATTR   105 /* Index in name list */
#define COMPARE_OP  106 /* Comparison operator */
#define IMPORT_NAME 107 /* Index in name list */
#define IMPORT_FROM 108 /* Index in name list */

#define JUMP_FORWARD    110 /* Number of bytes to skip */
#define JUMP_IF_FALSE   111 /* "" */
#define JUMP_IF_TRUE    112 /* "" */
#define JUMP_ABSOLUTE   113 /* Target byte offset from beginning of code */

#define LOAD_GLOBAL 116 /* Index in name list */

#define CONTINUE_LOOP   119 /* Start of loop (absolute) */
#define SETUP_LOOP  120 /* Target address (relative) */
#define SETUP_EXCEPT    121 /* "" */
#define SETUP_FINALLY   122 /* "" */

#define LOAD_FAST   124 /* Local variable number */
#define STORE_FAST  125 /* Local variable number */
#define DELETE_FAST 126 /* Local variable number */

#define RAISE_VARARGS   130 /* Number of raise arguments (1, 2 or 3) */
/* CALL_FUNCTION_XXX opcodes defined below depend on this definition */
#define CALL_FUNCTION   131 /* #args + (#kwargs<<8) */
#define MAKE_FUNCTION   132 /* #defaults */
#define BUILD_SLICE     133 /* Number of items */

#define MAKE_CLOSURE    134     /* #free vars */
#define LOAD_CLOSURE    135     /* Load free variable from closure */
#define LOAD_DEREF      136     /* Load and dereference from closure cell */ 
#define STORE_DEREF     137     /* Store into cell */ 

/* The next 3 opcodes must be contiguous and satisfy
   (CALL_FUNCTION_VAR - CALL_FUNCTION) & 3 == 1  */
#define CALL_FUNCTION_VAR          140  /* #args + (#kwargs<<8) */
#define CALL_FUNCTION_KW           141  /* #args + (#kwargs<<8) */
#define CALL_FUNCTION_VAR_KW       142  /* #args + (#kwargs<<8) */

/* Support for opargs more than 16 bits long */
#define EXTENDED_ARG  143

2.7 解析 pyc

        由于包含嵌套 PyCodeObjectpyc 中的二进制数据实际上是有结构的,可以以 XML格式进行解析,从而可视化。使用 pycparser

        而 Python 库中 disdis 方法可以对 code对象 进行解析。接收 code对象,输出 字节码指令信息。

dis.dis 的输出:第一列,是 字节码指令 对应的 源代码 在 Python 程序中的行数

  • 第二列,是当前 字节码指令 在 co_code 中的偏移位置
  • 第三列,当前的字节码指令
  • 第四列,当前字节码指令的参数

test.py

import sys

a = 1

def b():
    print a
    a = 2
    print a
>>> source = open('/Users/chao/Desktop/test.py').read()
>>> co = compile(source, 'test.py', 'exec')
>>> import dis
>>> dis.dis(co)
  1           0 LOAD_CONST               0 (-1)
              3 LOAD_CONST               1 (None)
              6 IMPORT_NAME              0 (sys)
              9 STORE_NAME               0 (sys)

  3          12 LOAD_CONST               2 (1)
             15 STORE_NAME               1 (a)

  5          18 LOAD_CONST               3 (<code object b at 0x1005dc930, file "test.py", line 5>)
             21 MAKE_FUNCTION            0
             24 STORE_NAME               2 (b)
             27 LOAD_CONST               1 (None)
             30 RETURN_VALUE
>>> type(co)
<type 'code'>
>>> dir(co)
['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
>>> print co.co_names
('sys', 'a', 'b')
>>> print co.co_name
<module>
>>> print co.co_filename
test.py

2.8 生成pyc文件

        之所以需要pyc文件,因为py文件是可以直接看到源码的,如果你是开发商业软件的话,不可能把源码也泄漏出去吧?所以就需要编译为pyc后,再发布出去。当然,pyc文件也是可以反编译的,不同版本编译后的pyc文件是不同的,根据python源码中提供的opcode,可以根据pyc文件反编译出 py文件源码,网上可以找到一个反编译python2.3版本的pyc文件的工具,不过该工具从python2.4开始就要收费了,如果需要反编译出新版本的pyc文件的话,就需要自己动手了,不过你可以自己修改python的源代码中的opcode文件,重新编译 python,从而防止不法分子的破解。

2.8.1 生成单个pyc文件(py_compile.compile)

        python提供了内置的类库来实现把py文件编译为pyc文件,这个模块就是 py_compile 模块。

        使用方法非常简单,如下所示,直接在idle中,就可以把一个py文件编译为pyc文件了。(假设在windows环境下)

import py_compile

py_compile.compile(r'H:\game\test.py')

compile函数原型:

compile(file[, cfile[, dfile[, doraise]]])

  • file 表示需要编译的py文件的路径
  • cfile 表示编译后的pyc文件名称和路径,默认为直接在file文件名后加c 或者 o,o表示优化的字节码
  • dfile 在错误信息中显示的file用dfile替换
  • doraise 可以是两个值,True或者False,如果为True,则会引发一个PyCompileError,否则如果编译文件出错,则会有一个错误,默认显示在sys.stderr中,而不会引发异常

2.8.2 批量生成pyc文件(compileall.compile_dir)

        一般来说,我们的工程都是在一个目录下的,一般不会说仅仅编译一个py文件而已,而是需要把整个文件夹下的py文件都编译为pyc文件,python又为了我们提供了另一个模块:compileall 。使用方法如下:

import compileall

compileall.compile_dir(r'H:\game')

        也可以直接用命令行编译一个目录下的文件,如:# python -m compileall /root/src/

        这样就把game目录,以及其子目录下的py文件编译为pyc文件了。嘿嘿,够方便吧。来看下compile_dir函数的说明:

compile_dir(dir[, maxlevels[, ddir[, force[, rx[, quiet]]]]])

  • dir 表示需要编译的文件夹位置
  • maxlevels 表示需要递归编译的子目录的层数,默认是10层,即默认会把10层子目录中的py文件编译为pyc
  • ddir 用来作为基本路径,用于将产生错误消息的文件名   原文:it is used as the base path from which the filenames used in error messages will be generated。
  • force 如果为True,则会强制编译为pyc,即使现在的pyc文件是最新的,还会强制编译一次,pyc文件中包含有时间戳,python编译器会根据时间来决定,是否需要重新生成一次pyc文件
  • rx 表示一个正则表达式,比如可以排除掉不想要的目录,或者只有符合条件的目录才进行编译
  • quiet 如果为True,则编译后,不会在标准输出中,打印出信息

3 pyc和pyo的生成方法

        pyc的作用是用来跨平台使用的,和Java中的Class文件类似。pyc文件是一种字节码文件,可以加快Python解释器的加载速度,当然也可以用来做简单的防源码泄露保护。

        pyo则是优化过后的字节码文件,不过pyo更像编译型语言里的中间文件。

        我们可以通过Python提供的py_compile模块来进行源代码的编译。

        py_compile模块只提供3个方法,分别是有关编译异常PyCompileError有关编译compile有关程序入口main。

compile方法原形:

        compile(file, cfile=None, dfile=None, doraise=False, optimize=-1)

有5个参数:

  • file:必选参数,要编译的源文件
  • cfile:编译后的文件,默认在源文件目录下的__pycache__/源文件名.解释器类型-python版本.字节码类型。     ###例如:__pycache__/abc.cpython-34.pyo
  • dfile:错误消息文件,默认和cfile一样
  • doraise:是否开启异常处理,默认False
  • optimize:优化字节码级别

这里分为4个等级,文档中是这样写的:

optimize级别:

        param optimize: The optimization level for the compiler. Valid values are -1, 0, 1 and 2. A value of -1 means to use the optimization level of the current interpreter, as given by -O command line options.

optimize为1时,优化字节码级别为最高

  • -1和0:设置pyc优化级别
  • 1和2:设置pyo优化级别
  • 数字越小,优化级别越高

示例:

        准备源文件a.py和b.py,内容相同,就是一句 print("python") 代码。

import py_compile

py_compile.compile(file = "a.py",cfile = "a.pyc",optimize=-1)

py_compile.compile(file = "b.py",cfile = "b.pyo",optimize=1)

运行后可以看到已经成功编译成字节码文件了,分别为a.pycb.pyo

可以使用 python a.pycpython b.pyo 命令运行这2个字节码文件。

也可以直接通过Python加载模块来运行:

#编译成pyc

python -m py_compile 源代码

#编译成pyo

python -O -m py_compile 源代码

        这确实可以简单地保护我们的代码,同时似乎看起来像是加密的效果,但是要注意,这不是加密,只是把源码变成优化后的字节码而已,如果想要获得源码,我们一样可以通过逆向编译来得到源码,目前有专门逆向Python字节码的工具存在。

4 pyd

        pyd可以让我们的代码更安全。如果真的想要保护代码,为何不考虑把它变成python扩展模块?(目前还没有pyd被反编译的消息)

        pyd是Python中的扩展模块,相当于windows的dll,不同的是pyd只供python调用而已。实际上,大部分的包、小模块都是以pyd形式发布的。如果特别感兴趣的小伙伴可以深入研究下setuptools和distutils。在把源代码转换成功pyd之前,我们需要用到Cython包。

pip list | findstr "Cython"

检查是否安装了Cython,没有请pip install Cython安装即可。

4.1 编译单个pyd

步骤1:生成C代码

import Cython.Build
#导入Build模块

Cython.Build.cythonize("a.py")
#a.py转换成C代码

       cythonize运行完成之后,无异常的情况下会在a.py的目录下创建一个a.c文件,同时会返回一个distutils.extension.Extension对象列表。

        一定要注意的是:如果在Python Shell测试,一定要用绝对路径,否则会ValueError异常,cythonize不会从sys.path中读路径。

步骤2:利用distutils生成pyd扩展模块

此时我们可以用distutils包来编译成我们要的pyd模块。

编译a.pypyd

import Cython.Build
import distutils.core

a = Cython.Build.cythonize("a.py")
#返回distutils.extension.Extension对象列表

distutils.core.setup(
name = 'pyd的编译',#包名称
version = "1.0",#包版本号
ext_modules= a,#扩展模块
author = "百家号——斌哥说Python",#作者
author_email='binnlzeng@163.com'#作者邮箱
)

python 执行编译的脚本 build   或    python 执行编译的脚本 build_ext

此时会在编译脚本所在目录生成一个build目录,里面存着C语言.def文件.o文件,还有我们要的pyd文件。

4.2 批量编译pyd文件

4.2.1 误区

       此时我们已经生成了1个pyd文件,如果我们是扩展包/模块的开发者,怎么批量编译呢?

      以下两个错误的示例:

(1):

a = Cython.Build.cythonize("a.py")
b = Cython.Build.cythonize("b.py")
distutils.core.setup(
...,
ext_modules= [a,b]
)

(2)

a = Cython.Build.cythonize("a.py")
a.append(Cython.Build.cythonize("b.py"))
distutils.core.setup(
...,
ext_modules= a
)

犯这样的错原因却是因为:

a = Cython.Build.cythonize("a.py")
type(a)

提示<class 'list'>

没错,Cython.Build.cythonize返回的是一个列表,里面有只有1个distutils.extension.Extension对象

报错如下:

需要1个Extension或者是2个元组。

4.2.2 批量编译pyd

方法1:提取我们要的Extension对象

import Cython.Build
import distutils.core

a = Cython.Build.cythonize("a.py")[0] #提取Extension对象
b = Cython.Build.cythonize("b.py")[0]
distutils.core.setup(
name = 'pyd的编译', #包名称
version = "1.0", #包版本号
ext_modules= [a,b], #被扩展的模块
author = "百家号——斌哥说Python", #作者
author_email='binnlzeng@163.com' #作者邮箱
)

方法2:转换成C代码后再进行Extension对象实例化

import Cython.Build
import distutils.core

Cython.Build.cythonize("a.py")
Cython.Build.cythonize("b.py")
distutils.core.setup(
name = 'pyd的编译', #包名称
version = "1.0", #包版本号
ext_modules= [distutils.core.Extension('a',["a.c"]),distutils.core.Extension('b', ['b.c'])], #被扩展的模块
#[
#distutils.core.Extension('a',["a.c"]),
#distutils.core.Extension('b', ['b.c'])
#]
author = "百家号——斌哥说Python", #作者
author_email='binnlzeng@163.com' #作者邮箱
)

        pycpyo相对而言安全性较低,pyd是目前解决Python开发中代码安全性最优的一个方案。

        但是要注意一点:无论是pyc还是pyo、pyd,都是跟着Python版本走的,不要指望Python2.7的东西在Python3上完美运行。

        PS:如果遇到running build...提示,删掉build目录重新编译即可。

 

 

 

https://baijiahao.baidu.com/s?id=1618495304088415793&wfr=spider&for=pc

https://www.jianshu.com/p/152820587935

https://www.jianshu.com/p/b7315edbf7b7

https://www.cnblogs.com/hu-yewen/p/5449058.html

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值