從一個實例開始
假設(shè)有天你決定創(chuàng)建一個類瓢宦,用來存儲攝氏溫度刃泡。當(dāng)然這個類也需要實現(xiàn)一個將攝氏溫度轉(zhuǎn)換為華氏溫度的方法怒炸。一種實現(xiàn)的方式如下:
class Celsius:
def __init__(self, temperature = 0):
self.temperature = temperature
def to_fahrenheit(self):
return (self.temperature * 1.8) + 32
我們可以用這個類產(chǎn)生一個對象叨橱,然后按照我們期望的方式改變該對象的溫度屬性:
>>> # create new object
>>> man = Celsius()
>>> # set temperature
>>> man.temperature = 37
>>> # get temperature
>>> man.temperature
37
>>> # get degrees Fahrenheit
>>> man.to_fahrenheit()
98.60000000000001
這里額外的小數(shù)部分是轉(zhuǎn)換成華氏溫度時由于浮點運(yùn)算誤差造成的(你可以在Python解釋器中試試1.1 + 2.2)攻谁。每當(dāng)我們賦值或獲取任何對象的屬性時,例如上面展示的溫度轧叽,Python都會從對象的__dict__字典中搜索它苗沧。
>>> man.__dict__
{'temperature': 37}
因此,man.temperature在其內(nèi)部就變成了man.__dict__['temperature']
現(xiàn)在炭晒,讓我們進(jìn)一步假設(shè)我們的類在客戶中很受歡迎待逞,他們開始在其程序中使用這個類。他們對該類生成的對象做了各種操作网严。有一天识樱,一個受信任的客戶來找我們,建議溫度不能低于-273攝氏度(熱力學(xué)的同學(xué)可能會提出異議屿笼,它實際上是-273.15)牺荠,也被稱為絕對零翁巍÷恳唬客戶進(jìn)一步要求我們實現(xiàn)這個值約束。作為一個以爭取客戶滿意度為己任的公司灶壶,我們很高興地聽從了建議肝断,發(fā)布了1.01版本,升級了我們現(xiàn)有的類驰凛。
使用Getters和Setters
對于上邊的約束胸懈,一個很容易想到的解決方案是隱藏其溫度屬性(使其私有化),并且定義新的用于操作溫度屬性的getter和setter接口恰响∪で可以這么實現(xiàn):
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中的私有變量。
>>> 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
這個更新成功地實現(xiàn)了新約束枢劝,我們不再允許設(shè)置溫度低于-273度井联。
請注意,Python中實際上是沒有私有變量的您旁。有一些簡單的被遵循的規(guī)范烙常。Python本身不會應(yīng)用任何限制。
>>> c._temperature = -300
>>> c.get_temperature()
-300
但這樣并不會讓人很放心鹤盒。上述更新的最大問題是蚕脏,所有在他們的程序中使用了我們先前類的客戶都必須更改他們的代碼:obj.temperature改為obj.get_temperature()侦副,所有的賦值語句也必須更改,比如obj.temperature = val改為obj.set_temperature(val)驼鞭。這樣的重構(gòu)會給那些擁有成千上萬行代碼的客戶帶來很大的麻煩跃洛。
總而言之,我們的更新是不向后兼容地终议。這就是需要property閃亮登場的地方汇竭。
Property的作用
對于上邊的問題,Python式的解決方式是使用property穴张。這里是我們已經(jīng)實現(xiàn)了的一個版本:
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()的內(nèi)部增加了一個print()函數(shù)细燎,用來清楚地觀察它們是否正在執(zhí)行。代碼的最后一行皂甘,創(chuàng)建了一個property對象temperature玻驻。簡單地說,property將一些代碼(get_temperature和set_temperature)附加到成員屬性(temperature)的訪問入口偿枕。任何獲取temperature值的代碼都會自動調(diào)用get_temperature()璧瞬,而不是去字典表(dict)中進(jìn)行查找。同樣的渐夸,任何賦給temperature值的代碼也會自動調(diào)用set_temperature()嗤锉。這是Python中一個很酷的功能。我們實際演示一下墓塌。
>>> c = Celsius()
Setting value
從上邊的代碼中我們可以看到瘟忱,即使當(dāng)我們創(chuàng)建一個對象時,set_temperature()也會被調(diào)用苫幢。你能猜到為什么嗎访诱?原因是,當(dāng)一個對象被創(chuàng)建時韩肝,init()方法被調(diào)用触菜。該方法有一行代碼self.temperature = temperature。這個任務(wù)會自動調(diào)用set_temperature()方法哀峻。
>>> c.temperature
Getting value
0
同樣的涡相,對于屬性的任何訪問,例如c.temperature谜诫,也會自動調(diào)用get_temperature()方法漾峡。這就是property所作的事情。這里有一些額外的實例喻旷。
>>> c.temperature = 37
Setting value
>>> c.to_fahrenheit()
Getting value
98.60000000000001
我們可以看到生逸,通過使用property,我們在不需要客戶代碼做任何修改的情況下,修改了我們的類槽袄,并實現(xiàn)了值約束烙无。因此我們的實現(xiàn)是向后兼容的,這樣的結(jié)果遍尺,大家都很高興截酷。
最后需要注意的是,實際溫度值存儲在私有變量_temperature中乾戏。屬性temperature是一個property對象迂苛,是用來為這個私有變量提供接口的。
深入挖掘property
在Python中鼓择,property()是一個內(nèi)置函數(shù)三幻,用于創(chuàng)建和返回一個property對象。該函數(shù)的簽名為:
property(fget=None, fset=None, fdel=None, doc=None)
這里呐能,fget是一個獲取屬性值的函數(shù)念搬,fset是一個設(shè)置屬性值的函數(shù),fdel是一個刪除屬性的函數(shù)摆出,doc是一個字符串(類似于注釋)朗徊。從函數(shù)實現(xiàn)上看,這些函數(shù)參數(shù)都是可選的偎漫。所以爷恳,可以按照如下的方式簡單的創(chuàng)建一個property對象。
>>> property()
<property object at 0x0000000003239B38>
Property對象有三個方法骑丸,getter(), setter()和delete()舌仍,用來在對象創(chuàng)建后設(shè)置fget,fset和fdel通危。這就意味著,這行代碼:temperature = property(get_temperature,set_temperature)可以被分解為:
# make empty property
temperature = property()
# assign fget
temperature = temperature.getter(get_temperature)
# assign fset
temperature = temperature.setter(set_temperature)
它們之間是相互等價的灌曙。
熟悉Python中裝飾器decorator的程序員能夠認(rèn)識到上述結(jié)構(gòu)可以作為decorator實現(xiàn)菊碟。我們可以更進(jìn)一步,不去定義名字get_temperature和set_temperature在刺,因為他們不是必須的逆害,并且污染類的命名空間。為此蚣驼,我們在定義getter函數(shù)和setter函數(shù)時重用名字temperature魄幕。下邊的代碼展示如何實現(xiàn)它。
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的實現(xiàn)方式颖杏,都很簡單纯陨,推薦使用。在Python尋找property時,你很可能會遇到這種類似的代碼結(jié)構(gòu)翼抠。