emptypage.jp > Notes

Python でイベント指向のプログラミングを実現する

2010年4月16日公開
English version

ほかのプログラミング言語でちょっとよさげなアイデアが採用されているのを見ると、それを自分のお気に入りの言語でも実現したくなってくるものらしく、C# のイベント機構を Python でも実現できないかと考える人は少なくないようです。「c# event python」といったキーワードで検索すると、同様のことを試みたページが少なからず見つかります。以下もそれらと同様の試みのひとつですが、デスクリプタという Python の興味深い仕組みを使い、少し進んだ実装を目指します。

前提

C# のイベントの概念とそれがどう素敵なのかについての説明は省略します。

シンプルな実装

イベントをクラスとしてシンプルに実装すると、次のようなものになるでしょう。

最後の 3 行では、e.add(handler), e.remove(handler), e.fire(earg) をそれぞれ e += handler, e -= handler, e(earg) と書けるようにしています。詳しくは Python のドキュメントを参照してください。

このモジュールを利用するスクリプトは、例えば次のようになります。

実行結果は次の通り。

C:\...>python event_sample.py
foo!

利用者がイベント機構の意味と由来を承知していて、それに従ってコードを書くことが期待できるなら、これで十分な実装と言えます。しかし改良したいと思う点もあります。

  1. イベントクラスのインスタンスを __init__ メソッドの中で用意するので、Publisher クラスがどのようなイベントを発行するのか(また、そもそもイベント機構を持っているのか)がクラスの利用者からはわかりにくい。
  2. イベントハンドラの呼び出しで、self.evt_foo(self) のように、「第一引数に自分自身を呼び出す」という約束事を忘れないようにしなければならない。

これらはドキュメンテーションを工夫すれば乗り越えられるかもしれませんが、もうすこし考えてみます。

イベントを発行する時ではなく、登録時に発行元としてコンストラクタ (__init__) で渡してしまうという手も考えられます。しかしすぐにわかることですが、それは結局 self.evt_foo = eventEvent(self) のように、問題の場所を移動させているだけです。

イベントを発行する仕組みを備えた(たとえば EventPublisher といった名前の)基本クラスを作り、イベント機構を利用するクラスはこの基本クラスを継承してもらうという方法も考えられます。しかし、クラスの(場合によっては多重の)継承を強制するのは避けたいものです。できればもっと気楽に使いたい。

メソッドやプロパティを定義するように、クラスの情報としてイベントを定義できるような仕組みは、Python の言語仕様を改訂することでしか実現できないのでしょうか。それがそうでもないのです。Python の property がどのように実装されているのかを想像してみてください。

より使いやすく

上記の問題を解決する実装を以下に示します。

使い方

改良版 event.py のコードを見る前に、それを利用する側であるサンプルスクリプトのコードを見てください。イベントをより宣言的に、言語に組み込みの機能のように利用できています。

Publisher クラスの定義を見てください。イベントは、__init__ メソッドの中でなく、クラスに属するオブジェクトとして作成しています。このほうが、クラスがどのようなイベントを発生させるのかがわかりやすいし、コードを書くのも楽になります。event.Event クラスの引数にはそのイベントの説明を入れますが、これは help(Publisher) を実行したときの説明に使われます。

Publisher.evt_foo イベントは、foo メソッドを実行したときに発生するわけですが、そのイベントの発行方法は self.evt_foo() となっています。クラスのオブジェクトをメソッドのように呼び出しています。これで、後述しますが、イベントハンドラの第一引数にはイベントの発行元である Publisher クラスのインスタンスが渡されます(ほんとです)。シンプルな実装では、ここで発行元を渡すために引数に self を明示しなければなりませんでしたが、それが不要になっています。self.evt_foo() は、引数をひとつ付けて、self.evt_foo(earg) のように呼び出すこともできます。ハンドラにはふたつの引数をとる関数ないしメソッドを登録するのですが、前者の場合にはハンドラにはイベントの発行元インスタンスと None が、後者の場合では発行元インスタンスと呼び出し時に付けたこの earg が渡されることになります。

改良版のほうが、イベントが Python コードの一部としてよりなじんでいるように感じることと思います。

実行結果は次の通り(見た目は改良前と同じです)。

C:\...>python event_sample.py
foo!

実装

以下が改良版 event.py のコードです。行数は増えてますが、それでも 100 行もありません。なお、ダウンロード版の event.py は以下のコードを基にもう少し改良したものになっている場合があります。しかしその場合でも基本的な仕組みは変わりません。

見慣れない特殊メソッドが使われています。まず以下の解説を読んでいただいて、そのあとコードをじっくり見てください。

