• 8.13 实现数据模型的类型约束
    • 问题
    • 解决方案
    • 讨论

    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