装饰器是 Python 编程中常用的一个功能,可以将通用的逻辑抽象成装饰器,通过装饰器语法应用到不同的目标上,达到增强或修改目标逻辑的目的。

先来看一个简单的例子

# 打印耗时的装饰器
def log(f):
def inner(*args, **kw):
start = time.perf_counter()
result = f(*args, **kw)
cost = time.perf_counter() - start
print(f"cost {cost:.1} seconds")
return result
return inner # 通过 @ 语法将装饰器应用到目标对象上
@log
def download(url):
requests.get(url) download("https://www.baidu.com")
# 输出如下
# cost 0.1 seconds

被装饰的目标是 download 函数,用于下载文件,通过 @ 语法被打印耗时的装饰器 log 修饰,download 在被调用的时候就会打印下载文件的耗时。那么打印耗时的逻辑是怎么嵌入到 download 函数中的呢,这里的关键就是需要知道 Python 解释器是怎么处理 @ 语法的。不妨用 Python 提供的 dis 模块反汇编一下 download 的定义

@log
def download(url):
requests.get(url)

反汇编之后得到的字节码如下(不同 Python 版本反汇编的结果略有差异)

2           0 LOAD_NAME                0 (log)

3           2 LOAD_CONST               0 (<code object download at 0x7f7872e363a0, file "<dis>", line 2>)
4 LOAD_CONST 1 ('download')
6 MAKE_FUNCTION 0
8 CALL_FUNCTION 1
10 STORE_NAME 1 (download)
12 LOAD_CONST 2 (None)
14 RETURN_VALUE # download 函数本来的函数体
Disassembly of <code object download at 0x7f7872e363a0, file "<dis>", line 2>:
4 0 LOAD_GLOBAL 0 (requests)
2 LOAD_METHOD 1 (get)
4 LOAD_FAST 0 (url)
6 CALL_METHOD 1
8 POP_TOP
10 LOAD_CONST 0 (None)
12 RETURN_VALUE

可以看到,一开始 MAKE_FUNCTION 指令通过编译好的 download 函数体对应的 code 对象构造出一个新的函数对象存在 download 变量中,然后通过 CALL_FUNCTION 指令调用 log 函数(参数是 download 变量),最后再通过 STORE_NAME 指令将 log 函数的返回结果存到 download 变量中。所以从语义上说,logdownload 进行装饰的效果等价于下面的伪代码

download = make_function(code) # 构造最初的 download 函数对象
download = log(download) # 调用 log 函数将返回值赋给 download 变量

log 函数的返回是 inner 函数,也就是说 download 在被修饰之后指向的是 inner 函数,而 inner 函数中的 f 变量又指向了 download 在被修饰之前所指向的函数对象,所以最后在调用 download 的时候除了有原来的下载功能之外,还打印了耗时。

装饰前后 download 变量的变化如下图所示

在上面的例子中我们称 log 为装饰器,因为 log@ 同一行且是 @ 后面的内容,这是 Python 语法规定的。log 是一个函数,且定义了一个参数。但是装饰器本身不一定非得是函数,只要是值为可以通过传递一个位置参数进行调用的可调用对象的表达式即可。也就是说

  • 装饰器就是一个表达式
  • 表达式的值是一个可调用对象
  • 这个可调用对象可以通过位置传参的方式传递一个参数进行调用

简单来说,装饰器的本质是可以通过形如 A(f) 的方式进行调用的可调用对象(A@ 后面整个表达式的值)

可调用对象可以认为是像函数一样可以在后面加括号和参数进行调用的对象,函数本身就是一种可调用对象,还有类对象以及定义了 __call__ 函数的类实例对象等都是可调用对象。

下面的函数 wrapper 就是一个满足条件的装饰器,hello 开始是个空函数,被 wrapper 装饰之后就变成了字符串 "zikcheng"。当然这个装饰器没什么用,只是为了说明定义一个满足语法的装饰器很简单,不一定非要定义内部函数。

