パート17: 「コールバック」ではない方法¶
はじめに¶
このパートではコールバックに主眼を置き直します。
Twisted で generators を使ってコールバックを記述するもうひとつのテクニックを紹介しましょう。
このやり方がどうやって動くのかを示し、純粋な Deferred
を使う場合と照らし合わせてみます。
最後に、このテクニックを使って詩のクライアントのひとつを書き直しましょう。
とはいえ、なんでこの手法がコールバックを生成するのにふさわしいかを理解するために、まずはジェネレータの動作を復習しておきましょうか。
ジェネレータに関する簡単な復習¶
ご存知のように、Python のジェネレータは関数内で yield
を使って生成できる “restartable function” (TODO: 公式ドキュメントの和訳を見ておく) です。
こうすることで、ある関数は、一連のステップ内で実行するために利用できる iterator
(イテレータ) を返す、ジェネレータ関数になります。
イテレータのそれぞれのサイクルは関数を再始動させ、次の yield
に達するまで処理を続けます。
ジェネレータ (と、イテレータ) はしばしば lazily-created sequences of values (TODO: 日本語を調べる/考える) を表現するために使われます。 inline-callbacks/gen-1.py にあるサンプルコードを見てみましょう。
def my_generator():
print 'starting up'
yield 1
print "workin'"
yield 2
print "still workin'"
yield 3
print 'done'
for n in my_generator():
print n
ここでは、ジェネレータに 1, 2, 3 というシーケンスを生成させています。
コードを実行させてみると、ジェネレータの print
文が for
ループ内の print
文にそれぞれのループで織り交ぜられていることが分かるでしょう。
ジェネレータ自身を生成することで、このコードをもっと明示的にすることもできます。 (inline-callbacks/gen-2.py):
def my_generator():
print 'starting up'
yield 1
print "workin'"
yield 2
print "still workin'"
yield 3
print 'done'
gen = my_generator()
while True:
try:
n = gen.next()
except StopIteration:
break
else:
print n
シーケンスとして考えてみると、ジェネレータは後続の値を取得するためのオブジェクトにすぎません。 しかし、ジェネレータ自身の観点から考えてみることもできます。
ジェネレータ関数はループによって呼び出されるまで動き出しません (
next
メソッドを使います)。ジェネレータ関数が動き出すと、ループに処理を返すまで動き続けます (
yield
を使います)。ループが他のコード (
print
文のように) を実行しているときは、ジェネレータは動作していません。ジェネレータが動作しているときは、ループは動作していません (ジェネレータを待つためにブロックされています)。
ジェネレータがループに制御を委譲 (
yield
) すると、ジェネレータが再度実行されるまで、任意の時間が渡されます (さらに任意の量のコードが実行されるかもしれません)。
非同期システムにおけるコールバックの動作とそっくりですね。
while
ループを reactor、ジェネレータを yield
文で分割された一連のコールバックとみなせます。
すべてのコールバックは同じローカル変数の名前空間を共有し、その名前空間はあるコールバックから次のコールバックに引き継がれる点も見逃せません。
さらに、一度に複数のジェネレータを有効にでき (inline-callbacks/gen-3.py
の例をみてください)、それぞれのコールバックはお互いに混ぜこぜになります。
ちょうど、Twisted のようなシステムで独立した非同期タスクを持てるようにです。
それでもいくつか見逃していることもあります。
コールバックは reactor から呼ばれるだけではありませんし、情報を受け取ることもできます。
遅延オブジェクトのチェーンの一部であるとき、コールバックは Python の単一の値として結果を受け取るか、 Failure
としてエラーを受け取ります。
Python 2.5 からは inline-callbacks/gen-4.py で示すように、 ジェネレータを再始動させるときに情報を送信できるような方法に拡張されました。
class Malfunction(Exception):
pass
def my_generator():
print 'starting up'
val = yield 1
print 'got:', val
val = yield 2
print 'got:', val
try:
yield 3
except Malfunction:
print 'malfunction!'
yield 4
print 'done'
gen = my_generator()
print gen.next() # start the generator
print gen.send(10) # send the value 10
print gen.send(20) # send the value 20
print gen.throw(Malfunction()) # raise an exception inside the generator
try:
gen.next()
except StopIteration:
pass
Python 2.5 かそれ以降のバージョンでは、 yield
文は値の評価式です。
そして、ジェネレータを再始動させるコードは next
ではなく send
メソッドを使うという決定もできます (next
を使うと、その値は None
です)。
さらに、 throw
メソッドを使って、ジェネレータの「内側」から任意の例外を投げることができます。
なんて素晴らしいんでしょう!
インラインコールバック¶
ジェネレータに値と例外を send
することと throw
することについて復習したことが分かると、ジェネレータを一連のコールバックとして想定できます。
遅延オブジェクトに含まれるものかのように、それは結果か失敗のどちらかを受け取ります。
コールバックは yield
によって分割され、それぞれの yield
の評価値は次のコールバックへの結果となります (もしくは yield
が例外を投げると “failure” になります)。
図35はその対応を示します。
一連のコールバックが遅延オブジェクト内でチェーンとしてまとめられると、それぞれのコールバックは結果をひとつ前から受け取ります。
ジェネレータを使ってやってみるのは簡単そうですね。ジェネレータの前回の実行分から受け取った値を send
し (yield
した値ですね)、
それを使って次回は再始動させるだけです。
しかし、ちょっと馬鹿馬鹿しくも思えます。
ジェネレータは開始時にその値を計算するのに、なんで送り返すなんてことをするのでしょう?
次回に必要ならばジェネレータは値を変数に保存しておくこともできるでしょう。
何が重要なのでしょうか?
“パート13: Deferred と行こう”で学んだことを思い出してください。遅延オブジェクト内のコールバックは遅延オブジェクト自身を返すことができましたよね。 この場合、外側の遅延オブジェクトは内側の遅延オブジェクトが開始するまで止まっていますので、外側の遅延オブジェクトのチェーンでの次のコールバック (もしくはエラー用コールバック) は、内側の遅延オブジェクトからの結果 (もしくは失敗) を引数として呼び出されます。
それでは、ジェネレータが通常の Python の値ではなく遅延オブジェクトを yield
した場合を想像してみてください。
ジェネレータは停止 (“paused”) され、自動化されます。
ジェネレータはいつも、すべての yield
文の後で明示的に再始動されるまで停止します。
ですから、遅延オブジェクトが発火するまで、ジェネレータの再始動を遅らせることが可能です。
このとき私たちは値を send
する (遅延オブジェクトが成功したら) か、例外を throw
する (遅延オブジェクトが失敗したら) かのどちらかです。
これによってジェネレータを純粋な非同期コールバックのシーケンスにしていますし、そしてこれこそが twisted.internet.defer 内の inlineCallbacks 関数の背後にある考え方なのです。
inlineCalbacks¶
inline-callbacks/inline-callbacks-1.py にあるプログラム例について考えてみましょう。
from twisted.internet.defer import inlineCallbacks, Deferred
@inlineCallbacks
def my_callbacks():
from twisted.internet import reactor
print 'first callback'
result = yield 1 # yielded values that aren't deferred come right back
print 'second callback got', result
d = Deferred()
reactor.callLater(5, d.callback, 2)
result = yield d # yielded deferreds will pause the generator
print 'third callback got', result # the result of the deferred
d = Deferred()
reactor.callLater(5, d.errback, Exception(3))
try:
yield d
except Exception, e:
result = e
print 'fourth callback got', repr(result) # the exception from the deferred
reactor.stop()
from twisted.internet import reactor
reactor.callWhenRunning(my_callbacks)
reactor.run()
この例を実行してみると、ジェネレータが最後まで実行され、reactor を停止させることが分かりますね。
この例は inlineCallbacks
関数のいくつかの側面を表しています。
ひとつめに、 inlineCallbacks
はデコレータであり、つねにジェネレータ関数、つまり yield
を使う関数をデコレートします。
inlineCallbacks
全体の目的は、上述したスキームに沿って、ジェネレータを一連の非同期なコールバックにしてしまうことです。
ふたつめに、 inlineCallbacks
でデコレートされた関数を呼び出すと、 next
か send
あるいは throw
自身を呼び出す必要がありません。
細かいことはデコレータが面倒をみてくれますので、ジェネレータが最後まで実行されることを保証してくれます (例外を投げないと仮定してください)。
みっつめとして、ジェネレータから遅延オブジェクトではない値を yield
すると、 yield
の結果としてそれと同じ値を伴って即座に再始動されます。
そして最後に、ジェネレータから遅延オブジェクトを yield
すると、それが発火されるまで再始動されません。
遅延オブジェクトが発火すると、 yield
の結果は遅延オブジェクトからの値に過ぎません。
失敗した場合は yield
文が例外を投げます。
ここでの例外は Failure
ではなく普通の Exception
であることに注意してください。
yield
文を try/except
節で囲むことで例外を捕まえることができます。
この例では、短い時間の後で遅延オブジェクトを発火させるために callLater
を使っているに過ぎません。
コールバックチェーンの中にノンブロッキングな遅延を詰めるにはお手軽な方法ですが、通常は、ジェネレータから呼び出される他の非同期操作 (つまり get_poetry
) が返す遅延オブジェクトを yield
させるでしょうね。
ここまでで inlineCallbacks
でデコレートされた関数がどのように動き出すか分かりましたが、実際にそれを呼び出して得られる戻り値は何でしょうか?
お考えのように、遅延オブジェクトです。
いつジェネレータが停止するのか正確には分かりませんので (複数個の遅延オブジェクトを yield
するかもしれません)、デコレートされた関数自身は非同期であり、遅延オブジェクトが適切な戻り値なのです。
戻り値である遅延オブジェクトは、ジェネレータが yield
するかもしれない遅延オブジェクトのひとつではないことに注意してください。
むしろ、ジェネレータが完全に動作を完了した後でのみ発火する (もしくは例外を投げる) 遅延オブジェクトです。
ジェネレータが例外を投げると、戻り値である遅延オブジェクトは Failure
でラップされた例外を引数としてエラー用のコールバックチェーンを発火させます。
しかし、ジェネレータに通常の値を返してもらいたかったら、 defer.returnValue
関数を使って “return” させなくてはなりません。
通常の return
文のように、ジェネレータを停止させるでしょう (実際は特別な例外を投げます)。
inline-callbacks/inline-callbacks-2.py の例は両方の可能性を表現しています。
クライアント 7.0¶
新しいバージョンの詩のクライアントで動作するように inlineCallbacks
を配置してみましょう。
コードは twisted-client-7/get-poetry.py にあります。
twisted-client-6/get-poetry.py のクライアント 6.0 と比較したくなるでしょう。
関連のある変更点は poetry_main にあります。
def poetry_main():
addresses = parse_args()
xform_addr = addresses.pop(0)
proxy = TransformProxy(*xform_addr)
from twisted.internet import reactor
results = []
@defer.inlineCallbacks
def get_transformed_poem(host, port):
try:
poem = yield get_poetry(host, port)
except Exception, e:
print >>sys.stderr, 'The poem download failed:', e
raise
try:
poem = yield proxy.xform('cummingsify', poem)
except Exception:
print >>sys.stderr, 'Cummingsify failed!'
defer.returnValue(poem)
def got_poem(poem):
print poem
def poem_done(_):
results.append(_)
if len(results) == len(addresses):
reactor.stop()
for address in addresses:
host, port = address
d = get_transformed_poem(host, port)
d.addCallbacks(got_poem)
d.addBoth(poem_done)
reactor.run()
新しいバージョンでは、 inlineCallbacks
ジェネレータ関数である get_transformed_poem
は、詩を取得することと、その後に変換を適用することの両方に責任を持ちます (変換サービス経由で)。
どちらの操作も非同期ですから、それぞれの時点で遅延オブジェクトを渡し、(暗黙的に) その結果を待ちます。
クライアント 6.0 では、変換に失敗すると元の詩を返すだけです。
ジェネレータ内では非同期なエラーを処理するために try
/except
節を使えることを確認しましょう。
新しいクライアントも以前と同じ方法でテストできます。 まずは変換サーバを起動させましょう。
python twisted-server-1/tranformedpoetry.py --port 10001
続いて二つの詩のサーバを起動させます。
python twisted-server-1/fastpoetry.py --port 10002 poetry/fascination.txt
python twisted-server-1/fastpoetry.py --port 10003 poetry/science.txt
それでは新しいクライアントを実行させましょう。
python twisted-client-7/get-poetry.py 10001 10002 10003
クライアントがエラーをどのように処理するかを確認するために、ひとつかそれ以上のサーバを停止させてみてください。
議論¶
Deferred
オブジェクトのように、 inlineCallbacks
関数は非同期コールバックを構成する新しい方法を提示してくれます。
そして遅延オブジェクトがあるときと同じように、 inlineCallbacks
はゲームのルールを変えません。
特に、コールバックは一度にひとつしか動作しませんし、reactor から呼び出されます。
これまでと同じようにインラインコールバックからのトレースバックを出力することで、このことを確認できます。
inline-callbacks/inline-callbacks-tb.py のサンプルスクリプトにある通りです。
このコードを実行させてみると、トレースバックのトップに reactor.run()
があり、途中にたくさんのヘルパー関数、それから一番下に私たちのコールバックがあります。
図29を適応させることができます。
これは、 inlineCallbacks
ジェネレータが遅延オブジェクトを yield
するとき何が起こるかを見せることにより、遅延オブジェクト内で、あるコールバックがもうひとつの遅延オブジェクトを返すときに起こることを説明してくれます。
図36を見てください。
描かれている考え方は一緒ですので、両方の場合で同じ図が活躍してくれていますね。— ある非同期操作がもう一つを待つことになります。
inlineCallbacks
と遅延オブジェクトはこれと同じ問題の多くを解決するのに、どちらか片方を選ぶのは何故でしょう?
inlineCallbacks
の潜在的な利点はいくつかあります。
コールバックは名前空間を共有しますので、追加の状態を渡す必要がありません。
コールバックの順番を簡単に確認できます。上から下に実行するだけです。
個別のコールバックの関数宣言と暗黙的な呼び出し制御がありませんので、タイピング量は概して少なくて済みます。
エラーは親しみのある
try
/except
節で処理されます。
落とし穴も潜んでいます。
ジェネレータの中のコールバックを個別に呼び出すことはできません。このことは、コードの再利用を難しくしてしまいます。 遅延オブジェクトを使うと、遅延オブジェクトを構築するコードは任意のコールバックを任意の順番で追加できます。
ジェネレータのコンパクトな形式は非同期コールバックも含まれているという事実をぼんやりさせてしまいます。 通常の連続的な関数の出現と見た目は似ていますが、ジェネレータは全く異なる振る舞いをみせます。
inlineCallbacks
関数は非同期なプログラミングモデルの学習を回避するための方法ではありません。
すべてのテクニックを持ってすれば、選択に必要な経験はこれまでの慣習が提供してくれるでしょう。
まとめ¶
このパートでは、 inlineCallbacks
デコレータに関することと、Python ジェネレータの形式で非同期なコールバックのシーケンスを表現する方法ついて学びました。
“パート18: Deferreds En Masse” では、 並行 (“parallel”) な非同期操作の集合を管理する方法を学んでいきましょう。
おすすめの練習問題¶
なぜ
inlineCallbacks
関数は複数形なのでしょうか?inlineCallbacks とそのヘルパー関数である _inlineCallbacks の実装を学習してください。 “the devil is in the details” というフレーズを考えてみましょう。
N
個のyield
文を持つジェネレータにはいくつのコールバックが含まれるでしょうか? ループやif
文は存在しないと仮定してください。詩のクライアント 7.0 は一度にみっつのジェネレータを実行することがあります。 概念的には、お互いに混ざり合う組み合わせはいくつあるでしょう? 詩のクライアントでの呼び出され方と
inlineCallbacks
の実装を考えてみると、実際に可能な組み合わせはいくつでしょう?クライアント 7.0 にある
got_poem
コールバックをジェネレータ内に移動させてください。同様に
poem_done
コールバックをジェネレータ内に移動させてください。 注意!何があっても reactor が終了できるように、全ての失敗する場合を処理してください。 reactor を停止させるために、どのようにして出来上がったコードを遅延オブジェクトを使ったものと比べましょうか?while
ループ内のyield
文を含むジェネレータは、概念的に無限数列を表現できます。inlineCallbacks
でデコレートされたそのようなジェネレータは何を表すのでしょうか?