在 Python 中有很多地方可以看到*
和**
。在某些情形下,無論是對於新手程式設計師,還是從其他很多沒有完全相同操作符的程式語言遷移過來的人來說,這兩個操作符都可能有點神祕。因此,我想討論一下這些操作符的本質及其使用方式。
多年以來,*
和**
操作符的功能不斷增強。在本文中,我將討論目前這些操作符所有的使用方法,並指出哪些使用方法只能在目前的 Python 版本中應用。因此,如果你學習過 Python 2 中*
和**
的使用方法,那麼我建議你至少瀏覽一下本文,因為 Python 3 中新增了許多*
和**
的新用途。
如果你是新接觸 Python 不久,還不熟悉關鍵字引數(亦稱為命名引數),我建議你首先閱讀我有關Python中的關鍵字引數的文章。
不屬於我們討論範圍的內容
在本文中, 當我討論*
和**
時,我指的是*
和**
字首 操作符,而不是 中綴 操作符。
也就是說,我講述的不是乘法和指數運算:
1 2 3 4 |
>>> 2 * 5 10 >>> 2 ** 5 32 |
那麼我們在討論什麼內容呢?
我們討論的是*
和**
字首運算子,即在變數前使用的*
和**
運算子。例如:
1 2 3 4 |
>>> numbers = [2, 1, 3, 4, 7] >>> more_numbers = [*numbers, 11, 18] >>> print(*more_numbers, sep=', ') 2, 1, 3, 4, 7, 11, 18 |
上述程式碼中展示了*
的兩種用法,沒有展示**
的用法。
這其中包括:
- 使用
*
和**
向函式傳遞引數 - 使用
*
和**
捕獲被傳遞到函式中的引數 - 使用
*
接受只包含關鍵字的引數 - 使用
*
在元組解包時捕獲項 - 使用
*
將迭代項解壓到列表/元組中 - 使用
**
將字典解壓到其他字典中
即使你認為自己已經熟悉*
和 **
的所有使用方法,我還是建議你檢視下面的每個程式碼塊,以確保都是你熟悉的內容。在過去的幾年裡,Python 核心開發人員不斷地為這些操作符新增新的功能,對於使用者來說很容易忽略*
和 **
‘的一些新用法。
星號用於將可迭代物件拆分並分別作為函式引數
當呼叫函式時,*
運算子可用於將一個迭代項解壓縮到函式呼叫中的引數中:
1 2 3 4 5 |
>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato'] >>> print(fruits[0], fruits[1], fruits[2], fruits[3]) lemon pear watermelon tomato >>> print(*fruits) lemon pear watermelon tomato |
print(*fruits)
程式碼行將fruits
列表中的所有項作為獨立的引數傳遞給print
函式呼叫,甚至不需要我們知道列表中有多少個引數。
*
運算子在這裡遠不止是語法糖而已。要想用一個特定的迭代器將所有項作為獨立的引數傳輸,若不使用*
是不可能做到的,除非列表的長度是固定的。
下面是另一個例子:
1 2 3 4 5 |
def transpose_list(list_of_lists): return [ list(row) for row in zip(*list_of_lists) ] |
這裡我們接受一個二維列表並返回一個“轉置”的二維列表。
1 2 |
>>> transpose_list([[1, 4, 7], [2, 5, 8], [3, 6, 9]]) [[1, 2, 3], [4, 5, 6], [7, 8, 9]] |
**
操作符完成了類似的操作,只不過使用了關鍵字引數。**
運算子允許我們獲取鍵-值對字典,並在函式呼叫中將其解壓為關鍵字引數。
1 2 3 |
>>> date_info = {'year': "2020", 'month': "01", 'day': "01"} >>> filename = "{year}-{month}-{day}.txt".format(**date_info) >>> filename '2020-01-01.txt' ` |
根據我的經驗,使用**
將關鍵字引數解壓縮到函式呼叫中並不常見。我最常看到它的地方是在實現繼承時:對uper()
的呼叫通常包括*
和**
。
如 Python 3.5 那樣,在函式呼叫中,*
和**
都可以被多次使用。
有時,多次使用*
會很方便:
1 2 3 4 |
>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato'] >>> numbers = [2, 1, 3, 4, 7] >>> print(*numbers, *fruits) 2 1 3 4 7 lemon pear watermelon tomato ` |
多次使用**
也可以達到相似的效果:
1 2 3 4 5 6 7 8 |
>>> date_info = {'year': "2020", 'month': "01", 'day': "01"} >>> track_info = {'artist': "Beethoven", 'title': 'Symphony No 5'} >>> filename = "{year}-{month}-{day}-{artist}-{title}.txt".format( ... **date_info, ... **track_info, ... ) >>> filename '2020-01-01-Beethoven-Symphony No 5.txt' |
不過,在多次使用**
時需要特別小心。Python 中的函式不能多次指定相同的關鍵字引數,因此在每個字典中與**
一起使用的鍵必須能夠相互區分,否則會引發異常。
星號用於壓縮被傳遞到函式中的引數
在定義函式時,*
運算子可用於捕獲傳遞給函式的位置引數。位置引數的數量不受限制,捕獲後被儲存在一個元組中。
1 2 3 4 |
from random import randint def roll(*dice): return sum(randint(1, die) for die in dice) |
這個函式接受的引數數量不受限制:
1 2 3 4 5 6 |
>>> roll(20) 18 >>> roll(6, 6) 9 >>> roll(6, 6, 6) 8 |
Python 的print
和zip
函式接受的位置引數數量不受限制。*
的這種引數壓縮用法,允許我們建立像print
和zip
一樣的函式,接受任意數量的引數。
**
運算子也有另外一個功能:我們在定義函式時,可以使用**
捕獲傳進函式的任何關鍵字引數到一個字典當中:
1 2 3 4 5 6 |
def tag(tag_name, **attributes): attribute_list = [ f'{name}="{value}"' for name, value in attributes.items() ] return f"<{tag_name} {' '.join(attribute_list)}>" |
**
將捕獲我們傳入這個函式中的任何關鍵字引數,並將其放入一個字典中,該字典將引用attributes
引數。
1 2 3 4 |
>>> tag('a', href="http://treyhunner.com") '<a href="http://treyhunner.com">' >>> tag('img', height=20, width=40, src="face.jpg") '<img height="20" width="40" src="face.jpg">' |
只有關鍵字引數的位置引數
在 Python 3 中,我們現在擁有了一種特殊的語法來接受只有關鍵字的函式引數。只有關鍵字的引數是只能 使用關鍵字語法來指定的函式引數,也就意味著不能按照位置來指定它們。
在定義函式時,為了接受只有關鍵字的引數,我們可以將命名引數放在*
後:
1 2 3 4 5 |
def get_multiple(*keys, dictionary, default=None): return [ dictionary.get(key, default) for key in keys ] |
上面的函式可以像這樣使用:
1 2 3 |
>>> fruits = {'lemon': 'yellow', 'orange': 'orange', 'tomato': 'red'} >>> get_multiple('lemon', 'tomato', 'squash', dictionary=fruits, default='unknown') ['yellow', 'red', 'unknown'] |
引數dictionary
和default
在*keys
後面,這意味著它們只能 被指定為關鍵字引數。如果我們試圖按照位置來指定它們,我們會得到一個報錯:
1 2 3 4 5 |
>>> fruits = {'lemon': 'yellow', 'orange': 'orange', 'tomato': 'red'} >>> get_multiple('lemon', 'tomato', 'squash', fruits, 'unknown') Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: get_multiple() missing 1 required keyword-only argument: 'dictionary' |
這種行為是通過 PEP 3102 被引入到 Python 中的。
沒有位置引數關鍵字的引數
只使用關鍵字引數的特性很酷,但是如果您希望只使用關鍵字引數而不捕獲無限的位置引數呢?
Python 使用一種有點奇怪的 單獨*
語法來實現:
1 2 3 4 5 6 |
def with_previous(iterable, *, fillvalue=None): """Yield each iterable item along with the item before it.""" previous = fillvalue for item in iterable: yield previous, item previous = item |
這個函式接受一個迭代器
引數,可以按照位置或名字來指定此引數(作為第一個引數),以及關鍵字引數fillvalue
,這個填充值引數只使用關鍵字。這意味著我們可以像下面這樣呼叫 with_previous:
1 2 |
>>> list(with_previous([2, 1, 3], fillvalue=0)) [(0, 2), (2, 1), (1, 3)] |
但像這樣就不可以:
1 2 3 4 |
>>> list(with_previous([2, 1, 3], 0)) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: with_previous() takes 1 positional argument but 2 were given ` |
這個函式接受兩個引數,其中fillvalue
引數必須被指定為關鍵字引數。
我通常在獲取任意數量的位置引數時只使用關鍵字引數,但我有時使用這個*
強制按照位置指定一個引數。
實際上,Python 的內建sorted
函式使用了這種方法。如果你檢視sorted
的幫助資訊,將看到以下資訊:
1 2 3 4 5 6 7 |
>>> help(sorted) Help on built-in function sorted in module builtins: sorted(iterable, /, *, key=None, reverse=False) Return a new list containing all items from the iterable in ascending order. A custom key function can be supplied to customize the sort order, and the reverse flag can be set to request the result in descending order. |
在sorted
的官方說明中,有一個單獨的*
引數。
星號用於元組拆包
Python 3 還新添了一種 *
運算子的使用方式,它只與上面定義函式時和呼叫函式時*
的使用方式相關。
現在,*
操作符也可以用於元組拆包:
1 2 3 4 5 6 7 8 9 10 |
>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato'] >>> first, second, *remaining = fruits >>> remaining ['watermelon', 'tomato'] >>> first, *remaining = fruits >>> remaining ['pear', 'watermelon', 'tomato'] >>> first, *middle, last = fruits >>> middle ['pear', 'watermelon'] |
如果你想知道什麼情況下可以在你自己的程式碼中使用它,請檢視我關於 Python 中的 tuple 解包 文章中的示例。在那篇文章中,我將展示如何使用*
操作符作為序列切片的替代方法。
通常當我教*
的時候,我告訴大家只能在多重賦值語句中使用一個*
表示式。實際來說這是不正確的,因為可以在巢狀解包中使用兩個*
(我在元組解包文章中討論了巢狀解包):
1 2 3 4 5 6 7 8 9 10 |
>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato'] >>> first, second, *remaining = fruits >>> remaining ['watermelon', 'tomato'] >>> first, *remaining = fruits >>> remaining ['pear', 'watermelon', 'tomato'] >>> first, *middle, last = fruits >>> middle ['pear', 'watermelon'] |
但是,我從來沒見過它有什麼實際用處,即使你因為它看起來有點神祕而去尋找一個例子,我也並不推薦這種使用方式。
將此新增到 Python 3.0 中的 PEP 是 PEP 3132,其篇幅不是很長。
列表文字中的星號
Python 3.5 通過 PEP 448 引入了大量與*
相關的新特性。其中最大的新特性之一是能夠使用*
將迭代器轉儲到新列表中。
假設你有一個函式,它以任一序列作為輸入,返回一個列表,其中該序列和序列的倒序連線在了一起:
1 2 |
def palindromify(sequence): return list(sequence) + list(reversed(sequence)) |
此函式需要多次將序列轉換為列表,以便連線列表並返回結果。在 Python 3.5 中,我們可以這樣編寫函式:
1 2 |
def palindromify(sequence): return [*sequence, *reversed(sequence)] |
這段程式碼避免了一些不必要的列表呼叫,因此我們的程式碼更高效,可讀性更好。
下面是另一個例子:
1 2 |
def rotate_first_item(sequence): return [*sequence[1:], sequence[0]] |
該函式返回一個新列表,其中給定列表(或其他序列)中的第一項被移動到了新列表的末尾。
*
運算子的這種使用是將不同型別的迭代器連線在一起的好方法。*
運算子適用於連線任何種類的迭代器,然而 +
運算子只適用於型別都相同的特定序列。
除了建立列表儲存迭代器以外,我們還可以將迭代器轉儲到新的元組或集合中:
1 2 3 4 5 6 |
>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato'] >>> (*fruits[1:], fruits[0]) ('pear', 'watermelon', 'tomato', 'lemon') >>> uppercase_fruits = (f.upper() for f in fruits) >>> {*fruits, *uppercase_fruits} {'lemon', 'watermelon', 'TOMATO', 'LEMON', 'PEAR', 'WATERMELON', 'tomato', 'pear'} |
注意,上面的最後一行使用了一個列表和一個生成器,並將它們轉儲到一個新的集合中。在此之前,並沒有一種簡單的方法可以在一行程式碼中完成這項工作。曾經有一種方法可以做到這一點,可是並不容易被記住或發現:
兩個星號用於字典文字
PEP 448 還通過允許將鍵/值對從一個字典轉儲到一個新字典擴充套件了**
操作符的功能:
1 2 3 4 5 |
>>> date_info = {'year': "2020", 'month': "01", 'day': "01"} >>> track_info = {'artist': "Beethoven", 'title': 'Symphony No 5'} >>> all_info = {**date_info, **track_info} >>> all_info {'year': '2020', 'month': '01', 'day': '01', 'artist': 'Beethoven', 'title': 'Symphony No 5'} |
我還寫了另一篇文章:在Python中合併字典的慣用方法。
不過,**
操作符不僅僅可以用於合併兩個字典。
例如,我們可以在複製一個字典的同時新增一個新值:
1 2 3 4 |
>>> date_info = {'year': '2020', 'month': '01', 'day': '7'} >>> event_info = {**date_info, 'group': "Python Meetup"} >>> event_info {'year': '2020', 'month': '01', 'day': '7', 'group': 'Python Meetup'} |
或者在複製/合併字典的同時重寫特定的值:
1 2 3 4 |
>>> event_info = {'year': '2020', 'month': '01', 'day': '7', 'group': 'Python Meetup'} >>> new_info = {**event_info, 'day': "14"} >>> new_info {'year': '2020', 'month': '01', 'day': '14', 'group': 'Python Meetup'} |
Python 的星號非常強大
Python 的 *
和 **
運算子不僅僅是語法糖。 *
和 **
運算子允許的某些操作可以通過其他方式實現,但是往往更麻煩和更耗費資源。而且 *
和 **
運算子提供的某些特性沒有替代方法實現:例如,函式在不使用 *
時就無法接受任意數量的位置引數。
在閱讀了*
和 **
運算子的所有特性之後,您可能想知道這些奇怪操作符的名稱。不幸的是,它們的名字並不簡練。我聽說過*
被稱為“打包”和“拆包“運算子。我還聽說過其被稱為“splat”(來自 Ruby 世界),也聽說過被簡單地稱為“star”。
我傾向於稱這些操作符為“星”和“雙星”或“星星”。這種叫法並不能區分它們和它們的中綴關係(乘法和指數運算),但是通常我們可以從上下文清楚地知道是在討論字首運算子還是中綴運算子。
請勿在不理解*
和 **
運算子的前提下記住它們的所有用法!這些操作符有很多用途,記住每種操作符的具體用法並不重要,重要的是瞭解你何時能夠使用這些操作符。我建議使用這篇文章作為一個備忘單或者製作你自己的備忘單來幫助你在 Python 中使用解*
和 **
。
喜歡我的教學風格嗎?
想了解更多關於 Python 的知識嗎?我每週通過實時聊天分享我最喜歡的 Python 資源、回答 Python 問題。在下方註冊,我將回答你提出的關於如何使 Python 程式碼更具有描述性、可讀性和更 Python 化的問題。