Skip to main content

类里的装饰器

用装饰器打印函数调用​

这篇文章我们来讲一个跟装饰器也就是 decorator 有关的用法。我们看这里我们写了一个 decorator log_function,这个 decorator 也很简单,就是在运行一个函数之前先打印一下函数开始了,再打印一下这个函数的 argument,在运行函数之后呢打一个这个函数结束了。那我们在下面写了一个简单的斐波那契函数。我们把这个斐波那契函数加上我们的装饰器看一下结果。

第 1 步 · 用 log_function 装饰 fib1 / 10
main.py
def log_function(func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper
@log_function
def fib(n):
if n <= 1:
return 0
return fib(n - 1) + fib(n - 2)
fib(3)

可以看到啊虽然这个输出呢用处似乎不是那么大,但是它确实是工作了的。如果你细细的分析的话,也是能感受到里面帮助你理解递归的一些信息。

为什么要把装饰器放进类里​

然而装饰器的功能并不是我们今天的重点。我们在写装饰器的时候,经常会把这个装饰器函数放到一个文件的全局的命名空间里,也就是把这个函数定义在这个文件的最外层。然而我们知道并不是所有的函数都适合放到全局空间的。那有的时候我就是想把我的装饰器放到一个类里面,我希望把它抽象,或者说我有若干个功能相似的装饰器,我就是希望把它们归类到同一个类里面。那到这里的时候很多人就不知道该怎么办了啊。所以我们这篇文章的重点是讲如何把一个装饰器放到类里面。

装饰器作为对象方法​

好我们来看我们左边的程序,这是我们最容易想到的一种实现。把装饰器挪到类里面,本质上它可能就相当于把一个函数变成类的方法,对不对?那我们就按照一般的方式把这个函数加上一个 self argument,然后直接把它扔到类里面就可以了。那当你使用这种方法的时候,你首先要意识到一个类的方法必须由这个类的对象所调用。所以当我们想使用这个装饰器的时候,我们首先要建立一个 Decorators 这个 class 的 object,也就是第 11 行这里的 d 等于 Decorators。那接下来我们就可以用这个 d.log_function 来装饰这个斐波那契函数。我们可以看到这种使用方式,最后得到的结果跟之前是一样的。

第 2 步 · 把装饰器放进类里2 / 10
main.py
class Decorators:
def log_function(self, func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper
d = Decorators()
def log_function(func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper
@d.log_function
@log_function
def fib(n):
if n <= 1:
return 0
return fib(n - 1) + fib(n - 2)
fib(3)

那这么用有什么问题呢?它确实是有。首先非常明显的,我们为了使用这个装饰器,迫不得已的要建立一个新的对象,这个事情我非常不爽。其次我这个装饰器里面莫名其妙出现了一个 self argument,然而我这个装饰器压根就没有用到它,对吧?

装饰器作为类方法​

好那解决第一个问题,我们知道有一种方法,可以把一个类里面定义的 method 变成 classmethod,也就是把这个方法从一个对象才能调用的方法,变成一个类就可以调用的方法。这里呢我们可以使用这个 classmethod 装饰器。我们看在使用了 classmethod 装饰器之后,这个 log_function 就变成了一个类方法。那在第 12 行的时候我们就不需要再新建一个对象才能使用这个装饰器了。我们可以直接用这个 Decorators 类的 log_function 来调用这个装饰器。那现在的状态呢确实是比之前要稍微好一些,但是这个 classmethod 它第一个 argument 依然是 class,我们还是没有用到这个变量对吧?

第 3 步 · 改成类方法3 / 10
main.py
class Decorators:
@classmethod
def log_function(cls, func):
def log_function(self, func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper
@Decorators.log_function
d = Decorators()
@d.log_function
def fib(n):
if n <= 1:
return 0
return fib(n - 1) + fib(n - 2)
fib(3)

当然了在某些情况下我们可能会想使用到这个 class 里面的一些 member,甚至是一个 object 里面的一些 member,也就是我这个装饰器啊,会根据我这个 class 或者这个 object 的变化改变它自己的功能。那这种情况下我们当然可以把它作为一个对象方法,或者把它作为一个类方法。可如果我们就想用现在的这个装饰器,它确实是跟对象无关,跟类也无关的时候,我们单纯的只是想把它放到类里面做一个封装。

装饰器作为静态方法​

我们就可以把这个 classmethod 变成一个 staticmethod。我们看我们可以把这个 log_function 里面的第一个 argument,刚才的 self 或者 class 直接删掉,然后使用这个 staticmethod 去装饰它。这样这个 log_function 就变成了一个静态方法。它依然是被封装在这个类里的,但是它的功能与这个类其他的东西是无关的。在使用的时候,我们依然是用这个 Decorators.log_function。但是我们看 log_function 的 definition,已经跟刚才我们写的一模一样了。那当你仅仅需要把一个装饰器封装到一个类里面的时候,这就是一个非常不错的方式。

第 4 步 · 改成静态方法4 / 10
main.py
class Decorators:
@staticmethod
def log_function(func):
@classmethod
def log_function(cls, func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper
@Decorators.log_function
def fib(n):
if n <= 1:
return 0
return fib(n - 1) + fib(n - 2)
fib(3)

用类里的装饰器装饰方法​

然而这还没完。接下来我们要讲一个很多人都遇到过然后放弃了的问题,就是如果我想用我类里面定义的属于这个类的装饰器,去装饰我这个类里面的方法,我应该怎么做?我们看我把这个 fib 函数给挪到了这个 Decorators 类里。这其实是我们非常常见的一种可能性。就是我一个类里面的很多方法,我都希望用装饰器帮它装饰一下,但是我这个装饰器只会在这个类里面用,我又想把这个装饰器给封装到这个类里来。很多人这时候就不知道该怎么办了。

如果像我们刚才讲的使用 staticmethod,你会发现它直接给你报错了。

第 5 步 · 在类里用静态方法装饰 fib5 / 10
main.py
class Decorators:
@staticmethod
def log_function(func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper
@log_function
def fib(self, n):
if n <= 1:
return 0
return self.fib(n - 1) + self.fib(n - 2)
@Decorators.log_function
def fib(n):
if n <= 1:
return 0
return fib(n - 1) + fib(n - 2)
d = Decorators()
d.fib(3)
fib(3)
Python 3.10 起不再报错

这一步的输出是用 Python 3.9 录的。从 Python 3.10 开始,staticmethod 对象本身可以直接调用,所以在类里用它装饰 fib 也能正常工作,不会报错。

那有的人可能说我直接把它当成一个正常的类的 method 呢,当然依然是不对的。

第 6 步 · 在类里用对象方法装饰 fib6 / 10
main.py
class Decorators:
def log_function(self, func):
@staticmethod
def log_function(func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper
@log_function
def fib(self, n):
if n <= 1:
return 0
return self.fib(n - 1) + self.fib(n - 2)
d = Decorators()
d.fib(3)

那在这里告诉大家,这种情况下正确的用法是:直接把 log_function 这个函数原封不动的放到这个 Decorators class 里,不需要给它加任何的装饰器。好多人看到这可能就懵了对吧,说这是什么东西?我从来没有在 Python 里见到过长成这样的类的方法。然而如果你运行一下这段程序的话,你会发现它是正常工作的。

第 7 步 · 不加任何装饰器7 / 10
main.py
class Decorators:
def log_function(func):
def log_function(self, func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper
@log_function
def fib(self, n):
if n <= 1:
return 0
return self.fib(n - 1) + self.fib(n - 2)
d = Decorators()
d.fib(3)

当然由于我们修改了 fib 这个函数,所以我们在打印 args 的时候呢,它多了一个东西。但是我们可以看到 log_function 这个 decorator,它是正常工作的。那很多人可能觉得莫名其妙对吧,为什么呢?

类定义中的辅助函数​

原因其实也很简单。我们在之前的文章里面提到过,当我们在定义一个新的 class 的时候,它本质上是把 class 里面的这个 code block,在另外一个命名空间运行了一下。而我们只要把这个 code block 单独拿出来,它为什么工作,所有人都知道。只不过我们非常不习惯,在 class 里面看到这种没有 self 的 method。那实际上这个 log_function 它也不是一个 method。你可以认为它是在我们定义这个类的时候,里面的一个辅助函数,或者是一个辅助的 decorator。什么意思呢?

当我们尝试在这个类的定义之外,使用 d.log_function 的时候,它会报错。因为它会尝试把这个 log_function,当成一个正常的对象方法去解读。

第 8 步 · 在类外通过对象使用8 / 10
main.py
class Decorators:
def log_function(func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper
@log_function
def fib(self, n):
if n <= 1:
return 0
return self.fib(n - 1) + self.fib(n - 2)
d = Decorators()
# d.fib(3)
@d.log_function
def f():
pass
f()
d.fib(3)

当然如果你直接使用 Decorators.log_function,它是可以工作的。因为这个 Decorators.log_function,就是一个正常的函数。

第 9 步 · 在类外通过类使用9 / 10
main.py
class Decorators:
def log_function(func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper
@log_function
def fib(self, n):
if n <= 1:
return 0
return self.fib(n - 1) + self.fib(n - 2)
d = Decorators()
# d.fib(3)
@Decorators.log_function
@d.log_function
def f():
pass
f()

在类内外都能使用的装饰器​

好那最终的问题出现了:有没有一种方式,可以把我这个装饰器封装到类里,在这个类里可以正常使用,而在这个类之外,无论是通过对象调用,还是通过类调用,都可以正常使用呢?答案是有的。

你只需要在类定义的最后,加上这行代码。如果你看到过我之前,讲装饰器的文章的话,你就会意识到,这行代码的本质,和在 log_function 的定义上加一个 @staticmethod 这个装饰器是一样的。它们两个是等价的。只不过用装饰器的语法,你只能在定义它的同时装饰它。而当我们使用它的等价语法的时候,我们可以先定义它成一个正常的函数,然后在 class 的 definition 里面,把它当成一个正常的装饰器去使用,最后再通过 staticmethod,把这个 log_function,变成一个 static 的 method。

第 10 步 · 在类定义最后转成静态方法10 / 10
main.py
class Decorators:
def log_function(func):
def wrapper(*args, **kwargs):
print(f"function start!")
print(f"args: {args}")
ret = func(*args, **kwargs)
print(f"function end!")
return ret
return wrapper
@log_function
def fib(self, n):
if n <= 1:
return 0
return self.fib(n - 1) + self.fib(n - 2)
log_function = staticmethod(log_function)
d = Decorators()
# d.fib(3)
@Decorators.log_function
def f():
pass
@d.log_function
def g():
pass
f()
g()

我们看在增加了这行神奇的代码之后,无论我们是使用 Decorators.log_function,还是使用它的对象 d.log_function,都能正确的执行这个装饰器的功能了。

那实际上这个所谓技巧的全部原理,我在之前的文章中其实都提到了。只不过大家对这些原理的掌握,可能还不足够熟悉。所以闲来无事,还是可以翻翻我之前的文章。

好那这篇文章就到这里。希望大家有所帮助。