Python语言 :关于使用装饰器的技巧介绍

2023-11-15

转自:微点阅读  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 告诉我吧。
 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Python语言 :关于使用装饰器的技巧介绍 的相关文章

  • scipy 将一个稀疏矩阵的所有行附加到另一个稀疏矩阵

    我有一个 numpy 矩阵 想在其中附加另一个矩阵 这两个矩阵的形状为 m1 shape 2777 5902 m2 shape 695 5902 我想将 m2 附加到 m1 以便新矩阵的形状为 m new shape 3472 5902 当
  • Python 3 os.urandom

    在哪里可以找到完整的教程或文档os urandom 我需要获得一个随机 int 来从 80 个字符的字符串中选择一个字符 如果你只需要一个随机整数 你可以使用random randint a b 来自随机模块 http docs pytho
  • Sublime Text 插件开发中的全局 Python 包

    一 总结 我不知道 Sublime Text 插件开发人员如何使用 Sublime Text 查找全局 Python 包 而不是 Sublime Text 目录的 Python 包 Sublime Text使用自己的Python环境 而不是
  • 如何以“正确”的方式处理带有空字节的 Python unicode 字符串?

    Question PyWin32 似乎很乐意将 null 终止的 unicode 字符串作为返回值 我想以 正确 的方式处理这些字符串 假设我得到一个像这样的字符串 u C Users Guest MyFile asy x00 x00sy
  • 使用 python 中的公式函数使从 Excel 中提取的值的百分比相等

    import xlrd numpy excel Users Bob Desktop wb1 xlrd open workbook excel assignment3 xlsx sh1 wb1 sheet by index 0 colA co
  • Kivy - 有所有颜色名称的列表吗?

    在 Kivy 中 小部件 color属性允许输入其值作为字符串颜色名称 也 例如在 kv file Label color red 是否有所有可能的颜色名称的列表 就在这里 来自Kivy 的文档 https kivy org doc sta
  • 删除 Django 1.7 中的应用程序(和关联的数据库表)

    是否可以使用 Django 1 7 迁移来完全删除 卸载应用程序及其所有跟踪 主要是其所有数据库表 如果没有 在 Django 1 7 中执行此操作的适当方法是什么 python manage py migrate
  • pyspark 数据框中的自定义排序

    是否有推荐的方法在 pyspark 中实现分类数据的自定义排序 我理想地寻找 pandas 分类数据类型提供的功能 因此 给定一个数据集Speed列 可能的选项是 Super Fast Fast Medium Slow 我想实现适合上下文的
  • 如何在 Python 中加密并在 Java 中解密?

    我正在尝试在 Python 程序中加密一些数据并将其保存 然后在 Java 程序中解密该数据 在Python中 我像这样加密它 from Crypto Cipher import AES KEY 1234567890123456789012
  • 如何使用文本相似性删除 pandas 数据框中相似(不重复)的行?

    我有数千个数据 这些数据可能相似也可能不相似 使用 python 的默认函数 drop duplicates 并没有真正的帮助 因为它们只检测相似的数据 例如 如果我的数据包含类似以下内容怎么办 嗨 早上好 嗨 早上好 Python 不会将
  • Emacs 24.x 上的 IPython 支持

    我对 IPython 与 Emacs 的集成感到困惑 从 Emacs 24 开始 Emacs 附带了自己的python el 该文件是否支持 IPython 还是仅支持 Python 另外 维基百科 http emacswiki org e
  • Django send_mail SMTPSenderRefused 530 与 gmail

    一段时间以来 我一直在尝试使用 Django 从我正在开发的网站接收电子邮件 现在 我还没有部署它 并且我正在使用Django开发服务器 我不知道这是否会影响它 这是我的 settings py 配置 EMAIL BACKEND djang
  • Python新式类和__subclasses__函数

    有人可以向我解释为什么这有效 在 Python 2 5 中 class Foo object pass class Bar Foo pass print Foo subclasses 但这不是 class Foo pass class Ba
  • 使用Python计算目录的大小?

    在我重新发明这个特殊的轮子之前 有没有人有一个很好的例程来使用 Python 计算目录的大小 如果例程能够很好地以 Mb Gb 等格式格式化大小 那就太好了 这会遍历所有子目录 总结文件大小 import os def get size s
  • Jython 和 SAX 解析器:允许的实体不超过 64000 个?

    我做了一个简单的测试xml saxJython 中的解析器在处理大型 XML 文件 800 MB 时遇到以下错误 Traceback most recent call last File src project xmltools py li
  • 使用“默认”环境变量启动新的子进程

    我正在编写一个构建脚本来解析依赖的共享库 及其共享库等 这些共享库在正常情况下是不存在的PATH环境变量 为了使构建过程正常工作 让编译器找到这些库 PATH已更改为包含这些库的目录 构建过程是这样的 加载器脚本 更改 PATH gt 基于
  • 在 keras 中保存和加载权重

    我试图从我训练过的模型中保存和加载权重 我用来保存模型的代码是 TensorBoard log dir output model fit generator image a b gen batch size steps per epoch
  • 从 NumPy 数组到 Mat 的 C++ 转换 (OpenCV)

    我正在围绕 ArUco 增强现实库 基于 OpenCV 编写一个薄包装器 我试图构建的界面非常简单 Python 将图像传递给 C 代码 C 代码检测标记并将其位置和其他信息作为字典元组返回给 Python 但是 我不知道如何在 Pytho
  • TKinter 中的禁用/启用按钮

    我正在尝试制作一个像开关一样的按钮 所以如果我单击禁用按钮 它将禁用 按钮 有效 如果我再次按下它 它将再次启用它 我尝试了 if else 之类的东西 但没有成功 这是一个例子 from tkinter import fenster Tk
  • 多个对象以某种方式相互干扰[原始版本]

    我有一个神经网络 NN 当应用于单个数据集时 它可以完美地工作 但是 如果我想在一组数据上运行神经网络 然后创建一个新的神经网络实例以在不同的数据集 甚至再次同一组数据 上运行 那么新实例将产生完全错误的预测 例如 对 XOR 模式进行训练

