python的下劃線后众,繼承規(guī)則
打星號原因之一是因為這篇文章內(nèi)容太多凶赁,沒有寫完。二是因為這篇文章的難度深度很大臊恋,需要仔細揣摩才可以理解衣洁。由于大家對源碼不熟悉,所以只好用通俗而晦澀的語言解釋出來抖仅,抽空再做些補充(已經(jīng)補充完畢)闸与。
不要騙我,你是不是無數(shù)次的被python中坑爹的下劃線弄的暈頭轉(zhuǎn)向的岸售。不瞞你說我也曾經(jīng)被它弄的痛不欲生,那么現(xiàn)在我就幫你分析總結(jié)一下python中的下劃線到底代表什么意思厂画,看過這篇總結(jié)以后凸丸,任何下劃線都阻攔不了你reading的腳步。
變量:
python中的權(quán)限采用“你懂的”的規(guī)則袱院,不會顯式聲明給你屎慢,規(guī)則如下:
前面帶“_”的變量,例如:_var忽洛,標明是一個私有類型的變量腻惠,但是只起到標明的作用,外部類還是可以訪問到這個變量欲虚。但是帶有這樣的參數(shù)的變量或者包不能用from module import * 導(dǎo)入
前后帶兩個“_”的變量集灌,例如:__var,標明是內(nèi)置私有變量,外部類無法訪問,甚至是子類對象也無法訪問复哆。
大寫加下劃線的變量:標明是不會發(fā)生改變的全局變量欣喧,例如:USER_CONSTANT,這好比c++中的constant梯找。
函數(shù):
前面帶_的變量:標明是私有函數(shù)唆阿,同理只是用于標明而已。
前后兩個_的函數(shù)锈锤,標明是特殊函數(shù)(一般是module內(nèi)建函數(shù)驯鳖,比如init函數(shù),我們后面會講到)
注意:雙下劃線對解釋器有特殊的意義久免,我們在命名的時候一定要盡量避 免這種命名方式
接下來你可以看看這樣的一段代碼浅辙,可能會涉及到python的繼承方式,我們一并也都講了阎姥,這段內(nèi)容摘自CSDN某位博主摔握,我就借花獻佛了:
class A(object):
def __init__(self):
self.__private()
self.public()
def __private(self):
print 'A.__private()'
def public(self):
print 'A.public()'
class B(A):
def __private(self):
print 'B.__private()'
def public(self):
print 'B.public()'
b = B()
那么這段代碼的輸出是什么呢?
答案是:A.__private() B.public()
你肯定會很奇怪丁寄,這樣奇葩的輸出到底是怎么回事兒氨淌,產(chǎn)生疑問的原因也很簡單泊愧,因為你對python的機制還不了解,下面就進行分析(本來這一塊是放在python高級編程里講的盛正,這里遇到了一并就都倒出來吧):
一切都從為什么會輸出:A.__private()開始删咱。我們還是來看一下python的命名規(guī)則
根據(jù)python tutorial的說法,變量名(標識符)是python的一種原子元素(什么是原子元素豪筝,你可以參照數(shù)據(jù)庫操作的原子性來理解痰滋,也就是不可再分的),當(dāng)變量命名被綁定到一個對象的時候续崖,變量名就代指這個對象敲街,當(dāng)它出現(xiàn)在代碼塊中,它就是本地變量严望;當(dāng)它出現(xiàn)在模塊中它就是全局變量多艇。
根據(jù)該理論,我們可以把上述代碼塊分為三個代碼塊:類A的定義像吻、類B的定義和變量b的定義峻黍。類A定義了三個成員變量,類B定義了兩個成員變量拨匆。當(dāng)我們用dir(A)查看類A里面的東西的時候姆涩,我們發(fā)現(xiàn)了一些有趣的現(xiàn)象。
_A__private
__class__
__delattr__
__dict__
__doc__
__format__
__getattribute__
__hash__
__init__
__module__
__new__
__reduce__
__reduce_ex__
__repr__
__setattr__
__sizeof__
__str__
__subclasshook__
__weakref__
public
實際上惭每,除去init以外骨饿,我們發(fā)現(xiàn)A中我們額外定義的幾個類屬性變成了這樣:
_A__private
public
很明顯,問題出在了__private()上台腥。這里我們要深究一下python解釋器對私有類型成員的處理細節(jié)样刷。
當(dāng)在類中定義了私有變量的時候,在代碼正式生成以前览爵,python會自動的將這個私有變量轉(zhuǎn)換為長格式(變?yōu)楣灿校?/code>轉(zhuǎn)換的機制是這樣的:
在變量前端插入類名置鼻,再在前端插入一個下劃線字符。
這就是所謂的私有變量軋壓(跟我讀:ya ya)(private name mangling)蜓竹。這也就是類A中_A__private出現(xiàn)的原因箕母。
(其實可以從另一個角度去理解,表面上它聲明為私有變量而禁止別人訪問但是私底下還是要給自己類內(nèi)部的人訪問的俱济,所以要轉(zhuǎn)換一下)
注意1:因為軋壓會使得變量名字變長嘶是,所以一旦超過了255個字符以后,python會進行切斷蛛碌,要特別注意因此導(dǎo)致的命名沖突聂喇。
注意2:當(dāng)類名全部以下劃線命名的時候,軋壓規(guī)則失效。
嘗試把上述類A換成“_____”
那么當(dāng)類A經(jīng)過軋壓之后希太,它的代碼變成了:
class A(object):
def __init__(self):
self._A__private() # 這行變了
self.public()
def _A__private(self): # 這行也變了
print 'A.__private()'
def public(self):
print 'A.public()'
因為類B定義的時候沒有覆蓋init方法克饶,所以調(diào)用的仍然是A.init,即執(zhí)行了self._A__private().(這一點就涉及到下面要講的python的繼承了)
最后在講繼承之前還有一點特別的重要誊辉,剛剛dir()出來的一堆東西矾湃,除了我們自己定義的,還有系統(tǒng)為每個類自定義的屬性和方法
堕澄,那么它們有什么用呢邀跃?我們這里詳細講解一下:
自定義屬性:
- class.__doc__ 類型幫助信息
- class.__name__ 類型名稱
- class.__dict__ 類型字典,存儲所有該類實例成員的信息,如果這么說你不明白的話蛙紫,請自行print 類名.dict觀察拍屑。
- class.__class__ 類類型】痈担可能你會覺得奇怪僵驰,那是因為Python里面所有的東西都是對象,甚至比java都要面向?qū)ο蟛靡稀T凇秔ython源碼分析》的時候我會講到。
- class.__module__ 類型所在的模塊
自定義方法(也稱保留方法):
目的 代碼 實際調(diào)用
初始化 x=class() x.__init__()
字符串官方表現(xiàn) repr(x) x.__repr__()
字符串非正式 str(x) x.__str__()
字節(jié)數(shù)組非正式值 bytes(x) x.__bytes__()
格式化字符串的值
format(x,format_spec) x.__format__(format_spec)
注意:
str()一般是將數(shù)值轉(zhuǎn)成字符串继准。
repr()是將一個對象轉(zhuǎn)成字符串顯示枉证,注意只是顯示用,有些對象轉(zhuǎn)成字符串沒有直接的意思移必。如list,dict使用str()是無效的室谚,但使用repr可以,這是為了看它們都有哪些值崔泵,為了顯示之用秒赤。 類似于java的toString方法。
在最后憎瘸,我們再給大家總結(jié)一下python中常見的下劃線函數(shù)的作用以及其調(diào)用情況入篮,這一部分要求多看多想。
part1:迭代器類似類
序號 目的 所編寫代碼 Python 實際調(diào)用
① 遍歷某個序列 iter(seq) seq.__iter__()
② 從迭代器中獲取下一個值 next(seq) seq.__next__()
③ 按逆序創(chuàng)建一個迭代器 reversed(seq) seq.__reversed__()
無論何時創(chuàng)建迭代器都將調(diào)用 __iter__() 方法幌甘。這是用初始值對迭代器進行初始化的絕佳之處潮售。
無論何時從迭代器中獲取下一個值都將調(diào)用 __next__() 方法。
__reversed__() 方法并不常用锅风。它以一個現(xiàn)有序列為參數(shù)酥诽,并將該序列中所有元素從尾到頭以逆序排列生成一個新的迭代器。
part2 :計算屬性 (這一部分非常難理解皱埠,有時間我再下面更新講解)
序號 目的 所編寫代碼 Python 實際調(diào)用
① 獲取一個計算屬性(無條件的) x.my_property x.__getattribute__('my_property')
② 獲取一個計算屬性(后備) x.my_property
x.__getattr__('my_property')
③ 設(shè)置某屬性 x.my_property = value x.__setattr__('my_property',value)
④ 刪除某屬性 del x.my_property x.__delattr__('my_property')
⑤ 列出所有屬性和方法 dir(x) x.__dir__()
如果某個類定義了 __getattribute__() 方法肮帐,在 每次引用屬性或方法名稱時 Python 都調(diào)用它(特殊方法名稱除外,因為那樣將會導(dǎo)致討厭的無限循環(huán))边器。
如果某個類定義了 __getattr__() 方法训枢,Python 將只在正常的位置查詢屬性時才會調(diào)用它托修。如果實例 x 定義了屬性 color, x.color 將 不會 調(diào)用x.__getattr__('color')肮砾;而只會返回 x.color 已定義好的值诀黍。
無論何時給屬性賦值,都會調(diào)用 __setattr__() 方法仗处。
無論何時刪除一個屬性眯勾,都將調(diào)用 __delattr__() 方法。
如果定義了 __getattr__() 或 __getattribute__() 方法婆誓, __dir__() 方法將非常有用吃环。通常,調(diào)用 dir(x) 將只顯示正常的屬性和方法洋幻。如果 __getattr()__方法動態(tài)處理 color 屬性郁轻, dir(x) 將不會將 color 列為可用屬性∥牧簦可通過覆蓋 __dir__() 方法允許將 color 列為可用屬性好唯,對于想使用你的類但卻不想深入其內(nèi)部的人來說,該方法非常有益燥翅。
part3:可比較的類
序號 目的 所編寫代碼 Python 實際調(diào)用
相等 x == y x.__eq__(y)
不相等 x != y x.__ne__(y)
小于 x < y x.__lt__(y)
小于或等于 x <= y x.__le__(y)
大于 x > y x.__gt__(y)
大于或等于 x >= y x.__ge__(y)
布爾上上下文環(huán)境中的真值 if x: x.__bool__()
如果定義了 __lt__() 方法但沒有定義 __gt__() 方法骑篙,Python 將通過經(jīng)交換的算子調(diào)用 __lt__() 方法。然而森书,Python 并不會組合方法靶端。例如,如果定義了 __lt__() 方法和 __eq()__ 方法凛膏,并試圖測試是否 x <= y杨名,Python 不會按順序調(diào)用 __lt__() 和 __eq()__ 。它將只調(diào)用__le__() 方法猖毫。
part4:可序列化的類
Python 支持任意對象的序列化和反序列化台谍。(多數(shù) Python 參考資料稱該過程為 “pickling” 和 “unpickling”)。該技術(shù)對與將狀態(tài)保存為文件并在稍后恢復(fù)它非常有意義吁断。所有的 內(nèi)置數(shù)據(jù)類型 均已支持 pickling 典唇。如果創(chuàng)建了自定義類,且希望它能夠 pickle胯府,閱讀 pickle 協(xié)議 了解下列特殊方法何時以及如何被調(diào)用介衔。
序號 目的 所編寫代碼 Python 實際調(diào)用
自定義對象的復(fù)制 copy.copy(x) x.__copy__()
自定義對象的深度復(fù)制 copy.deepcopy(x) x.__deepcopy__()
在 pickling 之前獲取對象的狀態(tài) pickle.dump(x, file) x.__getstate__()
序列化某對象 pickle.dump(x, file) x.__reduce__()
序列化某對象(新 pickling 協(xié)議) pickle.dump(x, file, protocol_version) x.__reduce_ex__(protocol_version)
* 控制 unpickling 過程中對象的創(chuàng)建方式 x = pickle.load(file) x.__getnewargs__()
* 在 unpickling 之后還原對象的狀態(tài) x = pickle.load(file) x.__setstate__()
* 要重建序列化對象,Python 需要創(chuàng)建一個和被序列化的對象看起來一樣的新對象骂因,然后設(shè)置新對象的所有屬性炎咖。__getnewargs__() 方法控制新對象的創(chuàng)建過程,而 __setstate__() 方法控制屬性值的還原方式。
part5:something amazing
如果知道自己在干什么乘盼,你幾乎可以完全控制類是如何比較的升熊、屬性如何定義,以及類的子類是何種類型绸栅。
序號 目的 所編寫代碼 Python 實際調(diào)用
類構(gòu)造器 x = MyClass() x.__new__()
*類析構(gòu)器 del x x.__del__()
只定義特定集合的某些屬性
x.__slots__()
自定義散列值 hash(x) x.__hash__()
獲取某個屬性的值 x.color type(x).__dict__['color'].__get__(x, type(x))
設(shè)置某個屬性的值 x.color = 'PapayaWhip' type(x).__dict__['color'].__set__(x, 'PapayaWhip')
刪除某個屬性 del x.color type(x).__dict__['color'].__del__(x)
控制某個對象是否是該對象的實例 your class isinstance(x, MyClass) MyClass.__instancecheck__(x)
控制某個類是否是該類的子類 issubclass(C, MyClass) MyClass.__subclasscheck__(C)
控制某個類是否是該抽象基類的子類 issubclass(C, MyABC) MyABC.__subclasshook__(C)
上面就簡單科普python下劃線的知識级野,接著我們談?wù)刾ython的重點問題:繼承
繼承之功能不贅述,繼承之特點也是各有千秋粹胯,C++的多繼承蓖柔,java的單繼承,父類方法的調(diào)用機制等等风纠,基礎(chǔ)不扎實也夠喝一壺了况鸣。python繼承機制與平時所理解的有所不同。
先來說說super竹观,我們平時把父類也叫做超類镐捧,那么super也是調(diào)用父類屬性(attribute)的時候時候所用到的關(guān)鍵字,對沒錯臭增,關(guān)鍵字懂酱,但是super在python里面是一個內(nèi)建類型,盡管它的使用方法和函數(shù)有點兒類似誊抛,但是它實際上還是一個內(nèi)建類型列牺。(什么是內(nèi)建類型?顧名思義就是內(nèi)部構(gòu)建的類芍锚,諸如None昔园,數(shù)值類型蔓榄,字符串類型)
>>>super
<type 'super'>
這就證實了我所言非虛并炮,如果你已經(jīng)習(xí)慣了直接調(diào)用父類并將self作為第一個參數(shù),來訪問類型的特性甥郑,super的用法可能讓你有點兒混亂逃魄。(關(guān)于self的用法,我也會在后面的文章中作為補充澜搅,作為最基本的你就記著定義類方法一定要用self作為第一個參數(shù))我們可以看看下面的代碼:
>>> class Father(object):
... def say(self):
... print('Hello my child')
...
>>> class Child(Father):
... def say(self):
... Father.say(self)
... print('clean your bedroom')
...
>>>Tom = Child()
>>>Tom.say()
Hello my child
clean your bedroom
上述這段代碼是沒有問題伍俘,但是你是不是有疑問在里面。粗心的人可能覺得這沒問題啊勉躺,先顯示調(diào)用父類癌瘾,再調(diào)子類。沒錯饵溅,F(xiàn)ather.say( )這一行確實調(diào)用了超類的say( )方法,將self作為第一個參數(shù)傳入妨退。但是它傳遞self,是Child的self。拋開這個疑問不管咬荷,我們再來看看如果要用super實現(xiàn)相同效果的話該怎么用:
>>>class Child(Father):
... def say(self):
... super(Father, self).say()
,,, print 'clean your bedroom'
如上所示冠句,簡單的二重繼承,你可能覺得問題不大幸乒,但面對多重繼承的時候懦底,super無論是在使用上還是閱讀上都是非常費力的,比較而言第一種不用super的方式還是比較符合繼承邏輯的罕扎。
那么在如何避免使用super以及解釋我們剛剛的問題之前聚唐,我們先要來看看python中的方法解析順序(MRO)。這個較之于軋壓壳影,又有所不同拱层。
在python2.3以前的版本(當(dāng)然了,我們現(xiàn)在最常用的是2.7宴咧,但是官方力推的是3.0以上的版本根灯,比如html解析器在3.0左右的版本就已經(jīng)支持的不是很好了,我們在閱讀一門語言規(guī)范的時候當(dāng)然要比較一下各個版本的做出了哪一塊兒的改進)掺栅,類繼承是按照舊的C3規(guī)則進行的烙肺。在舊的C3規(guī)則中,如果一個類有兩個祖先氧卧,MRO計算很簡單:
class Base1 class Base2
\ /
\ /
class MyClass
>>>class Base1:
... pass
...
>>>class Base2:
... def method(self):
... print 'Base2'
...
>>>class MyClass(Base1, Base2):
... pass
...
>>>here.MyClass()
>>>here.method()
Base2
以上的解析順序是:當(dāng)here.method被調(diào)用的時候桃笙,解釋程序?qū)⒉檎襇yClass中的方法,然后在Base1中查找沙绝,最后在Base2中查找搏明。
好的,上面的內(nèi)容可能沒什么難事闪檬,因為它還在我們正常的認知范圍內(nèi)~那么現(xiàn)在我們在兩個父類上面加一個公共類星著,然后你再猜一下代碼的輸出是什么。
class BaseBase
/ \
/ \
class Base1 class Base2
\ /
\ /
class MyClass
>>>class BaseBase:
... def method(self):
... print 'BaseBase'
...
>>>class Base1(BaseBase):
... pass
...
>>>class Base2(BaseBase):
... def method(self):
... print 'Base2'
...
>>>class MyClass(Base1, Base2):
... pass
...
>>>here = MyClass()
>>>here.method()
好了粗悯,你可以猜一下現(xiàn)在的輸出是什么了虚循,如果你猜不到,那就請繼續(xù)耐心的看完這篇教程样傍。
答案是:BaseBase
我們沒有意圖去解釋舊的python規(guī)則中的這種繼承現(xiàn)象横缔,而且無論是源代碼中還是實際應(yīng)用中,這種繼承方式也是極為罕見的衫哥。但正是由于這種舊的MRO規(guī)則會產(chǎn)生這種古怪的輸出茎刚,2.3以后的較新版本中的輸出變?yōu)榱耍?code>Base2。
如此古怪的輸出結(jié)果撤逢,導(dǎo)致我們想問一個問題膛锭,Python的繼承輸出結(jié)果還是不是可以預(yù)測的?
我們來簡單解釋一下MRO的規(guī)則:MRO說白了其實就是一顆繼承樹
我們來檢測一下MyClass中的繼承順序:
>>>def L(klass):
... print [k.__name__ for k in klass.__mro__]
...
>>>L(MyClass)
['MyClass', 'Base1', 'Base2', 'BaseBase', 'object']
tips:
你也可以直接用print MyClass.__mro__來查看,或者用MyClass.mro()捌斧,這就是上面我們講到的下劃線函數(shù)的實際調(diào)用問題。
那么現(xiàn)在我們就能夠明白泉沾,子父類同名方法的調(diào)用是要遵循MRO樹的順序的捞蚂。然后你還需要記住的是python的調(diào)用都是顯示聲明的
。
希望沒有把你繞暈跷究,讓我們回到super這里來
我們前面已經(jīng)講過了一種多重繼承姓迅,當(dāng)發(fā)生多重繼承的時候,普通的調(diào)用可能也會陷入困境俊马,那么super就更不必說丁存,多重繼承使用super是相當(dāng)危險的,原因在于python類的初始化函數(shù)柴我,更進一步在于python父類的初始化函數(shù)需要我們顯示的調(diào)用
解寝,我們來看看這么一個歷程,來自:http://fuhm.net/super-harmful
混用super和傳統(tǒng)調(diào)用:
class A(object):
def __init__(self):
print 'A'
super(A,self).__init__()
class B(object):
def __init__(self):
print 'B'
super(B,self).__init__()
class C(A,B):
def __init__(self):
print 'C'
A.__init__(self)
B.__init__(self)
print 'MRO: ', [x.__name__ for x in C.__mro__]
c = C()
大膽猜一下輸出:
MRO: ['C', 'A', 'B', 'object']
C A B B
很詭異啊,為什么是這種輸出艘儒,為什么多了一個B呢聋伦?MRO樹里面的關(guān)系明明沒有重復(fù)項,而且調(diào)用的順序也是我們按照自己意愿聲明的界睁,為什么多了一個B觉增?
原因是:C實例調(diào)用了A.__init_(self),這樣一來,我們在A中的super(A,self).___init_____( )函數(shù)將調(diào)用B的構(gòu)造程序翻斟,還不理解逾礁?那么我們看看上面的MyClass的調(diào)用順序就一目了然了,如果它想調(diào)用根類的函數(shù)访惜,它是按照優(yōu)先兄弟的順序來調(diào)用的嘹履。這么看來super在python的用法就特別的混亂。
我們的經(jīng)驗是如果你想將一個父類子類話债热,你應(yīng)該先檢查一下這個父類的mro特性砾嫉,如果mro不存在,那么說明這個類是一個舊式的類阳柔,python還沒有將它加入mro特性焰枢,那我們?yōu)榱税踩蛻?yīng)該避免使用super蚓峦。
如果一個類_mro_特性舌剂,則快速的檢查一下MRO所涉及的類的構(gòu)造程序代碼,如果到處都使用了super暑椰,那你也可以使用它霍转,否則你就試著保持一致性而不要混用。
那么都使用super一汽,拒絕混用會不會達到理想的效果呢避消?讓我們來看下面一段代碼:
>>>class BaseBase(object):
def __init__(self):
print 'basebase'
super(BaseBase, self).__init__()
>>>class Base1(BaseBase):
def __init__(self):
print 'base1'
super(Base1, self).__init__()
>>>class Base2(BaseBase):
def __init__(self, arg):
print 'base2'
super(Base2, self)
>>>class MyClass(Base1, Base2):
def __init__(self, arg):
print 'my base'
super(MyClass, self).__init__(arg)
>>>m = MyClass(10)
然后它不僅輸出了my base,而且還輸出了一堆錯誤低滩,看了一下TypeError: __init__() tales exactly 1 argument(2 given).
嗯哼,super大法也不是那哪兒都好用的岩喷,可能你說恕沫,不就是一個參數(shù)問題么,腦洞開一下纱意,python是可以接受動態(tài)參數(shù)的婶溯,對啊,*args和**kw確實是可以解決這個問題呢偷霉。把上面的:
__init__(self, arg)迄委、__init__(self)
全部改成:__init__(self, *args, **kw)
這樣做確實解決了我們的bug,贊类少!得到正確的輸出:
my base
base1
base2
basebase
但是這是一種糟糕的修復(fù)方法叙身,因為它使所有的構(gòu)造函數(shù)接受所有類型的參數(shù),就好比屋頂?shù)教幎际锹┒戳蚰叶春艽笮沤危裁椿覊m垃圾蟲子都能進來,同理残吩,這會使我們的代碼變得極度脆弱虏两。
上面我們了解了super濫用導(dǎo)致的代碼可讀性,然后我們也看到了使用顯示調(diào)用也并不能給我們帶來多少優(yōu)惠世剖,其實我們應(yīng)該抱著這樣一個思想定罢,本來??就不可能跑得比汽車快,你怎么鞭笞它旁瘫,也是于事無補的祖凫。就像python本來對多繼承支持的就不是很好,我們何必要強python所難呢酬凳。
所以當(dāng)我們用到繼承的時候惠况,我們一定要提醒自己以下幾點:
- 一定要避免多重繼承,如果非用不可宁仔,我們會有相應(yīng)的設(shè)計模式替代稠屠。(后續(xù)再更新)
- 如果不使用多繼承,super的使用就必須一致翎苫,在類層次結(jié)構(gòu)中权埠,應(yīng)該在所有的地方都使用super或者是徹底不使用它。
- 不要混用老式類和新式類煎谍,如何辨別參見MRO攘蔽。
- 調(diào)用父類的時候必須檢查類層次,為了避免出現(xiàn)任何問題呐粘,每次調(diào)用父類的時候我們不要偷懶满俗,我們在終端可以快速的查看所涉及的MRO(使用方法上面已經(jīng)說明了)