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.  
  8. def __set__(self, instance, value):
  9. instance.__dict__[self.name] = value
  10.  
  11.  
  12. # Descriptor for enforcing types
  13. class Typed(Descriptor):
  14. expected_type = type(None)
  15.  
  16. def __set__(self, instance, value):
  17. if not isinstance(value, self.expected_type):
  18. raise TypeError('expected ' + str(self.expected_type))
  19. super().__set__(instance, value)
  20.  
  21.  
  22. # Descriptor for enforcing values
  23. class Unsigned(Descriptor):
  24. def __set__(self, instance, value):
  25. if value < 0:
  26. raise ValueError('Expected >= 0')
  27. super().__set__(instance, value)
  28.  
  29.  
  30. class MaxSized(Descriptor):
  31. def __init__(self, name=None, **opts):
  32. if 'size' not in opts:
  33. raise TypeError('missing size option')
  34. super().__init__(name, **opts)
  35.  
  36. def __set__(self, instance, value):
  37. if len(value) >= self.size:
  38. raise ValueError('size must be < ' + str(self.size))
  39. super().__set__(instance, value)

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

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

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

  1. class Stock:
  2. # Specify constraints
  3. name = SizedString('name', size=8)
  4. shares = UnsignedInteger('shares')
  5. price = UnsignedFloat('price')
  6.  
  7. def __init__(self, name, shares, price):
  8. self.name = name
  9. self.shares = shares
  10. 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.  
  12. return decorate
  13.  
  14. # Example
  15. @check_attributes(name=SizedString(size=8),
  16. shares=UnsignedInteger,
  17. price=UnsignedFloat)
  18. class Stock:
  19. def __init__(self, name, shares, price):
  20. self.name = name
  21. self.shares = shares
  22. 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.  
  10. # Example
  11. class Stock2(metaclass=checkedmeta):
  12. name = SizedString(size=8)
  13. shares = UnsignedInteger()
  14. price = UnsignedFloat()
  15.  
  16. def __init__(self, name, shares, price):
  17. self.name = name
  18. self.shares = shares
  19. 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.  
  6. # Metaclass
  7. class Point(metaclass=checkedmeta):
  8. x = Integer()
  9. 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.  
  7. def __set__(self, instance, value):
  8. if not isinstance(value, expected_type):
  9. raise TypeError('expected ' + str(expected_type))
  10. super_set(self, instance, value)
  11.  
  12. cls.__set__ = __set__
  13. return cls
  14.  
  15.  
  16. # Decorator for unsigned values
  17. def Unsigned(cls):
  18. super_set = cls.__set__
  19.  
  20. def __set__(self, instance, value):
  21. if value < 0:
  22. raise ValueError('Expected >= 0')
  23. super_set(self, instance, value)
  24.  
  25. cls.__set__ = __set__
  26. return cls
  27.  
  28.  
  29. # Decorator for allowing sized values
  30. def MaxSized(cls):
  31. super_init = cls.__init__
  32.  
  33. def __init__(self, name=None, **opts):
  34. if 'size' not in opts:
  35. raise TypeError('missing size option')
  36. super_init(self, name, **opts)
  37.  
  38. cls.__init__ = __init__
  39.  
  40. super_set = cls.__set__
  41.  
  42. def __set__(self, instance, value):
  43. if len(value) >= self.size:
  44. raise ValueError('size must be < ' + str(self.size))
  45. super_set(self, instance, value)
  46.  
  47. cls.__set__ = __set__
  48. return cls
  49.  
  50.  
  51. # Specialized descriptors
  52. @Typed(int)
  53. class Integer(Descriptor):
  54. pass
  55.  
  56.  
  57. @Unsigned
  58. class UnsignedInteger(Integer):
  59. pass
  60.  
  61.  
  62. @Typed(float)
  63. class Float(Descriptor):
  64. pass
  65.  
  66.  
  67. @Unsigned
  68. class UnsignedFloat(Float):
  69. pass
  70.  
  71.  
  72. @Typed(str)
  73. class String(Descriptor):
  74. pass
  75.  
  76.  
  77. @MaxSized
  78. class SizedString(String):
  79. pass

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

原文:

http://python3-cookbook.readthedocs.io/zh_CN/latest/c08/p13_implementing_data_model_or_type_system.html