Python

【Python】イテレータとジェネレータを使って要素の反復処理を書いてみよう

イテレータとジェネレータを使って要素の反復処理を書いてみよう Python

前回の記事でPythonの非同期処理として、コルーチンや非同期処理のモジュール「asyncio」内の関数についての使い方などを説明しました。

今回も、前回同様少し応用的な内容になりますが「イテレータとジェネレータ」について説明したいと思います。

この記事を読んで分かること
  • Pythonでのイテレータとジェネレータについての内容がわかる
  • イテレータについてクラスの実装方法その使い方がわかる
  • ジェネレータについて関数の実装方法とその使い方がわかる
スポンサーリンク

イテレータとは

イテレータってなんでしょうか?

はい、そこからですね

Pythonには、多数の値を管理するためのデータがいろいろと用意されています。

例えばリスト、セット、タプル、辞書などを今まで使ってきました。

これらはfor文などで順番に値を取り出して処理することができます。

obj = ("one","two","three")
for val in obj:
    print(val)

これの実行結果は以下です。

one
two
three

この例ではタプルから順に値を取り出して表示しています。

このようにリストやタプルなどの値では、ただ多数の値を保管するだけでなく、forなどで「値を順番に取り出す」処理が行えます。

この場合、forとコンテナでこんなやりとりが行われています。

  1. for側で引数のコンテナに「次の値を下さい」と要求する
  2. コンテナ側は保管されている値から順に値を取り出しfor側に渡す
  3. コンテナ側で全ての値を渡し終えると「もうありません」という例外をfor側に渡す
  4. for側で例外を受け取るとfor文を終了する

このように、for側は「コンテナに1つずつ値を要求する」という仕組みになっており、コンテナ側では「順番に値を渡していく」という仕組みを備えています。

この両者が連携してforの繰り返しが機能しています。

このように、要求に応じて保管してある値を1つずつ順番に取り出していく機能を持ったクラスをイテレータと呼びます。

イテレータはPythonに限らず、プログラム全般でよく聞く処理方法です

ちなみに、Python標準で用意されているコンテナは基本的にイテレータとしての機能を備えています。

ポイント

イテレータは、次々に値を取り出して実行する仕組みを持ったクラス

スポンサーリンク

イテレータの構造

ではイテレータはクラス内でどのような仕組みになっているのでしょうか。

実は、あらかじめ用意されているメソッドを実装するだけで、自前のイテレータが作れます。

イテレータは以下のようにクラスとして定義します。

そしてイテレータとして利用できるクラスにするためには、2つのメソッドを用意しておく必要があります。

class クラス名 :
    def __iter__(self):

        return イテレータ

    def __next__(self):
        …..次の要素を用意して返す…..

それぞれ以下の役割を果たします。

イテレータを返すメソッドです。Pythonでは、イテレータを要求する時にはこのメソッドが呼び出されます。ここで、イテレータのインスタンスを返す処理を用意しておきます。作成しているクラスのインスタンスそのものをイテレータとして使えるようにするならば「return self」と書きます。

イテレータで次の要素を要求されたときに呼び出されます。ここで次の値をreturnするように処理を用意します

つまりイテレータクラスには「イテレータを返す」「次の処理を返す」という2つの処理が必要です

ポイント

イテレータは、クラスに「__iter__」メソッドと「__next__」メソッドを用意する

スポンサーリンク

イテレータクラスを作る

では実際に簡単なイテレータクラスを作って利用してみましょう。

必要最小限の機能だけを持った「MyIterator」クラスを作り、それを利用します。

class MyIterator:

    def __init__(self, *val):
        self._value = val
        self._count = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self._count < len(self._value):
            res = self._value[self._count]
            self._count += 1
            return res
        else:
            raise StopIteration()

obj = MyIterator("one","two","three","four","five","six","seven","eight","nine","ten")

for val in obj:
    print(val)

ここでは、18行目の「MyIterator」インスタンスを作成する際に引数に渡した値をそのまま保管し、for文で順番に取り出していることが分かります。

4行目で、初期化の「__init__」メソッドで可変長引数をプライベート変数「_value」に保管しています。

そして12行目の「__next__」メソッドで「_value」から順番に値を取り出してreturnしています。
また現在どこまで取り出したかが分かるように「_count」変数に取り出した位置情報を保管しています。

11行目で「_count」の値は「_value」の要素数未満かをチェックし「_value」の要素数と同じになったら「もう保管された値はない」ということで、15行目の処理で「StopIteration」という例外インスタンスを作成してraiseしています。

これはイテレータからこれ以上もう値が出せない場合に発生する例外で、この例外が発生するとfor側で繰り返し処理をやめて次の処理に進みます

実行結果は以下となります。

one
two
three
four
five
six
seven
eight
nine
ten
スポンサーリンク

ジェネレータとyield

イテレータは多数の値を保管するコンテナですが、Pythonでは「関数」を使っていくつもの値を取り出していくイテレータのような働きをするものを作ることが出来ます。

これを「ジェネレータ」と呼びます。

ジェネレータは、以下のような形で定義される関数です。

def 関数名(引数) :
    …..関数内の処理…..

    yield 値

ジェネレータの関数では「yield」という構文が使われます。

これは値を返すものですが、returnと違い、値を返してもそこで処理が終わりません
続きの処理があればさらに続けて実行していきます。

