深入理解 python 虚拟机:字节码教程(1)——原来装饰器是这样实现的
深入理解 python 虚拟机:字节码教程(1)——原来装饰器是这样实现的
在本篇文章当中主要给大家介绍在 cpython 当中一些比较常见的字节码,从根本上理解 python 程序的执行。在本文当中主要介绍一些 python 基本操作的字节码,并且将从字节码的角度分析函数装饰器的原理!
Python 常见字节码
LOAD_CONST
这个指令用于将一个常量加载到栈中。常量可以是数字、字符串、元组、列表、字典等对象。例如:
>>> dis.dis(lambda: 42)
1 0 LOAD_CONST 1 (42)
2 RETURN_VALUE
LOAD_NAME
这个指令用于将一个变量加载到栈中。例如:
>>> dis.dis(lambda: x)
1 0 LOAD_GLOBAL 0 (x)
2 RETURN_VALUE
>>>
STORE_NAME
这个指令用于将栈顶的值存储到一个变量中。例如:
>>> dis.dis("x=42")
1 0 LOAD_CONST 0 (42)
2 STORE_NAME 0 (x)
4 LOAD_CONST 1 (None)
6 RETURN_VALUE
BINARY_ADD
这个指令用于对栈顶的两个值进行加法运算并将结果推送到栈中。
>>> dis.dis(lambda: x + y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 BINARY_ADD
6 RETURN_VALUE
BINARY_SUBTRACT
这个指令用于对栈顶的两个值进行减法运算并将结果推送到栈中。
>>> dis.dis(lambda: x - y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 BINARY_SUBTRACT
6 RETURN_VALUE
同样的加减乘除取余数的字节码如下所示:
>>> dis.dis(lambda: x + y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 BINARY_ADD
6 RETURN_VALUE
>>> dis.dis(lambda: x - y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 BINARY_SUBTRACT
6 RETURN_VALUE
>>> dis.dis(lambda: x * y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 BINARY_MULTIPLY
6 RETURN_VALUE
>>> dis.dis(lambda: x / y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 BINARY_TRUE_DIVIDE
6 RETURN_VALUE
>>> dis.dis(lambda: x // y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 BINARY_FLOOR_DIVIDE
6 RETURN_VALUE
>>> dis.dis(lambda: x % y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 BINARY_MODULO
6 RETURN_VALUE
COMPARE_OP
这个指令用于比较栈顶的两个值,并且将比较得到的结果压入栈中,这个字节码后面后一个字节的参数,表示小于大于不等于等等比较符号。例如:
>>> dis.dis(lambda: x - y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 BINARY_SUBTRACT
6 RETURN_VALUE
>>> dis.dis(lambda: x > y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 COMPARE_OP 4 (>)
6 RETURN_VALUE
>>> dis.dis(lambda: x < y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 COMPARE_OP 0 (<)
6 RETURN_VALUE
>>> dis.dis(lambda: x != y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 COMPARE_OP 3 (!=)
6 RETURN_VALUE
>>> dis.dis(lambda: x <= y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 COMPARE_OP 1 (<=)
6 RETURN_VALUE
>>> dis.dis(lambda: x >= y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 COMPARE_OP 5 (>=)
6 RETURN_VALUE
>>> dis.dis(lambda: x == y)
1 0 LOAD_GLOBAL 0 (x)
2 LOAD_GLOBAL 1 (y)
4 COMPARE_OP 2 (==)
6 RETURN_VALUE
RETURN_VALUE
将栈顶元素弹出作为返回值。
BUILD_LIST
这个指令用于创建一个列表。例如:
>>> dis.dis(lambda: [a, b, c, e])
1 0 LOAD_GLOBAL 0 (a)
2 LOAD_GLOBAL 1 (b)
4 LOAD_GLOBAL 2 (c)
6 LOAD_GLOBAL 3 (e)
8 BUILD_LIST 4
10 RETURN_VALUE
这条字节码指令有一个参数表示栈空间当中列表元素的个数,在上面的例子当中这个参数是 4 。
BUILD_TUPLE
这个指令用于创建一个元组。例如:
>>> dis.dis(lambda: (a, b, c))
1 0 LOAD_GLOBAL 0 (a)
2 LOAD_GLOBAL 1 (b)
4 LOAD_GLOBAL 2 (c)
6 BUILD_TUPLE 3
8 RETURN_VALUE
同样的这个字节码也有一个参数,表示创建元组的元素个数。
BUILD_MAP
这个指令用于创建一个字典。例如:
BUILD_SET
和 list 和 tuple 一样,这条指令是用于创建一个集合对象,同样的这条指令也有一个参数表示用于创建集合的元素的个数。
>>> dis.dis(lambda: {a, b, c, d})
1 0 LOAD_GLOBAL 0 (a)
2 LOAD_GLOBAL 1 (b)
4 LOAD_GLOBAL 2 (c)
6 LOAD_GLOBAL 3 (d)
8 BUILD_SET 4
10 RETURN_VALUE
BUILD_CONST_KEY_MAP
这条指令是用于创建一个字典对象,同样的这条指令也有一个参数,表示字典当中元素的个数。
>>> dis.dis(lambda: {1:2, 3:4})
1 0 LOAD_CONST 1 (2)
2 LOAD_CONST 2 (4)
4 LOAD_CONST 3 ((1, 3))
6 BUILD_CONST_KEY_MAP 2
8 RETURN_VALUE
从字节码角度分析装饰器的原理
如果你是一个 pythoner 那么你肯定或多或少听说过装饰器,这是一个 python 的语法糖我们可以用它来做很多有趣的事情,比如在不修改源代码的基础之上给函数附加一些功能,比如说计算时间。
import time
def eval_time(func):
def cal_time(*args, **kwargs):
start = time.time()
r = func(*args, **kwargs)
end = time.time()
return r, end - start
return cal_time
@eval_time
def fib(n):
a = 0
b = 1
while n > 0:
n -= 1
a, b = b, a + b
return a
在上面的代码当中我们实现了一个计算斐波拉契数列的函数,除此之外还写了一个 eval_time 函数用于计算函数执行的时间,现在调用函数 fib(10),程序的输出如下所示:
>>>fib(10)
(55, 5.9604644775390625e-06)
可以看到实现了我们想要的效果。
现在我们使用一个更加简单的例子来模拟上面的代码结构,方便我们对上面函数执行的过程进行分析:
s = """
def decorator(func):
print("Hello")
return func
@decorator
def fib(n):
pass
"""
dis.dis(s)
上面的 dis 函数的输出对应代码的字节码如下所示:
2 0 LOAD_CONST 0 (<code object decorator at 0x108068d40, file "<dis>", line 2>)
2 LOAD_CONST 1 ('decorator')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (decorator)
6 8 LOAD_NAME 0 (decorator)
7 10 LOAD_CONST 2 (<code object fib at 0x1075c1710, file "<dis>", line 6>)
12 LOAD_CONST 3 ('fib')
14 MAKE_FUNCTION 0
16 CALL_FUNCTION 1
18 STORE_NAME 1 (fib)
20 LOAD_CONST 4 (None)
22 RETURN_VALUE
Disassembly of <code object decorator at 0x108068d40, file "<dis>", line 2>:
3 0 LOAD_GLOBAL 0 (print)
2 LOAD_CONST 1 ('Hello')
4 CALL_FUNCTION 1
6 POP_TOP
4 8 LOAD_FAST 0 (func)
10 RETURN_VALUE
Disassembly of <code object fib at 0x1075c1710, file "<dis>", line 6>:
8 0 LOAD_CONST 0 (None)
2 RETURN_VALUE
- 执行第一条指令 LOAD_CONST,这条指令主要是加载一个 code object 对象,这个对象里面主要是包含函数 decorator 的字节码,主要是上面字节码的第二块内容。在执行完这条字节码之后栈空间如下所示:
- 执行完第二条指令 LOAD_CONST 之后,会将字符串 decorator 加载进入栈空间当中。
- 执行第三条指令 MAKE_FUNCTION,这条字节码的作用是在虚拟机内部创建一个函数,函数的名称为 decorator,函数对应的字节码则是在先前压入栈空间当中的 code object 对象,这条指令还会将创建好的函数对象压入栈中。
- STORE_NAME,条字节码会将栈顶的元素弹出,并且将 co_names[oparg] 指向这个对象,在上面的字节码当中 co_names[oparg] 就是 decorator 。
- LOAD_NAME,这条字节码就是将 co_names[oparg] 对应的名字指向的对象重新加载进入栈空间当中,也就是上面的 decorator 函数加入进行栈空间当中。
- 接下来的三条字节码 LOAD_CONST,LOAD_CONST 和 MAKE_FUNCTION,在执行这三条字节码之后,栈空间如下所示:
- 接下来的一条指令非常重要,这条指令便是装饰器的核心原理,CALL_FUNCTION 这条指令有一个参数 i,在上面的字节码当中为 1,也就是说从栈顶开始的前 i 个元素都是函数参数,调用的函数在栈空间的位置为 i + 1 (从栈顶往下数),那么在上面的情况下就是说调用 decorator 函数,并且将 fib 函数作为 decorator 函数的参数,decorator 函数的返回值再压入栈顶。在上面的代码当中 decorator 函数返回值也是一个函数,也就是 decorator 函数的参数,即 fib 函数。
- 接下来便是 STORE_NAME 字节码,这条字节码的含义我们在前面已经说过了,就是将栈顶元素弹出,保存到 co_names[oparg] 指向的对象当中,在上面的代码当中也就是将栈顶的对象保存到 fib 当中。栈顶元素 fib 函数是调用函数 decorator 的返回值。
看到这里就能够理解了原来装饰器的最根本的原理不就是函数调用嘛,比如我们最前面的用于计算函数执行时间的装饰器的原理就是:
fib = eval_time(fib)
将 fib 函数作为 eval_time 函数的参数,再将这个函数的返回值保存到 fib 当中,当然这个对象必须是可调用的,不然后面使用 fib() 就会保存,我们可以使用下面的代码来验证这个效果。
def decorator(func):
return func()
@decorator
def demo():
return "function demo return string : Demo"
print(demo)
执行上面的程序结果为:
function demo return string : Demo
可以看到 demo 已经变成了一个字符串对象而不再是一个函数了,因为 demo = decorator(demo)
,而在函数 decorator 当中返回值是 demo 函数自己的返回值,因此才打印了字符串。
总结
在本篇文章当中主要给大家介绍了 python 当中一些基础的字节码对应的含义以及示例代码,本篇文章最重要的便是从字节码的角度解释了装饰器的本质原理,这对我们以后使用装饰器非常有帮助,可以灵活的控制和了解装饰器其中发生的故事。
本篇文章是深入理解 python 虚拟机系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython
更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore
关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。
深入理解 python 虚拟机:字节码教程(1)——原来装饰器是这样实现的的更多相关文章
- 深入理解JVM - 虚拟机字节码执行引 - 第八章
概述从外观上看起来,所有的 Java 虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果.主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行. 运行时 ...
- Python虚拟机函数机制之闭包和装饰器(七)
函数中局部变量的访问 在完成了对函数参数的剖析后,我们再来看看,在Python中,函数的局部变量时如何实现的.前面提到过,函数参数也是一种局部变量.所以,其实局部变量的实现机制与函数参数的实现机制是完 ...
- 《深入理解Java虚拟机》-----第8章 虚拟机字节码执行引擎——Java高级开发必须懂的
概述 执行引擎是Java虚拟机最核心的组成部分之一.“虚拟机”是一个相对于“物理机”的概念 ,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的,而 ...
- 【java虚拟机系列】从java虚拟机字节码执行引擎的执行过程来彻底理解java的多态性
我们知道面向对象语言的三大特点之一就是多态性,而java作为一种面向对象的语言,自然也满足多态性,我们也知道java中的多态包括重载与重写,我们也知道在C++中动态多态是通过虚函数来实现的,而虚函数是 ...
- 深入理解JVM虚拟机5:虚拟机字节码执行引擎
虚拟机字节码执行引擎 转自https://juejin.im/post/5abc97ff518825556a727e66 所谓的「虚拟机字节码执行引擎」其实就是 JVM 根据 Class 文件中给 ...
- 《python解释器源码剖析》第8章--python的字节码与pyc文件
8.0 序 我们日常会写各种各样的python脚本,在运行的时候只需要输入python xxx.py程序就执行了.那么问题就来了,一个py文件是如何被python变成一系列的机器指令并执行的呢? 8. ...
- Java虚拟机-字节码执行引擎
概述 Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,成为各种虚拟机执行引擎的统一外观(Facade).不同的虚拟机引擎会包含两种执行模式,解释执行和编译执行. 运行时帧栈结构 栈帧(Sta ...
- 虚拟机字节码指令表 JVM
虚拟机字节码指令表 标签(空格分隔): Java基础 JVM 记录虚拟机字节码指令,方便分析.以下内容来自<深入理解Java虚拟机> 字节码 助记符 指令含义 0x00 nop 什么都不做 ...
- Java虚拟机--虚拟机字节码执行引擎
Java虚拟机--虚拟机字节码执行引擎 所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果. 运行时栈帧结构 用于支持虚拟机进行方法调用和方 ...
- java虚拟机字节码执行引擎
定义 java虚拟机字节码执行引擎是jvm最核心的组成部分之一,它做的事情很简单:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果.在不同的虚拟机实现里,执行引擎在执行java代码 ...
随机推荐
- 10s后再次获取手机验证码
一般手机验证码获取都会加个间隔时间 js代码如下: function getDxCode(btn){ var reg = /^1[3|4|5|7|8][0-9]{9}$/; //验证规则 var mo ...
- base64格式上传图片方法
function dataURItoBlob(dataURI) { const byteString = atob(dataURI.split(',')[1]); const mimeString = ...
- Git本地仓库的文件夹不显示红色感叹号、绿色对号等图标
参考 https://blog.csdn.net/Elon15/article/details/125898375 主要是 在文件名前加8个空格(最少8个)!!!!
- 乘积小于K的子数组
乘积小于K的子数组 给你一个整数数组 nums 和一个整数 k ,请你返回子数组内所有元素的乘积严格小于 k 的连续子数组的数目. 示例 1: 输入:nums = [10,5,2,6], k = 10 ...
- 面向对象ooDay9
精华笔记: 多态:多种形态 同一个对象被造型为不同的类型时,有不同的功能-------所有对象都是多态的(明天总结详细讲) 对象的多态:水.我.你...... 同一类型的引用在指向不同的对象时,有不同 ...
- uni-app 小程序在iOS系统无法长按复制问题
最近在使用uni-app开发移动端应用,有客户反映微信小程序版本在使用是无法长按复制问题,在安卓系统上却是正常的. 检查了下代码,对text标签都设置了selectable属性,寻找万能的度娘还是没有 ...
- oracle中的merge into用法解析
一:merge into的形式 MERGE INTO [target-table] A USING [source-table sql] B ON([conditional expression] a ...
- 【cs231n】knn作业笔记
完成了assignment-1中knn相关内容的作业,记录一下遇到的知识点和问题 knn.ipynb的内容大致包括: 1.数据集的建立 主要是通过切片函数,如下图选取前5000张图片和其标记作为训练数 ...
- UG二次开发-内存访问违例
在项目中修改路径参数后重算发生了内存访问违例的错误,经过调试,发现是下面这一行出的错 surfaceContourBuilder1.Commit(); 经过反复调试,发现这个东西不能随便放,不可以想当 ...
- 2020.6.6OO学期末总结
0.前言 本次博客是对整个java及oo学习情况的一个概略性总结,目的在于反思这半年来的学习情况和实际感受,和具体学习方面的理解和问题. 1.作业过程总结 看着自己一个学期做的所有作业,我想起的是总是 ...