第045講:魔法方法:屬性訪問
目錄
0. 請寫下這一節課你學習到的內容:格式不限,回憶並複述是加強記憶的好方式!
0. 請問以下程式碼的作用是什麼?這樣寫正確嗎?(如果不正確,請改正)
1. 自定義該類的屬性被訪問的行為,你應該重寫哪個魔法方法?
2. 在不上機驗證的情況下,你能推斷以下程式碼分別會顯示什麼嗎?
3. 在不上機驗證的情況下,你能推斷以下程式碼分別會顯示什麼嗎?
0. 按要求重寫魔法方法:當訪問一個不存在的屬性時,不報錯且提示“該屬性不存在!”
2. 修改上邊【測試題】第 4 題,使之可以正常執行:編寫一個 Counter 類,用於實時檢測物件有多少個屬性。
0. 請寫下這一節課你學習到的內容:格式不限,回憶並複述是加強記憶的好方式!
我們這節課說說魔法方法關於屬性訪問的應用。我們知道可以使用點操作符(.)的形式去訪問物件屬性。我們在類與物件相關的BIF這一節中,我們可以使用幾個BIF有禮貌的去訪問屬性,例如:
>>> class C: def __init__(self): self.x = 'x-man' >>> c = C() >>> c.x 'x-man' >>> getattr(c, 'x', '沒有這個屬性') 'x-man' >>> getattr(c, 'y', '沒有這個屬性') '沒有這個屬性'
另外,我們還介紹過setattr, delattr 分別是設定屬性和刪除屬性,忘了的話可以回頭看一下筆記。然後還介紹了property函式的用法,property使得我們以屬性的方法去訪問屬性。例如:
>>> class C: def __init__(self, size = 10): self.size = size def getSize(self): return self.size def setSize(self, value): self.size = value def delSize(self): del self.size x = property(getSize, setSize, delSize) >>> c = C() >>> c.x 10 >>> c.x = 1 >>> c.size 1 >>> del c.x >>> c.size Traceback (most recent call last): File "<pyshell#24>", line 1, in <module> c.size AttributeError: 'C' object has no attribute 'size'
關於屬性訪問也有相應的魔法方法來管理,通過這些魔法方法的重寫可以隨心所欲的控制物件的屬性訪問。下面是今天要講解的四個魔法方法:
(一)__getattr__(self, name)
–定義當用戶試圖獲取一個不存在的屬性時的行為
(二)__getattribute__(self, name)
–定義當該類的屬性被訪問時的行為
(三)__setattr__(self, name, value)
–定義當一個屬性被設定時的行為
(四)__delattr__(self, name)
–定義當一個屬性被刪除時的行為
也就是說,我們只要重寫以上四個魔法方法,就可以空置物件的屬性訪問了,我們舉例說明來測試一下這四個魔法方法的前後關係、因果關係:
>>> class C:
def __getattr__(self, name):
print("getattr")
def __getattribute__(self, name):
print("getattribute")
return super().__getattribute__(name)
def __setattr__(self, name, value):
print("setatrtr")
super().__setattr__(name, value)
def __delattr__(self, name):
print("delattr")
super().__delattr__(name)
>>> c = C()
>>> c.x
getattribute
getattr
>>> c.x = 1
setatrtr
>>> c.x
getattribute
1
>>> del c.x
delattr
>>> c.x
getattribute
getattr
我們通過 print() 來在呼叫該魔法方法的時候列印一下,這是最好的除錯方式。第一次c.x 的時候,物件是沒有任何屬性的,此時會先訪問 getattribute,當屬性不存在時,再去訪問 getattr,設定屬性時訪問 setattr,刪除屬性時訪問 delattr。
這幾個魔法方法在使用時,需要注意 死迴圈 陷阱。舉例說明:我們試著寫下面這個程式:
•寫一個矩形類,預設有寬和高兩個屬性;
•如果為一個叫square的屬性賦值,那麼說明這是一個正方形,值就是正方形的邊長,此時寬和高都應該等於邊長。
class Rectangle:
def __init__(self, width = 0, height = 0):
self.width = width
self.height = height
def __setattr__(self, name, value):
if name = 'square':
self.width = value
self.height = value
else:
self.name = value
def getArea(self):
return self.width * self.height
============ RESTART: C:/Users/XiangyangDai/Desktop/上課程式碼/45-1.py ============
>>> r = Rectangle(4, 5)
Traceback (most recent call last):
File "<pyshell#50>", line 1, in <module>
r = Rectangle(4, 5)
File "C:/Users/XiangyangDai/Desktop/上課程式碼/45-1.py", line 3, in __init__
self.width = width
File "C:/Users/XiangyangDai/Desktop/上課程式碼/45-1.py", line 10, in __setattr__
self.name = value
File "C:/Users/XiangyangDai/Desktop/上課程式碼/45-1.py", line 10, in __setattr__
self.name = value
..........
當我們執行並初始化時,就會進入死迴圈,這是為什麼呢?這是因為初始化時,就會呼叫 __setattr__魔法方法,然後就會執行 self.name = value,而這個又會呼叫 __setattr__魔法方法,然後就進入死迴圈了,解決方法是什麼呢?
就是把 self.name = value 這條語句改為呼叫基類的 __setattr__魔法方法(未被改寫的魔法方法)。如下:
class Rectangle:
def __init__(self, width = 0, height = 0):
self.width = width
self.height = height
def __setattr__(self, name, value):
if name == 'square':
self.width = value
self.height = value
else:
super().__setattr__(name, value)
def getArea(self):
return self.width * self.height
>>> r = Rectangle(4, 5)
>>> r.height
5
>>> r.width
4
>>> r.getArea()
20
>>> r.square = 10
>>> r.width
10
>>> r.height
10
>>> r.getArea()
100
除了 __setattr__魔法方法,__getattribute__魔法方法也會陷入死迴圈的陷阱,如果一直去獲得,就會重複的獲得,死迴圈。推薦的解決方法就是使用基類的方法去設定、去獲得。
測試題
0. 請問以下程式碼的作用是什麼?這樣寫正確嗎?(如果不正確,請改正)
def __setattr__(self, name, value):
self.name = value + 1
答:這段程式碼試圖在物件的屬性發生賦值操作的時候,將實際的值 +1賦值給相應的屬性。但這麼寫法是錯誤的,因為每當屬性被賦值的時候, __setattr__() 會被呼叫,而裡邊的 self.name = value + 1 語句又會再次觸發 __setattr__() 呼叫,導致無限遞迴。
程式碼應該這樣寫:
def __setattr__(self, name, value):
self.__dict__[name] = value + 1
或者:
def __setattr__(self, name, value):
super().__setattr__(name, value+1)
1. 自定義該類的屬性被訪問的行為,你應該重寫哪個魔法方法?
答:__getattribute__(self, name)
2. 在不上機驗證的情況下,你能推斷以下程式碼分別會顯示什麼嗎?
>>> class C:
def __getattr__(self, name):
print(1)
def __getattribute__(self, name):
print(2)
def __setattr__(self, name, value):
print(3)
def __delattr__(self, name):
print(4)
>>> c = C()
>>> c.x = 1
# 位置一,請問這裡會顯示什麼?
>>> print(c.x)
# 位置二,請問這裡會顯示什麼?
答:位置一會顯示 3,因為 c.x = 1 是賦值操作,所以會訪問 __setattr__() 魔法方法;位置二會顯示 2 和 None,因為 x 是屬於例項物件 c 的屬性,所以 c.x 是訪問一個存在的屬性,因此會訪問 __getattribute__() 魔法方法,但我們重寫了這個方法,使得它不能按照正常的邏輯返回屬性值,而是列印一個 2 代替,由於我們沒有寫返回值,所以緊接著返回 None 並被 print() 打印出來。
3. 在不上機驗證的情況下,你能推斷以下程式碼分別會顯示什麼嗎?
>>> class C:
def __getattr__(self, name):
print(1)
return super().__getattr__(name)
def __getattribute__(self, name):
print(2)
return super().__getattribute__(name)
def __setattr__(self, name, value):
print(3)
super().__setattr__(name, value)
def __delattr__(self, name):
print(4)
super().__delattr__(name)
>>> c = C()
>>> c.x
答:在不上機的情況下,我相信80%以上的魚油很難猜到正確的答案T_T
>>> c = C()
>>> c.x
2
1
Traceback (most recent call last):
File "<pyshell#31>", line 1, in <module>
c.x
File "<pyshell#29>", line 4, in __getattr__
return super().__getattr__(name)
AttributeError: 'super' object has no attribute '__getattr__'
為什麼會如此顯示呢?我們來分析下:首先 c.x 會先呼叫 __getattribute__() 魔法方法,列印 2;然後呼叫 super().__getattribute__(),找不到屬性名 x,因此會緊接著呼叫 __getattr__() ,於是列印 1;但是你猜到了開頭沒猜到結局……當你希望最後以 super().__getattr__() 終了的時候,Python 竟然告訴你 AttributeError,super 物件木有 __getattr__ !!
求證:
>>> dir(super)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__self_class__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__thisclass__']
4. 請指出以下程式碼的問題所在:
class Counter:
def __init__(self):
self.counter = 0
def __setattr__(self, name, value):
self.counter += 1
super().__setattr__(name, value)
def __delattr__(self, name):
self.counter -= 1
super().__delattr__(name)
答:初學者重寫屬性魔法方法很容易陷入的一個誤區就是木有“觀前顧後”。
以下注釋:
class Counter:
def __init__(self):
self.counter = 0 # 這裡會觸發 __setattr__ 呼叫
def __setattr__(self, name, value):
self.counter += 1
“””既然需要 __setattr__ 呼叫後才能真正設定 self.counter 的值,所以這時候 self.counter 還沒有定義,所以沒法 += 1,錯誤的根源。”””
super().__setattr__(name, value)
def __delattr__(self, name):
self.counter -= 1
super().__delattr__(name)
動動手
0. 按要求重寫魔法方法:當訪問一個不存在的屬性時,不報錯且提示“該屬性不存在!”
程式碼清單:
>>> class Demo:
def __getattr__(self, name):
return '該屬性不存在!'
>>> demo = Demo()
>>> demo.x
'該屬性不存在!'
1. 編寫 Demo 類,使得下邊程式碼可以正常執行:
>>> demo = Demo()
>>> demo.x
'FishC'
>>> demo.x = "X-man"
>>> demo.x
'X-man'
程式碼清單:
>>> class Demo:
def __getattr__(self, name):
self.name = 'FishC'
return self.name
2. 修改上邊【測試題】第 4 題,使之可以正常執行:編寫一個 Counter 類,用於實時檢測物件有多少個屬性。
程式實現如下:
>>> c = Counter()
>>> c.x = 1
>>> c.counter
1
>>> c.y = 1
>>> c.z = 1
>>> c.counter
3
>>> del c.x
>>> c.counter
2
程式碼清單:
class Counter:
def __init__(self):
super().__setattr__('counter', 0)
def __setattr__(self, name, value):
super().__setattr__('counter', self.counter + 1)
super().__setattr__(name, value)
def __delattr__(self, name):
super().__setattr__('counter', self.counter - 1)
super().__delattr__(name)