パート4: Twisted で詩を

最初の Twisted クライアント

Twisted はサーバを書くために使われることが多いのですが、クライアントの方が簡単ですし、できるだけ簡単なところから始めていくことにしましょう。最初の詩のクライアントを Twisted を使って書いてみましょう。ソースコードは twisted-client-1/get-poetry.py にあります。その前にサーバを立ち上げておきましょう。

python blocking-server/slowpoetry.py --port 10000 poetry/ecstasy.txt --num-bytes 30
python blocking-server/slowpoetry.py --port 10001 poetry/fascination.txt
python blocking-server/slowpoetry.py --port 10002 poetry/science.txt

そうしたら、次のようにしてクライアントを実行してみてください。

python twisted-client-1/get-poetry.py 10000 10001 10002

以下のような出力を得られるでしょう。

Task 1: got 60 bytes of poetry from 127.0.0.1:10000
Task 2: got 10 bytes of poetry from 127.0.0.1:10001
Task 3: got 10 bytes of poetry from 127.0.0.1:10002
Task 1: got 30 bytes of poetry from 127.0.0.1:10000
Task 3: got 10 bytes of poetry from 127.0.0.1:10002
Task 2: got 10 bytes of poetry from 127.0.0.1:10001
...
Task 1: 3003 bytes of poetry
Task 2: 623 bytes of poetry
Task 3: 653 bytes of poetry
Got 3 poems in 0:00:10.134220

Twisted を使っていない非同期クライアントと一緒ですね。本質的に全く同じことをしていますので驚くようなことではありません。どのように動作しているかを理解するためにソースコードを見ていきましょう。これから議論していくソースコードを確認できるようにクライアントのファイルをエディタで開いてください。

注意:パート1で述べたように、いくつかの非常に低レベルな API を使うことから Twisted の使い方を学習していきます。このようにすることで、Twisted の抽象化レイヤを回避し、内側から外側 (inside out) に向かって Twisted のことを学んでいけます。しかしこのことは、始めに学習するたくさんの API は実際のコードを書くときにはあまり使わない、ということを意味します。最初の方のプログラムは練習用の課題として記憶に留めておくだけで構いません。製品レベルのソフトウェアの書き方の例ではありません。

Twisted クライアントは PoetrySocket オブジェクトの集合を生成することから始めます。PoetrySocket は実際のネットワークソケットを作り、サーバに接続し、ノンブロッキングモードに切り替えることで自身を初期化します。

self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect(address)
self.sock.setblocking(0)

以降では、ソケットを直接扱うような抽象化のレベルに出会うことはなくなるでしょう。でも、今のところはまだ必要です。ネットワーク接続を確立した後では、 PoetrySocket は自分自身を addReader メソッドを介して reactor に渡します。

# tell the Twisted reactor to monitor this socket for reading
from twisted.internet import reactor
reactor.addReader(self)

このメソッドは、データが来るかを監視していて欲しいファイルディスクリプタを Twisted に渡します。ファイルディスクリプタとコールバックではなくオブジェクトを Twisted に渡しているのは何故でしょう?そして、Twisted は間違いなく詩に特有のコードを持ち合わせていないのに、私たちが渡したオブジェクトの扱い方をどうやって知るのでしょうか?私を信じてください。確認しましたから。 twisted.internet.interfaces モジュールを開いて、私に付いてきてくださいね。

Twisted のインターフェイス

Twisted には interfaces と呼ばれるサブモジュールがたくさんあります。それぞれが Interface クラスの集まりを定義しています。バージョン 8.0 では、Twisted は zope.interface をそれらのクラスの基底クラスとして使っていますが、このパッケージの詳細は私たちにとっては取り立てて重要ではありません。あなたがまさに目にしてしているもののように、私たちが興味あるのは Twisted 自身 におけるインターフェイスのサブクラスなのです。

インターフェイスの主な目的のひとつはドキュメントです。Python プログラマなら間違いなく ダックタイピング に慣れているでしょう。原則としてオブジェクトの型はクラス階層における位置ではなく外部に公開されたインターフェイスによって決定される、ということです。したがって、同じ公開インターフェイスを表す二つのオブジェクト (アヒルのように歩き、アヒルのように鳴く、というやつです) は、ダックタイピングが考慮されている限り、同じようなものなのです (a duck!)。とはいえ、インターフェイスではアヒルのように歩くとは何たるかを特定する方法はいくぶん定式化されています。

twisted.internet.interfaces のソースコードを addReader メソッドの定義まで進めてください。 IReactorFDSet インターフェイスで宣言されており、次のようになっているでしょう。

def addReader(reader):
    """
    I add reader to the set of file descriptors to get read events for.

    @param reader: An L{IReadDescriptor} provider that will be checked for
                   read events until it is removed from the reactor with
                   L{removeReader}.

    @return: C{None}.
    """

