如何工作
它通常被描述为一种解释性语言 - 当程序运行时,您的源代码被翻译为本机 CPU 指令 - 但这只是部分正确。 与许多解释性语言一样,源代码实际上被编译成虚拟机的一组指令,解释器是虚拟机的实现。 这种中间格式称为“字节码”。
因此,留下的文件不仅仅是源代码的一些“更快”或“优化”版本; 它们是程序运行时虚拟机将执行的字节码指令。
让我们看一个例子。 这是经典的“你好,世界!” 写有:
[] 查看纯文本
def hello() print("你好,世界!")
这是它变成的字节码(翻译成人类可读的形式):
[] 查看纯文本
0 0(打印)2 1(“你好,世界!”)4 1
上面的列表是如果您键入 hello() 函数并使用解释器运行它将会执行的内容。 不过,这可能看起来有点奇怪,所以让我们深入了解一下到底发生了什么。
在虚拟机中
使用基于堆栈的虚拟机。 也就是说,它完全围绕堆栈数据结构(您可以将项目“推”到结构的“顶部”,或从“顶部”“弹出”项目)。
使用三种类型的堆栈:
1. 调用堆栈。 这是正在运行的程序的主要结构。 对于每个当前活动的函数调用,它都有一个项目 - 一个“框架”,堆栈的底部是程序的入口点。 每个函数调用都会将一个新帧推送到调用堆栈上,并且每次函数调用返回时,都会弹出其帧。
在每个帧中,都有一个评估堆栈(也称为数据堆栈)。 该堆栈是执行函数的地方。 执行代码主要涉及将内容推入堆栈、操作它们以及将它们弹出。
2. 同样在每一帧中,都有一个块堆栈。 使用它来跟踪某些类型的控制结构:循环、try/块和 with 块将所有条目推送到块堆栈上,当您退出其中一个结构时,块堆栈会弹出。 这有助于了解哪些块在任何特定时刻处于活动状态,例如 or break 语句可能会影响正确的块。
3. 大多数字节码指令对当前调用堆栈帧的计算堆栈进行操作,尽管也有一些指令可以执行其他操作(例如跳转到特定指令或对块堆栈进行操作)。
为了感受这一点,假设我们有一些调用如下函数的代码:(,2)。 会将其转换为四个字节码指令的序列:
1. 查找函数对象并将其推入计算堆栈顶部的指令
2.另一条指令,查找变量并将其推入计算堆栈的顶部
3. 一条指令将文字整数值 2 压入计算堆栈的顶部
4. 说明
该指令的参数为2,表示需要从栈顶弹出两个位置参数; 那么被调用的函数将位于顶部,并且也可以弹出(对于涉及关键字参数的函数,使用不同的指令 - 但具有类似的操作原理,第三条指令用于涉及使用 * 解包的函数调用或 ** 运算符)。 一旦它具有所有这些功能,它就会在调用堆栈上分配一个新帧,填充函数调用的局部变量,并执行该帧内的字节码。 一旦完成,该帧将从调用堆栈中弹出,并且原始帧中的返回值将被推送到计算堆栈的顶部。
访问并理解字节码
如果你想使用它,标准库中的 dis 模块是一个巨大的帮助; dis模块为字节码提供了一个“反汇编器”,可以轻松获得人类可读的版本并找到各种字节代码指令。 dis 模块的文档检查其内容。 提供字节码指令的完整列表、它们的作用及其参数。
例如,要获取上面 hello() 函数的字节码列表,我将其输入到解释器中,然后运行:
[] 查看纯文本
dis dis.dis(你好)
函数 dis.dis() 将反汇编函数、方法、类、模块、编译代码对象或包含源代码的字符串文字,并打印出可读版本。 dis 模块中另一个方便的函数是 distb()。 您可以向它传递一个对象,或者在引发异常后调用它,它会在异常上分解调用堆栈中最顶层的函数,打印其字节码,并将指针异常插入到引发它的指令中。
查看为每个函数编译的已编译代码对象也很有用,因为执行函数会利用这些代码对象的属性。 下面是查看 hello() 函数的示例:
[] 查看纯文本
>>> 你好。 ",第 1 行> >>> 你好 .. (无,'你好,世界!') >>> 你好 .. () >>> 你好 .. ('print',)
代码对象可以作为函数的属性进行访问,并且具有一些重要的属性:
是函数体中出现的任何文字的元组
是一个元组,包含函数体中使用的任何局部变量的名称
是函数体中引用的任何非本地名称的元组
许多字节码指令 - 特别是那些加载推入堆栈的值或将值存储在变量和属性中的指令 - 使用这些元组的索引作为它们的参数。
现在我们可以理解hello()函数的字节码列表了:
0:告诉在索引 0 处查找名称引用的全局对象(这是打印函数)并将其推入计算堆栈
1:获取索引 1 处的文字值并将其推入(索引 0 处的值是文字 None,因为如果未显式到达 None,函数调用将具有隐式返回值)
1:告诉调用一个函数; 它需要从堆栈中弹出一个位置参数,然后新堆栈的顶部将是要调用的函数。
“原始”字节码(作为非人类可读字节)也可用作代码对象的属性。 迪斯。 如果您想尝试手动反汇编该函数,可以使用列表从十进制字节值中查找字节码指令的名称。
使用字节码
既然您已经读到这里,您可能会想:“好吧,我想这很酷,但是了解这一点的实际价值是什么?” 抛开为了好奇而好奇,理解字节码在很多方面都是有用的。
首先,理解执行模型可以帮助您推理代码。 人们喜欢开玩笑说 C 是“可移植汇编程序”,您可以很好地猜测 C 源代码中的机器指令是什么样子。 了解字节码将为您提供相同的功能 - 如果您可以预测源代码将转换成什么字节码,您可以就如何编写和优化它做出更好的决策。
其次,理解字节码是回答有关字节码问题的有用方法。 例如,我经常看到新程序员想知道为什么某些结构比其他结构更快(例如为什么 {} 比 dict() 更快)。 了解如何访问和读取字节码可以让您找出答案(尝试: dis.dis("{}") 与 dis.dis("dict()") )。
最后,了解字节码以及如何执行它为程序员不经常涉及的特定类型的编程提供了有用的视角:面向堆栈的编程。 如果您曾经使用过像 FORTH 这样的面向堆栈的语言,那么这可能是旧消息,但如果您是这种方法的新手,那么了解字节码并了解其面向堆栈的编程模型的工作原理是对您的编程知识的一种巧妙的扩展方法。