【问题标题】:How exactly is Python Bytecode Run in CPython?Python 字节码在 CPython 中究竟是如何运行的?
【发布时间】:2021-07-02 18:49:19
【问题描述】:

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

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

【问题讨论】:

  • 我认为这个问题非常好。我也想了很久
  • 如果你想查看字节码,你可以使用dis模块。而且,通过一些工作(或快速谷歌)和 marshal 模块,您可以弄清楚 Python 如何读取 .pyc 文件(如果需要,还可以了解字节码的 dis 块)。
  • 您可能还想查看inspect 模块,它可以帮助您在运行时找到活动对象中的字节码,以便您检查它。 (另外,请注意,字节码本身不足以运行——它取决于一个环境,该环境可以包括codefunction 对象的其他成员、本地/全局等)
  • Ned Batchelder 在The structure of .pyc files 上有一篇很棒的博文。它假定您已经了解字节码(以及编译和 ceval 的工作方式)和 marshal 模块,并且只解释了特定于 pyc 的内容。 (但请注意,细节有点过时了——这是一篇关于 Python 2.5 的 5 年前的帖子……)

标签: python cpython python-internals


【解决方案1】:

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

什么执行字节码?

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

【讨论】:

    【解决方案2】:

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

    (这里比较棘手的一点是理解闭包单元。要真正做到这一点,您需要有 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。

    【讨论】:

      【解决方案3】:

      看了 thg4535 的回答,相信你会对 ceval.c 上的以下解释感兴趣:Hello, ceval.c!

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

      【讨论】:

        【解决方案4】:

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

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

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

        【讨论】:

        • 太棒了。谢谢。我读了整个文件,有点惊讶它确实这么简单。当您剥离抽象时,我们周围的一切都变得如此简单,这真是令人惊讶
        • @kishinmanglani:您可能还想阅读compile.c 中的代码。一旦你了解了它是如何编译函数定义(尤其是闭包)和一些相关的东西(特别是推导式),你几乎已经把所有困难的东西都搞定了。
        • @eyquem:它并不像看起来那么复杂。一半以上的代码是大的 switch 语句,其中大部分是 cmets 和样板文件,您无需了解 100 多个操作码中的每一个如何工作即可获得这个想法……
        • 非常感谢!!我试图在 CPython 中找到 ROT_THREE 的实现 :)
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-03-03
        • 1970-01-01
        • 2020-02-13
        • 1970-01-01
        相关资源
        最近更新 更多