8.6 創(chuàng)建可管理的屬性

2018-02-24 15:26 更新

問題

你想給某個(gè)實(shí)例attribute增加除訪問與修改之外的其他處理邏輯,比如類型檢查或合法性驗(yàn)證。

解決方案

自定義某個(gè)屬性的一種簡單方法是將它定義為一個(gè)property。例如,下面的代碼定義了一個(gè)property,增加對(duì)一個(gè)屬性簡單的類型檢查:

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

    # Getter function
    @property
    def first_name(self):
        return self._first_name

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

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

上述代碼中有三個(gè)相關(guān)聯(lián)的方法,這三個(gè)方法的名字都必須一樣。第一個(gè)方法是一個(gè) getter 函數(shù),它使得 first_name 成為一個(gè)屬性。其他兩個(gè)方法給 first_name 屬性添加了 setterdeleter 函數(shù)。需要強(qiáng)調(diào)的是只有在 first_name 屬性被創(chuàng)建后,后面的兩個(gè)裝飾器 @first_name.setter@first_name.deleter 才能被定義。

property的一個(gè)關(guān)鍵特征是它看上去跟普通的attribute沒什么兩樣,但是訪問它的時(shí)候會(huì)自動(dòng)觸發(fā) getter 、setterdeleter 方法。例如:

>>> a = Person('Guido')
>>> a.first_name # Calls the getter
'Guido'
>>> a.first_name = 42 # Calls the setter
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "prop.py", line 14, in first_name
        raise TypeError('Expected a string')
TypeError: Expected a string
>>> del a.first_name
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
AttributeError: can't delete attribute
>>>

在實(shí)現(xiàn)一個(gè)property的時(shí)候,底層數(shù)據(jù)(如果有的話)仍然需要存儲(chǔ)在某個(gè)地方。因此,在get和set方法中,你會(huì)看到對(duì) _first_name``屬性的操作,這也是實(shí)際數(shù)據(jù)保存的地方。另外,你可能還會(huì)問為什么 ``__init__() 方法中設(shè)置了 self.first_name 而不是 self._first_name 。在這個(gè)例子中,我們創(chuàng)建一個(gè)property的目的就是在設(shè)置attribute的時(shí)候進(jìn)行檢查。因此,你可能想在初始化的時(shí)候也進(jìn)行這種類型檢查。通過設(shè)置 self.first_name ,自動(dòng)調(diào)用 setter 方法,這個(gè)方法里面會(huì)進(jìn)行參數(shù)的檢查,否則就是直接訪問 self._first_name 了。

還能在已存在的get和set方法基礎(chǔ)上定義property。例如:

class Person:
    def __init__(self, first_name):
        self.set_first_name(first_name)

    # Getter function
    def get_first_name(self):
        return self._first_name

    # Setter function
    def set_first_name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._first_name = value

    # Deleter function (optional)
    def del_first_name(self):
        raise AttributeError("Can't delete attribute")

    # Make a property from existing get/set methods
    name = property(get_first_name, set_first_name, del_first_name)

討論

一個(gè)property屬性其實(shí)就是一系列相關(guān)綁定方法的集合。如果你去查看擁有property的類,就會(huì)發(fā)現(xiàn)property本身的fget、fset和fdel屬性就是類里面的普通方法。比如:

>>> Person.first_name.fget
<function Person.first_name at 0x1006a60e0>
>>> Person.first_name.fset
<function Person.first_name at 0x1006a6170>
>>> Person.first_name.fdel
<function Person.first_name at 0x1006a62e0>
>>>

通常來講,你不會(huì)直接取調(diào)用fget或者fset,它們會(huì)在訪問property的時(shí)候自動(dòng)被觸發(fā)。

只有當(dāng)你確實(shí)需要對(duì)attribute執(zhí)行其他額外的操作的時(shí)候才應(yīng)該使用到property。有時(shí)候一些從其他編程語言(比如Java)過來的程序員總認(rèn)為所有訪問都應(yīng)該通過getter和setter,所以他們認(rèn)為代碼應(yīng)該像下面這樣寫:

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

    @property
    def first_name(self):
        return self._first_name

    @first_name.setter
    def first_name(self, value):
        self._first_name = value

不要寫這種沒有做任何其他額外操作的property。首先,它會(huì)讓你的代碼變得很臃腫,并且還會(huì)迷惑閱讀者。其次,它還會(huì)讓你的程序運(yùn)行起來變慢很多。最后,這樣的設(shè)計(jì)并沒有帶來任何的好處。特別是當(dāng)你以后想給普通attribute訪問添加額外的處理邏輯的時(shí)候,你可以將它變成一個(gè)property而無需改變原來的代碼。因?yàn)樵L問attribute的代碼還是保持原樣。

Properties還是一種定義動(dòng)態(tài)計(jì)算attribute的方法。這種類型的attributes并不會(huì)被實(shí)際的存儲(chǔ),而是在需要的時(shí)候計(jì)算出來。比如:

import math
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return math.pi * self.radius ** 2

    @property
    def diameter(self):
        return self.radius ** 2

    @property
    def perimeter(self):
        return 2 * math.pi * self.radius

在這里,我們通過使用properties,將所有的訪問接口形式統(tǒng)一起來,對(duì)半徑、直徑、周長和面積的訪問都是通過屬性訪問,就跟訪問簡單的attribute是一樣的。如果不這樣做的話,那么就要在代碼中混合使用簡單屬性訪問和方法調(diào)用。下面是使用的實(shí)例:

>>> c = Circle(4.0)
>>> c.radius
4.0
>>> c.area  # Notice lack of ()
50.26548245743669
>>> c.perimeter  # Notice lack of ()
25.132741228718345
>>>

盡管properties可以實(shí)現(xiàn)優(yōu)雅的編程接口,但有些時(shí)候你還是會(huì)想直接使用getter和setter函數(shù)。例如:

>>> p = Person('Guido')
>>> p.get_first_name()
'Guido'
>>> p.set_first_name('Larry')
>>>

這種情況的出現(xiàn)通常是因?yàn)镻ython代碼被集成到一個(gè)大型基礎(chǔ)平臺(tái)架構(gòu)或程序中。例如,有可能是一個(gè)Python類準(zhǔn)備加入到一個(gè)基于遠(yuǎn)程過程調(diào)用的大型分布式系統(tǒng)中。這種情況下,直接使用get/set方法(普通方法調(diào)用)而不是property或許會(huì)更容易兼容。

最后一點(diǎn),不要像下面這樣寫有大量重復(fù)代碼的property定義:

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

    @property
    def first_name(self):
        return self._first_name

    @first_name.setter
    def first_name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._first_name = value

    # Repeated property code, but for a different name (bad!)
    @property
    def last_name(self):
        return self._last_name

    @last_name.setter
    def last_name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._last_name = value

重復(fù)代碼會(huì)導(dǎo)致臃腫、易出錯(cuò)和丑陋的程序。好消息是,通過使用裝飾器或閉包,有很多種更好的方法來完成同樣的事情??梢詤⒖?.9和9.21小節(jié)的內(nèi)容。

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)