随机推荐

  • PLSQL Developer设置

    PLSQL Developer设置 解决用PLSQL Developer查询时数据大小超过100M的提示问题 Tools gt Preferences gt SQL Window Maximum Result Set size 0 is u
  • error 1962

    今天电脑出了一点小毛病 报错 error 1962 No operation system found 由于很多的资料还有项目在里边存着 而且只有一个c盘 这可怎么解决 对于一个英文不好的中国人来说 这可是个麻烦事 静下心 仔细琢磨还是能够
  • C++11新特性

    1 列表初始化 2 变量类型推导 3 范围for循环 4 final与override 5 智能指针 6 新增加容器 静态数组array forward list以及unordered系列 7 默认成员函数控制 8 右值引用 9 lambd
  • gcc -lpthread

    转自 http www cnblogs com suntp p 6473751 html 如果用gcc编译使用了POSIX thread的程序时 通常需要加额外的选项 以便使用thread safe的库及头文件 一些老的书里说直接增加链接选
  • 《机器学习的随机矩阵方法》

    机器学习的随机矩阵方法 作者 Romain Couillet 和Zhenyu Liao 出版商 剑桥大学出版社 第 1 版 2022 年 10 月 31 日 语言 英语 精装版 408 页 ISBN 10 1009123238 ISBN 1
  • chrome浏览器https证书不安全页面打开设置

    访问https协议的页面浏览器都会加载此页面所需要的证书 在证书不被信任 即证书不是有正规CA机构颁发的话 通常是由自己通过证书生成工具或命令生成的 chrome浏览器会提示页面不安全而不会直接访问该页面 此时有两种选择 选择安全方式 不再
  • 面试题创作0005,请说明Linux 和 AI的关系(联系和区别)

    请说明Linux 和 AI的关系 联系和区别 可以在AI的业务应用 平台服务提供 平台设备商 集成电路开发等各个跟AI相关的行业来寻找联系和区别
  • MATLAB——矩阵与阵列

    变量及操作 变量命名规则 赋值语句 运算符和表达式 矩阵产生与表示 直接输入法创建矩阵 向量法创建矩阵 函数法创建矩阵 特殊矩阵 矩阵元素的引用 矩阵单个元素与行列提取 向量标识方式 矩阵基本操作 矩阵提取子块 合并短阵 转置与展开 提取子
  • Lion闭源大语言模型的对抗蒸馏框架实践

    Lion闭源大语言模型的对抗蒸馏框架实践 概述 对抗蒸馏框架概述 我们基于高级闭源LLM的基础上提炼一个学生LLM 该LLM具有三个角色 教师 裁判和生成器 有三个迭代阶段 模仿阶段 对于一组指令 将学生的响应与老师的响应对齐 区分阶段 识
  • ESP8266-NodeMCU(一)

    ESP8266 NodeMCU开发板的驱动有CH340和CP210等等 本文使用ESP8266 NodeMCU CH340驱动版本 一 开发板详解 NodeMCU是一个开源的IoT物联网硬件开发板 由于它支持WIFI功能且使用方法十分类似A
  • RAM处理器的8种寻址方式

    什么是寻址 寻址是指找到存储数据或指令的地址 然后读取其中的内容 寻址方式就是处理器根据指令中给出的地址信息来寻找有效地址的方式 是确定本条指令的数据地址以及下一条要执行的指令地址的方法 ARM处理器采用的RISC架构 CPU本身是不能直接
  • MyBatis resultMap collection标签 返回基本类型集合 如:List<Long> List<String> List<Integer>等

    class xxDTO private Long id private Set
  • vc 判断某个盘符是否为移动硬盘盘符

    在使用GetDriveType获取磁盘类型时 一般小容量的U盘直接返回DRIVE REMOVABLE 倒是不用再进行下一步的判断 而大容量U盘和移动硬盘的盘符返回值和本地硬盘盘符返回值都是DRIVE FIXED 需要再进行判断 如果是IDE
  • 【paddlepaddle】一键人物抠图

    效果 环境准备 win11 python3 8 pip install paddlepaddle i https pypi tuna tsinghua edu cn simple pip install paddlehub i https
  • animation 动画的定义和使用

    keyframes 定义动画 keyframes myname 0 50 100 调用动画 div animation name myfirst animation duration 5s animation timing function
  • Unity 开发人员转CGE(castle Game engine)城堡游戏引擎指导手册

    Unity 开发人员的城堡游戏引擎概述 一 简介 2 Unity相当于什么GameObject 3 如何设计一个由多种资产 生物等组成的关卡 4 在哪里放置特定角色的代码 例如生物 物品 Unity 中 向 GameObject 添加 Mo
  • U盘启动盘制作(步骤详细)

    U盘启动盘制作 在制作启动盘之前我们需要先准备一个8G以上的U盘 和一台能上网的电脑 一 下载系统镜像 根据自己需要的系统版本去下载官方的镜像文件 记得要下载纯净的镜像 否则在后续安装好系统后会出现捆绑的现象 可以直接去下面这个网站下载 下
  • rsync实现文件服务器间文件同步

    rsync介绍 rsync命令工具可以实现服务器间的文件同步 全量或者增量 比如使用 size only来检查源端文件和目标端文件大小是否一致决定是否需要同步 由此同步的功能扩展 可以实现本机不同目录文件拷贝 快速删除海量文件等功能 但要注
  • MySQL隔离级别

    表结构和表数据如下 id 自增主键 uid 唯一索引 name price 普通索引 pictures 33 a Apple 12 NULL 34 b banana 5 NULL 35 c cherry 51 NULL 36 d date
  • Python语言 :关于使用装饰器的技巧介绍

    转自 微点阅读 https www weidianyuedu com 导语 装饰器 Decorator 是 Python 里的一种特殊工具 它为我们提供了一种在函数外部修改函数的灵活能力 它有点像一顶画着独一无二 符号的神奇帽子 只要将它戴