Python進階之“property(屬性)”詳解

一、@property 簡單實例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

class Rectangle(object):

  @property

  def width(self):

    #變量名不與方法名重複,改爲true_width,下同

    return self.true_width

 

  @property

  def height(self):

    return self.true_height

s = Rectangle()

#與方法名一致

s.width = 1024

s.height = 768

print(s.width,s.height)

二、保存溫度實例

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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章