Address
304 North Cardinal St.
Dorchester Center, MA 02124

Work Hours
Monday to Friday: 7AM - 7PM
Weekend: 10AM - 5PM

Pythonのジェネレータについての解説

【Python入門】ジェネレータとは?for文との関係や、yieldとprint/returnとの違いは?

初心者に向けてわかりやすくPythonのジェネレータの基本を解説。ジェネレータイテレータや、yield文とprint/returnとの違いなども解説。

今回は、Pythonの勉強を始めた初心者・入門者にとってわかりにくい概念の1つ、ジェネレータ(ジェネレータ関数)を簡単な具体例のコードを用いてわかりやすく解説しようと思います。

今回の解説で以下の点について初歩のイメージを持ってもらえると思います。

  • ジェネレータのイメージ
  • ジェネレータとイテラブルとイテレータの関係
  • ジェネレータ関数とfor文との関係
  • yield文とprint関数との違い
  • yiled文とreturn文との違い
  • nextメソッドとnext関数の使い方

Pythonのジェネレータとは?その定義と要点

公式の定義

まず公式ドキュメントを見てみましょう。このようにう用語集には書かれています。

(ジェネレータ) generator iterator を返す関数です。 通常の関数に似ていますが、 yield 式を持つ点で異なります。 yield 式は、 for ループで使用できたり、next() 関数で値を 1 つずつ取り出したりできる、値の並びを生成するのに使用されます。

通常はジェネレータ関数を指しますが、文脈によっては ジェネレータイテレータ を指す場合があります。 意図された意味が明らかでない場合、 明瞭化のために完全な単語を使用します。

Python公式ドキュメント:用語集

初心者には全く意味がわかりませんね。

ひとまず要点として次の3点だけ意識しておきましょう。

要点

  • ジェネレータは関数の1種で、yield文を含む関数のこと。ジェネレータ関数とも言う
  • ジェネレータは「連続して何かをする場合に、1回ごとに処理を中断し、次の命令があるまで待機する」ような関数のイメージ
  • ジェネレータは実行されると、ジェネレータイテレータ(generator iterator)というオブジェクト(ジェネレータイテレータオブジェクト)を返す

それではこれら3点を意識しながら、次の説明を読んでいってください。

前提知識:イテラブルとイテレータ

まず前提知識としてイテラブルオブジェクトとイテレータオブジェクトについて簡単なイメージだけでも持っているとジェネレータの理解に役立ちます。

イテラブルとイテレータについては、「【初心者にもわかりやすく】Pythonのイテレータについて、その基本とfor文の仕組みを解説」でも解説しましたので、ぜひ読んでみてください。

両者の関係について大雑把にいえば、

  • イテレータはイテラブルオブジェクトの1種
  • __iter__メソッドと__next__メソッドがあればイテレータ

ということです。

前準備:辞書を作成

まず今回のジェネレータの解説のために、辞書を用意します。その辞書については前回の記事「Pythonで辞書の中に辞書を追加する方法と辞書のネスト化についての初心者向け解説」で作成した辞書をそのまま使います。

この記事で作った辞書は次のようなコードでした。
#新しい辞書の作成
student_dict={
    'id':'A0283456', 
    'name':'Tom', 
}

#新しい要素(キー:値)の追加
student_dict['scores'] = {
    'Mathematics': 53, 
    'Engish': 78,
    'Science': 36, 
}

#表示
student_dict

この辞書student_dictは、下表のキーと値がセットになったものです。

キー
id文字列としてのA0283456
name文字列としてのTom
scores辞書としての {‘Engish’: 78, ‘Mathematics’: 53, ‘Science’: 36}}

実行結果は、

{'id': 'A0283456', 'name':
'Tom', 'scores': {'Mathematics': 53, 'Engish': 78, 'Science': 36}}

となります。

ジェネレータを作る方法とジェネレータイテレータオブジェクト

関数内にyield文を書けばいいだけ

それではさっそくジェネレータを作ってみましょう。ジェネレータとは、上述のように、

  • ジェネレータは関数の1種で、yield文を含む関数のこと。ジェネレータ関数とも言う
  • ジェネレータは実行されると、ジェネレータイテレータ(generator iterator)を返す

でしたので、以下のようにします。

#ジェネレーターの作成
def gnrtr(obj):
    for i in obj:
        yield i

ジェネレータはあくまで関数の1種ですので、普通に関数を定義する方法で作成します。またyied文を含まなければなりません。

このyieldb文ですが、ひとまず今の時点では「一度処理を中断させて、『進め』の指示がでるまで待機」という文だとイメージしておいてください。

次にこのジェネレータを実行し、その実行結果を新しい変数(オブジェクト)に格納しましょう。

gen_itrtr=gnrtr(student_dict.items())  

このようにします。

辞書オブジェクトのitems()メソッドについても前回の記事「Pythonで辞書の中に辞書を追加する方法と辞書のネスト化についての初心者向け解説」に解説していますので、また読んでおいてください。

変数名の中のgenはgeneratorを、itrtrという部分はiteratorを示しています。

ジェネレータはジェネレータイテレータオブジェクトを作る

こうしてできあがったgen_itrtrは、「ジェネレータイテレータ」と呼ばれるオブジェクトとなります。

その型を調べてみましょう。

type(gen_itrtr)

実行すると、

<class 'generator'>

となります。まさにジェネレータですね。

ジェネレータ(ジェネレータ関数)からは、こうしたジェネレータイテレータという特殊なオブジェクトが作成されます。

