当サイトは、アフィリエイト広告を利用しています

【Python】デコレーターの使い方と使いどころ

作成日:2024月08月27日
更新日:2024年08月27日

pythonのデコレーターの使い方とつかいどころ
をまとめておく。

デコレーターとは?

pythonのデコレーターは既存の関数やメソッドをラップすることで
その振る舞いを拡張、または変更することができる機能のこと。

デコレーターを使うことで元の関数のソースを変更せずに
機能を追加したり、共通の処理をまとめたりできる。

デコレーターの使い方

シンプルなデコレーターを使って使い方をまとめる。

デコレーターの作成

デコレーターを使う手順は

  • デコレーター関数の作成
  • 対象の関数にデコレーターを付与

になる。

この手順でより対象の関数はデコレーターでラップされているので
関数実行時、デコレーター処理が適用された状態で実行されるようになる。

デコレーター関数の作成

デコレーター関数を作成する。

デコレーター関数
def my_decorator(func):
def wrapper():
print("デコレーターの前処理")
func()
print("デコレーターの後処理")
return wrapper

引数funcをラップしてデコレーター処理としてprint出力を行う関数。
funcにはデコレーター処理を適用したい関数を渡すようにする。

デコレーターの付与

デコレーター処理を適用したい関数にデコレーターを付与する

対象関数
def my_decorator(func):
def wrapper():
print("前処理")
func()
print("後処理")
return wrapper
@my_decorator
def say_hello():
print("こんにちは!")
def main():
say_hello()
if __name__ == "__main__":
main()
# 実行結果
# 前処理
# こんにちは!
# 後処理

say_hello()関数にデコレーター「@my_decorator」を付与する。
デコレーターはデコレーター関数名になる。

デコレーターの動作

実行時の動きとしては下記のようになる。

  • my_decorator(func): say_hello関数を引数として受け取る
  • wrapper(): デコレーターが返す新しい関数で、元のfuncを呼び出す前後に追加の処理をする
  • @my_decorator: この記述により、say_helloが自動的にmy_decoratorによって修飾される

引数つき関数にデコレーターを適用する

引数を持つ関数にもデコレーターを適用することができる。
別に引数がなくてもいいので基本がこっちで作ったほうが拡張性は高くなる

引数あり
def my_decorator(func):
def wrapper(*args, **kwargs):
print("デコレーターの前処理")
result = func(*args, **kwargs)
print("デコレーターの後処理")
return result
return wrapper
@my_decorator
def say_hello(name):
print(f"こんにちは、{name}さん!")
def main():
say_hello(name = "tom")
if __name__ == "__main__":
main()

(*args, **kwargs)にすることで任意の数の位置引数とキーワード引数の両方を受け取ることができる。
※要はなんでも受けれる

*args, **kwargsから値をとる

下記のようにすれば引数の値を取得することも可能

引数取得
def my_decorator(func):
def wrapper(*args, **kwargs):
# キーワード引数から 'name' を取得
name_value_key = kwargs.get('name')
if not name_value_key and args:
name_value_arg = args[0] # 位置引数として取得
print(f'位置引数:{name_value_arg}')
else:
print(f'キーワード引数:{name_value_key}')
result = func(*args, **kwargs)
print("デコレーターの後処理")
return result
return wrapper
@my_decorator
def say_hello(name):
print(f"こんにちは、{name}さん!")
def main():
say_hello(name = "tom") # キーワード引数で実行
say_hello("john") # 位置引数で実行
if __name__ == "__main__":
main()
# 実行結果
# キーワード引数:tom
# こんにちは、tomさん!
# デコレーターの後処理
# 位置引数:john
# こんにちは、johnさん!
# デコレーターの後処理

デコレーターのつかいどころ

デコレーターの使いどころとしていくつかサンプルを
実装する。

共通のログを出力する

実行する関数をログに出したい場合は
関数名をログに出力するデコレーターを作成すれば
無駄なコードを書かなくて済む

ログ共通化
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
def my_decorator(func):
def wrapper(*args, **kwargs):
logger.info(f"関数 {func.__name__} が実行されました")
result = func(*args, **kwargs)
return result
return wrapper
@my_decorator
def say_hello(name):
print(f"こんにちは、{name}さん!")
@my_decorator
def say_goodbye(name):
print(f"さよなら、{name}さん!")
def main():
say_hello(name = "tom")
say_goodbye(name = "tom")
if __name__ == "__main__":
main()
# 実行結果
# 2024-08-27 21:38:39,140 - INFO - 関数 say_hello が実行されました
# こんにちは、tomさん!
# 2024-08-27 21:38:39,141 - INFO - 関数 say_goodbye が実行されました
# さよなら、tomさん!

下記のFlaskで作ったREST APIPIではアクセスログを出力用デコレーターを作成して使用している

※access_logのところらへん

またpythonのloggingについては下記で詳しくまとめています

エラーハンドリングをまとめる

同じ処理のエラーハンドリング(try~exept)をデコレーターにすることで
無駄な実装を減らせる。

デコレーター

pymongo_error_wrapper
# dbアクセスエラー用ラッパー
def pymongo_error_wrapper(func):
def wrapper(self, collection_name, *args, **kwargs):
try:
return func(self, collection_name, *args, **kwargs)
except PyMongoError as e:
e.detail = f"Failed to find documents in database '{self.db_name}', collection '{collection_name}'"
raise e
except Exception as e:
logger.error(f"An unexpected error occurred: {str(e)}")
raise
return wrapper

引数の関数(MongoDBアクセス処理)をtry~exeptでラップするデコレーターを作る

dbアクセス処理

MongoDBアクセス処理関数にデコレーターをつける

database_model.py
from app.database import Database
from app.error_handling import pymongo_error_wrapper
class DatabaseModel:
# dbごとにインスタンスを作成
def __init__(self, db_name):
self.db = Database.client[db_name]
self.db_name = db_name
# 全件取得
@pymongo_error_wrapper
def find_documents(self, collection_name, query={}, projection={'_id': 0}):
collection = self.db[collection_name]
return list(collection.find(query, projection))
# 指定取得
@pymongo_error_wrapper
def find_document_by_id(self, collection_name, document_id, projection={'_id': 0}):
collection = self.db[collection_name]
return collection.find_one({"user_id": document_id}, projection)
# 登録
@pymongo_error_wrapper
def insert_document(self, collection_name, document):
collection = self.db[collection_name]
return collection.insert_one(document)
# 削除
@pymongo_error_wrapper
def delete_document(self, collection_name, document_id):
collection = self.db[collection_name]
return collection.delete_one({"user_id": document_id})
# 更新
@pymongo_error_wrapper
def update_document(self, collection_name, document_id, update_data):
collection = self.db[collection_name]
return collection.update_one(
{"user_id": document_id},
{"$set": update_data}
)

上記二つは

より抜粋

まとめ

Pythonのデコレーターを使うことで共通処理や関数の拡張を
関数自体をいじらずにできるのでかなり便利だと感じた。

今後は積極的に取り入れていきたい。

関連記事

新着記事

タグ別一覧
top