目录
2.8.1 生成单个pyc文件(py_compile.compile)
2.8.2 批量生成pyc文件(compileall.compile_dir)
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
动态库 - 对比
java
:a.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_compile
和compile
可以帮助手动产生 pyc 文件。
每一个PyCodeObject对象都包含了每一个Code Block中所有Python源代码经过编译后得到的byte code序列,Python会将这些字节码序列和PyCodeObject对象一起存储在pyc文件中。pyc 文件内容是二进制的,要了解pyc文件,首先我们必须清楚PyCodeObject中大部分域所代表的含义。
Field | Content |
co_argcount | Code Block的位置参数个数,比如说一个函数的位置参数个数 |
co_nlocals | Code 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()
函数的代码很长,但是逻辑很简单,内部其实就是针对不同的类型(int
、string
等)做不同的操作。但是最源头都是w_long
和w_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_STRING
,w_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_object
是w_object
的逆运算),开始从pyc
文件读取数据,创建PyCodeObject
对象。 - 当读取到
TYPE_INTERNED
标识符时,将标识符后面的字符串进行intern
操作,然后将它加到string
指向的PyListObject
对象中; - 当读取到
TYPE_STRINGREF
标识符时,直接根据写入时存储的索引ID
在string
中查找已经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
由于包含嵌套 PyCodeObject,pyc 中的二进制数据实际上是有结构的,可以以 XML格式进行解析,从而可视化。使用 pycparser。
而 Python 库中 dis 的 dis 方法可以对 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.pyc和b.pyo。
可以使用 python a.pyc 和python 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.py成pyd
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' #作者邮箱
)
pyc和pyo相对而言安全性较低,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