Python Bytecode究竟是如何在CPython中运行的?

时间:2013-11-11 21:53:58

标签: python cpython python-internals

我试图了解Python的工作原理(因为我一直都在使用它!)。根据我的理解,当你运行python script.py之类的东西时,脚本会被转换为字节码,然后解释器/ VM / CPython - 实际上只是一个C程序 - 读取python字节码并相应地执行程序。

如何读取此字节码?它是否类似于在C中读取文本文件的方式?我不确定Python代码是如何转换为机器代码的。是这样的情况,Python解释器(CLI中的python命令)实际上只是一个已经转换为机器代码的预编译C程序,然后python字节码文件只是通过该程序?换句话说,我的Python程序是否从未实际转换为机器代码? python解释器是否已经在机器代码中,所以我的脚本永远不必是?

3 个答案:

答案 0 :(得分:27)

是的,您的理解是正确的。在CPython解释器中基本上(非常基本上)有一个巨大的switch语句,它说“如果当前的操作码是如此,那么就这样做”。

http://hg.python.org/cpython/file/3.3/Python/ceval.c#l790

其他实现,如Pypy,有JIT编译,即他们即时将Python翻译成机器代码。

答案 1 :(得分:18)

如果要查看某些代码的字节码(无论是源代码,实时函数对象还是代码对象等),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_namescode个对象的属性,它包含一个名字元组局部变量。换句话说,LOAD_NAME 0推送与第0个局部变量关联的值(并且dis有助于查找它并看到第0个局部变量名为'i')。

这足以看到一串字节码是不够的;解释器还需要代码对象的其他属性,在某些情况下还需要函数对象的属性(也是本地和全局环境的来源)。

inspect模块还有一些工具可以帮助您进一步调查实时代码。

这足以弄清楚很多有趣的东西。例如,您可能知道Python在编译时计算出函数中的变量是本地的,闭包的还是全局的,这取决于您是否在函数体中的任何位置(以及任何nonlocal或{ {1}}陈述);如果你编写三个不同的函数并比较它们的反汇编(以及相关的其他属性),你可以很容易地弄清楚它一定要做什么。

(这里有一点很难理解闭包单元。要真正得到这个,你需要有3个级别的函数,看看中间的那个函数如何为最里面的函数转发。)


要了解字节码的解释方式以及堆栈计算机的工作原理(在CPython中),您需要查看ceval.c源代码。 thy435和eyquem的回答已经涵盖了这一点。


了解如何读取global个文件只需要更多信息。 Ned Batchelder有一个很棒的(如果稍微过时的)博客文章The structure of .pyc files,它涵盖了所有棘手且没有很好记录的部分。 (请注意,在3.3中,一些与导入相关的血腥代码已从C移到Python,这使得它更容易理解。)但基本上,它只是一些头信息和模块的pyc对象,序列化marshal


要了解源如何编译为字节码,这是有趣的部分。

Design of CPython's Compiler解释了一切如何运作。 (Python Developer's Guide的其他部分也很有用。)

对于早期的东西 - 标记和解析 - 你可以使用ast模块直接跳转到进行实际编译的时间点。然后参见compile.c了解AST如何转换为字节码。

宏可能有点难以解决,但是一旦你掌握了编译器如何使用堆栈下降到块中的想法,以及它如何使用那些code和朋友在当前发出字节码这一切都很有意义。

首先让大多数人惊讶的是功能的运作方式。函数定义的主体被编译成代码对象。然后将函数定义本身编译成代码(在封闭的函数体,模块等内部),在执行时,从该代码对象构建函数对象。 (一旦你想到闭包必须如何工作,很明显为什么它会以这种方式工作。闭包的每个实例都是一个具有相同代码对象的独立函数对象。)


现在你已经准备好开始修补CPython来添加自己的语句,对吗?好吧,正如Changing CPython's Grammar所示,有很多东西要正确(如果你需要创建新的操作码,还有更多的东西)。你可能会发现学习PyPy以及CPython更容易,并且首先开始攻击PyPy,只有在你知道你正在做的事情是明智和可行的时候才会回到CPython。

答案 2 :(得分:5)

阅读了thg4535的答案,我相信你会在ceval.c上找到有趣的以下解释:Hello, ceval.c!

这篇文章是Yaniv Aknin撰写的一系列文章的一部分,他是我的粉丝:Python's Innards