Python中的星號本質以及使用方法介紹

翻譯:Python 開發者 - 一汀, 英文:Trey Hunner

http://blog.jobbole.com/114655/

Python開發者

 

在 Python 中有很多地方可以看到***。在某些情形下,無論是對於新手程序員,還是從其他很多沒有完全相同操作符的編程語言遷移過來的人來說,這兩個操作符都可能有點神祕。因此,我想討論一下這些操作符的本質及其使用方式。

多年以來,***操作符的功能不斷增強。在本文中,我將討論目前這些操作符所有的使用方法,並指出哪些使用方法只能在目前的 Python 版本中應用。因此,如果你學習過 Python 2 中***的使用方法,那麼我建議你至少瀏覽一下本文,因爲 Python 3 中添加了許多***的新用途。

如果你是新接觸 Python 不久,還不熟悉關鍵字參數(亦稱爲命名參數),我建議你首先閱讀我有關Python中的關鍵字參數的文章。

 

1、不屬於我們討論範圍的內容

在本文中, 當我討論***時,我指的是*** 前綴 操作符,而不是 中綴 操作符。

也就是說,我講述的不是乘法和指數運算:

>>> 2 * 5

10

>>> 2 ** 5

32

 

2、那麼我們在討論什麼內容呢?

我們討論的是***前綴運算符,即在變量前使用的***運算符。例如:

>>> numbers = [2, 1, 3, 4, 7]

>>> more_numbers = [*numbers, 11, 18]

>>> print(*more_numbers, sep=', ')

2, 1, 3, 4, 7, 11, 18

 

上述代碼中展示了*的兩種用法,沒有展示**的用法。

這其中包括:

  1. 使用***向函數傳遞參數

  2. 使用***捕獲被傳遞到函數中的參數

  3. 使用*接受只包含關鍵字的參數

  4. 使用*在元組解包時捕獲項

  5. 使用*將迭代項解壓到列表/元組中

  6. 使用**將字典解壓到其他字典中

 

即使你認爲自己已經熟悉* 和 **的所有使用方法,我還是建議你查看下面的每個代碼塊,以確保都是你熟悉的內容。在過去的幾年裏,Python 核心開發人員不斷地爲這些操作符添加新的功能,對於使用者來說很容易忽略* 和 **‘的一些新用法。

 

3、星號用於將可迭代對象拆分並分別作爲函數參數

當調用函數時,*運算符可用於將一個迭代項解壓縮到函數調用中的參數中:

>>> 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函數調用,甚至不需要我們知道列表中有多少個參數。

*運算符在這裏遠不止是語法糖而已。要想用一個特定的迭代器將所有項作爲獨立的參數傳輸,若不使用*是不可能做到的,除非列表的長度是固定的。

下面是另一個例子:

def transpose_list(list_of_lists):

    return [

        list(row)

        for row in zip(*list_of_lists)

    ]

 

這裏我們接受一個二維列表並返回一個“轉置”的二維列表。

>>> transpose_list([[1, 4, 7], [2, 5, 8], [3, 6, 9]])

[[1, 2, 3], [4, 5, 6], [7, 8, 9]]


**操作符完成了類似的操作,只不過使用了關鍵字參數。**運算符允許我們獲取鍵-值對字典,並在函數調用中將其解壓爲關鍵字參數。

>>> 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 那樣,在函數調用中,***都可以被多次使用。

有時,多次使用*會很方便:

>>> fruits = ['lemon', 'pear', 'watermelon', 'tomato']

>>> numbers = [2, 1, 3, 4, 7]

>>> print(*numbers, *fruits)

