若是你曾经写过或者用过 Python,你可能已经习惯了看到 Python 源代码文件;它们的名称以.Py 结尾。你可能还见过另外一种类型的文件是 .pyc 结尾的,它们就是 Python “字节码”文件。(在 Python3 的时候这个 .pyc 后缀的文件不太好找了,它在一个名为__pycache__的子目录下面。).pyc文件能够防止Python每次运行时都从新解析源代码,该文件大大节省了时间。html
Python是如何工做的python
Python 一般被描述为一种解释语言,在这种语言中,你的源代码在程序运行时被翻译成CPU指令,但这只是说对了部分。和许多解释型语言同样,Python 实际上将源代码编译为虚拟机的一组指令,Python 解释器就是该虚拟机的实现。其中这种中间格式称为“字节码”。sass
所以,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 将转换为一系列字节码指令:
一个LOAD_NAME指令,用于查找函数对象 my_function,并将其推送到计算栈的顶部。
另外一个 LOAD_NAME 指令去查找变量 my_variable,并将其推送到计算栈的顶部。
一个 LOAD_CONST 指令将一个整数 2 推送到计算栈的顶部。
一个 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.
访问和理解字节码
输入以下内容,而后运行它:
函数 dis.dis() 将反汇编一个函数、方法、类、模块、编译过的 Python 代码对象、或者字符串包含的源代码,以及显示出一我的类可读的版本。dis 模块中另外一个方便的功能是 distb()。你能够给它传递一个 Python 追溯对象,或者在发生预期外状况时调用它,而后它将在发生预期外状况时反汇编调用栈上最顶端的函数,并显示它的字节码,以及插入一个指向到引起意外状况的指令的指针。
它也能够用于查看 Python 为每一个函数构建的编译后的代码对象,由于运行一个函数将会用到这些代码对象的属性。这里有一个查看 hello() 函数的示例:
代码对象在函数中能够以属性 __code__ 来访问,而且携带了一些重要的属性:
许多字节码指令--尤为是那些推入到栈中的加载值,或者在变量和属性中的存储值--在这些元组中的索引做为它们参数。
所以,如今咱们可以理解 hello() 函数中所列出的字节码:
LOAD_GLOBAL 0:告诉 Python 经过 co_names (它是 print 函数)的索引 0 上的名字去查找它指向的全局对象,而后将它推入到计算栈。
LOAD_CONST 1:带入 co_consts 在索引 1 上的字面值,并将它推入(索引 0 上的字面值是 None,它表示在 co_consts 中,由于 Python 函数调用有一个隐式的返回值 None,若是没有显式的返回表达式,就返回这个隐式的值 )。
CALL_FUNCTION 1:告诉 Python 去调用一个函数;它须要从栈中弹出一个位置参数,而后,新的栈顶将被函数调用。
“原始的” 字节码--是非人类可读格式的字节--也能够在代码对象上做为 co_code 属性可用。若是你有兴趣尝试手工反汇编一个函数时,你能够从它们的十进制字节值中,使用列出 dis.opname 的方式去查看字节码指令的名字。
基本反汇编
函数dis()能够打印 Python 源代码(模块、类、方法、函数或代码对象)的反汇编表示。能够经过从命令行运行 dis 来反汇编 dis_simple.py 之类的模块。
输出按列组织,包含原始源代码行号,代码对象中的指令地址,操做码名称以及传递给操做码的任何参数。
对于简单的代码咱们能够经过命令行的形式执行下面的命令:
python3-mdisdis_simple.py
输出
在这里源代码转换为4个不一样的操做来建立和填充字典,而后将结果保存到一个局部变量。
首先解释每一行各列参数的含义:
以第一条指令为例:
第一列 数字(1)表示对应源代码的行数。
第二列(可选)指示当前执行的指令(例如,当字节码来自帧对象时)【这个例子没有】
第三列 一个标签,表示从以前的指令到此可能的JUMP 【这个例子没有】
第四列 数字是字节码中对应于字节索引的地址(这些是2的倍数,由于Python 3.6每条指令使用2个字节,而在之前的版本中可能会有所不一样)指令LOAD_CONST在0位置。
第五列 指令自己对应的人类可读的名字这里是"LOAD_CONST"
第六列 Python内部用于获取某些常量或变量,管理堆栈,跳转到特定指令等的指令的参数(若是有的话)。
第七列 计算后的实际参数。
而后让咱们看看这个过程:
因为 Python 解释器是基于栈的,因此前几步是用LOAD_CONST将常量按正确顺序放入到栈中,而后使用 BUILD_MAP 弹出要增长到字典的新键和值。用 STORE_NAME 将所获得的dict对象绑定名为my_dict.
反汇编函数
须要注意的是上面的命令行反编译的形式,不能自动的递归反编译函数,因此咱们要使用在文件中导入dis的模式进行反编译,就像下面这样。
运行命令
python3dis_function.py
而后获得如下结果
要查看函数的内部,必须把函数传递到dis().由于这里打印的是函数内部的东西,因此没有显示函数的在外层的行编号,而是从2开始的。
下面解析下每一行指令的含义:
LOAD_GLOBAL 用来加载全局变量,包括指定函数名,类名,模块名等全局符号,这里是len函数,LOAD_FAST 通常加载局部变量的值,也就是读取值,用于计算或者函数调用传参等,这里就是传入参数args。
通常是先指定要调用的函数,而后压参数,最后经过 CALL_FUNCTION 调用。
STORE_FAST 保存值到局部变量。也就是把结果赋值给 STORE_FAST。
下面的print由于2个参数因此LOAD_FAST了2次,POP_TOP删除堆栈顶部(TOS)项。LOAD_CONST加载const变量,好比数值、字符串等等,这里由于是print因此值为None。
最后经过RETURN_VALUE来肯定函数结尾。
要打印一个函数的总结信息咱们可使用dis的show_code的方法,它包含使用的参数和名的相关信息,show_code的参数就是这个函数对象,代码以下:
运行以后,结果以下
能够看到返回的内容有函数,方法,参数等信息。
反汇编类
上面咱们知道了如何反汇编一个函数的内部,一样的咱们也能够用相似的方法反汇编一个类。
咱们看一个例子:
运行之和获得以下结果
从总体内容来看,结果分为了两部分Disassembly of __init__和Disassembly of __str__,Disassembly就是反汇编的意思。
首先分析__init__部分:
而后须要注意的一点是,方法是按照字母的顺序列出的,因此在部分,先看到name再看到self,可是他们都是 LOAD_FAST。
STORE_ATTR实现self.name = name。
而后LOAD_CONST一个None和RETURN_VALUE标志着函数结束。
接下来分析__str__部分:
LOAD_CONST将'MyObject({})'加载到栈
而后经过 LOAD_METHOD 调用字符串format方法。这个方法是Python3.7新加入的。
LOAD_FAST 也就是到了self了。
LOAD_ATTR 通常是调用某个对象的方法时。这里就是self.name的.name操做
CALL_METHOD 是 python3.7 新增长的内容,这里是执行方法。
RETURN_VALUE表示函数的结束。
上面字符串的拼接咱们用了format,以前我一直推荐用f-string,下面就让咱们经过字节码来分析,为何f-string比format要高快。
代码其余代码不变,把return改为如下内容:
returnf'MyObject({self.name})'
再次执行,下面咱们只看__str__函数的部分。
对比发现咱们这里没有了调用方法的操做LOAD_METHOD,取而代之使用了用于实现fstring的FORMAT_VALUE指令。以后经过BUILD_STRING链接堆栈中的计数字符串并将结果字符串推入堆栈.为何format慢呢, python中的函数调用具备至关大的开销。 当使用str.format()时,CALL_METHOD 中花费的额外时间是致使str.format()比fstring慢得多。
使用反汇编调试
调试一个异常时,有时要查看哪一个字节码带来了问题。这个时候就颇有用了,要对一个错误周围的代码反汇编,有多种方法。第一种策略是在交互解释器中使用dis()报告最后一个异常。
若是没有向dis()传入任何参数,那么它会查找一个异常,并显示致使这个异常的栈顶元素的反汇编效果。
命令行上使用
打开个人命令行执行以下操做:
行号后面的-->就是致使错误的操做码,一个LOAD_NAME指令,因为没有定义变量i,因此没法将与这个名关联的值加载到栈中。
代码中使用distb
程序还能够打印一个活动的traceback的有关信息,将它传递到distb()方法。
下面的程序中有个DiviedByZero异常;可是这个公式有两个除法,因此不清楚是哪一部分出错,此时咱们就可使用下面的方法:
运行以后输出
结果反映的字节码很长咱们不用全看了,看最开始出现--> 就能够知道错误的位置了。
其中SETUP_FINALLY 字节码的含义是将try块从try-except子句推入块堆栈。
这里能够看出将LOAD_NAME 将j压入栈以后就报错了。因此能够推断出在(i/j)就出错了。
参考资料
docs.python.org/zh-cn/3.7/l…
opensource.com/article/18/…
hackernoon.com/a-closer-lo…