1. 程式人生 > 實用技巧 >Python魔法方法之屬性訪問 ( __getattr__, __getattribute__, __setattr__, __delattr__ )

Python魔法方法之屬性訪問 ( __getattr__, __getattribute__, __setattr__, __delattr__ )

Python魔法方法之屬性訪問 ( __getattr__, __getattribute__, __setattr__, __delattr__ )

通常情況下,我們在訪問類或者例項物件的時候,會牽扯到一些屬性訪問的魔法方法,主要包括:

① __getattr__(self, name): 訪問不存在的屬性時呼叫

② __getattribute__(self, name):訪問存在的屬性時呼叫(先呼叫該方法,檢視是否存在該屬性,若不存在,接著去呼叫①)

③ __setattr__(self, name, value):設定例項物件的一個新的屬性時呼叫

④ __delattr__(self, name):刪除一個例項物件的屬性時呼叫

為了驗證以上,現列出程式碼如下:

 1 class Test:
 2     def __getattr__(self, name):
 3         print('__getattr__')
 4 
 5     def __getattribute__(self, name):
 6         print('__getattribute__')
 7 
 8     def __setattr__(self, name, value):
 9         print('__setattr__')
10 
11     def __delattr__(self, name):
12 print('__delattr__') 13 14 >>> t=Test() 15 >>> t.x 16 __getattribute__

如上述程式碼所示,x並不是Test類例項t的一個屬性,首先去呼叫 __getattribute__() 方法,得知該屬性並不屬於該例項物件;但是,按照常理,t.x應該列印 __getattribute__ 和__getattr__,但實際情況並非如此,為什麼呢?難道以上Python的規定無效嗎?

不要著急,聽我慢慢道來!

例項物件屬性尋找的順序如下:

① 首先訪問 __getattribute__() 魔法方法(隱含預設呼叫,無論何種情況,均會呼叫此方法)

② 去例項物件t中查詢是否具備該屬性: t.__dict__ 中查詢,每個類和例項物件都有一個 __dict__ 的屬性

③ 若在 t.__dict__ 中找不到對應的屬性, 則去該例項的類中尋找,即 t.__class__.__dict__

④ 若在例項的類中也招不到該屬性,則去父類中尋找,即 t.__class__.__bases__.__dict__中尋找

⑤ 若以上均無法找到,則會呼叫 __getattr__ 方法,執行內部的命令(若未過載 __getattr__ 方法,則直接報錯:AttributeError)

以上幾個流程,即完成了屬性的尋找。

但是,以上的說法,並不能解釋為什麼執行 t.x 時,不列印 ’__getattr__' 啊?

你看,又急了不是,作為一名程式猿,一定要有耐心 ^_^

問題就出在了步驟的第④步,因為,一旦過載了 __getattribute__() 方法,如果找不到屬性,則必須要手動加入第④步,否則無法進入到 第⑤步 (__getattr__)的。

驗證一下以上說法是否正確:

方法一:採用object(所有類的基類)

 1 class Test:
 2     def __getattr__(self, name):
 3         print('__getattr__')
 4 
 5     def __getattribute__(self, name):
 6         print('__getattribute__')
 7         object.__getattribute__(self, name)
 8 
 9     def __setattr__(self, name, value):
10         print('__setattr__')
11 
12     def __delattr__(self, name):
13         print('__delattr__')
14 
15         
16 >>> t=Test()
17 >>> t.x
18 __getattribute__
19 __getattr__

怎麼樣,顯示出來了吧?哈哈

方法二:採用 super() 方法

 1 class Test:
 2     def __getattr__(self, name):
 3         print('__getattr__')
 4 
 5     def __getattribute__(self, name):
 6         print('__getattribute__')
 7         super().__getattribute__(name)
 8 
 9     def __setattr__(self, name, value):
10         print('__setattr__')
11 
12     def __delattr__(self, name):
13         print('__delattr__')
14 
15         
16 >>> t=Test()
17 >>> t.x
18 __getattribute__
19 __getattr__

哈哈,醬紫也可以哦 ^v^

那麼方法一和方法二有什麼不同呢?仔細看一下你就會發現,其實就是很小的一點不同而已:

1 #方法一:使用基類object的方法
2 def __getattribute__(self, name):
3     print('__getattribute__')
4     object.__getattribute__(self, name)
5 
6 #方法二:使用super()方法(有的認為super()是類,此處暫以方法處理)
7 def __getattribute__(self, name):
8         print('__getattribute__')
9         super().__getattribute__(name)

