加载中...

8.13 实现数据模型的类型约束


问题

你想定义某些在属性赋值上面有限制的数据结构。

解决方案

在这个问题中,你需要在对某些实例属性赋值时进行检查。所以你要自定义属性赋值函数,这种情况下最好使用描述器。

下面的代码使用描述器实现了一个系统类型和赋值验证框架:

  1. # Base class. Uses a descriptor to set a value
  2. class Descriptor:
  3. def __init__(self, name=None, **opts):
  4. self.name = name
  5. for key, value in opts.items():
  6. setattr(self, key, value)
  7. def __set__(self, instance, value):
  8. instance.__dict__[self.name] = value
  9. # Descriptor for enforcing types
  10. class Typed(Descriptor):
  11. expected_type = type(None)
  12. def __set__(self, instance, value):
  13. if not isinstance(value, self.expected_type):
  14. raise TypeError('expected ' + str(self.expected_type))
  15. super().__set__(instance, value)
  16. # Descriptor for enforcing values
  17. class Unsigned(Descriptor):
  18. def __set__(self, instance, value):
  19. if value < 0:
  20. raise ValueError('Expected >= 0')
  21. super().__set__(instance, value)
  22. class MaxSized(Descriptor):
  23. def __init__(self, name=None, **opts):
  24. if 'size' not in opts:
  25. raise TypeError('missing size option')
  26. super().__init__(name, **opts)
  27. def __set__(self, instance, value):
  28. if len(value) >= self.size:
  29. raise ValueError('size must be < ' + str(self.size))
  30. super().__set__(instance, value)

这些类就是你要创建的数据模型或类型系统的基础构建模块。下面就是我们实际定义的各种不同的数据类型:

  1. class Integer(Typed):
  2. expected_type = int
  3. class UnsignedInteger(Integer, Unsigned):
  4. pass
  5. class Float(Typed):
  6. expected_type = float
  7. class UnsignedFloat(Float, Unsigned):
  8. pass
  9. class String(Typed):
  10. expected_type = str
  11. class SizedString(String, MaxSized):
  12. pass

然后使用这些自定义数据类型,我们定义一个类:

  1. class Stock:
  2. # Specify constraints
  3. name = SizedString('name', size=8)
  4. shares = UnsignedInteger('shares')
  5. price = UnsignedFloat('price')
  6. def __init__(self, name, shares, price):
  7. self.name = name
  8. self.shares = shares
  9. self.price = price

然后测试这个类的属性赋值约束,可发现对某些属性的赋值违法了约束是不合法的:

  1. >>> s.name
  2. 'ACME'
  3. >>> s.shares = 75
  4. >>> s.shares = -10
  5. Traceback (most recent call last):
  6. File "<stdin>", line 1, in <module>
  7. File "example.py", line 17, in __set__
  8. super().__set__(instance, value)
  9. File "example.py", line 23, in __set__
  10. raise ValueError('Expected >= 0')
  11. ValueError: Expected >= 0
  12. >>> s.price = 'a lot'
  13. Traceback (most recent call last):
  14. File "<stdin>", line 1, in <module>
  15. File "example.py", line 16, in __set__
  16. raise TypeError('expected ' + str(self.expected_type))
  17. TypeError: expected <class 'float'>
  18. >>> s.name = 'ABRACADABRA'
  19. Traceback (most recent call last):
  20. File "<stdin>", line 1, in <module>
  21. File "example.py", line 17, in __set__
  22. super().__set__(instance, value)
  23. File "example.py", line 35, in __set__
  24. raise ValueError('size must be < ' + str(self.size))
  25. ValueError: size must be < 8
  26. >>>

还有一些技术可以简化上面的代码,其中一种是使用类装饰器:

  1. # Class decorator to apply constraints
  2. def check_attributes(**kwargs):
  3. def decorate(cls):
  4. for key, value in kwargs.items():
  5. if isinstance(value, Descriptor):
  6. value.name = key
  7. setattr(cls, key, value)
  8. else:
  9. setattr(cls, key, value(key))
  10. return cls
  11. return decorate
  12. # Example
  13. @check_attributes(name=SizedString(size=8),
  14. shares=UnsignedInteger,
  15. price=UnsignedFloat)
  16. class Stock:
  17. def __init__(self, name, shares, price):
  18. self.name = name
  19. self.shares = shares
  20. self.price = price

