為什麼繼承 Python 內建型別會出問題?!

itread01 2020-11-15 16:15:14
Python itread01


> 本文出自“Python為什麼”系列,請檢視[全部文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUyOTk2MTcwNg==&action=getalbum&album_id=1338406397180084225&subscene=0&scenenote=https%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzUyOTk2MTcwNg%3D%3D%26mid%3D2247485945%26idx%3D1%26sn%3D02f1ac9a690f57accefeed7a1ea1247b%26chksm%3Dfa584e7ccd2fc76af5d45ebbc43c1e4d379a0fcfeee8ce70111a8a293c4b7efc8ac7a82dfd6a%26xtrack%3D1%26scene%3D0%26subscene%3D91%26sessionid%3D1596284425%26clicktime%3D1596284604%26enterid%3D1596284604%26ascene%3D7%26devicetype%3Dandroid-28%26version%3D2700103f%26nettype%3DWIFI%26abtest_cookie%3DAAACAA%253D%253D%26lang%3Dzh_CN%26exportkey%3DAa1rI96xIRQ8cDJChmQS9BU%253D%26pass_ticket%3DP%252BUyocSqsqUN5JuCQOyjZNpQH%252Fwm0bsN6NchdKKM9CFDDEu0ZPKsRpo8Utu4BBRc%26wx_header%3D1#wechat_redirect) 不久前,`Python貓` 給大家推薦了一本書《流暢的Python》([點選可跳轉閱讀](https://mp.weixin.qq.com/s/A4_DD2fvceNk1apn9MQcXA)),那篇文章有比較多的“溢美之詞”,顯得比較空泛…… 但是,《流暢的Python》一書值得反覆回看,可以溫故知新。最近我偶然翻到書中一個有點詭異的知識點,因此準備來聊一聊這個話題——**子類化內建型別可能會出問題?!** ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gk0qx3a7iuj20uq0hnq8g.jpg) ## 1、內建型別有哪些? 在正式開始之前,我們首先要科普一下:**哪些是 Python 的內建型別?** 根據官方文件的分類,內建型別(Built-in Types)主要包含如下內容: ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gk1f8ng1ozj20m90d3my7.jpg) 詳細文件:https://docs.python.org/3/library/stdtypes.html 其中,有大家熟知的[數字型別](https://mp.weixin.qq.com/s/0XpPaH53II5yO9Lfh80ZOw)、序列型別、文字型別、對映型別等等,當然還有我們之前介紹過的[布林型別](https://mp.weixin.qq.com/s/JVhXjQKcd8uds8yTUumZJw)、[...物件](https://mp.weixin.qq.com/s/SOSN_p74eDHv3tJnSJIZfg) 等等。 在這麼多內容裡,本文只關注那些作為`可呼叫物件`(callable)的內建型別,也就是跟內建函式(built-in function)在表面上相似的那些:**int、str、list、tuple、range、set、dict……** 這些型別(type)可以簡單理解成其它語言中的類(class),但是 Python 在此並沒有用習慣上的大駝峰命名法,因此容易讓人產生一些誤解。 在 Python 2.2 之後,這些內建型別可以被子類化(subclassing),也就是可以被繼承(inherit)。 ## 2、內建型別的子類化 眾所周知,對於某個普通物件 x,Python 中求其長度需要用到公共的內建函式 len(x),它不像 Java 之類的面嚮物件語言,後者的物件一般擁有自己的 x.length() 方法。(PS:關於這兩種設計風格的分析,推薦閱讀 [這篇文章](https://mp.weixin.qq.com/s/pKQT5wvyaSNFvnJexiCC8w)) 現在,假設我們要定義一個列表類,希望它擁有自己的 length() 方法,同時保留普通列表該有的所有特性。 實驗性的程式碼如下(僅作演示): ```python # 定義一個list的子類 class MyList(list): def length(self): return len(self) ``` 我們令 MyList這個自定義類繼承 list,同時新定義一個 length() 方法。這樣一來,MyList 就擁有 append()、pop() 等等方法,同時還擁有 length() 方法。 ```python # 新增兩個元素 ss = MyList() ss.append("Python") ss.append("貓") print(ss.length()) # 輸出:2 ``` 前面提到的其它內建型別,也可以這樣作子類化,應該不難理解。 順便發散一下,**內建型別的子類化有何好處/使用場景呢?** 有一個很直觀的例子,當我們在自定義的類裡面,需要頻繁用到一個列表物件時(給它新增/刪除元素、作為一個整體傳遞……),這時候如果我們的類繼承自 list,就可以直接寫 self.append()、self.pop(),或者將 self 作為一個物件傳遞,從而不用額外定義一個列表物件,在寫法上也會簡潔一些。 還有其它的好處/使用場景麼?歡迎大家留言討論~~ ## 3、內建型別子類化的“問題” 終於要進入本文的正式主題了:) 通常而言,在我們教科書式的認知中,**子類中的方法會覆蓋父類的同名方法,也就是說,子類方法的查詢優先順序要高於父類方法。** 下面看一個例子,父類 Cat,子類 PythonCat,都有一個 say() 方法,作用是說出當前物件的 inner_voice: ```python # Python貓是一隻貓 class Cat(): def say(self): return self.inner_voice() def inner_voice(self): return "喵" class PythonCat(Cat): def inner_voice(self): return "喵喵" ``` 當我們建立子類 PythonCat 的物件時,它的 say() 方法會優先取到自己定義出的 inner_voice() 方法,而不是 Cat 父類的 inner_voice() 方法: ```python my_cat = PythonCat() # 下面的結果符合預期 print(my_cat.inner_voice()) # 輸出:喵喵 print(my_cat.say()) # 輸出:喵喵 ``` 這是程式語言約定俗成的慣例,是一個基本原則,學過面向物件程式設計基礎的同學都應該知道。 然而,當 Python 在實現繼承時,**似乎不完全**會按照上述的規則運作。它分為兩種情況: - 符合常識:對於用 Python 實現的類,它們會遵循“子類先於父類”的原則 - 違背常識:對於實際是用 C 實現的類(即str、list、dict等等這些內建型別),在顯式呼叫子類方法時,會遵循“子類先於父類”的原則;但是,**在存在隱式呼叫時,**它們似乎會遵循“父類先於子類”的原則,即通常的繼承規則會在此失效 對照 PythonCat 的例子,相當於說,直接呼叫 my_cat.inner_voice() 時,會得到正確的“喵喵”結果,但是在呼叫 my_cat.say() 時,則會得到超出預期的“喵”結果。 下面是《流暢的Python》中給出的例子(12.1章節): ```python class DoppelDict(dict): def __setitem__(self, key, value): super().__setitem__(key, [value] * 2) dd = DoppelDict(one=1) # {'one': 1} dd['two'] = 2 # {'one': 1, 'two': [2, 2]} dd.update(three=3) # {'three': 3, 'one': 1, 'two': [2, 2]} ``` ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gknw0m0nzuj20q90ag758.jpg) 在這個例子中,dd['two'] 會直接呼叫子類的\_\_setitem\_\_()方法,所以結果符合預期。如果其它測試也符合預期的話,最終結果會是{'three': [3, 3], 'one': [1, 1], 'two': [2, 2]}。 然而,初始化和 update() 直接呼叫的分別是從父類繼承的\_\_init\_\_()和\_\_update\_\_(),再由它們**隱式地**呼叫_\_setitem\_\_()方法,此時卻並沒有呼叫子類的方法,而是呼叫了父類的方法,導致結果超出預期! 官方 Python 這種實現雙重規則的做法,有點違背大家的常識,如果不加以注意,搞不好就容易踩坑。 那麼,為什麼會出現這種例外的情況呢? ## 4、內建型別的方法的真面目 我們知道了內建型別不會隱式地呼叫子類覆蓋的方法,接著,就是`Python貓`的刨根問底時刻:為什麼它不去呼叫呢? 《[流暢的Python](https://mp.weixin.qq.com/s/A4_DD2fvceNk1apn9MQcXA)》書中沒有繼續追問,不過,我試著胡亂猜測一下(應該能從原始碼中得到驗證):**內建型別的方法都是用 C 語言實現的,事實上它們彼此之間並不存在著相互呼叫,所以就不存在呼叫時的查詢優先順序問題。** 也就是說,前面的“\_\_init\_\_()和\_\_update\_\_()會隱式地呼叫_\_setitem\_\_()方法”這種說法並不準確! 這幾個魔術方法其實是相互獨立的!\_\_init\_\_()有自己的 setitem 實現,並不會呼叫父類的\_\_setitem\_\_(),當然跟子類的\_\_setitem\_\_()就更沒有關係了。 從邏輯上理解,字典的\_\_init\_\_()方法中包含\_\_setitem\_\_()的功能,因此我們以為前者會呼叫後者,**這是慣性思維的體現,**然而實際的呼叫關係可能是這樣的: ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gkok0fm3ucj20u10c574u.jpg) 左側的方法開啟語言介面之門進入右側的世界,在那裡實現它的所有使命,並不會折返回原始介面查詢下一步的指令(即不存在圖中的紅線路徑)。不折返的原因很簡單,即 C 語言間程式碼呼叫效率更高,實現路徑更短,實現過程更簡單。 同理,dict 型別的 get() 方法與\_\_getitem\_\_()也不存在呼叫關係,如果子類只覆蓋了\_\_getitem\_\_()的話,當子類呼叫 get() 方法時,實際會使用到父類的 get() 方法。(PS:關於這一點,《流暢的Python》及 PyPy 文件的描述都不準確,它們誤以為 get() 方法會呼叫\_\_getitem\_\_()) 也就是說,Python 內建型別的方法本身不存在呼叫關係,儘管它們在底層 C 語言實現時,可能存在公共的邏輯或能被複用的方法。 我想到了“[Python為什麼](https://github.com/chinesehuazhou/python-whydo)”系列曾分析過的《[Python 為什麼能支援任意的真值判斷?](https://mp.weixin.qq.com/s/g6jZX0IdH9xpM7BMV3-ToQ)》。在我們寫`if xxx`時,它似乎會隱式地呼叫\_\_bool\_\_()和\_\_len\_\_()魔術方法,然而實際上程式依據 POP_JUMP_IF_FALSE 指令,會直接進入純 C 程式碼的邏輯,並不存在對這倆魔術方法的呼叫! 因此,在意識到 C 實現的特殊方法間相互獨立之後,我們再回頭看內建型別的子類化,就會有新的發現: ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gkokkxoyapj20z60jcdgy.jpg) 父類的\_\_init\_\_()魔術方法會打破語言介面實現自己的使命,然而它跟子類的\_\_setitem\_\_()並不存在通路,即圖中紅線路徑不可達。 特殊方法間各行其是,由此,我們會得出跟前文不同的結論:**實際上 Python 嚴格遵循了“子類方法先於父類方法”繼承原則,並沒有破壞常識!** 最後值得一提的是,\_\_missing\_\_()是一個特例。《流暢的Python》僅僅簡單而含糊地寫了一句,沒有過多展開。 經過初步實驗,我發現當子類定義了此方法時,get() 讀取不存在的 key 時,正常返回 None;但是 \_\_getitem\_\_() 和 dd['xxx'] 讀取不存在的 key 時,都會按子類定義的\_\_missing\_\_()進行處理。 ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gkosoha3krj20f70a63ys.jpg) 我還沒空深入分析,懇請知道答案的同學給我留言。 ## 5、內建型別子類化的最佳實踐 綜上所述,內建型別子類化時並沒有出問題,只是由於我們沒有認清特殊方法(C 語言實現的方法)的真面目,才會導致結果偏差。 那麼,這又召喚出了一個新的問題:**如果非要繼承內建型別,最佳的實踐方式是什麼呢?** 首先,如果在繼承內建型別後,並不重寫(overwrite)它的特殊方法的話,子類化就不會有任何問題。 其次,如果繼承後要重寫特殊方法的話,記得要把所有希望改變的方法都重寫一遍,例如,如果想改變 get() 方法,就要重寫 get() 方法,如果想改變 \_\_getitem\_\_()方法,就要重寫它…… 但是,如果我們只是想重寫某種邏輯(即 C 語言的部分),以便所有用到該邏輯的特殊方法都發生改變的話,例如重寫\_\_setitem\_\_()的邏輯,同時令初始化和update()等操作跟著改變,那麼該怎麼辦呢? 我們已知特殊方法間不存在複用,也就是說單純定義新的\_\_setitem\_\_()是不夠的,那麼,怎麼才能對多個方法同時產生影響呢? PyPy 這個非官方的 Python 版本發現了這個問題,它的做法是令內建型別的特殊方法發生呼叫,建立它們之間的連線通路。 官方 Python 當然也意識到了這麼問題,不過它並沒有改變內建型別的特性,而是提供出了新的方案:UserString、UserList、UserDict…… ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gkotx6gj14j20rz04taap.jpg) 除了名字不一樣,基本可以認為它們等同於內建型別。 這些類的基本邏輯是用 Python 實現的,相當於是把前文 C 語言介面的某些邏輯搬到了 Python 介面,在左側建立起呼叫鏈,如此一來,就解決了某些特殊方法的複用問題。 對照前文的例子,採用新的繼承方式後,結果就符合預期了: ```python from collections import UserDict class DoppelDict(UserDict): def __setitem__(self, key, value): super().__setitem__(key, [value] * 2) dd = DoppelDict(one=1) # {'one': [1, 1]} dd['two'] = 2 # {'one': [1, 1], 'two': [2, 2]} dd.update(three=3) # {'one': [1, 1], 'two': [2, 2], 'three': [3, 3]} ``` 顯然,**如果要繼承 str/list/dict 的話,最佳的實踐就是繼承`collections`庫提供的那幾個類。** ## 6、小結 寫了這麼多,是時候作 ending 了~~ 在本系列的前一篇文章中,Python貓從查詢順序與執行速度兩方面,分析了“[為什麼內建函式/內建型別不是萬能的](https://mp.weixin.qq.com/s/YtfPlE9JAIS3tpLBGFo5ag)”,本文跟它一脈相承,也是揭示了內建型別的某種神祕的看似是缺陷的行為特徵。 本文雖然是從《流暢的Python》書中獲得的靈感,然而在語言表象之外,我們還多追問了一個“為什麼”,從而更進一步地分析出了現象背後的原理。 簡而言之,**內建型別的特殊方法是由 C 語言獨立實現的,它們在 Python 語言介面中不存在呼叫關係,因此在內建型別子類化時,被重寫的特殊方法只會影響該方法本身,不會影響其它特殊方法的效果。** 如果我們對特殊方法間的關係有錯誤的認知,就可能會認為 Python 破壞了“子類方法先於父類方法”的基本繼承原則。(很遺憾《流暢的Python》和 PyPy 都有此錯誤的認知) 為了迎合大家對內建型別的普遍預期,Python 在標準庫中提供了 UserString、UserList、UserDict 這些擴充套件類,方便程式設計師來繼承這些基本的資料型別。 寫在最後:本文屬於“[Python為什麼](https://github.com/chinesehuazhou/python-whydo)”系列(Python貓出品),該系列主要關注 Python 的語法、設計和發展等話題,以一個個“為什麼”式的問題為切入點,試著展現 Python 的迷人魅力。若你有其它感興趣的話題,歡迎填在《[Python的十萬個為什麼?](https://mp.weixin.qq.com/s/jobdpO7BWWON0ruLNpn31Q) 》裡的調查問卷中。 ![](http://ww1.sinaimg.cn/large/68b02e3bgy1gfffh3g28lj2076076q3e.jpg) 公眾號【**Python貓**】, 本號連載優質的系列文章,有Python為什麼系列、喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫作、優質英文推薦與翻譯等等,歡迎關
版权声明
本文为[itread01]所创,转载请带上原文链接,感谢
https://www.itread01.com/content/1605427385.html

  1. 利用Python爬虫获取招聘网站职位信息
  2. Using Python crawler to obtain job information of recruitment website
  3. Several highly rated Python libraries arrow, jsonpath, psutil and tenacity are recommended
  4. Python装饰器
  5. Python实现LDAP认证
  6. Python decorator
  7. Implementing LDAP authentication with Python
  8. Vscode configures Python development environment!
  9. In Python, how dare you say you can't log module? ️
  10. 我收藏的有关Python的电子书和资料
  11. python 中 lambda的一些tips
  12. python中字典的一些tips
  13. python 用生成器生成斐波那契数列
  14. python脚本转pyc踩了个坑。。。
  15. My collection of e-books and materials about Python
  16. Some tips of lambda in Python
  17. Some tips of dictionary in Python
  18. Using Python generator to generate Fibonacci sequence
  19. The conversion of Python script to PyC stepped on a pit...
  20. Python游戏开发,pygame模块,Python实现扫雷小游戏
  21. Python game development, pyGame module, python implementation of minesweeping games
  22. Python实用工具,email模块,Python实现邮件远程控制自己电脑
  23. Python utility, email module, python realizes mail remote control of its own computer
  24. 毫无头绪的自学Python,你可能连门槛都摸不到!【最佳学习路线】
  25. Python读取二进制文件代码方法解析
  26. Python字典的实现原理
  27. Without a clue, you may not even touch the threshold【 Best learning route]
  28. Parsing method of Python reading binary file code
  29. Implementation principle of Python dictionary
  30. You must know the function of pandas to parse JSON data - JSON_ normalize()
  31. Python实用案例,私人定制,Python自动化生成爱豆专属2021日历
  32. Python practical case, private customization, python automatic generation of Adu exclusive 2021 calendar
  33. 《Python实例》震惊了,用Python这么简单实现了聊天系统的脏话,广告检测
  34. "Python instance" was shocked and realized the dirty words and advertisement detection of the chat system in Python
  35. Convolutional neural network processing sequence for Python deep learning
  36. Python data structure and algorithm (1) -- enum type enum
  37. 超全大厂算法岗百问百答(推荐系统/机器学习/深度学习/C++/Spark/python)
  38. 【Python进阶】你真的明白NumPy中的ndarray吗?
  39. All questions and answers for algorithm posts of super large factories (recommended system / machine learning / deep learning / C + + / spark / Python)
  40. [advanced Python] do you really understand ndarray in numpy?
  41. 【Python进阶】Python进阶专栏栏主自述:不忘初心,砥砺前行
  42. [advanced Python] Python advanced column main readme: never forget the original intention and forge ahead
  43. python垃圾回收和缓存管理
  44. java调用Python程序
  45. java调用Python程序
  46. Python常用函数有哪些?Python基础入门课程
  47. Python garbage collection and cache management
  48. Java calling Python program
  49. Java calling Python program
  50. What functions are commonly used in Python? Introduction to Python Basics
  51. Python basic knowledge
  52. Anaconda5.2 安装 Python 库(MySQLdb)的方法
  53. Python实现对脑电数据情绪分析
  54. Anaconda 5.2 method of installing Python Library (mysqldb)
  55. Python implements emotion analysis of EEG data
  56. Master some advanced usage of Python in 30 seconds, which makes others envy it
  57. python爬取百度图片并对图片做一系列处理
  58. Python crawls Baidu pictures and does a series of processing on them
  59. python链接mysql数据库
  60. Python link MySQL database