35. 反復処理の仕組み: イテラブルとイテレータ
この本を通して、あなたは for ループを使って、リスト(list)、文字列、辞書(dictionary)、そしてほかのコレクションを反復処理してきました。for item in my_list: のようなコードを、数え切れないほど書いてきたはずです。ですが、Python が for ループを実行するとき、裏側では実際に何が起きているのでしょうか?Python はどのようにして、さまざまな種類のコレクションを順にたどる方法を知っているのでしょうか?
この章では、for ループが動く仕組みである Python の 反復プロトコル(iteration protocol) を見ていきます。イテラブル(iterable)(ループできるオブジェクト)と、イテレータ(iterator)(値を実際に順に取り出していくオブジェクト)について学びます。この違いを理解すると、Python の動作への理解が深まり、第36章でジェネレータ(generator)を扱う準備ができます。
35.1) オブジェクトがイテラブルであるとはどういうことか
35.1.1) イテラビリティの概念
イテラブル(iterable) とは、for ループで反復できる任意の Python オブジェクトのことです。「反復できる」というのは、Python がそのオブジェクトからアイテムを一つずつ、順番に取り出せるという意味です。
あなたはすでに多くのイテラブルを扱ってきました:
# リストはイテラブルです
numbers = [1, 2, 3, 4, 5]
for num in numbers:
print(num) # Output: 1, 2, 3, 4, 5 (on separate lines)
# 文字列はイテラブルです
text = "Python"
for char in text:
print(char) # Output: P, y, t, h, o, n (on separate lines)
# 辞書はイテラブルです(デフォルトではキーを反復します)
student = {"name": "Alice", "age": 20, "grade": "A"}
for key in student:
print(key) # Output: name, age, grade (on separate lines)これらのオブジェクト(リスト、文字列、辞書、タプル(tuple)、セット(set)、range、ファイル(file))はすべて、Python の 反復プロトコル(iteration protocol)(Python がそれらを反復できるようにするためのルールの集合)をサポートしているため、イテラブルです。
35.1.2) オブジェクトをイテラブルにするもの
オブジェクトがイテラブルであるためには、__iter__() という特別なメソッドを実装している必要があります。このメソッドは イテレータ(iterator) オブジェクトを返します。まだ詳細は気にしなくて大丈夫です—次のセクションでイテレータを見ていきます。
組み込みの iter() 関数を使ってそのオブジェクトからイテレータを取得できるか試すことで、オブジェクトがイテラブルかどうかを確認できます:
# オブジェクトがイテラブルかどうかをテストする
numbers = [1, 2, 3]
iterator = iter(numbers) # 動作します - リストはイテラブルです
print(type(iterator)) # Output: <class 'list_iterator'>
text = "Hello"
iterator = iter(text) # 動作します - 文字列はイテラブルです
print(type(iterator)) # Output: <class 'str_iterator'>
# イテラブルではないオブジェクトで試す
value = 42
try:
iterator = iter(value) # 失敗します - 整数はイテラブルではありません
except TypeError as e:
print(f"Error: {e}") # Output: Error: 'int' object is not iterableイテラブルなオブジェクトに対して iter() を呼び出すと、Python はそのオブジェクトの __iter__() メソッドを呼び出してイテレータを返します。オブジェクトがこのメソッドを持っていない場合、TypeError が発生します。
35.1.3) イテラブルとシーケンス
すべてのイテラブルがシーケンス(sequence)であるわけではないことを理解するのが重要です。シーケンスは、インデックスアクセスをサポートし、順序が定義されている特定の種類のイテラブルです。
# シーケンスはインデックスアクセスをサポートします
my_list = [10, 20, 30]
print(my_list[0]) # Output: 10
my_string = "Python"
print(my_string[2]) # Output: t
# セットはイテラブルですが、シーケンスではありません(インデックスなし、順序の保証なし)
my_set = {1, 2, 3}
for item in my_set:
print(item) # 動作します - セットはイテラブルです
# しかしインデックスアクセスはできません
try:
print(my_set[0]) # 失敗します - セットはインデックスアクセスをサポートしません
except TypeError as e:
print(f"Error: {e}") # Output: Error: 'set' object is not subscriptable重要な違い: すべてのシーケンス(リスト、タプル、文字列、range)はイテラブルですが、すべてのイテラブルがシーケンスではありません。セットと辞書はイテラブルですが、インデックスアクセスをサポートしないため、シーケンスではありません。
35.1.4) イテラビリティが重要な理由
イテラビリティを理解すると、次のことに役立ちます:
- 何をループできるかが分かる: どんなイテラブルでも
forループで動きます - エラーメッセージを理解できる: 「object is not iterable」は、
forループで使えないことを意味します - 内包表記を使える: リスト、セット、辞書の内包表記は、どんなイテラブルでも動きます
- 組み込み関数を使える:
sum()、max()、min()、sorted()など多くの組み込み関数は、どんなイテラブルでも受け取れます
# これらはすべて、イテラブルを受け取れるので動作します
numbers = [1, 2, 3, 4, 5]
print(sum(numbers)) # Output: 15
text = "Python"
print(max(text)) # Output: y (highest alphabetically)
# セットでも動作します
unique_values = {10, 5, 20, 15}
print(sorted(unique_values)) # Output: [5, 10, 15, 20]35.2) Python における身近なイテレータ(ファイル、range、辞書など)
35.2.1) イテレータとは
イテレータ(iterator) は、データのストリーム(stream)を表すオブジェクトです。次の要素を求めるたびに、1つずつ値を返します。イテレータがすべての値を返し終えると、使い尽くされて(exhausted)再利用できません。
イテレータは、本のしおりのようなものだと考えてください:
- シーケンスのどこまで進んだかを覚えています
- 次の要素を要求できます
- 末尾まで到達すると、新しいイテレータを作らない限り戻れません
イテラブルとイテレータの主な違いは次のとおりです:
- イテラブル(iterable) は、反復 できる もの(例: リスト)
- イテレータ(iterator) は、反復を 実際に行う もの(リストを順にたどる仕組み)
# リストはイテラブルです
numbers = [1, 2, 3]
# イテラブルからイテレータを取得する
iterator = iter(numbers)
# イテレータは別のオブジェクトです
print(type(numbers)) # Output: <class 'list'>
print(type(iterator)) # Output: <class 'list_iterator'>35.2.2) for ループにおけるイテレータ
for ループを書くと、Python は裏側で自動的にイテレータを作ります:
numbers = [10, 20, 30]
# あなたが書くもの:
for num in numbers:
print(num)
# Python が内部で行うこと(概念的に):
# 1. iter(numbers) を呼び出してイテレータを取得する
# 2. イテレータに対して next() を繰り返し呼び出す
# 3. イテレータが StopIteration を送出したら停止するこれを明示的に書くと次のようになります:
numbers = [10, 20, 30]
# 手動で反復する(for が自動的に行うこと)
iterator = iter(numbers)
try:
print(next(iterator)) # Output: 10
print(next(iterator)) # Output: 20
print(next(iterator)) # Output: 30
print(next(iterator)) # Would raise StopIteration
except StopIteration:
print("No more items") # Output: No more itemsfor ループは StopIteration 例外を自動的に処理するので、通常のコードでは目にすることがありません。
35.2.3) イテレータとしてのファイルオブジェクト
ファイルオブジェクトは、イテレータの優れた例です。ファイルを反復処理すると、1行ずつ読み込みます:
# サンプルファイルを作成する
with open("students.txt", "w") as file:
file.write("Alice\n")
file.write("Bob\n")
file.write("Charlie\n")
# ファイルを1行ずつ読み込む
with open("students.txt", "r") as file:
for line in file:
print(line.strip()) # Output: Alice, Bob, Charlie (on separate lines)ファイルオブジェクトは、イテラブルでありイテレータでもあります。ファイルに対して iter() を呼ぶと、自分自身を返します:
with open("students.txt", "r") as file:
iterator = iter(file)
print(file is iterator) # Output: True (same object)
# 行を手動で読み込む
print(next(iterator)) # Output: Alice
print(next(iterator)) # Output: Bob
print(next(iterator)) # Output: Charlieこれはメモリ効率が良い方法です。Python はファイル全体をメモリに読み込まず、要求に応じて1行ずつ読み込みます。
35.2.4) イテレータとしての range オブジェクト
range オブジェクトは、必要に応じて数値を生成するイテラブルです:
# range はイテラブルです
numbers = range(1, 4)
print(type(numbers)) # Output: <class 'range'>
# range からイテレータを取得する
iterator = iter(numbers)
print(type(iterator)) # Output: <class 'range_iterator'>
# イテレータを使う
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3range はすべての数値をメモリに保持しないため、メモリ効率が良いです。必要になったときに各数値を計算します:
# この range は 100万個の数値を表しますが、最小限のメモリしか使いません
large_range = range(1000000)
print(type(large_range)) # Output: <class 'range'>
# イテレータを取得する
iterator = iter(large_range)
print(next(iterator)) # Output: 0
print(next(iterator)) # Output: 1
# ... 100万個の値まで続けられます35.2.5) 辞書イテレータ
辞書(dictionary)は、キー、値、アイテム(キーと値のペア)に対して異なるイテレータを提供します:
student = {"name": "Alice", "age": 20, "grade": "A"}
# キーを反復する(デフォルト)
for key in student:
print(key) # Output: name, age, grade (on separate lines)
# 明示的に keys イテレータを取得する
keys_iterator = iter(student.keys())
print(next(keys_iterator)) # Output: name
print(next(keys_iterator)) # Output: age
# 値を反復する
values_iterator = iter(student.values())
print(next(values_iterator)) # Output: Alice
print(next(values_iterator)) # Output: 20
# items(キーと値のペア)を反復する
items_iterator = iter(student.items())
print(next(items_iterator)) # Output: ('name', 'Alice')
print(next(items_iterator)) # Output: ('age', 20)35.2.6) イテレータは使い切りである
イテレータ(iterator)の重要な性質として、一度しか使えません。使い切るとリセットされません:
numbers = [1, 2, 3]
iterator = iter(numbers)
# イテレータを最初にたどる
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3
# イテレータはすでに使い切られています
try:
print(next(iterator)) # Raises StopIteration
except StopIteration:
print("Iterator exhausted") # Output: Iterator exhausted
# もう一度反復するには、新しいイテレータを作る
iterator = iter(numbers)
print(next(iterator)) # Output: 1 (fresh start)これは、イテラブル自身とは異なります。イテラブルは何度でも反復できます:
numbers = [1, 2, 3]
# 1回目の反復
for num in numbers:
print(num) # Output: 1, 2, 3
# 2回目の反復(問題なく動きます - 新しいイテレータが作られます)
for num in numbers:
print(num) # Output: 1, 2, 335.3) iter() と next() を使ってイテラブルを順に進める
35.3.1) iter() 関数
iter() 関数はイテラブル(iterable)を受け取り、イテレータ(iterator)を返します。これが反復プロトコル(iteration protocol)の最初のステップです:
# さまざまなイテラブルからイテレータを作る
numbers = [10, 20, 30]
iterator = iter(numbers)
print(type(iterator)) # Output: <class 'list_iterator'>
text = "Hi"
text_iterator = iter(text)
print(type(text_iterator)) # Output: <class 'str_iterator'>
my_set = {1, 2, 3}
set_iterator = iter(my_set)
print(type(set_iterator)) # Output: <class 'set_iterator'>イテラブルの種類ごとに専用のイテレータ型が返りますが、どれも使い方は同じです—next() を呼び出して次の値を取得します。
35.3.2) next() 関数
next() 関数は、イテレータ(iterator)から次のアイテムを取得します。もうアイテムがなければ StopIteration を送出します:
colors = ["red", "green", "blue"]
iterator = iter(colors)
# 1つずつアイテムを取得する
print(next(iterator)) # Output: red
print(next(iterator)) # Output: green
print(next(iterator)) # Output: blue
# もうアイテムがない
try:
print(next(iterator)) # Raises StopIteration
except StopIteration:
print("No more colors") # Output: No more colors35.3.3) next() にデフォルト値を指定する
next() の第2引数としてデフォルト値を渡せます。イテレータが使い切られているときに StopIteration 例外を送出する代わりに、next() は指定したデフォルト値を返します:
numbers = [1, 2, 3]
iterator = iter(numbers)
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2
print(next(iterator)) # Output: 3
print(next(iterator, "Done")) # Output: Done (default value, no exception)
print(next(iterator, "Done")) # Output: Done (still exhausted)これは、例外処理なしで反復の終わりを丁寧に扱いたいときに便利です:
35.4) __iter__ と __next__ を使ったカスタムイテレータの作成
35.4.1) カスタムイテレータを作る理由
Python の組み込みイテラブル(リスト、文字列、ファイル)は、ほとんどの一般的なケースをカバーしています。しかし、特別な振る舞いのために独自のイテラブルオブジェクトを作る必要があることもあります:
- カスタムのロジックでシーケンスを生成する
- 自分で設計したデータ構造を反復処理する
- 大規模データセットをメモリ効率よく反復処理する
- 遅延評価(lazy evaluation)(必要になったときだけ値を計算する)を実装する
カスタムイテレータを作るには、2つの特別なメソッド __iter__() と __next__() を実装する必要があります。
35.4.2) イテレータプロトコル
オブジェクトをイテレータ(iterator)にするには、次を実装する必要があります:
__iter__(): イテレータオブジェクト自身(通常はself)を返す__next__(): シーケンスの次の値を返す。終わったらStopIterationを送出する
class SimpleCounter:
"""An iterator that counts from start to end."""
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
"""Return the iterator object (self)."""
return self
def __next__(self):
"""Return the next value or raise StopIteration."""
if self.current > self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# カスタムイテレータを使う
counter = SimpleCounter(1, 5)
for num in counter:
print(num)
# Output: 1
# Output: 2
# Output: 3
# Output: 4
# Output: 5何が起きているか分解してみましょう:
forループがiter(counter)を呼び出し、counter.__iter__()を呼び出してcounter自身を受け取ります- ループは
next(counter)を繰り返し呼び出し、これはcounter.__next__()を呼び出します __next__()の各呼び出しが次の数値を返し、currentをインクリメントしますcurrent > endになると、__next__()がStopIterationを送出し、ループが止まります
35.4.3) カスタムイテレータを手動で使う
カスタムイテレータは iter() と next() を使って手動で使うこともできます:
counter = SimpleCounter(10, 13)
# イテレータを取得する(自分自身を返します)
iterator = iter(counter)
print(iterator is counter) # Output: True
# 値を手動で取得する
print(next(iterator)) # Output: 10
print(next(iterator)) # Output: 11
print(next(iterator)) # Output: 12
print(next(iterator)) # Output: 13
# いま使い切られました
try:
print(next(iterator))
except StopIteration:
print("Counter exhausted") # Output: Counter exhausted35.4.4) イテレータは使い切りである(再確認)
イテレータ(iterator)は一度しか使えないことを思い出してください:
counter = SimpleCounter(1, 3)
# 1回目の反復
for num in counter:
print(num) # Output: 1, 2, 3
# 2回目の反復(動きません - イテレータは使い切られています)
for num in counter:
print(num) # Nothing printed - iterator is already exhaustedもう一度反復するには、新しいインスタンスを作る必要があります:
# 反復のたびに新しいカウンタを作る
for num in SimpleCounter(1, 3):
print(num) # Output: 1, 2, 3
for num in SimpleCounter(1, 3):
print(num) # Output: 1, 2, 3 (new iterator)35.4.5) (イテレータだけでなく)イテラブルクラスを作る
多くの場合、クラス自体はイテラブルで、反復のたびに新しいイテレータを作るようにしたいはずです。これを行うには、イテラブルとイテレータを分けます:
class CounterIterable:
"""An iterable that creates fresh counter iterators."""
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
"""Return a new iterator each time."""
return CounterIterator(self.start, self.end)
class CounterIterator:
"""The actual iterator that does the counting."""
def __init__(self, start, end):
self.current = start
self.end = end
def __iter__(self):
return self
def __next__(self):
if self.current > self.end:
raise StopIteration
value = self.current
self.current += 1
return value
# これで何度でも反復できます
counter = CounterIterable(1, 3)
# 1回目の反復
for num in counter:
print(num) # Output: 1, 2, 3
# 2回目の反復(__iter__ が新しいイテレータを作るので動きます)
for num in counter:
print(num) # Output: 1, 2, 3このパターンは関心事を分離します:
CounterIterableはイテラブルで、イテレータを作る方法を知っていますCounterIteratorはイテレータで、値をどのように順に取り出すかを知っています
35.4.6) 実用例: カスタムデータ構造を反復する
カスタムデータ構造のためのイテレータを作りましょう—シンプルなプレイリストです:
class Playlist:
"""A music playlist that can be iterated over."""
def __init__(self):
self.songs = []
def add_song(self, title, artist):
"""Add a song to the playlist."""
self.songs.append({"title": title, "artist": artist})
def __iter__(self):
"""Return an iterator for the playlist."""
return PlaylistIterator(self.songs)
class PlaylistIterator:
"""Iterator for stepping through songs in a playlist."""
def __init__(self, songs):
self.songs = songs
self.index = 0
def __iter__(self):
return self
def __next__(self):
if self.index >= len(self.songs):
raise StopIteration
song = self.songs[self.index]
self.index += 1
return song
# プレイリストを使う
playlist = Playlist()
playlist.add_song("Imagine", "John Lennon")
playlist.add_song("Bohemian Rhapsody", "Queen")
playlist.add_song("Hotel California", "Eagles")
# 曲を反復する
print("Now playing:")
for song in playlist:
print(f" {song['title']} by {song['artist']}")
# Output: Now playing:
# Output: Imagine by John Lennon
# Output: Bohemian Rhapsody by Queen
# Output: Hotel California by Eagles
# もう一度反復できます(新しいイテレータが作られます)
print("\nReplay:")
for song in playlist:
print(f" {song['title']}")
# Output: Replay:
# Output: Imagine
# Output: Bohemian Rhapsody
# Output: Hotel California35.4.7) カスタムイテレータを使うべきとき
次の場合にカスタムイテレータを作成してください:
- 遅延評価が必要: すべてを保存するのではなく、必要に応じて値を生成する
- カスタムデータ構造がある:
forループで使えるように、反復可能にする - 特別な反復ロジックが必要: アイテムをスキップする、値を変換する、複雑なステップを実装する
- メモリ効率が重要: 大きなシーケンスを保存せずに生成する
ただし、第36章では yield キーワードを使って、より簡単にイテレータを作る方法として ジェネレータ(generator) を学びます。ジェネレータはたいてい、__iter__() と __next__() を手動で実装するよりも、より簡潔で理解しやすいという理由でよく使われます。
カスタムイテレータを作る方法を理解すると、たとえ多くの場合は代わりにジェネレータを使うとしても、Python の反復プロトコル(iteration protocol)がどのように動作するかを理解する洞察が得られます。ここで学んだ概念—__iter__()、__next__()、そして StopIteration—は、次の章でジェネレータやその他の高度な反復テクニックを理解するための基礎です。