どぶお/Pythonで遊ぼう!

関数に情報を持たせる  

何をするか  

プログラミングスタイルにもよりますが、ディスパッチャからの関数呼び出しで戻り値以外に情報を渡したい時があります。
私の場合、JSON-RPCディスパッチャを作成した時に、内部の関数からの戻り値(原則としてディクショナリ)のオブジェクト構造のバージョンを持たせたい時がある、と思ったからです。そのため、相当限定された状況でしか必要にならないかもしれません・・・

例えば以下のような感じ。関数名等は適当です。

res = my_func("Test")
version = get_version_data(my_func)

こうすればresには通常の戻り値が入り、仮想の関数get_version_dataで関数my_func自体が持っている(例えば)バージョン情報を取得することができます。まあ、戻り値にリストを返したりすればいいのかもしれませんが、戻り値の構造が変わってしまうのもイヤな場合に使います。

デコレータを使っているのでデコレータの概念を理解している必要があります。
方法は2つ。

  1. デコレータの中に自由変数を持たせてクロージャを参照して値を取得する
  2. デコレータで関数自体にプロパティを作成してしまう

ここの例ではPython 2.5で動作確認しました。

自由変数を用いる方法  

私自身、仕組みを完全に理解しているわけではありませんが、Pythonでのクロージャを使用します。クロージャについては以下の記事で大変わかりやすく解説されています。この記事を読んでいてこの方法を思いつきました、感謝!

この記事で解説されている自由変数という仕組みを利用すると、トリッキーですがデータを持たせることができます。以下にサンプルコードを示します。

#!/usr/bin/python
import types
def cell_extra_data(data):
    def _cell_extra_data(func):
        def _decorated_func(*args, **kw):
            data     # 自由変数として認識される
            return func(*args, **kw)
        return _decorated_func
    return _cell_extra_data

def get_extra_data(func):
    for obj in func.func_closure:
        if not isinstance(obj.cell_contents, types.FunctionType):
            return obj.cell_contents

@cell_extra_data("MyDATA1")
def func1(a):
    print "FUNC1:", a
    return a

func1("ARG1")                                   # FUNC1: ARG1
print "DATA for FUNC1:", get_extra_data(func1)  # DATA for FUNC1: MyDATA1

cell_extra_dataデコレータでfunc1をデコレートします。この際のcell_extra_data()の引数が追加される情報になります。function以外の値なら何でもOKです。ただし動作原理上、複数のデコレータを使う場合、最外部(?)にしか適用できません

@cell_extra_data("Ver/1.1")
@foo_deco
@bar_deco
def my_func():
...

このようになります。

@foo_deco
@cell_extra_data
...

のような順ではクロージャの探索ができないので有効になりません。

しくみ  

前出のfunc_closureのひみつの解説に従うとcell_extra_data中の_decorated_funcのクロージャはdata, funcという二つの自由変数を持ちます。cell_extra_dataでデコレートされたfunc1の実体は_decorated_funcなので、

>>> func1
<function _decorated_func at 0xb7407b8c>
>>> func1.func_closure
(<cell at 0xb742e47c: str object at 0xb7411060>, <cell at 0xb742e4ac: function object at 0xb7407764>)

このようにfunc1.func_closure中にはdata(strオブジェクト)およびfunc(functionオブジェクト)を保持しているセルが格納されているので、この中からdataを取得すればcell_extra_data("MyDATA1")で渡したMyDATA1を取得できます。

>>> func1.func_closure[0].cell_contents
'MyDATA1'

ここではたまたま、data(strオブジェクト), func(functionオブジェクト)の順になっていますが、環境によらず常に順序が同一かは分からないので、データ取得用関数のget_extra_data()ではfunctionインスタンスかどうかを確認して、それ以外のものを返すようにしています。

このようにトリッキーな方法なのでPythonが進化していってもずっと使えるかどうかは分かりません。まあ、とりあえず2.xとか3.0ぐらいでは問題ないでしょうが(たぶん)・・・

デコレータの動作
デコレータに関しては解説記事がたくさんあるのでそちらを参照して戴くとして、乱暴にまとめてしまうと上の例でのデコレータの動作は以下の通りです。

デコレートされたfunc1 = cell_extra_data("MyDATA1")(func1)

つまり、

  1. cell_extra_data("MyDATA1")を実行し_cell_extra_dataを返す
  2. _cell_extra_data(func1)を実行し、_decorated_funcを返す

これによりデコレートされたfunc1("ARG1")を実行することは_decorated_func("ARG1")、つまりcell_extra_data("MyDATA1")(func1)("ARG1")を実行するのと同じことなのです。

関数自体にプロパティを作成する方法  

上の自由変数を使うよりもシンプルです。簡単に言うと、

func.__setattr__("extra_data", data)

を実行するだけです。
この例ではextra_dataプロパティを追加しているので、

func.extra_data

でdataを取得することができます。以下にサンプルコードを載せます。

#!/usr/bin/python
def add_extra_data(data):
    def _add_extra_data(func):
        def _decorated_func(*args, **kw): return func(*args, **kw)
        _decorated_func.__setattr__("extra_data", data)
        return _decorated_func
    return _add_extra_data

@add_extra_data("MyDATA2")
def func2(a):
    print "FUNC2:", a
    return a

func2("ARG2")                             # FUNC2: ARG2
print "DATA for FUNC2:", func2.extra_data # DATA for FUNC2: MyDATA2

この例では単純にfunc2.extra_dataを参照するだけでデータを取り出せるので扱いは簡単かも。ただ通常のオブジェクトに勝手にプロパティを追加するのがちょっと気持ち悪い気もしますね。
この方法では、変なトリックは使っていないので動作の仕組みなどもシンプルでわかりやすいです。

コメント  

何か他に面白い方法があれば教えて下さい [smile]
Pythonって奥が深いですねぇ。