8.8 子類中擴(kuò)展property

2018-02-24 15:26 更新

問題

在子類中,你想要擴(kuò)展定義在父類中的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并擴(kuò)展了 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
>>>

如果你僅僅只想擴(kuò)展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)

討論

在子類中擴(kuò)展一個property可能會引起很多不易察覺的問題,因為一個property其實是 getter、setterdeleter 方法的集合,而不是單個方法。因此,但你擴(kuò)展一個property的時候,你需要先確定你是否要重新定義所有的方法還是說只修改其中某一個。

在第一個例子中,所有的property方法都被重新定義。在每一個方法中,使用了 super() 來調(diào)用父類的實現(xiàn)。在 setter 函數(shù)中使用 super(SubPerson, SubPerson).name.__set__(self, value) 的語句是沒有錯的。為了委托給之前定義的setter方法,需要將控制權(quán)傳遞給之前定義的name屬性的 __set__() 方法。不過,獲取這個方法的唯一途徑是使用類變量而不是實例變量來訪問它。這也是為什么我們要使用 super(SubPerson, SubPerson) 的原因。

如果你只想重定義其中一個方法,那只使用 @property 本身是不夠的。比如,下面的代碼就無法工作:

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

如果你試著運行會發(fā)現(xiàn)setter函數(shù)整個消失了:

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

你應(yīng)該像之前說過的那樣修改代碼:

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

這么寫后,property之前已經(jīng)定義過的方法會被復(fù)制過來,而getter函數(shù)被替換。然后它就能按照期望的工作了:

>>> 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() 來將控制權(quán)傳遞給前面的實現(xiàn)。

值的注意的是上面演示的第一種技術(shù)還可以被用來擴(kuò)展一個描述器(在8.9小節(jié)我們有專門的介紹)。比如:

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

最后值的注意的是,讀到這里時,你應(yīng)該會發(fā)現(xiàn)子類化 setterdeleter 方法其實是很簡單的。這里演示的解決方案同樣適用,但是在 Python的issue頁面 報告的一個bug,或許會使得將來的Python版本中出現(xiàn)一個更加簡潔的方法。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號