プログラミング
記事内に商品プロモーションを含む場合があります

Pythonのデコレータ「@マーク」の使い方|初心者向け徹底入門

Aru

Pythonにはデコレータというものが存在しています。Pythonのデコレータは、コードをシンプルに保ちながら機能を追加できる便利なツールですが、突然「@マーク」が出てきて混乱しやすいです。本記事では、デコレータの実装例を通してデコレータの使い方について解説します。

デコレータとは

デコレーターは関数やメソッドを書き換えずに変更を行ったり拡張したりすることが可能な機能です。既存のコードを変更せずに、デコレータを追加するだけで変更ができることが大きなメリットです。

デコレータの役割とメリット

  • コードの再利用性の向上
    デコレータを使うことで各関数で共通する処理を何度も書く手間を減らすことができます
  • 可読性の向上
    例えば、入力値チェックなどをデコレーターで行うことで、関数の本質的な部分に集中して作成することが可能です。
    • 例:エラーチェックやログ出力、処理時間計測などを関数に追加

デコレータを使ってみる

以下、簡単な例を使って実際にデコレータを使ってみます。

実装例

デコレーターの動きを知るために、関数の呼び出し前と呼び出し後にメッセージを表示するデコレータを作成して使ってみます。

プログラムはいかになります。

def my_decorator(func):
    def wrapper():
        print("処理を開始します")
        result = func()
        print("処理が完了しました")
        return result
    return wrapper

@my_decorator
def say_hello():
    print("こんにちは!")

say_hello()

my_decoratorがデコレータ関数です。デコレータの引数は関数となります。このプログラムを実行すると以下のような出力が得られます

say_hello()を呼び出しただけなのに、「処理を開始します」「処理が完了しました」という出力が追加されていることがわかると思います。これがデコレータの機能になります。

実行結果
処理を開始します
こんにちは!
処理が完了しました

デコレータの動き

上記のプログラムの解説です

  • my_decorator は関数を受け取り、別の関数 wrapper を返す関数です
  • wrapper関数では、元の関数 func を呼び出す前後に処理を追加しています
  • @my_decoratorsay_hello = my_decorator(say_hello) と同じ意味でになります。つまり、say_hello 関数を引数として my_decorator が呼び出される形にになります。

プログラムの動きは以下になります

  1. say_hellomy_decorator(say_hello)に書き換えられる
  2. my_decoratorは、wrapper関数を返す
  3. 結果としてwrapper()という呼び出しになり、「処理を開始します」「処理が完了しました」が追加される

デコレータの動作は上記のようになります。my_decoratorが関数wrapperを返し、最終的にはwrapper()となることでwrapper関数が呼び出されることになります。

デコレータの構文

デコレータの基本構文

デコレータの基本構文は以下になります

  • @decorator_name は、その下に書かれた関数の定義に適用される
  • デコレータは関数を受け取り、別の関数を返す必要がある

基本的には、上記の2つのルールだけですが、書き方自体はある程度テンプレート化しています。

引数と戻り値がある場合のデコレート

関数に引数や戻り値がある場合はwrapper関数で受け取り、関数に引き渡します。

Pythonでは、*args, **kwargsとすることで引数を全て受け取り、関数に渡すことができます。

以下のプログラムは、引数のある関数に対してデコレータを使う例です。

def my_decorator(func):
    def wrapper(*args, **kwargs): # *args と **kwargs で引数を可変長に対応
        print("処理を開始します")
        result = func(*args, **kwargs)
        print("処理が完了しました")
        return result
    return wrapper

@my_decorator
def add(a, b):
    return a + b

print(add(2, 3)) # 出力:処理を開始します、5、処理が完了しました
*args**kwargs の役割

これらを使うことで、どんな引数を持つ関数でもデコレートできるようになります

  • *args: 位置引数をタプルとして受け取る
  • **kwargs: キーワード引数を辞書として受け取る

なお、引数はデコレーターで追加、変更することも可能です。デコレータで引数を修正することで、関数の挙動を変化させることもできます。

例えば、以下のようにするとデコレータで引数を二乗して計算することが可能です。このプログラムではnew_args = [x * x for x in args]として、引数を再計算してnew_argsを作成しています。

def my_decorator(func):
    def wrapper(*args, **kwargs): 
        print("処理を開始します")
        new_args = [x * x for x in args]
        result = func(*new_args, **kwargs)
        print("処理が完了しました")
        return result
    return wrapper

