Python语言 :关于使用装饰器的技巧介绍_def wrapper(wrapped, instance, args, kwargs):-程序员宅基地

技术标签: python  编程语言  Python  开发语言  

转自:微点阅读  https://www.weidianyuedu.com

导语


装饰器(Decorator) 是 Python 里的一种特殊工具,它为我们提供了一种在函数外部修改函数的灵活能力。它有点像一顶画着独一无二 @ 符号的神奇帽子,只要将它戴在函数头顶上,就能悄无声息的改变函数本身的行为。

你可能已经和装饰器打过不少交道了。在做面向对象编程时,我们就经常会用到 @staticmethod和 @classmethod 两个内置装饰器。此外,如果你接触过 click 模块,就更不会对装饰器感到陌生。click 最为人所称道的参数定义接口 @click.option(...) 就是利用装饰器实现的。

除了用装饰器,我们也经常需要自己写一些装饰器。在这篇文章里,我将从 最佳实践 和 常见错误两个方面,来与你分享有关装饰器的一些小知识。

最佳实践
1. 尝试用类来实现装饰器
绝大多数装饰器都是基于函数和 闭包 实现的,但这并非制造装饰器的唯一方式。事实上,Python 对某个对象是否能通过装饰器( @decorator)形式使用只有一个要求:decorator 必须是一个“可被调用(callable)的对象。

 
# 使用 callable 可以检测某个对象是否“可被调用”

>>> def foo(): pass

...

>>> type(foo)

<class "function">

>>> callable(foo)

True

函数自然是“可被调用”的对象。但除了函数外,我们也可以让任何一个类(class)变得“可被调用”(callable)。办法很简单,只要自定义类的 __call__ 魔法方法即可。

 
class Foo:

def __call__(self):

print("Hello, __call___")

foo = Foo()

# OUTPUT: True

print(callable(foo))

# 调用 foo 实例

# OUTPUT: Hello, __call__

foo()

基于这个特性,我们可以很方便的使用类来实现装饰器。

下面这段代码,会定义一个名为 @delay(duration) 的装饰器,使用它装饰过的函数在每次执行前,都会等待额外的 duration 秒。同时,我们也希望为用户提供无需等待马上执行的 eager_call 接口。

 
import time

import functools

class DelayFunc:

def __init__(self, duration, func):

self.duration = duration

self.func = func

def __call__(self, *args, **kwargs):

print(f"Wait for {self.duration} seconds...")

time.sleep(self.duration)

return self.func(*args, **kwargs)

def eager_call(self, *args, **kwargs):

print("Call without delay")

return self.func(*args, **kwargs)

def delay(duration):

"""装饰器:推迟某个函数的执行。同时提供 .eager_call 方法立即执行

"""

# 此处为了避免定义额外函数,直接使用 functools.partial 帮助构造

# DelayFunc 实例

return functools.partial(DelayFunc, duration)

如何使用装饰器的样例代码:

 
@delay(duration=2)

def add(a, b):

return a + b

# 这次调用将会延迟 2 秒

add(1, 2)

# 这次调用将会立即执行

add.eager_call(1, 2)

@delay(duration) 就是一个基于类来实现的装饰器。当然,如果你非常熟悉 Python 里的函数和闭包,上面的 delay 装饰器其实也完全可以只用函数来实现。所以,为什么我们要用类来做这件事呢?

与纯函数相比,我觉得使用类实现的装饰器在特定场景下有几个优势:

实现有状态的装饰器时,操作类属性比操作闭包内变量更符合直觉、不易出错

实现为函数扩充接口的装饰器时,使用类包装函数,比直接为函数对象追加属性更易于维护

更容易实现一个同时兼容装饰器与上下文管理器协议的对象(参考 unitest.mock.patch)

2. 使用 wrapt 模块编写更扁平的装饰器
在写装饰器的过程中,你有没有碰到过什么不爽的事情?不管你有没有,反正我有。我经常在写代码的时候,被下面两件事情搞得特别难受:

