【備忘録】時間計測のデコレータを作りながら、デコレータについて勉強した

# はじめに
　筆者は卒業研究の中でプログラムの実行時間を計測したいなと思っていました。プログラム中に`time.time()`で現在時刻を取得し、差分計算をしても良いのですが **デコレータ**を使うともっと楽に実装し、使い回しも容易そうだなと思ったので実装してみました。

実装ついでデコレータについて調べてみたので備忘録としてまとめてみます。

# 実装するもの
　今回は関数の実行時間を測定するtimer_decoratorを自作してみようと思います。
 全体はこんな感じです。

 ```python
import time
from functools import wraps
from typing import Callable

def timer_decorator() -> Callable:
    """
    関数の実行時間を計測するデコレータ関数

    Returns:
        Callable: デコレータ関数
    """
    def decorator(func: Callable) -> Callable:
        @wraps(func)
        def wrapper(*args, **kwargs):
            start_time = time.time()
            result = func(*args, **kwargs)
            end_time = time.time()
            execution_time = end_time - start_time
            print(f"{func.__name__}の実行時間: {execution_time:.4f}秒")
            return result
        return wrapper
    return decorator

 ```

 また利用方法はこのようにします。

 ```python
@timer_decorator()
def sample_function(x: int, y: int) -> int:
    time.sleep(4) # 適当に時間がかかるようにする
    return x + y

result = sample_function(x=5, y=3)
print(f"結果: {result}")

 ```

 # デコレータとは
 デコレータとは、既存の関数に新たな機能を追加するための仕組みです。
 関数のコードを変更せずに、実行前後に処理を追加したり、引数や戻り値の値を操作するものです。

 たとえば、今回であれば`sample_function`という関数自体の実装を変更することなく`@timer_decorator()`というデコレータを関数上部に記述することで、時間計測の処理を関数の処理に追加することができます。

 よく見る例だと、FastAPIやFlaskなどで見られる
以下のような記述が、
```python
@router.get("/hello")
async def hello():
    """hello function"""
    return {"message": "hello FastAPI!"}
```
また、`dataclass`, `dataclass_json`などに見られる
```python
from dataclasses import dataclass

from dataclasses_json import LetterCase, dataclass_json


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class Hoge
    attr1: str
    attr1: str
```

のような記述もデコレータです。

dataclassのように`class`にもデコレータを作成でき、これはクラスデコレータと呼ばれるようです。

# デコレータを使うメリット
デコレータを利用するメリットには以下のようなものがあると考えます。

- 再利用正の向上
- 可読性の向上
- 関心の分離

何でもかんでもデコレータにするのは良くないですが、実装の選択肢として簡単な書き方は知っておいて損がないと考えます（存在と概要さえ知っておけば、AIに書いてもらえますしね♪）

# timer_decoratorの実装

では実際に今回のdecoratorを実装してみます。

改めて私の実装を見てみましょう。
```python
def timer_decorator() -> Callable:
   """
   関数の実行時間を計測するデコレータ関数

   Returns:
       Callable: デコレータ関数
   """
   def decorator(func: Callable) -> Callable:
       @wraps(func)
       def wrapper(*args, **kwargs):
           start_time = time.time()
           result = func(*args, **kwargs)
           end_time = time.time()
           execution_time = end_time - start_time
           print(f"{func.__name__}の実行時間: {execution_time:.4f}秒")
           return result
       return wrapper
   return decorator

```

`decorator()`関数はデコレートする対象となる関数を受け取り、`wrapper()`関数を返します。

`wrapper()`関数では全体として、以下の処理を行っています。

1. time.time()で開始時刻を取得
1. デコレートする対象を実行し`result`に取得
1. time.time()で終了時刻を取得する
1. 実行時間の計測(execution_time)
1. 実行時間の表示と、デコレート関数の実行結果を返す

ではここで出てきた`@wraps(func)`の役割はなんなのでしょうか？これがなければ一見するとかなり冗長なコードに見えますよね。

`@wraps(func)`はデコレートされた関数の関数名やdocstring(メタデータ)を保持します。

簡単にいうと、`wrapper()`関数から元の関数の情報にアクセスすることを目的としています。

今回は`func.__name__`を参照するために利用していますが、参照の必要がなければ不要な記述です。

# まとめ
この記事では、timer_decoratorの実装を通して、Pythonのデコレータについて解説しました。 
今回のtimer_decoratorの実装を応用することで、関数のログ出力やエラー処理、キャッシュなども実装できると考えています！

<details><summary>シンプルなログ出力</summary>

```python
import logging

def log_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"{func.__name__} が呼び出されました。 引数: {args}, {kwargs}")
        result = func(*args, **kwargs)
        logging.info(f"{func.__name__} が終了しました。 戻り値: {result}")
        return result
    return wrapper
```

</details>


記事中の誤りや誤字脱字等がありましたら、編集リクエストやコメント等で**やさしく**ご指摘お願いいたします！

最後までお読みいただきありがとうございました！