Manjusaka

Manjusaka

Pythonにおけるジェネレーターとコルーチンについて話しましょう

前書き#

今週は Flask について続けて書こうと思っていましたが、考えた結果、気分を変えて、理解が難しいけれども非常に重要な Python の生成器とコルーチンについて話しましょう。

生成器の基礎知識#

皆さんは生成器に馴染みがあると思いますが、私が気持ちよく続けて自慢できるように、生成器とは何かを少し説明しましょう。
例えば、Python で、範囲 (1,100000) のリストを生成したい場合、私たちは無思考で以下のコードを書きました。

def generateList(start,stop):
	tempList=[]
	for i in range(start,stop):
		tempList.append(i)
	return tempList

注 1:ここで、なぜ range(start,stop) を直接返さないのかという質問が出ました。良い質問です。ここには基本的な問題が関わっています。range のメカニズムはどうなっているのでしょうか。これはバージョンによって異なります。Python 2.x のバージョンでは、range(start,stop) は実質的に事前に生成された list であり、list オブジェクトは Iterator であるため、for 文で使用できます。
Python 2.x の range
次に、Python 2.x には xrange という文があり、これは Generator オブジェクトを生成します。
Python 2.x の xrange
Python 3 では、状況が少し変わりました。コミュニティは rangexrange の分裂が煩わしいと感じたため、これらを統合しました。したがって、Python 3 では xrange の構文糖が廃止され、range のメカニズムも list ではなく Generator を生成するようになりました。
Python 3 の range

しかし、皆さんは一つの問題を考えたことがありますか?もし私たちが非常に大きなデータ量を生成したい場合、事前にデータを生成する行為は明らかに賢明ではありません。これは大量のメモリを消費します。そこで、Python は私たちに新しい方法を提供しました。Generator(生成器)です。

def generateList1(start,stop):
	for i in range(start,stop):
		yield i

if __name__=="__main__":
	c=generateList1(1,100000)
	for i in c:
		print(i)

そうです、Generator の特性の一つは、データを一度に生成するのではなく、イテラブルなオブジェクトを生成し、イテレーション時に私たちが書いたロジックに基づいてその開始タイミングを制御することです。

Generator の深堀り#

ここで一つの疑問があるかもしれません。皆さんは Python の開発者たちがこのような使用シーンのために特別に Generator メカニズムを作ったとは考えないでしょう。では、Generator には他にどんな使用シーンがあるのでしょうか。もちろん、タイトルを見てください。そうです、Generator のもう一つの大きな役割はコルーチンとして使用されることです。しかしその前に、私たちは Generator を深く理解する必要があります。そうすれば、後の説明が楽になります。

Generator の内蔵メソッドについて#

Python のイテラブルオブジェクトに関する背景知識#

まず、Python のイテレーションプロセスを見てみましょう。
Python では、イテレーションには二つの概念があります。一つは Iterable、もう一つは Iterator です。それぞれを見てみましょう。
まず、Iterable はプロトコルのように理解できます。ある ObjectIterable かどうかを判断する方法は、iter を実装しているかどうかを見ることです。もし iter を実装していれば、それは Iterable オブジェクトと見なされます。無駄な議論はやめて、直接コードを見て理解しましょう。

class Counter:
    def __init__(self, low, high):
        self.current = low
        self.high = high

    def __iter__(self):
        return self

    def next(self):  # Python 3: def __next__(self)
        if self.current > self.high:
            raise StopIteration
        else:
            self.current += 1
            return self.current - 1

if __name__ == '__main__':
	a=Counter(3,8)
	for c in a:
		print(c)

さて、上記のコードで何が起こったのか見てみましょう。まず、for 文はイテレートするオブジェクトが IterableIterator かを判断します。もし __iter__ メソッドを実装しているオブジェクトであれば、それは Iterable オブジェクトです。for ループは最初にオブジェクトの __iter__ メソッドを呼び出して Iterator オブジェクトを取得します。では、Iterator オブジェクトとは何でしょうか?ここでは、next() メソッド(注:Python3 では next メソッド)を実装しているものと近似的に理解できます。

OK、先ほどの話に戻りましょう。上記のコードでは、for 文が Iterable オブジェクトか Iterator オブジェクトかを判断します。もし Iterable オブジェクトであれば、__iter__ メソッドを呼び出して Iterator オブジェクトを取得し、次に for ループは Iterator オブジェクト内の next()(注:Python3 では __next__)メソッドを呼び出してイテレーションを行います。イテレーションプロセスが終了するまで、StopIteration 例外が発生します。

さて、Generator について話しましょう#

前のコードを見てみましょう:

def generateList1(start,stop):
	for i in range(start,stop):
		yield i

if __name__=="__main__":
	c=generateList1(1,100000)
	for i in generateList1:
		print(i)

まず、Generator は実際には Iterator オブジェクトであることを確認しましょう。OK、上記のコードを見てみましょう。最初に forgenerateList1Iterator オブジェクトであることを確認し、次に next() メソッドを呼び出してさらにイテレーションを行います。OK、ここであなたは next() メソッドがどのように generateList1 をさらにイテレーションさせるのか疑問に思うでしょう。その答えは Generator の内蔵 send() メソッドにあります。もう一度コードを見てみましょう。

def generateList1(start,stop):
	for i in range(start,stop):
		yield i
if __name__=="__main__":
	a=generateList1(0,5)
	for i in range(0,5):
		print(a.send(None))

