派生类的类/元类方法装饰器

Posted

技术标签:

【中文标题】派生类的类/元类方法装饰器【英文标题】:Class/metaclass method decorator for derived class 【发布时间】:2019-05-02 22:19:35 【问题描述】:

我有一个元类,它定义了一个类级别属性,该属性对于每个子类应该是唯一的,但在每个子类的实例之间共享。

class MetaValidator(type):
    def __new__(
             cls, name, bases, dct
    ):
        new_cls = super().__new__(cls, name, bases, dct)
        new_cls.valid_funcs = []
        return new_cls

现在我想实现一个装饰器,将装饰类方法附加到派生类中的valid_funcs。但是,因为派生类仍在定义中,所以我没有对派生装饰器的引用,所以我最终追加到基类。这是我的代码:

class Validator(object, metaclass=MetaValidator):

    @classmethod
    def add(cls, f):
        cls.valid_funcs.append(f)
        return f

    def _validate(self, **kwargs):
        for f in self.valid_funcs:
            params = inspect.signature(f).parameters.keys()
            f_kwargs = name: kwargs[name] for name in params
            f(**f_kwargs)

    def validate(self, **kwargs):
        self._validate(**kwargs)

class A(Validator):

    @staticmethod
    @Validator.add
    def test_func(x):
        return x

class B(Validator):

    @staticmethod
    @Validator.add
    def test_func(x, y):
        return x, y

a = A()
a.validate(x="In A")
b = B()
b.validate(x="In B", y=" Called with arg y")

print(Validator.valid_funcs)
print(a.valid_funcs)
print(b.valid_funcs)

打印出来:

[<function A.test_func at 0x7f0189d4fc80>, 
<function B.test_func at 0x7f0189d4fd08>]
[]
[]

我想要:

[]
[<function A.test_func at 0x7f0189d4fc80>]
[<function B.test_func at 0x7f0189d4fd08>]

【问题讨论】:

您使用元类是否有特定原因?为什么不简单地使用类装饰器添加类属性? 我也可以这样做,但是我必须将装饰器添加到每个派生类中,这更加麻烦。它也无助于 @add 装饰器的主要问题。如果使用装饰器而不是元类有帮助,那么我会接受它。 如果您使用的是 python 3.6+,则不需要这样做。您可以改用__subclass_init__。容易得多。 哎呀抱歉:__init_subclass__(我总是要查一下) 【参考方案1】:

在类主体中的函数上执行装饰器时,还没有类对象。首先执行类体,然后创建类。

不是让装饰器寻找要改变的类属性,而是将属性添加到装饰的函数对象。元类_validate()实现然后查找具有此属性的任何对象,并在创建类对象后将它们添加到列表中。

我将假设您希望保留装饰器将装饰项目添加到列表中的顺序:

from itertools import count

class Validator(metaclass=MetaValidator):
    @classmethod
    def add(cls, f):
        _count = getattr(Validator.add, '_count', None)
        if _count is None:
            _count = Validator.add.__func__._count = count()
        f._validator_function_id = next(_count)
        return f

在元类中:

class MetaValidator(type):
    def __new__(cls, name, bases, dct):
        new_cls = super().__new__(cls, name, bases, dct)
        registered = []
        for v in dct.values():
            id = getattr(v, '_validator_function_id', None)
            if id is None and isinstance(v, (staticmethod, classmethod)):
                # unwrap staticmethod or classmethod decorators
                id = getattr(v.__func__, '_validator_function_id', None)
            if id is not None:
                registered.append((id, v))
        new_cls.valid_funcs = [f for _, f in sorted(registered)]
        return new_cls

请注意,如果您使用的是 Python 3.6 或更新版本,那么您就不再需要元类了。您可以将相同的逻辑放入class.__init_subclass__ method。

请注意,这会注册未绑定的对象。对于staticmethod 对象,这意味着调用将失败:

TypeError: <staticmethod object at 0x10d1b7048> is not a callable object

在这种情况下,您可能想要注册__func__ 属性,或者使用.__get__ 将对象“绑定”到某物(staticmethod 无论如何都会忽略绑定上下文)`。

如果您在_validate() 方法中显式绑定,那么您实际上不必使用staticmethod 对象:

def _validate(self, **kwargs):
    for f in self.valid_funcs:
        bound = f.__get__(self)
        signature = inspect.signature(bound)
        bound(**name: kwargs[name] for name in signature.parameters)

现在@validator.add 将与staticmethodclassmethod 和常规函数一起使用。

如果您有_validate() 方法查找方法,则可以为您完成绑定。您可以在这里选择支持继承,只需使用dir()getattr()

from operator import itemgetter
from itertools import count


class Validator:
    @classmethod
    def add(cls, f):
        _count = getattr(Validator.add, '_count', None)
        if _count is None:
            _count = Validator.add.__func__._count = count()
        f._validator_function_id = next(_count)
        return f

    def _list_validators(self):
        objects = (getattr(self, name) for name in dir(self))
        return sorted(
            (o for o in objects if hasattr(o, '_validator_function_id')),
            key=attrgetter('_validator_function_id'))

    def _validate(self, **kwargs):
        for f in self._list_validators():
            signature = inspect.signature(f)
            f(**name: kwargs[name] for name in signature.parameters)

getattr() 为您提供绑定对象,无需进一步绑定。

【讨论】:

【参考方案2】:

虽然让元类__new__ 处理向valid_funcs 添加函数是一种选择,但另一种选择是在类甚至存在之前将valid_funcs 注入到类主体的命名空间中,使用__prepare__

class MetaValidator(type):
    @classmethod
    def __prepare__(cls, name, bases, **kwds):
        ns = super().__prepare__(name, bases, **kwds)
        ns['valid_funcs'] = []
        return ns

def register(func_list):
    def inner_register(func):
        func_list.append(func)
        return func
    return inner_register

class A(metaclass=MetaValidator):
    @register(valid_funcs)
    def method(self):
        ...

不过,我可能会跳过所有元类的内容并要求类自己做valid_funcs = []。元类的额外复杂性不值得为每个类节省一行样板代码。

【讨论】:

以上是关于派生类的类/元类方法装饰器的主要内容,如果未能解决你的问题,请参考以下文章

面向对象编程-总复习

9.python 共享引用与单例

装饰器、装饰器类与类装饰器(三)

元类的“__init_subclass__”方法在此元类构造的类中不起作用

从0开始的TypeScriptの十二:装饰器

从0开始的TypeScriptの十二:装饰器