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.