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

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

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

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

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

## 公式ドキュメントの内容
参考にしたドキュメントを抜粋します。

### 8. 複合文 (compound statement) / [8.7. 関数定義](https://docs.python.org/ja/3.11/reference/compound_stmts.html#function-definitions)

…

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

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

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

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

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

…

### 用語集 / [decorator](https://docs.python.org/ja/3.11/glossary.html#term-decorator)

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

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

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

@staticmethod
def f(arg):
    ...
```

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

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

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

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

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

```python
def f2(f):
    return f
```

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

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

これで完成です。

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

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

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

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

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

```python
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` の中でも外でも使えます）

```python
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
```

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

```python
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()
```

```text:実行結果
decorated by @f1('foobar')
decorated by @f2
```

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

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

---

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

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

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

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

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

```python
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` 関数にはこれらのデコレータが適用されており、実行時にそれぞれの処理が実行されることが確認できます。

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