用于制作 python 装饰器类的精益接口

Posted

技术标签:

【中文标题】用于制作 python 装饰器类的精益接口【英文标题】:A lean interface for making python decorator classes 【发布时间】:2020-10-05 12:06:03 【问题描述】:

我一直在寻找一个面向对象的设置来制作装饰器工厂。一个简单的版本可以在this *** answer 中找到。但我对界面的简单性并不完全满意。

我设想的接口类型将使用特殊类 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'

【问题讨论】:

我不会投赞成票,因为我认为自己太投入了,但是这个问题,断章取义,感觉太宽泛了(或者在一堆毫无意义的情况下“需要更多关注”措辞更改)“请为我做我的工作”问题。 “请为我做我的工作”相去甚远——我将自己发布一个有效的答案,但想向那里的专家公开,因为可能有一个更优雅的解决方案, 问题是问题中没有真正的问题。 @Blckknght 有一个明确的问题,在我看来,它是“编写一些具有所描述行为的代码”——但我建议在进行任何进一步的审核之前等待预示的自我回答关于这个问题的活动。 如果你问“是否有另一种完全独立的方式来做到这一点”,那么这并不适合 Code Review,但它也不适合 Stack Overflow,因为它会倒退回到我之前在 cmets 中提到的“请为我工作”。 【参考方案1】:

我们可以从这样的基础开始:

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__

它通过了问题中列出的测试,但我对这种面向对象的魔法并不是很有经验,所以很想看到替代解决方案。

【讨论】:

ParameterPKSignature 是什么?此外,鉴于您将此签名附加到 __new__Parameter('self', PK) 看起来很可疑。为什么__new__子类没有声明参数时不抱怨关键字参数? 为什么DecoraDecorator 是两个不同的类? @user2357112supportsMonica:确实,我忘记了导入和别名。对不起。添加。关于DecoratorDecora 的分离,更多的是保持一个简单/轻量级的DecoratorDecora 分开,试图创建一个更小的界面。

以上是关于用于制作 python 装饰器类的精益接口的主要内容,如果未能解决你的问题,请参考以下文章

Python 中的装饰器类

装饰器类学习小结

字符串装饰器类导致大量构建错误

带参数的 Python 装饰器类

设计模式之装饰器模式

python 装饰器