实现带参数的装饰器时,层层嵌套的函数代码特别难写、难读

因为函数和类方法的不同,为前者写的装饰器经常没法直接套用在后者上

比如,在下面的例子里,我实现了一个生成随机数并注入为函数参数的装饰器。

 
import random

def provide_number(min_num, max_num):

"""装饰器:随机生成一个在 [min_num, max_num] 范围的整数,追加为函数的第一个位置参数

"""

def wrapper(func):

def decorated(*args, **kwargs):

num = random.randint(min_num, max_num)

# 将 num 作为第一个参数追加后调用函数

return func(num, *args, **kwargs)

return decorated

return wrapper

@provide_number(1, 100)

def print_random_number(num):

print(num)

# 输出 1-100 的随机整数

# OUTPUT: 72

print_random_number()

@provide_number 装饰器功能看上去很不错,但它有着我在前面提到的两个问题:嵌套层级深、无法在类方法上使用。如果直接用它去装饰类方法,会出现下面的情况:

 
class Foo:

@provide_number(1, 100)

def print_random_number(self, num):

print(num)

# OUTPUT: <__main__.Foo object at 0x104047278>

Foo().print_random_number()

Foo 类实例中的 print_random_number 方法将会输出类实例 self ,而不是我们期望的随机数 num。

之所以会出现这个结果,是因为类方法(method)和函数(function)二者在工作机制上有着细微不同。如果要修复这个问题, provider_number 装饰器在修改类方法的位置参数时,必须聪明的跳过藏在 *args 里面的类实例 self 变量,才能正确的将 num 作为第一个参数注入。

这时,就应该是 wrapt 模块闪亮登场的时候了。 wrapt 模块是一个专门帮助你编写装饰器的工具库。利用它,我们可以非常方便的改造 provide_number 装饰器,完美解决“嵌套层级深”和“无法通用”两个问题,

 
import wrapt

def provide_number(min_num, max_num):

@wrapt.decorator

def wrapper(wrapped, instance, args, kwargs):

# 参数含义:

#

# - wrapped:被装饰的函数或类方法

# - instance:

# - 如果被装饰者为普通类方法,该值为类实例

# - 如果被装饰者为 classmethod 类方法,该值为类

# - 如果被装饰者为类/函数/静态方法,该值为 None

#

# - args:调用时的位置参数(注意没有 * 符号)

# - kwargs:调用时的关键字参数(注意没有 ** 符号)

#

num = random.randint(min_num, max_num)

# 无需关注 wrapped 是类方法或普通函数,直接在头部追加参数

args = (num,) + args

return wrapped(*args, **kwargs)

return wrapper

<... 应用装饰器部分代码省略 ...>

# OUTPUT: 48

Foo().print_random_number()

使用 wrapt 模块编写的装饰器,相比原来拥有下面这些优势:

嵌套层级少:使用 @wrapt.decorator 可以将两层嵌套减少为一层

更简单:处理位置与关键字参数时,可以忽略类实例等特殊情况

更灵活:针对 instance 值进行条件判断后,更容易让装饰器变得通用

常见错误
1. “装饰器”并不是“装饰器模式”
“设计模式”是一个在计算机世界里鼎鼎大名的词。假如你是一名 Java 程序员,而你一点设计模式都不懂,那么我打赌你找工作的面试过程一定会度过的相当艰难。

但写 Python 时,我们极少谈起“设计模式”。虽然 Python 也是一门支持面向对象的编程语言,但它的 鸭子类型 设计以及出色的动态特性决定了,大部分设计模式对我们来说并不是必需品。所以,很多 Python 程序员在工作很长一段时间后,可能并没有真正应用过几种设计模式。

不过 “装饰器模式(Decorator Pattern)” 是个例外。因为 Python 的“装饰器”和“装饰器模式”有着一模一样的名字,我不止一次听到有人把它们俩当成一回事,认为使用“装饰器”就是在实践“装饰器模式”。但事实上,它们是两个完全不同的东西。

