深入理解 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)——原来装饰器是这样实现的的更多相关文章

  1. 深入理解JVM - 虚拟机字节码执行引 - 第八章

    概述从外观上看起来,所有的 Java 虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果.主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行. 运行时 ...

  2. Python虚拟机函数机制之闭包和装饰器(七)

    函数中局部变量的访问 在完成了对函数参数的剖析后,我们再来看看,在Python中,函数的局部变量时如何实现的.前面提到过,函数参数也是一种局部变量.所以,其实局部变量的实现机制与函数参数的实现机制是完 ...

  3. 《深入理解Java虚拟机》-----第8章 虚拟机字节码执行引擎——Java高级开发必须懂的

    概述 执行引擎是Java虚拟机最核心的组成部分之一.“虚拟机”是一个相对于“物理机”的概念 ,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的,而 ...

  4. 【java虚拟机系列】从java虚拟机字节码执行引擎的执行过程来彻底理解java的多态性

    我们知道面向对象语言的三大特点之一就是多态性,而java作为一种面向对象的语言,自然也满足多态性,我们也知道java中的多态包括重载与重写,我们也知道在C++中动态多态是通过虚函数来实现的,而虚函数是 ...

  5. 深入理解JVM虚拟机5:虚拟机字节码执行引擎

    虚拟机字节码执行引擎   转自https://juejin.im/post/5abc97ff518825556a727e66 所谓的「虚拟机字节码执行引擎」其实就是 JVM 根据 Class 文件中给 ...

  6. 《python解释器源码剖析》第8章--python的字节码与pyc文件

    8.0 序 我们日常会写各种各样的python脚本,在运行的时候只需要输入python xxx.py程序就执行了.那么问题就来了,一个py文件是如何被python变成一系列的机器指令并执行的呢? 8. ...

  7. Java虚拟机-字节码执行引擎

    概述 Java虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,成为各种虚拟机执行引擎的统一外观(Facade).不同的虚拟机引擎会包含两种执行模式,解释执行和编译执行. 运行时帧栈结构 栈帧(Sta ...

  8. 虚拟机字节码指令表 JVM

    虚拟机字节码指令表 标签(空格分隔): Java基础 JVM 记录虚拟机字节码指令,方便分析.以下内容来自<深入理解Java虚拟机> 字节码 助记符 指令含义 0x00 nop 什么都不做 ...

  9. Java虚拟机--虚拟机字节码执行引擎

    Java虚拟机--虚拟机字节码执行引擎 所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果. 运行时栈帧结构 用于支持虚拟机进行方法调用和方 ...

  10. java虚拟机字节码执行引擎

    定义 java虚拟机字节码执行引擎是jvm最核心的组成部分之一,它做的事情很简单:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果.在不同的虚拟机实现里,执行引擎在执行java代码 ...

随机推荐

  1. cuda安装的问题

    小学期老师给的文档,里面要加入这几个环境变量 他这排版有问题,我就去网上找了几个cuda环境变量的配置 保姆级的CUDA的下载安装使用,详细的环境变量配置,不仅仅让你能够安装,还会教你弄懂为什么要这样 ...

  2. netty库包冲突问题

    参考 http://www.yayihouse.com/yayishuwu/chapter/1603 报错信息:Caused by: java.lang.NoSuchFieldError: netty ...

  3. React中的CSS模块

    CSS模块       使用步骤:         1.新建一个XXX.moudle.css文件         2.在组件中引入css           impor classes(变量) fro ...

  4. JavaScript基础学习之一

    目录 let和var之间的区别 作用域不同 变量提升 暂时性死区(temporal dead zone,简称 TDZ) 相同作用域下的重复声明 脚本调用 数据类型 Boolean Object 对象 ...

  5. 估计人数【最小路径重复点覆盖】【直接在(i,j)建一个新点】

    估计人数 题意 思路 用最少的人,走完这几条线.最小重复路径点覆盖问题 建图之后,跑一下二分图. 考虑建图:图中'1'连着完下.或者右走.我们把图中所有的1编号,然后建图,然后floly,然后匈牙利. ...

  6. jxg项目Day5-关于项目打包

    springboot项目中手动打包,加以下依赖: <parent> <artifactId>spring-boot-dependencies</artifactId> ...

  7. 第二节 printf语句和C++中的判断结构

    第二节 printf语句和C++中的判断结构 1.1printf语句作用 保留几位小数:%.4lf 保留四位小数 %.3lf 保留三位小数, 格式化输出:整数:printf("%5d!&qu ...

  8. bilibili视频常用快捷键

    Esc退出全屏 Q点赞.长按三连 W投币 E收藏 D开/关弹幕 F开/关全屏 M静音 [ 多P 上一个 ] 多P下一个 Enter发弹幕 Space播放/暂停 →单次快进5s,长按倍速播放 ←快退5s ...

  9. 用猿大师的VLC播放插件在高版本Chrome播放RTSP视频流,并抓图、录像、回放、倍速等

    因为项目上需要把海康威视摄像头集成到WEB网页中播放,于是开始了对WEB播放摄像头方案的各种折腾. 2015年之前还可以用VLC原生播放器在Chrome.Firefox等浏览器中直接播放,延迟比较低, ...

  10. button 样式 触发器

    <Style x:Key="Style.OkOperationButton" TargetType="ButtonBase"> <Setter ...