Python

【Python】イテラブルとイテレータについて解説

この記事では、Python のイテラブルとイテレータについて解説します。

結論から言ってしまうとイテラブルとイテレータは以下のようなオブジェクトです。

  • イテラブル = 「イテレータを生成できるオブジェクト」
  • イテレータ = 「要素を1つずつ取り出せるオブジェクト」

それでは、もう少し詳しく見ていきましょう❗️

イテラブルとは?

イテラブルとは、for文 で要素を1つずつ取り出せるような反復可能なオブジェクトのことを言います。「イテラブルオブジェクト」とも呼ばれたりもします。

Python には以下のようなイテラブルが用意されています。

  • シーケンス型(リスト、タプル、range)
  • マッピング型(辞書)
  • テキストシーケンス型(文字列)
  • バイナリシーケンス型(bytes、bytearray、memoryview)

試しに for文 で要素を取り出せるか検証してみます。

ls = [1, 2, 3]
ds = {'a': 1, 'b': 2, 'c': 3}
s = 'abc'
bs = b'abc'

for l, d, c, b in zip(ls, ds, s, bs):
    print(f'リスト: {l}, 辞書: {d}, 文字列: {c}, バイト: {b}')

実行結果

リスト: 1, 辞書: a, 文字列: a, バイト: 97
リスト: 2, 辞書: b, 文字列: b, バイト: 98
リスト: 3, 辞書: c, 文字列: c, バイト: 99

全てのオブジェクトで要素が取り出せたのでイテラブルであることが確認できました!

繰り返し処理の仕組み

for文 が繰り返し処理する流れを見てみましょう!

  1. for文が指定されたイテラブルからイテレータオブジェクトを生成
  2. 反復処理されるたびにイテレータの__next__メソッドを呼び出す
  3. __next__メソッドは順番に沿って要素を1つずつ返す
  4. 次の要素がなくなり、StopIteration例外を送出するとループが終了する

このような手順で繰り返し処理を行なっています。つまり、イテラブルとは「イテレータオブジェクトを生成できるオブジェクトのこと」を言います。

イテラブルの条件

任意のクラスにイテラブルとしての振る舞いを追加するには、__iter__()メソッド か __getitem()__メソッド を定義する必要があります。

__iter__()メソッド

このメソッドは、イテレータオブジェクトを返します。詳しくは後述しますが、イテレータとはデータを1つずつ取得できる型のことを言います。

__getitem__()メソッド

オブジェクト[key]で要素を取得できるようにする特殊メソッドです。ただし、key0から開始するシーケンスでなければならない。

この2つのメソッドのどちらかが定義されていれば、そのオブジェクトからイテレータオブジェクトを生成することができます。つまり、イテラブルとなります。

イテレータオブジェクトを生成

イテレータオブジェクトを生成するには、iter()関数を使います。

l = [1, 2, 3]

# iter関数は__iter__メソッド呼び出す
print(iter(l))

実行結果

<list_iterator object at 0x7f82e4a2ae50>

サンプルとして適当なクラスに__getitem__()メソッドを定義し、イテレータオブジェクトを生成できるか試してみましょう!

class MyIterable:

    def __init__(self, x, y, z):
        self._x = x
        self._y = y
        self._z = z

    def __getitem__(self, item):
        return (self._x, self._y, self._z)[item]


# インスタンス化
mi = MyIterable(1, 2, 3)

# イテレータオブジェクト生成
iterator = iter(mi)
print(iterator)

実行結果

<iterator object at 0x7fa5da234f10>

ユーザー定義クラスからイテレータオブジェクトを生成することができました。

イテレータとは?

イテレータとは、型の1つで順番に要素を取得できるオブジェクトのことを言います。

具体的には __next__()メソッド が呼ばれるたびに要素を順番に返すオブジェクトのことです。そして、返せる要素がなくなった時に StopIteration例外 を送出します。

基本的にそれだけのオブジェクトです。

試しに既存のイテラブルから iter()関数 を使ってイテレータオブジェクトを生成し、要素を取得して挙動の違いを見てみましょう!

for文 を使って要素を取得してみますが、特に変化はありません。

l = [1, 2, 3]

iterator = iter(l)

for i in iterator:
    print(i)

実行結果

1
2
3

for文 は、渡されたイテラブルからイテレータを生成してループ処理しているので、初めからイテレータを渡したところで何ら変化はありません。

イテレータを使う上で注意しなければならないのは 2度目の呼び出しです。イテレータは基本的に使い切りのオブジェクトなので 1度要素を取り出してしまうと次からは取り出せなくなってしまいます。

l = [1, 2, 3]
iterator = iter(l)

print('1度目の呼び出し')
for i in iterator:
    print(i)

print('2度目の呼び出し')
for i in iterator:
    print(i)

実行結果

1度目の呼び出し
1
2
3
2度目の呼び出し

これは list()などで変換した場合も同様なので注意。

l = [1, 2, 3]
iterator = iter(l)

# リストに変換
list(iterator)

print('1度目の呼び出し')
for i in iterator:
    print(i)

実行結果

1度目の呼び出し

リストなどが何度も for文 などで処理できていたのは、その都度イテレータを生成していたから ということがよくわかりますね。

