必要性を問われると...改めてジェネレータの意味を理解する上で今回の質問を例に何かご提言...
###質問者さんの課題の推測
generator関数を書いてはおられますが、それが正しいかどうか確信できないということは「generatorがどう動くのかのメカニズムがわからないためどう書くべきかが確信できない」といったところではないでしょうか。
つまり「generatorがコルーチン的な動作をすること」の把握が質問者さんの課題なのだろうと思います。
コルーチンの振る舞いがわかっていないとgeneratorとそれを使う側がどういう動きになるのかがわからず、それゆえ以下のような流れに陥ったのではないでしょうか。
=> なにやら特殊っぽいgeneratorというものをどう書いたらいいのかわからない
=> while文をfor文に変更してみたり、if文の論理を微妙に変えてみたりしたがやはり謎が残ったまま
###とりあえず正解コードは?
元の関数で印字している値を列挙するのが目的なのでしたら、
print(i)
=> yield i
へ置き換えるだけでよく、それ以外は一文字も変更する必要はありません。
###generator(コルーチン)の動きの解説(めいたもの)
generatorの振る舞いは先に伸べたように「コルーチン」と呼ばれる少々特殊なものです。そのため普通の関数呼び出しのイメージでとらえようとすると、その動作や働きを正しく把握することはできません。本体にyield文が含まれている関数(それ即ちgenerator)は普通の関数とは異なる以下のような振る舞いをします。
text
1def gen(a, b): #G0
2 yield a #G1
3 yield b #G2
4
5g = generate(1, 2) #M1
6for e in g: #M2
7 print(e) #M3
8 #M4
先に動きの説明の都合上、for文がループの各要素をどうやって決めるかの内部的な仕組みを述べます。
(ここから)
for文はループごとにinの後ろに指定された値(本件ではg)で決まるiterator(要素を順番に一つずつ列挙するためのオブジェクト)に対して__next__
メソッドを呼び出します。そしてその結果の値をその回のループの制御変数(ここでは変数e)の値にしてループ本体を実行します。これを「もう要素がない」という状態になるまで繰り返します。
(ここまで)
さて上のコードの動きですが・・・
M1: gen
関数を呼び出すとG0の地点で中断した状態になります。そしてその中断状態を表すようなgenerator iteratorが結果として返され、それがgen()
呼び出しの結果の値となります。
M2(1回目): __next__()
が呼び出されたとき、M1で生成されたgenerator iteratorを通じて**gen
は最後に中断した状態(つまりG0の地点)から実行を再開**します。再開する際にはgen
のコンテキスト(引数やローカル変数の値、for文の内側なら何回目のループだったかなども含む)は全て「以前に中断したときと同じ状態で再開される」ことに注意してください。その後のgen
の実行は次のyield文が出現するまで継続します。G1まで進むとyieldに指定した値a(つまり1)が__next__()
からreturnされます。ここでgen
は再び中断状態になります。
M3: 1が印字される
M2(2回目): 2回目の__next__()
が呼び出されるとgen
はG1の直後から再開し、G2のyieldで再び中断し、bの値(つまり2)が__next__()
からreturnされます。
M3: 2が印字される
M2(3回目): 3回目ではgen
はG2の直後から動き出しますがyieldがそれ以上出現せずに関数定義が完了(要するにreturn)します。このとき__next__
は「yieldされずにgeneratorの実行が終わったのでもう列挙すべき要素はない」と判断し呼び出し元(for文)に「もうないです」といいます(内部的にはStopIteration例外をraiseします)
M4: for文によるループ終了
...
こういう仕組みで動いていることをイメージすると、「generatorを書くには列挙したい値が求まるたびに、単にその値に対してyieldを書けばよいだけ」ということがわかってくるのではないでしょうか?