2 1 3 4 7 lemon pear watermelon tomato `


多次使用**也可以達到相似的效果:

>>> 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 中的函數不能多次指定相同的關鍵字參數,因此在每個字典中與**一起使用的鍵必須能夠相互區分,否則會引發異常。

 

4、星號用於壓縮被傳遞到函數中的參數

在定義函數時,*運算符可用於捕獲傳遞給函數的位置參數。位置參數的數量不受限制,捕獲後被存儲在一個元組中。

from random import randint  

 

def roll(*dice):    

    return sum(randint(1, die) for die in dice)


這個函數接受的參數數量不受限制:

>>> roll(20)

18

>>> roll(6, 6)

9

>>> roll(6, 6, 6)

8

 

Python 的printzip函數接受的位置參數數量不受限制。*的這種參數壓縮用法,允許我們創建像printzip一樣的函數,接受任意數量的參數。

**運算符也有另外一個功能:我們在定義函數時,可以使用** 捕獲傳進函數的任何關鍵字參數到一個字典當中:

def tag(tag_name, **attributes):

    attribute_list = [

        f'{name}="{value}"'

        for name, value in attributes.items()

    ]    

    return f"<{tag_name} {' '.join(attribute_list)}>"

 

** 將捕獲我們傳入這個函數中的任何關鍵字參數,並將其放入一個字典中,該字典將引用attributes參數。

>>> 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">'

 

 5、只有關鍵字參數的位置參數

在 Python 3 中,我們現在擁有了一種特殊的語法來接受只有關鍵字的函數參數。只有關鍵字的參數是只能 使用關鍵字語法來指定的函數參數,也就意味着不能按照位置來指定它們。

在定義函數時,爲了接受只有關鍵字的參數,我們可以將命名參數放在*後:

def get_multiple(*keys, dictionary, default=None):

    return [

        dictionary.get(key, default)

        for key in keys

    ]

 

上面的函數可以像這樣使用:

>>> fruits = {'lemon': 'yellow', 'orange': 'orange', 'tomato': 'red'}

>>> get_multiple('lemon', 'tomato', 'squash', dictionary=fruits,default='unknown')

['yellow', 'red', 'unknown']

 

參數dictionarydefault*keys後面,這意味着它們只能 被指定爲關鍵字參數。如果我們試圖按照位置來指定它們,我們會得到一個報錯:

>>> 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 中的。

 

6、沒有位置參數關鍵字的參數

只使用關鍵字參數的特性很酷,但是如果您希望只使用關鍵字參數而不捕獲無限的位置參數呢?

Python 使用一種有點奇怪的 單獨* 語法來實現:

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:

 

>>> list(with_previous([2, 1, 3], fillvalue=0))

[(0, 2), (2, 1), (1, 3)]


但像這樣就不可以:

>>> 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的幫助信息,將看到以下信息:

>>> 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 inascending 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的官方說明中,有一個單獨的*參數。

 

7、星號用於元組拆包

Python 3 還新添了一種 * 運算符的使用方式,它只與上面定義函數時和調用函數時*的使用方式相關。

現在,*操作符也可以用於元組拆包:

>>> 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 解包 文章中的示例。在那篇文章中,我將展示如何使用*操作符作爲序列切片的替代方法。

通常當我教*的時候,我告訴大家只能在多重賦值語句中使用一個*表達式。實際來說這是不正確的,因爲可以在嵌套解包中使用兩個*(我在元組解包文章中討論了嵌套解包):

>>> 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,其篇幅不是很長。

 

8、列表文字中的星號

Python 3.5 通過 PEP 448 引入了大量與*相關的新特性。其中最大的新特性之一是能夠使用*將迭代器轉儲到新列表中。

假設你有一個函數,它以任一序列作爲輸入,返回一個列表,其中該序列和序列的倒序連接在了一起:

def palindromify(sequence):  

    return list(sequence) + list(reversed(sequence))

 

此函數需要多次將序列轉換爲列表,以便連接列表並返回結果。在 Python 3.5 中,我們可以這樣編寫函數:

 

def palindromify(sequence):  

    return [*sequence, *reversed(sequence)]

 

這段代碼避免了一些不必要的列表調用,因此我們的代碼更高效,可讀性更好。

下面是另一個例子:

def rotate_first_item(sequence):    

    return [*sequence[1:], sequence[0]]

 

該函數返回一個新列表,其中給定列表(或其他序列)中的第一項被移動到了新列表的末尾。

* 運算符的這種使用是將不同類型的迭代器連接在一起的好方法。* 運算符適用於連接任何種類的迭代器,然而 + 運算符只適用於類型都相同的特定序列。

除了創建列表存儲迭代器以外,我們還可以將迭代器轉儲到新的元組或集合中:

>>> 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'}

 

注意,上面的最後一行使用了一個列表和一個生成器,並將它們轉儲到一個新的集合中。在此之前,並沒有一種簡單的方法可以在一行代碼中完成這項工作。曾經有一種方法可以做到這一點,可是並不容易被記住或發現:

 

9、兩個星號用於字典文本

PEP 448 還通過允許將鍵/值對從一個字典轉儲到一個新字典擴展了**操作符的功能:

>>> 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中合併字典的慣用方法。

不過,**操作符不僅僅可以用於合併兩個字典。

例如,我們可以在複製一個字典的同時添加一個新值:

>>> 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'}

 

或者在複製/合併字典的同時重寫特定的值:

 

>>> 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'}

 

10、Python 的星號非常強大

Python 的 * 和 ** 運算符不僅僅是語法糖。 * 和 ** 運算符允許的某些操作可以通過其他方式實現,但是往往更麻煩和更耗費資源。而且 * 和 ** 運算符提供的某些特性沒有替代方法實現:例如,函數在不使用 * 時就無法接受任意數量的位置參數。

在閱讀了* 和 ** 運算符的所有特性之後,您可能想知道這些奇怪操作符的名稱。不幸的是,它們的名字並不簡練。我聽說過* 被稱爲“打包”和“拆包“運算符。我還聽說過其被稱爲“splat”(來自 Ruby 世界),也聽說過被簡單地稱爲“star”。

我傾向於稱這些操作符爲“星”和“雙星”或“星星”。這種叫法並不能區分它們和它們的中綴關係(乘法和指數運算),但是通常我們可以從上下文清楚地知道是在討論前綴運算符還是中綴運算符。

請勿在不理解* 和 ** 運算符的前提下記住它們的所有用法!這些操作符有很多用途,記住每種操作符的具體用法並不重要,重要的是瞭解你何時能夠使用這些操作符。我建議使用這篇文章作爲一個備忘單或者製作你自己的備忘單來幫助你在 Python 中使用解* 和 ** 。

 

喜歡我的教學風格嗎?

想了解更多關於 Python 的知識嗎?我每週通過實時聊天分享我最喜歡的 Python 資源、回答 Python 問題。

尚學堂推出《13天搞定Python網絡爬蟲》視頻教程,學習成爲Python爬蟲工程師,薪資槓槓的!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章