Python では、yield を含む関数をジェネレーターと呼ぶことを聞いたことがあるかもしれません。ジェネレーターとは何ですか?
まずジェネレーターのことは脇に置いて、一般的なプログラミングのトピックを使用して、歩留まりの概念を説明しましょう。
フィボナッチ数列の生成方法
フィボナッチ数列は非常に単純な再帰数列で、最初と 2 番目の数を除いて、最初の 2 つの数を加算することで任意の数を得ることができます。コンピューター プログラムを使用してフィボナッチ数列の最初の N 個の数値を出力することは、多くの初心者でも簡単に次の関数を作成できます。
リスト1. フィボナッチ数列の最初のN個の数値の単純な出力 def fab(max):
n, a, b = 0, 0, 1
while n < max:
print b
a, b = b, a + b
n = n + 1
fab(5) を実行すると、次の出力が得られます:
>>> fab(5) 1 1 2 3 5
結果に問題はありませんが、経験豊富な開発者は、fab 関数内で直接数字を出力するために print を使用すると、関数の再利用性が低下すると指摘するでしょう。これは、fab 関数が None を返し、他の関数が関数によって生成されたシーケンスを取得できないためです。
fab 関数の再利用性を向上させるには、シーケンスを直接出力せず、List を返すことが最善です。以下は、fab 関数の 2 番目のバージョンの書き換えです:
リスト2. フィボナッチ数列第2バージョンの最初のN個の数値を出力する
def fab(max):
n, a, b = 0, 0, 1
L = []
while n < max:
L.append(b)
a, b = b, a + b
n = n + 1
return L
次のメソッドを使用して、fab 関数によって返されたリストを出力できます:
>>> for n in fab(5): ... print n ... 1 1 2 3 5
書き換えられた fab 関数は List を返すことで再利用性の要件を満たすことができますが、経験豊富な開発者は、パラメータ max が増加するにつれて動作中にこの関数が占有するメモリが増加することを指摘するでしょう。メモリ占有量を制御したい場合は、それが最適です。リストを使用しないでください
中間結果を保存するには、反復可能なオブジェクトを反復処理します。たとえば、Python2.x のコード:
リスト 3. 反復可能オブジェクトの反復処理
for i in range(1000): pass
結果は 1000 個の要素のリストとコード:
for i in xrange(1000): pass
1000 個の要素のリストは生成されませんが、各反復で次の値が返され、占有されるメモリ領域はほとんどありません。 xrange は List を返すのではなく、反復可能なオブジェクトを返すためです。
iterable を使用すると、fab 関数を iterable をサポートするクラスに書き直すことができます。次に示すのは、Fab の 3 番目のバージョンです。
リスト 4. 第三バージョンclass Fab(object): def __init__(self, max): self.max = max self.n, self.a, self.b = 0, 0, 1 def __iter__(self): return self def next(self): if self.n < self.max: r = self.b self.a, self.b = self.b, self.a + self.b self.n = self.n + 1 return r raise StopIteration()
Fab クラスは next() を通じてシーケンス内の次の数値を継続的に返し、メモリ使用量は常に一定です: >>> for n in fab(5):
... print n
...
1
1
2
3
5
ただし、クラスを使用して書き直されたこのバージョンのコードは、fab 関数の最初のバージョンよりもはるかに簡潔ではありません。 Fab 関数の最初のバージョンの単純さを維持しながら、反復可能な効果を得るには、Yield が便利です。
リスト 5. yield
def fab(max): n, a, b = 0, 0, 1 while n < max: yield b # print b a, b = b, a + b n = n + 1 '''
を使用した 4 番目のバージョン 最初のバージョンと比較して、fab の 4 番目のバージョンでは print b を yield b に変更しただけで、単純さを維持しながら反復可能な効果が得られました。 4 番目のバージョンの fab を呼び出すことは、2 番目のバージョンの fab とまったく同じです:
>>> for n in fab(5): ... print n ... 1 1 2 3 5
簡単に言うと、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() 1 >>> f.next() 1 >>> f.next() 2 >>> f.next() 3 >>> f.next() 5 >>> f.next() Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
関数の実行が終了すると、ジェネレーターは自動的に StopIteration 例外をスローし、反復が完了したことを示します。 for ループでは、StopIteration 例外を処理する必要はなく、ループは正常に終了します。
次の結論を導き出すことができます: yield を持つ関数はジェネレーターです。ジェネレーターの生成は関数呼び出しのように見えますが、 next() が呼び出されるまで関数コードは実行されません (next( は for で自動的に呼び出されます)。実行を開始する前に、loop) )) を実行します。実行フローは関数の流れに従って実行されますが、yield ステートメントが実行されるたびに中断され、次の実行は次の yield ステートメントから継続されます。通常の実行中に関数が yield によって数回中断され、各中断が yield を通じて現在の反復値を返しているように見えます。
関数をジェネレーターとして書き直すと、クラスのインスタンスを使用して next() 値を計算する場合と比較して、コードが簡潔になるだけでなく、反復処理が可能になるという利点は明らかです。実行プロセスは非常に明確です。
如何判断一个函数是否是一个特殊的 generator 函数?可以利用 isgeneratorfunction 判断:
清单 7. 使用 isgeneratorfunction 判断
>>> from inspect import isgeneratorfunction >>> isgeneratorfunction(fab) True
要注意区分 fab 和 fab(5),fab 是一个 generator function,而 fab(5) 是调用 fab 返回的一个 generator,好比类的定义和类的实例的区别:
清单 8. 类的定义和类的实例
>>> import types >>> isinstance(fab, types.GeneratorType) False >>> isinstance(fab(5), types.GeneratorType) True
fab 是无法迭代的,而 fab(5) 是可迭代的:
>>> from collections import Iterable >>> 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 的例子
def read_file(fpath): BLOCK_SIZE = 1024 with open(fpath, 'rb') as f: while True: block = f.read(BLOCK_SIZE) if block: yield block else: return
以上仅仅简单介绍了 yield 的基本概念和用法,yield 在 Python 3 中还有更强大的用法,我们会在后续文章中讨论。
注:本文的代码均在 Python 2.7 中调试通过
以上がPython の収量使用率分析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。