Python黑魔法系列之使用@property或__getattr__創建動態類屬性


Python黑魔法系列之創建動態類屬性

[2]博客文章 python python黑魔法

在面向對象編程中,我們一般把名詞性的東西映射成屬性,動詞性的東西映射成方法。在python中他們對應的分別是屬性self.xxx和類方法。但有時我們需要的屬性需要根據其他屬性動態的計算,此時如果直接使用屬性方法處理,會導致數據不同步。本文介紹了使用__getattr__@property兩種方法來動態創建類屬性。

問題

假定我們現在有一個矩形類,有以下屬性和方法:

  • 屬性:長,寬
  • 方法:平移,旋轉

這是一個很常規的類,我們很容易寫出代碼如下:

class Rectangle(object):
    def __init__(self, width=0, height=0):
        self.width = width
        self.height = height
    def rotate(self):
        pass
    def move(self):
        pass

如果此時我們需要添加一個屬性面積,那麼該怎麼做呢?

添加self.area屬性

最直觀的想法是在__init__中添加self.area屬性。但如果這樣,會導致什麼問題呢?我們知道面積等於長乘以寬,因此它雖然是一個屬性,但並不同於之前的長和寬,它是根據長和寬動態計算的,依賴於長和寬。如果直接設置其爲屬性,那麼我們需要在初始化和每一次改變長和寬時更新其值,並且需要防止用戶直接對它進行賦值。

定義類方法def area(self)

既然面積是根據長和寬進行計算的,那麼我們可以定義類方法,執行該方法時求面積:

class Rectangle(object):
    def __init__(self, width=0, height=0):
        self.width = width
        self.height = height
    def rotate(self):
        pass
    def move(self):
        pass
    def area(self):
        return self.height*self.width

我們來測試一下:

In [5]: ra = Rectangle(3,4)
In [6]: ra.area()
Out[6]: 12

沒有問題,但是這樣看起來很醜陋,因爲它的直觀感受應該是屬性,但我們卻得用方法來求。那麼有沒有什麼辦法可以使其看起來像屬性呢?

@property裝飾器

裝飾器是python中一個很有用的特性,它可以通過在函數前一行指定@decorator來使函數執行額外的操作。關於裝飾器更多的用法請參見此處
此處的@property是python內置的一種裝飾器,其功能是使方法看起來像是屬性。在方法area前添加該語句,可以像調用屬性那樣調用該方法。

class Rectangle(object):
    def __init__(self, width=0, height=0):
        self.width = width
        self.height = height
    def rotate(self):
        pass
    def move(self):
        pass
    @property
    def area(self):
        return self.height*self.width

我們來測試一下:

In [11]: ra = Rectangle(3,4)
In [12]: ra.area
Out[12]: 12
In [13]: ra.area = 20
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-13-d02891713168> in <module>()
----> 1 ra.area = 20

AttributeError: can't set attribute

從上面可以看到,我們可以成功用訪問屬性的方法得到面積,但是賦值的時候會報錯。這是符合我們理解的,因爲面積無法直接賦值,只有改變長或寬才能改變面積。

使用__getattr__方法獲取屬性

Python中前後加下劃線的方法被稱爲魔法方法(更多參見python官方文檔),此處的__getattr__方法可以提供更強大的屬性訪問方法。

class Rectangle(object):
    """docstring for Rectangle"""
    def __init__(self, width=0, height=0):
        self.width = width
        self.height = height

    def __getattr__(self, name):    # 【1】
        if name == 'area':
            return self.width*self.height
        # 【2】

【1】:如此定義後,如果我們初始化一個實例ra = Rectangle(3,4),當我們執行ra.area時,系統會先查找是否存在這個屬性,若不存在,則調用ra.__getattr__('area')。我們在其中定義返回長乘寬,故能夠返回面積。
【2】:此處對於未匹配的情況未做處理,因此程序在訪問不存在的屬性時不會返回任何值,但也不會出錯。
我們來驗證一下:

In [29]: ra = Rectangle(3,4)
In [30]: ra.area
Out[30]: 12
In [31]: ra.notexist    
# 此處沒有報錯信息
In [32]: ra.area = 100
In [33]: ra.area
Out[33]: 100            # 【3】
In [34]: ra.width
Out[34]: 3
In [35]: ra.height
Out[35]: 4
In [36]: ra.notexist = 100
In [37]: ra.notexist
Out[37]: 100            # 【3】

【3】:此處可見在定義了__getattr__後,我們可以對其進行賦值改變它的值,並且可以對不存在的值進行賦值,這些行爲可能引發程序的錯誤。所以我們還要對其賦值行爲進行顯示定義,避免這種行爲。這個可以通過定義__setattr__方法實現。

最終的完整程序:

class Rectangle(object):
    def __init__(self, width=0, height=0):
        self.width = width
        self.height = height

    def __setattr__(self, name, value):
        if name == 'area':
            raise Exception('area can not be set. Please set the area by set width and height.')
        else:
            self.__dict__[name] = value

    def __getattr__(self, name):
        if name == 'area':
            return self.width*self.height
        else:
            raise AttributeError

if __name__ == '__main__':
    rt = Rectangle(5, 6)
    print rt.area
    rt.width, rt.height = 3, 4
    print rt.area
    rt.area = 100

輸出結果爲:

30
12
---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-38-221966af6d6f> in <module>()
     21     rt.width, rt.height = 3, 4
     22     print rt.area
---> 23     rt.area = 100

<ipython-input-38-221966af6d6f> in __setattr__(self, name, value)
      6     def __setattr__(self, name, value):
      7         if name == 'area':
----> 8             raise Exception('area can not be set. Please set the area by set width and height.')
      9         else:
     10             self.__dict__[name] = value

Exception: area can not be set. Please set the area by set width and height.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章