def wrapper(a=1, b="hello"):
return "zikcheng" @wrapper
def hello():
pass print(hello, type(hello))
# 输出
# zikcheng <class 'str'>

再回到 logdownload 的例子,如果我们想在打印耗时的同时打印负载类型(比如这里是 IO,对于计算密集型的负载类型是 CPU),可以做如下修改

def log_outer(load_type):
def log(f):
def inner(*args, **kw):
start = time.perf_counter()
result = f(*args, **kw)
cost = time.perf_counter() - start
print(f"{load_type} cost {cost:.1} seconds")
return result
return inner
return log @log_outer("IO")
def download(url):
requests.get(url) download("https://www.baidu.com")
# 输出如下
# IO cost 0.1 seconds

这个例子中的装饰器表达式为 log_outer("IO"),因为 @ 符号后面的内容是 log_outer("IO") 整体,而不是 log_outerlog_outer("IO") 的返回值为 log 函数,因此修饰 download 的装饰器还是 log 函数,只不过这次 log 函数是一个闭包,引用了外部函数的 load_type 变量用于打印负载类型。本质上还是 download = log_outer("IO")(download)

如果要打印负载类型,除了上面这种方式,还可以通过定义一个类来实现

class LoadLog:
def __init__(self, load_type):
self.load_type = load_type def __call__(self, f):
def inner(*args, **kw):
start = time.perf_counter()
result = f(*args, **kw)
cost = time.perf_counter() - start
print(f"{self.load_type} cost {cost:.1} seconds")
return result
return inner @LoadLog("IO")
def download(url):
requests.get(url) download("https://www.baidu.com")
# 输出如下
# IO cost 0.1 seconds

同理,download = LoadLog("IO")(download),装饰器是 LoadLog("IO"),值为 LoadLog 类实例,是一个可调用对象,最终 download 指向 __call__ 的返回值也就是 inner 函数,效果跟之前一样。

只要我们明白装饰器的本质是值为可调用对象的表达式,那么无论怎么变形,是三层嵌套函数还是类就都很好理解了。

上面例子中被装饰的对象都是函数,实际上类也是可以被装饰的,这里就不展开了。

本文先通过一个简单的示例引入装饰器概念,然后分析其反汇编的字节码揭示了装饰器的本质,最后通过无嵌套函数、三层嵌套函数和类分别作为装饰器来加强对装饰器本质的理解,希望文章能对你有所帮助。

Python 装饰器原理的更多相关文章

  1. python装饰器原理

    妙处在于装饰器的两个return 1.装饰器 # 使用闭包 def wrap(fun): def check(): print("正在检查用户权限!") fun() return ...

  2. Python 装饰器原理剖析

    以下内容仅用于帮助个人理解装饰器这个概念,案例可能并不准确. 什么是装饰器? 我们知道iPhone 应用商店中有成千上万的APP,我们也知道苹果系统每年都会大版本更新增加很多新功能.这些功能要想发挥出 ...

  3. 【低门槛 手把手】python 装饰器(Decorators)原理说明

    本文目的是由浅入深地介绍python装饰器原理 装饰器(Decorators)是 Python 的一个重要部分 其功能是,在不修改原函数(类)定义代码的情况下,增加新的功能 为了理解和实现装饰器,我们 ...

  4. python 装饰器、递归原理、模块导入方式

    1.装饰器原理 def f1(arg): print '验证' arg() def func(): print ' #.将被调用函数封装到另外一个函数 func = f1(func) #.对原函数重新 ...

  5. Python函数装饰器原理与用法详解《摘》

    本文实例讲述了Python函数装饰器原理与用法.分享给大家供大家参考,具体如下: 装饰器本质上是一个函数,该函数用来处理其他函数,它可以让其他函数在不需要修改代码的前提下增加额外的功能,装饰器的返回值 ...

  6. 关于python装饰器

    关于python装饰器,不是系统的介绍,只是说一下某些问题 1 首先了解变量作用于非常重要 2 其次要了解闭包 def logger(func): def inner(*args, **kwargs) ...

  7. Python装饰器详解

    python中的装饰器是一个用得非常多的东西,我们可以把一些特定的方法.通用的方法写成一个个装饰器,这就为调用这些方法提供一个非常大的便利,如此提高我们代码的可读性以及简洁性,以及可扩展性. 在学习p ...

  8. Python装饰器由浅入深

    装饰器的功能在很多语言中都有,名字也不尽相同,其实它体现的是一种设计模式,强调的是开放封闭原则,更多的用于后期功能升级而不是编写新的代码.装饰器不光能装饰函数,也能装饰其他的对象,比如类,但通常,我们 ...

  9. Python装饰器模式学习总结

    装饰器模式,重点在于装饰.装饰的核心仍旧是被装饰对象. 类比于Java编程的时候的包装模式,是同样的道理.虽然概念上稍有不同但是原理上还是比较相近的.下面我就来谈一谈我对Python的装饰器的学习的一 ...

  10. Python 装饰器(Decorator)

    装饰器的语法为 @dec_name ,置于函数定义之前.如: import atexit @atexit.register def goodbye(): print('Goodbye!') print ...

