本文講解了 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 的方法吧。
參考
(本文完)