如果您想查看某些代码的字节码(无论是源代码、实时函数对象还是代码对象等),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] 关联的值压入堆栈。
要理解这一点,你必须知道字节码解释器是一个虚拟的stack machine,而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 有一篇很棒的(如果有点过时)博客文章,名为 The structure of .pyc files,其中涵盖了所有棘手且没有充分记录的部分。 (请注意,在 3.3 中,一些与导入相关的血腥代码已从 C 移至 Python,这使其更容易理解。)但基本上,它只是一些头信息和模块的 code 对象,由 @ 序列化987654327@.
要了解源代码如何编译为字节码,这是有趣的部分。
Design of CPython's Compiler 解释了一切是如何工作的。 (Python Developer's Guide 的其他部分也很有用。)
对于早期的东西——标记化和解析——你可以使用ast 模块直接跳转到需要进行实际编译的地方。然后查看 compile.c 了解 AST 是如何变成字节码的。
宏可能有点难以完成,但是一旦你掌握了编译器如何使用堆栈来下降到块,以及它如何使用compiler_addop 和朋友在当前级别发出字节码的想法,这一切都说得通。
一开始让大多数人感到惊讶的是函数的工作方式。函数定义的主体被编译成代码对象。然后函数定义本身被编译成代码(在封闭的函数体、模块等内部),当执行时,从该代码对象构建一个函数对象。 (一旦你想到了闭包必须如何工作,就很明显它为什么会这样工作。闭包的每个实例都是一个单独的函数对象,具有相同的代码对象。)
现在您已准备好开始修补 CPython 以添加您自己的语句,对吗?好吧,正如Changing CPython's Grammar 所示,有很多事情要做(如果您需要创建新的操作码,还有更多)。你可能会发现学习PyPy 和 CPython 会更容易,首先开始学习 PyPy,只有在你知道自己正在做的事情是明智和可行的时候才回到 CPython。