また、イテレータオブジェクトは next()関数 を使って次の要素を取得できます。

l = [1, 2, 3]

iterator = iter(l)

print(next(iterator))
print(next(iterator))
print(next(iterator))
print(next(iterator))

実行結果

1
2
3
Traceback (most recent call last):
  File "main.py", line 8, in <module>
    print(next(iterator))
StopIteration

イテレータプロトコル

__iter__()メソッド と __next__()メソッド を合わせて「イテレータプロトコル」と呼びます。

イテレータプロトコルを任意のクラスに実装することで、そのクラスがどのように反復処理するかを定義することができます。

では、この2つのメソッドがどのようなものなのか見ていきましょう!

__iter__()メソッド

__iter__()メソッド は、__next__()メソッド が定義されているオブジェクトを返すメソッドです。

def __iter__(self):
    return __next__メソッドが定義されているオブジェクト

自身に __next__()メソッド が定義されている場合 self を返すこともできる。

def __iter__(self):
    return self

__next__()メソッド

__next__()メソッド は、要素をどのように返すかを定義するメソッドです。呼び出されるたびに要素を返します。返す要素がなければ StopIteration例外 を送出します。

def __next__(self):
    # 要素を返す
    # 返す要素がなければStopIteration例外を送出

イテレータプロトコルを実装してみる

以下のコードは、コンストラクタで受け取った不特定多数の要素を for文 でループ処理できるようにしました。

class MyIterator:

    def __init__(self, *args):
        self._args = args
        self._index = 0

    # __next__メソッドを定義しているオブジェクトを返す
    def __iter__(self):
        print('__iter__')
        return self

    # 呼び出されるたびに新しい要素を返す
    # 最後にStopIterationを返す
    def __next__(self):
        print('__next__')
        if self._index == len(self._args):
            print('StopIteration')
            raise StopIteration()
        arg = self._args[self._index]
        self._index += 1
        return arg


myiter = MyIterator(1, 2, 3)

for arg in myiter:
    print(arg)

実行結果

__iter__
__next__
1
__next__
2
__next__
3
__next__
StopIteration

簡単にメソッドの内容を見てみましょう!

__iter__メソッド

自身に __next__()メソッド を定義したので、__iter__メソッド では self を返しました。

def __iter__(self): 
    print('__iter__') 
    return self

__next__メソッド

呼び出されるたびに _index変数 をインクリメントし、毎回新たな要素を返します。_index変数 と _args変数 の長さが同じになったら返す要素がないので StopIteration例外 を送出します。

def __next__(self):
    print('__next__')
    if self._index == len(self._args):
        print('StopIteration')
        raise StopIteration()
    arg = self._args[self._index]
    self._index += 1
    return arg

イテレータプロトコルをバラバラに実装する

__iter__()メソッド と __next__()メソッド は同じクラスに定義する必要はありません。先ほどの MyIterationクラス のイテレータプロトコルをバラバラに実装してみます。

# __iter__メソッドを定義したクラス
class MyIter:

    def __init__(self, *args):
        self._args = args

    def __iter__(self):
        print('__iter__')
        # MyNextを呼び出す
        return MyNext(*self._args)


# __next__メソッドを定義したクラス
class MyNext:

    def __init__(self, *args):
        self._args = args
        self._index = 0
    
    def __next__(self):
        print('__next__')
        if self._index == len(self._args):
            print('StopIteration')
            raise StopIteration()
        arg = self._args[self._index]
        self._index += 1
        return arg

myiter = MyIter(1, 2, 3)

for arg in myiter:
    print(arg)

実行結果

__iter__
__next__
1
__next__
2
__next__
3
__next__
StopIteration

他のクラスでも同じ __next__()メソッド を使いたい場合は、そのためのクラスを作成すると同じような実装を何度もしなくて済みます。

複雑になる場合も外部に実装することでクラスをすっきりさせることができます。

iter関数を使う

iter()関数 は、反復プロトコル(__iter__()メソッドを定義しているオブジェクト)かシーケンスプロトコル(__getitem__()メソッドを定義しているオブジェクト)からイテレータオブジェクトを返します。

iter(オブジェクト)

サンプル

以下のコードでは、iter()関数 を使って可変長引数(タプル)のイテレータオブジェクトを自身の __iter__()メソッド で返しています。

class MyIter:
    def __init__(self, *args):
        self.__args = args

    def __iter__(self):
        return iter(self.__args)


myiter = MyIter(1, 2, 3)
for arg in myiter:
    print(arg)

実行結果

1
2
3

これを少し応用することで以下のようにインスタンス変数をまとめて for文 で取得できるようなクラスを作成できます。

class Person:
    def __init__(self, name, age, gender):
        self.name = name
        self.age = age
        self.gender = gender

def __iter__(self):
    # インスタンス変数をタプルにまとめて、イテレータオブジェクトとして返す
    return iter((self.name, self.age, self.gender))


p = Person('田中太郎', '20', '男')

for arg in p:
    print(arg)

実行結果

田中太郎
20
男

まとめ

この記事では、Python のイテラブルとイテレータについて解説しました。

これらのことを覚えておけばイテレータを実装することはとても容易です。(実装する機会は少ないですが...)

それでは今回の内容はここまでです。ではまたどこかで〜( ・∀・)ノ