类里的装饰器
用装饰器打印函数调用
这篇文章我们来讲一个跟装饰器也就是 decorator 有关的用法。我们看这里我们写了一个 decorator log_function,这个 decorator 也很简单,就是在运行一个函数之前先打印一下函数开始了,再打印一下这个函数的 argument,在运行函数之后呢打一个这个函数结束了。那我们在下面写了一个简单的斐波那契函数。我们把这个斐波那契函数加上我们的装饰器看一下结果。
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 retreturn wrapper@log_functiondef fib(n):if n <= 1:return 0return fib(n - 1) + fib(n - 2)fib(3)
可以看到啊虽然这个输出呢用处似乎不是那么大,但是它确实是工作了的。如果你细细的分析的话,也是能感受到里面帮助你理解递归的一些信息。
为什么要把装饰器放进类里
然而装饰器的功能并不是我们今天的重点。我们在写装饰器的时候,经常会把这个装饰器函数放到一个文件的全局的命名空间里,也就是把这个函数定义在这个文件的最外层。然而我们知道并不是所有的函数都适合放到全局空间的。那有的时候我就是想把我的装饰器放到一个类里面,我希望把它抽象,或者说我有若干个功能相似的装饰器,我就是希望把它们归类到同一个类里面。那到这里的时候很多人就不知道该怎么办了啊。所以我们这篇文章的重点是讲如何把一个装饰器放到类里面。
装饰器作为对象方法
好我们来看我们左边的程序,这是我们最容易想到的一种实现。把装饰器挪到类里面,本质上它可能就相当于把一个函数变成类的方法,对不对?那我们就按照一般的方式把这个函数加上一个 self argument,然后直接把它扔到类里面就可以了。那当你使用这种方法的时候,你首先要意识到一个类的方法必须由这个类的对象所调用。所以当我们想使用这个装饰器的时候,我们首先要建立一个 Decorators 这个 class 的 object,也就是第 11 行这里的 d 等于 Decorators。那接下来我们就可以用这个 d.log_function 来装饰这个斐波那契函数。我们可以看到这种使用方式,最后得到的结果跟之前是一样的。
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 retreturn wrapperd = 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 retreturn wrapper@d.log_function@log_functiondef fib(n):if n <= 1:return 0return fib(n - 1) + fib(n - 2)fib(3)
那这么用有什么问题呢?它确实是有。首先非常明显的,我们为了使用这个装饰器,迫不得已的要建立一个新的对象,这个事情我非常不爽。其次我这个装饰器里面莫名其妙出现了一个 self argument,然而我这个装饰器压根就没有用到它,对吧?
装饰器作为类方法
好那解决第一个问题,我们知道有一种方法,可以把一个类里面定义的 method 变成 classmethod,也就是把这个方法从一个对象才能调用的方法,变成一个类就可以调用的方法。这里呢我们可以使用这个 classmethod 装饰器。我们看在使用了 classmethod 装饰器之后,这个 log_function 就变成了一个类方法。那在第 12 行的时候我们就不需要再新建一个对象才能使用这个装饰器了。我们可以直接用这个 Decorators 类的 log_function 来调用这个装饰器。那现在的状态呢确实是比之前要稍微好一些,但是这个 classmethod 它第一个 argument 依然是 class,我们还是没有用到这个变量对吧?
class Decorators:@classmethoddef 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 retreturn wrapper@Decorators.log_functiond = Decorators()@d.log_functiondef fib(n):if n <= 1:return 0return 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,已经跟刚才我们写的一模一样了。那当你仅仅需要把一个装饰器封装到一个类里面的时候,这就是一个非常不错的方式。
class Decorators:@staticmethoddef log_function(func):@classmethoddef 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 retreturn wrapper@Decorators.log_functiondef fib(n):if n <= 1:return 0return fib(n - 1) + fib(n - 2)fib(3)
用类里的装饰器装饰方法
然而这还没完。接下来我们要讲一个很多人都遇到过然后放弃了的问题,就是如果我想用我类里面定义的属于这个类的装饰器,去装饰我这个类里面的方法,我应该怎么做?我们看我把这个 fib 函数给挪到了这个 Decorators 类里。这其实是我们非常常见的一种可能性。就是我一个类里面的很多方法,我都希望用装饰器帮它装饰一下,但是我这个装饰器只会在这个类里面用,我又想把这个装饰器给封装到这个类里来。很多人这时候就不知道该怎么办了。
如果像我们刚才讲的使用 staticmethod,你会发现它直接给你报错了。
class Decorators:@staticmethoddef log_function(func):def wrapper(*args, **kwargs):print(f"function start!")print(f"args: {args}")ret = func(*args, **kwargs)print(f"function end!")return retreturn wrapper@log_functiondef fib(self, n):if n <= 1:return 0return self.fib(n - 1) + self.fib(n - 2)@Decorators.log_functiondef fib(n):if n <= 1:return 0return fib(n - 1) + fib(n - 2)d = Decorators()d.fib(3)fib(3)
这一步的输出是用 Python 3.9 录的。从 Python 3.10 开始,staticmethod 对象本身可以直接调用,所以在类里用它装饰 fib 也能正常工作,不会报错。
那有的人可能说我直接把它当成一个正常的类的 method 呢,当然依然是不对的。
class Decorators:def log_function(self, func):@staticmethoddef log_function(func):def wrapper(*args, **kwargs):print(f"function start!")print(f"args: {args}")ret = func(*args, **kwargs)print(f"function end!")return retreturn wrapper@log_functiondef fib(self, n):if n <= 1:return 0return self.fib(n - 1) + self.fib(n - 2)d = Decorators()d.fib(3)
那在这里告诉大家,这种情况下正确的用法是:直接把 log_function 这个函数原封不动的放到这个 Decorators class 里,不需要给它加任何的装饰器。好多人看到这可能就懵了对吧,说这是什么东西?我从来没有在 Python 里见到过长成这样的类的方法。然而如果你运行一下这段程序的话,你会发现它是正常工作的。
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 retreturn wrapper@log_functiondef fib(self, n):if n <= 1:return 0return 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,当成一个正常的对象方法去解读。
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 retreturn wrapper@log_functiondef fib(self, n):if n <= 1:return 0return self.fib(n - 1) + self.fib(n - 2)d = Decorators()# d.fib(3)@d.log_functiondef f():passf()d.fib(3)
当然如果你直接使用 Decorators.log_function,它是可以工作的。因为这个 Decorators.log_function,就是一个正常的函数。
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 retreturn wrapper@log_functiondef fib(self, n):if n <= 1:return 0return self.fib(n - 1) + self.fib(n - 2)d = Decorators()# d.fib(3)@Decorators.log_function@d.log_functiondef f():passf()
在类内外都能使用的装饰器
好那最终的问题出现了:有没有一种方式,可以把我这个装饰器封装到类里,在这个类里可以正常使用,而在这个类之外,无论是通过对象调用,还是通过类调用,都可以正常使用呢?答案是有的。
你只需要在类定义的最后,加上这行代码。如果你看到过我之前,讲装饰器的文章的话,你就会意识到,这行代码的本质,和在 log_function 的定义上加一个 @staticmethod 这个装饰器是一样的。它们两个是等价的。只不过用装饰器的语法,你只能在定义它的同时装饰它。而当我们使用它的等价语法的时候,我们可以先定义它成一个正常的函数,然后在 class 的 definition 里面,把它当成一个正常的装饰器去使用,最后再通过 staticmethod,把这个 log_function,变成一个 static 的 method。
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 retreturn wrapper@log_functiondef fib(self, n):if n <= 1:return 0return self.fib(n - 1) + self.fib(n - 2)log_function = staticmethod(log_function)d = Decorators()# d.fib(3)@Decorators.log_functiondef f():pass@d.log_functiondef g():passf()g()
我们看在增加了这行神奇的代码之后,无论我们是使用 Decorators.log_function,还是使用它的对象 d.log_function,都能正确的执行这个装饰器的功能了。
那实际上这个所谓技巧的全部原理,我在之前的文章中其实都提到了。只不过大家对这些原理的掌握,可能还不足够熟悉。所以闲来无事,还是可以翻翻我之前的文章。
好那这篇文章就到这里。希望大家有所帮助。