1. 程式人生 > >Python中的下劃線

Python中的下劃線

Python中下劃線的5種含義

簡介

本篇介紹Python中單下劃線和雙下劃線(“dunder”)的各種含義和命名約定,名稱修飾(name mangling)的工作原理,以及它如何影響你自己的Python類。
單下劃線和雙下劃線在Python變數和方法名稱中都各有其含義。有一些含義僅僅是依照約定,被視作是對程式設計師的提示 - 而有一些含義是由Python直譯器嚴格執行的。

在本文中,我將討論以下五種下劃線模式和命名約定,以及它們如何影響Python程式的行為:

  • 單前導下劃線:_var
  • 單末尾下劃線:var_
  • 雙前導下劃線:__var
  • 雙前導和末尾下劃線:__var__
  • 單下劃線:_

在文章結尾處,你可以找到一個簡短的“速查表”,總結了五種不同的下劃線命名約定及其含義

1. 單前導下劃線 _var

當涉及到變數和方法名稱時,單個下劃線字首有一個約定俗成的含義。 它是對程式設計師的一個提示 - 意味著Python社群一致認為它應該是什麼意思,但程式的行為不受影響。
下劃線字首的含義是告知其他程式設計師:以單個下劃線開頭的變數或方法僅供內部使用。 該約定在PEP 8中有定義。
這不是Python強制規定的。 Python不像Java那樣在“私有”和“公共”變數之間有很強的區別。 這就像有人提出了一個小小的下劃線警告標誌,說:
“嘿,這不是真的要成為類的公共介面的一部分。不去管它就好。“
看看下面的例子:

class Test:
   def __init__(self):
       self.foo = 11
       self._bar = 23

如果你例項化此類,並嘗試訪問在__init__建構函式中定義的foo和_bar屬性,會發生什麼情況? 讓我們來看看:

>>> t = Test()
>>> t.foo
11
>>> t._bar
23

你會看到_bar中的單個下劃線並沒有阻止我們“進入”類並訪問該變數的值。
這是因為Python中的單個下劃線字首僅僅是一個約定 - 至少相對於變數和方法名而言。
但是,前導下劃線的確會影響從模組中匯入名稱的方式。

假設你在一個名為my_module的模組中有以下程式碼:

# This is my_module.py:

def external_func():
   return 23

def _internal_func():
   return 42

現在,如果使用萬用字元從模組中匯入所有名稱,則Python不會匯入帶有前導下劃線的名稱(除非模組定義了覆蓋此行為的__all__列表):

>>> from my_module import *
>>> external_func()
23
>>> _internal_func()
NameError: "name '_internal_func' is not defined"

順便說一下,應該避免萬用字元匯入,因為它們使名稱空間中存在哪些名稱不清楚。 為了清楚起見,堅持常規匯入更好。
與萬用字元匯入不同,常規匯入不受前導單個下劃線命名約定的影響:

>>> import my_module
>>> my_module.external_func()
23
>>> my_module._internal_func()
42

我知道這一點可能有點令人困惑。 如果你遵循PEP 8推薦,避免萬用字元匯入,那麼你真正需要記住的只有這個:
單個下劃線是一個Python命名約定,表示這個名稱是供內部使用的。 它通常不由Python直譯器強制執行,僅僅作為一種對程式設計師的提示。

2. 單末尾下劃線 var_

有時候,一個變數的最合適的名稱已經被一個關鍵字所佔用。 因此,像class或def這樣的名稱不能用作Python中的變數名稱。 在這種情況下,你可以附加一個下劃線來解決命名衝突:

>>> def make_object(name, class):
SyntaxError: "invalid syntax"

>>> def make_object(name, class_):
...    pass

總之,單個末尾下劃線(字尾)是一個約定,用來避免與Python關鍵字產生命名衝突。 PEP 8解釋了這個約定。

3. 雙前導下劃線 __var

到目前為止,我們所涉及的所有命名模式的含義,來自於已達成共識的約定。 而對於以雙下劃線開頭的Python類的屬性(包括變數和方法),情況就有點不同了。

雙下劃線字首會導致Python直譯器重寫屬性名稱,以避免子類中的命名衝突。
這也叫做名稱修飾(name mangling) - 直譯器更改變數的名稱,以便在類被擴充套件的時候不容易產生衝突。
我知道這聽起來很抽象。 因此,我組合了一個小小的程式碼示例來予以說明:

class Test:
   def __init__(self):
       self.foo = 11
       self._bar = 23
       self.__baz = 23

讓我們用內建的dir()函式來看看這個物件的屬性:

>>> 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直譯器所做的名稱修飾。 它這樣做是為了防止變數在子類中被重寫。
讓我們建立另一個擴充套件Test類的類,並嘗試重寫建構函式中新增的現有屬性:

class ExtendedTest(Test):
   def __init__(self):
       super().__init__()
       self.foo = 'overridden'
       self._bar = 'overridden'
       self.__baz = 'overridden'

現在,你認為foo,_bar和__baz的值會出現在這個ExtendedTest類的例項上嗎? 我們來看一看:

>>> t2 = ExtendedTest()
>>> t2.foo
'overridden'
>>> t2._bar
'overridden'
>>> t2.__baz
AttributeError: "'ExtendedTest' object has no attribute '__baz'"

等一下,當我們嘗試檢視t2 .__ baz的值時,為什麼我們會得到AttributeError? 名稱修飾被再次觸發了! 事實證明,這個物件甚至沒有__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

