天天看点

Python类与对象学习心得-6:在子类中扩展 property

前面已经介绍了在 Python 中特性(property)的用法。这里介绍下在子类中如何扩展定义在父类中的特性(property)的功能。

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

class Person:
    def __init__(self, name):
        self.name = name

    # Getter function
    @property
    def name(self):
        return self._name

    # Setter function
    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._name = value

    # Deleter function
    @name.deleter
    def name(self):
        raise AttributeError("Can't delete attribute")           

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

class SubPerson(Person):
    @property
    def name(self):
        print('Getting name')
        return super().name

    @name.setter
    def name(self, value):
        print('Setting name to', value)
        super(SubPerson, SubPerson).name.__set__(self, value) # 注意这里 super() 的新用法

    @name.deleter
    def name(self):
        print('Deleting name')
        super(SubPerson, SubPerson).name.__delete__(self)           

接下来使用这个新类:

>>> s = SubPerson('Guido')
Setting name to Guido
>>> s.name
Getting name
'Guido'
>>> s.name = 'Larry'
Setting name to Larry
>>> s.name = 42
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "example.py", line 16, in name
        raise TypeError('Expected a string')
TypeError: Expected a string
>>>           

如果你仅仅只想扩展父类中特性(property)的某一个方法,那么就得像下面这样写:

class SubPerson(Person):
    @Person.name.getter
    def name(self):
        print('Getting name')
        return super().name           

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

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

下面解释一下这种用法。

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

在第一个例子中,所有的特性(property)方法都被重新定义。在每一个方法中,使用了 super() 来调用父类的实现。在 setter 函数中使用 super(SubPerson, SubPerson).name.__set__(self, value) 的语句是没有错的。为了委托(delegate)给父类中实现的 setter ,需要将控制权传递给父类定义的 name 特性的 __set__() 方法。不过,name 特性是父类中定义的方法集合,只能以类变量(class variable)而不是实例变量(instance variable)的方式访问到其中的 __set__() 方法,这也是为什么我们要使用 super(SubPerson, SubPerson) 操作的原因(返回 SubPerson 在它的 MRO 列表中的下一个类对象)。

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

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

如果你试着运行,会发现父类中定义的 setter 函数整个消失了:

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

你应该像下面这样修改代码:

class SubPerson(Person):
    @Person.name.getter
    def name(self):
        print('Getting name')
        return super().name           

这样写后,name 特性之前在 Person 类中定义过的所有方法会被复制过来,紧接着其中的 name.getter 函数会被替换。然后它就能按照期望的方式工作了:

>>> s = SubPerson('Guido')
>>> s.name
Getting name
'Guido'
>>> s.name = 'Larry'
>>> s.name
Getting name
'Larry'
>>> s.name = 42
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "example.py", line 16, in name
        raise TypeError('Expected a string')
TypeError: Expected a string
>>>           

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

值得注意的是上面演示的第一种技术还可以被用来扩展一个描述符(descriptor)。比如:

# A descriptor
class String:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, cls):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        instance.__dict__[self.name] = value

# A class with a descriptor
class Person:
    name = String('name')

    def __init__(self, name):
        self.name = name

# Extending a descriptor with a property
class SubPerson(Person):
    @property
    def name(self):
        print('Getting name')
        return super().name

    @name.setter
    def name(self, value):
        print('Setting name to', value)
        super(SubPerson, SubPerson).name.__set__(self, value)

    @name.deleter
    def name(self):
        print('Deleting name')
        super(SubPerson, SubPerson).name.__delete__(self)           

最后值得注意的是,读到这里时,你或许会发现子类化 setter 和 deleter 方法已经做了些许简化了。这里演示的解决方案同样适用,但是在  Python 的 issue 页面报告的一个 bug,或许会使得将来的 Python 版本中出现一个更加简洁的方法。