- 原文地址:Multiple assignment and tuple unpacking improve Python code readability
- 原文做者:Trey Hunner
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:lsvih
- 校對者:Zheaoli
不管是教導新手仍是資深 Python 程序員,我都發現 不少 Python 程序員沒有充分利用多重賦值這一特性。html
多重賦值(也常常被稱爲元組解包或者可迭代對象解包)能讓你在一行代碼內同時對多個變量進行賦值。這種特性在學習時看起來很簡單,但在真正須要使用時再去回想它可能會比較麻煩。前端
在本文中,將介紹什麼是多重賦值,舉一些經常使用的多重賦值的樣例,並瞭解一些較少用、常被忽視的多重賦值用法。python
請注意在本文中會用到 f-strings 這種 Python 3.6 以上版本纔有的特性,若是你的 Python 版本較老,可使用字符串的 format
方法來代替這種特性。android
在本文中,我將使用「多重賦值」、「元組解包」、「迭代對象解包」等不一樣的詞,但他們其實表示的是同一個東西。ios
Python 的多重賦值以下所示:git
>>> x, y = 10, 20
複製代碼
在這兒咱們將 x
設爲了 10
,y
設爲了 20
。程序員
從更底層的角度看,咱們實際上是建立了一個 10, 20
的元組,而後遍歷這個元組,將拿到的兩個數字按照順序分別賦給 x
與 y
。github
寫成下面這種語法應該更容易理解:後端
>>> (x, y) = (10, 20)
複製代碼
在 Python 中,元組周圍的括號是能夠忽略的,所以在「多重賦值」(寫成上面這種元組形式的語法)時也能夠省去。下面幾行代碼都是等價的:數組
>>> x, y = 10, 20
>>> x, y = (10, 20)
>>> (x, y) = 10, 20
>>> (x, y) = (10, 20)
複製代碼
多重賦值常被直接稱爲「元組解包」,由於它在大多數狀況下都是用於元組。但其實咱們能夠用除了元組以外的任何可迭代對象進行多重賦值。下面是使用列表(list)的結果:
>>> x, y = [10, 20]
>>> x
10
>>> y
20
複製代碼
下面是使用字符串(string)的結果:
>>> x, y = 'hi'
>>> x
'h'
>>> y
'i'
複製代碼
任何能夠用於循環的東西都能和元組解包、多重賦值同樣被「解開」。
下面是另外一個能夠證實多重賦值能用於任何數量、任何變量(甚至是咱們本身建立的對象)的例子:
>>> point = 10, 20, 30
>>> x, y, z = point
>>> print(x, y, z)
10 20 30
>>> (x, y, z) = (z, y, x)
>>> print(x, y, z)
30 20 10
複製代碼
請注意,在上面例子中的最後一行咱們僅交換了變量的名稱。多重賦值可讓咱們輕鬆地實現這種情形。
下面咱們將討論如何使用多重賦值。
你會常常在 for
循環中看到多重賦值。下面舉例說明:
先建立一個字典(dict):
>>> person_dictionary = {'name': "Trey", 'company': "Truthful Technology LLC"}
複製代碼
下面這種循環遍歷字典的方法比較少見:
for item in person_dictionary.items():
print(f"Key {item[0]} has value {item[1]}")
複製代碼
但你會常常看到 Python 程序員經過多重賦值來這麼寫:
for key, value in person_dictionary.items():
print(f"Key {key} has value {value}")
複製代碼
當你在 for 循環中寫 for X in Y
時,實際上是告訴 Python 在循環的每次遍歷時都對 X
作一次賦值。與用 =
符號賦值同樣,這兒也可使用多重賦值。
這種寫法:
for key, value in person_dictionary.items():
print(f"Key {key} has value {value}")
複製代碼
在本質上與這種寫法是一致的:
for item in person_dictionary.items():
key, value = item
print(f"Key {key} has value {value}")
複製代碼
與前一個例子相比,咱們其實就是去掉了一個沒有必要的額外賦值。
所以,多重賦值在用於將字典元素解包爲鍵值對時十分有用。此外,它還在其它地方也可使用:
在內置函數 enumerate
的值拆分紅對時,也是多重賦值的一個頗有用的場景:
for i, line in enumerate(my_file):
print(f"Line {i}: {line}")
複製代碼
還有 zip
函數:
for color, ratio in zip(colors, ratios):
print(f"It's {ratio*100}% {color}.")
複製代碼
for (product, price, color) in zip(products, prices, colors):
print(f"{product} is {color} and costs ${price:.2f}")
複製代碼
若是你還對 enumerate
和 zip
不熟悉,請參閱做者以前的文章 looping with indexes in Python。
有些 Python 新手常常在 for
循環中看到多重賦值,而後就認爲它只能與循環一塊兒使用。但其實,多重賦值不只能夠用在循環賦值時,還能夠用在其它任何須要賦值的地方。
不多有人在代碼中對索引進行硬編碼(好比 point[0]
、items[1]
、vals[-1]
):
print(f"The first item is {items[0]} and the last item is {items[-1]}")
複製代碼
當你在 Python 代碼中看到有硬編碼索引時,通常均可以設法使用多重賦值來讓你的代碼更具可讀性。
下面是一些使用了硬編碼索引的代碼:
def reformat_date(mdy_date_string):
"""Reformat MM/DD/YYYY string into YYYY-MM-DD string."""
date = mdy_date_string.split('/')
return f"{date[2]}-{date[0]}-{date[1]}"
複製代碼
咱們能夠經過多重賦值,分別對月、天、年三個變量進行賦值,讓代碼更具可讀性:
def reformat_date(mdy_date_string):
"""Reformat MM/DD/YYYY string into YYYY-MM-DD string."""
month, day, year = mdy_date_string.split('/')
return f"{year}-{month}-{day}"
複製代碼
所以當你準備對索引進行硬編碼時,請停下來想想是否是應該用多重賦值來改善代碼的可讀性。
在咱們對可迭代對象進行解包時,多重賦值的條件是很是嚴格的。
若是將一個較大的可迭代對象解包到一組數量更小的對象中,會報下面的錯誤:
>>> x, y = (10, 20, 30)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)
複製代碼
若是將一個較小的可迭代對象解包到一組數量更多的對象中,會報下面的錯誤:
>>> x, y, z = (10, 20)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 3, got 2)
複製代碼
這種嚴格的限制其實很棒,若是咱們在處理元素時出現了非預期的對象數量,多重賦值會直接報錯,這樣咱們就能發現一些尚未被發現的 bug。
舉個例子。假設咱們有一個簡單的命令行程序,經過原始的方式接受參數,以下所示:
import sys
new_file = sys.argv[1]
old_file = sys.argv[2]
print(f"Copying {new_file} to {old_file}")
複製代碼
這個程序但願接受兩個參數,以下所示:
$ my_program.py file1.txt file2.txt
Copying file1.txt to file2.txt
複製代碼
但若是在運行程序時輸入了三個參數,也不會有任何報錯:
$ my_program.py file1.txt file2.txt file3.txt
Copying file1.txt to file2.txt
複製代碼
因爲咱們沒有驗證接收到的參數是否爲 2 個,所以不會報錯。
若是使用多重賦值來代替硬編碼索引,在賦值時將會驗證程序是否真的接收到了指望個數的參數:
import sys
_, new_file, old_file = sys.argv
print(f"Copying {new_file} to {old_file}")
複製代碼
注意: 咱們用了一個名爲 _
的變量,意思是咱們不想關注 sys.argv[0]
(對應的是咱們程序的名稱)。用 _
對象來忽略不需關注的變量是一種經常使用的語法。
根據上文,咱們一直多重賦值能夠用來代替硬編碼的索引,而且它嚴格的條件能夠確保咱們處理的元組或可迭代對象的大小是正確的。
此外,多重賦值還能用於代替硬編碼的數組拆分。
「拆分」是一種手動將 list 或其它序列中的部分元素取出的方法、
下面是一種用數字索引進行「硬編碼」拆分的方法:
all_after_first = items[1:]
all_but_last_two = items[:-2]
items_with_ends_removed = items[1:-1]
複製代碼
當你在拆分時發現沒有在拆分索引中用到變量,那麼就能用多重賦值來替代它。爲了實現多重賦值拆分數組,咱們將用到一個以前沒提過的特性:*
符號。
*
符號於 Python 3 中加入了多重賦值的語法中,它可讓咱們在解包時拿到「剩餘」的元素:
>>> numbers = [1, 2, 3, 4, 5, 6]
>>> first, *rest = numbers
>>> rest
[2, 3, 4, 5, 6]
>>> first
1
複製代碼
所以,*
可讓咱們在取數組末尾時替換硬編碼拆分。
下面兩行是等價的:
>>> beginning, last = numbers[:-1], numbers[-1]
>>> *beginning, last = numbers
複製代碼
下面兩行也是等價的:
>>> head, middle, tail = numbers[0], numbers[1:-1], numbers[-1]
>>> head, *middle, tail = numbers
複製代碼
有了 *
和多重賦值以後,你能夠替換一切相似於下面這樣的代碼:
main(sys.argv[0], sys.argv[1:])
複製代碼
能夠寫成下面這種更具自描述性的代碼:
program_name, *arguments = sys.argv
main(program_name, arguments)
複製代碼
總之,若是你寫了硬編碼的拆分代碼,請考慮一下你能夠用多重賦值來讓這些拆分的邏輯更加清晰。
這個特性是 Python 程序員長期以來常常忽略的一個東西。它雖然不如我以前提到的幾種多重複值用法經常使用,可是當你用到它的時候會深入體會到它的好處。
在前文,咱們已經看到多重賦值用於解包元組或者其它的可迭代對象,但還沒看過它更進一步地進行深度解包。
下面例子中的多重賦值是淺度的,由於它只進行了一層的解包:
>>> color, point = ("red", (1, 2, 3))
>>> color
'red'
>>> point
(1, 2, 3)
複製代碼
而下面這種多重賦值能夠認爲是深度的,由於它將 point
元組也進一步解包成了 x
、y
、z
變量:
>>> color, (x, y, z) = ("red", (1, 2, 3))
>>> color
'red'
>>> x
1
>>> y
2
複製代碼
上面的例子可能比較讓人迷惑,因此咱們在賦值語句兩端加上括號來讓這個例子更加明瞭:
>>> (color, (x, y, z)) = ("red", (1, 2, 3))
複製代碼
能夠看到在第一層解包時獲得了兩個對象,可是這個語句將第二個對象再次解包,獲得了另外的三個對象。而後將第一個對象及新解出的三個對象賦值給了新的對象(color
、x
、y
、z
)。
下面以這兩個 list 爲例:
start_points = [(1, 2), (3, 4), (5, 6)]
end_points = [(-1, -2), (-3, 4), (-6, -5)]
複製代碼
下面的代碼是舉例用淺層解包來處理上面的兩個 list:
for start, end in zip(start_points, end_points):
if start[0] == -end[0] and start[1] == -end[1]:
print(f"Point {start[0]},{start[1]} was negated.")
複製代碼
下面用深度解包來作一樣的事情:
for (x1, y1), (x2, y2) in zip(start_points, end_points):
if x1 == -x2 and y1 == -y2:
print(f"Point {x1},{y1} was negated.")
複製代碼
請注意在第二個例子中,在處理對象時,對象的類型明顯更加清晰易懂。深度解包讓咱們能夠明顯的看到,在每次循環中咱們都會收到兩個二元組。
深度解包一般會在每次獲得多個元素的嵌套循環中使用。例如,你能在同時使用 enumerate
與 zip
時應用深度多重賦值:
items = [1, 2, 3, 4, 2, 1]
for i, (first, last) in enumerate(zip(items, reversed(items))):
if first != last:
raise ValueError(f"Item {i} doesn't match: {first} != {last}")
複製代碼
前面我提到過多重賦值對於可迭代對象的大小以及解包的大小是很是嚴格的,在復讀解包中咱們也能夠利用這點嚴格控制可迭代對象的大小。
這麼寫能夠正常運行:
>>> points = ((1, 2), (-1, -2))
>>> points[0][0] == -points[1][0] and points[0][1] == -point[1][1]
True
複製代碼
這種看起來 bug 的代碼也能正常運行:
>>> points = ((1, 2, 4), (-1, -2, 3), (6, 4, 5))
>>> points[0][0] == -points[1][0] and points[0][1] == -point[1][1]
True
複製代碼
這種寫法也能運行:
>>> points = ((1, 2), (-1, -2))
>>> (x1, y1), (x2, y2) = points
>>> x1 == -x2 and y1 == -y2
True
複製代碼
可是這樣不行:
>>> points = ((1, 2, 4), (-1, -2, 3), (6, 4, 5))
>>> (x1, y1), (x2, y2) = points
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)
複製代碼
在給變量多重賦值時咱們其實也對可迭代對象作了一次特殊的斷言(assert)。所以多重賦值既能讓別人更容易理清你的代碼(由於有着更好的代碼可讀性),也能讓電腦更好地理解你的代碼(由於對代碼進行了確認保證了正確性)。
在前文我提到的多重賦值都用的是元組類型的語法(tuple-like),但其實多重賦值能夠用於任何可迭代對象。而這種相似元組的語法也使得多重賦值常被稱爲「元組解包」。而更準確地來講,多重賦值應該叫作「可迭代對象解包」。
前文中我尚未提到過,多重賦值能夠寫成 list 類型的語法(list-like)。
下面是一個應用 list 語法的最簡單多重賦值示例:
>>> [x, y, z] = 1, 2, 3
>>> x
1
複製代碼
這種寫法看起來很奇怪。爲何在元組語法以外還要容許這種 list 語法呢?
我也不多使用這種特性,但它在一些特殊狀況下能讓代碼更加簡潔。
舉例,假設我有下面這種代碼:
def most_common(items):
return Counter(items).most_common(1)[0][0]
複製代碼
咱們用心良苦的同事決定用深度多重賦值將代碼重構成下面這樣:
def most_common(items):
(value, times_seen), = Counter(items).most_common(1)
return value
複製代碼
看到賦值語句左側的最後一個逗號了嗎?很容易會將它漏掉,並且這個逗號讓代碼看起來不三不四。這個逗號在這段代碼中是作什麼事的呢?
此處的尾部逗號實際上是構造了一個單元素的元組,而後對此處進行深度解包。
能夠將上面的代碼換種寫法:
def most_common(items):
((value, times_seen),) = Counter(items).most_common(1)
return value
複製代碼
這種寫法讓深度解包的語法更加明顯了。但我更喜歡下面這種寫法:
def most_common(items):
[(value, times_seen)] = Counter(items).most_common(1)
return value
複製代碼
賦值中的 list 語法讓它更加的清晰,能夠明確看出咱們將一個單元素可迭代對象進行了解包,並將單元素又解包並賦值給 value
與 times_seen
對象。
當我看到這種代碼時,能夠很是肯定咱們解包的是一個單元組 list(事實上代碼作的也正是這個)。咱們在此處用了 collections 模組中的 Counter 對象。Counter
對象的 most_common
方法可讓咱們指定返回 list 的長度。在此處咱們將 list 限制爲僅返回一個元素。
當你在解包有不少的值的結構(好比說 list)或者有肯定個數值的結構(好比說元組)時,能夠考慮用 list 語法來對這些相似 list 的結構進行解包,這樣能讓代碼更加具備「語法正確性」。
若是你樂意,還能夠用對類 list 結構使用 list 語法解包時應用一些 list 的語法(常見的例子爲在多重賦值時使用 *
符號):
>>> [first, *rest] = numbers
複製代碼
我本身其實不經常使用這種寫法,由於我沒有這個習慣。但若是你以爲這種寫法有用,能夠考慮在你本身的代碼中用上它。
結論:當你在代碼中用多重賦值時,能夠考慮在什麼時候的時候用 list 語法來讓你的代碼更具自解釋性並更加簡潔。這有時也能提高代碼的可讀性。
多重賦值能夠提升代碼的可讀性與正確性。它能使你代碼更具自描述性,同時也能夠對正在進行解包的可迭代對象的大小進行隱式斷言。
據我觀察,人們常常忘記多重賦值能夠替換硬編碼索引,以及替換硬編碼拆分(用 *
語法)。深度多重賦值,以及同時使用元組語法和 list 語法也常被忽視。
認清並記住全部多重賦值的用例是很麻煩的。請隨意使用本文做爲你使用多重賦值的參考指南。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。