それは、関数内の処理がどこまで進んで、どのような内容になっているのかという情報を記憶・保持している特殊なオブジェクトです。

上のほうで、

このyieldb文ですが、ひとまず今の時点では「1回情報を返したら、そこで一度処理を中断させて、『進め』の指示がでるまで待機」という文だとイメージしておいてください。

と書いたのはそういうことです。

ジェネレータイテレータはイテレータの1種

出来上がったジェネレータイテレータですが、これは名前の通りイテレータの1種です。

そしてイテレータとは次の性質をもつものでした。

  • イテレータはイテラブルオブジェクトの1種
  • __iter__メソッドと__next__メソッドがあればイテレータ

実際にdir()関数を使って、iter()メソッドとnext()メソッドがあるか調べてみましょう。

dir(gen_itrtr)

結果は次のようになります。

['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']

きちんと、__iter__と、__next__とが存在しています。よってイテレータです。

nextメソッド/next関数の使い方

yielc文によるジェネレータイテレータオブジェクトは__next__メソッドまたはnext()関数によって、「1回ずつ」処理を進めることが可能です。次のコードを見てください。

#1行ずつ実行してみてください
gen_itrtr.__next__()  
next(gen_itrtr)  
gen_itrtr.__next__()  
next(gen_itrtr)  # 辞書の要素はなくなったのでStopIterationという例外が出る

__next__メソッドとnext()関数を交互実行させました。試しにみなさんも1行ずつ実行させてみてください。その各行ごとの実行結果は次のようになります。

コード結果
1gen_itrtr.__next__() (‘id’, ‘A0283456’)
2next(gen_itrtr)(‘name’, ‘Tom’)
3gen_itrtr.__next__() (‘scores’, {‘Mathematics’:
53, ‘Engish’: 78, ‘Science’: 36})
4next(gen_itrtr)Traceback (most recent call last):
File “”, line 1, in
StopIteration

yield文は、上述のように「『進め』の指示がでるまで待機」というイメージでした。

そして__next__メソッドまたはnext()関数によって「進めの指示」が与えられ、次の1回分だけ処理が進みます。

上表の結果からyield文の動きのイメージがわかってもらえると思います。

ジェネレータとfor文:in演算子の後に自作の関数を設定できる

上のコードでは、次のようにしてジェネレータイテレータとしてgen_itrtrを作りました。

gen_itrtr=gnrtr(student_dict.items())  

このジェネレータイテレータは、イテレータの1種ですが、イテレータはイテラブルオブジェクトの1種でもあります。したがってジェネレータイテレータ(上で作ったgen_itrtr)は、イテラブルオブジェクトでもあります。

そしてfor文のinについては、「in イテラブルオブジェクト」という使い方です。

つまり、ジェネレーターイテレータをfor文で使うことが可能です。次のコードを見てください。

for i in gnrtr(student_dict.items()):
    print(i)

これを実行すると、

('id', 'A0283456')
('name', 'Tom')
('scores', {'Mathematics': 
53, 'Engish': 78, 'Science': 36})

となります。

普通はfor文のinの後には、リストだったりrange関数だったりを使うことが多いと思います。

しかし、このようにして自作のジェネレータから作ったジェネレータイテレータ(オブジェクト)を使うことも可能です。

yieldとprintとreturnの相違点

printとreturnの違いは「【Python入門】関数で使うreturn文とprint関数の違いについての初歩的な説明」にて解説しました。

yieldとprint()の違い

yield文とprint()の違いは、まさに上の項目、nextメソッド/next関数の使い方「ジェネレータとfor文:in演算子の後に自作の関数を設定できる」という両者におけるコードを実行してもらってその結果を見るとわかってもらえると思います。

改めて簡単に説明しますと、

for i in [0, 1, 2]:
    print(i)

この実行結果は、

0
1
2

となります。

一方で、

for i in [0, 1, 2]:
    yeild i

このコードはエラーになります。なぜなら、yield文は、必ずdef~で定義される関数の中に入っている必要があるからです。しかし上のコードはそうなっていません。

これがまず文法上というか見た目上の使い方の違いです。

次にyieldとprintの動作上の違いですが、

上コードのようにprint()はfor文で実行すると一気に結果が表示されています。

しかし、yield文は「『進め』の指示がでるまで待機」というイメージで、上項目「nextメソッド/next関数の使い方」で見たように、nextメソッドが呼ばれるまで、またはnext関数によって実行されるまでは処理が中断されたままの状態となります。

この点で動作上も決定的な違いがあります。

yieldとreturn文の違い

yield文とreturn文はどちらも「なんらかのデータを返す」という働きは同じです。しかし、return文については公式で次のように書かれています。

ジェネレータ関数では、 return 文はジェネレータの終わりを示し、 StopIteration 例外を送出させます。返された値は (あれば)、 StopIteration を構成する引数に使われ、 StopIteration.value 属性になります。

よって、ジェネレータ関数の中でreturn文を使うと、そこでジェネレータ関数は処理を終えて完全終了、かつ例外も生じさせるため、その後にnextメソッド/next関数を使っても、

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

となるだけです。

yieldは「一時停止かつ次のnextメソッド/next関数に向けた待機」ですが、returnは「完全終了かつ例外を発生」という点で決定的に異なるわけですね。

def ジェネレーター():
    (処理)
    yiled ○
    return △

このようなコードはおかしなことになるということですね。

愛を分かち合いましょう