Python @property 詳解

NaN不等於NaN發表於2019-02-12

本文講解了 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

(本文完)

相關文章