【问题标题】:How to provide value validation at abstract class level?如何在抽象类级别提供值验证?
【发布时间】:2016-05-24 00:41:50
【问题描述】:

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

我想要求要设置的值是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 类/方法应该是什么样子。我从一个类开始并调用了accepts 方法,但这会导致错误:

function 对象没有属性 '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

【问题讨论】:

    标签: validation python-3.x setter python-decorators


    【解决方案1】:

    我们中的一个人对decoratorsdescriptors(例如属性)和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 要简单得多,因为属性设置器只需要两个参数:selfnew_value

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

    • 调用基类属性设置器很难看,但您可以将其隐藏在辅助函数中

    • 您可能希望使用自定义元类来确保运行验证代码(这也将避免丑陋的基类属性调用)


    我在上面建议了一个元类,以消除直接调用基类的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 先完成所有工作,然后再进行一些额外的处理。即:

    • 遍历创建的类并查找属性
    • 检查基类以查看它们是否具有匹配的抽象属性
    • 检查抽象属性的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
    

    【讨论】:

    • 困惑可能在于我。我正在冒险进入 Python 的更深奥和更高级的领域(这是我最有趣的地方)。我将使用您的解决方案。我对使用自定义元类的建议感兴趣。你能提供一个关于那个项目符号的链接作为一个好的起点吗?我想避免调用基类属性设置器。
    • 顺便说一句,我想知道是什么引发了关于该主题的任何混淆的危险信号,所以我知道我可能会感到困惑。 :) 你能用简短的解释来编辑吗?非常感谢。
    • @IAbstract:这是您的ValidateProperty 装饰器,它不是常规函数的正确装饰器,更不用说类中的方法(以及属性!;)。查看 this answer 以获得关于一般装饰器的非常好的解释。
    • @IAbstract:这里是an answer of mine,它只涉及元类(见下半部分)。在接下来的几天里,我将尝试在这里勾勒出一个验证抽象元类。
    • @IAbstract:刚刚注意到你把它标记为 Python 3——我更新了答案(一直在使用 Python 2 语法)。
    猜你喜欢
    • 1970-01-01
    • 2018-07-29
    • 2012-09-27
    • 1970-01-01
    • 1970-01-01
    • 2012-06-23
    • 2012-09-04
    • 1970-01-01
    • 2018-04-17
    相关资源
    最近更新 更多