你在寫程式命名變數的時候,是 snake_case
還是 camelCase
派別呢?Python 有一項跟其他程式語言不太一樣的特色,底線(_
)並不只有 snake_case
這種命名變數的功能,而還有很多特殊的功用。
舉例而言,只要你稍微讀過 Python 程式碼,一定有看過 def __init__(self)
,這個函式前後各兩個底線可是有特別意義的,多一個、少一個底線都不行!在 Python 裡,這種有趣的底線用法還真不少。
因此,這則筆記我將分享 7 種 Python 裡單底線(_
)與雙底線(__
)的意義還有實用技巧,幫助你好好看懂底線寫法、成為「有底線」的 Python 工程師。
目錄
更好讀的數字常數
你有沒有遇過在程式碼裡寫數字的字面常數(Literals)時,數字大到你無法一眼看出是幾位數的狀況呢?在 Python 3.6 以上的版本(PEP 515),你可以用底線解決這個煩惱!
數字的字面常數(Numeric Literals)加上底線後將不會影響該常數的意義,你可以運用底線提升程式內常數的可讀性。筆者最愛用的就是把底線當成千位符號使用:
# 這樣寫一個常數
# 你能一眼看出來它是百萬、千萬、還是億嗎?
>>> num = 30000000.0
>>> num
30000000.0
# 常數加上底線,意思不會變
# 既然不能加上逗點、就把底線當成千位符號使用
# 一看就知道是三千萬、可讀性大提升!
>>> num = 30_000_000.0
>>> num
30000000.0
除了千位符號,工作中用到二進位或十六進位的時候,底線也能幫助你為數字常數加上不同的資料表示法(例如 Nibble 或是 Word):
# 二進位 Bit 資料用半位元組(Nibble)表示
>>> bin_data = 0b_1000_1110_0110_1111
>>> bin_data
36463
# 用字組(Word)表示記憶體位置
>>> addr = 0x_ABCD_F90C
>>> addr
2882402572
直譯器的最近運算結果
Python 的直譯器(interpreter)會把你最近一個運算過的數值,儲存在名為 _
(單底線)的變數裡。並且,回傳給直譯器的值才會存在 _
,如果你把運算後的數值指定給某個變數就不會儲存在 _
了,聽起來可能有點抽象,請直接看範例:
>>> (1+2+3)*100
600
>>> _
600
>>> 594_0009_527
5940009527
>>> _
5940009527
# 注意,如果數值已指定給某變數,就不會儲存在單底線了
>>> tmp = [i*2 for i in range(5)]
>>> _
5940009527
>>> tmp
[0, 2, 4, 6, 8]
>>> _
[0, 2, 4, 6, 8]
# 我自己覺得打開 Python 當計算機按的時候
# 單底線還不錯用啦 (˚∀˚)
>>> 350+128
478
>>> _ * 3
1434
>>> _ - 1200
234
>>> _ ** 3
12812904
存放想忽略的數值
_
(單底線)本身可以是變數名稱,拿來儲存資料。不過,要是你真的寫 _ = [3, 5, 7]
,其他人應該會對你的變數命名感到頭痛。_
當然不適合拿來存放有意義的資料,相反地,如果是非必要使用、可忽略的資料,就適合用 _
刻意讓閱讀程式碼的人知道這個存下來的資訊不重要。筆者實際上常用的是以下兩個情境:
迴圈
# 迴圈內有用到 i
>>> for i in range(3):
... print(f"第 {i} 行")
...
第 0 行
第 1 行
第 2 行
# 每個迴圈內容都一樣,表示 i 存了什麼根本不重要!
>>> for i in range(3):
... print(f"你好 ʕ •ᴥ•ʔ")
...
你好 ʕ •ᴥ•ʔ
你好 ʕ •ᴥ•ʔ
你好 ʕ •ᴥ•ʔ
# 所以乾脆用單底線改寫
# 讓其他程式碼閱讀者知道:「第幾個迴圈」是不重要的資訊
>>> for _ in range(3):
... print(f"你好 ʕ •ᴥ•ʔ")
...
你好 ʕ •ᴥ•ʔ
你好 ʕ •ᴥ•ʔ
你好 ʕ •ᴥ•ʔ
解包
解包(Unpacking)是把多個元素分別儲存在各個變數的技巧,在某些時候,並不是所有要解包的內容都是必要的資料,那些用不到的資料就別耗腦力想變數命名了,全都丟進 _
吧。
# 範例:
# tuple 格式的國小資料
# (姓名, 性別, 入學年份, 國小名稱, 年級)
# 我們可以用解包技巧(Unpacking)把多個元素分別儲存在各個變數
>>> name, gender, year, school, gradelevel = ('豪豪', '男', 2001, '西松國小', 3)
>>> name
'豪豪'
>>> gradelevel
3
# 如果我們操作資料只需要用到「姓名」與「年級」兩項資訊
# 中間的其他資料我們可以用單底線來暫時存放,也就是,刻意地忽略
>>> name, _, _, _, gradelevel = ('曼玉', '女', 2010, '健康國小', 6)
>>> name
'曼玉'
>>> gradelevel
6
# 更酷的是,單底線再加上 Extended Iterable Unpacking (PEP 3132) 技巧
# 程式碼會再更簡短
>>> name, *_, gradelevel = ('朝偉', '男', 1998, '敦化國小', 5)
>>> name
'朝偉'
>>> gradelevel
5
順帶一提筆者好豪喜歡的一項資料科學小技巧,我們有時候會需要隨機抽樣一小部分資料來探索,比起尋找抽樣專用的函式,我會偷懶地直接用超常見的 train_test_split()
函式加上單底線技巧,來進行資料隨機抽樣:
>>> import numpy as np
>>> from sklearn.model_selection import train_test_split
>>> X, y = np.arange(10).reshape((5, 2)), range(5)
>>> X
array([[0, 1],
[2, 3],
[4, 5],
[6, 7],
[8, 9]])
# 隨機抽樣出三分之二的資料
>>> X_sample, _, y_sample, _ = train_test_split(
... X, y, test_size=0.33, random_state=42)
...
>>> X_sample
array([[4, 5],
[0, 1],
[6, 7]])
>>> y_sample
[2, 0, 3]
# 至於 _ 內究竟存了什麼
# 就不管了 ¯\_(ツ)_/¯
命名:前單底線(_var
)
在變數或者函式名稱前面加上單底線、變成 _var
的形式,是用於提示其他使用與閱讀你的程式碼的人,告訴大家這項變數或函式是內部使用,請你「最好不要用」。但這項前單底線的命名終究只是慣例(Convention)、只是微弱地提醒,實際上加上前單底線並不影響變數與函式的功能,所以其他人硬是要用這些變數與函式也不會真的受到阻礙。
假設我們有個類別(Class)儲存員工資訊,內部的 _title
變數、還有 _print_employee_info()
函式,是我們不建議其他程式設計師拿來用的部分:
class Employee:
"""Class that contains basic information about an employee."""
def __init__(self, name: str, job: str, salary: int = 0) -> None:
self.name = name
self.job = job
self.salary = salary
self._title = f"{self.job} - {self.name}"
def get_name(self):
return self.name
def _print_employee_info(self):
print(f"月薪 ${self.salary} 的 {self._title}")
return None
>>> hao = Employee("好豪", "資料科學家", 22_000)
# 一般命名方法的變數可以取用
>>> hao.name
'好豪'
# 前單底線命名的變數代表「內部使用」或「不建議使用」
# 但其實還是可以取得
>>> hao._title
'資料科學家 - 好豪'
# 同樣地,一般命名方法的函式當然可以用
>>> hao.get_name()
'好豪'
# 前單底線命名的函式代表「內部使用」或「不建議使用」
# 但你還是可以呼叫得到 (˚∀˚)
>>> hao._print_employee_info()
月薪 $22000 的 資料科學家 - 好豪
需要提醒的是,在 import 模組的時候,如果你的寫法是全部內容都 import 的 from my_module import *
,前單底線命名的變數跟函式不會被 import!
# 範例模組:my_module.py
def function_a():
print('一般命名的函式')
def _function_b():
print('前單底線命名的函式')
>>> from my_module import *
>>> function_a()
一般命名的函式
>>> _function_b()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name '_function_b' is not defined
# 如果你刻意要使用前單底線命名的內容
# 需要明確寫出你要 import 它
>>> from my_module import _function_b
>>> _function_b()
前單底線命名的函式
補充:當你不只是需要用前單底線「提醒」、還需要強制保護資料不被使用者更動的時候…
- 如果類別內要保護的屬性資料只有一、兩個,可以使用
@property
語法保護變數 - 如果類別內所有的屬性資料都要被保護不被更動,請使用
dataclass
內的frozen
功能
命名:後單底線(var_
)
要是你想用的變數或函式命名,已經在 Python 內建出現過了,你可不能跟 Python 搶名字!以下範例就真的「霸佔」某個 Python 內建功能的命名,看看發生什麼事:
# 相信大家都知道,dict 是 Python 內建類別
>>> dict
<class 'dict'>
>>> dict()
{}
>>> d = dict()
>>> d['a'] = 123
>>> d
{'a': 123}
# 如果你真的把一個「字典」功能的變數用 'dict' 命名
>>> dict = {'apple': '蘋果', 'banana': '香蕉', 'grava': '拔辣'}
>>> dict
{'apple': '蘋果', 'banana': '香蕉', 'grava': '拔辣'}
# 你會發現你不能再正常使用 Python 的 dict()!
# 因為 dict 被你「覆蓋」掉了
>>> d = dict()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'dict' object is not callable
為了避免變數命名與 Python 內建衝突,程式設計慣例是為會與 Python 內建重複的命名後方加上單底線。
# 你如果堅持要用 'dict' 來命名
# 通融一下加上底線改成 'dict_'
# 就能跟 Python 和平共處了!
>>> dict_ = {'apple': '蘋果', 'banana': '香蕉', 'grava': '拔辣'}
>>> dict_
{'apple': '蘋果', 'banana': '香蕉', 'grava': '拔辣'}
>>> d = dict()
>>> d
{}
與 Python 內建的命名衝突如果出現在函式的參數,Python 甚至會在你命名的當下就回報錯誤,最常見的就是不能用類別的 class
關鍵字來命名函式參數。
# 函式參數用 'class' 命名會直接出錯
>>> def my_function(class): print(class)
File "<stdin>", line 1
def my_function(class): print(class)
^
SyntaxError: invalid syntax
# 真要用的話就加個底線吧
>>> def my_function(class_): print(class_)
>>> my_function(class_="hello world")
hello world
這就是為什麼有些 Python 套件裡的函式會用 class_
當作參數命名,例如使用 BeautifulSoup 來尋找網頁元素中的某個 class 時,雖然你要找的是 CSS 的 class 屬性,但是在函式中參數必須寫 class_
:
# BeautifulSoup 的 find_all() 函式節錄範例
>>> soup.find_all("a", class_="sister")
[<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
命名:前雙底線(__var
)
前面跟命名有關的兩種單底線用法 _var
與 var_
,都是命名慣例為主,接下來談到的雙底線 __var
與 __var__
則會與 Python 的運作邏輯直接相關。
前雙底線(__var
)的功能,是避免你所創造類別(Class)內的變數或函式,被子類別所覆寫(Override)。
我們自創一個簡單的類別來看 Python 會對前雙底線命名的變數與函式做什麼:
class MyClass:
def __init__(self):
self.bar = 0
self.foo = 1
self._foo = 2
self.foo_ = 3
self.__foo = 4
前或後單底線跟前面學過的一樣,只是命名慣例,用法沒什麼意外之處。唯有前雙底線的變數,不能用普通的方式取得:
>>> my_class = MyClass()
>>> my_class.bar
0
>>> my_class.foo
1
>>> my_class._foo
2
>>> my_class.foo_
3
>>> my_class.__foo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute '__foo'
>>> dir(my_class)
['_MyClass__foo', '__class__', ... (中間省略) ... , '__weakref__', '_foo', 'bar', 'foo', 'foo_']
用 dir()
函式觀察該變數內所有屬性後,前雙底線命名的 __foo
根本不存在!再仔細看,還會看到多了一個有趣的 _MyClass__foo
,把它取出來會發現,資料內容跟我們設定的 __foo
一樣!
>>> my_class._MyClass__foo
4
Python 直譯器主動修改你的前雙底線變數的這項行為,稱為 名字修飾(Name Mungling),它的作用是保護類別內的變數,不讓它在子類別被覆蓋掉。
以下範例我們接著創建子類別試試看,就能知道名字修飾的好處了:
# 注意:
# 1. 此類別繼承自 MyClass
# 2. 此類別內「沒有」宣告類別變數 bar
class MyChildClass(MyClass):
def __init__(self):
super().__init__()
self.foo = 11111
self._foo = 22222
self.foo_ = 33333
self.__foo = 44444
>>> my_child_class = MyChildClass()
# 從 MyClass 繼承來的
>>> my_child_class.bar
0
# 以下三個變數都被子類別覆蓋了
>>> my_child_class.foo
11111
>>> my_child_class._foo
22222
>>> my_child_class.foo_
33333
# __foo 依然不能直接取得
>>> my_child_class.__foo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyChildClass' object has no attribute '__foo'
# 需要利用我們剛剛學的名字修飾來取得 __foo
>>> my_child_class._MyChildClass__foo
44444
# 使用正確的名字修飾
# 就可以取得父類別(MyClass)的 __foo 變數資料
# 最重要的是:父類別的這個前雙底線命名變數 *沒有* 被子類別覆蓋!
>>> my_child_class._MyClass__foo
4
透過前雙底線的名字修飾機制,父類別跟子類別的變數即使命名相同,都透過「被 Python 直譯器擅自改名字」的方式通通保留下來了,不會互相覆蓋!
上方範例是用前雙底線命名變數,在類別內用前雙底線命名函式,同樣也適用名字修飾。此外,名字修飾是發生在函式宣告的當下,亦即,函式主體(Function Body)內,你可以用還沒名字修飾的前雙底線命名來取用你的變數或函式。以下範例一併介紹上述兩個要點:
class MyHahaClass:
def __laugh(self):
print("哈哈 ^_^")
def call_laugh_method(self):
return self.__laugh() # 特別注意函式主體的此處還沒有發生名字修飾
>>> my_haha_class = MyHahaClass()
# 同樣地,函式命名如果是前雙底線,不能直接用該命名呼叫
>>> my_haha_class.__laugh()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyHahaClass' object has no attribute '__laugh'
# 用經過名字修飾的函式命名就可以了
>>> my_haha_class._MyHahaClass__laugh()
哈哈 ^_^
# 或是,在函式主體內的另個函式可以用 self.__laugh() 來呼叫它
>>> my_haha_class.call_laugh_method()
哈哈 ^_^
對於名字修飾,筆者好豪在《Python 神乎其技》書中還看到一段特別有趣且令我驚奇的程式碼,能幫助我們更理解名字修飾機制運作,以下我直接引用 書中 的範例:
_MangledGlobal__mangled = 23
class MangledGlobal:
def test(self):
return __mangled
仔細觀察,MangledGlobal
根本沒創建任何類別變數(例如 self.__mangled = 123
),但 test()
這個函式經過名字修飾後還是能正常執行:
>>> MangledGlobal().test()
23
看懂這個範例之後你有沒有更加理解,名字修飾乍聽之下複雜,但其實行為很單純:在宣告類別的時候,Python 直譯器看到前雙底線的變數或函式就幫你(字面上地)修改命名。筆者自己是這樣想像:
- 在
test()
函式裡,我的肉眼看到的是return __mangled
- Python 直譯器的眼睛看到的則是
return _MangledGlobal__mangled
。
以下再稍微修改剛剛的範例,我們來看看會發生什麼事:
_MangledGlobal__mangled = 23
class MangledGlobal:
def __init__(self):
self.__mangled = 9527
def test(self):
return __mangled
def test2(self):
return self.__mangled
>>> MangledGlobal().test()
23
>>> MangledGlobal().test2()
9527
看到這裡,你是更理解前雙底線與名字修飾,還是更一頭霧水了呢?
命名:前後雙底線(__var__
)
前跟後都有雙底線命名的函式,被稱為 Magic Method 或 “Dunder” Method,在 Python 會用特殊的邏輯運作。
(註記:如果前跟後都有雙底線,Python 直譯器就不會使用上個小節提到的名字修飾了)
例如類別內總是會見到的 def __init__(self)
代表的是建構子(Constructor),每次你創建一個實例(Instance),__init__()
函式自動會第一個被執行。
再以 __lt__()
舉例,lt 是 Less Than 的縮寫,你的類別只要有實作這個函式,你就可以用小於符號(<
)來呼叫該函式了。
class MyLessClass:
def __lt__(self, another):
print('hello world')
>>> my_less_class1 = MyLessClass()
>>> my_less_class2 = MyLessClass()
>>> my_less_class1.__lt__(my_less_class2)
hello world
>>> my_less_class1 < my_less_class2
hello world
# 目前這個「小於」只是被實作
# 邏輯上根本沒做到比大小
明明類別宣告裡完全沒寫到 <
這個符號,只要實作 __lt__()
就能透過 <
符號運作了,前後雙底線是不是如 Magic Method 這個名稱、像魔法一樣神奇呢?
不過,正是因為前後雙底線命名的函式用法高度取決於 Python 內部的邏輯或是你使用的框架(Framework),如果你對這些 Magic Method 運作邏輯還不太熟悉,最好不要輕易使用前後雙底線的命名方式比較好。
結語
這則筆記總共分享了 7 種 Python 底線的運用方式,其中四種是跟變數還有函式命名有關:
- 前單底線(
_var
):告知該變數或函式是類別內部使用的命名慣例 - 後單底線(
var_
):避免跟 Python 內建功能重複命名 - 前雙底線(
__var
):透過名字修飾機制防止子類別覆蓋變數或函式 - 前後雙底線(
__var__
):有特殊運作邏輯的 Magic Method
另外三種則是寫 Python 程式好用的小技巧:
- 常數加上底線
30_000_000
可以更好讀 - 在直譯器中用
_
查看最近一項運算結果 - 迴圈或是解包時,想忽略的數值,直接用
_
來命名
相信看完這篇文章後,以後你看到 Python 程式碼的各種底線將不再茫然!
參考資料:
- 《Python 神乎其技》
- 書中文章摘錄可以 線上免費閱讀
- PEP 8: Naming Styles
- 《Python 3.9 技術手冊》
還想知道更多 Python 相關技巧嗎?推薦你閱讀好豪的其他 Python 分享文章,學會更多實用程式設計方法:
- Python 函式 zip() 教學:同時迭代多個 list,學習刷題與資料分析技巧
- Python dataclass 教學:輕鬆定義資料類別
- Python dict 預設值技巧,key 找不到也不用擔心
- Python 函式的可變物件預設引數陷阱
也感謝你閱讀這篇筆記,歡迎追蹤 好豪的粉絲專頁 與 Threads 帳號,我會持續分享 Python 技巧、資料科學等知識;也歡迎點選下方按鈕將本文加入書籤、或者分享給更多正在學 Python 的朋友。