阅读 Python Cookbook 并查看描述符,尤其是在使用类属性时强制执行类型的示例。我正在编写一些有用的类,但我也想强制执行不变性。怎么办?改编自本书的类型检查描述符:
class Descriptor(object):
def __init__(self, name=None, **kwargs):
self.name = name
for key, value in kwargs.items():
setattr(self, key, value)
def __set__(self, instance, value):
instance.__dict__[self.name] = value
# by default allows None
class Typed(Descriptor):
def __init__(self, expected_types=None, **kwargs):
self.expected_types = expected_types
super().__init__(**kwargs)
def __set__(self, instance, value):
if value is not None and not isinstance(value, self.expected_types):
raise TypeError('Expected: {}'.format(str(self.expected_types)))
super(Typed, self).__set__(instance, value)
class T(object):
v = Typed(int)
def __init__(self, v):
self.v = v
尝试 #1:将self.is_set
属性添加到类型化
# by default allows None
class ImmutableTyped(Descriptor):
def __init__(self, expected_types=None, **kwargs):
self.expected_types = expected_types
self.is_set = False
super().__init__(**kwargs)
def __set__(self, instance, value):
if self.is_set:
raise ImmutableException(...)
if value is not None and not isinstance(value, self.expected_types):
raise TypeError('Expected: {}'.format(str(self.expected_types)))
self.is_set = True
super(Typed, self).__set__(instance, value)
错误,因为在执行以下操作时,ImmutableTyped
是"全局"的,因为它在类的所有实例中都是单例。实例化t2
时,is_set
在上一个对象中已经为 True。
class T(object):
v = ImmutableTyped(int)
def __init__(self, v):
self.v = v
t1 = T()
t2 = T() # fail when instantiating
尝试#2:__set__
中的思想实例是指包含属性的类,因此尝试检查instance.__dict__[self.name]
是否仍然是类型。这也是错误的。
想法#3:通过接受返回T实例__dict__
的"fget
"方法,使Typed的使用更类似于@property
。这需要定义 T 中的函数,类似于:
@Typed
def v(self):
return self.__dict__
这似乎是错误的。
如何实现不可变性和类型检查作为描述符?
现在这是我解决问题的方法:
class ImmutableTyped:
def __set_name__(self, owner, name):
self.name = name
def __init__(self, *, immutable=False, types=None)
self.immutable == immutable is True
self.types = types if types else []
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
if self.immutable is True:
raise TypeError('read-only attribute')
elif not any(isinstance(value, cls)
for cls in self.types):
raise TypeError('invalid argument type')
else:
instance.__dict__[self.name] = value
旁注:__set_name__
可用于允许您在初始化中不指定属性名称。这意味着您可以执行以下操作:
class Foo:
bar = ImmutableTyped()
并且ImmutableTyped
的实例将自动具有name
属性bar
,因为我键入了要在__set_name__
方法中发生的属性。
无法成功做出这样的描述。也许它也不必要地复杂。以下方法+property
使用就足够了。
# this also allows None to go through
def check_type(data, expected_types):
if data is not None and not isinstance(data, expected_types):
raise TypeError('Expected: {}'.format(str(expected_types)))
return data
class A():
def __init__(self, value=None):
self._value = check_type(value, (str, bytes))
@property
def value(self):
return self._value
foo = A()
print(foo.value) # None
foo.value = 'bla' # AttributeError
bar = A('goosfraba')
print(bar.value) # goosfraba
bar.value = 'bla' # AttributeError
class ImmutableTyped(object):
def __set_name__(self, owner, name):
self.name = name
def __init__(self, *, types=None):
self.types = tuple(types or [])
self.instances = {}
return None
def __get__(self, instance, owner):
return instance.__dict__[self.name]
def __set__(self, instance, value):
is_set = self.instances.setdefault(id(instance), False)
if is_set:
raise AttributeError("read-only attribute '%s'" % (self.name))
if self.types:
if not isinstance(value, self.types):
raise TypeError("invalid argument type '%s' for '%s'" % (type(value), self.name))
self.instances[id(instance)] = True
instance.__dict__[self.name] = value
return None
例子:
class Something(object):
prop1 = ImmutableTyped(types=[int])
something = Something()
something.prop1 = "1"
将给予:
TypeError: invalid argument type '<class 'str'>' for 'prop1'
和:
something = Something()
something.prop1 = 1
something.prop1 = 2
将给予:
TypeError: read-only attribute 'prop1'
class NameControl:
"""
Descriptor which don't allow to change field value after initialization.
"""
def __init__(self):
self.defined = False
def __set__(self, instance, value):
if not self.defined:
self.defined = True
elif self.defined:
raise ValueError