python字节码

如果您曾经编写过Python,或者甚至只是使用过Python,那么您可能已经习惯了查看Python源代码文件。 它们的名称以.py结尾。 而且您可能还看到了另一种类型的文件,其名称以.pyc结尾,并且您可能已经听说它们是Python的“字节码”文件。 (这些在Python 3上很难看到-而不是与.py文件位于同一目录中,而是进入一个名为__pycache__的子目录中。)也许您已经听说这节省了一些时间这样可以避免Python每次运行时都必须重新解析源代码。

但是除了“哦,那是Python字节码”之外,您真的知道这些文件中的内容以及Python如何使用它们吗?

如果没有,那么今天就是您的幸运日! 我将带您了解什么是Python字节码,Python如何使用它执行代码以及如何了解它可以为您提供帮助。

Python如何工作

Python通常被描述为一种解释性语言-一种在程序运行时将您的源代码翻译为本机CPU指令的语言-但这仅部分正确。 像许多解释语言一样,Python实际上将源代码编译为虚拟机的一组指令,而Python解释器是该虚拟机的实现。 这种中间格式称为“字节码”。

因此,Python留下的那些.pyc文件不仅仅是源代码的某些“更快”或“优化”版本; 它们是将在程序运行时由Python的虚拟机执行的字节码指令。

让我们来看一个例子。 这是经典的“你好,世界!” 用Python编写:

def hello 
     
     ( 
     
     ) 
     
     

    
     
     print 
     
     ( 
     
     "Hello, World!" 
     
     )

这是它变成的字节码(翻译成人类可读的形式):

2           0 LOAD_GLOBAL              0 (print)
     
     

            2 LOAD_CONST               1 ('Hello, World!')
     
     

            4 CALL_FUNCTION            1

如果键入该hello()函数并使用CPython解释器运行它,则上面的清单是Python将执行的内容。 不过,这看起来可能有些怪异,所以让我们更深入地了解正在发生的事情。

在Python虚拟机内部

CPython使用三种类型的堆栈:

  1. 调用堆栈 。 这是正在运行的Python程序的主要结构。 对于每个当前活动的函数调用,它都有一个项目(“框架”),堆栈的底部是程序的入口点。 每个函数调用都会将一个新的框架推送到调用堆栈上,并且每次函数调用返回时,都会弹出其框架。
  2. 在每一帧中,都有一个评估堆栈 (也称为数据堆栈 )。 该堆栈是执行Python函数的地方,执行Python代码主要包括将事物压入该堆栈,对其进行处理以及将其弹出。
  3. 同样在每一帧中,都有一个块堆栈 。 Python使用它来跟踪某些类型的控制结构:循环, try / except块以及with块的所有控件都会导致将条目压入块堆栈,并且每当您退出这些结构之一时,就会弹出块堆栈。 这可以帮助Python知道在任何给定时刻哪些块处于活动状态,例如, continue或break语句可以影响正确的块。

Python中的大多数字节码指令可操纵当前调用堆栈帧的评估堆栈,尽管有些指令还可以做其他事情(例如跳转到特定指令或操纵块堆栈)。

为了对此有所了解,假设我们有一些调用函数的代码,例如: my_function(my_variable, 2) 。 Python会将其转换为四个字节码指令的序列:

  1. 一条LOAD_NAME指令,用于查找函数对象my_function并将其推入评估堆栈的顶部
  2. 另一个LOAD_NAME指令查找变量my_variable并将其压入评估堆栈顶部
  3. 一条LOAD_CONST指令,将文字整数值2推入评估堆栈顶部
  4. CALL_FUNCTION指令

CALL_FUNCTION指令的参数为2,这表示Python需要从栈顶弹出两个位置参数。 然后调用的函数将位于最上面,并且也可以弹出(对于涉及关键字参数的函数,使用另一条指令CALL_FUNCTION_KW ,但是具有相似的操作原理,并且使用第三条指令CALL_FUNCTION_EX用于涉及使用*或**运算符解压缩参数的函数调用)。 一旦Python拥有了所有这些功能,它将在调用堆栈上分配一个新框架,填充函数调用的局部变量,并在该框架内执行my_function的字节码。 完成后,该框架将从调用堆栈中弹出,并且在原始框架中, my_function的返回值将被推入评估堆栈的顶部。

访问和理解Python字节码

如果您想解决这个问题,Python标准库中的dis模块将为您带来巨大的帮助。 dis模块为Python字节码提供了“反汇编程序”,可轻松获得易于理解的版本并查找各种字节码指令。 dis模块的文档详细介绍了其内容,并提供了字节码指令的完整列表以及它们的功能和参数。

