- 出典:itsCoder の WeeklyBolg プロジェクト
- itsCoder ホームページ:http://itscoder.com/
- 著者:Manjusaka
- レビューア:allenwu,Brucezz
前書き#
今週は 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 にはxrange
という文があり、これは Generator オブジェクトを生成します。
Python 3 では、状況が少し変わりました。コミュニティはrange
とxrange
の分裂が面倒だと感じたため、これらを統合しました。したがって、Python 3 ではxrange
の構文糖が廃止され、range
のメカニズムもlist
ではなく Generator を生成するようになりました。
しかし、皆さんは考えたことがありますか?もし非常に大きなデータ量を生成したい場合、事前にデータを生成するのは非常に賢明ではありません。大量のメモリを消費します。そこで、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 はおおよそプロトコルとして理解できます。Object が Iterable かどうかを判断する方法は、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
文はイテレートするオブジェクトが Iterable か Iterator かを判断します。もし __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、上記のコードを見てみましょう。最初に for
は generateList1
が Iterator オブジェクトであることを確認し、次に 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)
はまず3
をnewvalue
に代入し、その後プログラムは残りのコードを実行し、次の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 メソッドを実装する理由(イテレーターについて、理解を助けるために、いくつかの内容を簡略化しました。具体的にはこの質問の高評価の回答を参照してください)