cover_effective_python

【 Effective Python, 2nd Edition 】* 書式 (starred expression) をアンパックで活用しよう! 投稿一覧へ戻る

Tags: Python , unpack , Effective , expression , starred

Published 2020年6月16日17:42 by T.Tsuyoshi

通常のアンパックでは、事前にアンパック対象のシーケンスの長さを知っている必要があります。


ですから、次のようなリストから高得点上位2つだけをアンパック構文で取得しようとするとエラーになります。


scores = [78, 93, 61, 94, 88, 59, 71, 98, 66]
scores_descending = sorted(scores, reverse=True)
highest, second_highest = scores_descending

# Traceback:
# ValueError: too many values to unpack (expected 2)




それじゃぁ、と、インデックスとスライスを利用して、上位2つそれぞれと、残り全部を取得します。


highest = scores_descending[0]
second_highest = scores_descending[1]
others = scores_descending[2:]
print(f"{highest}, {second_highest}, {others}") # 98, 94, [93, 88, 78, 71, 66, 61, 59]



もちろん問題なく動作します。
が、1つのシーケンスをこのようにインデックス番号やスライスを利用して複数のサブセットに分割する場合、
いずれかのインデックス番号を変更した場合には他の部分も全て書き換える必要が生じる等、
将来的にエラーを発生させる余地を大きくしてしまいます。


そこで...


highest, second_highest, *others = scores_descending
print(f"{highest}, {second_highest}, {others}") # 98, 94, [93, 88, 78, 71, 66, 61, 59]



アンパックで * 書式 (starred expression) を利用することで、上の例のようにスッキリと取り出すことが可能です。


また * 書式はアンパック構文中の何処にでも記述することができます。


higehest, *others, lowest = scores_descending
print(f"{highest}, {lowest}, {others}") # 98, 59, [94, 93, 88, 78, 71, 66, 61]

*others, second_lowest, lowest = scores_descending
print(f"{lowest}, {second_lowest}, {others}") # 59, 61, [98, 94, 93, 88, 78, 71, 66]



ただし * 書式だけでシーケンス要素の全てを受けることはできません (これはシーケンス全体=シーケンスそのものを取得しようという行為ですからほぼ意味はないですね)。
必ず1つ以上の特定要素と共に利用する必要があります。


*all = scores_descending # SyntaxError: starred assignment target must be in a list or tuple



また、2つ以上の * 書式を1つのアンパック構文中に含めることもできません(切れ目が分かりませんから当然ですね)。


highest, *middle, *second_middle, lowest = scores_descending # SyntaxError: two starred expressions in assignment



ただし、おススメはできませんが、多階層構造になっているものをアンパックする場合には複数の * 書式を1つの構文中に記述できます。
繰り返しますが、おススメできません。あくまでアンパック中で * 書式を利用する際の可能性、と考えてご覧ください。。


high_grades = {
'Math': ('Nana', 'Yuka', 'Megu'),
'Programming': ('Saki', 'Hana'),
'German': ('Hana', 'Nana', 'Eri'),
'Finance': ('Saki', 'Megu'),
}

((sub1, (highest1, *others1)), (sub2, (highest2, *others2)), *other_sub) = high_grades.items()

print(f"{sub1} での最高得点者は {highest1}, その他 {len(others1)} 人。") # Math での最高得点者は Nana, その他 2 人。
print(f"{sub2} での最高得点者は {highest2}, その他 {len(others2)} 人。") # Programming での最高得点者は Saki, その他 1 人。
print(f"他の教科: {other_sub}") # 他の教科: [('German', ('Hana', 'Nana', 'Eri')), ('Finance', ('Saki', 'Megu'))]



ここまでの例でもお分かりのように、アンパック中の * 書式はリストを返します。
もし該当する要素がない場合でも、空のリストが返ってきます。


short_list = [1, 2]
first, second, *rest = short_list
print(f"{first}, {second}, {rest}") # 1, 2, []



アンパックは実体のあるシーケンスに対してだけではなく、イテレーターに対しても利用できます。


it = iter(range(1, 3))
first, second = it
print(f"{first} and {second}.") # 1 and 2.



何の役にもたたなそうですが、* 書式と一緒に利用すると...
例えば、ユーザー情報を保存した CSV ファイルがあり、それを1行ずつ読み込んでいく generator があるとします。


def generate_user_info_from_csv():
yield("名前", "住所", "電話番号", "年齢")
...



もちろんインデックス番号とスライスを利用しても処理可能です。


all_csv_rows = list(generate_user_info_from_csv())
header = all_csv_rows[0]
data_rows = all_csv_rows[1:]
print(f"項目: {header}") # 項目: ('名前', '住所', '電話番号', '年齢')
print(f"データは {len(data_rows)} 行あります。") # データは 2000 行あります。



ですが、アンパックと * 書式を利用することでこんなにスッキリします。


it = generate_user_info_from_csv()
header, *data_rows = it
print(f"項目: {header}") # 項目: ('名前', '住所', '電話番号', '年齢')
print(f"データは {len(data_rows)} 行あります。") # データは 2000 行あります。



ただし注意点もあります。


* 書式は常にリストを返します。ということは、元のデータが非常に大きい場合、メモリ消費が非常に大きい、もしくは使い切ってしまう可能性さえある、ということです。
ですから、イテレータを対象として * 書式を含んだアンパックを利用する場合は、最終的に取得するデータの大きさを常に意識しておかなければいけません。


まとめ

1: アンパックする際の特定要素以外の残り全ての要素は * 書式を利用してリストとして取得することができる。

2: * 書式はアンパック構文中の何処にでも記述可能で、0 個以上の要素を含むリストを返す。

3: あるリストを重複のないサブセットに分解する場合、* 書式を含むアンパック構文を利用することで、インデックス番号とスライスを利用した場合と比較して簡潔に記述することが可能で、かつ、変更に伴うエラー発生の抑止にもなる。

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)