Pythonの歩留まりの分析
イールドのある関数はPythonではジェネレーターと呼ばれていると聞いたことがあるかもしれませんが、ジェネレーターとは何ですか?
ジェネレーターを脇に置いて、一般的なプログラミングトピックでyieldの概念を示しましょう。
フィボナッチシーケンスを生成する方法
フィボナッチシーケンスは非常に単純な再帰シーケンスですが、最初と2番目の番号を除いて、最初の2つの番号を加算することで任意の番号を取得できます。コンピュータープログラムを使用してFibonacciシーケンスの最初のN個の番号を出力することは、非常に単純な問題です。多くの初心者は、次の関数を簡単に作成できます。
リスト1.Fibonacciシーケンスの最初のN個の番号を出力するだけです
インスタンス
#!/usr/bin/python
# -*- coding: UTF-8 -*-
def fab(max):
n, a, b = 0, 0, 1
while n < max:
print b
a, b = b, a + b
n = n + 1
fab(5)
上記のコードを実行すると、次の出力が得られます。
1
1
2
3
5
結果に問題はありませんが、経験豊富な開発者は、fab関数がNoneを返し、他の関数が関数によって生成された番号シーケンスを取得できないため、fab関数に直接番号を印刷すると関数の再利用性が低下することを指摘します。
fab関数の再利用性を向上させるには、シーケンスを直接印刷するのではなく、リストを返すのが最善です。以下は、書き直されたfab関数の2番目のバージョンです。
リスト2.Fibonacciシーケンスの上位N個を出力します。第2版
インスタンス
#!/usr/bin/python
# -*- coding: UTF-8 -*-
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
for n in fab(5):
print n
次のように、fab関数によって返されるリストを印刷できます。
1
1
2
3
5
書き直されたfab関数は、Listを返すことで再利用性の要件を満たすことができますが、経験豊富な開発者は、操作中に関数が占有するメモリは、パラメータmaxの増加とともに増加することを指摘します。メモリを制御する場合占有、リストを使用しないことをお勧めします
中間結果を保存しますが、反復可能なオブジェクトを反復処理します。たとえば、Python 2.xでは、次のコードを使用します。
リスト3.反復可能なオブジェクトを反復処理する
for i in range(1000): pass
1000個の要素のリストが生成され、コードは次のようになります。
for i in xrange(1000): pass
1000要素のリストは生成されませんが、各反復で次の値が返され、メモリスペースは非常に小さくなります。xrangeはリストを返さないため、反復可能なオブジェクトを返します。
iterableを使用すると、fab関数をiterableをサポートするクラスに書き換えることができます。Fabの3番目のバージョンは次のとおりです。
リスト4.3番目のバージョン
インスタンス
#!/usr/bin/python
# -*- coding: UTF-8 -*-
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()
for n in Fab(5):
print n
Fabクラスは、next()を介してシーケンス内の次の番号を継続的に返し、メモリ使用量は常に一定です。
1
1
2
3
5
ただし、このバージョンはクラスから書き直されているため、コードはfab関数の最初のバージョンよりもはるかに簡潔です。fab関数の最初のバージョンの簡潔さを維持し、同時に反復可能な効果を取得したい場合は、yieldが役立ちます。
リスト5.yieldを使用した第4版
インスタンス
#!/usr/bin/python
# -*- coding: UTF-8 -*-
def fab(max):
n, a, b = 0, 0, 1
while n < max:
yield b # 使用 yield
# print b
a, b = b, a + b
n = n + 1
for n in fab(5):
print n
最初のバージョンと比較して、fabの4番目のバージョンはprint bをbを生成するように変更するだけで、シンプルさを維持しながら反復可能な効果を実現しました。
ファブの4番目のバージョンを呼び出すことは、ファブの2番目のバージョンとまったく同じです。
1
1
2
3
5
簡単に言えば、yieldの関数は、関数をジェネレーターに変換することです。yieldの関数は、通常の関数ではなくなります。Pythonインタープリターは、それをジェネレーターとして扱います。fab(5)を呼び出しても、fab関数は実行されません。代わりに、反復可能なオブジェクトを返します!forループが実行されると、ループが実行されるたびにfab関数内のコードが実行されます。実行がyield bに達すると、fab関数は反復値を返します。次の反復では、コードは次のyieldBステートメントから実行を継続します。ローカル変数は最後の中断前とまったく同じように見えるため、関数は再び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例外を処理する必要はなく、ループは正常に終了します。
次の結論を導き出すことができます。
イールドを持つ関数はジェネレーターであり、通常の関数とは異なります。ジェネレーターの生成は関数呼び出しのように見えますが、next()(forループではnext())を呼び出すまで関数コードを実行しません。 ))実行を開始します。実行フローは関数のフローに従って実行されますが、yieldステートメントが実行されるたびに中断され、反復値が返されます。次の実行は、次のyieldステートメントから続行されます。通常の実行中に関数がyieldによって数回中断されたように見え、中断するたびに、yieldを通じて現在の反復値が返されます。
イールドの利点は明らかです。関数をジェネレーターに書き換えると、反復能力が得られます。クラスインスタンスを使用して状態を保存し、次のnext()値を計算する場合と比較して、コードが簡潔であるだけでなく、実行フローも非常に明確です。
関数が特別なジェネレータ関数であるかどうかを判断するにはどうすればよいですか?isgeneratorfunctionを使用して、以下を判別できます。
リスト7.isgeneratorfunctionを使用して決定する
>>>from inspect import isgeneratorfunction
>>> isgeneratorfunction(fab)
True
fabとfab(5)の違いに注意してください。fabはジェネレーター関数であり、fab(5)は、クラスの定義とクラスのインスタンスの違いと同じように、fabを呼び出すことによって返されるジェネレーターです。
リスト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関数を呼び出すたびに、新しいジェネレータインスタンスが生成され、インスタンスは相互に影響を与えません。
>>>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
リターンの役割
ジェネレーター関数では、戻り値がない場合は、デフォルトで関数が完了するまで実行されます。実行中に戻る場合は、StopIterationを直接スローして、反復を終了します。
もう一つの例
歩留まりのもう1つの例は、ファイルの読み取りです。ファイルオブジェクトに対してread()メソッドを直接呼び出すと、予測できないメモリ使用量が発生します。良い方法は、固定長のバッファーを使用してファイルの内容を継続的に読み取ることです。yieldを使用すると、ファイルを読み取るための反復クラスを作成する必要がなくなり、ファイルを簡単に読み取ることができます。
リスト9.別の歩留まりの例
インスタンス
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