一、@property 簡單實例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
二、保存溫度實例
property 它使得面向對象的編程更加簡單。在詳細解釋和深入瞭解Python中的property之前,讓我們先了解爲什麼我們需要用到property?
假設有天你決定創建一個類,用來存儲攝氏溫度。當然這個類也需要實現一個將攝氏溫度轉換爲華氏溫度的方法。一種實現的方式如下:
1 2 3 4 5 |
class Celsius: def __init__(self, temperature = 0): self.temperature = temperature def to_fahrenheit(self): return (self.temperature * 1.8) + 32 |
我們可以用這個類產生一個對象,然後按照我們期望的方式改變該對象的溫度屬性:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
>>> # create new object >>> man = Celsius()
>>> # set temperature >>> man.temperature = 37
>>> # get temperature >>> man.temperature 37
>>> # get degrees Fahrenheit >>> man.to_fahrenheit() 98.60000000000001 |
這裏額外的小數部分是轉換成華氏溫度時由於浮點運算誤差造成的(你可以在Python解釋器中試試1.1 + 2.2)。每當我們賦值或獲取任何對象的屬性時,例如上面展示的溫度,Python都會從對象的__dict__
字典中搜索它。
1 2 |
>>> man.__dict__ {'temperature': 37} |
因此,man.temperature在其內部就變成了man.__dict__['temperature']
現在,讓我們進一步假設我們的類在客戶中很受歡迎,他們開始在其程序中使用這個類。他們對該類生成的對象做了各種操作。有一天,一個受信任的客戶來找我們,建議溫度不能低於-273攝氏度(熱力學的同學可能會提出異議,它實際上是-273.15),也被稱爲絕對零。客戶進一步要求我們實現這個值約束。作爲一個以爭取客戶滿意度爲己任的公司,我們很高興地聽從了建議,發佈了1.01版本,升級了我們現有的類。
三、使用 Property 中的 Getters和Setters
對於上邊的約束,一個很容易想到的解決方案是隱藏其溫度屬性(使其私有化),並且定義新的用於操作溫度屬性的getter和setter接口。可以這麼實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Celsius: def __init__(self, temperature = 0): self.set_temperature(temperature)
def to_fahrenheit(self): return (self.get_temperature() * 1.8) + 32
# new update 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 |
從上邊可以看出,我們定義了兩個新方法get_temperature()
和set_temperature()
,此外屬性temperature也被替換爲了_temperature
。最前邊的下劃線(_)用於指示Python中的私有變量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
>>> c = Celsius(-277) Traceback (most recent call last): ... ValueError: Temperature below -273 is not possible
>>> c = Celsius(37) >>> c.get_temperature() 37 >>> c.set_temperature(10)
>>> c.set_temperature(-300) Traceback (most recent call last): ... ValueError: Temperature below -273 is not possible |
這個更新成功地實現了新約束,我們不再允許設置溫度低於-273度。請注意,Python中實際上是沒有私有變量的。有一些簡單的被遵循的規範。Python本身不會應用任何限制。
1 2 3 |
>>> c._temperature = -300 >>> c.get_temperature() -300 |
上述更新的最大問題是,所有在程序中使用了我們先前類的客戶都必須更改他們的代碼:
obj.temperature改爲obj.get_temperature(),所有的賦值語句也必須更改,比如obj.temperature = val改爲obj.set_temperature(val);這樣的重構會給那些擁有成千上萬行代碼的客戶帶來很大的麻煩。總而言之,我們的更新是不向後兼容地,這就是需要property的地方。
四、Property的作用
對於上邊的問題,Python式的解決方式是使用property。這裏是我們已經實現了的一個版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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) |
我們在get_temperature()
和set_temperature()
的內部增加了一個print()函數,用來清楚地觀察它們是否正在執行。
代碼的最後一行,創建了一個property對象temperature:
簡單地說,property將一些代碼(get_temperature
和set_temperature
)附加到成員屬性(temperature)的訪問入口;任何獲取temperature值的代碼都會自動調用get_temperature()
,而不是去字典表(__dict__
)中進行查找。同樣的,任何賦給temperature值的代碼也會自動調用set_temperature()
。實際演示一下。
1 2 |
>>> c = Celsius() Setting value |
從上邊的代碼中我們可以看到,即使當我們創建一個對象時,set_temperature()
也會被調用,這是爲什麼呢?
原因是,當一個對象被實例化時,__init__()
方法被自動執行;該方法有一行代碼self.temperature = temperature;這個任務會自動調用set_temperature()
方法。
1 2 3 |
>>> c.temperature Getting value 0 |
同樣的,對於屬性的任何訪問,例如c.temperature,也會自動調用get_temperature()
方法。這就是property所作的事情。這裏有一些額外的實例。
1 2 3 4 5 6 |
>>> c.temperature = 37 Setting value
>>> c.to_fahrenheit() Getting value 98.60000000000001 |
我們可以看到,通過使用property,我們在不需要客戶代碼做任何修改的情況下,修改了我們的類,並實現了值約束。因此我們的實現是向後兼容的,這樣的結果,大家都很高興。
最後需要注意的是,實際溫度值存儲在私有變量_temperature
中,屬性temperature是一個property對象,是用來爲這個私有變量提供接口的。
五、深入挖掘property
在Python中,property()是一個內置函數,用於創建和返回一個property對象。該函數的簽名爲:
1 |
property(fget=None, fset=None, fdel=None, doc=None) |
fget是一個獲取屬性值的函數,fset是一個設置屬性值的函數,fdel是一個刪除屬性的函數,doc是一個字符串(類似於註釋)。從函數實現上看,這些函數參數都是可選的。所以,可以按照如下的方式簡單的創建一個property對象。
1 2 |
>>> property() <property object at 0x0000000003239B38> |
Property對象有三個方法,getter()、setter()和delete(),用來在對象創建後設置fget、fset和fdel。這就意味着,這行代碼:temperature = property(get_temperature,set_temperature) 可以被分解爲:
1 2 3 4 5 6 |
# make empty property temperature = property() # assign fget temperature = temperature.getter(get_temperature) # assign fset temperature = temperature.setter(set_temperature) |
它們之間是相互等價的。
熟悉Python中裝飾器decorator的程序員能夠認識到上述結構可以作爲decorator實現。我們可以更進一步,不去定義名字get_temperature和set_temperature,因爲他們不是必須的,並且污染類的命名空間。爲此,我們在定義getter函數和setter函數時重用名字temperature。下邊的代碼展示如何實現它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Celsius: def __init__(self, temperature = 0): self._temperature = temperature
def to_fahrenheit(self): return (self.temperature * 1.8) + 32 # 獲取 @property def temperature(self): print("Getting value") return self._temperature # 設置 @temperature.setter def temperature(self, value): if value < -273: raise ValueError("Temperature below -273 is not possible") print("Setting value") self._temperature = value |
上邊的兩種生成property的實現方式,都很簡單,推薦使用裝飾器版本,在Python尋找property時,你很可能會遇到這種類似的代碼結構。