天天看點

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)

    @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) 的語句是沒有錯的。為了委托給之前定義的 setter 方 法,需要将控制權傳遞給之前定義的 name 屬性的 __set__() 方法。不過,擷取這個方法的唯一途徑是使用類變量而不是執行個體變量來通路它。這也是為什麼我們要使用 super(SubPerson, SubPerson) 的原因。

如果你隻想重定義其中一個方法,那隻使@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           

這麼寫後,property 之前已經定義過的方法會被複制過來,而 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 類名。如果你不知道到底是哪個基類定義了 property,那你隻能通過重新定義 所有 property 并使用 super() 來将控制權傳遞給前面的實作。 值的注意的是上面示範的第一種技術還可以被用來擴充一個描述器。比如:

# 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)