Decorator Playground
果然还是得上 notebook
装饰器是一种用于修改函数或类行为的设计模式。它们通常以函数或类的形式出现。
警告:此部分内容全是个人理解,请以官方文档为准
核心机制:装饰器接受一个函数或类作为输入,并返回一个经过修改或增强的对象来替换原来位置的定义,以便在不改变原始代码的情况下添加额外的行为。多个装饰器会以嵌套方式被应用。
另外涉及到的一个概念闭包(closure),是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。[1] 大概就是说,函数定义时如果引用到了外部定义中的对象,这个引用也会被作为函数定义的一部分被保存。(个人理解请以官方文档为准)
注意
尽管在类或函数定义的上一行使用 @
符紧接装饰器(e.g. @wrapper
)能方便地装饰这个类或函数,人们也都是这样做的,但 ——
@
符只是一个语法糖。下面的两个单元格中的代码在语义上完全等价。
—— 希望这有助于你的理解。
def f(arg):
...
f = wrapper(f)
2
3
@wrapper
def f(arg):
...
2
3
(嗯这里的 wrapper
指的是一个装饰器)
装饰器函数
此种装饰器以函数的形式存在。它接收一个函数,返回一个被修改 / 新的函数,或者接收一个类,返回一个被修改的类。
装饰函数的装饰器函数
最常用。用于装饰一个函数的函数。一般用于在函数执行前后添加行为(比如记日志、做资源检查与释放、重试什么的)
一个简单的例子如下,实现了一个最简单的重试装饰器:
def retry(func):
def wrapped(*args, **kwargs):
while True:
try:
return func(*args, **kwargs) # <- 原始函数在这里!
except Exception as e:
pass
return wrapped
2
3
4
5
6
7
8
试着找出这个装饰器的问题(答案藏在本节某处喵 w)
再将它与第三方库
retry
中成熟的重试装饰器做对比,看看大佬是怎么做的。(如果愿意的话还可以去翻翻它的底裤源代码,看看具体是怎么实现的)
我们来试试使用这个简单的装饰器吧。
这是一个 trouble_maker
,它有 1/3
的概率报错。
尝试在不同的函数定义下(使用装饰器和不使用装饰器)反复运行它们,看看会不会报错、
def trouble_maker(msg):
from random import randint
if randint(1, 3) == 1:
raise RuntimeError(msg)
2
3
4
@retry
def trouble_maker(msg):
from random import randint
if randint(1, 3) == 1:
raise RuntimeError(msg)
2
3
4
5
trouble_maker("wwwwwwwwww")
好一个 trouble maker,笑死
包括装饰函数的装饰器函数在内的,返回值为函数或参数含有函数的函数,被称作高阶函数。Python 中有一个专门的标准库对这样的函数提供支持:functools
装饰类的装饰器函数
用于装饰一个类的函数。与上面那哥们不同,它一般用于给类添加方法或属性。
一个简单的例子如下:
def add_method(cls):
def new_method(self):
print("new method of class", cls, ", instance", self)
print("name:", self.name)
cls.new_method = new_method
return cls
2
3
4
5
6
它给传入的类添加了一个新方法,并假设传入的类拥有 name
这个属性。调用此方法时会直接打印所属类、所属实例和 name
属性。
让我们试着使用它。
@add_method
class Kelas:
def __init__(self, name):
self.name = name
2
3
4
k = Kelas("Nyan")
k.new_method()
2
Output
new method of class <class '__main__.Kelas'> , instance <__main__.Kelas object at 0x000001E106C94350>
name: Nyan
2
装饰器类
此种装饰器以类的形式出现。与装饰器函数的机制差不多,但写法不同,自由度也更高。(也用得相对很少)
前面提到,调用装饰器返回的东西会用来替换原来位置的定义。装饰器类也一样,它使用自己实例化产生的实例替代被装饰的类或函数,原始类 / 函数被保存为实例的一个属性。
装饰函数的装饰器类
装饰器类实例化产生的实例必须带有 __call__
方法,这个特殊方法能让实例变得可被调用,不然也就无法替代原始函数了。
一个简单的例子如下。这个例子展示了一个用于计数调用次数的装饰器类。使用方法仍然与先前的一致。
将 __call__
方法注释掉再试试,看看会发生什么。
class CountCalls:
def __init__(self, func):
self.func = func
self.call_count = 0
def __call__(self, *args, **kwargs):
self.call_count += 1
print(f"Calling {self.func.__name__} {self.call_count} times")
return self.func(*args, **kwargs)
2
3
4
5
6
7
8
9
@CountCalls
def say_hi():
print("Hi!")
2
3
say_hi()
Output
Calling say_hi 24 times
Hi!
2
装饰类的装饰器类
装饰器类产生的实例会替代原始类,这个实例的 __call__
方法会代理原始类的 __init__
方法。
同样是一个简单的例子。这个例子展示了一个用于计数实例化次数的装饰器类。使用方法仍然与先前的一致。
class CountInstance:
def __init__(self, cls):
self.cls = cls
self.inst_count = 0
def __call__(self, *args, **kwargs):
self.inst_count += 1
print(f"Instantiating {self.cls.__name__} {self.inst_count} times")
return self.cls(*args, **kwargs)
2
3
4
5
6
7
8
9
@CountInstance
class Cat:
def __init__(self) -> None:
print("A new kitten is born!")
2
3
4
if "cats" in vars():
cats.append(Cat())
else:
cats = [Cat()]
2
3
4
Output
Instantiating Cat 5 times
A new kitten is born!
2
一些内置装饰器
虽然官方文档里「装饰的概念也适用于类,但通常较少这样使用」这样说了,但是标准库里可还是有一堆用类实现的装饰器啊 ww
(仅列举出部分)
装饰器 | 说明 |
---|---|
staticmethod | 将一个方法声明为静态方法 |
classmethod | 将一个方法声明为类方法 |
property | 将一个方法装饰为property 属性 |
abc.abstractmethod | 将一个方法声明为抽象方法 |
functools.wraps | 用于在装饰器定义中复制原函数的元数据,参见官方文档 |
functools.singledispatch | 用于方便地定义泛型函数,参见官方文档 |