共计 6838 个字符,预计需要花费 18 分钟才能阅读完成。
深刻了解 python 虚拟机:字节码灵魂——Code obejct
在本篇文章当中次要给大家深刻介绍在 cpython 当中十分重要的一个数据结构 code object! 在上一篇文章 深刻了解 python 虚拟机:pyc 文件构造,咱们简略介绍了一下在 code object 当中有哪些字段以及这些字段的简略含意,在本篇文章当中将会举一些例子以便更加深刻了解这些字段。
Code Object 数据结构
typedef struct { | |
PyObject_HEAD | |
int co_argcount; /* #arguments, except *args */ | |
int co_kwonlyargcount; /* #keyword only arguments */ | |
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 aren't used in either hash or comparisons, except for | |
co_name (used in both) and co_firstlineno (used only in | |
comparisons). This is done to preserve the name and line number | |
for tracebacks and debuggers; otherwise, constant de-duplication | |
would collapse identical functions/lambdas defined on different lines. | |
*/ | |
unsigned char *co_cell2arg; /* Maps cell vars which are arguments. */ | |
PyObject *co_filename; /* unicode (where it was loaded from) */ | |
PyObject *co_name; /* unicode (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; |
上面是 code object 当中各个字段的作用:
- 首先须要理解一下代码块这个概念,所谓代码块就是一个小的 python 代码,被当做一个小的单元整体执行。在 python 当中常见的代码块块有:函数体、类的定义、一个模块。
- argcount,这个示意一个代码块的参数个数,这个参数只对函数体代码块有用,因为函数可能会有参数,比方下面的 pycdemo.py 是一个模块而不是一个函数,因而这个参数对应的值为 0。
- co_code,这个对象的具体内容就是一个字节序列,存储实在的 python 字节码,次要是用于 python 虚拟机执行的,在本篇文章当中临时不详细分析。
- co_consts,这个字段是一个列表类型的字段,次要是蕴含一些字符串常量和数值常量,比方下面的 “\_\_main\_\_” 和 100。
- co_filename,这个字段的含意就是对应的源文件的文件名。
- co_firstlineno,这个字段的含意为在 python 源文件当中第一行代码呈现的行数,这个字段在进行调试的时候十分重要。
- co_flags,这个字段的次要含意就是标识这个 code object 的类型。0x0080 示意这个 block 是一个协程,0x0010 示意这个 code object 是嵌套的等等。
- co_lnotab,这个字段的含意次要是用于计算每个字节码指令对应的源代码行数。
- co_varnames,这个字段的次要含意是示意在一个 code object 本地定义的一个名字。
- co_names,和 co_varnames 相同,示意非本地定义然而在 code object 当中应用的名字。
- co_nlocals,这个字段示意在一个 code object 当中本地应用的变量个数。
- co_stackszie,因为 python 虚拟机是一个栈式计算机,这个参数的值示意这个栈须要的最大的值。
- co_cellvars,co_freevars,这两个字段次要和嵌套函数和函数闭包无关,咱们在后续的文章当中将具体解释这个字段。
CodeObject 详细分析
当初咱们应用一些理论的例子来剖析具体的 code object。
import dis | |
import binascii | |
import types | |
d = 10 | |
def test_co01(c): | |
a = 1 | |
b = 2 | |
return a + b + c + d |
在后面的文章当中咱们提到过一个函数是包含一个 code object 对象,test_co01 的 code object 对象的输入后果(残缺代码见 co01)如下所示:
code | |
argcount 1 | |
nlocals 3 | |
stacksize 2 | |
flags 0043 0x43 | |
code b'6401007d01006402007d02007c01007c0200177c0000177400001753' | |
9 0 LOAD_CONST 1 (1) | |
3 STORE_FAST 1 (a) | |
10 6 LOAD_CONST 2 (2) | |
9 STORE_FAST 2 (b) | |
11 12 LOAD_FAST 1 (a) | |
15 LOAD_FAST 2 (b) | |
18 BINARY_ADD | |
19 LOAD_FAST 0 (c) | |
22 BINARY_ADD | |
23 LOAD_GLOBAL 0 (d) | |
26 BINARY_ADD | |
27 RETURN_VALUE | |
consts | |
None | |
1 | |
2 | |
names ('d',) | |
varnames ('c', 'a', 'b') | |
freevars () | |
cellvars () | |
filename '/tmp/pycharm_project_396/co01.py' | |
name 'test_co01' | |
firstlineno 8 | |
lnotab b'000106010601' |
- 字段 argcount 的值等于 1,阐明函数有一个参数,这个函数 test_co01 有一个参数 c 是互相对应的。
- 字段 nlocals 的值等于 3,阐明在函数 test_co01 当中一个一共实现了三个函数本地变量 a, b, c。
- 字段 names,对应代码代码当中的 co_names,依据后面的定义就是 d 这个全局变量在函数 test_co01 当中应用,然而却没有在函数当中定义了。
- 字段 varnames,这个就示意在本地定义应用的变量了,在函数 test_co01 当中次要有三个变量 a, b, c。
- 字段 filename,就是 python 文件的地址了。
- 字段 firstlineno 阐明函数的第一行呈现在对应 python 代码的 第 8 行。
Flags 字段详细分析
咱们具体应用 python3.5 的源代码进行剖析,在 cpython 虚拟机的具体实现如下所示(Include/code.h):
/* Masks for co_flags above */ | |
#define CO_OPTIMIZED 0x0001 | |
#define CO_NEWLOCALS 0x0002 | |
#define CO_VARARGS 0x0004 | |
#define CO_VARKEYWORDS 0x0008 | |
#define CO_NESTED 0x0010 | |
#define CO_GENERATOR 0x0020 | |
/* The CO_NOFREE flag is set if there are no free or cell variables. | |
This information is redundant, but it allows a single flag test | |
to determine whether there is any extra work to be done when the | |
call frame it setup. | |
*/ | |
#define CO_NOFREE 0x0040 | |
/* The CO_COROUTINE flag is set for coroutine functions (defined with | |
``async def`` keywords) */ | |
#define CO_COROUTINE 0x0080 | |
#define CO_ITERABLE_COROUTINE 0x0100 |
如果 flags 字段和下面的各个宏定义进行 & 运算,如果失去的后果大于 0,则阐明合乎对应的条件。
下面的宏定义的含意如下所示:
- CO_OPTIMIZED,这个字段示意 code object 是被优化过的,应用函数本地定义的变量。
- CO_NEWLOCALS,这个字段的含意为当这个 code object 的代码被执行的时候会给栈帧当中的 f_locals 对象创立一个 dict 对象。
- CO_VARARGS,示意这个 code object 对象是否含有地位参数。
- CO_VARKEYWORDS,示意这个 code object 是否含有关键字参数。
- CO_NESTED,示意这个 code object 是一个嵌套函数。
- CO_GENERATOR,示意这个 code object 是一个生成器。
- CO_COROUTINE,示意这个 code object 是一个协程函数。
- CO_ITERABLE_COROUTINE,示意 code object 是一个可迭代的协程函数。
- CO_NOFREE,这个示意没有 freevars 和 cellvars,即没有函数闭包。
当初再剖析一下后面的函数 test_co01 的 flags,他对应的值等于 0x43,则阐明这个函数满足三个个性别离是 CO_NEWLOCALS,CO_OPTIMIZED 和 CO_NOFREE。
freevars & cellvars
咱们应用上面的函数来对这两个字段进行剖析:
def test_co02(): | |
a = 1 | |
b = 2 | |
def g(): | |
return a + b | |
return a + b + g() |
下面的函数的信息如下所示(残缺代码见 co02):
code | |
argcount 0 | |
nlocals 1 | |
stacksize 3 | |
flags 0003 0x3 | |
code | |
b'640100890000640200890100870000870100660200640300640400860000' | |
b'7d0000880000880100177c00008300001753' | |
15 0 LOAD_CONST 1 (1) | |
3 STORE_DEREF 0 (a) | |
16 6 LOAD_CONST 2 (2) | |
9 STORE_DEREF 1 (b) | |
18 12 LOAD_CLOSURE 0 (a) | |
15 LOAD_CLOSURE 1 (b) | |
18 BUILD_TUPLE 2 | |
21 LOAD_CONST 3 (<code object g at 0x7f133ff496f0, file "/tmp/pycharm_project_396/co01.py", line 18>) | |
24 LOAD_CONST 4 ('test_co02.<locals>.g') | |
27 MAKE_CLOSURE 0 | |
30 STORE_FAST 0 (g) | |
20 33 LOAD_DEREF 0 (a) | |
36 LOAD_DEREF 1 (b) | |
39 BINARY_ADD | |
40 LOAD_FAST 0 (g) | |
43 CALL_FUNCTION 0 (0 positional, 0 keyword pair) | |
46 BINARY_ADD | |
47 RETURN_VALUE | |
consts | |
None | |
1 | |
2 | |
code | |
argcount 0 | |
nlocals 0 | |
stacksize 2 | |
flags 0013 0x13 | |
code b'8800008801001753' | |
19 0 LOAD_DEREF 0 (a) | |
3 LOAD_DEREF 1 (b) | |
6 BINARY_ADD | |
7 RETURN_VALUE | |
consts | |
None | |
names () | |
varnames () | |
freevars ('a', 'b') | |
cellvars () | |
filename '/tmp/pycharm_project_396/co01.py' | |
name 'g' | |
firstlineno 18 | |
lnotab b'0001' | |
'test_co02.<locals>.g' | |
names () | |
varnames ('g',) | |
freevars () | |
cellvars ('a', 'b') | |
filename '/tmp/pycharm_project_396/co01.py' | |
name 'test_co02' | |
firstlineno 14 | |
lnotab b'0001060106021502' |
从下面的输入咱们能够看到的是,函数 test_co02 的 cellvars 为 (‘a’, ‘b’),函数 g 的 freevars 为 (‘a’, ‘b’),cellvars 示意在其余函数当中会应用本地定义的变量,freevars 示意本地会应用其余函数定义的变量。
再来剖析一下函数 test_co02 的 flags,他的 flags 等于 0x3 因为有闭包的存在因而 flags 不会存在 CO_NOFREE,也就是少了值 0x0040。
stacksize
这个字段存储的是在函数在被虚拟机执行的时候所须要的最大的栈空间的大小,这也是一种优化伎俩,因为在晓得所须要的最大的栈空间,所以能够在函数执行的时候间接调配指定大小的空间不须要在函数执行的时候再去从新扩容。
def test_stack(): | |
a = 1 | |
b = 2 | |
return a + b |
下面的代码相干字节码等信息如下所示:
code | |
argcount 0 | |
nlocals 2 | |
stacksize 2 | |
flags 0043 0x43 | |
code b'6401007d00006402007d01007c00007c01001753' | |
# 字节码指令 # 字节码指令参数 # 参数对应的值 | |
24 0 LOAD_CONST 1 (1) | |
3 STORE_FAST 0 (a) | |
25 6 LOAD_CONST 2 (2) | |
9 STORE_FAST 1 (b) | |
26 12 LOAD_FAST 0 (a) | |
15 LOAD_FAST 1 (b) | |
18 BINARY_ADD | |
19 RETURN_VALUE | |
consts | |
None # 下标等于 0 的常量 | |
1 # 下标等于 1 的常量 | |
2 # 下标等于 2 的常量 | |
names () | |
varnames ('a', 'b') | |
freevars () | |
cellvars () |
咱们当初来模仿一下执行过程,在模仿之前咱们首先来理解一下下面几条字节码的作用:
- LOAD_CONST,将常量表当中的下标等于 i 个对象加载到栈当中,对应下面的代码 LOAD_CONST 的参数 i = 1。因而加载测常量等于 1。因而当初栈空间如下所示:
- STORE_FAST,将栈顶元素弹出并且保留到 co_varnames 对应的下标当中,依据下面的字节码参数等于 0,因而将 1 保留到 co_varnames[0] 对应的对象当中。
- LOAD_CONST,将下标等于 2 的常量加载进入栈中。
- STORE_FAST,将栈顶元素弹出,并且保留到 varnames 下标为 1 的对象。
- LOAD_FAST,是取出 co_varnames 对应下标的数据,并且将其压入栈中。咱们间接间断执行两个 LOAD_FAST 之后栈空间的布局如下:
- BINARY_ADD,这个字节码指令是将栈空间的两个栈顶元素弹出,而后将两个数据进行相加操作,而后将相加失去的后果从新压入栈中。
- RETURN_VALUE,将栈顶元素弹出并且作为返回值返回。
从下面的整个执行过程来看整个栈空间应用的最大的空间长度为 2,因而 stacksize = 2。
总结
在本篇文章当中次要剖析了一些 code obejct 当中比拟重要的字段,code object 是 cpython 虚拟机当中一个比拟重要的数据结构,深刻的去了解这外面的字段对于咱们了解 python 虚拟机十分有帮忙。
本篇文章是深刻了解 python 虚拟机系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython
更多精彩内容合集可拜访我的项目:https://github.com/Chang-LeHung/CSCore
关注公众号:一无是处的钻研僧,理解更多计算机(Java、Python、计算机系统根底、算法与数据结构)常识。