學習軌道
/
Python
Python
/
練習
/
火車駕駛員
火車駕駛員

火車駕駛員

學習練習

簡介

解包指的是使用疊代,從集合(例如 list、tuple 或 dict)中取出元素的動作。 解包出來的值接著可以在同一道敘述中指定給變數,這通常稱為多重賦值。

特殊運算子 * 和 ** 經常出現在解包的場合,也常與多重賦值一起使用。

Caution

*<variable_name> 和 **<variable_name> 不該與 * 和 ** 搞混。* 和 ** 分別用於乘法和指數運算,而 *<variable_name> 和 **<variable_name> 則是打包與解包運算子。

多重賦值

在多重賦值中,賦值運算子(=)左邊的變數數量,必須與右邊的值的數量相符。 若要分隔這些值,請使用逗號 ,:

>>> a, b = 1, 2
>>> a
1

如果多重賦值拿到的變數數量與給定的值不符,就會擲出 ValueError:

>>> x, y, z = 1, 2

ValueError: too many values to unpack (expected 3, got 2)

多重賦值並不限於單一資料型態:

>>> x, y, z = 1, "Hello", True
>>> x
1

>>> y
'Hello'

>>> z
True

多重賦值可以用來交換 lists 中的元素。 這種做法在排序演算法中相當常見。 例如:

>>> numbers = [1, 2]
>>> numbers[0], numbers[1] = numbers[1], numbers[0]
>>> numbers
[2, 1]

由於 tuples 是不可變的,你無法在 tuple 中交換元素。

解包

Note

底下的範例使用 lists,但同樣的概念也適用於 tuples。

在 Python 中,可以把 list/tuple/dictionary 的元素解包到各個不同的變數中。 由於 lists/tuples 中的值有固定的順序,它們會依照同樣的順序解包到變數中:

>>> fruits = ["apple", "banana", "cherry"]
>>> x, y, z = fruits
>>> x
"apple"

如果有不需要的值,可以用 _ 標記它們:

>>> fruits = ["apple", "banana", "cherry"]
>>> _, _, z = fruits
>>> z
"cherry"

深層解包

從包在 list 或 tuple 裡的 list/tuple(也就是巢狀陣列/元組)中解包並指定值,做法和淺層解包相同,但通常需要加上限定詞來說明情境或位置:

>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>>> [[a, b], [c, d]] = fruits_vegetables
>>> a
"apple"

>>> d
"potato"

你也可以只深層解包巢狀 list/tuple 的其中一部分:

>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>>> [a, [c, d]] = fruits_vegetables
>>> a
["apple", "banana"]

>>> c
"carrot"

如果解包時變數的位置不正確,或值的數量不正確,就會得到 ValueError:

>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>>> [[a, b], [d]] = fruits_vegetables

ValueError: too many values to unpack (expected 1)

用 * 解包陣列/元組

在解包 list/tuple時,你可以用 * 運算子捕捉「剩下的」值。 這比對 list/tuple 切片(在某些情況下可讀性較差)更清楚。 例如,可以取出第一個元素,再把剩下的值放進一個不含第一個元素的新 list:

>>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>>> x, *last = fruits
>>> x
"apple"

>>> last
["banana", "cherry", "orange", "kiwi", "melon", "mango"]

我們也可以取出 list 開頭和結尾的值,同時把中間所有的值分成一組:

>>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>>> x, *middle, y, z = fruits
>>> y
"melon"

>>> middle
["banana", "cherry", "orange", "kiwi"]

我們也可以在深層解包時使用 *:

>>> fruits_vegetables = [["apple", "banana", "melon"], ["carrot", "potato", "tomato"]]
>>> [[a, *rest], b] = fruits_vegetables
>>> a
"apple"

>>> rest
["banana", "melon"]

解包字典

解包字典與解包 list/tuple 有點不同。 對字典進行疊代時,預設會疊代鍵。 所以解包 dict 時,只能解包鍵,不能解包值:

>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3}
>>> x, y, z = fruits_inventory
>>> x
"apple"

如果想解包值,可以使用 <dict>.values() 方法:

>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3}
>>> x, y, z = fruits_inventory.values()
>>> x
6

如果鍵和值都需要,就使用 <dict>.items() 方法。 <dict>.items() 會產生一個包含鍵值配對的 iterable view。 這些配對可以解包到 tuple 中:

>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3}
>>> x, y, z = fruits_inventory.items()
>>> x
("apple", 6)

打包

打包是指把多個值組成一個 list 並指定給變數的能力。 當你想要_解包_值、做些修改,再把結果_打包_回一個變數時,這就很有用。 它也能對 2 個以上的 lists/tuples/dicts 進行合併。

用 * 打包陣列/元組

打包 list/tuple 可以用 * 運算子來完成。 這會把所有值打包成一個 list/tuple。

>>> fruits = ("apple", "banana", "cherry")
>>> more_fruits = ["orange", "kiwi", "melon", "mango"]

# fruits and more_fruits are unpacked and then their elements are packed into combined_fruits
>>> combined_fruits = *fruits, *more_fruits

