65

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

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

4

4 回答 4

35

是的,你的理解是正确的。在 CPython 解释器中基本上(非常基本上)有一个巨大的 switch 语句,上面写着“如果当前的操作码是某某,那么做这个那个”。

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

其他实现,如 Pypy,具有 JIT 编译,即它们即时将 Python 转换为机器代码。

于 2013-11-11T22:05:53.113 回答
25

如果您想查看某些代码的字节码(无论是源代码、实时函数对象还是代码对象等),该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 在编译时根据您是否在函数体中的任何位置(以及在任何nonlocalglobal语句上)分配给函数中的变量是局部变量、闭包变量还是全局变量;如果您编写三个不同的函数并比较它们的反汇编(以及相关的其他属性),您可以很容易地弄清楚它必须做什么。

(这里比较棘手的一点是理解闭包单元。要真正做到这一点,您需要有 3 级函数,以查看中间的函数如何将事物转发到最里面的函数。)


要了解字节码是如何解释的以及堆栈机器是如何工作的(在 CPython 中),您需要查看ceval.c源代码。thy435 和 eyquem 的答案已经涵盖了这一点。


了解pyc文件的只读方式需要更多信息。Ned Batchelder 有一篇很棒的(如果有点过时)博客文章,名为.pyc 文件的结构,其中涵盖了所有棘手且没有充分记录的部分。(请注意,在 3.3 中,一些与导入相关的血腥代码已从 C 移至 Python,这使其更容易理解。)但基本上,它只是一些头信息和模块的code对象,由marshal.


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

CPython 编译器的设计解释了一切是如何工作的。( Python 开发人员指南的其他一些部分也很有用。)

对于早期的东西——标记化和解析——你可以使用ast模块直接跳转到需要进行实际编译的地方。然后看看compile.cAST 是如何变成字节码的。

宏可能有点难以完成,但是一旦你掌握了编译器如何使用堆栈下降成块的想法,以及它如何使用这些compiler_addop和朋友在当前级别发出字节码,这一切都说得通了。

一开始让大多数人感到惊讶的是函数的工作方式。函数定义的主体被编译成代码对象。然后函数定义本身被编译成代码(在封闭的函数体、模块等内部),当执行时,从该代码对象构建一个函数对象。(一旦你考虑了闭包必须如何工作,很明显它为什么会这样工作。闭包的每个实例都是具有相同代码对象的单独函数对象。)


现在您已准备好开始修补 CPython 以添加您自己的语句,对吗?嗯,正如改变 CPython 的语法所示,有很多事情要做(如果你需要创建新的操作码,还有更多)。你可能会发现学习PyPy和 CPython更容易,首先开始学习 PyPy,只有在你知道你正在做的事情是明智和可行的时候才回到 CPython。

于 2013-11-11T23:17:37.393 回答
6

阅读 thg4535 的答案后,我相信您会发现以下关于 ceval.c 的解释很有趣:您好,ceval.c!

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

于 2013-11-11T22:23:18.460 回答
0

当我们运行python程序时:1_python源代码用Cpython编译成字节码(字节码是.pyc格式的二进制文件,它用marshal进行序列化,它是一组用pvm解决的堆栈结构)2_然后pvm(python虚拟机/ python解释器)是stackbase机器(解决具有堆栈数据结构的任务的机器),它在字节码中逐行循环并执行它。

什么执行字节码?

字节码告诉 Python 解释器要执行哪个 C 代码。

于 2021-11-30T06:58:55.053 回答