いまさら聞けない!Pythonのforループの仕組みをStep-by-stepで理解する
はじめに
Pythonのforループって、なんだか難しくないですか??🤔
ナゾのrange()
関数が登場したり、リストを与えたらそのまま値を取得できたりと、はじめて出会う考え方も多いですよね。自分も初心者の頃は、これらの挙動を理解するのに苦労しました。実はこれまでも、なんとなくの理解だったりします。
そこで今回は、Pythonの繰り返し処理の仕組みをStep-by-stepで読み解くことで、forループで何が行われているかをしっかりと把握することを目指します。「forループは書けるけど、その仕組みはよくわかっていない」という方にオススメです。
例題
今回は、整数のリストを入力として、リストの中身を先頭から順に出力する問題を対象とします。
numbers = [11, 22, 33, 44, 55]
11
22
33
44
55
この出力は、以下のようなコードで実現できます。入門書などでは、2通りの書き方で説明されていることが多いですね。
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ループの実行の仕組み。
繰り返し処理のアルゴリズム
はじめに、繰り返し処理のアルゴリズム(手順や計算方法)を考えてみます。今回のケースの場合、以下の図のように、リストの中身を順に取得することで実現できますね。
繰り返し処理のアルゴリズム
この処理を実装するために、以下のような仕組みが必要だとわかります。
- リストの次の要素を取得する。
- 次の要素がなくなったらエラー(例外)を出す。
- 要素を取得できていたら、繰り返し処理を続ける。
今回は、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()
の処理を繰り返します。
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 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()
の中身を順に取得してみます。
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ループの例を見てみます。変数i
に0,1,2,3,4
の値を順に代入し、その変数を利用してnumbers
リストの中身を出力しています。
# range()を利用する方法
for i in range(len(numbers)):
print(numbers[i])
ここで、通常のforループの書き方と、range()
を使う場合のどちらを使うべきかを考えてみます。通常の書き方では、リストの中身を直接取得しています。range()
を使う場合は、リストの長さを取得し、range型のオブジェクトを生成し、添字を介してリストにアクセスしています。
そのため、今回のようなケースでは通常の書き方が簡潔です。ただし、操作対象のデータの中身が非常に大きい場合などは、range()
を利用すると効率的な場合があります。
enumerate()とzip()を利用したforループ
ここまでの内容を理解しておくと、入門書などでforループとセットで説明されるenumerate()
とzip()
の仕組みも理解しやすいです。これらの関数もイテレーターを生成します。そのため、next()
関数で中身を順に取得できます。
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つの変数に代入しています。
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ループをイマイチ覚えられないという方は、この記事を参考にしていただけると嬉しいです!
参考文献
Discussion