深入Python装饰器
目录
在之前的文章 入门Python装饰器 中,我们了解了函数是Python的第一类对象,因此也可以传递并当作参数,而装饰器是一个函数,它接受另一个函数并扩展这个函数的行为而不显式修改它。与此同时,也看到了如何创建一个简单的装饰器。
在本文中,我们将探索装饰器更高级的特性,包括如何使用以下特性:
- 类的装饰器 Decorators on classes
- 应用多个装饰器 Several decorators on one function
- 带参数的装饰器 Decorators with arguments
- 带可选参数的装饰器 Decorators that can optionally take arguments
- 有状态的装饰器 Stateful decorators
- 作为装饰器的类 Classes as decorators
类的装饰器
在类中使用装饰器有两种不同的方式。第一个非常接近于之前完成的函数:你可以装饰类的方法。这是引入装饰器的动机之一。
装饰类的方法
内置装饰器
Python中内置的一些常用的装饰器是 @classmethod
、@staticmethod
和@property
。
@classmethod
和@staticmethod
装饰器用于在类命名空间中定义的方法,但这些方法不连接到该类的特定实例之中。@property
装饰器用于为类属性定制getter
和setter
。
下面的Cicle类定义使用@classmethod
、@staticmethod
和@property
装饰器:
1 | class Circle: |
在这个类之中:
.cylinder_volume()
是常规方法.radius
是一个可变属性:可以将其设置为不同的值。通过定义setter方法,我们可以进行一些错误测试,以确保不会将其设置为无意义的负数。Properties 作为属性访问,不使用括号。.area
是一个不可变的属性:没有.setter()
方法的属性不能更改。即使它被定义为一个方法,它也可以被检索为一个没有括号的属性。.unit_circle()
是一个类方法。它不局限于一个特定的Circle实例。类方法通常用作工厂方法,可以创建类的特定实例。.pi()
是一个静态方法。它实际上并不依赖于Circle类,只是它是其名称空间的一部分。静态方法可以在实例或类上调用。
例如,Circle类可以这样使用:
1 | 5) c = Circle( |
自定义类的装饰器
让我们定义一个类,在这个类中,我们使用前面的@debug
和@timer
装饰器装饰它的一些方法:
1 | from decorators import debug, timer |
使用这个类,可以看到装饰器的效果:
1 | 1000) tw = TimeWaster( |
装饰整个类
在类上使用装饰器的另一种方法是装饰整个类。例如,这是在Python 3.7中的新dataclasses模块中完成的:
1 | from dataclasses import dataclass |
语法的含义类似于函数装饰器。在上面的例子中,可以通过编写PlayingCard = dataclass(PlayingCard)
来完成装饰。
类装饰器的一个常见用法是作为一些元类用例的更简单的替代。在这两种情况下,都可以动态地更改类的定义。
编写类装饰器与编写函数装饰器非常相似。唯一的区别是装饰器将接收一个类作为参数,而不是函数。事实上,上面看到的所有装饰器都可以作为类的装饰器工作。不过,当在类上使用装饰器而不是函数上使用它们时,它们的效果可能不是预想的那样。在下面的示例中,@timer
装饰器应用于一个类:
1 | from decorators import timer |
装饰类并不装饰它的方法。回想一下@timer
仅仅是TimeWaster = timer(TimeWaster)
的缩写。
在这里,@timer
只度量实例化类所需的时间:
1 | 1000) tw = TimeWaster( |
之后,我们将看到一个合适的定义类的装饰器的示例,即@singleton
,它确保一个类只有一个实例。
嵌套装饰器
可以将多个装饰器堆叠在一起,来应用到一个函数上:
1 | from decorators import debug, do_twice |
可以将其视为按顺序执行的装饰器。换句话说,@debug
调用@do_twice
,@do_twice
调用greet()
,或者可以理解为 debug(do_twice(greet()))
:
1 | "Eva") greet( |
更改@debug
和@do_twice
的顺序,观察差异:
1 | from decorators import debug, do_twice |
在这种情况下,@do_twice
也会被应用到@debug
上:
1 | "Eva") greet( |
带参数的装饰器
有时候,向装饰器传递参数是很有用的。例如,@do_twice
可以扩展为@repeat(num_times)
装饰器。然后,可以将执行被装饰函数的次数作为参数给出。
你可以这样做:
1 |
|
1 | "World") greet( |
考虑一下如何做到
1 | def repeat(num_times): |
通常情况下,装饰器会创建并返回一个内部包装器函数,因此完整地编写示例将会在内部函数中提供一个内部函数。
1 | def repeat(num_times): |
它看起来有点乱,但是我们只在一个额外的def
中添加了相同的装饰器模式,这些已经见过很多次了,这个def
可以处理装饰器的参数。让我们从最里面的函数开始:
1 | def wrapper_repeat(*args, **kwargs): |
这个wrapper_repeat()
函数接受任意参数并返回被装饰函数func()
的值。这个包装器函数还包含调用被装饰函数num_times
次循环。这与之前看到的包装器函数没有什么不同,只是它使用了必须从外部提供的num_times
参数。
接下来,便是装饰器函数:
1 | def decorator_repeat(func): |
同样,decorator_repeat()
与前面编写的装饰器函数完全相同,只是命名方式不同。这是因为我们为最外层的函数保留了基名repeat()
,这是用户将要调用的函数。
我们会发现,最外层的函数返回对装饰器函数的引用:
1 | def repeat(num_times): |
repeat()
函数有一些东西需要我们注意:
- 将
decorator_repeat()
定义为一个内部函数意味着repeat()
将引用一个函数对象——decorator_repeat
。之前,我们使用没有括号的repeat
引用函数对象。在定义带有参数的修饰符时,则需要添加括号。 num_times
参数似乎没有在repeat()
本身中使用。但是通过传递num_times
,可以创建一个闭包,其中存储num_times
的值,直到wrapper_repeat()
稍后使用它为止。
测试一下我们的装饰器函数@repeat(num_times=4)
1 |
|
1 | "World") greet( |
这正是我们想要的结果。
带可选参数的装饰器
我们还可以定义既可以使用参数,也可以不使用参数的装饰器。很可能,我们并不需要这样做,但是具有灵活性总归是很好的。
正如在前一节中看到的,当装饰器使用参数时,需要添加一个额外的外部函数。难点在于,我们编写代码要弄清楚装饰器是否被调用了,是否有参数。
由于装饰函数仅在没有参数的情况下调用装饰器时才直接传递,因此该函数必须是可选参数。这意味着装饰器参数必须全部由关键字指定。也可以用特殊的*
语法来加强这一点,这意味着以下所有的参数都是只有关键字的:
1 | def name(_func=None, *, kw1=val1, kw2=val2, ...): # 1 |
在这里,_func
参数充当标记,注意调用装饰器时是否带有参数:
- 如果
name
调用时没有参数,装饰后的函数将作为_func
传入。如果调用name
时带有参数,那么_func
将为None
,并且一些关键字参数可能已经从它们的默认值更改了。参数列表中的*
表示剩余的参数,并且不是位置参数。 - 这个例子中,带参数调用装饰器的情况下,返回一个装饰器函数,该函数可以读取并返回一个函数。
- 没有带参数调用装饰器,则立即将装饰器应用到函数上。
若想在在上一节的@repeat
装饰器中使用这个样板,可以编写以下代码:
1 | def repeat(_func=None, *, num_times=2): |
与原始的@repeat
进行比较。唯一的变化是添加了_func
参数以及在末尾添加了if-else
。
Python Cookbook一书中9.6节 带可选参数的装饰器 展示了一种替代方法,使用functools.partial()
这些例子表明,@repeat
现在可以在有或没有参数的情况下使用:
1 |
|
回想一下num_times的默认值是2:
1 | say_whee() |
有状态的装饰器
有时候,有一个可以跟踪状态的装饰器是很有用的。作为一个简单的例子,我们将创建一个装饰器,统计调用函数的次数。
在入门Python装饰器的开头,我们简单讨论了基于给定参数返回值的纯函数。有状态装饰器正好相反,返回值将取决于当前状态以及给定的参数。
在下一节中,将看到如何使用类来保持状态。但在简单的情况下,您也可以使用函数属性:
1 | import functools |
状态(调用函数的次数)存储在包装器函数的函数属性.num_calls
中。下面是使用它的效果:
1 | say_whee() |
作为装饰器的类
维护状态的典型方法是使用类。在本节中,您将看到如何使用类作为装饰器重写前一节中的@count_calls
示例。
回想一下,装饰器语法@my_decorator
只是表示func = my_decorator(func)
的一种更简单的方法。因此,如果my_decorator
是一个类,那么它需要在.__init__()
方法中将func
作为参数。此外,该类需要可调用,以便它能够代替已装饰过的函数。
要使类可调用,需要实现特殊的.__call__()
方法:
1 | class Counter: |
.__call__()
方法在每次尝试调用类的实例时执行:
1 | counter = Counter() |
因此,一个装饰器类的典型实现,需要实现.__init__()
和.__call__()
:
1 | import functools |
.__init__()
方法必须存储对函数的引用,并且可以执行任何其他必要的初始化。.__call__()
方法被调用而不是已装饰函数。它做的事情与我们前面示例中的wrapper()
函数基本相同。注意,应该需要使用functools.update_wrapper()
函数,而不是@functools.wrapper
。
这个@CountCalls
装饰器的工作原理与前一节相同:
1 | say_whee() |