The Meaning of Underscores in Python

The Meaning of Underscores in Python

This post is a translated version, original post can be found in The Meaning of Underscores in Python.

在 Python 當中,底線與雙底線有很多不同的意思,此篇將會講述它的運作方式以及要怎麼把它用在你寫的 Class 之中。


底線與雙底線對於 Python 變數和函式有著不同的意義。有的僅僅是慣用法,給工程師提示用,而有的卻會真的被 Python 直譯器執行。

如果你也對於 底線與雙底線在 Python 當中代表的含意 有興趣,那麼在這篇文章中,我會盡力讓你瞭解的。

首先讓我們來看看以下五種底線的寫法,並且在之後解釋它們在 Python 當中代表什麼:
* 在變數前有一底線:_var
* 在變數後有一底線:var_
* 在變數前有兩底線:__var
* 在變數前後各有兩底線:__var__
* 只有一個底線:_

而本篇文章的最後也會有個 Cheat Sheet 寫著上述五種不同的底線和他們的意義,還有一個教學影片手把手帶著你瞭解五種底線的行為。

在變數前有一底線:_var

當變數或函式名稱之前有著一個底線,它只有給予工程師暗示的功能,並不會實際對於程式的執行有什麼影響。

一個底線 暗示其他開發者這一個變數是作為內部使用,並不會被外部呼叫,而在 PEP8 當中也找到定義。

雖然 Python 不像 Java 那樣地嚴格規範私有變數(Private variable)與公有變數(Public variable),但 Python 就像放上一個底線來提示說:

嘿,這個變數並不會是這個類別中能夠被公開存取的東西,就別去動到他了。

所以讓我們來看看以下這個例子:

現在我們要嘗試來建立出 Test 類別的實例,然後試著存取在 __init__ 被定義的 foo_bar 兩個 attributes。

想必你也看到了 _bar 這樣子定義變數名稱無法阻止我們去存取到這個值,因為在 單一底線 僅代表著暗示。

然而在函式名稱卻會影響著 import 時的行為,想像你有個叫做 my_module 的模組:

而如果我們現在使用 wildcard import 來引用模組的所有名稱,Python 便不會把有底線開頭的物件引用進來(除非這個模組定義了 __all__ 這個 list 來改寫引用的行為):

順帶一提,wildcard import 應該要盡力避免,因為這麼做會讓此刻 Python 環境的命名空間(Namespace)混雜在一起,所以最好嚴謹一些地引用你需要的物件。

至於如果我們使用一般的 import 方式,就不會因為名稱的底線前綴有任何影響:

可能你現在對此有些疑惑,但如果你嚴格遵守 PEP8 所說的「不使用通用字(wildcard)來 import」,那麼你只需要記住這一句話:

單一底線在 Python 暗示變數、函式僅供內部使用。而通常它都不會被 Python 直譯器執行,僅僅作為開發者的提示。

在變數後有一底線:var_

有時候,最適合的名字已經被作為保留字使用,因此像是 classdef 就不能作為變數名稱。在這種情形之中,你可以在變數名稱結尾加上一個底線來解決命名衝突問題。

總而言之,在結尾加上一個底線是用於解決命名衝突,你可以在 PEP8 找到這個寫程式的習慣。

在變數前有兩底線:__var

到目前為止,我們所講到的命名模式所代表的意義都只是一種習慣而已,但在 Python 中,類別的變數與函式如果以雙底線開頭,則有些不一樣的意義了。

為了避免在子類別(subclass)當中發生命名衝突,雙底線的命名前綴會讓 Python 覆寫屬性名稱。

而這個行為我們也稱之為 命名修飾(Name Mangling),直譯器修改變數名稱,使之後類別更不容易產生衝突。這可能有些抽象,所以我用了一小段程式碼來做實驗,說明這件事情:

接下來以內建函式 dir() 來看看這個物件的所有屬性:

它給了我們這個物件所有的屬性。讓我們來找一下剛剛定義的 foo_bar__baz,我保證你也注意到了一些不一樣的地方。

  • self.foo 在 List 當中,沒有什麼不同的。
  • self._bar 也跟 self.foo 一樣,它只是多了一個底線。如同我剛剛所說,這個底線只是給開發者提示而已。
  • 然而 self.__baz 就有點不一樣了,在這個 List 當中你找不到它。

所以 __baz 發生什麼事情了?

仔細一看的話妳可以看到有一個叫做 _Test__baz 的物件,而這就是 Python 直譯器所做的命名修飾(Name Mangling),他能夠保護變數在子類別中不會被覆寫。

所以接下來我們創造了另一個繼承 Test 的類別,並試著去覆寫在建構子(constructor)之中的變數:

你覺得在 ExtendedTest 這個類別的實例之中,foo_bar__baz 的值會是什麼呢?讓我們來看一下:

等等,為什麼我們要讀取 t2.__baz 的時候會出現 AttributeError
當然是因為 Name Mangling 的緣故,所以這個物件甚至沒有 __baz 屬性。

如你所見,__baz 變成 _ExtendedTest__baz,避免不小心被修改到它的值。

但是原本的的 _Test__baz 還在:

