Pythonでfib数列を実現するいくつかの方法(yieldの応用)

5408 ワード

yield付き関数はPythonでgenerator(ジェネレータ)と呼ばれていますが、generatorとは何ですか?
私たちはまずgeneratorを捨てて、よくあるプログラミングテーマでyieldの概念を展示します.
フィボナッチ数列の生成方法
フィボナッチ(Fibonacci)数列は非常に簡単な再帰数列であり、最初の数と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関数の多重性を向上させるには,数列を直接印刷するのではなく,リストを返すことが望ましい.以下はfab関数の書き換え後の2番目のバージョンです.
リスト2.出力フィボナッチ数列前N個数第2版
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関数はリストに戻ることで多重性の要件を満たすことができるが,より経験のある開発者は,この関数が実行中に占有するメモリはパラメータmaxの増大に伴って増大すると指摘し,メモリ占有を制御するにはリストを用いないほうがよい.
をクリックして、中間結果を保存します.代わりにiterableオブジェクトで反復します.例えば、Python 2.xで、コード:
リスト3.iterableオブジェクトで反復
for i in range(1000): pass

1000要素のリストが生成され、コードが生成されます.
for i in xrange(1000): pass

1000要素のリストは生成されず、反復ごとに次の数値が返され、メモリ領域の占有量が小さい.xrangeはListではなくiterableオブジェクトを返すためです.
iterableではfab関数をiterableをサポートするclassに書き換えることができます.以下は3番目のバージョンのFabです.
リスト4.3番目のバージョン
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

しかし、classを使用して書き換えられたこのバージョンでは、コードは第1版のfab関数よりはるかに簡潔ではありません.第1版fab関数の簡潔性を維持しながらiterableの効果を得るには、yieldが役に立ちます.
リスト5.yieldの第4版を使う
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は、1番目のバージョンと比較してprint bをyield bに変更するだけで、簡潔性を保ちながらiterableの効果を得た.
呼び出しの第4版のfabと第2版のfabは完全に一致しています.
>>> for n in fab(5): 
 ...     print n 
 ... 
 1 
 1 
 2 
 3 
 5

簡単に言えば、yieldの役割は1つの関数をgeneratorに変えることであり、yieldを持つ関数はもう普通の関数ではなく、Python解釈器はそれをgeneratorと見なし、fab(5)を呼び出すとfab関数は実行されず、iterableオブジェクトに戻る!forループが実行されると、ループのたびにfab関数内部のコードが実行され、yield bに実行されるとfab関数は反復値を返し、次回の反復ではyield bの次の文からコードが実行され続け、関数のローカル変数は前回の実行中断前と全く同じように見え、関数は再びyieldに遭遇するまで実行され続けます.
fab(5)のnext()メソッドを手動で呼び出すこともできます(fab(5)はgeneratorオブジェクトであり、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

関数の実行が終了すると、generatorは自動的にStopIteration異常を放出し、反復が完了したことを示します.forサイクルでは、StopIteration異常を処理する必要がなく、サイクルは正常に終了します.
私たちは以下の結論を出すことができます.
yield付き関数はgeneratorであり、通常の関数とは異なり、generatorを生成して関数呼び出しのように見えますが、next()を呼び出す(forループでnext()が自動的に呼び出される)まで関数コードは実行されません.実行プロセスは関数のプロセスで実行されますが、yield文が実行されるたびに中断され、反復値が返され、次回の実行時にyieldの次の文から実行されます.関数が正常に実行されている間にyieldによって数回中断されたように見え、中断するたびにyieldを通じて現在の反復値が返されます.
yieldの利点は明らかであり,1つの関数を1つのgeneratorに書き換えると反復能力が得られ,クラスのインスタンス保存状態で次のnext()の値を計算するよりも,コードが簡潔であるだけでなく,実行フローが異常に明確である.
1つの関数が特殊な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を放出して反復を終了します.