Python @property 詳解

本文講解了 Python 的 property 特性,即一種符合 Python 哲學地設置 getter 和 setter 的方式。

Python 有一個概念叫做 property,它能讓你在 Python 的面向對象編程中輕鬆不少。在瞭解它之前,我們先看一下爲什麼 property 會被提出。

一個簡單的例子

比如說你要創建一個溫度的類Celsius,它能存儲攝氏度,也能轉換爲華氏度。即:

class Celsius:
    def __init__(self, temperature = 0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

我們可以使用這個類:

>>> # 創建對象 man
>>> man = Celsius()

>>> # 設置溫度
>>> man.temperature = 37

>>> # 獲取溫度
>>> man.temperature
37

>>> # 獲取華氏度
>>> man.to_fahrenheit()
98.60000000000001

最後額外的小數部分是浮點誤差,屬於正常現象,你可以在 Python 裏試一下 1.1 + 2.2

在 Python 裏,當我們對一個對象的屬性進行賦值或估值時(如上面的temperature),Python 實際上是在這個對象的 __dict__字典裏搜索這個屬性來操作。

>>> man.__dict__
{'temperature': 37}

因此,man.temperature實際上被轉換成了man.__dict__['temperature']

假設我們這個類被程序員廣泛的應用了,他們在數以千計的客戶端代碼裏使用了我們的類,你很高興。

突然有一天,有個人跑過來說,溫度不可能低於零下273度,這個類應該加上對溫度的限制。這個建議當然應該被採納。作爲一名經驗豐富的程序員,你立刻想到應該使用 setter 和 getter 來限制溫度,於是你將代碼改成下面這樣:

class Celsius:
    def __init__(self, temperature = 0):
        self.set_temperature(temperature)

    def to_fahrenheit(self):
        return (self.get_temperature() * 1.8) + 32

    # 更新部分
    def get_temperature(self):
        return self._temperature

    def set_temperature(self, value):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        self._temperature = value

很自然地,你使用了“私有變量”_temperature來存儲溫度,使用get_temperature()set_temperature()提供了訪問_temperature的接口,在這個過程中對溫度值進行條件判斷,防止它超過限制。這都很好。

問題是,這樣一來,使用你的類的程序員們需要把他們的代碼中無數個obj.temperature = val改爲obj.set_temperature(val),把obj.temperature改爲obj.get_temperature()。這種重構實在令人頭痛。

所以,這種方法不是“向下兼容”的,我們要另闢蹊徑。

@property 的威力!

想要使用 Python 哲學來解決這個問題,就使用 property。直接看代碼:

class Celsius:
    def __init__(self, temperature = 0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

    def get_temperature(self):
        print("Getting value")
        return self._temperature

    def set_temperature(self, value):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        print("Setting value")
        self._temperature = value

    # 重點在這裏
    temperature = property(get_temperature,set_temperature)

我們在class Celsius的最後一行使用了一個 Python 內置函數(類) property()。它接受兩個函數作爲參數,一個 getter,一個 setter,並且返回一個 property 對象(這裏是temperature)。

這樣以後,任何訪問temperature的代碼都會自動轉而運行get_temperature(),任何對temperature賦值的代碼都會自動轉而運行set_temperature()我們在代碼里加了print()便於測試它們的運行狀態。

>>> c = Celsius()  # 此時會運行 setter,因爲 __init__ 裏對 temperature 進行了賦值
Setting value

>>> c.temperature  # 此時會運行 getter,因爲對 temperature 進行了訪問
Getting value
0

需要注意的是,實際的溫度存儲在_temperature裏,temperature只是提供一個訪問的接口。

深入瞭解 Property

正如之前提到的,property()是 Python 的一個內置函數,同時它也是一個類。函數簽名爲:

property(fget=None, fset=None, fdel=None, doc=None)

其中,fget是一個 getter 函數,fset是一個 setter 函數,fdel是刪除該屬性的函數,doc是一個字符串,用作註釋。函數返回一個 property 對象。

一個 property 對象有 getter()setter()deleter()三個方法用來指定相應綁定的函數。之前的

temperature = property(get_temperature,set_temperature)

實際上等價於

# 創建一個空的 property 對象
temperature = property()
# 綁定 getter
temperature = temperature.getter(get_temperature)
# 綁定 setter
temperature = temperature.setter(set_temperature)

這兩個代碼塊等價。

熟悉 Python 裝飾器的程序員肯定已經想到,上面的 property 可以用裝飾器來實現。

通過裝飾器@property,我們可以不定義沒有必要的 get_temperature()set_temperature(),這樣還避免了污染命名空間。使用方式如下:

class Celsius:
    def __init__(self, temperature = 0):
        self._temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

    # Getter 裝飾器
    @property
    def temperature(self):
        print("Getting value")
        return self._temperature

    # Setter 裝飾器
    @temperature.setter
    def temperature(self, value):
        if value < -273:
            raise ValueError("Temperature below -273 is not possible")
        print("Setting value")
        self._temperature = value

你可以使用裝飾器,也可以使用之前的方法,完全看個人喜好。但使用裝飾器應該是更加 Pythonic 的方法吧。

參考

Python @property

(本文完)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章