我一直在寻找一个面向对象的设置来制作装饰器工厂。一个简单的版本可以在这个堆栈溢出答案中找到。但我对界面的简单性并不完全满意。
我设想的那种接口将使用我可以这样使用的特殊类Decora
:
class MultResult(Decora):
mult: int = 1
i_am_normal = Literal(True) # will not be included in the __init__
def __call__(self, *args, **kwargs): # code for the wrapped functoin
return super().__call__(*args, **kwargs) * self.mult
这将导致以下行为:
>>> @MultResult(mult=2)
... def f(x, y=0):
... return x + y
...
>>>
>>> signature(MultResult) # The decorator has a proper signature
<Signature (func=None, *, mult: int = 1)>
>>> signature(f) # The decorated has a proper signature
<Signature (x, y=0)>
>>> f(10)
20
>>>
>>> ff = MultResult(lambda x, y=0: x + y) # default mult=1 works
>>> assert ff(10) == 10
>>>
>>> @MultResult # can also use like this
... def fff(x, y=0):
... return x + y
...
>>> assert fff(10) == 10
>>>
>>> MultResult(any_arg=False) # should refuse that arg!
Traceback (most recent call last):
...
TypeError: TypeError: __new__() got unexpected keyword arguments: {'any_arg'}
我们可以从这样的基础开始:
from functools import partial, update_wrapper
class Decorator:
"""A transparent decorator -- to be subclassed"""
def __new__(cls, func=None, **kwargs):
if func is None:
return partial(cls, **kwargs)
else:
self = super().__new__(cls)
self.func = func
for attr_name, attr_val in kwargs.items():
setattr(self, attr_name, attr_val)
return update_wrapper(self, func)
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
它允许人们通过子类化和定义__new__
方法(应该调用其父级(来定义装饰器工厂。
然后,可以使用__init_subclass__
直接从子类的属性中提取该__new__
的所需参数,如下所示:
from inspect import Signature, Parameter
PK, KO = Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY
class Literal:
"""An object to indicate that the value should be considered literally"""
def __init__(self, val):
self.val = val
class Decora(Decorator):
_injected_deco_params = ()
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if '__new__' not in cls.__dict__: # if __new__ hasn't been defined in the subclass...
params = ([Parameter('self', PK), Parameter('func', PK, default=None)])
cls_annots = getattr(cls, '__annotations__', {})
injected_deco_params = set()
for attr_name in (a for a in cls.__dict__ if not a.startswith('__')):
attr_obj = cls.__dict__[attr_name] # get the attribute
if not isinstance(attr_obj, Literal):
setattr(cls, attr_name, attr_obj) # what we would have done anyway...
# ... but also add a parameter to the list of params
params.append(Parameter(attr_name, KO, default=attr_obj,
annotation=cls_annots.get(attr_name, Parameter.empty)))
injected_deco_params.add(attr_name)
else: # it is a Literal, so
setattr(cls, attr_name, attr_obj.val) # just assign the literal value
cls._injected_deco_params = injected_deco_params
def __new__(cls, func=None, **kwargs):
if cls._injected_deco_params and not set(kwargs).issubset(cls._injected_deco_params):
raise TypeError("TypeError: __new__() got unexpected keyword arguments: "
f"{kwargs.keys() - cls._injected_deco_params}")
if func is None:
return partial(cls, **kwargs)
else:
return Decorator.__new__(cls, func, **kwargs)
__new__.__signature__ = Signature(params)
cls.__new__ = __new__
它通过了问题中列出的测试,但我对这种面向对象的魔法不是很有经验,所以很想知道替代解决方案。