另外一种方式是使用元类:

  1. # A metaclass that applies checking
  2. class checkedmeta(type):
  3. def __new__(cls, clsname, bases, methods):
  4. # Attach attribute names to the descriptors
  5. for key, value in methods.items():
  6. if isinstance(value, Descriptor):
  7. value.name = key
  8. return type.__new__(cls, clsname, bases, methods)
  9. # Example
  10. class Stock2(metaclass=checkedmeta):
  11. name = SizedString(size=8)
  12. shares = UnsignedInteger()
  13. price = UnsignedFloat()
  14. def __init__(self, name, shares, price):
  15. self.name = name
  16. self.shares = shares
  17. self.price = price

讨论

本节使用了很多高级技术,包括描述器、混入类、super() 的使用、类装饰器和元类。不可能在这里一一详细展开来讲,但是可以在8.9、8.18、9.19小节找到更多例子。但是,我在这里还是要提一下几个需要注意的点。

首先,在 Descriptor 基类中你会看到有个 __set__() 方法,却没有相应的 __get__() 方法。如果一个描述仅仅是从底层实例字典中获取某个属性值的话,那么没必要去定义 __get__() 方法。

所有描述器类都是基于混入类来实现的。比如 UnsignedMaxSized 要跟其他继承自 Typed 类混入。这里利用多继承来实现相应的功能。

混入类的一个比较难理解的地方是,调用 super() 函数时,你并不知道究竟要调用哪个具体类。你需要跟其他类结合后才能正确的使用,也就是必须合作才能产生效果。

使用类装饰器和元类通常可以简化代码。上面两个例子中你会发现你只需要输入一次属性名即可了。

  1. # Normal
  2. class Point:
  3. x = Integer('x')
  4. y = Integer('y')
  5. # Metaclass
  6. class Point(metaclass=checkedmeta):
  7. x = Integer()
  8. y = Integer()

所有方法中,类装饰器方案应该是最灵活和最高明的。首先,它并不依赖任何其他新的技术,比如元类。其次,装饰器可以很容易的添加或删除。

最后,装饰器还能作为混入类的替代技术来实现同样的效果;

  1. # Decorator for applying type checking
  2. def Typed(expected_type, cls=None):
  3. if cls is None:
  4. return lambda cls: Typed(expected_type, cls)
  5. super_set = cls.__set__
  6. def __set__(self, instance, value):
  7. if not isinstance(value, expected_type):
  8. raise TypeError('expected ' + str(expected_type))
  9. super_set(self, instance, value)
  10. cls.__set__ = __set__
  11. return cls
  12. # Decorator for unsigned values
  13. def Unsigned(cls):
  14. super_set = cls.__set__
  15. def __set__(self, instance, value):
  16. if value < 0:
  17. raise ValueError('Expected >= 0')
  18. super_set(self, instance, value)
  19. cls.__set__ = __set__
  20. return cls
  21. # Decorator for allowing sized values
  22. def MaxSized(cls):
  23. super_init = cls.__init__
  24. def __init__(self, name=None, **opts):
  25. if 'size' not in opts:
  26. raise TypeError('missing size option')
  27. super_init(self, name, **opts)
  28. cls.__init__ = __init__
  29. super_set = cls.__set__
  30. def __set__(self, instance, value):
  31. if len(value) >= self.size:
  32. raise ValueError('size must be < ' + str(self.size))
  33. super_set(self, instance, value)
  34. cls.__set__ = __set__
  35. return cls
  36. # Specialized descriptors
  37. @Typed(int)
  38. class Integer(Descriptor):
  39. pass
  40. @Unsigned
  41. class UnsignedInteger(Integer):
  42. pass
  43. @Typed(float)
  44. class Float(Descriptor):
  45. pass
  46. @Unsigned
  47. class UnsignedFloat(Float):
  48. pass
  49. @Typed(str)
  50. class String(Descriptor):
  51. pass
  52. @MaxSized
  53. class SizedString(String):
  54. pass

这种方式定义的类跟之前的效果一样,而且执行速度会更快。设置一个简单的类型属性的值,装饰器方式要比之前的混入类的方式几乎快100%。现在你应该庆幸自己读完了本节全部内容了吧?^_^


还没有评论.