圖文詳解Python中最神秘的一個魔法函數
前言
一個非常神秘的魔術方法。
這個方法非常不起眼,用途狹窄,我幾乎從未註意過它,然而,當發現它可能是上述“定律”的唯一例外情況時,我認為值得再寫一篇文章來詳細審視一下它。
本文主要關註的問題有:(1) missing()到底是何方神聖?(2) missing()有什麼特別之處?擅長“大變活人”魔術?
(3) missing()是否真的是上述發現的例外?如果是的話,為什麼會有這種特例?
1、有點價值的missing()
從普通的字典中取值時,可能會出現 key 不存在的情況:
dd = {'name':'PythonCat'} dd.get('age') # 結果:None dd.get('age', 18) # 結果:18 dd['age'] # 報錯 KeyError dd.__getitem__('age') # 等同於 dd['age']
對於 get() 方法,它是有返回值的,而且可以傳入第二個參數,作為 key 不存在時的返回內容,因此還可以接受。但是,另外兩種寫法都會報錯。
為瞭解決後兩種寫法的問題,就可以用到 missing() 魔術方法。
現在,假設我們有一個這樣的訴求:從字典中取某個 key 對應的 value,如果有值則返回值,如果沒有值則插入 key,並且給它一個默認值(例如一個空列表)。
如果用原生的 dict,並不太好實現,但是,Python 提供瞭一個非常好用的擴展類collections.defaultdict
:
如圖所示,當取不存在的 key 時,沒有再報 KeyError,而是默認存入到字典中。
為什麼 defaultdict 可以做到這一點呢?
原因是 defaultdict 在繼承瞭內置類型 dict 之後,還定義瞭一個 missing() 方法,當 getitem取不存在的值時,它就會調用入參中傳入的工廠函數(上例是調用 list(),創建空列表)。
作為最典型的示例,defaultdict 在文檔註釋中寫到:
簡而言之,missing()的主要作用就是由getitem在缺失 key 時調用,從而避免出現 KeyError。
另外一個典型的使用例子是collections.Counter
,它也是 dict 的子類,在取未被統計的 key 時,返回計數 0:
2、神出鬼沒的missing()
由上可知,missing()在getitem()取不到值時會被調用,但是,我不經意間還發現瞭一個細節:getitem()在取不到值時,並不一定會調用missing()。
這是因為它並非內置類型的必要屬性,並沒有在字典基類中被預先定義。
如果你直接從 dict 類型中取該屬性值,會報屬性不存在:AttributeError: type object 'object' has no attribute '__missing__'
。
使用 dir() 查看,發現確實不存在該屬性:
如果從 dict 的父類即 object 中查看,也會發現同樣的結果。
這是怎麼回事呢?為什麼在 dict 和 object 中都沒有missing屬性呢?
然而,查閱最新的官方文檔,object 中分明包含這個屬性:
出處:3. Data model — Python 3.10.1 documentationmissing#object.missing
也就是說,理論上 object 類中會預定義missing,其文檔證明瞭這一點,然而實際上它並沒有被定義!文檔與現實出現瞭偏差!
如此一來,當 dict 的子類(例如 defaultdict 和 Counter)在定義missing 時,這個魔術方法事實上隻屬於該子類,也就是說,它是一個誕生於子類中的魔術方法!
據此,我有一個不成熟的猜想:getitem()會判斷當前對象是否是 dict 的子類,且是否擁有missing(),然後才會去調用它(如果父類中也有該方法,則不會先作判斷,而是直接就調用瞭)。
我在交流群裡說出瞭這個猜想,有同學很快在 CPython 源碼中找到驗證:
而這就有意思瞭,在內置類型的子類上才存在的魔術方法,縱觀整個 Python 世界,恐怕再難以找出第二例。
我突然有一個聯想:這神出鬼沒的missing(),就像是一個擅長玩“大變活人”的魔術師,先讓觀眾在外面透過玻璃看到他(即官方文檔),然而揭開門時,他並不在裡面(即內置類型),再變換一下道具,他又完好無損就出現瞭(即 dict 的子類)。
3、被施魔法的missing()
missing() 的神奇之處,除瞭它本身會變“魔術”之外,它還需要一股強大的“魔法”才能驅動。
我發現原生的魔術方法間相互獨立,它們在 C 語言界面可能有相同的核心邏輯,但是在 Python 語言界面,卻並不存在著調用關系:
魔術方法的這種“老死不相往來”的表現,違背瞭一般的代碼復用原則,也是導致內置類型的子類會出現某些奇怪表現的原因。
官方 Python 寧肯提供新的 UserString、UserList、UserDict 子類,也不願意復用魔術方法,唯一合理的解釋似乎是令魔術方法相互調用的代價太大。
但是,對於特例missing(),Python 卻不得不妥協,不得不付出這種代價!
missing() 是魔術方法的“二等公民”,它沒有獨立的調用入口,隻能被動地由 getitem() 調用,即missing() 依賴於getitem()。
不同於那些“一等公民”,例如 init()、enter()、len()、eq() 等等,它們要麼是在對象生命周期或執行過程的某個節點被觸發,要麼由某個內置函數或操作符觸發,這些都是相對獨立的事件,無所依賴。
missing() 依賴於getitem(),才能實現方法調用;而 getitem() 也要依賴 missing(),才能實現完整功能。
為瞭實現這一點,getitem()在解釋器代碼中開瞭個後門,從 C 語言界面折返回 Python 界面,去調用那個名為“missing”的特定方法。
而這就是真正的“魔法”瞭,目前為止,missing()似乎是唯一一個享受瞭此等待遇的魔術方法!
4、小結
Python 的字典提供瞭兩種取值的內置方法,即getitem() 和 get(),當取值不存在時,它們的處理策略是不一樣的:前者會報錯KeyError
,而後者會返回 None。
為什麼 Python 要提供兩個不同的方法呢?或者應該問,為什麼 Python 要令這兩個方法做出不一樣的處理呢?
這可能有一個很復雜(也可能是很簡單)的解釋,本文暫不深究瞭。
不過有一點是可以確定的:即原生 dict 類型簡單粗暴地拋KeyError
的做法有所不足。
為瞭讓字典類型有更強大的表現(或者說讓getitem()作出 get() 那樣的表現),Python 讓字典的子類可以定義missing(),供getitem()查找調用。
本文梳理瞭missing()的實現原理,從而揭示出它並非是一個毫不起眼的存在,恰恰相反,它是唯一一個打破瞭魔術方法間壁壘,支持被其它魔術方法調用的特例!
Python 為瞭維持魔術方法的獨立性,不惜煞費苦心地引入瞭 UserString、UserList、UserDict 這些派生類,但是對於 missing(),它卻選擇瞭妥協。
本文揭示出瞭這個魔術方法的神秘之處,不知你讀後有何感想呢?
5、總結
到此這篇關於Python中最神秘的一個魔法函數的文章就介紹到這瞭,更多相關Python魔法函數內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Python中最神秘missing()函數介紹
- Python 中的 Counter 模塊及使用詳解(搞定重復計數)
- 詳解Python中Addict模塊的使用方法
- python中defaultdict字典功能特性介紹
- python中defaultdict字典功能特性解析