複数のリストの要素に同時にアクセスする場合、何を使いますか? インデックス? いえいえ、もっといい方法がPythonにはあるんです。もちろん覚えていますよね?
以下のPythonコードでは2つのリストがある。1つは名前を格納し、もう1つはそのランクを格納している。これら2つのリストは同じインデックス位置に対応するデータが含まれているものとする。例えば、インデックス0について見ると、その名前はmikeであり、そのランクは5ということだ。そして、for文ではインデックスを用いて、それらのデータにアクセスをしている。このコードをもっとPythonらしいコードに直すにはどうすればよいだろう。
names = ['mike', 'pochi', 'tama', 'kuro', 'shiro']
ranks = [5, 2, 4, 1, 3]
for idx in range(len(ranks)):
print(f'{names[idx]}: {ranks[idx]}')
どうもHPかわさきです。あぶない、あぶない。編集長から指摘があって、ここを書き忘れていることに気が付きました(笑)。原稿を全て書き終わった後に書いているんですよね、この場所。問題とは関係ないことを書くこともあれば、関係あることを書くことも。フリーな枠として考えています。
というわけで今回はちょっと関係あることを。この問題は「インデックスと値の両方が必要なとき、どうしますか?」と対になるといってもいいでしょう。これでリンク先に飛ばれるとめっちゃヒントになるような気がするので、全部読んでからリンクを踏んでもらえるとうれしいです。
以下に正解のコード例を示します。
names = ['mike', 'pochi', 'tama', 'kuro', 'shiro']
ranks = [5, 2, 4, 1, 3]
for name, rank in zip(names, ranks):
print(f'{name}: {rank}')
zip関数を使うことで、2つのリストの同じインデックス位置にある要素がタプルとして反復されるようになります。これにより、インデックスを使って「names[idx]」「ranks[idx]」のようにアクセスする必要がなくなります。
zip関数は引数として渡した反復可能オブジェクト(リストなど)の同じインデックス位置にある要素をタプルにまとめて列挙してくれます。イメージとしては次の画像のようになるでしょう。
赤い線の部分がジッパー(ファスナー)っぽく見えるような表現にしてみました(見えるかな?)。とはいえ、ホントにこれが関数の名前の語源かどうかは知りません。語源を知っている方は教えてくださいな。
問題文のコードは以下のようなものでした。
names = ['mike', 'pochi', 'tama', 'kuro', 'shiro']
ranks = [5, 2, 4, 1, 3]
for idx in range(len(ranks)):
print(f'{names[idx]}: {ranks[idx]}')
これはリストranksの要素数を得て、そこからインデックス範囲を設定して、forループで2つのリストの要素にアクセスしています。これでも十分に動作しますが、ループで処理する対象がfor文の先頭を見てもハッキリしないという欠点があります。これに対して、zip関数を使うと次のようにforループで扱う対象がハッキリと分かるようになります。
names = ['mike', 'pochi', 'tama', 'kuro', 'shiro']
ranks = [5, 2, 4, 1, 3]
for name, rank in zip(names, ranks):
print(f'{name}: {rank}')
zip関数でタプルにまとめられた要素を複数のループ変数で受け取るようにすれば、ループ変数に分かりやすい名前を付けることもできます(上の例ではnameとrank)。for文のブロックで「names[idx]」「ranks[idx]」などと表現するよりも、これらを使って「name」「rank」のように表現した方が簡潔で読みやすいという人もいるでしょう(ささいな差ではありますが)。
インデックスを使う方法には別の問題もあります。例えば、namesとranksの要素数が異なっていたらどうなるでしょう。
names = ['mike', 'pochi', 'tama', 'kuro']
ranks = [5, 2, 4, 1, 3]
for idx in range(len(ranks)):
print(f'{names[idx]}: {ranks[idx]}')
この場合、for文を実行している最中にIndexError例外が発生してしまうでしょう。zip関数はこうした場合でも例外を発生せずに、最も要素数が少ないリストの反復が終わった時点で全体としての反復処理を終了してくれます。
names = ['mike', 'pochi', 'tama', 'kuro']
ranks = [5, 2, 4, 1, 3]
for name, rank in zip(names, ranks):
print(f'{name}: {rank}')
なお、zip関数に与える全ての反復可能オブジェクトの要素数が等しいはずだという場合もあります。そのときには、全ての反復可能オブジェクトの要素数が等しくなければ、それらの取得(生成)に問題があると判断して例外を発生させた方がよいかもしれません。そのときには、strictパラメーターにTrueを与えることで、要素数が等しくないときには例外を発生させることも可能です(Python 3.10以降)。以下はその例です。
names = ['mike', 'pochi', 'tama', 'kuro']
ranks = [5, 2, 4, 1, 3]
for name, rank in zip(names, ranks, strict=True):
print(f'{name}: {rank}')
この場合は、インデックスを使っているときと同様に、for文の実行中にValueError例外が発生します。そのメッセージは「反復可能オブジェクトのどれかが長いよ!」といった具合になるので、zip関数に与える反復可能オブジェクトに問題があるとハッキリ分かるでしょう。
このようなことから、インデックスを使って、複数のリスト(反復可能オブジェクト)にアクセスするコードを書くときにはzip関数を使うことがPythonならではやり方として推奨されています。最初は「うーむ」となるかもしれませんが、何度か使っているうちに「ああ、ここはzip関数の出番だね」となるはずなので、ドンドン使っていきましょう。
冒頭ではこの問題は「インデックスと値の両方が必要なとき、どうしますか?」と対になると言いましたが、それは「for idx in range(len(……)):」と書くことになったら、ちょっとコードを見直してみようという話になります。Pythonではこの記述を書かずに済むように、enumerate関数やzip関数が用意されているので、これらを活用するのが初心者脱出の第一歩となるでしょう。もちろん、元の表現が適切なこともあるので、絶対にコードを書き直すわけではないですが……。それでも、この表現が出てきたら、気にしてみましょう。
そして、今回紹介したzip関数を使う方法など、「Pythonならではのやり方」のことを「Pythonic」(パイソニック)と呼ぶことがあります。Pythonicなコードの書き方を身に付けていくと、自分のコードを書くときにも便利ですし、誰か別の人(あるいは何かの生成AI)が書いたコードを読み進めるときにも、その意味を理解しやすくなるはずです。
最後に(いつものごとく)zip関数については「Python入門」の「リストと繰り返し処理」でも触れています。興味のある方はぜひ目を通してくださいね。
Copyright© Digital Advantage Corp. All Rights Reserved.