屬性訪問引發的無限遞迴
起步
這篇 中介紹了幾種屬性訪問的區別,其中在__getattribute__
中提到使用基類的方法來獲取屬效能避免在方法中出現無限遞迴的情況
。 那麼那些情況會引發無限遞迴呢?
getattribute 引發的無限遞迴
class D(object): def __init__(self): self.test=20 self.test2=21 def __getattribute__(self,name): if name=='test': return 0 else: return self.__dict__[name] d = D() print(d.test) print(d.test2)
由於__getattribute__()
方法是無條件觸發,因此在訪問self.__dict__
時,會再次呼叫__getattribute__
,從而引發無限遞迴。
所以常常使用object
基類的__getattribute__
來避免這種情況:
def __getattribute__(self,name): if name=='test': return 0. else: return object.__getattribute__(self, name)
getattr 引發的無限遞迴
import copy class Tricky(object): def __init__(self): self.special = ["foo"] def __getattr__(self, name): if name in self.special: return "yes" raise AttributeError() t1 = Tricky() assert t1.foo == "yes" t2 = copy.copy(t1) assert t2.foo == "yes" print("This runs, but isn't covered.")
這裡存在了無限遞迴,你會發現最後兩行程式碼沒有執行。__getattr__
函式是當屬性不存在時被呼叫的,而函式裡使用的唯一屬性是self.special
。但它是在__init__
中建立的,所以它應該始終存在,是吧?
答案在於copy.copy
的工作方式上。複製物件時,它不會呼叫其__init__
方法。它建立一個新的空物件,然後將屬性從舊複製到新的。為了實現自定義複製,物件可以提供執行復制的功能,因此複製模組在物件上查詢這些屬性。這自然會呼叫__getattr__
。
如果在__getattr__
新增一些輸出追蹤:
def __getattr__(self, name): print(name) if name in self.special: return "yes" raise AttributeError()
得到的輸出是:
foo __getstate__ __setstate__ special special special special special ...
這裡發生的過程是:複製模組查詢__setstate__
屬性,該屬性不存在,因此呼叫了__getattr__
。新物件是空白的,它沒有呼叫__init__
來建立self.special
。由於該屬性不存在,因此呼叫__getattr__
,並開始無限遞迴。
Traceback (most recent call last): File "E:/workspace/test3.py", line 15, in <module> t2 = copy.copy(t1) File "E:\workspace\.env\lib\copy.py", line 106, in copy return _reconstruct(x, None, *rv) File "E:\workspace\.env\lib\copy.py", line 281, in _reconstruct if hasattr(y, '__setstate__'):
從異常資訊可以看出,引發這次雪崩的導火線是hasattr(y, '__setstate__')
。 在這個案例中,函式可以這樣修改:
def __getattr__(self, name): if name == "special": raise AttributeError() if name in self.special: return "yes" raise AttributeError()