Skip to main content

类装饰器

在之前一篇文章里,我们介绍过这个装饰器,也就是 decorator。在那篇文章里,我说这个装饰器就是一个输入是函数、输出也是函数的这么一个函数。那这个定义,其实它并不完全准确。这么说,只是让刚入门装饰器的人可以理解它的核心理念。

那在那篇文章的留言里,就有人让我讲一下类装饰器。那包括后面,也有很多人通过留言或者私信的方式,让我讲一讲这个类装饰器。所以这篇文章,我们就讲一下装饰器和类之间的爱恨纠葛。

那“类装饰器”这个词,包括它的英文 class decorator,实际上是有一定的歧义的。在有的地方,它是指可以当作装饰器的类;那在有些地方,它是指可以装饰类的装饰器。那这其实是两个正交的概念:一个聊的是装饰器本身,一个聊的是装饰器要装饰的对象。

那么通过今天的文章,你就可以知道,装饰器本身既可以是函数,也可以是类;而装饰的对象,同样既可以是函数,也可以是类。所以你要弄懂的是它背后的核心机制和理念,而不是一个一个的组合去硬背。

可以当作装饰器的类​

好,我们看下面的代码啊。我定义了一个 Timer 类,然后这个 Timer 呢,有一个 __init__ 函数。这个 __init__ 函数呢,take 一个 argument,是一个 function,然后把它保存在自己的这个 object 里。

第 1 步 · 用 Timer 类装饰函数1 / 6
main.py
import time
class Timer:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
start = time.time()
ret = self.func(*args, **kwargs)
print(f"Time: {time.time() - start}")
return ret
@Timer
def add(a, b):
return a + b
print(add(2, 3))

接下来呢,我还定义了它的一个魔术方法,是 __call__。那可能有的人对它熟悉,有的人对它不熟悉。有关魔术方法呢,未来我们有机会认真讲。但是这个 __call__ 呢,就是让所有这个类的实例都变成一个 callable。简单地理解呢,就是这个类的实例,这个类的对象,都可以当作函数用。你都可以调用它,在后面画一个小括号。

然后我把 Timer 这个类本身当作一个装饰器,装饰在了这个 add 函数上。这个 add 函数很简单啊,就是返回 a 加 b。那这个装饰器呢,跟我们上一篇文章里的例子其实是一样的。我们先看一下运行结果。可以看到呢,它依然是先打印出来了这个函数需要的时间,然后在第 17 行打印出来这个函数的返回值。

装饰器的等价形式​

那问题来了,这个 Timer 类究竟是怎么作为一个装饰器起作用的呢?那可能很多教程就要让你在这背了,对吧?一个类作为装饰器要怎么样,一个函数要怎么样,后面加了 argument 要怎么样。但是今天我教给你,你只需要记住一条万能公式,这些东西你全都不用背,就是这个装饰器语法糖背后真正的等价形式。也就是 13 行这个 @Timer 究竟做了什么。

而在上篇文章里面,这件事我讲过:在一个函数上做装饰器,就等价于这个函数名等于装饰器调用这个函数。也就是说,第 13 行的 @Timer,它就相当于在后面运行了一个 add 等于 Timer 括号 add。那如果你能把它转换成这个形式的话,你是不是就立刻可以明白到底发生什么事了?

第 2 步 · 写出等价形式2 / 6
main.py
import time
class Timer:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
start = time.time()
ret = self.func(*args, **kwargs)
print(f"Time: {time.time() - start}")
return ret
@Timer
def add(a, b):
return a + b
# 等价于
add = Timer(add)
print(add(2, 3))

这个装饰器相当于把 add 从一个函数变成了一个 Timer 类的对象。因为我做了一个 Timer(add) 嘛,Timer 是一个类,对不对?它相当于建立了一个这个类的对象。然后 add 被传进了它的 __init__ 函数里,__init__ 函数呢,take 一个 func,对不对?所以 add 作为一个参数被保存在了 self.func 里,然后整个这个 Timer object 被保存在了 add 这个名字里。

那接下来第 20 行 add(2, 3) 的时候,它实际上是调用了这个 Timer object 的 __call__ 这个函数,它相当于做了一个对象的调用。那这个时候呢,2 跟 3 就被传到了这个 __call__ 里面。所以它就运行了 start 等于时间,然后拿到一个 return value。self.func 就是原来的 add 函数,对不对?它就把 a 跟 b 加起来,然后返回到了 ret 里,然后打印了消耗的时间,再返回这个值,最后这个值再被 print 出来。

