今日はニッチな話題、Pythonのデコレーターについてお話ししましょう。
これはその名の通り、関数を装飾するシンプルかつ強力なツールです。
前提となる知識
Pythonでデコレータを扱う前に知っておくべき概念がいくつかあります。
高階関数とネストされた関数です。
1. 高次の関数
Python には、他の関数との間で関数を受け渡ししたり、返したりするための巧妙な方法があります。
これは多くのプログラミング言語ではサポートされておらず、プログラマは様々な多用途の操作を行うことができます。
他の関数を受け取ったり返したりする関数はすべて高次関数と呼ばれます。
例えば
def hof(func, num):
int res = func(num)
return res
|
お気づきのように、hof()
の最初のパラメータは func
で、これは後で呼び出される関数です。
同じように、高階関数も他の関数を返します。
おすすめ記事 – Pythonの再帰的関数
2. ネストされた関数
Pythonのもう一つの特徴として、関数の中に関数を宣言できることが挙げられ、これを便利なネスト関数と呼びます。
次の例を見てください。
def func(num):
def nested_func():
return num
return nested_func
|
ここで、func()
は別の関数を返すので高階関数であり、nested_func()
は別の関数の中で定義されているので(明らかに)ネストされた関数と言えます。
外側の関数に何を送るかによって、ネストされた関数の定義が完全に変わることがわかるでしょう。
これはカプセル化の実装やクロージャの作成に使用されますが、今回のチュートリアルの範囲外です。
Pythonのデコレータとは?
先ほど説明したように、平たく言えばデコレーターは関数を装飾するものです。
つまり、関数が行うことをより良くするために、関数にコードや機能を巻き付けるということです。
では、その例を見てみましょう。
まず最初に、2 つの数値を足すだけの装飾のないシンプルな関数を見てみましょう。
def sum (a, b):
print (a + b)
|
def decorator(function):
def wrapper(num1, num2):
print ( "##" , function.__name__, "of" , num1, "and" , num2, "##" )
function(num1, num2)
return wrapper
|
さて、2つの数値を受け取って何らかの数学的操作を行い、その結果を表示する数学的関数を大量に作るとします(Pythonのprintを参照)。
ここで、結果を表示する前に1行追加して、何が行われているか、どの数字が操作されたかを伝えるとします。
そうすると、出力は次のようになります。
## sum of 1 and 2 ##3
各関数を定義しながらこの行を追加することもできますが、関数が多くて装飾が1行では済まない場合は、デコレータを使用したほうがよいでしょう。
Python デコレータの構文
@decorator def sum (a, b):
print (a + b)
@decorator def difference(a, b):
print (a - b)
@decorator def product(a, b):
print (a * b)
|
このコードを理解するのは少し難しいので、一行ずつ見ていきましょう。
-
def decorator(function)
: ここで注意しなければならないことがいくつかあります。まず第一に、デコレータは関数として定義され、関数のように振る舞います。関数と同じように考えるのが一番です。次に、より重要なこととして、デコレータが受け取る引数はデコレートする関数であるということです。デコレータの名前は何でもいいということに注意しましょう。デコレータは複数の引数を受け取ることもできますが、それはまた別の機会にお話ししましょう。 -
def wrapper(num1, num2)
: これはおそらく、このコードの中で最も混乱する部分でしょう。デコレーターは常に、元の関数に何らかの機能を追加した関数を返さなければなりません。これは一般にラッパー関数と呼ばれます。この新しい関数は元の関数を置き換えるので、元の関数が持っているのと同じ数の引数 (この場合は 2 つ) を受け取らなければなりません。そのため、このデコレータは、引数を 2 つだけ持たない関数には適用できません。ただし、*args
を使用してこれを回避する方法はあります。 -
print(...)
: この例では、デコレータが元の関数に追加する機能を示します。関数名と 2 つの引数を、望んだとおりに表示していることに注意してください。この後、実際の出力が表示されるように関数を実行する必要があります。 -
function(num1, num2)
: wrapper()がどのように
function()` と同じことを行っているかは明らかですが、私たちが必要としている機能が追加されているので、次のステップも明らかです。 -
return wrapper
: 基本的には、decorator()
は私たちから関数を受け取り、wrapper()
を使ってその周りにデコレーションを施し、そして最初の関数を置き換えるwrapper()
を返します。wrapper()` は最初の関数を呼び出して追加の処理をしているので、基本的には最初の関数の拡張版と言えます。
これについては、デコレーターの使い方を見れば、すぐにわかるでしょう。
Python でデコレータを使う
さて、decorator という名前のデコレータを定義したので、それを使って3つの関数、sum (前に見た)、difference、product を強化しましょう。
from functools import wraps
def decorator(function):
@wraps (function)
def wrapper(num1, num2):
print ( "##" , function.__name__, "of" , num1, "and" , num2, "##" )
function(num1, num2)
return wrapper
|
ここで、@
という記号は、デコレータが次の関数で使われることをPythonに伝えるために使われています。
つまり、関数を定義した後、その関数は基本的にデコレータに渡され、デコレータはその関数の拡張バージョンを返します。
デコレータが返す関数は、元の関数を置き換えることになります。
では、その結果を見てみましょう。
sum()` を呼び出すと、その拡張版が実行されることに注意してください。
注意: デコレータを使用すると、関数のメタデータが破壊されます。
この例では、 sum.__name__
を呼び出すと、 sum
ではなく wrapper
が返されます。
docstringも、ラッパーがどのようなdocstringを持っているかに応じて変化します。
これを避けるには、 functools
から wraps
をインポートして、デコレータの中でラッパーを以下のように装飾するだけです。
この場合、ラッパーは関数のメタデータを使用して装飾されるので、 __name__
のような関数のメタデータとその docstring を保持することができます。
まとめ
以上、デコレータの使い方と、「@」記号の意味について詳しく説明しました。
また、別のチュートリアルでお会いしましょう。
参考文献 – https://www.python.org/dev/peps/pep-0318/