Python でイテラブルとイテレータの使い分け
Python には イテラブル と イテレータ という概念があります。
この二つの概念は、その違いが書かれた記事が多くありますが、その使い分けについてはあまり書かれていないです。
そこで、この記事では「どういう時にイテラブルを使い、どういう時にイテレータを使うのか」について書いていきます。
本題は ここから です。
イテラブルとイテレータの違い
イテラブルとイテレータの違いについては、以下の記事が詳しいです。
この記事内で、イテラブルとイテレータの違いについては以下のように書かれています。
イテラブルとは、for文で要素を1つずつ取り出せるような反復可能なオブジェクトのことを言います[1]。
イテレータについては、
イテレータとは型の1つで、順番に要素を取得できるオブジェクトのことを言います[1:1]。
と書かれています。
公式ドキュメントにも、以下のように書かれています。
イテラブルは、
要素を一度に 1 つずつ返せるオブジェクトです。 (省略) 反復可能オブジェクトを組み込み関数 iter() の引数として渡すと、 オブジェクトに対するイテレータを返します[2]。
イテレータは、
データの流れを表現するオブジェクトです。イテレータの
__next__()
メソッドを繰り返し呼び出す (または組み込み関数next()
に渡す) と、流れの中の要素を一つずつ返します[2:1]。
つまり、イテラブルは for 文で要素を 1 つずつ取り出せるイテレータを返すオブジェクト であり、イテレータは 順番に要素を取得できるオブジェクト ということになります。
重要なのは、 イテラブルオブジェクトはイテレータを返すオブジェクト ということです。
渡すタイミングは、 iter()
に渡した時と、 for
文に渡した時の二つです。
実装的な違い
以上がイテラブルとイテレータの役割的な違いですが、これらをクラスに実装する時の違いもあります。
それは、実装しなければならない特殊メソッドです。(実際には、 __getitem__
でも代用可能ですがここでは省略します。)
クラス | 実装しなければならない特殊メソッド |
---|---|
イテラブル | __iter__ |
イテレータ |
__iter__ , __next__
|
__iter__
メソッドは、イテラブルとイテレータの両方に実装しなければならないメソッドで、 iter()
に渡した時と for
文に渡した時に呼ばれます。
やることはシンプルで、イテレータを返すだけです。
そのため、 __iter__
メソッドの返り値は、 return
で返す必要はありません。
yield
で返すジェネレータイテレータとして実装してもいいです[3]。
なんであれ、 for
文に渡せるオブジェクトを返せばいいので、リストを作って return
してもいいです。
class MyIterable:
def __iter__(self) -> list[int]:
return [1, 2, 3]
class MyYieldIterable:
def __iter__(self) -> Generator[int, None, None]:
yield 1
yield 2
yield 3
次に、イテレータに実装しなければならない __next__
メソッドです。
これは、イテレータを返すオブジェクトに対して next()
を呼び出した時に呼ばれます。
for
文に渡した時には呼ばれず、 for
文を 回した時に 呼ばれます。
__next__
メソッドは、次の要素を返す必要があります。
だから、 return
で返す必要があります。
class MyIterator:
def __init__(self):
self.i = 0
self.max = 3
def __iter__(self) -> Iterator[int]:
return self
def __next__(self) -> int:
self.i += 1
if self.i > self.max:
raise StopIteration
return self.i
ここで、イテレータには __iter__
メソッドを実装していますが、実はこれはこの MyIterator
クラスが イテラブルでもあること を示しています。
実装しなければならないと書きましたが、実際にはイテレータと呼ばないなら __iter__
メソッドを実装する必要はありません。
例えば、以下のように __next__
メソッドだけを実装しても、(書き方が冗長ですが)機能します。
class NIterator:
def __init__(self):
self.i = 0
self.max = 3
def __next__(self) -> int:
self.i += 1
if self.i > self.max:
raise StopIteration
return self.i
it = NIterator()
for i in range(3):
print(next(it))
また、 __iter__
と __next__
メソッドを別々のクラスに実装しても、機能します。
class OnlyIter:
def __iter__(self) -> Iterator[int]:
return OnlyNext()
class OnlyNext:
def __init__(self):
self.i = 0
self.max = 3
def __next__(self) -> int:
self.i += 1
if self.i > self.max:
raise StopIteration
return self.i
for i in OnlyIter():
print(i)
イテレータは、自身を返す __iter__
メソッドを実装しているイテラブルである ということを覚えておいてください。
使い分け
ここから本題です。
イテラブルとイテレータは、どちらも for
文で使えます。
next()
を明示的に用いるならイテレータを使うべきですが、多くの場合は for
文で使うことが多いと思います。
そもそもこの記事を書いた理由
この記事を書いた理由は、現在作成中の音楽分析ライブラリ Neuma で、「 for
文で一つずつ Note
オブジェクトを返すオブジェクト」を作っていたからです。
ざっくり、書くと以下のようなものです。
@dataclass(init=False, repr=False)
class Motif(NoteIterator):
motif: tuple[NoteIterator] = field(default_factory=tuple)
def __init__(self, *motif: NoteIterator) -> None:
self._check_iterable(motif)
self.motif = motif
def __iter__(self) -> Self:
# わざわざ深いコピーを作らなければならない
motif = [iter(melody) for melody in self.motif]
return Motif(*motif)
def __next__(self) -> NoteBase:
tmp: int = self._index
motif_index: int = 0
for i, m in enumerate(self.motif):
if tmp - m.length < 0:
break
motif_index = i
if self._index < self.length:
# 状態を一々参照し next() で次の要素を返す
next_note: NoteBase = next(self.motif[motif_index])
else:
self._index = 0 # 回し切ったらリセット
raise StopIteration
self._index += 1
return next_note
_index
変数は継承元の NoteIterator
クラスにあり、 NoteIterator
クラスは __iter__
と __next__
の両方を実装するよう強制するため、 collection.abc.Iterator
を継承しています。
しっかり動くオブジェクトを作れはしたのですが、問題がありました。
というのも、このオブジェクトの各インスタンスは、「何番目までの Note
オブジェクトを返したのか」を保持していたり、「ループが終わったらリセットする」ような機能を持っています。
後述しますが、これらは 一つずつ値を取得したい時だけ に使うものです。
これは最終的に for
文に渡されます。
for
文では、 __next__
メソッドが自動的に呼ばれるのに next()
を使っています。
__iter__
で yield from
を使えばよりシンプルに書ける気がしたのです。
作っていて、あまりにも読みにくいため、イテラブルとイテレータの使い分けについて考えてみたところ、イテラブルとして実装するのがシンプルになりうると思い至りました。
その記録のために、この記事を書きました。
結論から言うと、 イテラブルは for
文で使うデータ 、イテレータは 途中で止めるオブジェクト です。
これは、オブジェクトが持っている 変数の違い です。
for
文で一つずつ要素を取り出す時、オブジェクトは「何番目までの要素を返したのか」は興味がないことが多いと思います。
また、 for
文は必ずしも最後までループするとは限らないため、ループが終わったらリセットする必要があります。
最後までループするとしても、同じオブジェクトを再度 for
文に渡すとき、最初からループすることを期待されていることが多いと思います。
これらの機能は、ただデータを保存するだけのオブジェクトにとってはメモリの無駄です。
イテレータとして実装すると変に private 変数が増えてしまいますし、 継承するときにも面倒です。
リセットの処理やわざわざ StopIteration
を送出する必要があります。
だからこそ、基本的にはイテラブルを使うべきだと思います。
正確には イテラブルで事足りる ですね。
データを for
文で一つずつ取り出すだけなら、 __iter__
メソッドで、イテラブルなリストなどのデータ型を返すか、 yield
を使って一つずつ返すかすれば、 for
文の呼び出しのたびに 新しいイテレータ が作られるので、リセット処理は不要ですし、イテラブルなデータ型が StopIteration
を送出するか、 yield
が終わると自動的に StopIteration
が送出されるので、 StopIteration
を送出する必要もありません。
class GeneratorIterable:
def __iter__(self):
yield from [1, 2, 3]
for i in GeneratorIterable():
print(i)
一方イテレータを使うのは、 途中で止めたい時 です。
「ここまでは終わったけど、次はここからやり直したい」という時に使います。
この時、内部で「何番目までの要素を返したのか」を保持している必要があります。
他にも 無限に要素を返して欲しい時 にも使いえます。
無限に要素を返して欲しい時には for
文では回せません。
必要な分 next()
で呼び出す方が自然です。(max
の値どうやって決めんの問題もある。)
まとめ
結論としては、基本的にはイテラブルで事足りるけれど、途中で止めたい時や無限に要素を返して欲しい時にはイテレータを使うべきです。
特にイテレータはデータを「都度生成する」系のものに向いています。
そうでない限り、イテラブルの方がシンプルに住む場合が多いのではないでしょうか。
実務経験がないので、実際にどのような場面で使うのか想像がつききっていない面もあると思います。
「こういう実装で使った」など、例があれば教えていただけると嬉しいです。
以下、イテラブルとイテレータの実装例です。
class GIterable:
def __init__(self, data: list[int]):
self.data = data
def __iter__(self):
yield from self.data
class GIterator:
def __init__(self):
self.n = 0
def __iter__(self):
return self
def __next__(self):
self.n += 1
return self.n
iterable = GIterable([1, 2, 3, 4, 5])
for i in iterable:
print(i)
g = GIterator()
for i in range(10):
print(next(g))
for i in range(10):
print(next(g))
そもそもこの記事を書いた理由、の続き
この記事にあるようなことを踏まえ考えてみると、 今回のオブジェクトはイテレータである必要がないと言う結論に至りました。
結果、以下のようにシンプルな実装となりました。
@dataclass(init=False, repr=False)
class Motif(NoteIterator):
motif: tuple[NoteIterator] = field(default_factory=tuple)
def __init__(self, *motif: NoteIterator) -> None:
self._check_iterable(motif)
self.motif = motif
def __iter__(self) -> Self:
- motif = [iter(melody) for melody in self.motif]
- return Motif(*motif)
+ for m in self.motif:
+ yield from m
- def __next__(self) -> NoteBase:
- tmp: int = self._index
- motif_index: int = 0
- for i, m in enumerate(self.motif):
- if tmp - m.length < 0:
- break
- motif_index = i
- if self._index < self.length:
- next_note: NoteBase = next(self.motif[motif_index])
- else:
- self._index = 0
- raise StopIteration
- self._index += 1
- return next_note
__next__
メソッドを丸ごと削除し、 __iter__
メソッドを yield from
で実装しました。
tuple
の中にある NoteIterator
を yield from
で返しています。
この Motif
クラスとの差分から同じように Note
を返す Variation
というとんでもないクラスもあったのですが、これも __next__
メソッドを削除し、 __iter__
メソッドを yield from
で実装しました。
結果として、分岐だらけで読みにくかった30行程度のコードが5行まで減りました。
あまりにも低レベルな実装をしていたんだなと反省しています。
可読性のためにも、高レベルな実装を心がけたいと思います。
Discussion