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

How to provide value validation at abstract class level?

我有一个 ABC BaseAbstract class,其中定义了几个 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 class/method 应该是什么样子。我从 class 开始并调用了 accepts 方法,但这会导致错误:

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 (e.g. properties), and 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就简单多了,因为属性setter只需要两个参数:selfnew_value

  • 当装饰器使用class时,装饰器接受参数,那么你需要__init__来保存参数,__call__实际处理 defd 函数

  • 调用基数 class 属性 setter 很丑陋,但您可以将其隐藏在辅助函数中

  • 你可能想使用自定义元class来确保验证码是运行(这也可以避免丑陋的基础-class 属性调用)


我在上面建议了一个 metaclass 来消除直接调用基础 class 的 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

这个子class是ABCMeta,并且让ABCMeta先完成它的所有工作,然后再做一些额外的处理。即:

  • 浏览创建的 class 并查找属性
  • 检查基础 classes 看它们是否有匹配的摘要属性
  • 检查摘要属性的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是一样的:

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