1. 程式人生 > >Python最會變魔術的魔術方法,我覺得是它!

Python最會變魔術的魔術方法,我覺得是它!

在[上篇文章中](https://mp.weixin.qq.com/s/FtlVNTYq60KSPti2xD4psA),我有一個核心的發現:**Python 內建型別的特殊方法(含魔術方法與其它方法)由 C 語言獨立實現,在 Python 層面不存在呼叫關係。** 但是,文中也提到了一個例外:一個非常神祕的魔術方法。 這個方法非常不起眼,用途狹窄,我幾乎從未注意過它,然而,當發現它可能是上述“定律”的唯一例外情況時,我認為值得再寫一篇文章來詳細審視一下它。 本文主要關注的問題有: (1) \_\_missing\_\_()到底是何方神聖? (2) \_\_missing\_\_()有什麼特別之處?擅長“大變活人”魔術? (3) \_\_missing\_\_()是否真的是上述發現的例外?如果是的話,為什麼會有這種特例? ## 1、有點價值的\_\_missing\_\_() 從普通的字典中取值時,可能會出現 key 不存在的情況: ```python dd = {'name':'PythonCat'} dd.get('age') # 結果:None dd.get('age', 18) # 結果:18 dd['age'] # 報錯 KeyError dd.__getitem__('age') # 等同於 dd['age'] ``` ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gkusjp2wqij20ex056wef.jpg) 對於 get() 方法,它是有返回值的,而且可以傳入第二個引數,作為 key 不存在時的返回內容,因此還可以接受。但是,另外兩種寫法都會報錯。 為了解決後兩種寫法的問題,就可以用到 \_\_missing\_\_() 魔術方法。 現在,假設我們有一個這樣的訴求:從字典中取某個 key 對應的 value,如果有值則返回值,如果沒有值則插入 key,並且給它一個預設值(例如一個空列表)。 如果用原生的 dict,並不太好實現,但是,Python 提供了一個非常好用的擴充套件類`collections.defaultdict`: ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gkut8gajytj20jc076t8q.jpg) 如圖所示,當取不存在的 key 時,沒有再報 KeyError,而是預設存入到字典中。 為什麼 defaultdict 可以做到這一點呢? 原因是 defaultdict 在繼承了內建型別 dict 之後,還定義了一個 \_\_missing\_\_() 方法,當 \_\_getitem\_\_取不存在的值時,它就會呼叫入參中傳入的工廠函式(上例是呼叫 list(),建立空列表)。 作為最典型的示例,defaultdict 在文件註釋中寫到: ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gl2y6dy9ltj20su07odg2.jpg) 簡而言之,**\_\_missing\_\_()的主要作用就是由\_\_getitem\_\_在缺失 key 時呼叫,從而避免出現 KeyError。** 另外一個典型的使用例子是`collections.Counter` ,它也是 dict 的子類,在取未被統計的 key 時,返回計數 0: ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gl2y56r60oj20ov04cq2x.jpg) ## 2、神出鬼沒的\_\_missing\_\_() 由上可知,\_\_missing\_\_()在\_\_getitem\_\_()取不到值時會被呼叫,但是,我不經意間還發現了一個細節:**\_\_getitem\_\_()在取不到值時,並不一定會呼叫\_\_missing\_\_()。** 這是因為它並非內建型別的必要屬性,並沒有在字典基類中被預先定義。 如果你直接從 dict 型別中取該屬性值,會報屬性不存在:`AttributeError: type object 'object' has no attribute '__missing__'` 。 使用 dir() 檢視,發現確實不存在該屬性: ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gkus6gzo37j20ng08g3zm.jpg) 如果從 dict 的父類即 object 中檢視,也會發現同樣的結果。 這是怎麼回事呢?為什麼在 dict 和 object 中都沒有\_\_missing\_\_屬性呢? 然而,查閱最新的官方文件,object 中分明包含這個屬性: ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gl2yuqkrwvj210j0390sx.jpg) 出處:https://docs.python.org/3/reference/datamodel.html?highlight=\_\_missing\_\_#object.\_\_missing\_\_ 也就是說,理論上 object 類中會預定義\_\_missing\_\_,其文件證明了這一點,然而實際上它並沒有被定義!文件與現實出現了偏差! 如此一來,當 dict 的子類(例如 defaultdict 和 Counter)在定義\_\_missing\_\_ 時,這個魔術方法事實上只屬於該子類,也就是說,**它是一個誕生於子類中的魔術方法!** 據此,我有一個不成熟的猜想:\_\_getitem\_\_()會判斷當前物件是否是 dict 的子類,且是否擁有\_\_missing\_\_(),然後才會去呼叫它(如果父類中也有該方法,則不會先作判斷,而是直接就呼叫了)。 我在交流群裡說出了這個猜想,有同學很快在 CPython 原始碼中找到驗證: ![Python貓_群聊圖片.jpg](http://ww1.sinaimg.cn/large/68b02e3bgy1gl8r69r9uzj20y20u0q84.jpg) 而這就有意思了,**在內建型別的子類上才存在的魔術方法,** 縱觀整個 Python 世界,恐怕再難以找出第二例。 我突然有一個聯想:這神出鬼沒的\_\_missing\_\_(),就像是一個擅長玩“大變活人”的魔術師,先讓觀眾在外面透過玻璃看到他(即官方文件),然而揭開門時,他並不在裡面(即內建型別),再變換一下道具,他又完好無損就出現了(即 dict 的子類)。 ## 3、被施魔法的\_\_missing\_\_() \_\_missing\_\_() 的神奇之處,除了它本身會變“魔術”之外,它還需要一股強大的“魔法”才能驅動。 在[上篇文章中](https://mp.weixin.qq.com/s/FtlVNTYq60KSPti2xD4psA),我發現原生的魔術方法間相互獨立,它們在 C 語言介面可能有相同的核心邏輯,但是在 Python 語言介面,卻並不存在著呼叫關係: ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gkokkxoyapj20z60jcdgy.jpg) 魔術方法的這種“老死不相往來”的表現,違背了一般的程式碼複用原則,也是導致內建型別的子類會出現某些奇怪表現的原因。 官方 Python 寧肯提供新的 UserString、UserList、UserDict 子類,也不願意複用魔術方法,唯一合理的解釋似乎是令魔術方法相互呼叫的代價太大。 但是,對於特例\_\_missing\_\_(),Python 卻不得不妥協,不得不付出這種代價! \_\_missing\_\_() 是魔術方法的“**二等公民** ”,它沒有獨立的呼叫入口,只能被動地由 \_\_getitem\_\_() 呼叫,即\_\_missing\_\_() 依賴於\_\_getitem\_\_()。 不同於那些“**一等公民** ”,例如 \_\_init\_\_()、\_\_enter\_\_()、\_\_len\_\_()、\_\_eq\_\_() 等等,它們要麼是在物件生命週期或執行過程的某個節點被觸發,要麼由某個內建函式或操作符觸發,這些都是相對獨立的事件,無所依賴。 **_\_missing\_\_() 依賴於\_\_getitem\_\_(),才能實現方法呼叫;而 \_\_getitem\_\_() 也要依賴 \_\_missing\_\_(),才能實現完整功能。** 為了實現這一點,\_\_getitem\_\_()在直譯器程式碼中開了個後門,從 C 語言介面折返回 Python 介面,去呼叫那個名為“_\_missing\_\_”的特定方法。 ![](http://ww1.sinaimg.cn/large/68b02e3bgy1glmapfk9dyj20xi0jlmyk.jpg) 而這就是真正的“魔法”了,目前為止,_\_missing\_\_()似乎是唯一一個享受了此等待遇的魔術方法! ## 4、小結 Python 的字典提供了兩種取值的內建方法,即\_\_getitem\_\_() 和 get(),當取值不存在時,它們的處理策略是不一樣的:**前者會報錯`KeyError`,而後者會返回 None。** 為什麼 Python 要提供兩個不同的方法呢?或者應該問,為什麼 Python 要令這兩個方法做出不一樣的處理呢? 這可能有一個很複雜(也可能是很簡單)的解釋,本文暫不深究了。 不過有一點是可以確定的:即原生 dict 型別簡單粗暴地拋`KeyError` 的做法有所不足。 為了讓字典型別有更強大的表現(或者說讓\_\_getitem\_\_()作出 get() 那樣的表現),Python 讓字典的子類可以定義_\_missing\_\_(),供\_\_getitem\_\_()查詢呼叫。 本文梳理了_\_missing\_\_()的實現原理,從而揭示出它並非是一個毫不起眼的存在,恰恰相反,**它是唯一一個打破了魔術方法間壁壘,支援被其它魔術方法呼叫的特例!** Python 為了維持魔術方法的獨立性,不惜煞費苦心地引入了 UserString、UserList、UserDict 這些派生類,但是對於 _\_missing\_\_(),它卻選擇了妥協。 本文揭示出了這個魔術方法的神祕之處,不知你讀後有何感想呢?歡迎留言