使用property為類中的資料新增行為

weixin_34104341發表於2020-04-07

對於物件導向程式設計特別重要的是,關注行為和資料的分離

在這之前,先來討論一些“壞”的物件導向理論,這些都告訴我們絕不要直接訪問屬性(如Java):

class Color:
    def __init__(self, rgb_value, name):
        self._rgb_value = rgb_value
        self._name = name

    def set_name(self, name):
        self._name = name

    def get_name(self):
        return self._name

字首有一個單下劃線的變數表明他們是類私有的,接著get和set方法提供了對每個變數的訪問方式,這個類在實際使用中一般採用如下的方式:

>>> c = Color('#ff0000', 'bright red')
>>> c.get_name()
'bright red'
>>> c.set_name('red')
>>> c.get_name()
'red'

這並不像python喜歡的直接訪問方式具有可讀性:

class Color:
    def __init__(self, rgb_value, name):
        self.rgb_value = rgb_value
        self.name = name

呼叫如下:

>>> c = Color('#ff0000', 'bright red')
>>> print(c.name)
bright red
>>> c.name = "red"
>>> print(c.name)
red

Java的這種方式方便在需要這些變數被賦值時新增額外的程式碼,例如我們想要驗證輸入值是否合理,則可以改變set_name()方法來實現:

def set_name(self, name):
    if not name:
        raise Expception("Invalid Name")
    self._name = name

 但是這樣會有一個問題,採用直接訪問屬性方法的程式碼,現在必須通過呼叫方法才能訪問原有的屬性,如果他們不改變自己的訪問方式,那麼程式碼就被破壞了。

而在python中可以使用property關鍵字來處理該問題,加入我們原本使用直接成員訪問的方法取訪問屬性,之後我們可以增加幾個方法,在不改變訪問介面的情況下,來對name這個變數進行取值和賦值。

class Color:
    def __init__(self, rgb_value, name):
        self.rgb_value = rgb_value
        self._name = name

    def _set_name(self, name):
        if not name:
            raise Exception("Invalid Name")
        self._name = name

    def _get_name(self):
        return self._name

    name = property(_get_name, _set_name)

先將name這個屬性改為一個(半)私有的_name屬性,接著我們新增兩個(半)私有方法對這個變數進行取值和賦值,並在賦值的時候進行驗證。最後我們在程式碼底部使用property關鍵字進行宣告。

現在Color類擁有了一個全新的name屬性,這個name屬性變為了一個property屬性,需要通過呼叫我們剛剛新增的兩個方法才能訪問或者改變其值而Color類仍能以前一個版本中相同的方式來使用,同時它還能支援對name賦值時進行驗證

>>> c = Color('#ff0000', 'bright red')
>>> print(c.name)
bright red
>>> c.name = 'red'
>>> print(c.name)
red
>>> c.name =""
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "color2.py", line 8, in _set_name
    raise Exception("Invalid Name")
Exception: Invalid Name

這樣,我們之前編寫的任何程式碼仍然能夠工作,但是,即便name變成了property屬性,也不能保證100%的安全,如果有人使它設定為空字串值,仍然可以通過直接訪問_name屬性的方式來達到目的。

 property是怎樣工作的

property函式實際上返回了一個物件,該物件通過我們指定的方法代理了全部對屬性值訪問或賦值的請求。

property建構函式實際上還可以接受兩個額外的引數:一個刪除函式和一個property的文字字串。在實際中很少用到刪除函式,但是如果需要用到記錄所刪除的值,那麼刪除函式還是很有用的。同時在我們滿足某個條件的情況下,刪除函式還可以否決刪除操作。文字字串是一個用來描述該property的字串。如果我們不提供文字字串這個引數,那麼該值將從property的第一個引數,也就是getter方法的文字字串中複製過來。

這裡有個例子,說明了什麼時候哪個方法被呼叫了:

