1. 程式人生 > >第045講:魔法方法:屬性訪問

第045講:魔法方法:屬性訪問

目錄

0. 請寫下這一節課你學習到的內容:格式不限,回憶並複述是加強記憶的好方式!

測試題

0. 請問以下程式碼的作用是什麼?這樣寫正確嗎?(如果不正確,請改正)

1. 自定義該類的屬性被訪問的行為,你應該重寫哪個魔法方法?

2. 在不上機驗證的情況下,你能推斷以下程式碼分別會顯示什麼嗎?

3. 在不上機驗證的情況下,你能推斷以下程式碼分別會顯示什麼嗎?

4. 請指出以下程式碼的問題所在:

動動手

0. 按要求重寫魔法方法:當訪問一個不存在的屬性時,不報錯且提示“該屬性不存在!”

1. 編寫 Demo 類,使得下邊程式碼可以正常執行:

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)