在Python2.x中,以上super的用法應該改為 super(Test, self).xxx,但3.x中,可以像上面程式碼一樣簡單使用。

那麼super到底是什麼東西呢?如果想詳細的理解,請點選這裡

哈哈,以上介紹完畢,那麼 __setattr__ 和 __delattr__ 方法相對簡單了多了:

 1 class Test:
 2     def __getattr__(self, name):
 3         print('__getattr__')
 4 
 5     def __getattribute__(self, name):
 6         print('__getattribute__')
 7         object.__getattribute__(self, name)
 8 
 9     def __setattr__(self, name, value):
10         print('__setattr__')
11 
12     def __delattr__(self, name):
13         print('__delattr__')
14 
15         
16 >>> t=Test()
17 >>> t.x=10
18 __setattr__
19 >>> del t.x
20 __delattr__

至此,關於屬性訪問,先告一段落,後續會有更高階的descirptor。

對了,再補充一點哈!

 1 class Test:
 2     def __init__(self):
 3         self.count = 0
 4     def __setattr__(self, name, value):
 5         print('__setattr__')
 6         self.count += 1
 7 
 8         
 9 >>> t=Test()
10 __setattr__
11 Traceback (most recent call last):
12   File "<pyshell#364>", line 1, in <module>
13     t=Test()
14   File "<pyshell#363>", line 3, in __init__
15     self.count = 0
16   File "<pyshell#363>", line 6, in __setattr__
17     self.count += 1
18 AttributeError: 'Test' object has no attribute 'count'

為什麼會出現上述情況呢?我還沒有呼叫__setattr__()呢,只是單純的定義了一個例項而已? @_@(咋回事?幻覺嗎)

看報錯資訊很容易明白,這是因為:

① __init__()時,給內部屬性 self.count進行了賦值;

② 賦值預設呼叫 __setattr__() 方法

③ 當呼叫 __setattr__()方法時,首先列印 '__setattr__'字串,而後執行 self.cout += 1操作

④ 當執行 self.cout 加 1 操作時,將會去尋找 count 這個屬性,然而,由於此時 __init__尚未完成,並不存在 count這個屬性,因此導致 'AttributeError' 錯誤

那麼該如何更改呢?可以這樣的:

 1 class Test:
 2     def __init__(self):
 3         self.count = 0
 4     def __setattr__(self, name, value):
 5         print('__setattr__')
 6         super().__setattr__(name, value+1)
 7 
 8         
 9 >>> t=Test()
10 __setattr__
11 >>> t.count
12 1

如何,問題解決了吧!

但是以上程式碼雖然解決了報錯的問題,深入體會一下,你會發現,採用此方法只是給 基類object增加了一個屬性 count,而並不是例項的屬性,因此,以上這種寫法避免使用

另外,再次將程式碼改進一下,如下:

 1 class Test:
 2     def __setattr__(self, name, value):
 3         self.name = value
 4 
 5         
 6 >>> t=Test()
 7 >>> t.x=1
 8 Traceback (most recent call last):
 9   File "<pyshell#413>", line 1, in <module>
10     t.x=1
11   File "<pyshell#411>", line 3, in __setattr__
12     self.name = value
13   File "<pyshell#411>", line 3, in __setattr__
14     self.name = value
15   File "<pyshell#411>", line 3, in __setattr__
16     self.name = value
17   [Previous line repeated 327 more times]
18 RecursionError: maximum recursion depth exceeded while calling a Python object

居然報錯了,看報錯資訊為 “遞迴錯誤”,我沒用遞迴啊,怎麼會有這個錯誤呢?

其實,原因很簡單:當我們給 t.x 賦值時,呼叫了 __setattr__()方法,進入該方法;該方法中,又來了一次賦值(self.name = value),又會去呼叫 __setattr__() 方法,持續這個死迴圈(子子孫孫無窮盡也,必須要有一代斷子絕孫);

我們知道,系統的資源是有限的,丫的你老是申請資源不釋放,系統哪來的那麼多資源給你自己用?因此,Python直譯器規定,遞迴深度不得超過200(不同版本不一樣),你超過了,不好意思,不帶你玩了!

所以,我們只好改變上述的問題了:

 1 t.x=2
 2 >>> class Test:
 3     def __setattr__(self, name, value):
 4         print('__setattr__() been called')
 5         super().__setattr__(name, value)
 6 
 7 >>> t=Test()
 8 >>> t.x=1
 9 __setattr__() been called
10 >>> t.x=2
11 __setattr__() been called

OK,至此,關於屬性操作的問題暫時完結吧!