随机推荐

  1. Hexo多台电脑同步

    如果换了电脑该如何同步Hexo的源文件?把hexo文件从一个电脑cope到另外一个电脑吗?答案肯定不是这样的,因为这里面有好多依赖包,好几万个文件呢,这样显然不合理. 本文提供一种多台电脑同步源文件的 ...

  2. Codeforces Round #805 (Div. 3)E.Split Into Two Sets

    题目链接:https://codeforces.ml/contest/1702/problem/E 题目大意: 每张牌上面有两个数字,现在有n张牌(n为偶数),问能否将这n张牌分成两堆,使得每堆牌中的 ...

  3. onps栈移植说明(3)——添加网卡

    4. 添加网卡 移植的最后一步就是编写网卡驱动然后将网卡添加到协议栈.网卡驱动其本质上完成的是数据链路层的工作,在整个通讯链路上处于通讯枢纽位置,通讯报文的发送和接收均由其实际完成.针对网卡部分的移植 ...

  4. mybatis的映射

    //一对一 <resultMap id="给resultMap取的id名" type="类名"> <result property=" ...

  5. 如何正确遵守 Python 代码规范

    前言 无规矩不成方圆,代码亦是如此,本篇文章将会介绍一些自己做项目时遵守的较为常用的 Python 代码规范. 命名 大小写 模块名写法: module_name 包名写法: package_name ...

  6. 833——B题题解

    题目链接 题目大意: 给一串字符串(只包含0~9),定义一个最优子串的定义:如果该子串同字符种类数大于最最多字符出现数,那么这个子串可以被称为最优子串. 解题思路: 大眼一看这个数据范围他用n^2的算 ...

  7. Git+Github协作开发流程

    采用合作开发者的方式进行协作开发,下面以zlcook和zlserver合作开发iqas-ios-record项目为例进行讲解,zlcook为项目创建者,zlserver为项目参与者.协作开发步骤如下: ...

  8. 数学建模经验分享会For浙商大管工学院

    数学建模介绍 数学建模与数学 数学建模和数学没有必然的关系 一个队里至少有一个数学较好 注重运筹学.统计学这两门课 数学建模做什么 时间:一般每年9月开学三周内 时长:3天3夜 目标:完成一份排版精美 ...

  9. win11如何双屏幕(1台主机2块显示器)

    1.买两块大小相宜.刷新率相同的屏幕(如诺刷新率不一样可能后期造成卡顿现象) 2.用数据线将两块屏幕都接主机上(现在买新款屏幕基本上都会送双头HDMI线,老旧款式可能是VGA) HDMI款 VGA款 ...

  10. Linux deb系统 nginx 配置解析php

    如果你是root用户可以不加sudo 首先安装php php-fpm nginx sudo apt-get install php php-fpm nginx -y nginx 是一个轻量级的http ...