1. 程式人生 > >Python 中的 property 屬性

Python 中的 property 屬性

Python中有個很讚的概念,叫做property,它使得面向物件的程式設計更加簡單。在詳細解釋和深入瞭解Python中的property之前,讓我們首先建立這樣一個直覺:為什麼我們需要用到property?

從一個例項開始

假設有天你決定建立一個類,用來儲存攝氏溫度。當然這個類也需要實現一個將攝氏溫度轉換為華氏溫度的方法。一種實現的方式如下:

class Celsius:
  def __init__(self, temperature = 0):
      self.temperature = temperature
  def to_fahrenheit(self):
      return
(self.temperature * 1.8) + 32

我們可以用這個類產生一個物件,然後按照我們期望的方式改變該物件的溫度屬性:

>>> # 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字典中搜索它。

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

因此,man.temperature在其內部就變成了man.dict[‘temperature’]

現在,讓我們進一步假設我們的類在客戶中很受歡迎,他們開始在其程式中使用這個類。他們對該類生成的物件做了各種操作。有一天,一個受信任的客戶來找我們,建議溫度不能低於-273攝氏度(熱力學的同學可能會提出異議,它實際上是-273.15),也被稱為絕對零。客戶進一步要求我們實現這個值約束。作為一個以爭取客戶滿意度為己任的公司,我們很高興地聽從了建議,釋出了1.01版本,升級了我們現有的類。

使用Getters和Setters

對於上邊的約束,一個很容易想到的解決方案是隱藏其溫度屬性(使其私有化),並且定義新的用於操作溫度屬性的getter和setter介面。可以這麼實現:

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

這個更新成功地實現了新約束,我們不再允許設定溫度低於-273度。

請注意,Python中實際上是沒有私有變數的。有一些簡單的被遵循的規範。Python本身不會應用任何限制。

>>> c._temperature = -300
>>> c.get_temperature()
-300

但這樣並不會讓人很放心。上述更新的最大問題是,所有在他們的程式中使用了我們先前類的客戶都必須更改他們的程式碼:obj.temperature改為obj.get_temperature(),所有的賦值語句也必須更改,比如obj.temperature = val改為obj.set_temperature(val)。這樣的重構會給那些擁有成千上萬行程式碼的客戶帶來很大的麻煩。

總而言之,我們的更新是不向後相容地。這就是需要property閃亮登場的地方。

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)

我們在get_temperature()和set_temperature()的內部增加了一個print()函式,用來清楚地觀察它們是否正在執行。程式碼的最後一行,建立了一個property物件temperature。簡單地說,property將一些程式碼(get_temperature和set_temperature)附加到成員屬性(temperature)的訪問入口。任何獲取temperature值的程式碼都會自動呼叫get_temperature(),而不是去字典表(dict)中進行查詢。同樣的,任何賦給temperature值的程式碼也會自動呼叫set_temperature()。這是Python中一個很酷的功能。我們實際演示一下。

>>> c = Celsius()
Setting value

從上邊的程式碼中我們可以看到,即使當我們建立一個物件時,set_temperature()也會被呼叫。你能猜到為什麼嗎?原因是,當一個物件被建立時,init()方法被呼叫。該方法有一行程式碼self.temperature = temperature。這個任務會自動呼叫set_temperature()方法。

>>> c.temperature
Getting value
0

同樣的,對於屬性的任何訪問,例如c.temperature,也會自動呼叫get_temperature()方法。這就是property所作的事情。這裡有一些額外的例項。

>>> c.temperature = 37
Setting value

>>> c.to_fahrenheit()
Getting value
98.60000000000001

我們可以看到,通過使用property,我們在不需要客戶程式碼做任何修改的情況下,修改了我們的類,並實現了值約束。因此我們的實現是向後相容的,這樣的結果,大家都很高興。

最後需要注意的是,實際溫度值儲存在私有變數_temperature中。屬性temperature是一個property物件,是用來為這個私有變數提供介面的。

深入挖掘property

在Python中,property()是一個內建函式,用於建立和返回一個property物件。該函式的簽名為:

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

這裡,fget是一個獲取屬性值的函式,fset是一個設定屬性值的函式,fdel是一個刪除屬性的函式,doc是一個字串(類似於註釋)。從函式實現上看,這些函式引數都是可選的。所以,可以按照如下的方式簡單的建立一個property物件。

>>> property()
<property object at 0x0000000003239B38>

Property物件有三個方法,getter(), setter()和delete(),用來在物件建立後設置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的程式設計師能夠認識到上述結構可以作為decorator實現。我們可以更進一步,不去定義名字get_temperature和set_temperature,因為他們不是必須的,並且汙染類的名稱空間。為此,我們在定義getter函式和setter函式時重用名字temperature。下邊的程式碼展示如何實現它。

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時,你很可能會遇到這種類似的程式碼結構。