理解Python中的装饰器
装饰器是Python中的一个重要概念,多用于在不修改原函数的基础上,为函数增加额外的功能。
基础装饰器
例如小李给女朋友买了一款iPhone12作为生日礼物,手机原封未拆封。
def gift():
print('iPhone12')
gift() # 运行显示礼物信息
但还是觉得礼物太单薄,于是又买了一盒德芙巧克力,一支dior的口红,并找了个精美的礼品盒包装了一下,盒子里放满了泡沫球。
def gift():
print('iPhone12')
def box(something):
print('='*5 + '礼物盒' + '='*5)
print('一盒泡沫球')
print('好多巧克力')
print('一支dior口红')
return something
gift = box(gift) # 将礼物包装后作为礼物
gift() # 显示礼物信息
运行后显示如下:
=====礼物盒=====
一盒泡沫球
好多巧克力
一支dior口红
iPhone12
这个box便是一个装饰器,它的参数是一个函数对象,同数字、字符串、列表、字典等数据类型一样,函数和类也可以作为函数的参数使用,毕竟在Python里人人平等,一切皆对象。
box在使用时依然返回了原来的gift,只是在拿到这个gift之前增加了两个额外的惊喜,然后我们把box作为gift使用即可。
装饰器本质上就是以函数作为参数,对函数做一些处理,并替换原函数的一种高阶函数。
上例中,使用装饰器表示为如下。
def box(something): # 以函数为参数的装饰器
print('='*5 + '礼物盒' + '='*5)
print('一盒泡沫球')
print('好多巧克力')
print('一支dior口红')
return something
@box # 挂载装饰器,会自动替换原函数
def gift():
print('iPhone12')
gift() # 这里面得到的gift实际上是装饰后的gift即`box(gift)`
运行后显示和上例相同。
处理函数参数
小李突然想到,买哪个颜色应该征询下女友的意见,也就是原来的gift应支持一个可供选择的颜色参数。
def gift(color):
print(f'iPhone12{color}版')
作为一个细心的boyfriend,小李需要根据对应的手机颜色选择同样颜色的泡沫球,也就是需要能获取到,被装饰的gift函数的参数。
这时候我们需要在盒子内部(box装饰器),重新准备一个新的礼物,根据颜色参数做不同的处理,然后根据颜色拿到指定的iPhone12礼物。
由于原来的装饰器box返回原gift函数,我们不能修改原函数的内容,因此无法对函数的参数做出相应调整。
这时候我们可以使用“狸猫换太子”,使用一个相似的函数new_gift
替换原有函数,原则如下:
- 替换的函数对象
new_gift
要与原函数gift
使用方式一致(参数一致) - 替换的函数对象
new_gift
要拥有原函数gift
的功能(可以在new_gift
中直接调用原函数gift
实现) - 装饰器由返回gift对象改为返回new_gift对象
大概方式如下:
def new_gift(color): # 参数和原gift参数一致
print(f'一盒{color}泡沫球') # 根据颜色准备泡沫球
print('好多巧克力')
print('一支dior口红')
return gift(color) # 调用原函数包含原函数功能 # Fixme gift在下面定义,在全局作用域下拿不到gift
def box(something):
print('='*5 + '礼物盒' + '='*5)
return new_gift # 替换为新函数,因为原函数不能修改,新函数可以自定义并包含原函数功能
@box
def gift(color):
print(f'iPhone12{color}版')
gift('红色')
由于new_gift
需要使用gift,所以gift必须定义在上面,并且不能使用@box进行装饰(因为box还没定义,box需要使用new_gift所以只能在下面定义,并且装饰@box后gift就变成了new_gift)。
注:
gift
依赖box是指使用@box
装饰gift必须先有box的定义
如何解决这个问题呢?
问题的核心是new_gift
如何拿到原函数gift
。我们注意到box其实不依赖gift
,但是可以通过func参数拿到实际的gift对象(@box
即box(gift)
, gift是调研box的实际参数)。
现在的条件可以梳理为:
- 装饰gift需要先定义box
- box可以拿到gift,new_gift需要gift,box需要new_gift
由于gift定义前全局(模块)作用域,无法拿到原函数gift, 而box中在调研传递实参后可以拿到gift,因此我们可以将new_gift直接定义在box函数内部。
这样既解决了new_gift需要gift的问题,也解决了box返回时需要new_gift的问题。
这种将一个函数定义到另一个函数内部,以突破作用域限制的用法就叫做闭包。
实现代码如下:
def box(something):
print('='*5 + '礼物盒' + '='*5)
def new_gift(color): # new_gift由于在box内部,可以拿到box中的参数
print(f'一盒{color}泡沫球')
print('好多巧克力')
print('一支dior口红')
return something(color) # func实际就是gift函数
return new_gift # 返回新礼物,新礼物调用时,增加一些惊喜,并返回原有礼物gift(color)的结果。
@box
def gift(color):
print(f'iPhone12{color}版')
gift('红色') # 实际上这里的gift是被box装饰后狸猫换太子的new_gift函数,而new_gift('红色'),返回原gift('红色')的结果。
在box内部为了根据参数做对应处理,我们新建了一个函数,函数内部也可以定义内部函数,内部函数new_gift可以获取并使用外部box函数的参数,如gift。
为了能获取到原有函数gift的参数,我们需要建立一个傀儡函数new_gift,这个函数和原函数gift的参数一致、返回结果一致,即new_gift('红色')返回的就是gift('红色')。
然后狸猫换太子,不再返回原来的gift函数对象,而是返回替换的new_gift函数对象。
运行后显示
=====礼物盒=====
一盒红色泡沫球
好多巧克力
一支dior口红
iPhone12红色版
注意:在装饰器box里,要返回一个函数对象,如上例中的
return gift
或本例中的return new_gift
。而在傀儡函数new_gift中,为了和原函数gift结果一致,要返回原函数的调用结果即gift(color)。
从普遍意义上讲,作为商家,为了装饰器box可以包装任何形式的礼物,无论礼物有什么参数都可以满足,这就要求我们的傀儡函数new_gift支持任意类型的参数即def new_gift(*args, **kwargs)
。
然后把无论什么参数*args, **kwargs
交由原函数gift(*args, **kwargs)
处理即可。
修改后,我们便得到一个通用的装饰器,可以包装任何礼物。
def box(something):
print('='*5 + '礼物盒' + '='*5)
def new_gift(*args, **kwargs): # 接受任意数量的参数
if args and len(args) > 0: # 由于参数不确定了,我们假设万一有参数,第一个参数是color参数
color = args[0]
print(f'一盒{color}泡沫球')
else:
print(f'一盒泡沫球')
print('好多巧克力')
print('一支dior口红')
result = something(*args, **kwargs) # 如果我们需要对原函数的结果做出处理,可以先获取到结果
# print(f'原函数结果{result}') 由于原函数gift没有return,这是其实是None
return result # 返回原函数结果
return new_gift
@box
def gift(color, pro=False): # 新的礼物函数,两个参数,默认买12,万一女友要Pro,也可以
if pro is True:
print(f'iPhone12 Pro{color}版')
else:
print(f'iPhone12{color}版')
gift('海蓝色', pro=True)
这样,无论被装饰的函数有几个参数,box装饰器都可以正常处理。
运行后显示如下。
=====礼物盒=====
一盒海蓝色泡沫球
好多巧克力
一支dior口红
iPhone12 Pro海蓝色版
带参装饰器
信心满满的小李觉得,在盒子上还可以做些文章,要根据女友的喜好选择不同形状的箱子,因此我们需要根据参数来定制我们的装饰器box,在盒子外面再加一层定制函数。
def custom_box(shape): # 根据参数定制装饰器
def box(something): # 装饰器函数
print('='*5 + f'{shape}礼物盒' + '='*5) # 根据形状定制
# ...
return box # 返回装饰器函数
此时我们得到一个可以根据参数进行定制的装饰器函数custom_box,这个装饰器接收到参数后,传递给真实装饰器box,并返回定制后box装饰器函数。
完整代码如下。
def custom_box(shape): # 根据参数定制装饰器 =====================
def box(something): # 实际的装饰器函数 ---------------------------
print('='*5 + f'{shape}礼物盒' + '='*5)
def new_gift(*args, **kwargs): # 傀儡函数 ..............
if args and len(args) > 0:
color = args[0]
print(f'一盒{color}泡沫球')
else:
print(f'一盒泡沫球')
print('好多巧克力')
print('一支dior口红')
result = something(*args, **kwargs)
return result # 返回原函数结果 ......................
return new_gift # 返回傀儡函数 ---------------------------
return box # 返回定制的装饰器 ===============================
@custom_box('心形') # 使用可定制的装饰器
def gift(color, pro=False):
if pro is True:
print(f'iPhone12 Pro{color}版')
else:
print(f'iPhone12{color}版')
gift('海蓝色', pro=True)
注意:装饰器在导入模块时立即计算的,即没调用
gift('海蓝色', pro=True)
之前就已经执行生成定制后的box。
运行后,结果如下。
=====心形礼物盒=====
一盒海蓝色泡沫球
好多巧克力
一支dior口红
iPhone12 Pro海蓝色版