如果您想查看某些代码的字节码(无论是源代码、实时函数对象还是代码对象等),该dis
模块会准确地告诉您您需要什么。例如:
>>> dis.dis('i/3')
1 0 LOAD_NAME 0 (i)
3 LOAD_CONST 0 (3)
6 BINARY_TRUE_DIVIDE
7 RETURN_VALUE
dis
文档解释了每个字节码的含义。例如LOAD_NAME
:
将关联的值压co_names[namei]
入堆栈。
要理解这一点,你必须知道字节码解释器是一个虚拟堆栈机,它是什么co_names
。模块文档有一个很好的inspect
表格,显示了最重要的内部对象的最重要的属性,所以你可以看到这co_names
是一个code
对象的属性,它包含一个局部变量名称的元组。换句话说,LOAD_NAME 0
推送与第 0 个局部变量关联的值(并dis
有助于查找它并看到第 0 个局部变量名为'i'
)。
这足以看出一串字节码是不够的;解释器还需要代码对象的其他属性,在某些情况下还需要函数对象的属性(这也是本地和全局环境的来源)。
该inspect
模块还有一些工具可以帮助您进一步研究实时代码。
这足以找出很多有趣的东西。例如,您可能知道 Python 在编译时根据您是否在函数体中的任何位置(以及在任何nonlocal
或global
语句上)分配给函数中的变量是局部变量、闭包变量还是全局变量;如果您编写三个不同的函数并比较它们的反汇编(以及相关的其他属性),您可以很容易地弄清楚它必须做什么。
(这里比较棘手的一点是理解闭包单元。要真正做到这一点,您需要有 3 级函数,以查看中间的函数如何将事物转发到最里面的函数。)
要了解字节码是如何解释的以及堆栈机器是如何工作的(在 CPython 中),您需要查看ceval.c
源代码。thy435 和 eyquem 的答案已经涵盖了这一点。
了解pyc
文件的只读方式需要更多信息。Ned Batchelder 有一篇很棒的(如果有点过时)博客文章,名为.pyc 文件的结构,其中涵盖了所有棘手且没有充分记录的部分。(请注意,在 3.3 中,一些与导入相关的血腥代码已从 C 移至 Python,这使其更容易理解。)但基本上,它只是一些头信息和模块的code
对象,由marshal
.
要了解源代码如何编译为字节码,这是有趣的部分。
CPython 编译器的设计解释了一切是如何工作的。( Python 开发人员指南的其他一些部分也很有用。)
对于早期的东西——标记化和解析——你可以使用ast
模块直接跳转到需要进行实际编译的地方。然后看看compile.c
AST 是如何变成字节码的。
宏可能有点难以完成,但是一旦你掌握了编译器如何使用堆栈下降成块的想法,以及它如何使用这些compiler_addop
和朋友在当前级别发出字节码,这一切都说得通了。
一开始让大多数人感到惊讶的是函数的工作方式。函数定义的主体被编译成代码对象。然后函数定义本身被编译成代码(在封闭的函数体、模块等内部),当执行时,从该代码对象构建一个函数对象。(一旦你考虑了闭包必须如何工作,很明显它为什么会这样工作。闭包的每个实例都是具有相同代码对象的单独函数对象。)
现在您已准备好开始修补 CPython 以添加您自己的语句,对吗?嗯,正如改变 CPython 的语法所示,有很多事情要做(如果你需要创建新的操作码,还有更多)。你可能会发现学习PyPy和 CPython更容易,首先开始学习 PyPy,只有在你知道你正在做的事情是明智和可行的时候才回到 CPython。