可以看到,它跟我们之前讲的函数 decorator,其实没有很大的区别。原来的函数装饰器呢,是函数调用一个函数,返回一个函数。更确切地说呢,是函数调用一个函数,返回一个 callable。那现在呢,是类调用一个函数,返回一个 object,它依然是一个 callable。你只需要把这个 decorator 在脑海中,转化成下面这个等价形式,你就非常容易理解了。我们甚至还可以在这里打印一下 add 的 type,可以看到,add 就是一个 Timer object。

第 3 步 · 打印 add 的类型3 / 6
main.py
import time
class Timer:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
start = time.time()
ret = self.func(*args, **kwargs)
print(f"Time: {time.time() - start}")
return ret
@Timer
def add(a, b):
return a + b
# 等价于
# add = Timer(add)
add = Timer(add)
print(type(add))
print(add(2, 3))

带参数的装饰器类​

好,那我们之前讲过,装饰器本身也可以有 argument,对不对?假如说我们举个例子,之前我们打印的不是 time 冒号,然后这个函数使用的时间吗?现在呢,我想让这个 time 冒号这个部分啊,变成可变的。也就是我想让第 13 行啊,变成这个样子。这个 Timer 本身呢,我还想给它传入一个参数 prefix,让它在打印的时候呢,先打印我给进去的这个 string,然后再打时间。

我们在上一篇文章里面聊过函数的装饰器,怎么完成这个事,对不对?那用类作为装饰器的时候,怎么完成这个事呢?你现在可能又有点发懵,对吧?那就返回到我们的那个万能公式,把它写成它的等价形式。

第 4 步 · 给装饰器加上 prefix4 / 6
main.py
import time
class Timer:
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
start = time.time()
ret = self.func(*args, **kwargs)
print(f"Time: {time.time() - start}")
return ret
@Timer(prefix="curr_time: ")
@Timer
def add(a, b):
return a + b
# 等价于
add = Timer(prefix="curr_time: ")(add)
# add = Timer(add)
print(type(add))
print(add(2, 3))

那第 13 行的那个装饰器呢,现在等价于第 18 行的这行代码。我们可以看到,add 等于 Timer 括号,prefix 等于 curr_time,然后再括号 add。也就是说,我们首先建立了一个 Timer object,然后调用这个 object,把 add 作为一个参数传进去,再把调用的返回值保存到 add 里。

那我们对应的类该怎么改呢?首先,Timer 作为一个 class,它的参数就不再是一个函数了,而是 prefix。接下来,当我们调用这个 Timer object 的时候,我们希望它的输入是一个函数,返回值也是一个函数。因此,这个 __call__ method,它的参数肯定是 function,对不对?然后它里面要再定义一个函数,这个函数会成为我们的返回值。

第 5 步 · 改写 Timer 的结构5 / 6
main.py
import time
class Timer:
def __init__(self, prefix):
self.prefix = prefix
def __init__(self, func):
self.func = func
def __call__(self, func):
def wrapper(*args, **kwargs):
start = time.time()
ret = self.func(*args, **kwargs)
print(f"Time: {time.time() - start}")
return ret
return wrapper
def __call__(self, *args, **kwargs):
start = time.time()
ret = self.func(*args, **kwargs)
print(f"Time: {time.time() - start}")
return ret
@Timer(prefix="curr_time: ")
def add(a, b):
return a + b
# 等价于
add = Timer(prefix="curr_time: ")(add)
print(add(2, 3))

而这个函数里面,就是刚才我们的计时,运行传进来的这个参数 func,然后再打印消耗的时间。那这个部分呢,长得就跟我们之前写的函数装饰器,很像了。

那其实只要你一步一步推,从这个 Timer(prefix) 返回了一个 object,然后它再做了一个 call,然后 add 传进 function,返回的是一个 wrapper,存到 add 里面。这么一路下来,其实逻辑就会变得非常顺。

当然了,现在只是结构是对的。那为了让这个程序的功能也正确,我们还需要把 time 冒号给改成 self.prefix。我们看,我们把 prefix 加上去,然后运行一下这条代码,大家可以看到,前面打印的部分就变了。

第 6 步 · 用上 self.prefix6 / 6
main.py
import time
class Timer:
def __init__(self, prefix):
self.prefix = prefix
def __call__(self, func):
def wrapper(*args, **kwargs):
start = time.time()
ret = func(*args, **kwargs)
print(f"{self.prefix}: {time.time() - start}")
ret = self.func(*args, **kwargs)
print(f"Time: {time.time() - start}")
return ret
return wrapper
@Timer(prefix="curr_time: ")
def add(a, b):
return a + b
# 等价于
# add = Timer(prefix="curr_time: ")(add)
add = Timer(prefix="curr_time: ")(add)
print(add(2, 3))