例如,要获取上面的hello()函数的字节码清单,我将其键入到Python解释器中,然后运行:

import 
     
     dis 
     
     
dis . 
     
     dis 
     
     ( hello 
     
     )

函数dis.dis()将反汇编函数,方法,类,模块,已编译的Python代码对象或包含源代码的字符串文字,并打印出易于阅读的版本。 dis模块中的另一个便捷函数是distb() 。 您可以将其传递给Python追溯对象,也可以在引发异常后调用它,它会在发生异常时反汇编调用堆栈上最顶层的函数,打印其字节码,并插入指向引发异常的指令的指针。例外。

查看Python为每个函数构建的已编译代码对象也很有用,因为执行函数会利用这些代码对象的属性。 这是查看hello()函数的示例:

>>> hello.__code__
     
     

<code object hello at 0x104e46930, file "<stdin>", line 1>
     
     

>>> hello.__code__.co_consts
     
     

(None, 'Hello, World!')
     
     

>>> hello.__code__.co_varnames
     
     

()
     
     

>>> hello.__code__.co_names
     
     

('print',)

该代码对象可作为函数上的属性__code__进行访问,并包含一些重要的属性:

  • co_consts是函数体内出现的所有文字的元组
  • co_varnames是一个元组,包含在函数体中使用的任何局部变量的名称
  • co_names是函数主体中引用的任何非本地名称的元组

许多字节码指令(尤其是那些将值加载到堆栈上或将值存储在变量和属性中的字节码指令)使用这些元组中的索引作为其参数。

因此,现在我们可以了解hello()函数的字节码列表了:

  1. LOAD_GLOBAL 0 :告诉Python来查找由该名称引用全局对象在索引0 co_names (这是print功能),并将其推入计算堆栈
  2. LOAD_CONST 1 :取字面值在指数1 co_consts和推动它(索引0处的值是文本None ,这是目前在co_consts因为Python函数调用有一个隐含的返回值None ,如果没有明确的return达到语句)
  3. CALL_FUNCTION 1 :告诉Python调用函数; 它需要从堆栈中弹出一个位置参数,然后新的堆栈顶部将成为要调用的函数。

“原始”字节码(作为人类不可读的字节)也可以在代码对象上作为属性co_code 。 如果您想尝试手动反汇编函数,则可以使用列表dis.opname从十进制字节值中查找字节码指令的名称。

使用字节码

到目前为止,您已经阅读完了,您可能会想:“好吧,我想那很酷,但是知道这一点的实际价值是什么?” 为了好奇而抛弃好奇心,了解Python字节码在某些方面很有用。

首先,了解Python的执行模型可帮助您推理代码。 人们喜欢开玩笑说C是一种“便携式汇编程序”,您可以在其中很好地猜测C特定源代码块将变成什么机器指令。 理解字节码将为您提供与Python相同的功能-如果您可以预期Python源代码转换成的字节码,则可以就如何编写和优化字节码做出更好的决策。

其次,了解字节码是回答有关Python问题的有用方法。 例如,我经常看到新的Python程序员想知道为什么某些构造比其他构造更快(例如为什么{}比dict()更快)。 知道如何访问和读取Python字节码可以让您算出答案(尝试: dis.dis("{}")与dis.dis("dict()") )。

最后,了解字节码以及Python如何执行它可以为Python程序员不常参与的一种特殊编程提供有用的视角:面向堆栈的编程。 如果您曾经使用过像FORTH或Factor这样的面向堆栈的语言,这可能是个老新闻,但是如果您不熟悉这种方法,那么学习Python字节码并了解其面向堆栈的编程模型是如何工作的,就很简单了。扩展您的编程知识的方法。

进一步阅读

如果您想了解有关Python字节码,Python虚拟机及其工作方式的更多信息,我建议以下资源:

  • Obi Ike-Nwosu撰写的《 Python虚拟机内部》是一本免费的在线书籍,深入研究了Python解释器,详细解释了Python的实际工作方式。
  • 由Allison Kaptur 用Python编写的Python解释器,是一个在Python本身(还有其他方面)中构建Python字节码解释器的教程,它实现了运行Python字节码的所有机制。
  • 最后,CPython解释器是开源的,您可以在GitHub上阅读它 。 字节码解释器的实现在文件Python/ceval.c 。 这是Python 3.6.4版本的文件 ; 字节码指令由从1266行开始的switch语句处理。

要了解更多信息,请在PyCon Cleveland 2018上参加James Bennett的演讲``关于字节的一点:理解Python字节码'' 。

翻译自: https://opensource.com/article/18/4/introduction-python-bytecode

python字节码