- 関数・クラスデコレーターの使い方を理解する
- 関数デコレーターを理解して自作できるようになる
- クラスデコレーターと関数デコレーターがある
- 関数やメソッド、クラスをデコレート(装飾)する機能
- デコレーターを使用すると、関数やメソッド、クラスそのものの中身を変えずに共通のロジックを適用することができる
-
ログ、トランザクション、セキュリティ、例外処理、キャッシュ、リトライなどでよく使用される
-
(例)Django
from django.db import transaction
@transaction.atomic
def viewfunc(request):
# This code executes inside a transaction.
do_stuff()
https://docs.djangoproject.com/en/4.0/topics/db/transactions/
- コードのブロックが正常に完了すると、変更はデータベースにコミットされる。例外が発生すると変更はロールバックされる。
デコレーターを適用する関数やメソッド、クラス定義の前に @デコレーター名
とつけるだけ!!。
@デコレーター名
def デコレート対象の関数:
pass
クラス、クラスのメソッドも同様
# クラスメソッドにデコレーターを適用
class User:
def __init__(self, name, age):
self.name = name
self.age = age
@property
def start_name(self):
return self.name[0]
user = User('panda-senpai', 17)
print(user.name)
print(user.start_name)
# クラスにデコレーターを適用
from dataclasses import dataclass
@dataclass # dataclassデコレーターをUserクラスに適用
class User:
name: str
age: int
user = User('panda', 29)
print(user.name)
print(user.age)
- dataclassデコレータを使用することで、コンストラクタのメソッドである__init__などが自動的に定義される。
サードパーティライブラリの retrying
- デコレート対象の関数内で例外が発生した際に、再度関数の実行を行ってくれるデコレーターretryを提供している。
$ pip install retrying
以下で動作確認してみよう
- @retryなし
- @retryあり
- stop_max_attempt_numberあり(最大リトライ回数を指定する引数)
from retrying import retry
@retry(stop_max_attempt_number=2)
def my_func():
for i in range(10):
if i == 3:
print('==============> 3だ!!!')
raise ValueError('3はエラー')
else:
print(f'{i=}です')
my_func()
- 1つの関数やメソッド、クラスに複数のデコレーターを適用できる。
- 一行ごとに1つのデコレーターを指定します。
以下の例で、複数のデコレーターを適用する構文と、代入文は等価です。
@my_decorator2
@my_decorator1
def func():
pass
デコレーターは内側のmy_decorator1から外側に向かって順に適用される。
デコレーターは下記の代入文と等価です。
@my_decorator
def func():
pass
func = my_decorator(func)
- (参考)https://www.python.org/dev/peps/pep-0318/
- デコレーターは対象のオブジェクトを置き換えるための機能です。
my_decorator()
関数(デコレーター関数)は関数func
を引数にとり、その返された結果をfunc
に代入することでfunc
を置き換えている。
- Pythonでは関数内に関数を定義することができる。
- 外側の関数の戻り値として内側の関数を返すことができる。
def func_greeting(name):
def print_greeting():
print(f'Hello! {name} さん')
return print_greeting
func = func_greeting('panda-senpai')
func() # () をつけてよびだすことができる
- Pythonでは、関数を別の関数の引数として与えることができます。
def greeting(name):
print(f'Hello {name} さん')
def after_greeting(func, name):
func(name)
print('今日はいいお天気ですね')
after_greeting(greeting, 'panda')
これらの機能を使用して作成するのがデコレーター!!
デコレーターは以下のように実装します。
- デコレーター関数を定義する
- デコレーター関数はデコレート対象の関数を引数として受け取るようにする
- デコレーター関数の中にデコレート対象の関数の変わりに呼び出されるラッパー関数を定義し、その中で引数で受け取った関数を呼びだす。関数呼び出しの前後に追加、変更の処理を加える
- デコレーター関数の戻り値として呼び出し可能オブジェクト(関数)を返す
def my_decorator(func): # デコレーター関数
def wrap_function(): # デコレート対象の関数の変わりに呼び出されるwrapper関数
print('デコレート対象の関数を呼ぶ前にしたい処理')
func()
print('デコレート対象の関数を呼んだ後にしたい処理')
return wrap_function
- 代入文を使用する書き方
def greeting():
print(f'こんにちは')
greeting = my_decorator(greeting)
greeting()
- @デコレーター名構文を使用して置き換える
@my_decorator
def greeting():
print(f'こんにちは')
greeting()
def my_decorator(func):
def wrap_function(*args, **kwargs):
print(f'{args=}:{kwargs=}')
return func(*args, **kwargs)
return wrap_function
@my_decorator
def my_sum(a, b, initial=0):
return initial + a + b ;
total = my_sum(10, 20, initial=100)
print(f'合計は {total} です')
- デコレーターを使用すると、関数が置き換わるため元の関数名やdocstringが失われ、ログを表示したい場合やエラーが発生した場合など正しく表示されないという問題がある。
- この問題を回避するために、functools.wrapsが提供されている。functools.wrapsを設定すると、名前やdocstringを元のデコレート対象の関数のものに設定してくれる。
- wrapsはデコレーターなので、wrapper関数にwrapsデコレーターを適用し、引数に元のデコレート対象の関数を設定。
- 通常、デコレーターを作成する際には、functools.wrapsを使用するとよいです!
# docstringや関数名が失われる例
def my_decorator(func):
def wrap_function():
'''wrap用の関数です'''
func()
print(f'function: {func.__name__} called')
return wrap_function
@my_decorator
def greeting():
'''あいさつを返す関数です'''
print(f'こんにちは')
greeting()
print(greeting.__name__)
print(greeting.__doc__)
# functools.wrapsを使用した例
from functools import wraps
def my_decorator(func):
@wraps(func) # この1文を足すのみ
def wrap_function():
'''wrap用の関数です'''
func()
print(f'function: {func.__name__} called')
return wrap_function
@my_decorator
def greeting():
'''あいさつを返す関数です'''
print(f'こんにちは')
- 引数を受け取ってデコレータ関数を生成する関数を作成する
def my_decorator(引数): # 引数を受け取ってデコレーター関数を生成する関数
def _my_decorator(func): # デコレーター関数
def wrap_function(): # デコレート対象の関数の変わりに呼び出されるwrapper関数
print('デコレート対象の関数を呼ぶ前にしたい処理')
func()
print('デコレート対象の関数を呼んだ後にしたい処理')
return wrap_function
return _my_decorator
- retryデコレーター
from functools import wraps
from time import sleep
def my_retry(stop_max_attempt_number=1):
def _my_retry(func):
@wraps(func)
def wrap_function():
for i in range(stop_max_attempt_number):
try:
return func()
except Exception as e:
if i < stop_max_attempt_number-1:
sleep(1)
else:
raise e
return wrap_function
return _my_retry
@my_retry(stop_max_attempt_number=2)
def my_func():
for i in range(10):
if i == 3:
print('==============> 3だ!!!')
raise ValueError('3はエラー')
else:
print(f'{i=}です')
my_func()