Python PR

【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

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

繰り返し処理の仕組み

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度要素を取り出してしまうと次からは取り出せなくなってしまいます。(2度目はすでに返せる要素がないので最初からStopIteration例外を送出する)

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例外を送出

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

イテレータプロトコルを実装したクラスを定義してみます。以下のコードでは、コンストラクタで受け取った不特定多数の引数を__next__()で1つずつ取り出せるようにしています :

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(オブジェクト)
  • 反復プロトコル = __iter__()メソッドを定義しているオブジェクト
  • シーケンスプロトコル = __getitem__()メソッドを定義しているオブジェクト

以下のコードでは、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のイテラブルとイテレータについて解説しました。

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

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