Python では、デコレーターは既存のオブジェクトに新しい機能を追加する際に、その構造を変更せずに済むようにするためのデザインパターンです。
デコレータは、拡張する関数の直前で呼び出さなければなりません。
デコレータを使用すると、メソッドや関数、あるいはクラスの機能を、 サブクラスを直接使用せずに動的に変更することができます。
これは、直接手を加えたくない関数の機能を拡張したい場合に有効な手段です。
デコレータパターンはどこでも実装できますが、Pythonはそのために、より表現力豊かな構文や機能を提供しています。
この記事では、Pythonのデコレータについて詳しく説明します。
デコレーターの作り方
Pythonでデコレータを作成する方法を見てみましょう。
例として、関数の出力文字列を小文字に変換するデコレータを作成します。
そのためには、デコレータ関数を作成し、その内部でラッパーを定義する必要があります。
次のスクリプトを見てください。
def lowercase(func):
def wrapper():
func_ret = func()
change_to_lowercase = func_ret.lower()
return change_to_lowercase
return wrapper
上のスクリプトでは、関数を引数にとる lowercase
という名前のデコレーターを作成しました。
この lowercase
関数を試すには、新しい関数を作成して、それをこのデコレータに渡します。
Python では関数がファーストクラスなので、関数を変数に代入したり、変数として扱ったりできることに注意してください。
このトリックを使ってデコレータの関数を呼び出すことにしましょう。
def hello_function():
return 'HELLO WORLD'
decorate = lowercase(hello_function)
print(decorate())
出力
hello world
上の2つのコードを1つにまとめることができることに注意してください。
HELLO WORLD” という文章を返す関数 hello_function()
を作成した。
そして、デコレーターを呼び出して、この関数名を変数 decorate に代入しながら引数として渡しています。
実行すると、その結果、文が小文字に変換されたことがわかる。
しかし、Pythonではもっと簡単にデコレータを適用する方法があります。
それは、デコレータ関数の名前の前に@
という記号を、デコレートする関数のすぐ上に追加することです。
@lowercase
def hello_function():
return 'HELLO WORLD'
print(hello_function())
出力
hello world
関数に複数のデコレーターを適用する方法
Python では、1 つの関数に複数のデコレータを適用することができます。
これを正しく行うには、通常のコードと同じ順序でデコレータを適用するようにしましょう。
たとえば、次のようなデコレータを考えてみましょう。
def split_sentence(func):
def wrapper():
func_ret = func()
output = func_ret.split()
return output
return wrapper
ここでは、入力文を受け取ってそれをさまざまなパーツに分割するデコレータを作成しました。
このデコレータには split_sentence
という名前をつけました。
それでは、 lowercase
と split_sentence
デコレータを一つの関数に適用してみましょう。
これらの処理を正しい順序で実行するために、以下のように適用する。
@split_sentence
@lowercase
def hello_function():
return 'HELLO WORLD'
print(hello_function())
出力
出力
helo_functionに
lowercaseと
split_sentence` デコレータを適用したので、文は二つに分割され、小文字に変換された。
デコレーター関数への引数の渡し方
Python のデコレータは、デコレートされた関数に渡される引数をインターセプトすることもできます。
この引数は、実行時にデコレータ関数に渡されます。
次の例を見てみましょう。
['hello', 'world']
出力
def my_decorator(func):
def my_wrapper(argument1, argument2):
print("The arguments are: {0}, {1}".format(argument1, argument2))
func(argument1, argument2)
return my_wrapper
@my_decorator
def names(firstName, secondName):
print("Your first and second names are {0} and {1} respectively".format(firstName, secondName))
print(names("Nicholas", "Samuel"))
上のスクリプトでは、デコレーターは argument1
と argument1
という 2 つの引数を受け取ります。
汎用デコレーターの作成
汎用デコレーターは、任意の関数に適用することができます。
この種のデコレータは、例えばデバッグの際に非常に役に立ちます。
これらのデコレータを定義するには、 args
と **kwargs
という引数を使用します。
すべての位置引数とキーワード引数は、それぞれこの 2 つの変数に格納される。
argsと
kwargs` を使用すると、関数を呼び出す際に任意の数の引数を渡すことができる。
The arguments are: Nicholas, Samuel
Your first and second names are Nicholas and Samuel respectively
出力
def my_decorator(func):
def my_wrapper(*args, **kwargs):
print('Positional arguments:', args)
print('Keyword arguments:', kwargs)
func(*args)
return my_wrapper
@my_decorator
def function_without_arguments():
print("No arguments")
function_without_arguments()
見ての通り、デコレータに引数は渡されませんでした。
では、位置引数にどのように値を渡すかを見てみましょう。
Positional arguments: ()
Keyword arguments: {}
No arguments
出力
出力
デコレータに 3 つの位置引数を渡しました。
キーワード引数を渡すには、関数呼び出しの中でキーワードを使わなければなりません。
以下はその例です。
@my_decorator
def function_with_arguments(x, y, z):
print(x, y, z)
function_with_arguments(5, 15, 25)
出力
Positional arguments: (5, 15, 25)
Keyword arguments: {}
5 15 25
2 つのキーワード引数がデコレータに渡されました。
次の節では、デコレータをデバッグする方法について説明します。
デコレータのデバッグ方法
ここまでで、関数をラップするためにデコレータを使用していることがおわかりいただけたと思います。
ラッパークロージャは、元の関数名やパラメータリスト、そして docstring を隠します。
たとえば たとえば、function_with_arguments
というデコレータのメタデータを取得しようとすると、 ラッパークロージャーのメタデータを取得することになります。
この例を見てみましょう。
@my_decorator
def passing_keyword_arguments():
print("Passing keyword arguments")
passing_keyword_arguments(firstName="Nicholas", secondName="Samuel")
出力
Positional arguments: ()
Keyword arguments: {'secondName': 'Samuel', 'firstName': 'Nicholas'}
Passing keyword arguments
これはデバッグの際に大きな課題となります。
しかし、Pythonはこの課題を解決するために、functools.wraps
デコレーターを提供しています。
これは、失われたメタデータをデコレートされたクロージャにコピーすることで動作します。
では、どのように動作するかデモしてみましょう。
function_with_arguments.__name__
'my_wrapper'
出力
import functools
def lowercase(func):
@functools.wraps(func)
def my_wrapper():
return func().lower()
return my_wrapper
ラッパーの関数に functools.wraps
を使用したので、”hello_function” の関数メタデータを検査することができます。
出力コード
出力
出力
出力
出力
@lowercase
def hello_function():
"Saying hello"
return 'HELLO WORLD'
print(hello_function())
上記のスクリプトは、メタデータがラッパーではなく、関数を参照していることを明確に示しています。
デコレータを定義するときは、常に functools.wraps
を使用することをお勧めします。
そうすれば、デバッグがずっと楽になります。
結論
デコレータの目的は、サブクラスを直接使用したり、 デコレーション対象のクラスやメソッド、関数のソースコードを変更したりせずに、 クラスやメソッド、関数の機能を動的に変化させることです。
今回は、シンプルで汎用的なデコレータの作成方法と、デコレータに引数を渡す方法を見ました。
また、 functools
モジュールを使って、開発中にデコレータをデバッグする方法についても見てきました。