雙下劃線名稱修飾對程式設計師是完全透明的。 下面的例子證實了這一點:

class ManglingTest:
   def __init__(self):
       self.__mangled = 'hello'

   def get_mangled(self):
       return self.__mangled

>>> ManglingTest().get_mangled()
'hello'
>>> ManglingTest().__mangled
AttributeError: "'ManglingTest' object has no attribute '__mangled'"

名稱修飾是否也適用於方法名稱? 是的,也適用。名稱修飾會影響在一個類的上下文中,以兩個下劃線字元(“dunders”)開頭的所有名稱:

class MangledMethod:
   def __method(self):
       return 42

   def call_it(self):
       return self.__method()

>>> MangledMethod().__method()
AttributeError: "'MangledMethod' object has no attribute '__method'"
>>> MangledMethod().call_it()
42

這是另一個也許令人驚訝的運用名稱修飾的例子:

_MangledGlobal__mangled = 23

class MangledGlobal:
   def test(self):
       return __mangled

>>> MangledGlobal().test()
23

在這個例子中,我聲明瞭一個名為_MangledGlobal__mangled的全域性變數。然後我在名為MangledGlobal的類的上下文中訪問變數。由於名稱修飾,我能夠在類的test()方法內,以__mangled來引用_MangledGlobal__mangled全域性變數。

Python直譯器自動將名稱__mangled擴充套件為_MangledGlobal__mangled,因為它以兩個下劃線字元開頭。這表明名稱修飾不是專門與類屬性關聯的。它適用於在類上下文中使用的兩個下劃線字元開頭的任何名稱。

有很多要吸收的內容吧。

老實說,這些例子和解釋不是從我腦子裡蹦出來的。我作了一些研究和加工才弄出來。我一直使用Python,有很多年了,但是像這樣的規則和特殊情況並不總是浮現在腦海裡。

有時候程式設計師最重要的技能是“模式識別”,而且知道在哪裡查閱資訊。如果您在這一點上感到有點不知所措,請不要擔心。慢慢來,試試這篇文章中的一些例子。

讓這些概念完全沉浸下來,以便你能夠理解名稱修飾的總體思路,以及我向您展示的一些其他的行為。如果有一天你和它們不期而遇,你會知道在文件中按什麼來查。

4. 雙前導和雙末尾下劃線 var

也許令人驚訝的是,如果一個名字同時以雙下劃線開始和結束,則不會應用名稱修飾。 由雙下劃線字首和字尾包圍的變數不會被Python直譯器修改:

class PrefixPostfixTest:
   def __init__(self):
       self.__bam__ = 42

>>> PrefixPostfixTest().__bam__
42

但是,Python保留了有雙前導和雙末尾下劃線的名稱,用於特殊用途。 這樣的例子有,init__物件建構函式,或__call — 它使得一個物件可以被呼叫。
這些dunder方法通常被稱為神奇方法 - 但Python社群中的許多人(包括我自己)都不喜歡這種方法。
最好避免在自己的程式中使用以雙下劃線(“dunders”)開頭和結尾的名稱,以避免與將來Python語言的變化產生衝突。

5.單下劃線 _

按照習慣,有時候單個獨立下劃線是用作一個名字,來表示某個變數是臨時的或無關緊要的。
例如,在下面的迴圈中,我們不需要訪問正在執行的索引,我們可以使用“_”來表示它只是一個臨時值:

>>> for _ in range(32):
...    print('Hello, World.')

你也可以在拆分(unpacking)表示式中將單個下劃線用作“不關心的”變數,以忽略特定的值。 同樣,這個含義只是“依照約定”,並不會在Python直譯器中觸發特殊的行為。 單個下劃線僅僅是一個有效的變數名稱,會有這個用途而已。
在下面的程式碼示例中,我將汽車元組拆分為單獨的變數,但我只對顏色和里程值感興趣。 但是,為了使拆分表示式成功執行,我需要將包含在元組中的所有值分配給變數。 在這種情況下,“_”作為佔位符變數可以派上用場:

>>> car = ('red', 'auto', 12, 3812.4)
>>> color, _, _, mileage = car

>>> color
'red'
>>> mileage
3812.4
>>> _
12

除了用作臨時變數之外,“_”是大多數Python REPL中的一個特殊變數,它表示由直譯器評估的最近一個表示式的結果。
這樣就很方便了,比如你可以在一個直譯器會話中訪問先前計算的結果,或者,你是在動態構建多個物件並與它們互動,無需事先給這些物件分配名字:

>>> 20 + 3
23
>>> _
23
>>> print(_)
23

>>> list()
[]
>>> _.append(1)
>>> _.append(2)
>>> _.append(3)
>>> _
[1, 2, 3]

Python下劃線命名模式 - 小結
以下是一個簡短的小結,即“速查表”,羅列了我在本文中談到的五種Python下劃線模式的含義:

Pattern Example Meaning
Single Leading Underscore _var Naming convention indicating a name is meant for internal use. Generally not enforced by the Python interpreter (except in wildcard imports) and meant as a hint to the programmer only.
Single Trailing Underscore var_ Used by convention to avoid naming conflicts with Python keywords.
Double Leading Underscore __var Triggers name mangling when used in a class context. Enforced by the Python interpreter.
Double Leading and Trailing Underscore var Indicates special methods defined by the Python language. Avoid this naming scheme for your own attributes.
Single Underscore _ Sometimes used as a name for temporary or insignificant variables (“don’t care”). Also: The result of the last expression in a Python REPL.