リストの要素にはインデックスでアクセスできますが、for文ならインデックスなしでも反復が可能です。でも、要素とインデックスの両方が必要なときにはどうしたらよいでしょう。ちょっと思い出してみませんか。
以下はリストの要素をそのインデックスと共に表示するPythonのコードである。このコードは問題なく動くが、もっとPythonらしくスマートに記述する方法がある。どうすればよいだろう?
mylist = ['a', 'b', 'c', 'e', 'f', 'g']
for idx in range(len(mylist)):
print(f'{idx}: {mylist[idx]}')
どうもHPかわさきです。
今回は「カンタン過ぎる!」と思われるかもしれませんが、このネタにさせてもらいました。Pythonに入門したばかりの人にも知っておいてほしいイディオムの紹介ともいえます。とはいえ、エライ人からは「あれ? もうネタ切れ?」と言われたこともご報告しておきましょう。ネタ切れちゃいますよ。何も考えていないように見えて、いろいろ考えているんですよ(今日のお昼は何にしようかな? とか)。
この手の「カンタン過ぎるかもしれない」ネタはこれからもたまに出していく予定です。よろしくお願いします(正直、手探り状態がまだ続いているのです)。
正解のコード例を以下に示します。
mylist = ['a', 'b', 'c', 'e', 'f', 'g']
for idx, value in enumerate(mylist):
print(f'{idx}: {value}')
正解のコード例では、enumerate関数を使って、リストのインデックスと対応する要素の両者をfor文のループ変数に取り出すようにすることで「range(len(mylist))」「mylist[idx]」のような書き方をしないようにしています。
リストの要素にはインデックスを使って「リスト[インデックス]」のようにしてアクセスできます。インデックスを先頭から順番に反復するにはrange関数を使って「range(リストの要素数)」とすることで取り出せます。そして、リストの要素数はlen関数を使って「len(リスト)」とすれば得られます。これらを組み合わせたのが問題文にあるコードです。
mylist = ['a', 'b', 'c', 'e', 'f', 'g']
for idx in range(len(mylist)):
print(f'{idx}: {mylist[idx]}')
こうした書き方はPythonのイディオムともいえますが、リストの要素とインデックスの両者を反復することが目的であれば、enumerate関数を使うのがPythonではより一般的な書き方です。
mylist = ['a', 'b', 'c', 'e', 'f', 'g']
for idx, value in enumerate(mylist):
print(f'{idx}: {value}')
enumerate関数は反復可能なオブジェクトを受け取り、そのインデックスと要素を「(インデックス, 要素)」というタプルとして反復してくれます。タプルとして反復されたインデックスと要素は「for idx, value in enumerate(...):」のように書くことで、個別のループ変数に取り出せることもポイントです。
こんな書き方をすることで、「range(len(mylist))」と書くことでどことなくスッキリしない気持ちがスッキリするようになってきたら、Pythonプログラマーとしてちょこっとステップアップしたといえるかもしれませんね。
とはいえ、「range(len(...))」という書き方が全くダメというわけではないことには注意しましょう。例えば、for文の中で何らかの条件を基にその要素の値を変更したいとします。
mylist = [0, 1, 2, 3, 4]
for value in mylist:
if value % 2 == 0:
value = value * 2
print(mylist)
例えば、上のコードはリストの要素を反復して、その値が2の倍数なら、それを2倍した値に変更するつもりのコードです。しかし、これを実行すると次のような結果になります。
for文の反復のたびに、ループ変数(value)にはリストの要素が順番に代入されていきます。しかし、ループ変数の値を書き換えるというのは「リストの要素を書き換える」とは意味が違うのです。書き換える前のループ変数は、リストの要素を参照していましたが、書き換えることでそのループ変数が別のオブジェクトを参照するようになるだけです。
以下は上のコードでループが進み、valueに4が代入されたところを図にしたものです。
このとき、「value *= 2」で何が行われるかというと、変数valueが参照する先が「8」というint型のオブジェクトになるだけで、リストの要素(mylist[4])の参照する先はそのままです。
というわけで、リストの要素を変更したいのであれば、やはりインデックスによるアクセスが必要になります。こんなときには「range(len(...))」という書き方が合っているかもしれません。
mylist = [0, 1, 2, 3, 4]
for idx in range(len(mylist)):
if mylist[idx] % 2 == 0:
mylist[idx] *= 2
print(mylist)
とはいえ、これはenumerate関数を使って次のようにも書けます。
mylist = [0, 1, 2, 3, 4]
for idx, value in enumerate(mylist):
if value % 2 == 0:
mylist[idx] *= 2
print(mylist)
あるいはリスト内包表記を使うのがいい、という人もいるでしょう。
mylist = [0, 1, 2, 3, 4]
mylist = [value * 2 if value % 2 == 0 else value for value in mylist]
print(mylist)
これらのコードのどちらがよいかの判断は筆者にはつきません(リスト内包表記は使わないかなぁ。if式を内包表記で使うと長くなりますよね)。リストの要素とインデックスを取得することが主であればenumerate関数を使って、リストの要素の変更までを考えるのであれば「range(len(...))」という書き方もアリくらいに考えておけばよいでしょう。
変数が参照であることで生まれるあんなことやそんなことについては、「忘れた頃に問題に出すからね」(ビシッ!)ということにしましょう。
そして、enumerate関数については「Python入門」の「リストと繰り返し処理」でも解説をしています。詳しいことはそちらを参照してください。
Copyright© Digital Advantage Corp. All Rights Reserved.