Address
304 North Cardinal St.
Dorchester Center, MA 02124
Work Hours
Monday to Friday: 7AM - 7PM
Weekend: 10AM - 5PM
Address
304 North Cardinal St.
Dorchester Center, MA 02124
Work Hours
Monday to Friday: 7AM - 7PM
Weekend: 10AM - 5PM
今回は、Pythonの勉強を始めた初心者・入門者にとってわかりにくい概念の1つ、ジェネレータ(ジェネレータ関数)を簡単な具体例のコードを用いてわかりやすく解説しようと思います。
今回の解説で以下の点について初歩のイメージを持ってもらえると思います。
Contents
まず公式ドキュメントを見てみましょう。このようにう用語集には書かれています。
(ジェネレータ) generator iterator を返す関数です。 通常の関数に似ていますが、 yield 式を持つ点で異なります。 yield 式は、 for ループで使用できたり、next() 関数で値を 1 つずつ取り出したりできる、値の並びを生成するのに使用されます。
Python公式ドキュメント:用語集
通常はジェネレータ関数を指しますが、文脈によっては ジェネレータイテレータ を指す場合があります。 意図された意味が明らかでない場合、 明瞭化のために完全な単語を使用します。
初心者には全く意味がわかりませんね。
ひとまず要点として次の3点だけ意識しておきましょう。
それではこれら3点を意識しながら、次の説明を読んでいってください。
まず前提知識としてイテラブルオブジェクトとイテレータオブジェクトについて簡単なイメージだけでも持っているとジェネレータの理解に役立ちます。
イテラブルとイテレータについては、「【初心者にもわかりやすく】Pythonのイテレータについて、その基本とfor文の仕組みを解説」でも解説しましたので、ぜひ読んでみてください。
両者の関係について大雑把にいえば、
ということです。
まず今回のジェネレータの解説のために、辞書を用意します。その辞書については前回の記事「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}}
となります。
それではさっそくジェネレータを作ってみましょう。ジェネレータとは、上述のように、
でしたので、以下のようにします。
#ジェネレーターの作成
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種です。
そしてイテレータとは次の性質をもつものでした。
実際に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__とが存在しています。よってイテレータです。
yielc文によるジェネレータイテレータオブジェクトは__next__メソッドまたはnext()関数によって、「1回ずつ」処理を進めることが可能です。次のコードを見てください。
#1行ずつ実行してみてください
gen_itrtr.__next__()
next(gen_itrtr)
gen_itrtr.__next__()
next(gen_itrtr) # 辞書の要素はなくなったのでStopIterationという例外が出る
__next__メソッドとnext()関数を交互実行させました。試しにみなさんも1行ずつ実行させてみてください。その各行ごとの実行結果は次のようになります。
行 | コード | 結果 |
---|---|---|
1 | gen_itrtr.__next__() | (‘id’, ‘A0283456’) |
2 | next(gen_itrtr) | (‘name’, ‘Tom’) |
3 | gen_itrtr.__next__() | (‘scores’, {‘Mathematics’: 53, ‘Engish’: 78, ‘Science’: 36}) |
4 | next(gen_itrtr) | Traceback (most recent call last): File “”, line 1, in StopIteration |
yield文は、上述のように「『進め』の指示がでるまで待機」というイメージでした。
そして__next__メソッドまたはnext()関数によって「進めの指示」が与えられ、次の1回分だけ処理が進みます。
上表の結果からyield文の動きのイメージがわかってもらえると思います。
上のコードでは、次のようにしてジェネレータイテレータとして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関数だったりを使うことが多いと思います。
しかし、このようにして自作のジェネレータから作ったジェネレータイテレータ(オブジェクト)を使うことも可能です。
printとreturnの違いは「【Python入門】関数で使うreturn文と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文はどちらも「なんらかのデータを返す」という働きは同じです。しかし、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 △
このようなコードはおかしなことになるということですね。