实现数据模型或类型系统
想定义各种类型的数据结构,但是要对允许分配给某些属性的值实施约束。在此问题中,基本上要面对某些实例属性的设置进行检查或声明。 为此,需要基于每个属性自定义属性的设置,此时应该使用描述符。一般通过类及类的继承系统来实现,如下示例:
# Base class. Uses a descriptor to set a value
class Descriptor:
def __init__(self, name=None, **opts):
self.name = name
for key, value in opts.items():
setattr(self, key, value)
def __set__(self, instance, value):
instance.__dict__[self.name] = value
# Descriptor for enforcing types
class Typed(Descriptor):
expected_type = type(None)
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError('expected ' + str(self.expected_type))
super().__set__(instance, value)
# Descriptor for enforcing values
class Unsigned(Descriptor):
def __set__(self, instance, value):
if value < 0:
raise ValueError('Expected >= 0')
super().__set__(instance, value)
class MaxSized(Descriptor):
def __init__(self, name=None, **opts):
if 'size' not in opts:
raise TypeError('missing size option')
super().__init__(name, **opts)
def __set__(self, instance, value):
if len(value) >= self.size:
raise ValueError('size must be < ' + str(self.size))
super().__set__(instance, value)
class Integer(Typed):
expected_type = int
class UnsignedInteger(Integer, Unsigned):
pass
class Float(Typed):
expected_type = float
class UnsignedFloat(Float, Unsigned):
pass
class String(Typed):
expected_type = str
class SizedString(String, MaxSized):
pass
class Stock:
# Specify constraints
name = SizedString('name', size=8)
shares = UnsignedInteger('shares')
price = UnsignedFloat('price')
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
>>> s = Stock('ACME', 50, 91.1)
>>> s.name
'ACME'
>>> s.shares = 75
>>> s.shares = -10
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "example.py", line 17, in __set__
super().__set__(instance, value)
File "example.py", line 23, in __set__
raise ValueError('Expected >= 0')
ValueError: Expected >= 0
>>> s.price = 'a lot'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "example.py", line 16, in __set__
raise TypeError('expected ' + str(self.expected_type))
TypeError: expected <class 'float'>
>>> s.name = 'ABRACADABRA'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "example.py", line 17, in __set__
super().__set__(instance, value)
File "example.py", line 35, in __set__
raise ValueError('size must be < ' + str(self.size))
ValueError: size must be < 8
>>>
另一种实现方式是使用类装饰器,如下:
def check_attributes(**kwargs):
def decorate(cls):
for key, value in kwargs.items():
if isinstance(value, Descriptor):
value.name = key
setattr(cls, key, value)
else:
setattr(cls, key, value(key))
return cls
return decorate
# Example
@check_attributes(name=SizedString(size=8), shares=UnsignedInteger,
price=UnsignedFloat)
class Stock:
def __Init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
第三种方式是使用元类,如下:
# A metaclass that applies checking
class checkedmeta(type):
def __new__(cls, clsname, bases, methods):
# Attach attribute names to the descriptors
for key, value in methods.items():
if isinstance(value, Descriptor):
value.name = key
return type(cls, clsname, bases, methods)
# Example
class Stock(metaclass=checkedmeta):
name = SizedString(size=8)
shares = UnsignedInteger()
price = UnsignedFloat()
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
本篇博文包含Python编程中的很多高级技巧和语言系统,如描述符,mixin类,super()的多重继承,类装饰器,元类等。这也是Python进阶中必须跨过的门槛。
· 首先,在Descriptor类中引入了_get_, _set_, __delete__等实现描述符协议的类方法。这在包含该类示例的类属性中使用属性访问时使用。
- 整个类型继承包含了mixin,如Unsigned和MaxSized被分别引入在UnsignedFloat和SizedString类中。
- 所有的__init__方法都引入opts的关键字参数,然后使用super()代理这些参数的初始化工作
- 使用类装饰器或元类通常对于简化用户的规范很有用。 在这些示例中,用户不再需要多次键入属性名称。
- 类装饰器和元类的代码只需扫描类字典以查找描述符。 找到后,它们仅根据键值填写描述符的name。
最后:在所有方法中,类装饰器解决方案都可以提供最大的灵活性和完整性。 一方面,它不依赖任何高级机制,例如元类。 其次,装饰器是可以很容易地根据需要从类定义中添加或删除。 例如,在装饰器中,可能有一个选项可以完全省略所添加的检查。 这些可能使检查成为可以根据需求打开或关闭(例如debug还是production模式)。
最后,类装饰器方法也可以代替mixin类,多重继承和对super()函数的棘手使用:
# Base class. Uses a descriptor to set a value
class Descriptor:
def __init__(self, name=None, **opts):
self.name = name
for key, value in opts.items():
setattr(self, key, value)
def __set__(self, instance, value):
instance.__dict__[self.name] = value
# Decorator for applying type checking
def Typed(expected_type, cls=None):
if cls is None:
return lambda cls: Typed(expected_type, cls)
super_set = cls.__set__
def __set__(self, instance, value):
if not isinstance(value, expected_type):
raise TypeError('expected ' + str(expected_type))
super_set(self, instance, value)
cls.__set__ = __set__
return cls
# Decorator for unsigned values
def Unsigned(cls):
super_set = cls.__set__
def __set__(self, instance, value):
if value < 0:
raise ValueError('Expected >= 0')
super_set(self, instance, value)
cls.__set__ = __set__
return cls
# Decorator for allowing sized values
def MaxSized(cls):
super_init = cls.__init__
def __init__(self, name=None, **opts):
if 'size' not in opts:
raise TypeError('missing size option')
super_init(self, name, **opts)
cls.__init__ = __init__
super_set = cls.__set__
def __set__(self, instance, value):
if len(value) >= self.size:
raise ValueError('size must be < ' + str(self.size))
super_set(self, instance, value)
cls.__set__ = __set__
return cls
# Specialized descriptors
@Typed(int)
class Integer(Descriptor):
pass
@Unsigned
class UnsignedInteger(Integer):
pass
@Typed(float)
class Float(Descriptor):
pass
@Unsigned
class UnsignedFloat(Float):
pass
@Typed(str)
class String(Descriptor):
pass
@MaxSized
class SizedString(String):
pass
上面例子中,所有的装饰器函数都是将cls原先的__init__或__set__方法改写,并重新赋值。
此替代方案中定义的类以与之前完全相同的方式工作(早期示例代码均未更改),只是所有内容的运行速度更快。 例如,设置类型属性的简单时序测试表明,类装饰器方法的运行速度比使用mixins的方法快100%。