IReactorFDSet は Twisted の reactor が実装するインターフェイスの内のひとつです。すべての Twisted の reactor には上記の docstring で説明されているように動く addReader と呼ばれるメソッドがあります。メソッド宣言には self 引数がありません。公開インターフェイスを宣言することにしか関与せず、 self 引数は実装の一部 (つまり、呼び出し側が self を明示的に渡さない、ということです) であるからです。インターフェイスオブジェクトは決してインスタンス化されませんし、現実の実装の基底クラスとして使われることもありません。

  • ノート1: 技術的なことをいうと、 IReactorFDSet はファイルディスクリプタを待ち受けるような reactor によって実装されているだけです。私が知る限りでは、現在利用可能な全ての reactor の実装がそうです。

  • ノート2: インターフェイスはドキュメント以外にも活用できます。 zope.interface モジュールは、クラスがひとつ以上のインターフェイスを実装することを明示的に宣言できるようにし、実行時にそれらの宣言を検証する機構を提供します。 適合 (adaptation) の概念もサポートします。あるインターフェイスを直接にはサポートしていないかもしれないオブジェクトにそのインターフェイスを動的に提供することです。しかし、より進んだユースケースには踏み込みません。

  • ノート3: インターフェイスと 抽象基底クラス の類似性に気付いたでしょうか。 最近になって Python の言語機構に加えられたものです。ここではその類似性と違いについて考えを進めませんが、 Glyph の エッセイ を読んでみるのも良いでしょう。 彼は Twisted プロジェクトの創始者であり、そのエッセイは核心に触れるものです。

上述の docstring によれば、 addReaderreader 引数は IReadDescriptor インターフェイスを実装しているべきです。このため、 PoetrySocket オブジェクトもそうしなくてはいけません。 この新しいインターフェイスを見つけるためにモジュールのソースコードをスクロールしていくと、次の記述に出会います。

class IReadDescriptor(IFileDescriptor):

    def doRead():
        """
        Some data is available for reading on your descriptor.
        """

PoetrySocket オブジェクトの doRead の実装も見つかるでしょう。Twisted の reactor に呼ばれたときはいつでも非同期にソケットからデータを読み込みます。このため、 doRead は実際のところコールバックです。しかし、Twisted に直接渡すのではなく、 doRead メソッドと一緒にオブジェクト内で渡します。これは Twisted フレームワークではよくある書き方です。関数を渡すのではなく、所定のインターフェイスを実装したオブジェクトを渡します。この方法だと、単一の引数で関連するコールバック (インターフェイスで定義されているメソッド) の集合を渡せるようになります。また、オブジェクトに保存された共有状態を介してコールバック同士でお互いに通信させられます。

それでは、 PoetrySocket オブジェクトで実装されているその他のコールバックは何でしょうか? IReadDescriptorIFileDescriptor の子クラスであることに注意してください。 IReadDescriptor を実装している全てのオブジェクトは IFileDescriptor も実装しなくてはいけない、ということです。ソースコードをもう少しスクロールしていくと、次の記述があります。

class IFileDescriptor(ILoggingContext):
    """
    A file descriptor.
    """

    def fileno():
        ...

    def connectionLost(reason):
        ...

docstring を示しましたが、これらのコールバックの目的は名前からして明確です。 fileno は着目しているファイルディスクリプタを返すべきですし、 connectionLost は接続を閉じたときに呼ばれます。そして、 PoetrySocket オブジェクトもこうしたメソッドを実装していることが分かりますね。

最後に、 IFileDescriptorILoggingContext を継承しています。ここではこれ以上は述べませんが、 logPrefix コールバックを実装する必要があるのはこのためです。 interfaces モジュールで詳しいことを確認できます。

NOTE: doRead はソケットが閉じられたときを示す特殊な値を返していることに気付いたかもしれません。 どうすれば分かるでしょうか?基本的には、これなしでは動作しませんでしたし、何をしているかを確認するために同じインターフェイスの Twisted 内の実装をチラッと見ました。 これについて腰を据えて学習したいかもしれません。ソフトウェアの文書はたまには間違っていたり不完全なこともあります。 たぶん、あなたがそのショックから立ち直ったとき、私はパート5を書き終えているでしょう。

コールバックについてもっと詳しく

Twisted を使った新しいクライアントは元々の非同期クライアントに極めて近い状態になりました。どちらのクライアントもそれぞれのソケットに接続し、そこから (非同期に) データを読み込みます。大きな違いは、Twisted のクライアントは select ループを必要としないことです。Twisted の reactor を代わりに使いますから。

doRead コールバックは最も重要なもののひとつです。ソケットから読み込み可能なデータがあると、Twisted はそれを呼び出します。図7にその処理の様子を表します。

_images/p04_reactor-doread.png

図7: doRead コールバック

