静かなる名辞

pythonとプログラミングのこと

2019/03/22:TechAcademyがteratailの質問・回答を盗用していた件
2019/03/26:TechAcademy盗用事件 公式発表と深まる疑念



【python】クラスでデコレータ!

はじめに

 デコレータといえば関数で作るものだと思っている人も大勢いると思いますが、クラスでも__call__メソッドを実装すればクラスインスタンスはcallableになり、呼び出しできるので、デコレータたりえます。

import time

class deco:
    def __init__(self, f):
        self.f = f
    def __call__(self, *args, **kwargs):
        t1 = time.time()
        ret = self.f(*args, **kwargs)
        t2 = time.time()
        print("deco:", t2-t1)
        return ret

@deco
def f(message):
    print("f:", message)

f("hoge")
""" =>
f: hoge
deco: 6.508827209472656e-05
"""

 このことには先に書いた記事で気づいたのですが、もう少し追求してみます。

 目次

デコレータの種類ごとに実装してみる

 こちらの記事によると、デコレータは4種類に分類できます。
qiita.com

 以下の2項目の組み合わせによる

  • 引数の有無
  • ラッパー関数を返すか否か

 ただし、引数なしでラッパー関数を返さないデコレータは簡単すぎて意味がない、としているので3種類考えることにします。

 これらすべてを実装できたらすごいですね。ということで、実装していきます。なお、以下で上記記事からの引用コードを一部示します。

引数なしでラッパー関数を返すデコレータ

def args_logger(f):
    def wrapper(*args, **kwargs):
        f(*args, **kwargs)
        print('args: {}, kwargs: {}'.format(args, kwargs))
    return wrapper

(引用)

 これはクラスのインスタンス自身をラッパー関数として取り扱えば簡単です。冒頭のコードもそれです。もうやったので除外。

引数ありでラッパー関数を返さない場合

funcs = []
def appender(*args, **kwargs):
    def decorator(f):
        # args や kwargsの内容によって処理内容を変えたり変えなかったり
        print(args)
        if kwargs.get('option1'):
            print('option1 is True')

        # 元の関数をfuncsに追加
        funcs.append(f)
    return decorator

(引用)

 簡単そうです。

funcs = []
class deco:
    def __init__(self, cond):
        self.cond = cond

    def __call__(self, f):
        print(self.cond)  # 条件によって処理を変えたり
        funcs.append(f)

@deco("aaa")
def hoge(message):
    print("hoge:", message)

@deco("bbb")
def fuga(message):
    print("fuga:", message)

for f in funcs:
    f("called")

""" =>
aaa
bbb
hoge: called
fuga: called
"""

 できた。

引数ありでラッパー関数を返す場合

def args_joiner(*dargs, **dkwargs):
    def decorator(f):
        def wrapper(*args, **kwargs):
            newargs = dargs + args  # リストの結合
            newkwargs = {**kwargs, **dkwargs}  # 辞書の結合 (python3.5以上で動く)
            f(*newargs, **newkwargs)
        return wrapper
    return decorator

(引用)

 ちょっとむずかしそうですかね。デコレータ自身の引数は__init__で受け取ることになります。次に__call__が関数を受け取るしかないのですが、更にwrapperがないと関数そのものの引数が取り扱えません。

 まあ、wrapperもメソッドで作れるので、実は難しいと見せかけて楽勝なのですが。

class deco:
    def __init__(self, decomessage):
        self.decome = decomessage

    def __call__(self, f):
        self.f = f
        return self.wrapper

    def wrapper(self, *args, **kwargs):
        print("deco:", self.decome)
        return self.f(*args, **kwargs)

@deco("hoge")
def func(x):
    print("func:", x)
func("foo")
    
@deco("fuga")
def func(x):
    print("func:", x)
func("bar")

@deco("piyo")
def func(x):
    print("func:", x)
func("baz")

""" =>
deco: hoge
func: foo
deco: fuga
func: bar
deco: piyo
func: baz
"""

 ということで、ぜんぶできました。よって、クラスもデコレータになれますね。

考察

 思いついたことをつらつらと考察します。

どうしてこの書き方が一般的ではないの?

 単純に__call__を使うのがキモいからだと思います。。。

利点は?

 幾つか考えられます。

  • 関数定義のネストが深くならない。
  • 通常の関数によるデコレータでは状態をコールスタック上で管理するが(クロージャ)、クラスで書くと明示的にselfの状態として取り扱える。

 以上2点によって、人によっては可読性が高いと感じる可能性があります。

  • 各種拡張が行いやすい

 通常のデコレータはいかんせんクロージャなので、あまり大規模なものの実装は避けると思います。この方法だと何しろクラスなので、その気になればなんでも作れるでしょう。まあ、クラスを他に定義しておいて普通のデコレータでそれのインスタンスを返しても良い訳ですが。

 まったく利点がない訳ではないということです。

欠点は?

  • とりあえずキモい。
  • 更に一般的でもない。
  • そもそも濫用しすぎ。
  • 決定的な利点がある訳ではないので、上記欠点がむしろ目立つ。

 うーん、苦しいかな。

総評

 面白いけど、これを使ったコードを書いて他人に渡すのは嫌がらせに近いかもしれない。
(いやでも、普通のデコレータもなかなか読みづらいけどなぁ)

まとめ

 とにかくできることはわかりました。「ここが駄目!」とか「こんなふうに活用できるね!」などの意見がありましたら、遠慮なくコメントに書いてください。

追記

 pandasのコードを読んでいたら、実際に使っている例を見かけました。

pandas/_decorators.py at master · pandas-dev/pandas · GitHub

 やるときはけっこうやるんですね。