Code objects 是 CPython 实现的低级细节。 代码对象是 CPython 对一段可运行 Python 代码的内部表示,例如函数、模块、类体或生成器表达式。当你运行一段代码时,它会被解析并编译成一个代码对象,然后由 CPython 虚拟机 (VM) 运行。代码对象包含直接操作 VM 内部状态的指令列表,例如“将堆栈顶部的两个对象加在一起,将它们弹出,然后将结果放入堆栈”。这类似于像 C 这样的语言的工作方式:您将代码编写为人类可读的文本,该代码由编译器转换为二进制格式,然后运行二进制代码(C 的机器代码和 Python 的所谓字节码)直接由 CPU(对于 C)或由 CPython VM 的虚拟 CPU 执行。
在 CPython 中,您可以使用标准库的dis模块来检查您的代码以(有点)可读格式编译成的字节码:
In [10]: def f(x, y):
....: return x + y
....:
In [11]: dis.dis(f)
2 0 LOAD_FAST 0 (x)
3 LOAD_FAST 1 (y)
6 BINARY_ADD
7 RETURN_VALUE
这个简单的函数被编译成四个指令的序列:将变量的值加载x到堆栈(LOAD_FAST
),加载()的值,y
从LOAD_FAST
堆栈中删除它们并将它们的和放回堆栈(BINARY_ADD
),并返回堆栈顶部的值 (RETURN_VALUE
)。如果您编译了等效的 C 函数,您可能会看到类似的机器代码指令序列,例如mov和add。
代码对象不仅包含指令本身,还包含 VM 运行代码所需的一些其他信息。在这个答案中,我将详细介绍代码对象中的确切内容。您可以通过访问函数上的属性在 Python shell 中亲自看到这一点,func_code
该函数有许多属性,我将一一介绍:
In [13]: f.func_code
Out[13]: ", line 1>
In [14]: f.func_code.co_
f.func_code.co_argcount f.func_code.co_code f.func_code.co_filename f.func_code.co_flags f.func_code.co_lnotab f.func_code.co_names f.func_code.co_stacksize
f.func_code.co_cellvars f.func_code.co_consts f.func_code.co_firstlineno f.func_code.co_freevars f.func_code.co_name f.func_code.co_nlocals f.func_code.co_varnames
我在这里只写关于 CPython 2.7 的文章。CPython 3 大体相似,但做了一些增量修改。除其他外,函数的代码对象现在位于f.__code__而不是,f.func_code并且添加了一个新属性co_kwonlyargcount以支持仅关键字参数。Python 3 的下一个版本可能会使用一个显着改变的字节码实现,称为 wordcode。其他 Python 实现,例如 PyPy 和 Jython,可能使用完全不同的方式来存储代码。
我还将主要提到功能。模块和类定义也是使用代码对象来实现的(确实,.pyc文件基本上包含序列化的模块代码对象),但是代码对象的很多特性只与函数相关。
co_argcount。这是函数采用的参数数量,不包括任何*args和**kwargs。字节码中的函数调用通过将所有参数压入堆栈然后调用CALL_FUNCTION; 然后co_argcount可用于确定函数是否传递了正确数量的变量。
co_cellvars 和 co_freevars。这两个用于实现嵌套函数范围。co_cellvars是一个元组,包含函数中所有变量的名称,这些变量也用于嵌套函数,并且co_freevars具有函数中使用的所有变量的名称,这些变量在封闭函数范围中定义。例如,这里y是 的 cellvarsf和 freevars 之间的g:
In [21]: def f(x):
....: y = 3
....: def g():
....: return y + 1
....: return g()
....:
In [22]: f.func_code.co_cellvars
Out[22]: ('y',)
In [23]: f.func_code.co_consts[2].co_freevars
Out[23]: ('y',)
与存储元组的其他几个代码对象属性一样,处理这些变量的字节码使用元组的索引。例如,y上面第 2 行的赋值被编译成一个STORE_DEREF带有参数 0 的操作码,表示它位于单元变量y中的位置 0,第y4 行的读取变成LOAD_DEREF带有参数 0 的操作码。DEREF操作码用于单元变量和freevars 和索引实际上是两个 ( co_cellvars + co_freevars) 的串联,所以如果一个函数有 cellvars(a, b)和 freevars (c, d),LOAD_DEREF2 将加载的值c和LOAD_DEREF1 将加载b。在 cellvar 和 freevar 中,名称按字母顺序列出。
我不熟悉这两个字段在运行时如何用于将信息从一个功能范围传递到另一个功能范围。
co_code,这是二进制格式的实际字节码,存储为普通的 Python 字符串。如上所示,它是VM的指令列表。函数从第一条指令开始执行,在遇到RETURN_VALUE指令时停止。在此答案的其他地方讨论了一些字节码,所有字节码都记录在dis模块文档,但我不会在这里讨论所有的说明。
字符串中的编码在co_code每条指令中使用可变数量的字节。每条指令都包含一个opcode ,它指示 VM 要执行的操作,加上一个可选参数,它始终是一个整数。操作码是一个单字节整数,因此可能有 256 个不同的操作码,尽管其中许多当前未使用。每个操作码都有一个名称,该名称显示在dis输出中(参见上面的几个示例)并在opcode标准库模块中定义。
所有整数值低于称为HAVE_ARGUMENT(Python 2.7 上为 90)的截止值的操作码都没有参数,因此它们的指令只占用一个字节。接受参数的操作码占用三个字节,其中第二个和第三个字节以小端顺序存储参数。如果参数太大而无法容纳这两个字节(即,它大于216= 65536),使用了一个特殊的操作码EXTENDED_ARG。例如,如果您想在函数中加载第 65537 个单元格变量(为什么要这样做呢?),您首先有一个EXTENDED_ARG带有参数 1 的指令,然后是LOAD_DEREF带有参数 1 的指令,表明您需要加载元素1 * 65536 + 1 = 65537.
co_consts。这是函数中使用的所有常量的元组,如整数、字符串和布尔值。它由LOAD_CONST操作码使用,它接受一个参数,该参数指示co_consts要从中加载的元组中的索引。例如,f下面是上面定义的函数的常量co_cellvars:
In [34]: print f.func_code.co_consts
(None, 3, <code object g at 0xf6796800, file "<ipython-input-1-a7b65140e37b>", line 3>)
元组中的第二个元素是3,因此赋值代码y = 3包含指令LOAD_CONST1,指示索引 1 处的常量应放入堆栈。同样,LOAD_CONST2 在创建嵌套函数时加载代码g。
函数代码对象中的第一个co_consts元素始终是函数的文档字符串,可能是None(就像这里一样)。否则,常量大多按照它们在字节码中首次使用的顺序排列,但 VM 不需要这样做,而且 CPython 的窥孔优化器在生成字节码后运行,有时会做出不遵守此顺序的更改。
co_filename。这是在其中创建代码的文件的名称。
co_firstlineno。生成代码对象的 Python 代码开头的 1 索引行号。与 结合使用co_lnotab,用于计算异常回溯等位置的行信息。
co_flags。这是一个整数,它结合了许多关于函数的布尔标志。它没有完全记录,但标志包括(使用inspect模块中定义的名称):
CO_OPTIMIZED
: 表示该函数是在启用 Python 优化的情况下编译的;我相信这只是意味着删除文档字符串和断言。CO_NEWLOCALS
:为除模块之外的所有代码对象设置;我猜这是对 CPython 的早期更改的残余。CO_VARARGS
: 该函数采用 *args。CO_VARKEYWORDS
: 该函数需要 **kwargs。CO_NESTED
: 该函数嵌套在另一个函数中。CO_GENERATOR
: 该函数是一个生成器函数。CO_NOFREE
: 如果函数没有单元格或自由变量,则设置。co_lnotab。这意味着行号表,并存储字节码指令到行号的压缩映射。它是一串二进制数据,其中每两个字节是一对(增加co_code字符串中的偏移量,增加 Python 行号)。第一个从 0 开始,第二个从 的值开始co_firstlineno。这是一个例子:
In [39]: def f(x):
....: x = 3
....: y = 4
....:
In [40]: f.func_code.co_lnotab
Out[40]: '\x00\x01\x06\x01'
In [41]: dis.dis(f)
2 0 LOAD_CONST 1 (3)
3 STORE_FAST 0 (x)
3 6 LOAD_CONST 2 (4)
9 STORE_FAST 1 (y)
12 LOAD_CONST 0 (None)
15 RETURN_VALUE
在这个函数中,代表六个字节的前两条指令来自函数代码的第一行,其余的来自第二行。co_lnotab可以更易读地呈现为 (0, 1), (6, 1) 。第一行表示我们应该将字节码偏移量加0,行号偏移量加1(第一行号是def行,但没有字节码直接对应)。然后第二行告诉我们将字节码偏移量增加 6,行号偏移量增加 1,这对应于接下来的 6 个字节在我们刚刚通过的行的事实。然后其余的代码隐含在我们现在到达的那一行。这在lnotab_notes.txt中有更详细的解释在 Python 源代码中。
在实践中,Python 有时会生成条目略多于所需条目的 lnotab。例如,由生成器表达式(例如(f(x) for x in lst))创建的代码对象总是有一个非空的 lnotab,即使所有代码都在第一行。
co_name。这是与代码对象相关联的对象(例如函数)的名称。
同名。在代码对象中用作属性、全局变量名称和导入名称的字符串元组。使用这些名称之一的操作码(例如,LOAD_ATTR)将这个元组的整数索引作为参数。这些是按第一次使用的顺序。
co_names。函数中局部变量的数量。据我所知,这只是co_varnames. 这可能是为了决定在调用函数时为局部变量分配多少空间。
co_stacksize。一个整数,表示函数将使用的最大堆栈空间量。这是必要的,因为与代码对象关联的 VM 堆栈是在调用代码时预先分配的。因此,如果co_stacksize太低,该函数可能会溢出其分配的堆栈并发生可怕的事情。
无法计算任意一段 Python 字节码将使用的堆栈空间量(这个问题看起来与停止问题很相似,但我不确定是否已正式证明是这种情况)。但是,由 CPython 生成的字节码表现良好,可以编写一个计算co_stacksize. 关键的简化假设是控制流图中的任何循环(例如,由循环生成的循环)对使用的堆栈空间没有净影响。
co_varnames。包含函数所有局部变量(包括参数)名称的元组。它包含第一个普通参数,然后是*args和**kwargs参数的名称(如果适用),然后是其他局部变量(按首次使用的顺序)。这些被LOAD_FAST类似的操作码使用,它们将这个元组的索引作为参数。在运行时,VM 分配一个相同大小的数组来保存每个变量的值。操作码被称为“快速”,因为此处加载仅涉及数组取消引用,而不是co_names使用LOAD_GLOBAL.
因此,这些操作码的操作顺序是: