🔄

いまさら聞けない!Pythonのforループの仕組みをStep-by-stepで理解する

2023/04/02に公開

はじめに

Pythonのforループって、なんだか難しくないですか??🤔

ナゾのrange()関数が登場したり、リストを与えたらそのまま値を取得できたりと、はじめて出会う考え方も多いですよね。自分も初心者の頃は、これらの挙動を理解するのに苦労しました。実はこれまでも、なんとなくの理解だったりします。

そこで今回は、Pythonの繰り返し処理の仕組みをStep-by-stepで読み解くことで、forループで何が行われているかをしっかりと把握することを目指します。「forループは書けるけど、その仕組みはよくわかっていない」という方にオススメです。

例題

今回は、整数のリストを入力として、リストの中身を先頭から順に出力する問題を対象とします。

入力
numbers = [11, 22, 33, 44, 55]
出力
11
22
33
44
55

この出力は、以下のようなコードで実現できます。入門書などでは、2通りの書き方で説明されていることが多いですね。

forループを使った解答例
numbers = [11, 22, 33, 44, 55]

# 通常の実装方法
for number in numbers:
    print(number)

# range()を利用する方法
for i in range(len(numbers)):
    print(numbers[i])

range()を利用した書き方は、なんとなく実行の仕組みがわかるものの、通常の実装方法はどういう仕組みで実現できているか、実はよくわかっていない方もいるのではないでしょうか。(たとえば、どっちにリストを書いたら良いか分からなくなったり...)この記事では、以下の内容をStep-by-stepで説明し、Pythonにおけるforループの仕組みを理解することを目指します。

  • 通常のforループの実行の仕組み。
  • range()を利用するforループの実行の仕組み。

繰り返し処理のアルゴリズム

はじめに、繰り返し処理のアルゴリズム(手順や計算方法)を考えてみます。今回のケースの場合、以下の図のように、リストの中身を順に取得することで実現できますね。

loop
繰り返し処理のアルゴリズム

この処理を実装するために、以下のような仕組みが必要だとわかります。

  • リストの次の要素を取得する。
  • 次の要素がなくなったらエラー(例外)を出す。
  • 要素を取得できていたら、繰り返し処理を続ける。

今回は、forループの仕組みを理解するために、forループを使わずにこの処理を実現する方法から考えてみます。

イテレーター

Pythonでは、上記の処理を実現するために、イテレーター(iterate=繰り返す)というものが存在します。繰り返し処理を実行できるオブジェクトを意味します。

リストなどのシーケンス(複数の要素を順序付けて格納するためのデータ型のこと)に対してiter()関数を使うことで、イテレーターを取得できます。さらにイテレーターに対してnext()関数を使うことで、イテレーターの値を順に取得できます。

文章だとわかりにくいので、具体的に実装してみます。以下のように、numbersリストのイテレーターを取得し、next()関数を繰り返しコールして、中身を順に取得します。ここでは5つの要素に対して、6回コールしています。

ソースコード
numbers = [11, 22, 33, 44, 55]

num_iter = iter(numbers)
print(next(num_iter))
print(next(num_iter))
print(next(num_iter))
print(next(num_iter))
print(next(num_iter))
print(next(num_iter))

出力は以下のようになります。リストの要素を順に出力できていますね!最後まで出力したあとにnext()をコールすると、StopIterationという例外が出ています。この例外によって、リストの終了を検知できます。

出力
$ python sample.py
11
22
33
44
55
Traceback (most recent call last):
  File "/sample.py", line 9, in <module>
    print(next(num_iter))
          ^^^^^^^^^^^^^^
StopIteration

whileループで繰り返し処理を実装する

次に、先ほどの例をwhileループで書き換えてみます。StopIterationの例外が出るまでnext()の処理を繰り返します。

while
numbers = [11, 22, 33, 44, 55]
num_iter = iter(numbers)

while True:
    try:
        number = next(num_iter)
        print(number)
    except StopIteration:
        break

今回は例外が出るところで自動的に処理を終了しています。これで、forループと同じ処理を実装できました。

出力
$ python sample.py
11
22
33
44
55

forループで繰り返し処理を実装する

whileループを使う場合だと、書くのが面倒ですね。そこで、最初に書いたforループが登場します。以下のような書き方で、whileループと同様の出力を得ることができます。

forループ
# 通常の実装方法
for number in numbers:
    print(number)

