トラック
/
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)

多重代入は1つのデータ型に限られません:

>>> 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/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()はキーと値のペアを含む反復可能なビューを生成します。 これらはtupleにアンパックできます:

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

パック

パックとは、複数の値を1つのlistにまとめて変数に代入する機能です。 値をもとに_アンパック_して変更を加え、その結果を再び_パック_して変数に戻したいときに便利です。 また、2つ以上のlists/tuples/dictsをマージすることもできます。

*を使ったlist/tupleのパック

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を参照してください。

**を使った辞書のパック

辞書のパックは、**演算子を使って行います。 これにより、ある辞書のキーと値のペアをすべて別の辞書にまとめたり、2つの辞書を結合したりできます。

>>> 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の値をまとめたtuplesのlistを返します:

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

説明

友人のリーナスは、都市の間で貨物列車を運転する機関士です。 列車の扱いはとても得意ですが、物流やコンピューターの扱いは得意ではありません。 そこで、列車の情報を整理し、ルートのデータの間違いを直すために、あなたのプログラミングの力を借りたいと考えています。

Note

この演習は、スライスやインデックス、さまざまなdictのメソッドを使えば簡単に解けます。 ただし、以下の各タスクは、パッキング、アンパッキング、多重代入の練習になるように解いてほしいと思っています。

1. すべての貨車のリストを作成する

友人はこれまで各貨車の識別子(ID)を記録してきましたが、システムが一度に処理しなければならない貨車の数はいつもはっきりしません。 このデータを1つのlistにまとめておけば、残りの物流プログラムはずっと扱いやすくなります。

任意の数の貨車IDを受け取る関数get_list_of_wagons()を実装しましょう。 それぞれのIDは正の整数です。 関数は、受け取ったIDを1つのlistとしてreturnする必要があります。

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

2. 貨車のリストを修正する

ここまでで、データとそれが物流プログラムでどのように使われるかが少しずつ見えてきたはずです。 IDシステムでは、機関車には必ず1というIDが割り当てられ、列車内の残りの貨車には1より大きいランダムなIDが割り当てられます。

友人は新しい貨車を2両、列車につなぐことになりましたが、システムを更新するのを忘れてしまいました! そのため、列車のlistの先頭2両を末尾に移動させないと、すべての順序が狂ってしまいます。

さらにややこしいことに、友人は、欠けていた貨車IDを含んでいるらしい2つ目のlistをたった今見つけました。 覚えているのは、新しい貨車を移動させたあと、この2つ目のlistにあるIDを、指定された機関車のすぐ後ろに置くべきだということだけです。

リーナスは、間違いを直してデータを一つにまとめてくれたら、とても感謝するでしょう。

貨車IDを含む2つのlistを受け取る関数fix_list_of_wagons()を実装しましょう。 1つ目のlistの最初の2つの要素を末尾に移動し、2つ目のlistの値を機関車ID(1)の後ろ(右側)に挿入します。 関数は、変更を加えたlistをreturnする必要があります。

>>> 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. 足りない停車駅を追加する

貨車のデータがすべて正しくなったので、リーナスは、システムのルート情報を更新してほしいと考えています。 輸送ルートでは、列車が貨物を積み下ろしするために、いくつかの駅に停車することがあります。 それぞれの輸送では、こうした中間の配送地点の数が異なることがあります。 友人は、足りない、または追加の配送情報を、システムのルートdictに反映してほしいと考えています。

ルートのdictと、その後に続く任意の数のキーワード引数を受け取る関数add_missing_stops()を実装しましょう。 これらの引数は、1つ以上の停車駅を持つdictの形でも、任意の数のstop_number=cityというキーワードペアでもかまいません。 関数は、追加されたすべての停車駅を順番に並べたlistを持つ追加のkeyで更新したルートdictを返す必要があります。

>>> 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. ルート情報を拡張する

リーナスはルートプログラムに取り組む中で、特定のルートに重要な詳細がいくつか欠けていることに気づきました。 最初のルート情報はdictとして組み立てられており、友人は、足りないものがあればそのdictを更新してほしいと考えています。 システム内のルートごとに必要な詳細は少しずつ異なるため、リーナスは汎用的な解決策をぜひとも望んでいます。

2つのdictを受け取るextend_route_information()という関数を実装しましょう。 1つ目のdictには、列車が走る区間の出発地と目的地の都市が入っています。

2つ目のdictには、列車の速度、長さ、温度など、その他のルートの詳細が入っています。 関数は、すべてのルート情報をまとめたdictを返す必要があります。

Note

2つ目のlistsには、例に示したものとは異なる、あるいはより多くのプロパティが含まれることがあります。

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

5. 貨車の車庫を修正する

リーナスが貨車の車庫を調査したところ、貨車が正しい順序で保管されていないことに気づきました。 各貨車にはIDのほかに、積んでいる貨物の種類に対応する色があります。 車庫では貨車は格子状に保管され、格子の各列には同じ色の貨車が並びます。

しかし、物流システムが表示する、車庫に保管する貨車のlistでは、_行_が色ごとにまとまっています。 ただし、保管用の格子が正しく機能するには、_列_が色でそろうように、各_行_に3つの異なる色が必要です。 友人は、貨車が正しく保管されるように、車両基地のlistを整理してほしいと考えています。

3つの要素を持つdictsを受け取るfix_wagon_depot()という関数を実装しましょう。 各listsの要素は、3つのtupleを含むサブリスト(つまり「行」)です。 各listsは(<wagon ID>, <wagon color>)のペアです。

関数は、貨車が正しい位置に入れ替わるように3つの「行」のtuplesを並べ替えた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を学んでマスターできます。すべて無料です。