python 的魔法方法
這裡只分析幾個可能會常用到的魔法方法,像__new__這種不常用的,用來做元類初始化的或者是__init__這種初始化使用的 每個人都會用的就不介紹了。
其實每個魔法方法都是在對內建方法的重寫,和做像裝飾器一樣的行為。理解這個道理 再嘗試去理解每個細節裝飾器會比較方便。
關於__str__和__repr__:
直接上例子:
class Test(object): def __init__(self, world): self.world = world def __str__(self): return 'world is %s str' % self.world def __repr__(self): return 'world is %s repr' % self.world t = Test('world_big') print str(t) print repr(t)
output:
world is world_big str
world is world_big repr
其實__str__相當於是str()方法 而__repr__相當於repr()方法。str是針對於讓人更好理解的字串格式化,而repr是讓機器更好理解的字串格式化。
其實獲得返回值的方法也很好測試,在我們平時使用ipython的時候,在不使用print直接輸出物件的時候,通常呼叫的就是repr方法,這個時候改寫repr方法可以讓他方便的輸出我們想要知道的內容,而不是一個預設內容。
關於__hash__和__dir__:
其實在實際應用中寫了這麼久python,也沒有用到需要這兩個方法出現的地方,但是在有些庫裡面是有看到過。
__hash__是hash()方法的裝飾器版本,而__dir__是dir()的裝飾器版本。
上程式碼展示一下__hash__用法:
class Test(object): def __init__(self, world): self.world = world x = Test('world') p = Test('world') print hash(x) == hash(p) print hash(x.world) == hash(p.world) class Test2(object): def __init__(self, song): self.song = song def __hash__(self): return 1241 x = Test2('popo') p = Test2('janan') print x, hash(x) print p, hash(p)
output:
False
True
<__main__.Test2 object at 0x101b0c590> 1241
<__main__.Test2 object at 0x101b0c4d0> 1241
可以看到這裡的hash()方法總是會返回int型的數字。可以用於比較一個唯一的物件,比方說一個不同記憶體的object不會相當,而相同字串hash之後就會相等。然後我們通過修改__hash__方法來修改hash函式的行為。讓他總是返回1241,也是可以輕鬆做到的。
另外一個方法是dir(),熟悉python的人都知道dir()可以讓我們檢視當前環境下有哪些方法和屬性可以進行呼叫。如果我們使用dir(object)語法,可以獲得一個物件擁有的方法和屬性。同樣的道理如果我們在類中定義了__dir__(),就可以指定哪些方法和屬效能夠被dir()方法所檢視查詢到。道理一樣我這裡不再貼出程式碼了,有興趣的朋友可以自己去試試。
關於控制引數訪問的__getattr__,__setattr__,__delattr__,__getattribute__:
__getattr__是一旦我們嘗試訪問一個並不存在的屬性的時候就會呼叫,而如果這個屬性存在則不會呼叫該方法。
來看一個__getattr__的例子:
class Test(object): def __init__(self, world): self.world = world def __getattr__(self, item): return item x = Test('world123') print x.world4
output:
world4
這裡我們並沒有world4屬性,在找不到屬性的情況下,正常的繼承object的物件都會丟擲AtrributeError的錯誤。但是這裡我通過__getattr__魔法方法改變了找不到屬性時候的類的行為。輸出了查詢的屬性的引數。
__setattr__是設定引數的時候會呼叫到的魔法方法,相當於設定引數前的一個鉤子。每個設定屬性的方法都繞不開這個魔法方法,只有擁有這個魔法方法的物件才可以設定屬性。在使用這個方法的時候要特別注意到不要被迴圈呼叫了。
下面來看一個例子:
class Test(object): def __init__(self, world): self.world = world def __setattr__(self, name, value): if name == 'value': object.__setattr__(self, name, value - 100) else: object.__setattr__(self, name, value) x = Test(123) print x.world x.value = 200 print x.value
output:
123
100
這裡我們先初始化一個Test類的例項x,通過__init__方法我們可以注意到,會給初始化的world引數進行賦值。這裡的self.world = world語句就是在做這個事情。
注意,這裡在進行world引數賦值的時候,就是會呼叫到__setattr__方法。這個例子來看world就是name,而後面的值的world就是value。我在__setattr__裡面做了一個行為改寫,我將判斷name 值是'value'的進行特殊處理,把它的value值減少100. 所以輸出了預期的結果。
我為什麼說__setattr__特別容易出現迴圈呼叫?因為任何賦值方法都會走這個魔法方法,如果你在你改寫__setattr__方法裡面使用了類似的賦值,又回迴圈呼叫回__setattr__方法。例如:
class Test(object): def __init__(self, world): self.world = world def __setattr__(self, name, value): self.name = value x = Test(123) print x.world
output:
RuntimeError: maximum recursion depth exceeded
這裡我們想讓__setattr__執行預設行為,也就是將value賦值給name,和object物件中的同樣方法,做類似的操作。但是這裡我們不呼叫父類__setattr__的方法來實現,做這樣的嘗試得到的結果就是,超過迴圈呼叫深度,報錯。因為這裡在執行初始化方法self.world = world的時候,就會呼叫__setattr__方法,而這裡的__setattr__方法裡面的self.name = value又會呼叫自身。所以造成了迴圈呼叫。所以使用該魔法方法的時候要特別注意。
__delattr__的行為和__setattr__特別相似,同樣需要注意的也是迴圈呼叫問題,其他都差不多,只是把屬性賦值變成了 del self.name這樣的表示。下面直接上個例子,不再多贅述。
class Test(object): def __init__(self, world): self.world = world def __delattr__(self, item): print 'hahaha del something' object.__delattr__(self, item) x = Test(123) del x.world print x.world
output:
hahaha del something
Traceback (most recent call last):
File "/Users/piperck/Desktop/py_pra/laplace_pra/practie_01_23/c2.py", line 12, in <module>
print x.world
AttributeError: 'Test' object has no attribute 'world'
可以看到我們將屬性刪除之後,就找不到那個屬性了。但是在刪除屬性的時候呼叫了__delattr__,我在裡面列印了一段話,在執行之前被打印出來了
__getattribute__和__getattr__方法唯一不同的地方是,上面我們已經介紹了__getattr__方法只能在找不到屬性的時候攔截呼叫,然後進行過載或者加入一些其他操作。但是__getattribute__更加強大,他可以攔截所有的屬性獲取。所以也容易出現我們上面提到的,迴圈呼叫的問題。下面上一個例子來說明這個問題:
class Test(object): def __init__(self, world): self.world = world def __getattribute__(self, item): print 'get_something: %s' % item return item x = Test(123) print x.world print x.pp
output:
get_something: world
world
get_something: pp
pp
可以看到,區別於__getattr__只攔截不存在的屬性,__getattribute__會攔截所有的屬性。所以導致了已經被初始化的world值123,也被改寫成了字串world。而不存在的屬性也被改寫了成了pp。
關於__dict__:
先上個例子:
class Test(object): fly = True def __init__(self, age): self.age = age
__dict__魔法方法可以被稱為系統,他是儲存各分層屬性的魔法方法。__dict__中,鍵為屬性名,值為屬性本身。可以這樣理解,在平時我們給類和例項定義的那些屬性,都會被儲存到__dict__方法中用於讀取。而我們平時使用的類似這樣的語法Test.fly 其實就是呼叫了類屬性,同樣可以寫成Test.__dict__['fly']。除了類屬性,還有例項屬性。當我們用類例項化一個例項,例如上文我們使用p = Test(2)例項化類Test,p也會具有__dict__屬性。這裡會輸出:
{'age': 2}
由上可以發現,python中的屬性是進行分層定義的。/object/Test/p這樣一層一層下來的。當我們需要呼叫某個屬性的時候,python會一層一層往上面遍歷上去。先從例項,然後例項的__class__的__dict__,然後是該類的__base__。這樣__dict__一路找上去。如果最後都沒有找到,就丟擲AttributeError錯誤。
這裡可以延伸一下,沒記錯的話,我前面有篇文章講了一個方法__slot__。__slots__方法就是通過限制__dict__,只讓類例項初始化__slots__裡面定義的屬性,而且讓例項不再擁有__dict__方法,來達到節約記憶體的目的。我將會就上面的那個例子重寫一下,來說明這個問題。
class Test(object): __slots__ = ['age'] fly = True def __init__(self, age): self.age = age
output:
In [25]: Test.__dict__ Out[25]: dict_proxy({'__doc__': None, '__init__': <function __main__.__init__>, '__module__': '__main__', '__slots__': ['age'], 'age': <member 'age' of 'Test' objects>, 'fly': True}) In [36]: p.__dict__ --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-36-3a1cec47d020> in <module>() ----> 1 p.__dict__ AttributeError: 'Test' object has no attribute '__dict__' In [37]: p.age Out[37]: 3 In [38]: p.fly Out[38]: True
可以看到,__slots__方法並沒有阻止由下至上的屬性查詢方法,只是不會再允許沒有包含在__slots__陣列中的屬性再被賦值給例項了。但這並不妨礙,繼續呼叫允許訪問的屬性,以及類屬性。
關於__get__,__set__,__del__:
在前面的文章裡面我也介紹過這三個魔法方法,雖然一般是用不到的,但是在寫庫的時候它們有特別的用途。他們是python另外一個協議descriptor的根基。
同一個物件的不同屬性之間可能存在依賴關係。當某個屬性被修改時,我們希望依賴於該屬性的其他屬性也同時變化。在這種環境下面__dict__方法就無法辦到。因為__dict__方法只能用來儲存靜態屬性。python提供了多種即時生成屬性的方法。其中一種就是property。property是特殊的屬性。比如我們為上面的例子增加一個property特性,使得他能夠動態變化。來看這個例子:
class Test(object): fly = True def __init__(self, age): self.age = age def whether_fly(self): if self.age <= 30: return True else: return False def just_try_try(self, other): pass whether_fly = property(whether_fly) p = Test(20) print p.age print p.whether_fly p.age = 40 print p.age print p.whether_fly
output:
20 True 40 False
可以看到 我們可以使用這種手段,動態修改屬性值。property有四個引數。前三個引數為函式,分別用於處理查詢特性、修改特性、刪除特性。最後一個引數為特性的文件,可以為一個字串,起說明作用。這裡我只是要到了第一個引數,查詢的時候動態修改他的返回值,而第二個引數是在修改值的時候就會體現出來。