コールバックが呼び出される度に、最大限のデータを読み込み、ブロックしないで止まります。パート3で述べたように、おかしな挙動でも (ブロックが必要なくても) Twisted は私たちのコードを止めません。記述した通りに実行でき、起こったことを確認できます。Twisted クライアントと同じディレクトリに、 twisted-client-1/get-poetry-broken.py と呼ばれる壊れたクライアントがあります。このクライアントはふたつの例外を投げる点が、これまで見てきたものと異なります。

  1. 壊れたクライアントはソケットをノンブロッキングにしません。

  2. doRead コールバックはソケットが閉じるまでバイト列を (たぶんブロックしながら) 読み続けるだけです。

それでは、次のようにして壊れたクライアントを実行させてみましょう。

python twisted-client-1/get-poetry-broken.py 10000 10001 10002

こんな感じの出力になるでしょう。

Task 1: got 3003 bytes of poetry from 127.0.0.1:10000
Task 3: got 653 bytes of poetry from 127.0.0.1:10002
Task 2: got 623 bytes of poetry from 127.0.0.1:10001
Task 1: 3003 bytes of poetry
Task 2: 623 bytes of poetry
Task 3: 653 bytes of poetry
Got 3 poems in 0:00:10.132753

タスクの順番がちょっと違うことを除けば、これは元々のブロッキングクライアントのように見えます。というよりは、壊れたクライアントはブロッキングクライアントであるからに他なりません。コールバック内でブロッキングの recv 呼び出しを使うことによって、非同期な Twisted プログラムを同期版に変更しました。ですから、非同期の利点が全くなく、 select ループの複雑さに向き合うことになりました。

Twisted のようなイベントループが提供する種類のマルチタスクは cooperative です。Twisted はファイルディスクリプタへの読み書きの準備ができたときに私たちに知らせてくれますが、ブロッキングしない程度の量のデータしか転送しないようにうまく振舞わなくてはいけません。そして、他の種類のブロッキングコール、 os.system のようなもの、を避けなくてはいけません。さらに、(CPU に影響を受ける) 長時間の計算を必要とするタスクがあると、それを小さなチャンクに分割するのは私たちの仕事になります。入出力のタスクを可能な限り進められるようにするためです。

壊れたクライアントがそれでも動作する、ということには意味があることに気をつけてください。きちんと全ての詩をダウンロードしますね。非同期入出力の効率性の利点を享受できない、というだけです。それでも、壊れたクライアントは元々のブロッキングクライアントより非常に早く動作することに気付くかもしれません。壊れたクライアントはプログラム開始時点で全てのサーバに接続するためです。サーバは即座にデータを送り始めて、たとえ (限界まで) 読み込めなくても OS は入力データのいくらかをバッファリングしますので、ブロッキングクライアントは一度にひとつのサーバからしかデータを読み込めませんが効率的に他のサーバからデータを受け取ります。

しかし、この「トリック」は短い詩のような少量のデータにしか機能しません。もし、たとえば、2000万語からなる、あるハッカーが世界最高の Lisp インタープリタを書いて本当の愛を勝ち取るという挑戦を綴った大作 (訳注:原文では epic sagas) を三つダウンロードしていたら、オペレーティングシステムのバッファはすぐに満杯になってしまい、壊れたクライアントは元々のブロッキングのものに比べて恐ろしく非効率的になっていたことでしょう。

まとめ

Twisted を使った最初の詩のクライアントに関してこれ以上述べることはありません。詩を待っている PoetrySockets がなくなったら connectionLost コールバックが reactor をシャットダウンさせる、ということには気をつけた方がよいかもしれません。プログラム内では詩をダウンロードする以外のことをやっていないように思われますのでのでそれほど大したテクニックではありませんが、より低レベルの reactor の API、 removeReadergetReaders 、を使っています。

ここでのクライアントの実装に使った Reader の API と同様に Writer もあります。これは、データを送り出す 間に監視しておきたいファイルディスクリプタに対して想像通りに動作します。もっと詳しいことは interfaces ファイルで確認してください。読み込みと書き出しを別々の API にしている理由は、 select の呼び出しが二種類のイベント (ファイルディスクリプタは読み書きのそれぞれで有効になる) を区別しているためです。もちろん、同じファイルディスクリプタで両方のイベントを待ち受けることもできます。

パート5: もっと Twisted の詩を”では、もう少し高次元の抽象化を使って Twisted による詩のクライアントの二つ目のバージョンを記述していきます。その過程で、Twisted のインターフェイスと API をもう少し学んでいきます。

おすすめの練習問題

  1. サーバへの接続に失敗したときにプログラムがクラッシュしないよう直してみましょう。

  2. 指定された時間で詩が終わらないようならクライアントにタイムアウトさせるようにするために callLater を使ってください。詩が時間内に終わるようならタイムアウトをキャンセルできるように、 callLater の戻り値を読み取ってください。