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例外が送出されます。

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

試しにイテレータオブジェクトから要素を取得してみましょう。

for文で使ってみる

特に変化はありません。

l = [1, 2, 3]

iterator = iter(l)

for i in iterator:
    print(i)

実行結果

1
2
3

next関数

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__メソッドを合わせてイテレータプロトコルと呼ばれます。

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

__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のイテラブルとイテレータについて解説しました。

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

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

最短3か月でエンジニア転職『DMM WEBCAMP COMMIT』