2009年3月17日火曜日

pythonで、Objective-Cのカテゴリのように、あるクラスのソースを変更せずに任意のメソッドを追加する方法

■結論
すべてのクラスには__bases__という隠しフィールドがあるので、そこに任意のスーパークラスを作って追加する


■何がしたいか
たとえば以下のようなクラスがあります。
class Foo:
def do_something(self, x):
  print x
  return

このクラスのソースコードに指一本触れずに以下のメソッドを追加したい。
  def another_method(self, x, y):
  print x+y
  return

って将軍様が言っていたので、一⑨さんは考えました。


■チーン!ひらめいた!
まずは以下のようにスーパークラスになるクラスを作ります。
class SampleCategory:
def another_method(self, x, y):
  print x+y
  return

そしてここからがポイント。__bases__というTupleに、今作ったスーパークラスのtype型を追加します。
スーパークラスを宣言した後で・・・
from com.akisute.example import Foo
Foo.__bases__ += (SampleCategory,)

これだけです。
なぜかこれでうまくいくのがPythonのふしぎ。

実際のソースコードの例は以下のようになります(以下、拙作のプログラムから抜粋)
#!/usr/bin/env python
# encoding: utf-8
"""Login Authentication plugin.
Extends BasicController to give it an additional function for authentication
using Google account.
"""
import sys
import os

class LoginAuthPlugin:
    def is_login(self):
        """Returns True if requesting user is logged in."""
        #Do some work here
        return false

from gaeo.controller import BaseController
BaseController.__bases__ += (LoginAuthPlugin,)


JavaScriptのprototype書き換えにも似ていますね。
ただしprototype書き換えに比べて優れているのは、__bases__はTupleなので、簡単に好きなだけスーパークラスを追加することが出来ます。
(prototypeだと複数のスーパークラスを追加するのがきわめて困難)
from com.akisute.example import Foo
Foo.__bases__ += (SampleCategory,)
Foo.__bases__ += (AnotherCategory,)


■元ネタ
GAEOでpluginを読み込んでBaseControllerを拡張するときにこのテクが使われていました。
GAEO作った人は凄い。