decoratorの挙動をちょっと変更しつつ適用する

メモ。

motivatation

最近、fanstaticを使っている時にデコレータを利用したいと思った。
(fanstaticはWSGIアプリのResponseの中を覗いてhead要素の合間に定義しておいたresource(js,cssなど)を読み込むタグを追加してくれるライブラリ((他にも機能はある)))
たいていの場合はjqueryとbackbone.jsとunderscore.jsを利用。そして、時々、ある関数だけjquery.toolsを使いたいとおもったりなどした。

このような時に以下のような機能が欲しいと思った。

  • 主として利用する機能はデフォルトで提供して欲しい
  • 時々、上で定義したデフォルトに+αで挙動を変更したい(具体的には、サポートするresourceを追加したい)

1つ目は、デコレータファクトリーのようなもの作れば解決できるだろうと思った。
2つ目は、作ったデコレータ同士を合成したり、機能を追加できれば良いだろうと思った。

code

こんな感じ。
特にfanstaticにこだわることはないので、今回はActionDecoratorFactoryという名前にした。
このクラスが生成するデコレータは、デコレート対象の関数の戻り値が返えされる前に、定義しておいたアクションを呼べる。

class ActionDecoratorFactory(object):
    def __init__(self, fns=None):
        self.fns = fns or set()
        self._input = None
        self._output = None

    def add(self, fn):
        new = ActionDecoratorFactory(self.fns[:])
        new.fns.append(fn)
        return new

    def merge(self, other):
        new = ActionDecoratorFactory(self.fns[:])
        new.fns.extend(other.fns)
        return new

    def __call__(self, wrapped):
        def with_action(*args, **kwargs):
            self._input = (args,kwargs)
            self._output = wrapped(*args, **kwargs)
            self.do_action()
            return self._output
        return with_action

    def do_action(self):
        for f in self.fns:
            f(self)

ひとつだけ注意点がある。(あとで書く)

how to use

このように使う。テキトーな関数を用意。
作ったクラスに引数として渡す。

def print_foo(self):
    print "foo"

def print_boo(self):
    print "boo"

def print_bar(self):
    print "bar"

with_foo = ActionDecoratorFactory([print_foo])
通常の利用時には生成されたデコレータをそのまま使う。
@with_foo
def identity(x):
    return x
何か機能を追加したい時には

何か機能を追加したい時には、self.add(decorator)という形で呼ぶ。(chainはできないので注意)

@with_foo.add(print_boo)
def sq(x):
    return x * x

sq(10)
# foo
# boo
複数のデコレータを合成したい場合には、

複数のデコレータを合成したい場合には、self.merge(decoratorfactory)という形で呼ぶ。

boo_decorator = ActionDecoratorFactory([print_boo])
@boo_decorator.merge(ActionDecoratorFactory([print_foo, print_bar]))
def double(x):
    return 2*x

double(2)
# boo
# foo
# bar

注意点

self.addとself.mergeが新しいインスタンスを返しているところ。
こうしないと、add,mergeに使った副作用(機能の追加/合成)が、他のデコレータ対象にも伝搬してしまう。