ここで何が出力されるべきでしょうか?答えは 0,1,2,3,4 です。結果は for ループで計算した結果と同じではありませんか?さて、私たちは次の結論を得ることができます。

Generator のイテレーションの本質は、内蔵の next() または __next__() メソッドを呼び出して、内蔵の send() メソッドを呼び出すことです。

Generator の内蔵メソッドについてのさらなる考察#

前述の結論を再確認しましょう。

Generator のイテレーションの本質は、内蔵の next() または __next__() メソッドを呼び出して、内蔵の send() メソッドを呼び出すことです。

今、例を見てみましょう。

def countdown(n):
    print "Counting down from", n
    while n >= 0:
        newvalue = (yield n)
        # If a new value got sent in, reset n with it
        if newvalue is not None:
            n = newvalue
        else:
            n -= 1

if __name__=='__main__':
	c = countdown(5)
	for x in c:
    	print x
    	if x == 5:
        	c.send(3)

さて、このコードの出力は何でしょうか?
答えは [5,2,1,0] です。これは非常に混乱しますね。心配しないでください。このコードの実行フローを見てみましょう。

コード実行フロー

簡単に言うと、send() 関数を呼び出すと、send(x) の値が newvalue に送信され、次の yield が出現するまで下に進みます。そして、値がプロセスの終了として返されます。その後、私たちの Generator はメモリの中で静かに眠り、次の send によって目覚めるのを待っています。

注 2:ある人が尋ねました。「ここで理解できないのは、c.send (3) は yield n が 3 を newvalue に返すのと同じですか?」良い質問です。実際、この問題は前のコードの実行図を見ればわかります。c.send(3) は最初に 3newvalue に割り当て、その後、プログラムは残りのコードを実行し、次の yield に出会うまで進みます。ここで、残りのコードを実行する際に、n の値はすでに 3 に変更されており、次に yield nreturn 3 に等しいことになります。その後、countdown という Generator はすべての変数の状態を凍結し、メモリの中で静かに待機し、次の next または __next__() メソッド、または send() メソッドによって目覚めるのを待ちます。

小さなヒント:直接 send() を呼び出す場合、最初は必ず send(None) を行ってください。そうしないと、Generator は本当にアクティブになりません。次の操作を行うことができません。

コルーチンについて#

まず、コルーチンの定義について、ウィキの一節を見てみましょう。

コルーチンは、特定の場所で実行を一時停止および再開するための複数のエントリポイントを許可することによって、非プリエンプティブマルチタスクのためにサブルーチンを一般化するコンピュータプログラムコンポーネントです。コルーチンは、協調タスク、例外、イベントループ、イテレータ、無限リスト、パイプなど、より一般的なプログラムコンポーネントを実装するのに適しています。
ドナルド・クヌースによれば、コルーチンという用語は 1958 年にメルビン・コンウェイによって造られ、彼がアセンブリプログラムの構築に適用したことに由来します。[1] コルーチンの最初の公表された説明は、1963 年に登場しました。

簡単に言うと、コルーチンはスレッドよりも軽量なモデルであり、起動と停止のタイミングを自分で制御できます。Python にはコルーチンという概念に特化したものはありませんが、一般的に Generator を特別なコルーチンとして扱います。考えてみてください。私たちは next または __next__() メソッド、または send() メソッドを使って Generator を起こし、指定したコードを実行した後、Generator は戻り、すべての状態を凍結します。これは私たちを非常に興奮させるのではないでしょうか!!

Generator に関する宿題#

今、私たちは二分木の後順遍歴を行います。この文章を読んでいる神々は、無思考でこれを書けるはずです。まずはコードを見てみましょう:

class Node(object):
    def __init__(self, val, left, right):
        self.val = val
        self.left = left
        self.right = right

def visit_post(node):
    if node.left:
        return visit_post(node.left)
    if node.right:
        return visit_post(node.right)
    return node.val

if __name__ == '__main__':
    node = Node(-1, None, None)
    for val in range(100):
        node = Node(val, None, node)
    print(list(visit_post(node)))

しかし、私たちは再帰の深さが深すぎると、スタックオーバーフローまたは Python の取引失敗が発生することを知っています。OK、Generator の力で、あなたのプログラマーの安全を守ります。コードを直接見てみましょう:

def visit_post(node):
    if node.left:
        yield node.left
    if node.right:
        yield node.right
    yield node.val

def visit(node, visit_method):
    stack = [visit_method(node)]
    while stack:
        last = stack[-1]
        try:
            yielded = next(last)
        except StopIteration:
            stack.pop()
        else:
            if isinstance(yielded, Node):
                stack.append(visit_method(yielded))
            elif isinstance(yielded, int):
                yield yielded

if __name__ == '__main__':
    node = Node(-1, None, None)
    for val in range(100):
        node = Node(val, None, node)
    visit_generator = visit(node, visit_method=visit_post)
    print(list(visit_generator))

見た目は非常に複雑ですね?心配しないでください。宿題として、皆さんはコメントで私にメッセージを残すことができます。私たちは一緒に Python の取引を行いましょう。

参考リンク#

1.あなたの Python を向上させる:‘yield’と‘Generators(生成器)’の説明
2.yield の力
3.http://my.oschina.net/1123581321/blog/160560
4.Python のイテレータが必ず__iter__メソッドを実装する理由(イテレータに関するもので、理解を容易にするために、いくつかのことを簡略化しました。具体的にはこの問題の高評価の回答を参照してください)

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。