yieldで返された値は、その関数の呼び出し元に送られます
yieldは処理を中断せずに、何度もその関数から値を要求されると、必要に応じて次々と値を返していきます

ポイント

ジェネレータは、yieldで値を返した後もさらに処理を続けて実行できる。よってyieldするごとに値が取り出されていくことになる。

スポンサーリンク

ジェネレータを作成する

説明だけだと分かりにくいと思いますので、実際の利用例を見てみましょう。

整数の値を引数に2からその値までの素数を順に取り出すジェネレータ関数を作成して利用してみます。

def getPrime(max):
    for i in range(2,max+1):
        flg = True
        for j in range(2,i//2+1):
            if i % j == 0:
                flg = False
                break
        if flg:
            yield i

for n in getPrime(100):
    print(n,end=" ")

ここでは、1行目のgetPrime関数の中でforを使い繰り返し処理をしています。

2行目で2からその数まで順に割り算をして、割り切れる数がなければ素数と判断できます。

4行目から7行目までで、さらにforを使って2から調べる数字の半分の値まで(それ以上は調べても割り切れる数字はないので)繰り返し割り算の余りを調べて、割り切れたら変数flgをFalseに変更します。

そして8行目で全て繰り返したところでflgがTrueのままだったら割り切れなかった(=素数)と判断し9行目で「yield i」とします。

ここで「yield i」されると、処理が一時停止します。

そして11行目のforで次の値が要求されると、続きから処理を実行し、yieldで値を返したら一時停止
また11行目のforで次の値が要求されると、続きから処理を実行し、yieldで値を返したら一時停止….という処理を繰り返していくのです。

これを実行すると、100までの素数が以下のように出力されます。

2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 
スポンサーリンク

for以外で利用する

イテレータもジェネレータもforなどで次の値を要求される度に値を返す点は同じですので、両方とも同じ用途で使うことが出来ます。

このイテレータとジェネレータはfor以外ではどのように使うのでしょうか。

forの場合は自動的に「次の値を取り出す」という処理を実施してくれました。

ですが、for以外のところで利用する場合は、明示的に次の値を取り出す必要があります。

それには「next」関数を使います。

変数 = イテレータまたはジェネレータ
next(変数)

イテレータインスタンスを作成し、それを変数に代入して取り出します

イテレータはクラスなのでインスタンスを作って変数に代入するのは分かりますが、ジェネレータは少しややこしいです。

ジェネレータの戻り値

ジェネレータは関数です。
普通、関数の戻り値を変数に入れるというのは、単にreturnされた値が代入されるだけです。

しかしジェネレータの場合は少し違います

例えば先ほどのプログラムの「getPrime」関数から、以下のように実行してみるとどうなるでしょう。

変数 = getPrime(100)

普通に考えるとgetPrime関数の戻り値が変数に代入されます。

しかしジェネレータにはreturnによる戻り値はありません
yieldはありますが、これはreturnの戻り値とは働きが違います

ジェネレータの関数の戻り値は「generator」というジェネレータのインスタンスが作成されて返されます

そして、next関数を使うことで、このオブジェクトから値を取り出します

getPrimeをnextで取り出す

実際にforを使わずにジェネレータを利用してみましょう。

先ほどの「getPrime」関数を使って最初の3つの素数を出力してみます。

def getPrime(max):
    for i in range(2,max+1):
        flg = True
        for j in range(2,i//2+1):
            if i % j == 0:
                flg = False
                break
        if flg:
            yield i

fn = getPrime(100)
print("first:"+str(next(fn)))
print("second:"+str(next(fn)))
print("third:"+str(next(fn)))

11行目で「fn = getPrime(100)」と、変数fnにジェネレータのインスタンスを代入し、12行目〜14行目のnext関数で順番に値を取り出しています

このようにジェネレータのインスタンスを引数にしてnext関数を呼び出す度に次の値が取り出されていきます

これを実行すると、素数の最初の3つの値が出力されます。

first:2
second:3
third:5
スポンサーリンク

ジェネレータの特徴

最後にジェネレータの特徴について触れておきます。

ジェネレータを使えば、リストのようにfor文を回して要素を取得することが出来ますが、上記の説明でも分かるように「yield」を使って毎回値を取得して返しています

これがリストとの違いで、例えば100億個の数字から何番目の値を取得する場合に、リストだと最初から100億個の数字のリストを保持しなければならず、リストを作るだけでも膨大なメモリ量が必要となります。

その点ジェネレータを使えば、値は毎回必要な分だけ生成するので、例えば無限長のシーケンスを作る場合でもメモリを気にせず作成することが出来ます

なので膨大なデータから特定のいくつかの要素が必要な場合は、全データをリストで持つのではなくジェネレータを使って必要な分だけ生成する方がいいですね

ただしジェネレータの欠点としては、繰り返しが1巡だけに限られるという点です。

なので繰り返しを複数回行う必要があるのなら、その都度ジェネレータを作り直す必要があります。

ジェネレータって使い回しができないんだね

特にジェネレータはその都度値を生成するため、値の生成に処理時間がかかりますので、その場合はリストを使うなどデータ量や用途に合わせて使用を検討することをオススメします。

スポンサーリンク

まとめ

今回は、Pythonでのイテレータとジェネレータの実装方法とその利用方法について学びました。