代码对象不仅包含指令本身,还包含 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 “”, 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’
(1)Python所有方向的学习路线(新版)
这是我花了几天的时间去把Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照上面的知识点去找对应的学习资源,保证自己学得较为全面。
最近我才对这些路线做了一下新的更新,知识体系更全面了。
(2)Python学习视频
包含了Python入门、爬虫、数据分析和web开发的学习视频,总共100多个,虽然没有那么全面,但是对于入门来说是没问题的,学完这些之后,你可以按照我上面的学习路线去网上找其他的知识资源进行进阶。
(3)100多个练手项目
我们在看视频学习的时候,不能光动眼动脑不动手,比较科学的学习方法是在理解之后运用它们,这时候练手项目就很适合了,只是里面的项目比较多,水平也是参差不齐,大家可以挑自己能做的项目去练练。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!