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

mer*_*ort 50 python cpython python-internals

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

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

geo*_*org 29

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

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

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

  • @kishinmanglani:你也可以阅读[`compile.c`]中的代码(http://hg.python.org/cpython/file/2908063f3287/Python/compile.c).一旦你完成它如何编译函数定义(特别是闭包)和一些相关的东西(特别是理解),你几乎得到了所有困难的东西. (3认同)
  • 惊人的。谢谢。我读了整个文件,有点惊讶它确实这么简单。当你剥离抽象概念时,我们周围的一切都变得多么简单,真是令人惊奇 (2认同)
  • @kishinmanglani你有没有读过这个文件?? !! ØO! (2认同)

aba*_*ert 18

如果你想看到某些代码的字节码(无论是源代码,实时函数对象还是代码对象等),dis模块都会告诉你你需要什么.例如:

>>> dis.dis('i/3')
  1           0 LOAD_NAME                0 (i)
              3 LOAD_CONST               0 (3)
              6 BINARY_TRUE_DIVIDE
              7 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

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 Developer's Guide的其他一些部分也很有用.)

对于早期的东西 - 标记和解析 - 你可以使用ast模块直接跳转到实际编译的时间点.然后看看compile.cAST如何变成字节码.

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

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


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


eyq*_*uem 5

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

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