8.8 子类中扩展property

问题

在子类中,你想要扩展定义在父类中的property的功能。

解决方案

考虑如下的代码,它定义了一个property:

  1. class Person:
  2. def __init__(self, name):
  3. self.name = name
  4.  
  5. # Getter function
  6. @property
  7. def name(self):
  8. return self._name
  9.  
  10. # Setter function
  11. @name.setter
  12. def name(self, value):
  13. if not isinstance(value, str):
  14. raise TypeError('Expected a string')
  15. self._name = value
  16.  
  17. # Deleter function
  18. @name.deleter
  19. def name(self):
  20. raise AttributeError("Can't delete attribute")

下面是一个示例类,它继承自Person并扩展了 name 属性的功能:

  1. class SubPerson(Person):
  2. @property
  3. def name(self):
  4. print('Getting name')
  5. return super().name
  6.  
  7. @name.setter
  8. def name(self, value):
  9. print('Setting name to', value)
  10. super(SubPerson, SubPerson).name.__set__(self, value)
  11.  
  12. @name.deleter
  13. def name(self):
  14. print('Deleting name')
  15. super(SubPerson, SubPerson).name.__delete__(self)

接下来使用这个新类:

  1. >>> s = SubPerson('Guido')
  2. Setting name to Guido
  3. >>> s.name
  4. Getting name
  5. 'Guido'
  6. >>> s.name = 'Larry'
  7. Setting name to Larry
  8. >>> s.name = 42
  9. Traceback (most recent call last):
  10. File "<stdin>", line 1, in <module>
  11. File "example.py", line 16, in name
  12. raise TypeError('Expected a string')
  13. TypeError: Expected a string
  14. >>>

如果你仅仅只想扩展property的某一个方法,那么可以像下面这样写:

  1. class SubPerson(Person):
  2. @Person.name.getter
  3. def name(self):
  4. print('Getting name')
  5. return super().name

或者,你只想修改setter方法,就这么写:

  1. class SubPerson(Person):
  2. @Person.name.setter
  3. def name(self, value):
  4. print('Setting name to', value)
  5. super(SubPerson, SubPerson).name.__set__(self, value)

讨论

在子类中扩展一个property可能会引起很多不易察觉的问题,因为一个property其实是 gettersetterdeleter 方法的集合,而不是单个方法。因此,当你扩展一个property的时候,你需要先确定你是否要重新定义所有的方法还是说只修改其中某一个。

在第一个例子中,所有的property方法都被重新定义。在每一个方法中,使用了 super() 来调用父类的实现。在 setter 函数中使用 super(SubPerson, SubPerson).name.set(self, value) 的语句是没有错的。为了委托给之前定义的setter方法,需要将控制权传递给之前定义的name属性的 set() 方法。不过,获取这个方法的唯一途径是使用类变量而不是实例变量来访问它。这也是为什么我们要使用 super(SubPerson, SubPerson) 的原因。

如果你只想重定义其中一个方法,那只使用 @property 本身是不够的。比如,下面的代码就无法工作:

  1. class SubPerson(Person):
  2. @property # Doesn't work
  3. def name(self):
  4. print('Getting name')
  5. return super().name

如果你试着运行会发现setter函数整个消失了:

  1. >>> s = SubPerson('Guido')
  2. Traceback (most recent call last):
  3. File "<stdin>", line 1, in <module>
  4. File "example.py", line 5, in __init__
  5. self.name = name
  6. AttributeError: can't set attribute
  7. >>>

你应该像之前说过的那样修改代码:

  1. class SubPerson(Person):
  2. @Person.name.getter
  3. def name(self):
  4. print('Getting name')
  5. return super().name

这么写后,property之前已经定义过的方法会被复制过来,而getter函数被替换。然后它就能按照期望的工作了:

  1. >>> s = SubPerson('Guido')
  2. >>> s.name
  3. Getting name
  4. 'Guido'
  5. >>> s.name = 'Larry'
  6. >>> s.name
  7. Getting name
  8. 'Larry'
  9. >>> s.name = 42
  10. Traceback (most recent call last):
  11. File "<stdin>", line 1, in <module>
  12. File "example.py", line 16, in name
  13. raise TypeError('Expected a string')
  14. TypeError: Expected a string
  15. >>>

在这个特别的解决方案中,我们没办法使用更加通用的方式去替换硬编码的 Person 类名。如果你不知道到底是哪个基类定义了property,那你只能通过重新定义所有property并使用 super() 来将控制权传递给前面的实现。

值的注意的是上面演示的第一种技术还可以被用来扩展一个描述器(在8.9小节我们有专门的介绍)。比如:

  1. # A descriptor
  2. class String:
  3. def __init__(self, name):
  4. self.name = name
  5.  
  6. def __get__(self, instance, cls):
  7. if instance is None:
  8. return self
  9. return instance.__dict__[self.name]
  10.  
  11. def __set__(self, instance, value):
  12. if not isinstance(value, str):
  13. raise TypeError('Expected a string')
  14. instance.__dict__[self.name] = value
  15.  
  16. # A class with a descriptor
  17. class Person:
  18. name = String('name')
  19.  
  20. def __init__(self, name):
  21. self.name = name
  22.  
  23. # Extending a descriptor with a property
  24. class SubPerson(Person):
  25. @property
  26. def name(self):
  27. print('Getting name')
  28. return super().name
  29.  
  30. @name.setter
  31. def name(self, value):
  32. print('Setting name to', value)
  33. super(SubPerson, SubPerson).name.__set__(self, value)
  34.  
  35. @name.deleter
  36. def name(self):
  37. print('Deleting name')
  38. super(SubPerson, SubPerson).name.__delete__(self)

最后值的注意的是,读到这里时,你应该会发现子类化 setterdeleter 方法其实是很简单的。这里演示的解决方案同样适用,但是在 Python的issue页面报告的一个bug,或许会使得将来的Python版本中出现一个更加简洁的方法。

原文:

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