@my_decorator
def add(a, b):
    return a + b

print(add(2, 3))

デコレータの使い方(応用)

時間計測

個人的によく使うデコレータで、関数の処理時間を計測するものになります。

プログラム中に書くと煩雑になりますが、@calc_timeを計測対象の関数定義の上に書くだけなので便利です。

import time

def calc_time(func):
    def wrapper():
        start = time.time()
        result = func()
        end = time.time()
        print(f"処理時間は{end - start:.3f}秒でした")
        return result
    return wrapper


@calc_time
def loop():
    for _ in range(100_000_000):
        pass
loop()

デコレーターを2つ以上並べる例

デコレータを複数並べた場合は並べた順番で呼び出されます。例えば以下のプログラムでは、my_decorator → my_decorator2 → say_helloの順で呼び出されます。

def my_decorator(func):
    def wrapper():
        print("処理を開始します")
        result = func()
        print("処理が完了しました")
        return result
    return wrapper

def my_decorator2(func):
    def wrapper():
        print("START")
        result = func()
        print("END")
        return result
    return wrapper

@my_decorator
@my_decorator2
def say_hello():
    print("こんにちは!")

say_hello()

プログラムの実行結果は以下になります。

実行結果
処理を開始します
START
こんにちは!
END
処理が完了しました

デコレータ自身の引数を持たせる例

Pythonのデコレータ自身に引数を持たせることもできます。

基本的な構造は以下のようになります。

デコレータの記述

  1. 外側の関数(引数を受け取る)
    デコレータの設定用の引数を受け取ります
  2. decorator関数
    元の関数を引数として受け取る関数です
  3. wrapper関数
    元の関数をラップして処理を追加する

下記のプログラムでは、デコレータで指定した回数だけ呼び出しを繰り返します。@repeat(3)となっているので、helloは3回呼び出されます。

def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def hello():
    print("Hello, world!")

hello()
実行結果
Hello, world!
Hello, world!
Hello, world!

デコレータを使って関数を登録し、登録した関数を呼び出す例

デコレータを使って関数を登録し、登録した関数を呼び出す例です。Flaskなどのフレームワークで利用されていることがあるもので、結構便利ですので紹介しておきます。

下記のプログラムではsampleクラスはsetcallの2つの関数を持ちます。

@sample.set(n)とすると、関数がnという番号で記録されます。

sample.call(n)とすると、対応する関数が呼び出されます。

class Sample:
    def __init__(self):
        self.registry = {}

    def set(self, n):
        def decorator(func):
            self.registry[n] = func  # n番目の関数を登録
            return func
        return decorator

    def call(self, n):
        if n in self.registry:
            return self.registry[n]()  # 登録された関数を実行
        else:
            raise ValueError(f"Function {n} is not registered.")

# Sample のインスタンス作成
sample = Sample()

# 関数を登録
@sample.set(1)
def func1():
    return "Function 1 executed"

@sample.set(2)
def func2():
    return "Function 2 executed"

# 関数を呼び出し
print(sample.call(1))  # "Function 1 executed"
print(sample.call(2))  # "Function 2 executed"

デコレータがfunc = my_decorator(func)だということを思い出すと、以下の記述は、

func1 = sample.set(1)(func)

という表記と同じになります。

@sample.set(1)
def func1():
    return "Function 1 executed"

デコレーターを使うことで、複数の関数を登録し条件に合わせて呼び出す関数を選択するような処理を書くことができます。処理する関数を追加・削除したい場合に便利な記述です。

Flaskでは、特定のURLが呼び出された場合の処理を、デコレータを使って追加していきます。慣れないうちは、わかりにくかったですが、慣れると書きやすいです。

まとめ

以上、デコレータの使い方についてまとめてみました。デコレータは使い方次第では強力です。特に、コードを変更せずに処理を追加できるのが便利です。フレームワークなどでも使われていることがあるので、役割と原理を覚えておいて損はないと思います。

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

ABOUT ME
ある/Aru
ある/Aru
IT&機械学習エンジニア/ファイナンシャルプランナー(CFP®)
専門分野は並列処理・画像処理・機械学習・ディープラーニング。プログラミング言語はC, C++, Go, Pythonを中心として色々利用。現在は、Kaggle, 競プロなどをしながら悠々自適に活動中
記事URLをコピーしました