ここでは、Python のイテレータとジェネレータについて、主に以下の3点に触れながら、詳しく解説しています。
- イテレータ・ジェネレータとは何か?
- イテレータ・ジェネレータの使い方
- それらを使って書けるコード例
使いこなせると便利なものなので、ぜひお役立て頂ければと思います。
1. イテレータとは
イテレータ “iterator” は、「反復する」「繰り返す」という意味の単語のイテレート “iterate” から来ています。そして、Python のオブジェクトには、
- イテラブル
- イテレータ
があります。まずは、両者の違いを押さえておきましょう。
イテレータ “iterator”
イテレータには、『値に含まれている要素を 1 個ずつ「繰り返し」取り出せるオブジェクト』という意味があります。
イテラブル “iterable”
イテラブルは、要素を順番に取り出せるオブジェクトのことです。例えば、リスト、文字列、タプルや辞書はイテラブルです。「初心者のためのPythonのfor in文の基礎」で、これらの 4 つのイテラブルから要素を順番に取り出す例を解説していますので、再度、確認してみてください。
イテレータとイテラブルの違い
イテレータとイテラブルの違いは、前者は要素を1個取り出すごとにどこまで取り出したかという状態を保持するのに対して、後者は取り出された要素の位置を管理しないことにあります。そのためイテレータは、一度、途中まで要素を取り出した後に、また同じことをすると、今度は、先ほどの続きから要素を取り出します。
1.1. イテラブルからイテレータを作る | iter 関数
それでは、実際に、イテレータを見てみましょう。
イテレータは iter 関数を使って、イテラブルから作ることができます。次のコードをご覧ください。イテラブルであるリストをイテレートに変換しています。
'''リストを作ります。'''
list = ["red", "blue", "green", "yellow"]
'''iter 関数でイテラブルからイテレータを作ることができます。'''
iter = iter(list)
'''それぞれの型を確認してみましょう。'''
print(type(list))
print(type(iter))
type 関数で、型を確認すると、 list はリスト型、iter はイテレータ型になっていることが分かりますね。型については「Pythonの型とは | 確認と変換の方法」でご確認ください。
1.2. イテレータから要素を順番に取り出す | next 関数
イテレータは、『値に含まれている要素を 1 個ずつ「繰り返し」取り出せるオブジェクト』です。そこで、上のイテレータから順番に要素を取り出してみましょう。
そのためには next 関数を使います。次のコードをご覧ください。
'''イテレータからは next 関数で要素を 1 つずつ取り出すことができます。'''
print(next(iter)) #最初の要素を取り出します。
print(next(iter)) #次の要素を取り出します。
print(next(iter)) #次の要素を取り出します。
print(next(iter)) #次の要素を取り出します。
イテレータが、どこまで要素を取り出されているのかという状態を保持しているため、next 関数を実行する度に、途中から取り出されていることが分かりますね。
なお、全ての要素を取り出したイテレータに、さらに next 関数を実行すると、StopIteration というエラーになります。
'''全ての要素を取り出したイテレータから、さらに要素を取り出そうとするとエラーになります。'''
print(next(iter)) #要素を全て取り出しているのでエラーになる。
2. ジェネレータとは
ジェネレータとは、「イテレータを作る関数」のことです。
イテレータではなく、イテラブルからも要素を順に取り出すことはできます。そのため、「わざわざジェネレータでイテレータを作る必要があるのか」と疑問に感じることもあるでしょう。
結論からいうと、ジェネレータを使いこなせるようになると、プログラムの高速化に繋がります。例えば、イテラブルから要素を順に取り出すには、まず、全ての要素をメモリ内に格納する必要があります。しかし、ジェネレータを使った場合は、随時要素を取り出すので、必要なだけの要素を取り出したら処理を終了できて、作業メモリが少なくて済むのです。
ジェネレータには、このようなメリットがあるので、是非抑えておきましょう。
2.1. ジェネレータ関数でイテレータを作る
それでは、ジェネレータ関数でイテレータを作る方法を見ていきましょう。そのためには、以下の 2 つのステップを挟む必要があります。
- def 文でジェネレータ関数を定義する。
- ジェネレータ関数を変数に代入してジェネレータ型オブジェクトを作る。
それでは解説していきます。
2.1.1. def 文でジェネレータ関数を定義する
ジェネレータからイテレータを作るには、まず def 文でジェネレータ関数を定義する必要があります。def 文については「Pythonのdef文を使った関数の作り方まとめ」をご覧ください。
次のコードで定義している generator() はもっとも基本的な部類のジェネレータ関数です。
'''ジェネレータ関数を定義します。'''
def generator():
yield "red" #関数を呼び出した時に返す値を yield で指定します。
yield "blue"
yield "green"
yield "yellow"
このようにジェネレータ関数は、通常の関数とは違い、return の代わりに yield で戻り値を設定します。これを実行すると、戻り値としてジェネレータが作られます。
しかし、次のコードのように、def 文で定義した関数の型は、当然、関数型です。そのため、この関数は、このままではジェネレータとして使うことはできません。
type(generator) #上で作成した generator 関数の型を調べてみましょう。
作成した generator 関数を、使うには、これを変数に代入して、ジェネレータ型オブジェクトを作る必要があります。これについては、「Pythonの関数の基礎知識と使い方と一覧まとめ」の「5. 関数オブジェクトについて」をご覧頂くと理解しやすいと思います。
2.1.2. ジェネレータ関数を変数に代入してジェネレータ型オブジェクトを作る
次に generator 関数を 変数 menu に代入して、ジェネレータ型オブジェクト menu を作ります。
'''generator 関数を変数 menu に代入して menu ジェネレータを作ります・'''
menu = generator()
'''menu の型を確認するとジェネレータ型オブジェクトであることが分かります。'''
type(menu)
この関数オブジェクト menu の型を確認してみるとジェネレータ(generator)となっていますね。これで、ジェネレータ型のオブジェクトが作られました。このジェネレータ型オブジェクトは、イテレータと同じように使うことができます。
それを次に見ていきましょう。
2.1.3. ジェネレータから要素を 1 個ずつ取り出す | next 関数
ジェネレータからは、イテレータと同じように、next 関数で要素を 1 個ずつ順番に取り出すことができます。
'''ジェネレータはイテレータと同じように next 関数で要素を取り出すことができます。'''
print(next(menu))
print(next(menu))
print(next(menu))
print(next(menu))
要素を全て取り出した後に、再度 next 関数を実行すると StopIteration エラーになるのも一緒です。
'''イテレータ同様全て取り出した後に next 関数を実行するとエラーになります。'''
print(next(menu))
generator 関数で作ったイテレータもイテラブルなので、for in 文で要素を取り出すことができます。
上の関数オブジェクト menu からは、すでに順番に要素を取り出しているので、generator 関数を、再度別の変数に代入しています。
'''ジェネレータにも for in 文が使えます。'''
generator = generator() #要素が空になっているのでもう一度ジェネレータを作ります。
'''for in 文で要素を取り出します。'''
for item in generator:
print(item)
2.2. ジェネレータを使ったコード例
ここからはジェネレータ関数を使ったコード例をいくつか見ていきましょう。
2.2.1. 数値列を作る
以下のコードでは、ジェネレータから取り出した値を使って、数値列を作っています。while 文や for in range 文は、それぞれ「初心者のための Python の while 文(繰り返し処理)の基礎と使い方まとめ」、「Pythonのfor in range文の重要知識と使い方まとめ」でご確認ください。
'''数値を作るジェネレータを作る関数を作ります。
num の値が、3, 6, 11, 18, 27, ... と増えていく関数です。'''
def num_generator():
n = 0
while True: #while True で無限ループする関数を作ります。
num = n*n + 2*n + 3 #数列式
yield num #ジェネレータが返す値
n += 1#ループ1回ごとにnに1を足す。
'''do 関数を作ります。左列に num の値を2で割った余り、
右列に num の値を 3 で割った余りを返します。'''
def do(num):
return(num%2, num%3) #numを2で割った余りと3で割った余りを返します。
'''ジェネレータ関数を gen に代入して ジェネレータ型オブジェクト gen を作ります。
これで gen から上の def 文で定義した値を 1 つずつ取り出すことが可能になります。'''
gen = num_generator() #gen ジェネレータを作ります。
'''ジェネレータが返す値を使って処理を行う。'''
for i in range(1, 10): #ブロックに書いた処理を9回行います。
num = next(gen) #numの値は3, 6, 11, 18 と入っていきます。
result = do(num) #numを2で割った余りを左列に、3で割った余りを右列に表示します。
print(result) #結果を見てみましょう。
コード中のコメントの解説をよく読んで、理解を深めてみてくださいね。コードを写経する時は、コピー&ペーストすると動かない可能性があるので、コメントを抜いたコードを 1 文字ずつ手入力してみてください。
2.2.2. FizzBuzz ゲームを作る
次に、ジェネレータを使って、有名な FizzBuzz ゲームを作ってみましょう。FizzBuzz ゲームとは、3 の倍数の時は Fizz 、 5 の倍数の時は Buzz 、 両方の倍数の時は FizzBuzz と表示するものです。
if 文については、「初心者のための Python の if 文(条件分岐)の基礎と使い方まとめ」でご確認ください。
'''FizzBuzz を関数で定義します。'''
def fizzbuzz():
n = 1
while True: #無限ループ
if n%15 == 0: #15の倍数の場合
yield "FizzBuzz"
elif n%3 == 0: #3の倍数の場合
yield "Fizz"
elif n%5 == 0: #5の倍数の場合
yield "Buzz"
else: #それ以外
yield str(n)
n += 1 #最後に n に 1 を足してループに戻ります。
'''fizzbuzz 関数で game ジェネレータを作り 20 回呼び出す。'''
game = fizzbuzz() #fizzbuzz関数を game に代入します。
for i in range(0, 20):
print(next(game))
2.2.3. 単語当てクイズ
もう一つ例を見てみましょう。単語当てクイズです。
def quiz(word):
hint = ""
for letter in word:
hint += letter
yield hint
ans = "Python"
quiz = quiz(ans)
while True:
try:
hint = next(quiz)
print(hint)
word = input("この単語は?:")
if ans.lower() == word.lower():
point = len(ans) - len(hint)
print(f"正解。得点は{point}です。")
break
else:
print("間違い\n")
except:
print("終了です。0点です。")
break
このコードは、ぜひ、ご自身で内容を紐解いてみてください。
3. 様々なジェネレータ式
ジェネレータ式には、ここまでお伝えしたもの以外にも様々なものがあります。そこで、ここでは、知っておくと便利な 3 つの式を解説しておきます。
- ジェネレータ内包表記
- ジェネレータに値を送る式
- サブジェネレータ、サブイテレータからジェネレータに値を送る式
です。
それぞれ見ていきましょう。
3.1. ジェネレータ内包表記
ジェネレータには、リスト内包表記に似た書式があります。この書き方を使うと、わざわざ、
- 関数を定義して、
- それを変数に代入して、
- ジェネレータ型オブジェクトを作る。
というステップを大幅に省略することができます。結果、よりシンプルで分かりやすいコードを書くことができるようになるので、ぜひ身につけておきたいものです。
ここでは、その書き方をみていきましょう。
例えば、次のようなジェネレータ内包表記を書くことができます。
'''ジェネレータ内包表記'''
odd_gen = (odd for odd in range(1, 6, 2)) #1から5の奇数だけ取り出します。
print(next(odd_gen))
print(next(odd_gen))
print(next(odd_gen))
このように、関数を代入したい変数を先に書いてから、それをイコール( = )で繋ぎ、代入するジェネレータを、括弧 ( ) で囲んだ中に書きます。この例では、括弧 ( ) の中に書いた式「odd for odd in range(1, 6, 2)」がジェネレータになります。
それを、変数 odd_gen に代入していますので、odd_gen はジェネレータ型オブジェクトになります。それは、 next 関数で、値を一つずつ取り出せていることから分かりますね。
同じように、偶数のイテレータも作ることができます。
'''ジェネレータ内包表記'''
even_gen = (even for even in range(2, 11, 2)) # 2 から 10 の偶数だけ取り出します。
'''ジェネレータは list 関数で list に変換することができます。'''
list(even_gen)
3.2. ジェネレータに値を送る
ジェネレータからは値を順番に取り出すだけではなく、そこに対して、別の値を送ることもできます。
そのためには、yield を別の変数に代入する式を書いておいて、そこに send 関数を使って、値を送ることができるようにしておきます。
以下に簡単な例を書いておきます。
'''ジェネレータの戻り値 yield を変数に代入した式を定義します。'''
def gen():
n = 0
while True:
received = yield n #yield を変数に代入して send() に入れた値を受け取ることができます。
if received: #もし変数receivedが値を受け取ったら、yieldはそこから始まります。
n = received
else:
n = n + 1
'''普通に値を取り出すと else 文が適用されるため 0からカウントアップされます。'''
gen = gen()
print(next(gen))
print(next(gen))
print(next(gen))
def 文の定義の中で、yield n を変数 received に代入していますね。n の初期値は 0 なので、このジェネレータから値を取り出すと、0, 1, 2 と 0 から順番にカウントアップしていきます。
それでは、次に、send 関数を使って、このジェネレータに、別の値を送ってみましょう。それが以下のコードです。
'''send 関数で、数値の10を送ると、if 文が適用されるため、
yield の値は 10 になります。'''
gen.send(10)
'''次に gen から値を取り出すと、11からカウントアップされます。'''
print(next(gen))
print(next(gen))
print(next(gen))
ジェネレータ gen に 10 が送られたので、次のカウントアップは、11 から始まります。このように、ジェネレータにも値を送ることが可能ということを、頭の隅に入れておきましょう。
3.3. サブジェネレータからメインジェネレータに値を送る
Python では、ジェネレータから取り出す値は、他のジェネレータやイテレータを呼び出すこともできます。その場合、値を呼びだす元のジェネレータは「サブジェネレータ」、値を呼び出す元のイテレータは「サブイテレータ」といいます。
これを使うときは、def 文で定義するときに
yield from サブイテレータ
yield from サブジェネレータ
と書きます。
次のコードをご覧ください。このコードでは、main_gen で作るジェネレータの値は、最初の “start” と最後の “end” 以外の値は、サブイテレータ、サブジェネレータのものを利用しています。
'''メインのジェネレータ'''
def main_gen(n):
yield "start"
yield from range(n, 0, -1) #サブイテレータ。
yield from "abc" #サブイテレータ。
yield from [10, 20, 30] #サブイテレータ。
yield from sub_gen() #下に書いたサブジェネレータから値を作る。
yield "end"
'''サブジェネレータ'''
def sub_gen():
yield "X"
yield "Y"
yield "Z"
ここから、ジェネレータ型オブジェクトを作り、値を確認してみましょう。
まず、このコードを、subgenerator.py というファイル名をつけて保存します。そして、次のコードで、import を使って、ファイルを読み込み、ジェネレータ型オブジェクト gen を作っています。import については、「Pythonのモジュールについて抑えておくべき知識とよく使うもの一覧」で解説しています。
そして、それを list 関数で変換して、値を確認しています。
import subgenerator
gen = subgenerator.main_gen(3)
list(gen)
いかがでしょうか。
yield from で指定したものから値が取り出されていますね。また yield from “abc” の値は、 “a”, “b”, “c” と 1 文字ずつ戻していることもわかります。
それぞれ、下図のように対応しています。
最初は、分からなくても問題ありません。学習が進んだ後に読み返せば、すんなりと理解できるようになっているものです。
4. まとめ
ここまで解説した通り、イテレータは、要素を 1 個ずつ順番に取り出せるオブジェクトのことです。そして、ジェネレータは、イテレータを都度、作りだすことができるものです。
これらをしっかり理解して使いこなせるようになるには、関数やモジュールの知識もしっかり押さえておく必要があります。もし、現時点で、少し難解に感じた方は、「Pythonの関数の基礎知識と使い方と一覧まとめ」をもう一度読んでみましょう。
コメント
コメント一覧 (2件)
「イテレータ “iterator”」、
「イテラブル “iterable”」、
「イテレータとイテラブルの違い」
これらの説明が整合性がなく、分かりにくいです。
「2. ジェネレータとは」
のところで、メモリの使用量と高速化の話が繋がっていないです。
メモリの節約になります、なら理解できますが、高速化できます、は理解できません。