🙌

Python でイテラブルとイテレータの使い分け

2023/08/13に公開

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 の中にある NoteIteratoryield from で返しています。
この Motif クラスとの差分から同じように Note を返す Variation というとんでもないクラスもあったのですが、これも __next__ メソッドを削除し、 __iter__ メソッドを yield from で実装しました。
結果として、分岐だらけで読みにくかった30行程度のコードが5行まで減りました。

あまりにも低レベルな実装をしていたんだなと反省しています。
可読性のためにも、高レベルな実装を心がけたいと思います。

脚注
  1. 【Python】イテラブルとイテレータについて解説します ↩︎ ↩︎

  2. 用語集 ↩︎ ↩︎

  3. Python のイテレータ生成クラスの使い方 ↩︎

Discussion