九游娛樂:介紹Python的魔術(shù)方法 - Magic Method
有些魔術(shù)方法,我們可能以后一輩子都不會再遇到了,這里也就只是簡單介紹下;
而有些魔術(shù)方法,巧妙使用它可以構(gòu)造出非常優(yōu)美的代碼,比如將復(fù)雜的邏輯封裝成簡單的API。
__init__我們很熟悉了,它在對象初始化的時候調(diào)用,我們一般將它理解為構(gòu)造函數(shù).
__new__是用來創(chuàng)建類并返回這個類的實例, 而__init__只是將傳入的參數(shù)來初始化該實例.
__new__在創(chuàng)建一個實例的過程中必定會被調(diào)用,但__init__就不一定,比如通過pickle.load的方式反序列化一個實例時就不會調(diào)用__init__。
__new__方法總是需要返回該類的一個實例,而__init__不能返回除了None的任何值。比如下面例子:
如果要講解__new__,往往需要牽扯到metaclass(元類)的介紹。
如果你有興趣深入,可以參考我的另一篇博客:理解Python的metaclass
在對象的生命周期結(jié)束時,__del__會被調(diào)用,可以將__del__理解為析構(gòu)函數(shù).
如果調(diào)用了foo.__del__(),對象本身仍然存在. 但是調(diào)用了del foo, 就再也沒有foo這個對象了.
請注意,如果解釋器退出的時候?qū)ο筮€存在,就不能保證__del__被確切的執(zhí)行了。所以__del__并不能替代良好的編程習慣。
總有人要吐槽Python缺少對于類的封裝,比如希望Python能夠定義私有屬性,然后提供公共可訪問的getter和 setter。Python其實可以通過魔術(shù)方法來實現(xiàn)封裝。
該方法定義了你試圖訪問一個不存在的屬性時的行為。因此,重載該方法可以實現(xiàn)捕獲錯誤拼寫然后進行重定向, 或者對一些廢棄的屬性進行警告。
__setattr__是實現(xiàn)封裝的解決方案,它定義了你對屬性進行賦值和修改操作時的行為。
不管對象的某個屬性是否存在,它都允許你為該屬性進行賦值,因此你可以為屬性的值進行自定義操作。有一點需要注意,實現(xiàn)__setattr__時要避免無限遞歸的錯誤,下面的代碼示例中會提到。
__delattr__與__setattr__很像,只是它定義的是你刪除屬性時的行為。實現(xiàn)__delattr__是同時要避免無限遞歸的錯誤。
__getattribute__定義了你的屬性被訪問時的行為,相比較,__getattr__只有該屬性不存在時才會起作用。
需要提醒的九游娛樂-官網(wǎng)app是,最好不要嘗試去實現(xiàn)__getattribute__,因為很少見到這種做法,而且很容易出bug。
__delattr__如果在其實現(xiàn)中出現(xiàn)del self.name這樣的代碼也會出現(xiàn)無限遞歸錯誤,這是一樣的原因。
我們從一個例子來入手,介紹什么是描述符,并介紹__get__,__set__,__delete__的使用。(放在這里介紹是為了跟上一小節(jié)介紹的魔術(shù)方法作對比)
在上面例子中,在還沒有對Distance的實例賦值前, 我們認為meter和foot應(yīng)該是各自類的實例對象, 但是輸出卻是數(shù)值。這是因為__get__發(fā)揮了作用.
我們只是修改了meter,并且將其賦值成為int,但foot也修改了。這是__set__發(fā)揮了作用.
描述器對象(Meter、Foot)不能獨立存在, 它需要被另一個所有者類(Distance)所持有。
描述器對象可以訪問到其擁有者實例的屬性,比如例子中Foot的instance.meter。
在面向?qū)ο缶幊虝r,如果一個類的屬性有相互依賴的關(guān)系時,使用描述器來編寫代碼可以很巧妙的組織邏輯。
一個類要成為描述器,必須實現(xiàn)__get__,__set__,__delete__中的至少一個方法。下面簡單介紹下:
參數(shù)instance是擁有者類的實例。參數(shù)owner是擁有者類本身。__get__在其擁有者對其讀值的時候調(diào)用。
可變?nèi)萜骱筒豢勺內(nèi)萜鞯膮^(qū)別在于,不可變?nèi)萜饕坏┵x值后,不可對其中的某個元素進行修改。
如果我們要自定義一些數(shù)據(jù)結(jié)構(gòu),使之能夠跟以上的容器類型表現(xiàn)一樣,那就需要去實現(xiàn)某些協(xié)議。
這里的協(xié)議跟其他語言中所謂的接口概念很像,一樣的需要你去實現(xiàn)才行,只不過沒那么正式而已。
如果要自定義不可變?nèi)萜黝愋?,只需要定義__len__和__getitem__方法;
如果要自定義可變?nèi)萜黝愋?,還需要在不可變?nèi)萜黝愋偷幕A(chǔ)上增加定義__setitem__和__delitem__。
如果你希望你的自定義數(shù)據(jù)結(jié)構(gòu)還支持可迭代, 那就還需要定義__iter__。
需要返回數(shù)值類型,以表示容器的長度。該方法在可變?nèi)萜骱筒豢勺內(nèi)萜髦斜仨殞崿F(xiàn)。
當你執(zhí)行self[key]的時候,調(diào)用的就是該方法。該方法在可變?nèi)萜骱筒豢勺內(nèi)萜髦幸捕急仨殞崿F(xiàn)。
如果想要該數(shù)據(jù)結(jié)構(gòu)被內(nèi)建函數(shù)reversed()支持,就還需要實現(xiàn)該方法。
如果沒有定義,那么Python會迭代容器中的元素來一個一個比較,從而決定返回True或者False。
dict字典類型會有該方法,它定義了key如果在容器中找不到時觸發(fā)的行為。
下面舉例,使用上面講的魔術(shù)方法來實現(xiàn)Haskell語言中的一個數(shù)據(jù)結(jié)構(gòu)。
我們再舉個例子,實現(xiàn)Perl語言的AutoVivification,它會在你每次引用一個值未定義的屬性時為你自動創(chuàng)建數(shù)組或者字典。
在Python中,關(guān)于自定義容器的實現(xiàn)還有更多實用的例子,但只有很少一部分能夠集成在Python標準庫中,比如Counter, OrderedDict等
with聲明是從Python2.5開始引進的關(guān)鍵詞。你應(yīng)該遇過這樣子的代碼:
在with聲明的代碼段中,我們可以做一些對象的開始操作和清除操作,還能對異常進行處理。
__enter__會返回一個值,并賦值給as關(guān)鍵詞之后的變量。在這里,你可以定義代碼段開始的一些操作。
__exit__定義了代碼段結(jié)束后的一些操作,可以這里執(zhí)行一些清除操作,或者做一些代碼段結(jié)束后需要立即執(zhí)行的命令,比如文件的關(guān)閉,socket斷開等。如果代碼段成功結(jié)束,那么exception_type, exception_value, traceback 三個參數(shù)傳進來時都將為None。如果代碼段拋出異常,那么傳進來的三個參數(shù)將分別為: 異常的類型,異常的值,異常的追蹤棧。
如果__exit__返回True, 那么with聲明下的代碼段的一切異常將會被屏蔽。
如果__exit__返回None, 那么如果有異常,異常將正常拋出,這時候with的作用將不會顯現(xiàn)出來。
這該示例中,IndexError始終會被隱藏,而TypeError始終會拋出。
Python對象的序列化操作是pickling進行的。pickling非常的重要,以至于Python對此有單獨的模塊pickle,還有一些相關(guān)的魔術(shù)方法。使用pickling, 你可以將數(shù)據(jù)存儲在文件中,之后又從文件中進行恢復(fù)。
下面舉例來描述pickle的操作。從該例子中也可以看出,如果通過pickle.load 初始化一個對象, 并不會調(diào)用__init__方法。
值得一提,從其他文件進行pickle.load操作時,需要注意有惡意代碼的可能性。另外,Python的各個版本之間,pickle文件可能是互不兼容的。
pickling并不是Python的內(nèi)建類型,它支持所有實現(xiàn)pickle協(xié)議(可理解為接口)的類。pickle協(xié)議有以下幾個可選方法來自定義Python對象的行為。
如果你希望unpickle時,__init__方法能夠調(diào)用,那么就需要定義__getinitargs__, 該方法需要返回一系列參數(shù)的元組,這些參數(shù)就是傳給__init__的參數(shù)。
如果pickle的數(shù)據(jù)包含了自定義的擴展類(比如使用C語言實現(xiàn)的Python擴展類)時,就需要通過實現(xiàn)__reduce__方法來控制行為了。由于使用過于生僻,這里就不展開繼續(xù)講解了。
令人容易混淆的是,我們知道,reduce()是Python的一個內(nèi)建函數(shù), 需要指出__reduce__并非定義了reduce()的行為,二者沒有關(guān)系。
下面的代碼示例很有意思,我們定義了一個類Slate(中文是板巖的意思)。這個類能夠記錄歷史上每次寫入給它的值,但每次pickle.dump時當前值就會被清空,僅保留了歷史。
運算符相關(guān)的魔術(shù)方法實在太多了,也很好理解,不打算多講。在其他語言里,也有重載運算符的操作,所以我們對這些魔術(shù)方法已經(jīng)很了解了。
強烈不推薦來定義__cmp__, 取而代之, 最好分別定義__lt__等方法從而實現(xiàn)比較功能。
下面我們定義一種類型Word, 它會使用單詞的長度來進行大小的比較, 而不是采用str的比較方式。
上面的代碼非常正常地實現(xiàn)了some_object的__add__方法。那么如果遇到相反的情況呢?
實現(xiàn)了類型轉(zhuǎn)化為complex(復(fù)數(shù), 也即1+2j這樣的虛數(shù))的行為.
在切片運算中將對象轉(zhuǎn)化為int, 因此該方法的返回值必須是int。用一個例子來解釋這個用法。
顯然不能。如果真的是這樣的線]這樣的寫法也應(yīng)該是通過的。而實際上,該寫法會拋出TypeError:
__index__了。雖然list和dict都實現(xiàn)了__getitem__方法, 但是它們的實現(xiàn)方式是不一樣的。如果希望上面例子能夠正常執(zhí)行, 需要實現(xiàn)Thing的
coerce()內(nèi)建函數(shù):官方文檔上的解釋是, coerce(x, y)返回一組數(shù)字類型的參數(shù), 它們被轉(zhuǎn)化為同一種類型,以便它們可以使用相同的算術(shù)運算符進行操作。如果過程中轉(zhuǎn)化失敗,拋出TypeError。
repr()時調(diào)用。str()和repr()都是返回一個代表該實例的字符串,主要區(qū)別在于: str()的返回值要方便人來看,而repr()的返回值要方便計算機看。
bool()時調(diào)用, 返回True或者False。你可能會問, 為什么不是命名為