装饰器,顾名思义指的是通过添加一些额外的功能,起到能丰富原有对象的作用。如果我们已经构造好对象也实现了一些功能,如果需要对这些对象新增某一功能,逐一在原有对象的基础上添加这个功能,虽然依然能完成丰富这个对象的作用,但是略显冗余。
好在Python 中的函数和 Java、C++不太一样,Python 中的函数可以像普通变量一样当做参数传递给另外一个函数(其实是Python中def|lambda函数|class本质均为变量,详情可以参考:知乎 https://www.zhihu.com/question/57470958 “冒泡”关于函数的讨论)。装饰器本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。这种“补丁式”编程很适用于有切面需求的场景。
本文将装饰器实例化为对“我的住宅”这一函数进行装饰。
实例化的被装饰函数——我的住宅:
def my_house():
print("This is my house!")
添置冰箱对我的住宅进行装饰:
def add_fridge(func):
def wrapper():
print("add fridge!")
return func()
return wrapper
my_house = add_fridge(my_house)
my_house()
输出:
add fridge!
This is my house!
add_fridge 既是一个装饰器,也是一个普通的函数。它把执行真正业务逻辑的函数 func 包裹在其中,看起来像my_house 被add _fridge 装饰了一样;add_fridge 返回的也是一个函数,这个函数的名字叫 wrapper。在这个例子中,函数进入和退出这一套流程 ,被称为一个横切面,这种编程方式被称为面向切面的编程。
写到这里,肯定有人疑惑,完全可以用在原函数上新增功能(形式一)或者调用新的功能函数(形式二)实现,形如:
形式一:
def my_house():
print("add fridge!")
print("This is my house!")
形式二:
def my_house():
print("This is my house!")
def add_fridge(func):
print("add fridge!")
func()
add_fridge(my_house)
在这个简单的场景中,使用这两种形式确实能实现我们想要的装饰效果。但是如果增加了几个形如your_house、his_house的对象呢,形式一需要在每一个对象中增加装饰的内容;形式二中调用的时候不再是调用真正的业务逻辑 my_house 函数,而是换成了 add_fridge 函数,这就破坏了原有的代码结构, 现在我们不得不每次都要把原来的那个 my_house 函数作为参数传递给 add_fridge 函数。
如果同时需要对多个函数进行装饰,使用赋值方式的装饰依然显得冗余。好在Python有语法糖,在Python中@ 符号就是装饰器的语法糖,它放在函数开始定义的地方,这样就可以省略最后一步再次赋值的操作。
使用语法糖后对 my_house 装饰就显得比较简洁了:
def add_fridge(func):
def wrapper():
print("add fridge!")
return func()
return wrapper
@add_fridge
def my_house():
print("This is my house!")
my_house()
具体业务场景中,被装饰的函数如果带有不定参数,理论上需要对函数进行重载,装饰器相应的也需要修改,但是由于Python中可以传入不定参数(所以Python中没有重载这种说法),实现变参量的函数也比较容易。
例如:
my_house本来装配有50寸、黑色TV,可以通过如下实现:
def add_fridge(func):
def wrapper(*args, **kwargs):
print("add fridge!")
return func(*args, **kwargs)
return wrapper
@add_fridge
def my_house(*args, **kwargs):
for item in args:
print("There exist " + item)
for key, value in kwargs.items():
print(key, "is", value, "inch")
print("This is my house!")
my_house("TV", size=50, corlor="black")
输出:
add fridge!
There exist TV
size is 50 inch
corlor is black
This is my house!
不论被修饰函数带多少个参数,装饰器都能完美实现装饰作用。
上一小节介绍了被装饰函数能带不定量参数,其实装饰器的语法允许我们在调用装饰器时提供其它参数;这样为装饰器的编写和使用提供了更大的灵活性。比如我们在用添置冰箱的时候需要指定冰箱的规格,诸如“单开门”、“双开门”、“三开门”,下面是是具体实现:
def add_fridge(standard):
def decorator(func):
def wrapper(*args, **kwargs):
print("add", standard, "fridge!")
return func(*args, **kwargs)
return wrapper
return decorator
@add_fridge(standard="Three-door")
def my_house(*args, **kwargs):
for item in args:
print("There exist " + item)
for key, value in kwargs.items():
print(key, "is", value)
print("This is my house!")
my_house("TV", size=50, corlor="black")
输出:
add Three-door fridge!
There exist TV
size is 50
corlor is black
This is my house!
函数使用的装饰器除了可以添加参数,一个函数还可以同时定义多个装饰器,比如我还想给我的住宅添置一台空调和一台洗衣机,实现如下:
def add_fridge(func):
def wrapper():
print("add fridge!")
return func()
return wrapper
def add_airCondition(func):
def wrapper():
print("add air condition!")
return func()
return wrapper
def add_washer(func):
def wrapper():
print("add washer!")
return func()
return wrapper
@add_fridge
@add_airCondition
@add_washer
def my_house():
print("This is my house!")
my_house()
输出:
add fridge!
add air condition!
add washer!
This is my house!
多个装饰器的调用顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,等效于:
my_house = add_fridge(add_airCondition(add_washer(my_house)))
上文中介绍的装饰器是函数,但是文章开始的时候说了装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。
使用类装饰器主要依靠类内部的 __call__方法,也就是说把类的构造函数当成了一个装饰器,它接受一个函数作为参数,并返回了一个对象,而由于对象实现了 call 方法,因此返回的对象相当于返回了一个函数。
例如,下例构造了添置设备的类AddFacilities,用类构造装饰器:
class AddFacilities(object):
def __init__(self, func):
self._func = func
def __call__(self, *args, **kwargs):
result = self._func(*args, **kwargs)
print("add some facilities!")
return result
@AddFacilities
def my_house():
print("This is my house!")
my_house()
输出:
This is my house!
add some facilities!
在正式认识装饰器之前,其实我们应该都接触过装饰器,例如使用property类做装饰器可以把类方法变成属性,可以通过类直接调用;staticmethod和classmethod类做装饰器能起到修饰类方法的作用,这种装饰方法在面向对象编程中比较常见,使用也比较方便。在这里用电视机作为对象,重点介绍一下@property装饰器的用法。
class TVset(object):
def __init__(self, color, size):
self.color = color
self.__size = size
def get_size(self):
return self.__size
def set_size(self, size):
if size < 0 or size > 120:
raise ValueError('invalid size')
self.__size = size
tv = TVset("black",50)
tv.set_size(130)
print(tv.get_size())
输出:
Traceback (most recent call last):
File "/Users/lwb/PycharmProjects/etd_final/src/ticket/__init__.py", line 14, in
tv.set_size(130)
File "/Users/lwb/PycharmProjects/etd_final/src/ticket/__init__.py", line 11, in set_size
raise ValueError('invalid size')
ValueError: invalid size
在上面TVset对象中,定义了color和__size两种属性,并且使用 get/set 方法来封装对__size属性的访问(在许多面向对象编程的语言中都很常见)。但是如果我想迎合python语法简洁的特点,在不改变对象逻辑的前提下直接访问/修改对象的属性,类装饰器@property能完美实现。实现方法如下:
class TVset(object):
def __init__(self, color, size):
self.color = color
self.__size = size
@property
def size(self):
return self.__size
@size.setter
def size(self, size):
if size < 0 or size > 120:
raise ValueError('invalid size')
self.__size = size
tv = TVset("black", 40)
tv.size = 50
print(tv.size)
输出:
50
get/set方法用property类装饰后,即可像使用属性一样设置TVset对象的__size属性。