本文介紹了Python中單下劃線和雙下劃線("dunder")的各種含義和命名約定负蚊,名稱修飾(name mangling)的工作原理饰及,以及它如何影響你自己的Python類。
單下劃線和雙下劃線在Python變量和方法名稱中都各有其含義层亿。有一些含義僅僅是依照約定,被視作是對程序員的提示诗箍,而有一些含義是由Python解釋器嚴格執(zhí)行的。
如果你想知道“Python變量和方法名稱中單下劃線和雙下劃線的含義是什么挽唉?”滤祖,我會盡我所能在這里為你解答。
在本文中瓶籽,我將討論以下五種下劃線模式和命名約定匠童,以及它們?nèi)绾斡绊慞ython程序的行為:
·單前導下劃線:_var
·單末尾下劃線:var_
·雙前導下劃線:__var
·雙前導和末尾下劃線:__var__
·單下劃線:_
在文章結(jié)尾處,你可以找到一個簡短的“速查表”塑顺,總結(jié)了五種不同的下劃線命名約定及其含義俏让,可讓你親身體驗它們的行為。
讓我們馬上開始茬暇!
1. 單前導下劃線 _var
當涉及到變量和方法名稱時,單個下劃線前綴有一個約定俗成的含義寡喝。 它是對程序員的一個提示 - 意味著Python社區(qū)一致認為它應該是什么意思糙俗,但程序的行為不受影響。
下劃線前綴的含義是告知其他程序員:以單個下劃線開頭的變量或方法僅供內(nèi)部使用预鬓。 該約定在PEP 8中有定義巧骚。
這不是Python強制規(guī)定的。 Python不像Java那樣在“私有”和“公共”變量之間有很強的區(qū)別格二。 這就像有人提出了一個小小的下劃線警告標志劈彪,說:
“嘿,這不是真的要成為類的公共接口的一部分顶猜。不去管它就好沧奴。“
看看下面的例子:
classTest:
???def?__init__(self):
???????self.foo?=?11
???????self._bar?=?23
如果你實例化此類长窄,并嘗試訪問在__init__構(gòu)造函數(shù)中定義的foo和_bar屬性滔吠,會發(fā)生什么情況纲菌? 讓我們來看看:
>>>?t?=?Test()
>>>?t.foo
11
>>>?t._bar
23
你會看到_bar中的單個下劃線并沒有阻止我們“進入”類并訪問該變量的值。
這是因為Python中的單個下劃線前綴僅僅是一個約定 - 至少相對于變量和方法名而言疮绷。
但是翰舌,前導下劃線的確會影響從模塊中導入名稱的方式。
假設(shè)你在一個名為my_module的模塊中有以下代碼:
#?This?is?my_module.py:
def?external_func():
???return23
def?_internal_func():
???return42
現(xiàn)在冬骚,如果使用通配符從模塊中導入所有名稱椅贱,則Python不會導入帶有前導下劃線的名稱(除非模塊定義了覆蓋此行為的__all__列表):
>>>?from?my_module?import?*
>>>?external_func()
23
>>>?_internal_func()
NameError:?"name?'_internal_func'?is?not?defined"
順便說一下,應該避免通配符導入只冻,因為它們使名稱空間中存在哪些名稱不清楚庇麦。 為了清楚起見,堅持常規(guī)導入更好属愤。
與通配符導入不同女器,常規(guī)導入不受前導單個下劃線命名約定的影響:
>>>?import?my_module
>>>?my_module.external_func()
23
>>>?my_module._internal_func()
42
我知道這一點可能有點令人困惑。 如果你遵循PEP 8推薦住诸,避免通配符導入驾胆,那么你真正需要記住的只有這個:
單個下劃線是一個Python命名約定,表示這個名稱是供內(nèi)部使用的贱呐。 它通常不由Python解釋器強制執(zhí)行丧诺,僅僅作為一種對程序員的提示。
2. 單末尾下劃線 var_
有時候奄薇,一個變量的最合適的名稱已經(jīng)被一個關(guān)鍵字所占用驳阎。 因此,像class或def這樣的名稱不能用作Python中的變量名稱馁蒂。 在這種情況下呵晚,你可以附加一個下劃線來解決命名沖突:
>>>?def?make_object(name,?class):
SyntaxError:?"invalid?syntax"
>>>?def?make_object(name,?class_):
...????pass
總之,單個末尾下劃線(后綴)是一個約定沫屡,用來避免與Python關(guān)鍵字產(chǎn)生命名沖突饵隙。 PEP 8解釋了這個約定。
3. 雙前導下劃線 __var
到目前為止沮脖,我們所涉及的所有命名模式的含義金矛,來自于已達成共識的約定。 而對于以雙下劃線開頭的Python類的屬性(包括變量和方法)勺届,情況就有點不同了驶俊。
雙下劃線前綴會導致Python解釋器重寫屬性名稱,以避免子類中的命名沖突免姿。
這也叫做名稱修飾(name mangling) - 解釋器更改變量的名稱饼酿,以便在類被擴展的時候不容易產(chǎn)生沖突。
我知道這聽起來很抽象胚膊。 因此嗜湃,我組合了一個小小的代碼示例來予以說明:
classTest:
???def?__init__(self):
???????self.foo?=?11
???????self._bar?=?23
???????self.__baz?=?23
讓我們用內(nèi)置的dir()函數(shù)來看看這個對象的屬性:
>>>?t?=?Test()
>>>?dir(t)
['_Test__baz',?'__class__',?'__delattr__',?'__dict__',?'__dir__',
'__doc__',?'__eq__',?'__format__',?'__ge__',?'__getattribute__',
'__gt__',?'__hash__',?'__init__',?'__le__',?'__lt__',?'__module__',
'__ne__',?'__new__',?'__reduce__',?'__reduce_ex__',?'__repr__',
'__setattr__',?'__sizeof__',?'__str__',?'__subclasshook__',
'__weakref__',?'_bar',?'foo']
以上是這個對象屬性的列表奈应。 讓我們來看看這個列表,并尋找我們的原始變量名稱foo购披,_bar和__baz - 我保證你會注意到一些有趣的變化杖挣。
·self.foo變量在屬性列表中顯示為未修改為foo。
·self._bar的行為方式相同 - 它以_bar的形式顯示在類上刚陡。 就像我之前說過的惩妇,在這種情況下,前導下劃線僅僅是一個約定筐乳。 給程序員一個提示而已歌殃。
·然而,對于self.__baz而言蝙云,情況看起來有點不同氓皱。 當你在該列表中搜索__baz時,你會看不到有這個名字的變量勃刨。
__baz出什么情況了波材?
如果你仔細觀察,你會看到此對象上有一個名為_Test__baz的屬性身隐。 這就是Python解釋器所做的名稱修飾廷区。 它這樣做是為了防止變量在子類中被重寫。
讓我們創(chuàng)建另一個擴展Test類的類贾铝,并嘗試重寫構(gòu)造函數(shù)中添加的現(xiàn)有屬性:
classExtendedTest(Test):
???def?__init__(self):
???????super().__init__()
???????self.foo?=?'overridden'
???????self._bar?=?'overridden'
???????self.__baz?=?'overridden'
現(xiàn)在隙轻,你認為foo,_bar和__baz的值會出現(xiàn)在這個ExtendedTest類的實例上嗎垢揩? 我們來看一看:
>>>?t2?=?ExtendedTest()
>>>?t2.foo
'overridden'
>>>?t2._bar
'overridden'
>>>?t2.__baz
AttributeError:?"'ExtendedTest'?object?has?no?attribute?'__baz'"
等一下玖绿,當我們嘗試查看t2 .__ baz的值時,為什么我們會得到AttributeError叁巨? 名稱修飾被再次觸發(fā)了斑匪! 事實證明,這個對象甚至沒有__baz屬性:
>>>?dir(t2)
['_ExtendedTest__baz',?'_Test__baz',?'__class__',?'__delattr__',
'__dict__',?'__dir__',?'__doc__',?'__eq__',?'__format__',?'__ge__',
'__getattribute__',?'__gt__',?'__hash__',?'__init__',?'__le__',
'__lt__',?'__module__',?'__ne__',?'__new__',?'__reduce__',
'__reduce_ex__',?'__repr__',?'__setattr__',?'__sizeof__',?'__str__',
'__subclasshook__',?'__weakref__',?'_bar',?'foo',?'get_vars']
正如你可以看到__baz變成_ExtendedTest__baz以防止意外修改:
>>>?t2._ExtendedTest__baz
'overridden'
但原來的_Test__baz還在:
>>>?t2._Test__baz
42
雙下劃線名稱修飾對程序員是完全透明的俘种。 下面的例子證實了這一點:
classManglingTest:
???def?__init__(self):
???????self.__mangled?=?'hello'
???def?get_mangled(self):
???????returnself.__mangled
>>>?ManglingTest().get_mangled()
'hello'
>>>?ManglingTest().__mangled
AttributeError:?"'ManglingTest'?object?has?no?attribute?'__mangled'"
名稱修飾是否也適用于方法名稱? 是的绝淡,也適用宙刘。名稱修飾會影響在一個類的上下文中,以兩個下劃線字符("dunders")開頭的所有名稱:
classMangledMethod:
???def?__method(self):
???????return42
???def?call_it(self):
???????returnself.__method()
>>>?MangledMethod().__method()
AttributeError:?"'MangledMethod'?object?has?no?attribute?'__method'"
>>>?MangledMethod().call_it()
42
這是另一個也許令人驚訝的運用名稱修飾的例子:
_MangledGlobal__mangled?=?23
classMangledGlobal:
???def?test(self):
???????return__mangled
>>>?MangledGlobal().test()
23
在這個例子中牢酵,我聲明了一個名為_MangledGlobal__mangled的全局變量悬包。然后我在名為MangledGlobal的類的上下文中訪問變量。由于名稱修飾馍乙,我能夠在類的test()方法內(nèi)布近,以__mangled來引用_MangledGlobal__mangled全局變量垫释。
Python解釋器自動將名稱__mangled擴展為_MangledGlobal__mangled,因為它以兩個下劃線字符開頭撑瞧。這表明名稱修飾不是專門與類屬性關(guān)聯(lián)的棵譬。它適用于在類上下文中使用的兩個下劃線字符開頭的任何名稱。
有很多要吸收的內(nèi)容吧预伺。
老實說订咸,這些例子和解釋不是從我腦子里蹦出來的。我作了一些研究和加工才弄出來酬诀。我一直使用Python脏嚷,有很多年了,但是像這樣的規(guī)則和特殊情況并不總是浮現(xiàn)在腦海里瞒御。
有時候程序員最重要的技能是“模式識別”父叙,而且知道在哪里查閱信息。如果您在這一點上感到有點不知所措肴裙,請不要擔心趾唱。慢慢來,試試這篇文章中的一些例子践宴。
讓這些概念完全沉浸下來鲸匿,以便你能夠理解名稱修飾的總體思路,以及我向您展示的一些其他的行為阻肩。如果有一天你和它們不期而遇带欢,你會知道在文檔中按什么來查。
4. 雙前導和雙末尾下劃線 _var_
也許令人驚訝的是烤惊,如果一個名字同時以雙下劃線開始和結(jié)束乔煞,則不會應用名稱修飾。 由雙下劃線前綴和后綴包圍的變量不會被Python解釋器修改:
classPrefixPostfixTest:
???def?__init__(self):
???????self.__bam__?=?42
>>>?PrefixPostfixTest().__bam__
42
但是柒室,Python保留了有雙前導和雙末尾下劃線的名稱渡贾,用于特殊用途。 這樣的例子有雄右,__init__對象構(gòu)造函數(shù)空骚,或__call__ --- 它使得一個對象可以被調(diào)用。
這些dunder方法通常被稱為神奇方法 - 但Python社區(qū)中的許多人(包括我自己)都不喜歡這種方法擂仍。
最好避免在自己的程序中使用以雙下劃線(“dunders”)開頭和結(jié)尾的名稱囤屹,以避免與將來Python語言的變化產(chǎn)生沖突。
5.單下劃線 _
按照習慣逢渔,有時候單個獨立下劃線是用作一個名字肋坚,來表示某個變量是臨時的或無關(guān)緊要的。
例如,在下面的循環(huán)中智厌,我們不需要訪問正在運行的索引诲泌,我們可以使用“_”來表示它只是一個臨時值:
>>>?for_?in?range(32):
...????print('Hello,?World.')
你也可以在拆分(unpacking)表達式中將單個下劃線用作“不關(guān)心的”變量,以忽略特定的值铣鹏。 同樣敷扫,這個含義只是“依照約定”,并不會在Python解釋器中觸發(fā)特殊的行為吝沫。 單個下劃線僅僅是一個有效的變量名稱呻澜,會有這個用途而已。
在下面的代碼示例中惨险,我將汽車元組拆分為單獨的變量羹幸,但我只對顏色和里程值感興趣。 但是辫愉,為了使拆分表達式成功運行栅受,我需要將包含在元組中的所有值分配給變量。 在這種情況下恭朗,“_”作為占位符變量可以派上用場:
>>>?car?=?('red',?'auto',?12,?3812.4)
>>>?color,?_,?_,?mileage?=?car
>>>?color
'red'
>>>?mileage
3812.4
>>>?_
12
除了用作臨時變量之外屏镊,“_”是大多數(shù)Python REPL中的一個特殊變量,它表示由解釋器評估的最近一個表達式的結(jié)果痰腮。
這樣就很方便了而芥,比如你可以在一個解釋器會話中訪問先前計算的結(jié)果,或者膀值,你是在動態(tài)構(gòu)建多個對象并與它們交互棍丐,無需事先給這些對象分配名字:
>>>?20?+?3
23
>>>?_
23
>>>?print(_)
23
>>>?list()
[]
>>>?_.append(1)
>>>?_.append(2)
>>>?_.append(3)
>>>?_
[1,?2,?3]
Python下劃線命名模式 - 小結(jié)
以下是一個簡短的小結(jié),即“速查表”沧踏,羅列了我在本文中談到的五種Python下劃線模式的含義:
本文轉(zhuǎn)自:https://www.py.cn/faq/python/13869.html