python代码被解释器执行时分为两步走:
一、python编译器将代码编译成字节码
二、python虚拟机执行字节码
由于这两步是一起的,所以在python编程中很少能看到字节码。但是想要提高代码效率就必须知道一段python源码对应的字节码是怎么样的,拨开迷雾看本质。本文分析python常见的能编译成字节码相关的函数。
compile
compile()
函数是python的内置函数,功能是将python源代码编译为code对象或AST对象。
函数定义:
compile(source, filename, mode[, flags[, dont_inherit]])
参数说明
:
- source:字符串或AST(Abstract Syntax Trees)对象,表示需要进行编译的Python代码
- filename:指定需要编译的代码文件名称,如果不是从文件读取代码则传递一些可辨认的值(通常是用'
') - mode:用于标识必须当做那类代码来编译,规则如下:
如果source是由一个代码语句序列组成,则指定mode='exec';
如果source是由单个表达式组成,则指定mode='eval';
如果source是由一个单独的交互式语句组成,则指定mode='single'。 - 另外两个可选参数暂不做介绍
简单例子
ss = """
a = 100
b = 200
c = a + b
print(c)
"""
co = compile(ss, 'string', 'exec')
print(co)
print(dir(co))
exec(ss)
<code object <module> at 0x7f51c25e8810, file "string", line 2>
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__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_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
300
变量co
就是一个字节码对象,通过dir(co)
能够看到该字节码对象中有很多属性。code对象
是一个对象,在CPyhon解释器中,该对象就是一个结构体,结构体如下:
typedef struct {
PyObject_HEAD /* 头部信息, 我们看到真的一切皆对象, 字节码也是个对象 */
int co_argcount; /* 可以通过位置参数传递的参数个数 */
int co_posonlyargcount; /* 只能通过位置参数传递的参数个数, Python3.8新增 */
int co_kwonlyargcount; /* 只能通过关键字参数传递的参数个数 */
int co_nlocals; /* 代码块中局部变量的个数,也包括参数 */
int co_stacksize; /* 执行该段代码块需要的栈空间 */
int co_flags; /* 参数类型标识 */
int co_firstlineno; /* 代码块在对应文件的行号 */
PyObject *co_code; /* 指令集, 也就是字节码, 它是一个bytes对象 */
PyObject *co_consts; /* 常量池, 一个元组,保存代码块中的所有常量。 */
PyObject *co_names; /* 一个元组,保存代码块中引用的其它作用域的变量 */
PyObject *co_varnames; /* 一个元组,保存当前作用域中的变量 */
PyObject *co_freevars; /* 内层函数引用的外层函数的作用域中的变量 */
PyObject *co_cellvars; /* 外层函数中作用域中被内层函数引用的变量,本质上和co_freevars是一样的 */
Py_ssize_t *co_cell2arg; /* 无需关注 */
PyObject *co_filename; /* 代码块所在的文件名 */
PyObject *co_name; /* 代码块的名字,通常是函数名或者类名 */
PyObject *co_lnotab; /* 字节码指令与python源代码的行号之间的对应关系,以PyByteObject的形式存在 */
//剩下的无需关注了
void *co_zombieframe; /* for optimization only (see frameobject.c) */
PyObject *co_weakreflist; /* to support weakrefs to code objects */
void *co_extra;
unsigned char *co_opcache_map;
_PyOpcache *co_opcache;
int co_opcache_flag;
unsigned char co_opcache_size;
} PyCodeObject;
可以看到该结构提中的有很多成员变量,这些成员变量也反应在co
的变量和方法中。比较重要的有:
- co_code 字节码
- co_consts 常量池
- co_names 符号池
到这里我们能看到的还是code对象
,并没有看到具体的字节码,co_code 是指向具体的字节码,但是由于是2进制,不便于查看。所有如果想看到纯粹的字节码,我们可以用下面的dis模块。
dis
dis是python的标准库模块,功能是将一段python代码编译成字节码指令。
python代码的执行过程分为两步:1.python解释器将代码编译成字节码;2.python虚拟机执行字节码。通常这两个步骤是一次性完成,所以我们看不到中间状态的字节码。而dis就可以将一段源码编译成字节码。
从上面的code对象
中可以到看字节码是其一个成员变量co_code
,字节码是被底层结构体PyCodeObject的成员co_code指向,那么dis可以取出字节码指令。
简单例子
import dis
ss = """
a = 100
b = 200
c = a + b
print(c)
"""
byte_str = dis.dis(ss)
ss这一段python代码的字节码就如下:
2 0 LOAD_CONST 0 (100)
2 STORE_NAME 0 (a)
3 4 LOAD_CONST 1 (200)
6 STORE_NAME 1 (b)
4 8 LOAD_NAME 0 (a)
10 LOAD_NAME 1 (b)
12 BINARY_ADD
14 STORE_NAME 2 (c)
5 16 LOAD_NAME 3 (print)
18 LOAD_NAME 2 (c)
20 CALL_FUNCTION 1
22 POP_TOP
24 LOAD_CONST 2 (None)
26 RETURN_VALUE
首先解释一下,python编译器编译过一段代码之后,生成的是code对象
,对象就是compile中看到的结构体。这个对象中有字节码信息co_code
,也有一些变量的信息,就是 co_consts
常量池 和 co_names
符号池。那么把这两个成员也打印出来。
(100, 200, None)
('a', 'b', 'c', 'print')
下面解释一下字节码。这段字节码就是python源码被编译之后的样子,也是最终被执行的对象。
2 0 LOAD_CONST 0 (100)
2 STORE_NAME 0 (a)
2 代表字节码对应的源码的行号
0 LOAD_CONST 代表字节码的行号和字节码指令
0 (100) 代表字节码的操作数
完整的说
2 0 LOAD_CONST 0 (100)
意思是:从常量池加载一个常量,加载的常量是序号为0,值为100
2 STORE_NAME 0 (a)
意思是:从符号池取一个符号,符号的序号是0,值为a,绑定到上一个操作的常量100。然后将a=100,存入命名空间中。
3 4 LOAD_CONST 1 (200)
6 STORE_NAME 1 (b)
和上面的类似。从常量池中取下标为1的常量,值为200,然后去符号池去下标为1的符号为b,绑定b=200,然后存入到命名空间中。
8 LOAD_NAME 0 (a)
10 LOAD_NAME 1 (b)
12 BINARY_ADD
14 STORE_NAME 2 (c)
8 LOAD_NAME 0 (a)
加载一个符号对应的值,符号是a,所以加载的值是100,
10 LOAD_NAME 1 (b)
加载符号b对应的值,值为200
12 BINARY_ADD
加法运算
14 STORE_NAME 2 (c)
从符号池去下标为2的元素,为符号c,然后绑定到上一步的结果300,然后存入到命名空间中。
inspect
inspect是python标准库模块,inspect模块四大功能:
1、类型检查(type checking)
2、获取源码(getting source code)
3、获取类和方法的参数信息(inspecting classes and functions)
4、解析堆栈(examining the interpreter stack)
其中对于分析代码字节码重要的包括:
inspect.stack()
获取调用者当前的堆栈信息
inspect.currentframe()
获取调用者当前Frame对象信息
简单例子
import inspect
def fun_demo():
a = 100
b = 200
c = a + b
s_ob = inspect.stack()
print(s_ob)
f_ob = inspect.currentframe()
print(f_ob)
print(dir(f_ob))
fun_demo()
[FrameInfo(frame=<frame at 0x7f9e884e4048, file 'inspect_demo.py', line 10, code fun_demo>, filename='inspect_demo.py', lineno=9, function='fun_demo', code_context=[' s_ob = inspect.stack()
'], index=0), FrameInfo(frame=<frame at 0x7f9e8861a9f8, file 'inspect_demo.py', line 18, code <module>>, filename='inspect_demo.py', lineno=18, function='<module>', code_context=['fun_demo()
'], index=0)]
<frame at 0x7f9e884e4048, file 'inspect_demo.py', line 13, code fun_demo>
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'f_back', 'f_builtins', 'f_code', 'f_globals', 'f_lasti', 'f_lineno', 'f_locals', 'f_trace', 'f_trace_lines', 'f_trace_opcodes']
Frame对象是python真正执行的对象,字节码对象code对象
属于Frame对象
。
我们都知道python解释器是模拟了真实的机器,所以需要有堆栈信息,而Frame就是python代码执行的堆栈信息的体现。其在Cpython中是一个结构体,如下:
typedef struct _frame {
PyObject_VAR_HEAD /* 可变对象的头部信息 */
struct _frame *f_back; /* 上一级栈帧, 也就是调用者的栈帧 */
PyCodeObject *f_code; /* PyCodeObject对象, 通过栈帧对象的f_code可以获取对应的PyCodeObject对象 */
PyObject *f_builtins; /* builtin命名空间,一个PyDictObject对象 */
PyObject *f_globals; /* global命名空间,一个PyDictObject对象 */
PyObject *f_locals; /* local命名空间,一个PyDictObject对象 */
PyObject **f_valuestack; /* 运行时的栈底位置 */
PyObject **f_stacktop; /* 运行时的栈顶位置 */
PyObject *f_trace; /* 回溯函数,打印异常栈 */
char f_trace_lines; /* 是否触发每一行的回溯事件 */
char f_trace_opcodes; /* 是否触发每一个操作码的回溯事件 */
PyObject *f_gen; /* 是否是生成器 */
int f_lasti; /* 上一条指令在f_code中的偏移量 */
int f_lineno; /* 当前字节码对应的源代码行 */
int f_iblock; /* 当前指令在栈f_blockstack中的索引 */
char f_executing; /* 当前栈帧是否仍在执行 */
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* 用于try和loop代码块 */
PyObject *f_localsplus[1]; /* 动态内存,维护局部变量+cell对象集合+free对象集合+运行时栈所需要的空间 */
} PyFrameObject;
其中 *f_code
就指向的是code对象
,除此之外还有
- *f_builtins 代码执行的内建命名空间
- *f_globals 代码执行的全局命名空间
- *f_locals 代码执行的局部命名空间
小结
通过这三个模块来剖析额字节码,涉及到很多python解释器的内容,更多细节待补充。