如何在抽象类级别提供值验证?

我有一个ABC BaseAbstract类,定义了几个getter / setter属性.

我想要要设置的值是一个int,从0到15.

@luminance.setter
@abstractproperty
@ValidateProperty(Exception, types=(int,), valid=lambda x: True if 0 <= x <= 15 else False)
def luminance(self, value):
    """
    Set a value that indicate the level of light emitted from the block

    :param value: (int): 0 (darkest) - 15 (brightest)
    :return:
    """
    pass

有人可以帮我弄清楚我的ValidateProperty类/方法应该是什么样子.我从一个类开始并调用了accept方法,但这导致了一个错误:

function object has no attribute ‘func_code’

目前来源:

class ValidateProperty(object):
    @staticmethod
    def accepts(exception, *types, **kwargs):
        def check_accepts(f, **kwargs):
            assert len(types) == f.func_code.co_argcount

            def new_f(*args, **kwds):
                for i, v in enumerate(args):
                    if f.func_code.co_varnames[i] in types and\
                            not isinstance(v, types[f.func_code.co_varnames[i]]):
                        arg = f.func_code.co_varnames[i]
                        exp = types[f.func_code.co_varnames[i]]
                        raise exception("arg '{arg}'={r} does not match {exp}".format(arg=arg,
                                                                                      r=v,
                                                                                      exp=exp))
                        # del exp       (unreachable)

                    for k,v in kwds.__iter__():
                        if k in types and not isinstance(v, types[k]):
                            raise exception("arg '{arg}'={r} does not match {exp}".format(arg=k,
                                                                                          r=v,
                                                                                          exp=types[k]))

                    return f(*args, **kwds)

            new_f.func_name = f.func_name
            return new_f

        return check_accepts

最佳答案 我们其中一个人对
decorators,
descriptors(例如房产)和
abstracts的工作方式感到困惑 – 我希望不是我. 😉

这是一个粗略的工作示例:

from abc import ABCMeta, abstractproperty

class ValidateProperty:
    def __init__(inst, exception, arg_type, valid):
        # called on the @ValidateProperty(...) line
        #
        # save the exception to raise, the expected argument type, and
        # the validator code for later use
        inst.exception = exception
        inst.arg_type = arg_type
        inst.validator = valid
    def __call__(inst, func):
        # called after the def has finished, but before it is stored
        #
        # func is the def'd function, save it for later to be called
        # after validating the argument
        def check_accepts(self, value):
            if not inst.validator(value):
                raise inst.exception('value %s is not valid' % value)
            func(self, value)
        return check_accepts

class AbstractTestClass(metaclass=ABCMeta):
    @abstractproperty
    def luminance(self):
        # abstract property
        return
    @luminance.setter
    @ValidateProperty(Exception, int, lambda x: 0 <= x <= 15)
    def luminance(self, value):
        # abstract property with validator
        return

class TestClass(AbstractTestClass):
    # concrete class
    val = 7
    @property
    def luminance(self):
        # concrete property
        return self.val
    @luminance.setter
    def luminance(self, value):
        # concrete property setter
        # call base class first to activate the validator
        AbstractTestClass.__dict__['luminance'].__set__(self, value)
        self.val = value

tc = TestClass()
print(tc.luminance)
tc.luminance = 10
print(tc.luminance)
tc.luminance = 25
print(tc.luminance)

结果如下:

7
10
Traceback (most recent call last):
  File "abstract.py", line 47, in <module>
    tc.luminance = 25
  File "abstract.py", line 40, in luminance
    AbstractTestClass.__dict__['luminance'].__set__(self, value)
  File "abstract.py", line 14, in check_accepts
    raise inst.exception('value %s is not valid' % value)
Exception: value 25 is not valid

要考虑几点:

> ValidateProperty更简单,因为属性设置器只接受两个参数:self和new_value
>当一个类用于装饰器,并且装饰器接受参数时,则需要__init__来保存参数,而__call__实际上用于处理defd函数
>调用基类属性setter很难看,但你可以在辅助函数中隐藏它
>您可能希望使用自定义元类来确保运行验证代码(这也可以避免丑陋的基类属性调用)

我建议使用上面的元类来消除直接调用基类的abstractproperty的需要,这里有一个例子:

from abc import ABCMeta, abstractproperty

class AbstractTestClassMeta(ABCMeta):

    def __new__(metacls, cls, bases, clsdict):
        # create new class
        new_cls = super().__new__(metacls, cls, bases, clsdict)
        # collect all base class dictionaries
        base_dicts = [b.__dict__ for b in bases]
        if not base_dicts:
            return new_cls
        # iterate through clsdict looking for properties
        for name, obj in clsdict.items():
            if not isinstance(obj, (property)):
                continue
            prop_set = getattr(obj, 'fset')
            # found one, now look in bases for validation code
            validators = []
            for d in base_dicts:
                b_obj = d.get(name)
                if (
                        b_obj is not None and
                        isinstance(b_obj.fset, ValidateProperty)
                        ):
                    validators.append(b_obj.fset)
            if validators:
                def check_validators(self, new_val):
                    for func in validators:
                        func(new_val)
                    prop_set(self, new_val)
                new_prop = obj.setter(check_validators)
                setattr(new_cls, name, new_prop)

        return new_cls

这是ABCMeta的子类,并且ABCMeta首先完成所有工作,然后进行一些额外的处理.即:

>浏览创建的类并查找属性
>检查基类以查看它们是否具有匹配的abstractproperty
>检查abstractproperty的fset代码,看它是否是ValidateProperty的一个实例
>如果是,请将其保存在验证器列表中
>如果验证器列表不为空

>创建一个包装器,在调用实际属性的fset代码之前调用每个验证器
>将找到的属性替换为使用包装器作为setter代码的新属性

ValidateProperty也有一点不同:

class ValidateProperty:

    def __init__(self, exception, arg_type):
        # called on the @ValidateProperty(...) line
        #
        # save the exception to raise and the expected argument type
        self.exception = exception
        self.arg_type = arg_type
        self.validator = None

    def __call__(self, func_or_value):
        # on the first call, func_or_value is the function to use
        # as the validator
        if self.validator is None:
            self.validator = func_or_value
            return self
        # every subsequent call will be to do the validation
        if (
                not isinstance(func_or_value, self.arg_type) or
                not self.validator(None, func_or_value)
                ):
            raise self.exception(
                '%r is either not a type of %r or is outside '
                'argument range' %
                (func_or_value, type(func_or_value))
                )

基本的AbstractTestClass现在使用新的AbstractTestClassMeta,并且在abstractproperty中直接有验证器代码:

class AbstractTestClass(metaclass=AbstractTestClassMeta):

    @abstractproperty
    def luminance(self):
        # abstract property
        pass

    @luminance.setter
    @ValidateProperty(Exception, int)
    def luminance(self, value):
        # abstract property validator
        return 0 <= value <= 15

最后一堂课是一样的:

class TestClass(AbstractTestClass):
    # concrete class

    val = 7

    @property
    def luminance(self):
        # concrete property
        return self.val

    @luminance.setter
    def luminance(self, value):
        # concrete property setter
        # call base class first to activate the validator
        # AbstractTestClass.__dict__['luminance'].__set__(self, value)
        self.val = value
点赞