目录
我突然想到这个问题,是因为在研究基于类的一类装饰器时,觉得它们的接口有些不自由。
像下面这样基于实例替换原理的装饰器:
from functools import update_wrapper
import time
class Delay:
def __init__(self, func, *, times: int=1) -> None:
self.func = func
self.times = times
update_wrapper(self, func)
def __call__(self, *args, **kwargs):
print(f'休息{self.times}秒先')
time.sleep(self.times)
return self.func(*args, **kwargs)
它的作用是在调用函数之前停止几秒,比如:
@Delay
def hi(name: str) -> None:
print(f'你好呀,{name}!')
hi('奶龙')
但是,这样的调用方式只能使用默认值。(๑•́ ₃ •̀๑)
我在想,要是它能够像下面这样支持3种调用方式该多好啊:
@Delay
def hi(name: str) -> None:
print(f'你好呀,{name}!')
@Delay(times=2)
def hello(name: str) -> None:
print(f'你好呀,{name}!')
@Delay()
def nihao(name: str) -> None:
print(f'你好呀,{name}!')
hi('奶龙')
hello('贝利亚')
nihao('毒液')
一. 经典方案的局限性
对这类装饰器实现可选参数,比较经典和简单的方案是使用partial:
from functools import update_wrapper, partial
import time
class Delay:
def __init__(self, func, *, times: int=1) -> None:
self.func = func
self.times = times
update_wrapper(self, func)
def __call__(self, *args, **kwargs):
print(f'休息{self.times}秒先')
time.sleep(self.times)
return self.func(*args, **kwargs)
def delay(*, times: int=1): #(1)
return partial(Delay, times=times)
@delay(times=2) #(2)
def hi(name: str) -> None:
print(f'你好呀,{name}!')
hi('奶龙')
(1):新定义的delay把接收到的times先传给Delay类,返回一个偏类。
(2):在需要传入参数时,使用偏类工厂函数delay而不是Delay。
这样做虽然简单又好用,但是满足不了我“3种接口一次满足”的愿望——毕竟都换人了(。ŏ_ŏ)
而且,装饰器有几个可选关键字参数,这个偏类工厂函数就必须也定义几个参数,扩展性不够强。
二. 预备元类方案
1. 元类方案的可行性
我分析了自己的需求,直接使用@Delay其实是在使用
func = Delay(func)。
而如果我想让@Delay(times=2)这种写法成功,就是想让
func = Delay(times=2)(func)
这种调用合法。
那么,就必须让Delay(times=2)这种本该创建实例的语句改为创建类,且让类中__init__方法的times参数得到相应修改。但是,像Delay(func)这样的调用方式仍然需要创建实例,这就显得很复杂。
想要这么大幅度地干涉实例创建,我感觉这一任务非元类莫属了——重写元类的__call__方法,让它干涉实例的创建过程。判断装饰器的使用方式,并决定是应该创建实例还是创建新类。
在看这个元类的实现方法之前,我们先说一说预备知识。
2. 函数的__kwdefaults__属性
函数的仅限关键字参数默认值存储在它的属性__kwdefaults__中,这个属性是一个字典:
def f(*, x=1, y=2):pass
print(f.__kwdefaults__)
如果我想让元类根据传入的值修改__init__方法的参数,就需要操作Delay.__init__.__kwdefaults__属性。
三. 使用元类
下面我们先看效果,通过指定这个元类,可以让装饰器实现3种接口一次满足,无论它有几个可选关键字参数:
# 使用元类MetaDecorator
class Delay(metaclass=MetaDecorator):
def __init__(self, func, *, times: int=1) -> None:
self.func = func
self.times = times
update_wrapper(self, func)
def __call__(self, *args, **kwargs):
print(f'休息{self.times}秒先')
time.sleep(self.times)
return self.func(*args, **kwargs)
# 方式一:不带括号,使用默认值
@Delay
def hi(name: str) -> None:
print(f'你好呀,{name}!')
# 方式二:传入参数
@Delay(times=2)
def hello(name: str) -> None:
print(f'你好呀,{name}!')
# 方式三:带括号,但是使用默认值
@Delay()
def nihao(name: str) -> None:
print(f'你好呀,{name}!')
hi('奶龙')
hello('贝利亚')
nihao('毒液')
现在我们看元类的代码:
class MetaDecorator(type):
"""
一个元类,用在实例替换型装饰器上时,\n
可以让它灵活地支持带参数和不带参数两种使用方式。\n
装饰器的其它参数必须全是可选关键字参数。
"""
def __call__(cls, func=None, **kwargs): #(1)
if func is not None: #(2)
return super().__call__(func, **kwargs)
else: #(3)
class NewDecorator(cls): #(4)
def __init__(self, func):
merged_kwargs = {
**getattr(cls.__init__, '__kwdefaults__', {}),
**kwargs
} #(5)
super().__init__(func, **merged_kwargs) #(6)
return NewDecorator
(1):在元类中重写__call__方法,这个方法干涉实例的创建。第一个参数是元类的实例——类cls;其次有一个位置参数留给func,默认值是None;然后,接受任意关键字参数,绑定像times=2这样的所有传入值。
(2):如果func不是None这个默认值,说明在创建实例时传入了函数。也就是有func = Delay(func)这样的操作,这说明装饰器是像@Delay这样用的。这时直接委托超类type的__call__方法创建实例并返回。
(3):否则,说明是在采用@Delay(times=2)这样的用法,应该让Delay(times=2)返回一个参数被修改过后的新类。
(4):创建一个新类NewDecorator,继承cls,并改写它的__init__方法来更改参数。
(5):这个“融合字典”将cls.__init__.__kwdefaults__和传入的参数kwargs融合,如果有相同的键,后者的值会覆盖前者的值,达到修改__init__的参数的目的。
(6):委托超类cls的__init__方法,传入的关键字参数来自“融合字典”。最后,返回这个动态创建的类。
有个小细节需要注意:我选择动态创建新类而不是直接修改cls的属性,因为这种属性修改是永久的。假设我们使用过@Delay(times=2),那么从此每当使用@Delay,times的默认值就会变成2,而不是最初的1。为了避免这种情况,我选择动态创建一个新类。
好了,本文的讨论到此为止。
最后呢,放一下完整的代码清单吧。本人实力不强,如果你检查出这个元类有Bug,请不吝赐教!这样鄙人也能学到新东西(* ̄▽ ̄)~*
四. 完整代码清单
from functools import update_wrapper
import time
class MetaDecorator(type):
"""
一个元类,用在实例替换型装饰器上时,\n
可以让它灵活地支持带参数和不带参数两种使用方式。\n
装饰器的其它参数必须全是可选关键字参数。
"""
def __call__(cls, func=None, **kwargs):
# 若func不是None,说明是@Decorator的使用方式
if func is not None:
return super().__call__(func, **kwargs)
# 否则是@Decorator(某些关键字参数)的使用方式
else:
class NewDecorator(cls):
def __init__(self, func):
merged_kwargs = {
**getattr(cls.__init__, '__kwdefaults__', {}),
**kwargs
}
super().__init__(func, **merged_kwargs)
return NewDecorator
class Delay(metaclass=MetaDecorator):
def __init__(self, func, *, times: int=1) -> None:
self.func = func
self.times = times
update_wrapper(self, func)
def __call__(self, *args, **kwargs):
print(f'休息{self.times}秒先')
time.sleep(self.times)
return self.func(*args, **kwargs)
@Delay
def hi(name: str) -> None:
print(f'你好呀,{name}!')
@Delay(times=2)
def hello(name: str) -> None:
print(f'你好呀,{name}!')
@Delay()
def nihao(name: str) -> None:
print(f'你好呀,{name}!')
hi('奶龙')
hello('贝利亚')
nihao('毒液')