“装饰器模式”是一个完全基于“面向对象”衍生出的编程手法。它拥有几个关键组成:一个统一的接口定义、若干个遵循该接口的类、类与类之间一层一层的包装。最终由它们共同形成一种“装饰”的效果。

而 Python 里的“装饰器”和“面向对象”没有任何直接联系,它完全可以只是发生在函数和函数间的把戏。事实上,“装饰器”并没有提供某种无法替代的功能,它仅仅就是一颗“语法糖”而已。下面这段使用了装饰器的代码:

 
@log_time

@cache_result

def foo(): pass

基本完全等同于下面这样:

 
def foo(): pass

foo = log_time(cache_result(foo))

装饰器最大的功劳,在于让我们在某些特定场景时,可以写出更符合直觉、易于阅读的代码。它只是一颗“糖”,并不是某个面向对象领域的复杂编程模式。

Hint: 在 Python 官网上有一个 实现了装饰器模式的例子,你可以读读这个例子来更好的了解它。

2. 记得用 functools.wraps() 装饰内层函数
下面是一个简单的装饰器,专门用来打印函数调用耗时:

 
import time

def timer(wrapped):

"""装饰器:记录并打印函数耗时"""

def decorated(*args, **kwargs):

st = time.time()

ret = wrapped(*args, **kwargs)

print("execution take: {} seconds".format(time.time() - st))

return ret

return decorated

@timer

def random_sleep():

"""随机睡眠一小会"""

time.sleep(random.random())

timer 装饰器虽然没有错误,但是使用它装饰函数后,函数的原始签名就会被破坏。也就是说你再也没办法正确拿到 random_sleep 函数的名称、文档内容了,所有签名都会变成内层函数 decorated 的值:

 
print(random_sleep.__name__)

# 输出 "decorated"

print(random_sleep.__doc__)

# 输出 None

这虽然只是个小问题,但在某些时候也可能会导致难以察觉的 bug。幸运的是,标准库 functools 为它提供了解决方案,你只需要在定义装饰器时,用另外一个装饰器再装饰一下内层 decorated 函数就行。

听上去有点绕,但其实就是新增一行代码而已:

 
def timer(wrapped):

# 将 wrapper 函数的真实签名赋值到 decorated 上

@functools.wraps(wrapped)

def decorated(*args, **kwargs):

# <...> 已省略

return decorated

这样处理后, timer 装饰器就不会影响它所装饰的函数了。

 
print(random_sleep.__name__)

# 输出 "random_sleep"

print(random_sleep.__doc__)

# 输出 "随机睡眠一小会"

3. 修改外层变量时记得使用 nonlocal
装饰器是对函数对象的一个高级应用。在编写装饰器的过程中,你会经常碰到内层函数需要修改外层函数变量的情况。就像下面这个装饰器一样:

 
import functools

def counter(func):

"""装饰器:记录并打印调用次数"""

count = 0

@functools.wraps(func)

def decorated(*args, **kwargs):

# 次数累加

count += 1

print(f"Count: {count}")

return func(*args, **kwargs)

return decorated

@counter

def foo():

pass

foo()

为了统计函数调用次数,我们需要在 decorated 函数内部修改外层函数定义的 count 变量的值。但是,上面这段代码是有问题的,在执行它时解释器会报错:

 
Traceback (most recent call last):

File "counter.py", line 22, in <module>

foo()

File "counter.py", line 11, in decorated

count += 1

UnboundLocalError: local variable "count" referenced before assignment

这个错误是由 counter 与 decorated 函数互相嵌套的作用域引起的。

当解释器执行到 count+=1 时,并不知道 count 是一个在外层作用域定义的变量,它把 count当做一个局部变量,并在当前作用域内查找。最终却没有找到有关 count 变量的任何定义,然后抛出错误。

为了解决这个问题,我们需要通过 nonlocal 关键字告诉解释器:“count 变量并不属于当前的 local 作用域,去外面找找吧”,之前的错误就可以得到解决。

 
def decorated(*args, **kwargs):

