我们中的一个人对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
当使用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