How to provide value validation at abstract class level?
我有一个abc baseabstract类,定义了几个getter/setter属性。
我想要求设置的值是一个
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类/方法应该是什么样子吗?我从一个类开始,调用了
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 要简单得多,因为属性设置程序只使用两个参数:self 和new_value 。当使用
class 作为装饰器时,装饰器接受参数,则需要__init__ 保存参数,__call__ 实际处理def d函数。调用基类属性setter是丑陋的,但可以将其隐藏在helper函数中。
您可能希望使用自定义元类来确保运行验证代码(这也将避免丑陋的基类属性调用)。
我建议使用上面的一个元类来消除直接调用基类的
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 |
该子类属于
- 浏览创建的类并查找属性
- 检查基类以查看它们是否具有匹配的AbstractProperty
- 检查abstractproperty的
fset 代码,看它是否是ValidateProperty 的实例。 - 如果是,请将其保存在验证器列表中
- 如果验证器列表不是空的
- 在调用实际属性的
fset 代码之前,生成一个将调用每个验证器的包装器。 - 将找到的属性替换为使用包装作为
setter 代码的新属性
- 在调用实际属性的
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)) ) |
基础
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 |