nonlocal count

count += 1

# <... 已省略 ...>

Hint:如果要了解更多有关 nonlocal 关键字的历史,可以查阅 PEP-3104

总结
在这篇文章里,我与你分享了有关装饰器的一些技巧与小知识。

一些要点总结:

一切 callable 的对象都可以被用来实现装饰器

混合使用函数与类,可以更好的实现装饰器

wrapt 模块很有用,用它可以帮助我们用更简单的代码写出复杂装饰器

“装饰器”只是语法糖,它不是“装饰器模式”

装饰器会改变函数的原始签名,你需要 functools.wraps

在内层函数修改外层函数的变量时,需要使用 nonlocal 关键字

看完文章的你,有没有什么想吐槽的?请留言或者在 项目 Github Issues 告诉我吧。
 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_45707610/article/details/124839441

智能推荐

分布式光纤传感器的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告_预计2026年中国分布式传感器市场规模有多大-程序员宅基地

文章浏览阅读3.2k次。本文研究全球与中国市场分布式光纤传感器的发展现状及未来发展趋势,分别从生产和消费的角度分析分布式光纤传感器的主要生产地区、主要消费地区以及主要的生产商。重点分析全球与中国市场的主要厂商产品特点、产品规格、不同规格产品的价格、产量、产值及全球和中国市场主要生产商的市场份额。主要生产商包括:FISO TechnologiesBrugg KabelSensor HighwayOmnisensAFL GlobalQinetiQ GroupLockheed MartinOSENSA Innovati_预计2026年中国分布式传感器市场规模有多大

07_08 常用组合逻辑电路结构——为IC设计的延时估计铺垫_基4布斯算法代码-程序员宅基地

文章浏览阅读1.1k次,点赞2次,收藏12次。常用组合逻辑电路结构——为IC设计的延时估计铺垫学习目的:估计模块间的delay,确保写的代码的timing 综合能给到多少HZ,以满足需求!_基4布斯算法代码

OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版-程序员宅基地

文章浏览阅读3.3k次,点赞3次,收藏5次。OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版

关于美国计算机奥赛USACO,你想知道的都在这_usaco可以多次提交吗-程序员宅基地

文章浏览阅读2.2k次。USACO自1992年举办,到目前为止已经举办了27届,目的是为了帮助美国信息学国家队选拔IOI的队员,目前逐渐发展为全球热门的线上赛事,成为美国大学申请条件下,含金量相当高的官方竞赛。USACO的比赛成绩可以助力计算机专业留学,越来越多的学生进入了康奈尔,麻省理工,普林斯顿,哈佛和耶鲁等大学,这些同学的共同点是他们都参加了美国计算机科学竞赛(USACO),并且取得过非常好的成绩。适合参赛人群USACO适合国内在读学生有意向申请美国大学的或者想锻炼自己编程能力的同学,高三学生也可以参加12月的第_usaco可以多次提交吗

MySQL存储过程和自定义函数_mysql自定义函数和存储过程-程序员宅基地

文章浏览阅读394次。1.1 存储程序1.2 创建存储过程1.3 创建自定义函数1.3.1 示例1.4 自定义函数和存储过程的区别1.5 变量的使用1.6 定义条件和处理程序1.6.1 定义条件1.6.1.1 示例1.6.2 定义处理程序1.6.2.1 示例1.7 光标的使用1.7.1 声明光标1.7.2 打开光标1.7.3 使用光标1.7.4 关闭光标1.8 流程控制的使用1.8.1 IF语句1.8.2 CASE语句1.8.3 LOOP语句1.8.4 LEAVE语句1.8.5 ITERATE语句1.8.6 REPEAT语句。_mysql自定义函数和存储过程

半导体基础知识与PN结_本征半导体电流为0-程序员宅基地

文章浏览阅读188次。半导体二极管——集成电路最小组成单元。_本征半导体电流为0

随便推点

【Unity3d Shader】水面和岩浆效果_unity 岩浆shader-程序员宅基地