解説

改良版では、自身を引数に含めることなしに、インスタンスのメソッドのようにイベントを呼び出すことができています。これはどういうことなのか。Python の対話モードを使って event.py の挙動を調べてみます。

C:\...>python
Python 2.6.5 (r265:79096, Mar 19 2010, 21:48:26) [MSC v.1500 32 bit (Intel)] on
win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import event
>>> class C(object):
...   evt = event.Event('test')
...
>>> C.evt
<event.Event object at 0x00AF4A70>
>>> c = C()
>>> c
<__main__.C object at 0x00AF4A30>
>>> c.evt
<event.EventHandler object at 0x00AF4950>
>>> 

クラスのオブジェクトとして呼び出した C.evt と、インスタンスのオブジェクトとして呼び出した c.evt で、得られているものが違っているのがわかります。後者では、event.EventHandler というクラスのインスタンスが返っています。このクラスは、C クラスのインスタンス c と、C クラスで定義したイベント evt とを関連付ける役割をしています。event.EventHandler インスタンスには、呼び出し元のオブジェクトと、それに関連付けられているイベントについての情報が格納されています。次のようにしてそれを確認できます。

>>> c.evt.obj
<__main__.C object at 0x00AF4A30>
>>> c.evt.event
<event.Event object at 0x00AF4A70>
>>>

これは、Python のデスクリプタ (descriptor, 漢字で書くなら「記述子」) という仕組みを使って実現しています。デスクリプタというのは、あるクラス Foo のインスタンスが x.foo = Foo() のように代入されたとして、それが y = x.foo などと評価された時に、 Foo インスタンス (x.foo) の側から自身が属する x についての情報を知ることができるような仕組みです。呼び出された時の文脈を知る仕組みとも言えます。上記の event.Event クラスの例では、C.evt はこのデスクリプタを使って、呼び出された時に自身の属しているオブジェクトがクラスオブジェクト (C) なのかそのインスタンス (c) なのかを判別し、それによって返す結果を変えているのです。

改良版 event.py の Event クラスには、__get__ という特殊メソッドが定義されています。これがデスクリプタの正体です。C.evt が評価される時には、このメソッドが self.__get__(None, C) として呼び出されます。一方、c.evt が評価される時は self.__get__(c, C) として呼び出されます。メソッドの第一引数が None の時は自分自身 (event.Event インスタンス) を返します。第一引数がクラスのインスタンスだった場合は、自分自身とそのオブジェクトとを関連づけたオブジェクトを EventHandler(self, obj) として作成し、それを返しています。

Python の property, classmethodstaticmethod も、このデスクリプタを利用して実装されています(これらは C で実装されてますが、やっていることは同じです)。

デスクリプタについての詳細は、「参考文献」を参照してください。

むすび

イベント機構は、シンプルな実装ではどう工夫しても self.evt.fire(self) とか、self.evt = event.Event(self) のように、呼び出し元と引数、あるいは右辺と左辺に両方とも self が出てくるような「お膳立て」の必要なコードがどこかに残ってしまいます。デスクリプタを使った実装ではそれを取り除くことができています。できあがったモジュールは、デスクリプタについて知識がない人でも使い方さえ教えられれば理解でき、また余計な約束ごとなく利用できるものになっています。イベント機構の実装は、デスクリプタの使用が適しているケースのひとつではないかと思います。

デスクリプタが問題解決に適しているケースというのは多くないと思いますが、これはアイデア次第で Python の言語自身が拡張されたかのような効果を生む可能性を秘めた面白い仕組みだと思います。

参考文献

Python でのイベント機構実装について

デスクリプタについて

事項

Python 開発コミュニティ、Python でのイベント機構の実装や Python デスクリプタの詳細について解説記事やサンプルコードを公開されている方々に感謝します。

内容については正確さを心がけていますが、知識不足により間違ったことを書いてしまっているかもしれません。記事の内容によって生じた損害等について作者はその責任を負いません。ご了承ください。記事に関するご指摘やご意見ご感想はいつでも歓迎です。ゲストブックやメールなどでご連絡ください。

クリエイティブ・コモンズ・ライセンス この文書は、クリエイティブ・コモンズ・ライセンスの下でライセンスされています。また記事に掲載しているサンプルコードはすべてパブリックドメインに置きます。

履歴

2014-08-17
サンプル・コードを Gist に置いた。
2011-03-08
英語版を追加。
2010-11-16
参考文献を追加。
2010-04-16
公開。

Copyright 2010 Masaaki Shibata <mshibata at emptypage.jp>