雙底線的命名修飾功能,對於開發者而言是完全透明的,看看以下例子:

那麼命名修飾是否也會在函式上發揮作用?確實如此,命名修飾會影響在類別中,所有以雙底線作為前綴的名稱:

接下來是另一個比較特別的例子:

這個例子當中,我定義了一個全域變數(global variable):_MangledGlobal__mangled

然後我透過一個名為 MangledGlobal 的類別去呼叫到存在全域的變數,因為命名修飾的關係,我能夠只透過在 test() 當中呼叫 __mangled 這個變數而參考到全域變數。

因為雙底線前綴的緣故,Python 能夠自動把 __mangled 展開成 __mangledGlobal__mangled。而這個例子是想要展示 Name Mangling 並不只在類別中的變數發生,它會作用於所有類別中以雙底線作為前綴的內容。

現在有很多需要細嚼慢嚥的知識

老實說,在最初寫下這篇文章時,並沒有打算要寫這些範例與解釋,完成這些範例也花了我一些時間去研究。雖然我已經使用 Python 很多年,但這些規則和例子並沒有這麼容易被構思出來。

有時候對開發者最重要的技能是 pattern recognition,還有知道去怎麼找到自己的資料。如果你覺得已經有點不堪重負了,不要擔心,花一些時間來玩玩這些例子吧!

在足夠瞭解這些概念之後,你已經能夠知道命名修飾的行為,在之後若有再碰到它,你就知道要去看哪些文件了。

題外話:什麼是「dunder」

也許你有聽過一些較有經驗的 Python 愛好者或從一些演講聽聞「dunder」這個詞,你可能會想知道它代表什麼意思。

在 Python 使用者社群中,雙底線(Double underscores)通常就稱為「dunders」。如此稱呼它的原因是因為 雙底線前綴時常出現在程式碼中,而為了不費口舌之力,所以就把「Double underscores」簡稱為「dunders」了

舉個例子,你可以把 __baz 唸成 dunder baz,而 __init__ 唸成 dunder init,雖然有人會認為應該要唸成 dunder init dunder,但這就只是口語上比較方便溝通的說法啦。

而這樣子的術語就像是 Python 工程師之間的密碼一樣 🙂

在變數前後各有兩底線:__var__

出乎意料之外的是,命名修飾並不會出現在前後綴皆是兩個底線的情況。當變數以雙底線作為前綴與後綴,經過 Python 直譯器之後發現什麼事也沒發生:

然而,雙底線前後綴是保留用於特殊用途的,這個規則涵蓋了像是做為物件建構子的 __init__,或做為讓物件能被呼叫的 __call__

這些以雙底線做為前後綴命名的方式稱為 Magic Methods但在 Python 使用者社群,很多人不喜歡這個作法,包含我也是

不過最好還是別以雙底線前後綴做為你的變數命名方式,避免在未來新的 Python 版本更新,你的程式就壞掉了。

只有一個底線:_

在慣例當中,單獨存在的單一底線 _ 有時被用以表示一個變數只是暫時性或不重要的。

舉個例子,在以下的迴圈中,我們不需要索引值(index,表示迴圈跑到第幾圈),所以我們可以使用 _ 來暗示此值只是一個暫時性的值:

你也能在 unpacking 時,以單一底線來表示某個值不重要,並直接忽略它。再重複一次,它的意義只是在慣例上這麼被使用,且它不會觸發 Python 直譯器的任何行為,單一底線就是一個有效的變數而被這麼使用而已。

在以下程式碼我會把一個名為 car 的 tuple 透過 unpack 將之解成分開的變數,但是我只想得到這臺車的 顏色里程數。然而,為了要解封,表示式需要把所有在 car 之中的變數都指定,所以在此 _ 就是一個很有用的占位符(Placeholder)了:

除了做為暫時變數使用以外,在 Python REPLs 也表示著上一次執行的結果。

如果你正使用著 Python 直譯器,且你想要看到上一個執行結果,這是一個十分方便的功能。或是你想要建立一個物件,卻還不想幫它取名字也能這麼做:

Python 的各種底線命名方式總結

這裡是一個總覽表,或者說是 Cheat Sheet,涵蓋了上述所說的五種不同型別:

Pattern Example Meaning
單底線前綴 _var 暗示此變數僅作為內部使用,一般而言不會被 Python 直譯器執行,但在萬用字元引用會無法引用
單底線後綴 var_ 避免命名衝突時使用
雙底線前綴 __var 在類別的變數與函式名稱中使用,會觸發 Python 的命名修飾機制
雙底線前後綴 __var__ 作為 Python 特殊函式(Magic Method)使用,避免在命名變數時這樣使用
單一底線 _ 有時作為暫時或不重要的變數名稱,同時也是 Python REPL 最後一個執行結果儲存的變數

影片手把手教你五種底線的意義

看看以下影片來瞭解雙底線前綴的 Python 命名修飾機制,與瞭解他們是怎麼影響你的類別與模組吧:

如果我有漏掉解釋什麼東西,或者想要補上你的想法?歡迎在原文下面留言,謝啦。

Leave a Reply

Your email address will not be published. Required fields are marked *