yield を含む関数を Python ではジェネレーターと呼ぶことを聞いたことがあるかもしれません。ジェネレーターとは何ですか?
まずジェネレーターのことは置いておいて、一般的なプログラミングのトピックを使用して、収量の概念を説明しましょう。
フィボナッチ数列の生成方法
フィボナッチ数列は非常に単純な再帰数列で、最初の数と2番目の数を除いて、最初の2つの数を加算することで任意の数を得ることができます。コンピュータープログラムを使用してフィボナッチ数列の最初の N 個の数値を出力することは非常に単純な問題であり、多くの初心者は次の関数を簡単に作成できます:
リスト 1. 単純にフィボナッチ数列の最初の N 個の数値を出力します
deffab(max): n, a, b =0, 0, 1 whilen < max: printb a, b =b, a +b n =n +1
fab( を実行します。 5) を実行すると、次の出力が得られます:
>>> fab(5)
結果に問題はありませんが、経験豊富な開発者は、print を使用して fab 関数内で直接数値を出力すると、関数がエラーを引き起こすことを指摘するでしょう。 to fab関数はNoneを返し、他の関数はこの関数で生成されたシーケンスを取得できないため、再利用性が低いです。
fab関数の再利用性を高めるには、シーケンスを直接出力するのではなく、Listを返すのがベストです。以下は、fab 関数の書き換えられた 2 番目のバージョンです:
リスト 2. フィボナッチ数列の最初の N 個の数値を出力します。 2 番目のバージョン
deffab(max): n, a, b =0, 0, 1 L =[] whilen < max: L.append(b) a, b =b, a +b n =n +1 returnL
次のメソッドを使用して、fab 関数によって返されたリストを出力できます:
>>> forn infab(5):
... printn
...
書き換えられたfab関数はListを返すことで再利用性の要件を満たすことができますが、より経験豊富な開発者は関数が実行されていると指摘するでしょうメモリパラメータ max が増加するにつれて、占有率も増加します。メモリ使用量を制御したい場合は、中間結果を保存するために List
を使用するのではなく、反復可能なオブジェクトを反復処理するのが最善です。たとえば、Python2 では、 pass
は 1000 個の要素のリストを生成しませんが、各反復で次の値を返し、占有するメモリ領域は非常に少なくなります。 xrange は List を返すのではなく、反復可能なオブジェクトを返すためです。
iterable を使用すると、fab 関数を iterable をサポートするクラスに書き換えることができます。 以下は、Fab の 3 番目のバージョンです。
リスト 4. 3 番目のバージョン
classFab(object): def__init__(self, max): self.max=max self.n, self.a, self.b =0, 0, 1 def__iter__(self): returnself defnext(self): ifself.n < self.max: r =self.b self.a, self.b =self.b, self.a +self.b self.n =self.n +1 returnr raiseStopIteration()
Fab クラスは、シーケンス内の次のものを継続的に返します。 next() 数値、メモリ使用量は常に一定です:
>>> forn inFab(5):
... printn
...
ただし、class を使用して書き直されたこのバージョンのコードは、最初のものとは大きく異なります。 fab のバージョン 関数は簡単に説明します。反復可能な効果を得ながら、fab 関数の最初のバージョンの単純さを維持したい場合は、yield が便利です:
リスト 5. yield を使用した fab の 4 番目のバージョン
deffab(max): n, a, b =0, 0, 1 whilen < max: yieldb # print b a, b =b, a +b n =n +1
fab の 4 番目のバージョンと最初のバージョンとの比較このバージョンでは、print b を yield b に変更するだけで、シンプルさを維持しながら反復可能な効果が得られます。
fab の 4 番目のバージョンの呼び出しは、fab の 2 番目のバージョンとまったく同じです:
>>> forn infab(5):
... printn
...
簡単に言えば、 yield の関数です。関数はジェネレーターになります。yield を持つ関数は、もはや通常の関数ではありません。fab(5) を呼び出すと、fab 関数は実行されず、反復可能なオブジェクトが返されます。 for ループが実行されると、各ループは fab 関数内のコードを実行し、yield b に到達すると、fab 関数は次の反復で、yield b の次のステートメントから実行を続けます。ローカル変数は前回の中断前とまったく同じであるため、関数は再び yield が発生するまで実行を続けます。
fab(5) の next() メソッドを手動で呼び出すこともできます (fab(5) は next() メソッドを持つジェネレーター オブジェクトであるため)。これにより、fab の実行プロセスをより明確に確認できます。
リスト6. 実行処理
>>> f =fab(5)
>>> f.next()
>>> f.next()
>>> f.next()
> >> f.next()
>>> f.next()
>>> f.next()
トレースバック (最新の呼び出し最後):
File""、行 1、StopIteration
関数の実行が終了すると、ジェネレーターは自動的に StopIteration 例外をスローし、反復が完了したことを示します。 for ループでは、StopIteration 例外を処理する必要はなく、ループは正常に終了します。
次の結論を導き出すことができます:
yield を持つ関数はジェネレーターです。ジェネレーターの生成は関数呼び出しのように見えますが、 next( が呼び出されるまで関数コードは実行されません。 . ) (next() は for ループ内で自動的に呼び出されます)。実行フローは関数の流れに従って実行されますが、yield ステートメントが実行されるたびに中断され、次の実行は次の yield ステートメントから継続されます。通常の実行中に関数が yield によって数回中断され、各中断が yield を通じて現在の反復値を返しているように見えます。
yield 的好处是显而易见的,把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。
如何判断一个函数是否是一个特殊的 generator 函数?可以利用 isgeneratorfunction 判断:
清单 7. 使用 isgeneratorfunction 判断
>>> frominspect importisgeneratorfunction
>>> isgeneratorfunction(fab)
True
要注意区分 fab 和 fab(5),fab 是一个 generator function,而 fab(5) 是调用 fab 返回的一个 generator,好比类的定义和类的实例的区别:
清单 8. 类的定义和类的实例
>>> importtypes
>>> isinstance(fab, types.GeneratorType)
False
>>> isinstance(fab(5), types.GeneratorType)
True
fab 是无法迭代的,而 fab(5) 是可迭代的:
>>> fromcollections importIterable
>>> isinstance(fab, Iterable)
False
>>> isinstance(fab(5), Iterable)
True
每次调用 fab 函数都会生成一个新的 generator 实例,各实例互不影响:
>>> f1 =fab(3)
>>> f2 =fab(5)
>>> print'f1:', f1.next()
f1: 1
>>> print'f2:', f2.next()
f2: 1
>>> print'f1:', f1.next()
f1: 1
>>> print'f2:', f2.next()
f2: 1
>>> print'f1:', f1.next()
f1: 2
>>> print'f2:', f2.next()
f2: 2
>>> print'f2:', f2.next()
f2: 3
>>> print'f2:', f2.next()
f2: 5
return 的作用
在一个 generator function 中,如果没有 return,则默认执行至函数完毕,如果在执行过程中 return,则直接抛出 StopIteration 终止迭代。
另一个例子
另一个 yield 的例子来源于文件读取。如果直接对文件对象调用 read() 方法,会导致不可预测的内存占用。好的方法是利用固定长度的缓冲区来不断读取文件内容。通过 yield,我们不再需要编写读文件的迭代类,就可以轻松实现文件读取:
清单 9. 另一个 yield 的例子
defread_file(fpath): BLOCK_SIZE =1024 with open(fpath, 'rb') as f: whileTrue: block =f.read(BLOCK_SIZE) ifblock: yieldblock else: return
以上仅仅简单介绍了 yield 的基本概念和用法,yield 在 Python 3 中还有更强大的用法,我们会在后续文章中讨论。
注:本文的代码均在 Python 2.7 中调试通过