[初心者向け] Python デコレータの 3 つのメリット

# はじめに

初投稿です。
過去 IT 講師をしておりましたが、今後は開発の案件に入る可能性が高いため、文章力を衰えさせないために技術の共有をしていきます。

Python の学習でまず困惑するのが、デコレータかと思います。便利かつ重要なトピックなので取り上げることにしました。

# デコレータ

「関数の中身を変更せずに、処理を付け加える」というような解説が散見されますが、初心者には分かりにくいので可能な限り分かりやすく解説していきます。

デコレータ `deco` と実質的な処理を記述するラッパー関数 `wrapper` を考えます。

```python:単純なデコレータ
def deco(func):
    def wrapper(*args, **kwargs):
        ...
    return wrapper
```

:::note
`*args` と `**kwargs` で、どんな形式の引数も全て受け取れます
:::

デコレータを定義する手順は以下の通りです。

1. 関数を引数とする関数の定義
2. その中にラッパー関数の定義
3. ラッパー関数内に実際の処理の記述
4. ラッパー関数自身の返却

関数を定義する際に `@` が付与されたデコレータ名を上に記述することで、関数内の記述に手を加えることなく、処理を追加できます。

```python:デコレータの付与
@deco
def func(args_𝟣, ...):
    ...
```

## 1. 繰り返しの防止

複数の関数が同様の処理を有する場合に、便利に使えます。
同様の処理を関数として独立させるイメージが近いでしょうか。

`func_1` ~ `func_3` に、`A` という処理を追加したいケースを考えます。

```python:複数の関数
def func_𝟣(...):
    ...

def func_𝟤(...):
    ...

def func_𝟥(...):
    ...
```

以下のように関数内に `A` と追記すれば、問題ないように思えます。

```python:同じ処理を有する複数の関数
def func_𝟣(...):
    A
    ...

def func_𝟤(...):
    A
    ...

def func_𝟥(...):
    A
    ...
```

これでは `A` が何回も登場しており記述が冗長なので、デコレータで処理をまとめてみましょう。

```python:デコレータを利用して同じ処理をまとめた記述
def deco(func):
    def wrapper(*args, **kwargs):
        A
        func(*args, **kwargs)
    return wrapper

@deco
def func_𝟣(...):
    ...

@deco
def func_𝟤(...):
    ...

@deco
def func_𝟥(...):
    ...
```

サンプルコードなので `A` は 1 行ですが、実際は複数行にわたる可能性があります。つまり `A` の行数に比例して、デコレータの恩恵は大きくなります。

## 2. 保守性の向上

他のメリットも確認していきましょう。

前の例だと `A` に変更が生じた際に、`func_1` ~ `func_3` 内の 3 箇所の `A` を触る必要があります。これでは影響箇所が大きくなり、修正が大変です。

そこで共通の処理をデコレータ内で定義すると、`A` の変更による影響を最低限に抑えられます。`deco` 内の `A` を変更するだけで、`func_1` ~ `func_3` の挙動を変更できます。

それぞれの関数を触る必要がなくなるため、保守性・拡張性の向上が見込めます。

```python:デコレータを利用して同じ処理をまとめた記述
def deco(func):
    def wrapper(*args, **kwargs):
        A
        func(*args, **kwargs)
    return wrapper

@deco
def func_𝟣(...):
    ...

@deco
def func_𝟤(...):
    ...

@deco
def func_𝟥(...):
    ...
```

## 3. 可読性の向上

追加される処理に名前を付けられるため、分かりやすくなるということです。

Web アプリケーションで用いられる Flask の関数で説明します。

```python:ルーティングの追加
@app.route("/hello")
def hello():
    return "Hello, World!"
```

実際の処理の内容はさておき、デコレータ名からルーティングに関する処理だということが予想できます。

このメリットを享受するためにも、デコレータ名はふさわしいものを付けるべきです。

## 実例

関数が少なくメリットが見えづらいですが、記述自体はそこまで難しくないです。

```python:関数の処理の前後で、文字列の出力
def deco(func):
    def wrapper(*args, **kwargs):
        print("START")
        func(*args, **kwargs)
        print("END")

    return wrapper


@deco
def hello():
    print("Hello")


hello()

--> START
    Hello
    END
```

# 応用的な記述法

## 複数のデコレータ

デコレータを複数適用させるには、単純に続けて記述するだけですが、順序が重要となります。

```python:2 つのデコレータ
def func_1(func):
    def wrapper(*args, **kwargs):
        print("A")
        func(*args, **kwargs)
        print("B")

    return wrapper


def func_2(func):
    def wrapper(*args, **kwargs):
        print("X")
        func(*args, **kwargs)
        print("Y")

    return wrapper
```

まずは `func_1` ➩ `func_2` で適用させます。

```python:複数のデコレータの適用
@func_1
@func_2
def msg_print_1(msg):
    print(msg)


msg_print_1("message")

--> A
    X
    message
    Y
    B
```

デコレータの記述を逆にします、結果を予想してみてください。

```python:デコレータの記述を逆に
@func_2
@func_1
def msg_print_2(msg):
    print(msg)


msg_print_2("message")

--> X
    A
    message
    B
    Y
```

デコレータといえども関数であることには変わりないので、デコレータを組み合わせて複雑な処理を記述できます。

## 引数付きデコレータ

デコレータが引数を受け取る場合ですが、ネストが深くなり複雑に見えます。

```python:引数付きデコレータ
def deco(some):
    def actual_deco(func)
        def wrapper(*args, **kwargs):
            ...
        return wrapper

    return actual_deco
```

`some` はデコレータが受け取った引数で、`actual_deco` は今までのデコレータ定義と同じです。

引数で与えた数から、関数実行前にカウントダウンを表示するデコレータを考えます。

```python:引数をとるデコレータ
def countdown(n=3):
    def actual_deco(func):
        def wrapper(*args, **kwargs):
            nonlocal n
            while n:
                print(n)
                n -= 1
            func(*args, **kwargs)

        return wrapper

    return actual_deco
```

デコレータに引数を与えて関数を実行すると、問題なく表示されます。

```python:5 からカウントダウン
@countdown(5)
def print_msg(msg):
    print(msg)


print_msg("Hello")

--> 5
    4
    3
    2
    1
    Hello
```

デコレータにはデフォルト引数を与えているので、引数を省略できます。ただし、`()` は省略できません。

```python:5 からカウントダウン
@countdown()
def print_msg(msg):
    print(msg)


print_msg("Hello")

--> 
    3
    2
    1
    Hello
```

# おわりに

分かりにくい箇所や誤りあれば、コメントをお願いします。