装饰类的装饰器​

好,那我们介绍了这个可以成为装饰器的类呢,其实用中文的语境讲,它更像“装饰器类”,而不是“类装饰器”。那接下来呢,我们就介绍一下真正的类的装饰器。

好,我们来看下面这一段代码。现在第 7 行,在一个 class 的 definition 上面,我给加了一个装饰器。那我们知道,在 Python 里面,一个自定义的 class 的对象,当你 print 它的时候,它是不会打印出任何有价值的信息的,它只会告诉你这个对象是一个什么类。那我们可以通过重载它的 __str__ 函数,来改变它 print 出来的结果。当然了,如果每一个对象都改的话,也挺麻烦的。所以我就写了这么一个简单的装饰类的装饰器。

第 1 步 · 给类加上 __str__1 / 3
main.py
def add_str(cls):
def __str__(self):
return str(self.__dict__)
cls.__str__ = __str__
return cls
@add_str
class MyObject:
def __init__(self, a, b):
self.a = a
self.b = b
o = MyObject(1, 2)
print(o)

我们先看一下效果。大家可以看到,我打印出来的东西是 a 是 1,b 是 2。这个也就是 o,也就是 MyObject 的这个对象里面的数据。

这时候你可能会想了,对吧?这个装饰器能给函数用,怎么还能给 class 用呢?它们之间有什么不同吗?每当你遇到装饰器感到迷茫的时候,你只需要把它换成它的等价形式。

第 2 步 · 写出等价形式2 / 3
main.py
def add_str(cls):
def __str__(self):
return str(self.__dict__)
cls.__str__ = __str__
return cls
@add_str
class MyObject:
def __init__(self, a, b):
self.a = a
self.b = b
# 等价于
MyObject = add_str(MyObject)
o = MyObject(1, 2)
print(o)

我们来换一下。我们知道,在 Python 中,定义的 class 本身它也是一个 object。那么第 7 行的 add_str,就相当于 14 行的 MyObject 等于 add_str(MyObject)。也就是 add_str 这个函数,是一个参数是 class、返回值也是 class 的函数。那我们再往上看这个 add_str 的 definition,它的参数是一个 class,cls,对不对?它的返回值是原封不动地返回了这个 class。只不过它在第 4 行的时候,把 class 的 __str__ 函数给替代成了它自定义的 str 函数。而它定义的这个 str 函数,是返回 self.__dict__ 的 str 形式。也就是说,这个 decorator 本质上就是重载了一下这个 class 的 __str__ 函数。

那如果你能把它成功地想象成这个等价形式的话,你就会发现,给类写装饰器也没有什么难的嘛。它就是输入一个类,返回一个类嘛。然后我在这个类上动动手脚,完成我需要的事情就可以了。

当然,同样的,这个装饰器也是可以传参数的。如果你弄不懂的话,依然是把它写成等价形式,然后再去想,我这个函数的返回值应该是一个什么。那样的话,这个函数的返回值就应该是一个函数了,而返回的这个函数,参数是一个 class,返回值是一个 class。

那通过今天这几个例子,大家应该可以感受到,不管是 decorator 本身,还是它装饰的对象,不管它们是函数还是类,只要你可以把它转换成等价形式,它都是比较好读的。而转换成等价形式之后,你就知道我需要写一个什么东西,它的输入应该是什么,它的输出应该是什么。就再也不用照着网上教程抄,然后还不知道对不对了。

objprint 中的真实案例​

当然,你可能会问,这么偏门的东西有用吗?当然有用了。在我写的 objprint 这个包里,就有一个 decorator,叫做 add_objprint。那对于任何你自定义的类,你只需要把这个装饰器装饰到这个类上,你就可以把这个类的信息用 print 给正常地打印出来了。

第 3 步 · 换成 objprint 的 add_objprint3 / 3
main.py
from objprint import add_objprint
def add_str(cls):
def __str__(self):
return str(self.__dict__)
cls.__str__ = __str__
return cls
@add_objprint
@add_str
class MyObject:
def __init__(self, a, b):
self.a = a
self.b = b
# 等价于
MyObject = add_str(MyObject)
o = MyObject(1, 2)
print(o)

我们运行一下。大家可以看到,它就打印出来,这个 o 是一个 MyObject 的对象,然后它里面两个 member variable,a 是 1,b 是 2。那这种用法对于 debug 来说,是相对来说比较简单的,因为你基本上只需要给你的 class 加一个 decorator 就 OK 了。

好,那今天的文章就到这里。希望对大家理解装饰器有所帮助。