关于python 3.x:如何在抽象类级别提供值验证?

How to provide value validation at abstract class level?

我有一个abc baseabstract类,定义了几个getter/setter属性。

我想要求设置的值是一个int和0-15。

1
2
3
4
5
6
7
8
9
10
11
@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 object has no attribute 'func_code'

电流源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

我们中的一个对装饰、描述符(例如属性)和抽象是如何工作感到困惑——我希望不是我。;)

下面是一个粗略的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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)

结果是:

1
2
3
4
5
6
7
8
9
10
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函数。

  • 调用基类属性setter是丑陋的,但可以将其隐藏在helper函数中。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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也有点不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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中有验证码:

1
2
3
4
5
6
7
8
9
10
11
12
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

最后一节课是一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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