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

ジェネレーターの深堀り#

ここで一つの疑問があるかもしれません。Python の開発者たちがこのような使用シーンのために特別に 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() メソッド(注:Python 3 では next メソッド)を実装しているものと近似的に理解できます。

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

さて、ジェネレーターについて話しましょう#

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

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 のイテレーションの本質は、内蔵の next() または __next__() メソッドを通じて内蔵の send() メソッドを呼び出すことです。

ここで一つの例を見てみましょう:

def countdown(n):
    print "Counting down from", n
    while n >= 0:
        newvalue = (yield n)
        # 新しい値が送信された場合、それで n をリセット
        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 に到達するまで進みます。ここで、残りのコードを実行する際に、yield n の前に n の値がすでに 3 に変更されているため、yield n はほぼ return 3 と同じです。その後、countdown という Generator はすべての変数の状態を凍結し、メモリの中で静かに待機し、次の next または __next__() メソッド、または send() メソッドの呼び出しを待ちます。

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

コルーチンについて#

まずコルーチンの定義を見てみましょう。ウィキからの引用です。

コルーチンは、特定の場所での実行を一時停止および再開するための複数のエントリポイントを許可することによって、非プリエンプティブマルチタスキングのためにサブルーチンを一般化するコンピュータプログラムのコンポーネントです。コルーチンは、協調タスク、例外、イベントループ、イテレーター、無限リスト、パイプなど、より一般的なプログラムコンポーネントの実装に適しています。
ドナルド・クヌースによれば、コルーチンという用語は 1958 年にメルビン・コンウェイによって造られ、アセンブリプログラムの構築に適用されました。最初に公開されたコルーチンの説明は 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 メソッドを実装する理由(イテレーターについて、理解を助けるために、いくつかの内容を簡略化しました。具体的にはこの質問の高評価の回答を参照してください)

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