class Silly:
      def _get_silly(self):
          print("You are getting silly")
          return self._silly
      def _set_silly(self, value):
          print("You are making silly {}".format(value))
          self._silly = value
      def _del_silly(self):
          print("Whoah, you killed silly!")
          del self._silly
      silly = property(_get_silly, _set_silly,
              _del_silly, "This is a silly property")

呼叫如下:

>>> s = Silly()
>>> s.silly = "funny"
You are making silly funny
>>> s.silly
You are getting silly
'funny'
>>> del s.silly
Whoah, you killed silly!

實際上,property通常只定義兩個函式就可以了:getter函式和setter函式。

建立property的另一種方法

property函式本身也可以使用裝飾器語法來使一個get函式變成property函式的引數。使用裝飾器只需要為函式名新增一個@符號作為字首,並把結果放在被裝飾函式的定義之前就可以了。

class Foo:
    @property
    def foo(self):
        return "bar"

上面的用法是property成為了一個裝飾器,這相當於foo = property(foo)。我們還可以為這個property指定一個setter函式:

class Foo:
    @property
    def foo(self):
        return self._foo

    @foo.setter
    def foo(self, value):  # 與上面的函式名是一樣的
        self._foo = value

首先裝飾了foo方式,使得它成為getter。接著用剛裝飾過的foo方式的setter屬性又裝飾一個新方法,這個新方法的名字和剛裝飾過的foo方法竟然是一樣的。property函式返回的是一個物件,而這個物件被自動設定為擁有一個setter屬性,而這個setter可以設定為裝飾器去裝飾其他的函式。

 何時該使用property屬性

python中資料、property屬性、方法都是類的屬性。方法只是一個可呼叫的屬性(相當於動詞),property屬性也只是一個能幫助我們進行決策的自定義屬性。 資料屬性和property屬性應該都是名詞,資料屬性和property屬性之間唯一的區別,就是當property屬性被檢索、賦值或者刪除的時候,我們可以自動呼叫一些自定義的動作

假如有個定製化行為的普遍需求,它要求對那些難以計算或者查詢起來花費多大的值(例如一個網路請求或者資料庫查詢)進行快取。我們的目的是本地儲存這個值以便面重複呼叫那些花費過大的計算。我們可以通過在property屬性中使用自定義的getter來達到這個目的。當該值第一次被檢索的時候,我們執行查詢或計算。接著就可以將這個值以物件中的私有屬性的形式快取在本地。之後,當再次請求這個值時,我們就可以返回快取的資料。

from urllib.request import urlopen
class WebPage:

    def __init__(self, url):
        self.url = url
        self._content = None
        
    @property
    def content(self):
        if not self._content:
            print("Retrieving New Page...")
            self._content = urlopen(self.url).read()
        return self._content

我們可以測試這段程式碼,看看頁面是不是隻被檢索了一次:

>>> import time
>>> webpage = WebPage("http://ccphillips.net/")
>>> now = time.time()
>>> content1 = webpage.content
Retrieving New Page...
>>> time.time() - now
14.74434518814087
>>> now = time.time()
>>> content2 = webpage.content
>>> time.time() - now
2.50469708442688
>>> content2 == content1
True

第一次載入頁面內容花費了14s,第二次花費了2s,這只是將文字寫入直譯器的時間。

自定義的getter對於需要依據物件中其他成員進行就按的屬性,也是非常有幫助的。例如,要計算一個整數列表中各元素的平均值:

class AverageList(list):
    @property
    def average(self):
        return sum(self) / len(self)

它整合自list,我們能夠輕易獲得類列表的行為。通過在類中加入一個property屬性,很快我們的列表就可以得到一個平均值屬性:

>>> a = AverageList([1,2,3,4])
>>> a.average
2.5

 

參考:

1、《Python3 物件導向程式設計》 [加]Dusty Philips 著

轉載於:https://www.cnblogs.com/anovana/p/8257728.html

相關文章