Pythonのデコレータが扱う値に副作用をもたせる

python


この記事は、 PMOB Advent Calendar 2016 の 7日目 の記事かもしれない。

デコレータが扱う値に副作用をもたせるやつです。

内容

デコレータで関数のパラメータをいじることを考えます。

def decorator(value):
    def wrapper(f):
        def _wrapper(*args, **kwargs):
            kwargs['key'] = value
            return f(*args, **kwargs)
        return _wrapper
    return wrapper

こんな感じで値をどうこうするデコレータを考えるとします。 この value がイミュータブルの場合、 状態によってこの関数の挙動を変えるには新しく関数をデコレータにかける必要があります。 ここで、設計上別の関数を与え続けることができない、 副作用で制御した方が無駄がないといったケースあると思います。 そこで、クラスメソッドにデコレータを作って云々する方法を紹介します。

やる

デコレータが見る値をインスタンス変数にして、 そこを弄ったら中身が変わることを確認します。

class WrapClass:
    def __init__(self):
        self.bias = 10

    def instance_wrapper(self, func):
        def wrapper(*args, **kwargs):
            kwargs['value'] += self.bias
            return func(*args, **kwargs)
        return wrapper


wrapper = WrapClass()

@wrapper.instance_wrapper
def func(value):
    return value*2

print(func(value=10))
# >>> 40

wrapper.bias = 100
print(func(value=10))
# >>> 220

ちゃんと変わってますねー。 方法はこれで良さそうです。

なにごとかね

上のコードで、 デコレータが閉じていない( self.bias のインスタンスへの参照が切られていない) ことを確認しました。 もうちょっと詳しく見てみます。

class WrapClass:
    def __init__(self):
        self.foo = None

    def instance_wrapper(self, func):
        def wrapper(*args, **kwargs):
            kwargs['id'] = id(self.foo)
            return func(*args, **kwargs)
        return wrapper


wrapper = WrapClass()

@wrapper.instance_wrapper
def f(id):
    return id
print(f())

wrapper.bias = 100
print(f())

@wrapper.instance_wrapper
def g(id):
    return id
print(g())

このコードを実行すると、 生成時、値の変更後、同インスタンスを用いた新しい関数定義のときに、 self.foo をそれぞれ同じ参照先に持っていることが確認できます。

ついでにデコレータにかけられた関数のスコープを見てみると、 以下のようになっています。

print(f)
# >>> <function WrapClass.instance_wrapper.<locals>.wrapper at 0x10878f158>

まあそらそうだって話なんですけどね。 Pythonの評価戦略はドキュメントのどっかにありました。 記事だと下のがわかりやすかったです。

https://jeffknupp.com/blog/2012/11/13/is-python-callbyvalue-or-callbyreference-neither/

コメント欄もおもしろいよ。 日本語のいい記事みたいなのはパッと見つかりませんでした。 ごめんね。 最近オライリーから出た本にいいのがあったような気もします。 おしまい。