# If there is no * on to the left of the "=" the result is a tuple
>>> combined_fruits
("apple", "banana", "cherry", "orange", "kiwi", "melon", "mango")

# If the * operator is used on the left side of "=" the result is a list.
# Note the trailing comma.
>>> *combined_fruits_too, = *fruits, *more_fruits
>>> combined_fruits_too
['apple', 'banana', 'cherry', 'orange', 'kiwi', 'melon', 'mango']

關於 * 用在等號左邊的更多背景,請參閱 PEP 3132。

用 ** 打包字典

打包字典是使用 ** 運算子來完成的。 這會把一個字典中所有的鍵-值配對打包進另一個字典,或把兩個字典合併在一起。

>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3}
>>> more_fruits_inventory = {"orange": 4, "kiwi": 1, "melon": 2, "mango": 3}

# fruits_inventory and more_fruits_inventory are unpacked into key-values pairs and combined.
>>> combined_fruits_inventory = {**fruits_inventory, **more_fruits_inventory}

# then the pairs are packed into combined_fruits_inventory
>>> combined_fruits_inventory
{"apple": 6, "banana": 2, "cherry": 3, "orange": 4, "kiwi": 1, "melon": 2, "mango": 3}

* 和 ** 在函式中的用法

用函式參數打包

當你建立的函式要接受任意數量的引數時,可以在函式定義中使用 *args 或 **kwargs。 *args 用來把任意數量的位置(非關鍵字)引數打包成 tuple, **kwargs 則用來把任意數量的關鍵字引數打包成字典。

*args 的用法:

# This function is defined to take any number of positional arguments

>>> def my_function(*args):
...     print(args)

# Arguments given to the function are packed into a tuple

>>> my_function(1, 2, 3)
(1, 2, 3)

>>> my_function("Hello")
("Hello")

>>> my_function(1, 2, 3, "Hello", "Mars")
(1, 2, 3, "Hello", "Mars")

**kwargs 的用法:

# This function is defined to take any number of keyword arguments

>>> def my_function(**kwargs):
...   print(kwargs)

# Arguments given to the function are packed into a dictionary

>>> my_function(a=1, b=2, c=3)
{"a": 1, "b": 2, "c": 3}

*args 和 **kwargs 也可以彼此搭配使用:

>>> def my_function(*args, **kwargs):
...   print(sum(args))
...   for key, value in kwargs.items():
...       print(str(key) + " = " + str(value))

>>> my_function(1, 2, 3, a=1, b=2, c=3)
6
a = 1
b = 2
c = 3

你也可以把參數寫在 *args 前面,以指定特定的位置引數。 這時個別的關鍵字引數就必須放在 **kwargs 前面。

Caution

引數必須照這樣的結構排列:

def my_function(<positional_args>, *args, <key-word_args>, **kwargs)

如果不遵守這個順序,就會出現錯誤。

>>> def my_function(a, b, *args):
...   print(a)
...   print(b)
...   print(args)

>>> my_function(1, 2, 3, 4, 5)
1
2
(3, 4, 5)

把引數寫成錯誤的順序會導致錯誤:

>>>def my_function(*args, a, b):
... print(args)

>>>my_function(1, 2, 3, 4, 5)
Traceback (most recent call last):
  File "c:\something.py", line 3, in <module>
    my_function(1, 2, 3, 4, 5)
TypeError: my_function() missing 2 required keyword-only arguments: 'a' and 'b'

解包到函式呼叫中

你可以用 * 把 list/tuple 的引數解包到函式呼叫中。 對於不接受 iterable 的函式來說,這非常實用:

>>> def my_function(a, b, c):
...   print(c)
...   print(b)
...   print(a)

numbers = [1, 2, 3]
>>> my_function(*numbers)
3
2
1

把 * 解包搭配 zip() 內建函式 使用,是另一個常見的用法。 zip() 函式接受多個 iterable,並回傳一個 list,裡面是將每個 iterable 的值分組而成的 tuples:

>>> values = (['x', 'y', 'z'], [1, 2, 3], [True, False, True])
>>> a, *rest = zip(*values)
>>> rest
[('y', 2, False), ('z', 3, True)]

說明

你的朋友 Linus 是位火車頭駕駛員,開著貨運列車往返於城市之間。 雖然他們很擅長處理列車的事,卻不太擅長處理物流或電腦。 他們想請你運用程式設計的專長,幫忙整理列車的細節,並修正路線資料中的錯誤。

Note

這道練習用切片、索引和各種 dict 方法就能輕鬆解決。 不過,我們希望你在解決以下每個任務的過程中,練習打包、解包和多重指派。

1. 建立所有車廂的陣列

你的朋友一直在記錄每個車廂的識別碼(ID),但他們永遠不確定系統在任何時刻需要處理多少個車廂。如果把這些資料打包成單一的 list,對物流程式的其餘部分來說會方便得多。

實作一個 get_list_of_wagons() 函式,接受任意數量的車廂 ID。 每個 ID 都會是正整數。 這個函式接著應該把傳入的 ID 以單一 list 的形式 return。

