用于类型检查和不变性的 Python 描述符



阅读 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

最新更新