Python代码对象code-object与__code__属性
0x01概念
代码对象 code object
是一段可执行的 Python 代码在 CPython 中的内部表示。
可执行的 Python 代码包括:
- 函数
- 模块
- 类
- 生成器表达式
当你运行一段代码时,它被解析并编译成代码对象,随后被 CPython
虚拟机执行。
代码对象包含一系列直接操作虚拟机内部状态的指令。
这跟你在用 C 语言编程时是类似的,你写出人类可读的文本,然后用编译器转换成二进制形式,二进制代码(C 的机器码或者是 Python 的字节码)被 CPU(对于 C 语言来说)或者 CPython 虚拟机虚拟的 CPU 直接执行。
代码对象除了包含 指令,还提供了虚拟机运行代码所需要的一些 额外信息。
0x02探索
以下的内容是在 Python 3.7 中实验的,而且主要是针对于函数来讲。至于模块和类虽然也是通过代码对象实现的(实际上,.pyc 文件里面就存放着序列化的模块代码对象),但是代码对象的大多数特性主要和函数相关。
关于版本需要注意两点:
- 在 Python 2 中,函数的代码对象通过 函数.func_code 来访问;而 Python 3 中,则需要通过 函数.code 来访问。
- Python 3 的代码对象增加了一个新属性 co_kwonlyargcount,对应强制关键字参数 keyword-only argument。
首先在控制台找出属于 函数.code 的所有不以双下划线开头的属性,一共有 15 个。
1 | for i in dir((lambda: 0).__code__) if not i.startswith('__')] li = [i |
官方文档:
属性 | 描述 |
---|---|
co_argcount | number of arguments (not including keyword only arguments, * or ** args) |
co_code | string of raw compiled bytecode |
co_cellvars | tuple of names of cell variables (referenced by containing scopes) |
co_consts | tuple of constants used in the bytecode |
co_filename | name of file in which this code object was created |
co_firstlineno | number of first line in Python source code |
co_flags | bitmap of CO_* flags, read more here |
co_lnotab | encoded mapping of line numbers to bytecode indices |
co_freevars | tuple of names of free variables (referenced via a function’s closure) |
co_kwonlyargcount | number of keyword only arguments (not including ** arg) |
co_name | name with which this code object was defined |
co_names | tuple of names of local variables |
co_nlocals | number of local variables |
co_stacksize | virtual machine stack space required |
co_varnames | tuple of names of arguments and local variables |
下面逐个解释:
co_argcount:函数接收参数的个数,不包括 *args 和 **kwargs 以及强制关键字参数。
1 | def test(a, b, c, d=1, e=2, *args, f=3, g, h=4, **kwargs): |
co_code
:二进制格式的字节码bytecode
,以字节串bytes
的形式存储(在 Python 2 中以 str 类型存储)。它为虚拟机提供一系列的指令。函数从第一条指令开始执行,在碰到RETURN_VALUE
指令的时候停止执行。
其他字节码指令 bytecode instruction 请参阅官方文档:
Python Bytecode Instructions
字节码中每个指令所占字节数是不一样的。
每条指令都有一个操作码 opcode
,它指明了虚拟机需要进行的操作,还有一个可选的参数,这个参数是一个整数。
操作码 opcode
是单字节的整数,所以最多有 256 个不同的操作码,尽管其中很多没有被用到。
每个操作码都有名字,在 dis
模块的 dis
函数的输出中可以见到,同时它们在 opcode
标准库模块中定义。
1 | from opcode import opname |
不接收参数的操作码占用一个字节,而接收参数的操作码占用三个字节,其中第二、第三个字节按照小端序 little-endian order
存储参数。如果参数无法用两个字节表示,比如说大于 65535,则会用到特殊的操作码 EXTENDED_ARG
。
co_cellvars
和co_freevars
:这两个属性用来实现嵌套函数的作用域。
co_cellvars
元组里面存储着所有被嵌套函数用到的变量名。
co_freevars
元组里面存储着所有被函数使用的在闭包作用域中定义的变量名。
这些元组内的变量名均按照字母表顺序排列。
如下例子所示,a
和c
是f
的cellvars
、是g
的freevars
。
1 | def f(a, b): |
-
co_consts
:在函数中用到的所有常量,比如整数、字符串、布尔值等等。它会被LOAD_CONST
操作码使用,该操作码需要一个索引值作为参数,指明需要从co_consts
元组中加载哪一个元素。
co_consts
元组的第一个元素是函数的文档字符串docstring
,如果没有则为None
。 -
co_filename
:代码对象所在的文件名。
test.py
1 | f = lambda: 0 |
- co_firstlineno:代码对象的第一行位于所在文件的行号。
1 | # comment |
-
co_flags
:这是一个整数,存放着函数的组合布尔标志位。
可以在inspect
模块的文档中查看这些标志位的具体含义:Code Objects Bit Flags
-
co_lnotab
:这个属性是 line number table 行号表的缩写。它以字节串 bytes 的形式存储,每两个字节是一对,分别是 co_code 字节串的偏移量和 Python 行号的偏移量。
具体参阅:lnotab_notes.txt -
co_kwonlyargcount
:存放强制关键字参数的个数。在 Python 2 中则没有这个属性。
1 | def test(a, b, c, d=1, e=2, *args, f=3, g, h=4, **kwargs): |
- co_name:是与代码对象关联的对象的名字。
1 | lambda: 0 func = |
- co_names:该属性是由字符串组成的元组,里面按照使用顺序存放了全局变量和被导入的名字。(注意官方文档的表格中说是局部变量的名字,实际上是不对的)
1 | a = 1 |
co_nlocals
:函数中局部变量的个数,相当于是co_varnames
的长度。co_stacksize
:一个整数,代表函数会使用的最大栈空间。co_varnames
:函数所有的局部变量名称(包括函数参数)组成的元组。
首先是位置参数、默认参数和强制关键字参数
然后是*args
和**kwargs
(如果有的话)
最后是按照第一次使用顺序排列的其他局部变量。
1 | def test(a, b, c, d=1, e=2, *args, f=3, g, h=4, **kwargs): |