0
1

Pythonのデコレータを公式ドキュメントを頼りに自作してみる

Posted at

Pythonを使うようになって、少しずつ「デコレータ」も使う場面が出てきました。これまではフレームワーク等で用意されているものを定型句として使うだけでしたが、デコレータの正体が何なのか、そして自分でも簡単に作れるのかが気になってきました。

Pythonの公式ドキュメントを読んだところ、デコレータについての記述は少ないものの、意外と単純な仕組みのようでした。(高階関数の知識は必要としますが)

そこで、実際に以下の記述例が動くよう f1f2 を作ってみることにしました。本記事ではその作成の思考過程を纏めています。

@f1(arg)
@f2
def func(): pass

公式ドキュメントの内容

参考にしたドキュメントを抜粋します。

8. 複合文 (compound statement) / 8.7. 関数定義

関数定義は一つ以上の デコレータ 式でラップできます。デコレータ式は関数を定義するとき、関数定義の入っているスコープで評価されます。その結果は、関数オブジェクトを唯一の引数にとる呼び出し可能オブジェクトでなければなりません。関数オブジェクトの代わりに、返された値が関数名に束縛されます。複数のデコレータはネストして適用されます。例えば、以下のようなコード:

@f1(arg)
@f2
def func(): pass

は、だいたい次と等価です

def func(): pass
func = f1(arg)(f2(func))

ただし、前者のコードでは元々の関数を func という名前へ一時的に束縛することはない、というところを除きます。

用語集 / decorator

(デコレータ) 別の関数を返す関数で、通常、 @wrapper 構文で関数変換として適用されます。デコレータの一般的な利用例は、 classmethod()staticmethod() です。

デコレータの文法はシンタックスシュガーです。次の2つの関数定義は意味的に同じものです:

def f(arg):
    ...
f = staticmethod(f)

@staticmethod
def f(arg):
    ...

同じ概念がクラスにも存在しますが、あまり使われません。デコレータについて詳しくは、 関数定義 および クラス定義 のドキュメントを参照してください。

作成してみる

ドキュメントに「デコレータの文法はシンタックスシュガー」や「func = f1(arg)(f2(func))」と書いてあるので、デコレータには専用の定義方法があるわけでなく、ただ高階関数を作ればいいだけのようです。

引数をとらない @f2 と、引数をとる @f1(arg) を作ってみます。

引数なし

まずは簡単なほう。 f = f2(f) というように、「関数を引数にとり同様の関数を返す」関数 f2 を作ってみます。

一番単純なのは、引数をそのまま返す関数です。

def f2(f):
    return f

とはいえこれだとデコレータの意味が無いので、 f をラップした別の関数を返すように改良します。(引数はとりあえず何にでも対応できるようにしています)

def f2(f):
    def wrapper(*args, **kwargs):
        print("decorated by @f2")
        return f(*args, **kwargs)
    return wrapper

これで完成です。

引数あり

引数ありの場合は、関数を実行した f1(arg)f2 と同様のオブジェクトになる必要があります。デコレータの適用を以下のように分解していいということです。

f1_arg = f1(arg)
@f1_arg
@f2
def func(): pass

一番単純なのは、先ほど作成した引数なし版 f2 を返すことでしょう。

def f1(*d_args, **d_kwargs):
    return f2

f2 に代わる関数をその場で作れば以下のようになります。 f2 のときより関数が1段多くなっています。

def f1(*d_args, **d_kwargs):
    def decorator(f):
        def wrapper(*args, **kwargs):
            print("decorated by @f1")
            return f(*args, **kwargs)
        return wrapper
    return decorator

せっかく f1 は引数をとっているため、それを使うよう改良します。( f1 のスコープであれば wrapper の中でも外でも使えます)

def f1(*d_args, **d_kwargs):
    def decorator(f):
        def wrapper(*args, **kwargs):
            d_args_repr = list(map(repr, d_args))
            d_args_repr.extend(f"{k}={repr(v)}" for k, v in d_kwargs.items())
            print(f"decorated by @f1({', '.join(d_args_repr)})")
            return f(*args, **kwargs)
        return wrapper
    return decorator

実行

全て繋げて実行してみます。

def f1(*d_args, **d_kwargs):
    def decorator(f):
        def wrapper(*args, **kwargs):
            d_args_repr = list(map(repr, d_args))
            d_args_repr.extend(f"{k}={repr(v)}" for k, v in d_kwargs.items())
            print(f"decorated by @f1({', '.join(d_args_repr)})")
            return f(*args, **kwargs)
        return wrapper
    return decorator

def f2(f):
    def wrapper(*args, **kwargs):
        print("decorated by @f2")
        return f(*args, **kwargs)
    return wrapper

# デコレータを使う
arg = "foobar"
@f1(arg)
@f2
def func(): pass

# 関数を実行する
func()
実行結果
decorated by @f1('foobar')
decorated by @f2

きちんと自作のデコレータ @f1, @f2func に適用されました。

今回は関数実行前にprintするだけでしたが、 wrapper の中を変えれば元の関数をリトライさせたりなど様々な装飾ができそうです。どのような使い道があるのかは、ライブラリなどの実装を見て勉強していこうと思います。


(おまけ)ChatGPTに作ってもらった

自分で何となく作った上記コードが妥当か答え合わせしたくて、ChatGPTにもほぼ同じ条件でリクエストしてみました。だいたい同じコードが作られました。

問い合わせ

以下のpythonコードが動くように f1 と f2 のサンプルを作ってください。

@f1(arg)
@f2
def func(): pass

返答

まず、 @f1(arg)@f2 が何かしらの処理を行うデコレータであると考えられます。例えば、 f1 が引数を受け取り、それに応じた処理を行うデコレータであり、 f2 が引数を受け取らず固定の処理を行うデコレータということになります。

それぞれのデコレータのサンプルを記述してみましょう。

def f1(arg):
    def decorator_func(func):
        def wrapper():
            print("f1 with arg:", arg)
            func()
        return wrapper
    return decorator_func

def f2(func):
    def wrapper():
        print("f2")
        func()
    return wrapper

@f1("example_arg")
@f2
def my_func():
    print("Executing my_func")

my_func()

このコードでは、 f1 は引数を受け取り、それを出力する処理を行うデコレータとして定義されています。 f2 は引数を受け取らず、固定の出力を行う処理を行うデコレータとして定義されています。 my_func 関数にはこれらのデコレータが適用されており、実行時にそれぞれの処理が実行されることが確認できます。

上記のコードを実行することで、 f1f2 のサンプルが作成された状態となります。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1