>>> get_list_of_wagons(1, 7, 12, 3, 14, 8, 5)
[1, 7, 12, 3, 14, 8, 5]

2. 修正車廂陣列

到了這裡,你已經開始對這些資料,以及它在物流程式中的用法有些感覺了。 ID 系統一律把火車頭的 ID 指派為 1,列車中其餘的車廂則隨機指派一個大於 1 的 ID。

你的朋友剛把兩節新車廂連上列車,卻忘了更新系統! 現在,列車 list 中的前兩個車廂必須移到最後,否則一切順序都會亂掉。

更麻煩的是,你的朋友剛剛又發現了第二個 list,裡面似乎包含了遺漏的車廂 ID。 他們只記得,新的車廂移好之後,第二個 list 裡的 ID 應該直接接在指定的火車頭後面。

Linus 會非常感謝你幫他們修正錯誤並整合資料。

實作一個 fix_list_of_wagons() 函式,接受兩個包含車廂 ID 的 lists。 它應該把第一個 list 的前兩個項目移到最後,並把第二個 list 中的值插入火車頭 ID(1)後方(也就是它的右側)。 這個函式接著應該 return 一個經過修改的 list。

>>> fix_list_of_wagons([2, 5, 1, 7, 4, 12, 6, 3, 13], [3, 17, 6, 15])
[1, 3, 17, 6, 15, 7, 4, 12, 6, 3, 13, 2, 5]

3. 新增缺少的停靠站

現在所有車廂資料都正確了,Linus 希望你更新系統的路線資訊。 沿著運輸路線,列車可能會在幾個不同的車站停靠,以裝卸貨物。 每趟旅程的中途交貨點數量都可能不同。 你的朋友希望你用任何缺少或額外的交貨資訊,更新系統的路線 dict。

實作一個 add_missing_stops() 函式,接受一個路線 dict,後面接著數量不固定的關鍵字引數。 這些引數的形式可以是包含一個或多個停靠站的 dict,也可以是任意數量的 stop_number=city 關鍵字配對。 你的函式接著應該回傳更新後的路線 dict,其中多了一個 key,存放依序加入的所有停靠站 list。

>>> add_missing_stops({"from": "New York", "to": "Miami"},
                      stop_1="Washington, DC", stop_2="Charlotte", stop_3="Atlanta",
                      stop_4="Jacksonville", stop_5="Orlando")

{"from": "New York", "to": "Miami", "stops": ["Washington, DC", "Charlotte", "Atlanta", "Jacksonville", "Orlando"]}

4. 擴充路線資訊

Linus 一直在開發路線程式,並注意到某些路線缺少了一些重要細節。 最初的路線資訊是以 dict 建構的,你的朋友希望你用任何可能缺少的內容更新那個 dict。 系統中的每條路線都需要稍微不同的細節,所以 Linus 真的很希望有個通用的解決方案。

實作一個名為 extend_route_information() 的函式,接受兩個 dicts。 第一個 dict 包含列車路線往返的起點與終點城市。

第二個 dict 包含其他路線細節,例如列車速度、長度或溫度。 這個函式應該回傳一個整合了所有路線資訊的 dict。

Note

第二個 dict 可以包含與範例所示不同或更多的屬性。

>>> extend_route_information({"from": "Berlin", "to": "Hamburg"}, {"length": "100", "speed": "50"})
{"from": "Berlin", "to": "Hamburg", "length": "100", "speed": "50"}

5. 整理車廂停放場

Linus 在勘查車廂停放場時,注意到車廂沒有按照正確的順序存放。 除了 ID 之外,每個車廂還有對應其載運貨物種類的顏色。 車廂在停放場中以網格存放,網格裡的每一欄都是相同顏色的車廂。

然而,物流系統顯示要存放進停放場的車廂 lists,其_列_卻按顏色分組。 但要讓存放網格正常運作,每一_列_應該有三種不同的顏色,這樣_欄_才能按顏色對齊。 你的朋友希望你整理停放場的車廂 lists,讓車廂能正確存放。

實作一個名為 fix_wagon_depot() 的函式,接受一個包含三個項目的 list。 每個 list 項目都是一個子陣列(或「列」),內含三個 tuples。 每個 tuple 都是一組 (<wagon ID>, <wagon color>) 配對。

你的函式應該回傳一個 list,其中三個「列」lists 已重新排序,讓車廂交換到正確的位置。

>>> fix_wagon_depot([
                    [(2, "red"), (4, "red"), (8, "red")],
                    [(5, "blue"), (9, "blue"), (13,"blue")],
                    [(3, "orange"), (7, "orange"), (11, "orange")],
                    ])

[
[(2, "red"), (5, "blue"), (3, "orange")],
[(4, "red"), (9, "blue"), (7, "orange")],
[(8, "red"), (13,"blue"), (11, "orange")]
]
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Python Exercism

準備好開始 火車駕駛員 了嗎?

註冊 Exercism,透過 17 個概念146 個練習 和真人引導來學習並精通 Python,全部免費。