<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          為什么繼承 Python 內(nèi)置類(lèi)型會(huì)出問(wèn)題?!

          共 6064字,需瀏覽 13分鐘

           ·

          2020-11-17 01:34

          △點(diǎn)擊上方“Python貓”關(guān)注 ,回復(fù)“1”領(lǐng)取電子書(shū)

          作者:豌豆花下貓

          來(lái)源:Python貓

          不久前,Python貓 給大家推薦了一本書(shū)《流暢的Python》(點(diǎn)擊可跳轉(zhuǎn)閱讀),那篇文章有比較多的“溢美之詞”,顯得比較空泛……

          但是,《流暢的Python》一書(shū)值得反復(fù)回看,可以溫故知新。最近我偶然翻到書(shū)中一個(gè)有點(diǎn)詭異的知識(shí)點(diǎn),因此準(zhǔn)備來(lái)聊一聊這個(gè)話題——子類(lèi)化內(nèi)置類(lèi)型可能會(huì)出問(wèn)題?!

          1、內(nèi)置類(lèi)型有哪些?

          在正式開(kāi)始之前,我們首先要科普一下:哪些是 Python 的內(nèi)置類(lèi)型?

          根據(jù)官方文檔的分類(lèi),內(nèi)置類(lèi)型(Built-in Types)主要包含如下內(nèi)容:

          詳細(xì)文檔:https://docs.python.org/3/library/stdtypes.html

          其中,有大家熟知的數(shù)字類(lèi)型、序列類(lèi)型、文本類(lèi)型、映射類(lèi)型等等,當(dāng)然還有我們之前介紹過(guò)的布爾類(lèi)型...對(duì)象 等等。

          在這么多內(nèi)容里,本文只關(guān)注那些作為可調(diào)用對(duì)象(callable)的內(nèi)置類(lèi)型,也就是跟內(nèi)置函數(shù)(built-in function)在表面上相似的那些:int、str、list、tuple、range、set、dict……

          這些類(lèi)型(type)可以簡(jiǎn)單理解成其它語(yǔ)言中的類(lèi)(class),但是 Python 在此并沒(méi)有用習(xí)慣上的大駝峰命名法,因此容易讓人產(chǎn)生一些誤解。

          在 Python 2.2 之后,這些內(nèi)置類(lèi)型可以被子類(lèi)化(subclassing),也就是可以被繼承(inherit)。

          2、內(nèi)置類(lèi)型的子類(lèi)化

          眾所周知,對(duì)于某個(gè)普通對(duì)象 x,Python 中求其長(zhǎng)度需要用到公共的內(nèi)置函數(shù) len(x),它不像 Java 之類(lèi)的面向?qū)ο笳Z(yǔ)言,后者的對(duì)象一般擁有自己的 x.length() 方法。(PS:關(guān)于這兩種設(shè)計(jì)風(fēng)格的分析,推薦閱讀 這篇文章

          現(xiàn)在,假設(shè)我們要定義一個(gè)列表類(lèi),希望它擁有自己的 length() 方法,同時(shí)保留普通列表該有的所有特性。

          實(shí)驗(yàn)性的代碼如下(僅作演示):

          #?定義一個(gè)list的子類(lèi)
          class?MyList(list):
          ????def?length(self):
          ????????return?len(self)

          我們令 MyList這個(gè)自定義類(lèi)繼承 list,同時(shí)新定義一個(gè) length() 方法。這樣一來(lái),MyList 就擁有 append()、pop() 等等方法,同時(shí)還擁有 length() 方法。

          #?添加兩個(gè)元素
          ss?=?MyList()
          ss.append("Python")
          ss.append("貓")

          print(ss.length())???#?輸出:2

          前面提到的其它內(nèi)置類(lèi)型,也可以這樣作子類(lèi)化,應(yīng)該不難理解。

          順便發(fā)散一下,內(nèi)置類(lèi)型的子類(lèi)化有何好處/使用場(chǎng)景呢?

          有一個(gè)很直觀的例子,當(dāng)我們?cè)谧远x的類(lèi)里面,需要頻繁用到一個(gè)列表對(duì)象時(shí)(給它添加/刪除元素、作為一個(gè)整體傳遞……),這時(shí)候如果我們的類(lèi)繼承自 list,就可以直接寫(xiě) self.append()、self.pop(),或者將 self 作為一個(gè)對(duì)象傳遞,從而不用額外定義一個(gè)列表對(duì)象,在寫(xiě)法上也會(huì)簡(jiǎn)潔一些。

          還有其它的好處/使用場(chǎng)景么?歡迎大家留言討論~~

          3、內(nèi)置類(lèi)型子類(lèi)化的“問(wèn)題”

          終于要進(jìn)入本文的正式主題了:)

          通常而言,在我們教科書(shū)式的認(rèn)知中,子類(lèi)中的方法會(huì)覆蓋父類(lèi)的同名方法,也就是說(shuō),子類(lèi)方法的查找優(yōu)先級(jí)要高于父類(lèi)方法。

          下面看一個(gè)例子,父類(lèi) Cat,子類(lèi) PythonCat,都有一個(gè) say() 方法,作用是說(shuō)出當(dāng)前對(duì)象的 inner_voice:

          #?Python貓是一只貓
          class?Cat():
          ????def?say(self):
          ????????return?self.inner_voice()
          ????def?inner_voice(self):
          ????????return?"喵"
          class?PythonCat(Cat):
          ????def?inner_voice(self):
          ????????return?"喵喵"

          當(dāng)我們創(chuàng)建子類(lèi) PythonCat 的對(duì)象時(shí),它的 say() 方法會(huì)優(yōu)先取到自己定義出的 inner_voice() 方法,而不是 Cat 父類(lèi)的 inner_voice() 方法:

          my_cat?=?PythonCat()
          #?下面的結(jié)果符合預(yù)期
          print(my_cat.inner_voice())?#?輸出:喵喵
          print(my_cat.say())?????????#?輸出:喵喵

          這是編程語(yǔ)言約定俗成的慣例,是一個(gè)基本原則,學(xué)過(guò)面向?qū)ο缶幊袒A(chǔ)的同學(xué)都應(yīng)該知道。

          然而,當(dāng) Python 在實(shí)現(xiàn)繼承時(shí),似乎不完全會(huì)按照上述的規(guī)則運(yùn)作。它分為兩種情況:

          • 符合常識(shí):對(duì)于用 Python 實(shí)現(xiàn)的類(lèi),它們會(huì)遵循“子類(lèi)先于父類(lèi)”的原則
          • 違背常識(shí):對(duì)于實(shí)際是用 C 實(shí)現(xiàn)的類(lèi)(即str、list、dict等等這些內(nèi)置類(lèi)型),在顯式調(diào)用子類(lèi)方法時(shí),會(huì)遵循“子類(lèi)先于父類(lèi)”的原則;但是,**在存在隱式調(diào)用時(shí),**它們似乎會(huì)遵循“父類(lèi)先于子類(lèi)”的原則,即通常的繼承規(guī)則會(huì)在此失效

          對(duì)照 PythonCat 的例子,相當(dāng)于說(shuō),直接調(diào)用 my_cat.inner_voice() 時(shí),會(huì)得到正確的“喵喵”結(jié)果,但是在調(diào)用 my_cat.say() 時(shí),則會(huì)得到超出預(yù)期的“喵”結(jié)果。

          下面是《流暢的Python》中給出的例子(12.1章節(jié)):

          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]}

          在這個(gè)例子中,dd['two'] 會(huì)直接調(diào)用子類(lèi)的__setitem__()方法,所以結(jié)果符合預(yù)期。如果其它測(cè)試也符合預(yù)期的話,最終結(jié)果會(huì)是{'three': [3, 3], 'one': [1, 1], 'two': [2, 2]}。

          然而,初始化和 update() 直接調(diào)用的分別是從父類(lèi)繼承的__init__()和__update__(),再由它們隱式地調(diào)用__setitem__()方法,此時(shí)卻并沒(méi)有調(diào)用子類(lèi)的方法,而是調(diào)用了父類(lèi)的方法,導(dǎo)致結(jié)果超出預(yù)期!

          官方 Python 這種實(shí)現(xiàn)雙重規(guī)則的做法,有點(diǎn)違背大家的常識(shí),如果不加以注意,搞不好就容易踩坑。

          那么,為什么會(huì)出現(xiàn)這種例外的情況呢?

          4、內(nèi)置類(lèi)型的方法的真面目

          我們知道了內(nèi)置類(lèi)型不會(huì)隱式地調(diào)用子類(lèi)覆蓋的方法,接著,就是Python貓的刨根問(wèn)底時(shí)刻:為什么它不去調(diào)用呢?

          流暢的Python》書(shū)中沒(méi)有繼續(xù)追問(wèn),不過(guò),我試著胡亂猜測(cè)一下(應(yīng)該能從源碼中得到驗(yàn)證):內(nèi)置類(lèi)型的方法都是用 C 語(yǔ)言實(shí)現(xiàn)的,事實(shí)上它們彼此之間并不存在著相互調(diào)用,所以就不存在調(diào)用時(shí)的查找優(yōu)先級(jí)問(wèn)題。

          也就是說(shuō),前面的“__init__()和__update__()會(huì)隱式地調(diào)用__setitem__()方法”這種說(shuō)法并不準(zhǔn)確!

          這幾個(gè)魔術(shù)方法其實(shí)是相互獨(dú)立的!__init__()有自己的 setitem 實(shí)現(xiàn),并不會(huì)調(diào)用父類(lèi)的__setitem__(),當(dāng)然跟子類(lèi)的__setitem__()就更沒(méi)有關(guān)系了。

          從邏輯上理解,字典的__init__()方法中包含__setitem__()的功能,因此我們以為前者會(huì)調(diào)用后者,**這是慣性思維的體現(xiàn),**然而實(shí)際的調(diào)用關(guān)系可能是這樣的:

          左側(cè)的方法打開(kāi)語(yǔ)言界面之門(mén)進(jìn)入右側(cè)的世界,在那里實(shí)現(xiàn)它的所有使命,并不會(huì)折返回原始界面查找下一步的指令(即不存在圖中的紅線路徑)。不折返的原因很簡(jiǎn)單,即 C 語(yǔ)言間代碼調(diào)用效率更高,實(shí)現(xiàn)路徑更短,實(shí)現(xiàn)過(guò)程更簡(jiǎn)單。

          同理,dict 類(lèi)型的 get() 方法與__getitem__()也不存在調(diào)用關(guān)系,如果子類(lèi)只覆蓋了__getitem__()的話,當(dāng)子類(lèi)調(diào)用 get() 方法時(shí),實(shí)際會(huì)使用到父類(lèi)的 get() 方法。(PS:關(guān)于這一點(diǎn),《流暢的Python》及 PyPy 文檔的描述都不準(zhǔn)確,它們誤以為 get() 方法會(huì)調(diào)用__getitem__())

          也就是說(shuō),Python 內(nèi)置類(lèi)型的方法本身不存在調(diào)用關(guān)系,盡管它們?cè)诘讓?C 語(yǔ)言實(shí)現(xiàn)時(shí),可能存在公共的邏輯或能被復(fù)用的方法。

          我想到了“Python為什么”系列曾分析過(guò)的《Python 為什么能支持任意的真值判斷?》。在我們寫(xiě)if xxx時(shí),它似乎會(huì)隱式地調(diào)用__bool__()和__len__()魔術(shù)方法,然而實(shí)際上程序依據(jù) POP_JUMP_IF_FALSE 指令,會(huì)直接進(jìn)入純 C 代碼的邏輯,并不存在對(duì)這倆魔術(shù)方法的調(diào)用!

          因此,在意識(shí)到 C 實(shí)現(xiàn)的特殊方法間相互獨(dú)立之后,我們?cè)倩仡^看內(nèi)置類(lèi)型的子類(lèi)化,就會(huì)有新的發(fā)現(xiàn):

          父類(lèi)的__init__()魔術(shù)方法會(huì)打破語(yǔ)言界面實(shí)現(xiàn)自己的使命,然而它跟子類(lèi)的__setitem__()并不存在通路,即圖中紅線路徑不可達(dá)。

          特殊方法間各行其是,由此,我們會(huì)得出跟前文不同的結(jié)論:實(shí)際上 Python 嚴(yán)格遵循了“子類(lèi)方法先于父類(lèi)方法”繼承原則,并沒(méi)有破壞常識(shí)!

          最后值得一提的是,__missing__()是一個(gè)特例?!读鲿车腜ython》僅僅簡(jiǎn)單而含糊地寫(xiě)了一句,沒(méi)有過(guò)多展開(kāi)。

          經(jīng)過(guò)初步實(shí)驗(yàn),我發(fā)現(xiàn)當(dāng)子類(lèi)定義了此方法時(shí),get() 讀取不存在的 key 時(shí),正常返回 None;但是 __getitem__() 和 dd['xxx'] 讀取不存在的 key 時(shí),都會(huì)按子類(lèi)定義的__missing__()進(jìn)行處理。

          我還沒(méi)空深入分析,懇請(qǐng)知道答案的同學(xué)給我留言。

          5、內(nèi)置類(lèi)型子類(lèi)化的最佳實(shí)踐

          綜上所述,內(nèi)置類(lèi)型子類(lèi)化時(shí)并沒(méi)有出問(wèn)題,只是由于我們沒(méi)有認(rèn)清特殊方法(C 語(yǔ)言實(shí)現(xiàn)的方法)的真面目,才會(huì)導(dǎo)致結(jié)果偏差。

          那么,這又召喚出了一個(gè)新的問(wèn)題:如果非要繼承內(nèi)置類(lèi)型,最佳的實(shí)踐方式是什么呢?

          首先,如果在繼承內(nèi)置類(lèi)型后,并不重寫(xiě)(overwrite)它的特殊方法的話,子類(lèi)化就不會(huì)有任何問(wèn)題。

          其次,如果繼承后要重寫(xiě)特殊方法的話,記得要把所有希望改變的方法都重寫(xiě)一遍,例如,如果想改變 get() 方法,就要重寫(xiě) get() 方法,如果想改變 __getitem__()方法,就要重寫(xiě)它……

          但是,如果我們只是想重寫(xiě)某種邏輯(即 C 語(yǔ)言的部分),以便所有用到該邏輯的特殊方法都發(fā)生改變的話,例如重寫(xiě)__setitem__()的邏輯,同時(shí)令初始化和update()等操作跟著改變,那么該怎么辦呢?

          我們已知特殊方法間不存在復(fù)用,也就是說(shuō)單純定義新的__setitem__()是不夠的,那么,怎么才能對(duì)多個(gè)方法同時(shí)產(chǎn)生影響呢?

          PyPy 這個(gè)非官方的 Python 版本發(fā)現(xiàn)了這個(gè)問(wèn)題,它的做法是令內(nèi)置類(lèi)型的特殊方法發(fā)生調(diào)用,建立它們之間的連接通路。

          官方 Python 當(dāng)然也意識(shí)到了這么問(wèn)題,不過(guò)它并沒(méi)有改變內(nèi)置類(lèi)型的特性,而是提供出了新的方案:UserString、UserList、UserDict……

          除了名字不一樣,基本可以認(rèn)為它們等同于內(nèi)置類(lèi)型。

          這些類(lèi)的基本邏輯是用 Python 實(shí)現(xiàn)的,相當(dāng)于是把前文 C 語(yǔ)言界面的某些邏輯搬到了 Python 界面,在左側(cè)建立起調(diào)用鏈,如此一來(lái),就解決了某些特殊方法的復(fù)用問(wèn)題。

          對(duì)照前文的例子,采用新的繼承方式后,結(jié)果就符合預(yù)期了:

          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 的話,最佳的實(shí)踐就是繼承collections庫(kù)提供的那幾個(gè)類(lèi)。

          6、小結(jié)

          寫(xiě)了這么多,是時(shí)候作 ending 了~~

          在本系列的前一篇文章中,Python貓從查找順序與運(yùn)行速度兩方面,分析了“為什么內(nèi)置函數(shù)/內(nèi)置類(lèi)型不是萬(wàn)能的”,本文跟它一脈相承,也是揭示了內(nèi)置類(lèi)型的某種神秘的看似是缺陷的行為特征。

          本文雖然是從《流暢的Python》書(shū)中獲得的靈感,然而在語(yǔ)言表象之外,我們還多追問(wèn)了一個(gè)“為什么”,從而更進(jìn)一步地分析出了現(xiàn)象背后的原理。

          簡(jiǎn)而言之,內(nèi)置類(lèi)型的特殊方法是由 C 語(yǔ)言獨(dú)立實(shí)現(xiàn)的,它們?cè)?Python 語(yǔ)言界面中不存在調(diào)用關(guān)系,因此在內(nèi)置類(lèi)型子類(lèi)化時(shí),被重寫(xiě)的特殊方法只會(huì)影響該方法本身,不會(huì)影響其它特殊方法的效果。

          如果我們對(duì)特殊方法間的關(guān)系有錯(cuò)誤的認(rèn)知,就可能會(huì)認(rèn)為 Python 破壞了“子類(lèi)方法先于父類(lèi)方法”的基本繼承原則。(很遺憾《流暢的Python》和 PyPy 都有此錯(cuò)誤的認(rèn)知)

          為了迎合大家對(duì)內(nèi)置類(lèi)型的普遍預(yù)期,Python 在標(biāo)準(zhǔn)庫(kù)中提供了 UserString、UserList、UserDict 這些擴(kuò)展類(lèi),方便程序員來(lái)繼承這些基本的數(shù)據(jù)類(lèi)型。

          寫(xiě)在最后:本文屬于“Python為什么”系列(Python貓出品),該系列主要關(guān)注 Python 的語(yǔ)法、設(shè)計(jì)和發(fā)展等話題,以一個(gè)個(gè)“為什么”式的問(wèn)題為切入點(diǎn),試著展現(xiàn) Python 的迷人魅力。若你有其它感興趣的話題,歡迎填在《Python的十萬(wàn)個(gè)為什么? 》里的調(diào)查問(wèn)卷中。

          Python貓技術(shù)交流群開(kāi)放啦!群里既有國(guó)內(nèi)一二線大廠在職員工,也有國(guó)內(nèi)外高校在讀學(xué)生,既有十多年碼齡的編程老鳥(niǎo),也有中小學(xué)剛剛?cè)腴T(mén)的新人,學(xué)習(xí)氛圍良好!想入群的同學(xué),請(qǐng)?jiān)诠?hào)內(nèi)回復(fù)『交流群』,獲取貓哥的微信(謝絕廣告黨,非誠(chéng)勿擾!)~

          近期熱門(mén)文章推薦:

          為什么說(shuō) Python 內(nèi)置函數(shù)并不是萬(wàn)能的?
          為什么 Python 多線程無(wú)法利用多核?
          Python 為什么不支持 switch 語(yǔ)句?
          詳解 Python 的二元算術(shù)運(yùn)算,為什么說(shuō)減法只是語(yǔ)法糖?

          瀏覽 31
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  97超碰在线免费观看 | 精品人妻一区二区三区含羞草 | 日韩激情网 | 成人亚洲性情网站www在线 | 午夜男女福利 |