ここまでの説明から、forループでは以下の処理をまとめて行なっていることがわかります。

  • numbersリストのイテレーターを生成する。
  • イテレーターの中身を順に取り出し、numberに代入する。
  • 値を取得できたらforループ内の処理を実行する。
  • StopIterationの例外が出たらforループを終了する。

このように、forループを使用することで、whileループとnext()関数を使った繰り返し処理を簡略化できます。

複雑な処理を簡潔に記載できる反面、Pythonのforループは具体的な動きを理解しづらくなっている気がします。このような内部実装を把握しておくと、プログラムが動作する仕組みをより深く理解できると思います。

range()を利用したforループ

次に、range()を利用したforループについて考えます。range()range型のオブジェクトを返す関数です。たとえばrange(5)は、「0~5までの整数の範囲を表すrange型のオブジェクト」を返します。range型のオブジェクトも、リストと同じようにイテレーターを取得できます。

......ややこしくなってきたので、具体的な実装を書いてみます。最初と同じように、iter()next()range()の中身を順に取得してみます。

range()
itr = iter(range(5))
print(next(itr))
print(next(itr))
print(next(itr))
print(next(itr))
print(next(itr))
print(next(itr))

整数を順に取得した後に、StopIteration例外が出力されました。

出力
$ python sample.py
0
1
2
3
4
Traceback (most recent call last):
  File "/sample.py", line 29, in <module>
    print(next(itr))
          ^^^^^^^^^
StopIteration

このことから、range()をforループに組み込むことで、値を順に取得できることがわかります。最初に例示した、range()を利用したforループの例を見てみます。変数i0,1,2,3,4の値を順に代入し、その変数を利用してnumbersリストの中身を出力しています。

range()
# range()を利用する方法
for i in range(len(numbers)):
    print(numbers[i])

ここで、通常のforループの書き方と、range()を使う場合のどちらを使うべきかを考えてみます。通常の書き方では、リストの中身を直接取得しています。range()を使う場合は、リストの長さを取得し、range型のオブジェクトを生成し、添字を介してリストにアクセスしています。

そのため、今回のようなケースでは通常の書き方が簡潔です。ただし、操作対象のデータの中身が非常に大きい場合などは、range()を利用すると効率的な場合があります。

enumerate()とzip()を利用したforループ

ここまでの内容を理解しておくと、入門書などでforループとセットで説明されるenumerate()zip()の仕組みも理解しやすいです。これらの関数もイテレーターを生成します。そのため、next()関数で中身を順に取得できます。

enumerate(), zip()
keys = ["a", "b", "c", "d", "e"]
numbers = [11, 22, 33, 44, 55]

e = enumerate(numbers)
print(next(e))
print(next(e))

z = zip(keys, numbers)
print(next(z))
print(next(z))

以下のように、enumerate()はリストの位置とリストの中身のタプルを、zip()は2つのリストを順に結合したタプルを返します。

出力
$ python sample.py
(0, 11)    # enumerate()の出力
(1, 22)
('a', 11)  # zip()の出力
('b', 22)

これをforループに組み込むと、以下のようになります。i, number = (0, 11)のように、タプルをアンパックして2つの変数に代入しています。

for
keys = ["a", "b", "c", "d", "e"]
numbers = [11, 22, 33, 44, 55]

for i, number in enumerate(numbers):
    print(f"i={i}, n={number}")

for key, number in zip(keys, numbers):
    print(f"k={key}, n={number}")

以下のように、リストの位置を取得したり、2つのリストを組み合わせたりできます。

出力
$ python sample.py
i=0, n=11
i=1, n=22
i=2, n=33
i=3, n=44
i=4, n=55
k=a, n=11
k=b, n=22
k=c, n=33
k=d, n=44
k=e, n=55

まとめ

今回はPythonの繰り返し処理の流れをStep-by-stepで見ていくことで、forループの仕組みを深く理解することができました。私もPython初心者の頃はforループの仕組みを理解するのに苦労しましたが、forループにはPython特有の仕組みが多く含まれていることを改めて実感しました。

Pythonをお手軽に使うだけなら、ここまで詳細に知る必要はないと思います。しかし、Pythonのforループをイマイチ覚えられないという方は、この記事を参考にしていただけると嬉しいです!

参考文献

https://qiita.com/tomotaka_ito/items/35f3eb108f587022fa09

GitHubで編集を提案

Discussion