文章浏览阅读2.8k次,点赞3次,收藏18次。游戏水面特效实现方式太多。咱们这边介绍的是一最简单的UV动画(无顶点位移),整个mesh由4个顶点构成。实现了水面效果(左图),不动代码稍微修改下参数和贴图可以实现岩浆效果(右图)。有要思路是1,uv按时间去做正弦波移动2,在1的基础上加个凹凸图混合uv3,在1、2的基础上加个水流方向4,加上对雾效的支持,如没必要请自行删除雾效代码(把包含fog的几行代码删除)S..._unity 岩浆shader

广义线性模型——Logistic回归模型(1)_广义线性回归模型-程序员宅基地

文章浏览阅读5k次。广义线性模型是线性模型的扩展,它通过连接函数建立响应变量的数学期望值与线性组合的预测变量之间的关系。广义线性模型拟合的形式为:其中g(μY)是条件均值的函数(称为连接函数)。另外,你可放松Y为正态分布的假设,改为Y 服从指数分布族中的一种分布即可。设定好连接函数和概率分布后,便可以通过最大似然估计的多次迭代推导出各参数值。在大部分情况下,线性模型就可以通过一系列连续型或类别型预测变量来预测正态分布的响应变量的工作。但是,有时候我们要进行非正态因变量的分析,例如:(1)类别型.._广义线性回归模型

HTML+CSS大作业 环境网页设计与实现(垃圾分类) web前端开发技术 web课程设计 网页规划与设计_垃圾分类网页设计目标怎么写-程序员宅基地

文章浏览阅读69次。环境保护、 保护地球、 校园环保、垃圾分类、绿色家园、等网站的设计与制作。 总结了一些学生网页制作的经验:一般的网页需要融入以下知识点:div+css布局、浮动、定位、高级css、表格、表单及验证、js轮播图、音频 视频 Flash的应用、ul li、下拉导航栏、鼠标划过效果等知识点,网页的风格主题也很全面:如爱好、风景、校园、美食、动漫、游戏、咖啡、音乐、家乡、电影、名人、商城以及个人主页等主题,学生、新手可参考下方页面的布局和设计和HTML源码(有用点赞△) 一套A+的网_垃圾分类网页设计目标怎么写

C# .Net 发布后,把dll全部放在一个文件夹中,让软件目录更整洁_.net dll 全局目录-程序员宅基地

文章浏览阅读614次,点赞7次,收藏11次。之前找到一个修改 exe 中 DLL地址 的方法, 不太好使,虽然能正确启动, 但无法改变 exe 的工作目录,这就影响了.Net 中很多获取 exe 执行目录来拼接的地址 ( 相对路径 ),比如 wwwroot 和 代码中相对目录还有一些复制到目录的普通文件 等等,它们的地址都会指向原来 exe 的目录, 而不是自定义的 “lib” 目录,根本原因就是没有修改 exe 的工作目录这次来搞一个启动程序,把 .net 的所有东西都放在一个文件夹,在文件夹同级的目录制作一个 exe._.net dll 全局目录

BRIEF特征点描述算法_breif description calculation 特征点-程序员宅基地

文章浏览阅读1.5k次。本文为转载,原博客地址:http://blog.csdn.net/hujingshuang/article/details/46910259简介 BRIEF是2010年的一篇名为《BRIEF:Binary Robust Independent Elementary Features》的文章中提出,BRIEF是对已检测到的特征点进行描述,它是一种二进制编码的描述子,摈弃了利用区域灰度..._breif description calculation 特征点

房屋租赁管理系统的设计和实现,SpringBoot计算机毕业设计论文_基于spring boot的房屋租赁系统论文-程序员宅基地

文章浏览阅读4.1k次,点赞21次,收藏79次。本文是《基于SpringBoot的房屋租赁管理系统》的配套原创说明文档,可以给应届毕业生提供格式撰写参考,也可以给开发类似系统的朋友们提供功能业务设计思路。_基于spring boot的房屋租赁系统论文