デコレータの実行を遅延させる仕組み。デコレートされた関数のテストがしやすくなったりする。
メタ情報を付与するだけのアノテーションのようにデコレータを使うことができそう。
pyramidのview_config関数で使用されている。
http://docs.pylonsproject.org/projects/venusian/en/latest/
使い方
-
デコレータ関数を定義するときに、vensuian.attachを呼び出す。
# theframework.py import venusian def jsonify(wrapped): def callback(scanner, name, ob): def jsonified(request): result = wrapped(request) return json.dumps(result) scanner.registry.add(name, jsonified) venusian.attach(wrapped, callback) return wrapped
-
1の関数をデコレータとして呼び出す
# theapp.py from theframework import jsonify @jsonify def logged_in(request): return {'result':'Logged in'}
jsonify関数の実態(callback関数)はまだ呼ばれていない。
-
スキャンする
import venusian import theapp class Registry(object): def __init__(self): self.registered = [] def add(self, name, ob): self.registered.append((name, ob)) registry = Registry() scanner = venusian.Scanner(registry = registry) scanner.scan(theapp)
この時点で、callback関数が呼ばれ、scanner.registryにjsonfiy関数でデコレートされたlogged_in関数が入ることになる。
カテゴリごとにわけてscanすることなどもできる。(ドキュメント参照)
実装(ざっくり)
attach
パッケージ
venusian/init.py
実装
def attach(wrapped, callback, category=None, depth=1):
""" Attach a callback to the wrapped object. It will be found
later during a scan. This function returns an instance of the
:class:`venusian.AttachInfo` class."""
wrappedの__venusian_callbacks__
メンバに、callback関数を登録する。
callback関数のホルダーにはCategoriesクラスを使っている。
# カテゴリ名をキー、コールバック関数のリストを値として持つクラス。
class Categories(dict):
def __init__(self, attached_to):
super(dict, self).__init__()
if attached_to is None:
self.attached_id = None
else:
self.attached_id = id(attached_to)
def attached_to(self, obj):
if self.attached_id:
return self.attached_id == id(obj)
return True
流れ
- sys._getframeしてwrappedが関数なのかクラスなのかなどを判定
- wrappedの
__venusian_callbacks__
メンバに値が設定されていなければ、Categoriesインスタンスを新たに生成し、設定。 - Categoriesインスタンスに対して、引数categoryをキーにcallbackを登録する。複数登録できるようlistになっている。
- Categoriesインスタンスやwrappedの情報を含んだAttachInfoクラスを生成して返す。
Scanner.scan
パッケージ
venusian/init.py
実装
class Scanner(object):
...
def scan(self, package, categories=None, onerror=None, ignore=None):
""" Scan a Python package and any of its subpackages. All
top-level objects will be considered; those marked with
venusian callback attributes related to ``category`` will be
processed.
...
attachされたオブジェクトに登録されたcallback関数を呼び出す。どれを対象とするかはcategoryやignoreで指定可能。
流れ
- packageに対してinspect.getmembersし、packageに所属するメンバーを取得。
-
各メンバーオブジェクトに対して内部関数invokeを適用。ここでcallbackが呼ばれる。
def invoke(mod_name, name, ob): """ mod_name: scan中のモジュール名 name: getmembersで得られたオブジェクト名 ob: nameに対応するオブジェクト """
- ignoreに指定されたオブジェクトでないかチェック。指定されたオブジェクトならreturn。
-
__venusian_callbacks__
メンバ(attachで登録されたCategoryインスタンス)があり、なおかつそのCategoryインスタンスが持つattached_toメンバがobと等しければ次に進む。 - callback呼び出しの対象となるcategoryを決める。scan関数のcategoriesパラメータが指定されていなければ、登録されているcategoryをすべて対象に含める。
- 各categoryに紐付いたcallback関数を取得し、実行する。
packageが
__path__
を持つか検証。持っていなければreturnpackageに対して
walk_package
関数を呼び出し、そのパッケージに属するモジュールのモジュールローダーのイテレータを取得する。
※この関数はpkgutil.walk_packages
とほぼ同じだが、ignoreを指定させたいので実装している。walk_packageの結果新しい未importのモジュールが見つかればimportし、そのモジュールに対してinspect.getmembers, invokeを行う。
感想
デコレートされたオブジェクトのメタ情報を結局はその関数自身にもたせている。(__venusian_callbacks__
メンバ)
この辺は動的にメンバーを追加できる言語ならではか。言語として関数にメタ情報を含ませる仕組みがあるといいと思うのだけど。
[追記]
python3のアノテーションの仕組みと共存できるようなやり方ができればいいかも。
func.annotationsに関数自身のメタ情報を持てるようにする、とか。
python3のアノテーションは引数と戻り値に関するメタ情報は持てるが、それ以外の情報(例えばjavaの@Deprecatedとか)は持てない。