如果你曾经编写或使用过 Python,你可能已经习惯于查看 Python 源代码文件;他们的名字以 .Py 结尾。你可能见过的另一种以 .pyc 结尾的文件是 Python“字节码”文件。 (在Python3中,.pyc后缀的文件不好找,在一个名为pycache的子目录下。).pyc文件可以避免Python每次运行都重新解析源码,大大节省了时间。
Python是如何工作的
Python 通常被描述为一种解释型语言,你的源代码在程序运行时被翻译成 CPU 指令,但这只是部分正确。与许多解释型语言一样,Python 实际上将源代码编译成一组用于虚拟机的指令,Python 解释器是该虚拟机的实现。其中,这种中间格式称为“字节码”。
因此,Python留下的这些.pyc文件是为了让运行速度变得“更快”,或者说是你源代码的“优化”版本;它们是运行在 Python 虚拟机指令上的字节码。
Python 虚拟机内幕
CPython 使用基于堆栈的虚拟机。也就是说,它完全围绕堆栈数据结构(你可以将项目“推”到结构的“顶部”,或将项目“弹出”到“顶部”)。
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 的返回值将被压入计算堆栈的顶部。
这个东西我们知道,字节码文件我们也知道,但是字节码怎么用呢? ok 不知道也没关系,我们接下来的话题都是围绕字节码展开的。 python中有一个模块可以通过反编译Python代码生成字节码。这个模块就是我们今天要说的--dis模块。
dis模块的使用
dis 模块包括用于操作 Python 字节码的函数,可以将其“反汇编”为更易于阅读的形式。查看解释器运行的字节码也有助于优化代码。此模块对于查找多线程中的竞争条件也很有用,因为它可用于评估代码线程控制可能切换的位置。参考源码Include/opcode.h找到官方的字节码列表。有关详细信息,请参阅官方文档。注意不同版本的python生成的字节码内容可能不同,这里我使用的是Python 3.8。
访问和理解字节码
输入如下内容,然后运行它:
def hello()
print("Hello, World!")
import dis
dis.dis(hello)
函数 dis.dis() 将反汇编函数、方法、类、模块、已编译的 Python 代码对象或字符串中包含的源代码,并显示人类可读的版本。 dis 模块中的另一个方便的函数是 distb()。你可以向它传递一个 Python traceback 对象,或者在意外发生时调用它,它会在意外发生时反汇编调用堆栈上的最顶层函数,显示其字节码,并插入一个指向导致异常的指令的指针。
它还可用于查看 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对象可以作为函数中的属性code来访问,它携带了一些重要的属性:
co_consts是存在于函数体内的任意实数的元组
co_varnames 是一个元组,包含函数体内使用的任何局部变量的名称
co_names是在函数体内引用的任意非本地名字的元组
许多字节码指令——尤其是那些将值加载到堆栈上,或将值存储在变量和属性中的指令——将索引作为它们的参数放入这些元组中。
所以现在我们可以理解 hello() 函数中列出的字节码了:
1. LOAD_GLOBAL 0:告诉Python在co_names的index 0上查找name指向的全局对象(就是打印函数),然后压入计算栈
2. LOAD_CONST 1:在索引1处带入co_consts的字面值并压入(索引0处的字面值为None,用co_consts表示,因为Python函数调用隐式返回值为None,如果有没有显式返回表达式,返回这个隐式值)。
3. CALL_FUNCTION 1:告诉Python调用一个函数;它需要从堆栈中弹出一个位置参数,然后,函数将调用新的堆栈顶部。
“原始”字节码——一种非人类可读格式的字节——也可用作代码对象的 co_code 属性。如果你有兴趣尝试手动反汇编函数,可以使用 dis.opname 从十进制字节值中查看字节码指令名称。
基本反汇编
函数 dis() 可以打印 Python 源代码(模块、类、方法、函数或代码对象)的反汇编表示。 dis_simple.py 等模块可以通过从命令行运行 dis 来反汇编。
dis_simple.py
#!/usr/bin/env python3
# encoding: utf-8
my_dict = {'a': 1}
输出按列组织,包含原始源代码行号、代码对象中指令的地址、操作码名称以及传递给操作码的任何参数。
对于简单的代码,我们可以通过命令行执行如下命令:
python3 -m dis dis_simple.py
输出
1 0 LOAD_CONST 0 ('a')
2 LOAD_CONST 1 (1)
4 BUILD_MAP 1
6 STORE_NAME 0 (my_dict)
8 LOAD_CONST 2 (None)
10 RETURN_VALUE
此处源代码转换为 4 个不同的操作来创建和填充字典,然后将结果保存到局部变量。
首先解释每一行各列参数的含义:
以第一条指令为例:
第一列 数字(1)表示对应源代码的行数。
第二列(可选)指示当前正在执行的指令(例如,当字节码来自帧对象时)[本示例没有]
第三列是一个标签,表示从上一条指令到这条指令可能的JUMP【本例没有】
第四列数字是字节索引对应的字节码中的地址(这些是 2 的倍数,因为 Python 3.6 每条指令使用 2 个字节,这可能与以前的版本不同)指令 LOAD_CONST 在 0 位置。
第五列是指令本身的人类可读名称,这里是“LOAD_CONST”
第六列是Python内部用来获取某些常量或变量、管理堆栈、跳转到特定指令等的指令的参数(如果有的话)。
第七列 计算后的实际参数。
然后让我们看看这个过程:
由于 Python 解释器是基于堆栈的,前几步是使用 LOAD_CONST 将常量以正确的顺序放入堆栈,然后使用 BUILD_MAP 弹出新的键和值以添加到字典中。将生成的带有 STORE_NAME 的字典对象绑定到 my_dict。
反汇编函数
需要注意的是,上述命令行反编译形式不能自动递归反编译函数,所以我们需要使用在文件中导入dis的方式进行反编译,如下图。
#dis_function.py
def f(*args):
nargs = len(args)
print(nargs, args)
if __name__ == '__main__':
import dis
dis.dis(f)
运行命令
python3 dis_function.py
然后得到以下结果
2 0 LOAD_GLOBAL 0 (len)
2 LOAD_FAST 0 (args)
4 CALL_FUNCTION 1
6 STORE_FAST 1 (nargs)
3 8 LOAD_GLOBAL 1 (print)
10 LOAD_FAST 1 (nargs)
12 LOAD_FAST 0 (args)
14 CALL_FUNCTION 2
16 POP_TOP
18 LOAD_CONST 0 (None)
20 RETURN_VALUE
要查看函数内部,必须将函数传递给 dis()。因为这里打印的是函数内部的内容,所以不显示函数外层的行号,而是从2开始。
下面解析下每一行指令的含义:
1、LOAD_GLOBAL用于加载全局变量,包括指定的函数名、类名、模块名等全局符号。这是 len 函数。 LOAD_FAST一般是加载局部变量的值,也就是读取值,用于计算或者函数调用传参等,这里是传入参数args。
2、一般是先指定要调用的函数,然后压入参数,最后通过CALL_FUNCTION调用。
3、STORE_FAST 保存值到局部变量。也就是把结果赋值给 STORE_FAST。
4、下面的print因为2个参数LOAD_FAST 2次,POP_TOP删除栈顶(TOS)项。 LOAD_CONST 加载const变量,比如values,strings等,其中value为None因为是print。
5、最后通过RETURN_VALUE来确定函数结尾。
要打印函数的摘要信息,我们可以使用 dis 的 show_code 方法,其中包含有关使用的参数和名称的信息。 show_code 的参数是函数对象。代码如下:
def f(*args):
nargs = len(args)
print(nargs, args)
if __name__ == '__main__':
import dis
dis.show_code(f)
运行结果
Name: f
Filename: dis_function_showcode.py
Argument count: 0
Kw-only arguments: 0
Number of locals: 2
Stack size: 3
Flags: OPTIMIZED, NEWLOCALS, VARARGS, NOFREE
Constants:
0: None
Names:
0: len
1: print
Variable names:
0: args
1: nargs
本文为原创文章,版权归知行编程网所有,欢迎分享本文,转载请保留出处!
内容反馈