技術圖文:Python魔法方法之屬性訪問詳解

背景

今天在B站學習“零基礎入門學習 Python”中的第45節“魔法方法:屬性訪問”,這也是我們組織的 Python基礎刻意練習活動 的學習任務,其中有這樣的一個題目。

練習要求

  • 寫一個矩形類,默認有寬和高兩個屬性。
  • 如果爲一個叫square的屬性賦值賦值,那麼說明這是一個正方形,值就是正方形的邊長,此時寬和高都應該等於邊長。

技術分析

我們先來看看有關於屬性的四個魔法方法:

  • __getattr__(self, name): 定義當用戶試圖獲取一個不存在的屬性時的行爲。
  • __getattribute__(self, name):定義當該類的屬性被訪問時的行爲(先調用該方法,查看是否存在該屬性,若不存在,接着去調用__getattr__)。
  • __setattr__(self, name, value):定義當一個屬性被設置時的行爲。
  • __delattr__(self, name):定義當一個屬性被刪除時的行爲。
class Test:
    def __getattr__(self, name):
        print('__getattr__')

    def __getattribute__(self, name):
        print('__getattribute__')

    def __setattr__(self, name, value):
        print('__setattr__')

    def __delattr__(self, name):
        print('__delattr__')


t = Test()
t.x

# __getattribute__

如上述代碼所示,x並不是Test實例對象t的一個屬性,首先去調用 __getattribute__() 方法,得知該屬性並不屬於該實例對象。但是,按照常理,t.x應該打印 __getattribute____getattr__,但實際情況並非如此,爲什麼呢?

實例對象屬性尋找的順序如下:

① 首先訪問 __getattribute__() 魔法方法(隱含默認調用,無論何種情況,均會調用此方法)。

② 接着,去t.__dict__中查找是否具備該屬性。

③ 若在 t.__dict__ 中找不到對應的屬性, 則去t.__class__.__dict__中尋找。

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

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

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

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

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

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

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

class Test:
    def __getattr__(self, name):
        print('__getattr__')

    def __getattribute__(self, name):
        print('__getattribute__')
        object.__getattribute__(self, name)

    def __setattr__(self, name, value):
        print('__setattr__')

    def __delattr__(self, name):
        print('__delattr__')


t = Test()
t.x

# __getattribute__
# __getattr__

方法二:採用 super() 方法

class Test:
    def __getattr__(self, name):
        print('__getattr__')

    def __getattribute__(self, name):
        print('__getattribute__')
        super().__getattribute__(name)

    def __setattr__(self, name, value):
        print('__setattr__')

    def __delattr__(self, name):
        print('__delattr__')


t = Test()
t.x

# __getattribute__
# __getattr__

以上介紹完畢,那麼 __setattr____delattr__ 方法相對簡單多了:

class Test:
    def __getattr__(self, name):
        print('__getattr__')

    def __getattribute__(self, name):
        print('__getattribute__')
        object.__getattribute__(self, name)

    def __setattr__(self, name, value):
        print('__setattr__')

    def __delattr__(self, name):
        print('__delattr__')


t = Test()
t.x = 1

# __setattr__

del t.x

# __delattr__

對了,再補充一點哈!

class Test:
    def __init__(self):
        self.count = 0

    def __setattr__(self, name, value):
        print('__setattr__')
        self.count += 1


t = Test()

# AttributeError: 'Test' object has no attribute 'count'

看報錯信息很容易明白,這是因爲:

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

② 賦值默認調用 __setattr__() 方法

③ 當調用 __setattr__()方法時,首先打印 __setattr__字符串,而後執行 self.cout += 1操作

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

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

class Test:
    def __init__(self):
        self.count = 0

    def __setattr__(self, name, value):
        print('__setattr__')
        super().__setattr__(name, value + 1)


t = Test()
print(t.count)

# __setattr__
# 1

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

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

class Test:
    def __setattr__(self, name, value):
        self.name = value


t = Test()
t.x = 'lsgo'

# RecursionError: maximum recursion depth exceeded

當我們給 t.x 賦值時,調用了 __setattr__()方法,進入該方法。該方法中,又來了一次賦值(self.name = value),又會去調用 __setattr__() 方法,持續這個死循環。

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

class Test:
    def __setattr__(self, name, value):
        print('__setattr__() been called')
        super().__setattr__(name, value)


t = Test()
t.x = 'lsgo'

# __setattr__() been called

print(t.x)

# lsgo

代碼實現

上面詳細介紹了關於屬性的四個魔法方法,下面我們來看實現要求的具體代碼:

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

    def __setattr__(self, key, value):
        if key == 'square':
            self.width = value
            self.height = value
        else:
            super().__setattr__(key, value)

    def getArea(self):
        return float(self.width) * float(self.height)


r = Rectangle(4, 5)
print(r.getArea())  # 20.0
r.square = 10
print(r.__dict__)  # {'width': 10, 'height': 10}
print(r.getArea())  # 100.0

總結

魔法方法是 Python 面向對象編程中最核心的內容,需要花費一定的精力才能將其掌握,參加 Python基礎刻意練習的小夥伴們加油! See You!


參考文獻

  • https://www.bilibili.com/video/av4050443/?p=46
  • https://www.runoob.com/python3/python3-tutorial.html
  • https://www.cnblogs.com/Jimmy1988/p/6804095.html

相關圖文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章