Decorator
はじめに
ここでは Python におけるデコレータについて解説します。
最初にデコレータに関する概論を述べます。
そして、デコレータを理解するために重要な概念である第一級オブジェクトやクロージャ、nonlocal
文などについて説明します。
その上で、デコレータの定義の仕方や使い方について解説し、また標準ライブラリにある使用頻度の高いデコレータの使用例を見ていきます。
最後に、複数のデコレータの適用や、より高度なパラメータ付きのデコレータについても詳述します。
デコレータとは何か
デコレータ (decorator) とは、関数を引数として取り、別の関数を返すような関数のことです。 デコレータにより、引数として与えられた関数を、その定義を変更することなく機能拡張することができます。 通常は、引数の関数に対して何らかの処理を加えるラッパー関数を定義し、それを返り値とするような関数を定義することでデコレータを作成します。 他の関数に対し、機能追加という装飾を施すことから、デコレータと呼ばれているわけです。
たとえば、
>>> def deco(func):
... def wrapper():
... print('running wrapper()')
... func()
... return wrapper
...
>>> def target():
... print('running target()')
...
>>> target = deco(target)
>>> target()
running wrapper()
running target()
において、関数 deco
はデコレータです。
deco
は引数として関数を取ります。
そして、その関数を使用する別の関数 wrapper
を作成し、それを戻り値としています。
wrapper
は、func
を呼び出す前に文字列を出力します。
言い換えれば、wrapper
は func
に、「実行前に文字列を出力する」という機能を付加しています。
最後に、target = deco(target)
により、関数 target
に deco
が適用され、その結果がtarget
へと再代入されます。
target
の呼び出し結果から、その効果を確認することができます。
デコレータを関数に適用する際は、次のように @
記号を利用するのが一般的です:
>>> @deco
... def another_target():
... print('running another_target()')
...
>>> another_target()
running wrapper()
running another_target()
つまり、
@deco
def f(...):
...
と
def f(...):
...
f = deco(f)
は等価です。
以上がデコレータに対する概論となります。 以下では、デコレータを理解するための前提知識や、デコレータに関する文法、使用例、またその応用などについて詳しく解説していきます。
Python における関数
Python では、関数は第一級オブジェクト (first-class object) です。 ここで、第一級オブジェクトとは、
- 変数に代入することができる
- 関数に引数として渡すことができる
- 関数から戻り値として返すことができる
ようなオブジェクトです。実際、
>>> my_print = print # 変数により関数を参照する
>>> my_print('hello from my_print')
hello from my_print
>>> list(map(lambda x: x**2, [1, 2, 3])) # 関数を引数として渡す
[1, 4, 9]
>>> def make_func():
... def inner():
... print('inner')
... return inner # 関数を戻り値として返す
...
>>> make_func()()
inner
からわかるように、Python では、関数を変数へと代入すること、関数の引数として関数を指定すること、関数の戻り値に関数を指定すること、などが可能です。 これらの事実から、Python における関数は第一級オブジェクトであるといえます。
また、引数として関数を取る、あるいは、関数を戻り値とする関数のことを、一般に高階関数 (higher-order function) と呼びます。 関数が第一級オブジェクトである Python は、高階関数をサポートしています。
クロージャ
クロージャとは、自身が定義された環境へとアクセス可能な関数のことです。 環境とは、具体的には、関数が定義されたスコープに存在する変数のことです。 クロージャの英語は Closure ですが、その意味を英和辞書などで調べてみると、閉鎖や封鎖などの訳語が見つかり、何かを閉じ込めること、包み込むことに関係する言葉であることが理解できます。 こうした意味から派生し、プログラミングの文脈におけるクロージャは、自身が定義された環境を包み込んでいるような関数を意味します。
百聞は一見に如かず、クロージャの具体例を見てみましょう:
>>> def make_printer(msg): # msgはmake_printerのローカル変数
... def printer(): # printerはクロージャ
... print(msg) # printerが定義された環境にある変数へとアクセスする
... return printer
...
>>> my_printer = make_printer('hello world')
>>> my_printer()
hello world
まず、関数 make_printer
は、内部で定義された関数を戻り値とする高階関数です。
make_printer
の内部では、関数 printer
が定義されています。
そして、printer
の内部では、自身の定義の外部に定義されている変数 msg
を参照しています。
この関数 printer
こそクロージャです。
続いて、make_printer
に引数 'hello world'
が与えられ、戻り値である関数が、変数 my_printer
へと格納されます。
最後に、my_printer
を呼び出すと、make_printer
に与えた引数 'hello world'
が出力されています。
ここでのポイントは、関数 make_printer
が処理を終えたあとでも、関数 printer
は make_printer
のローカル変数である msg
へとアクセスできるということです。
通常、関数のローカル変数は、その関数の処理が終わると削除されます。
上の例では、msg
は make_printer
のローカル変数であるため、return printer
の行を抜けると msg
にアクセスできなくなるはずです。
ところが、戻り値である printer
の内部から msg
が参照されていることにより、printer
はクロージャとなり、その結果、make_printer
の処理が終わったあとでもそのローカル変数へとアクセスできるようになります。
なお、変数 msg
は、関数 printer
のローカルスコープに束縛されている変数ではありません。
こうした変数を一般に自由変数 (free variable) と呼びます。
関数のローカル変数と自由変数は、関数の __code__
アトリビュートから確認することができます:
>>> printer.__code__.co_varnames # ローカル変数
()
>>> printer.__code__.co_freevars # 自由変数
('msg',)
printer.__code__.co_varnames
と printer.__code__.co_freevars
は、printer
のローカル変数と自由変数をそれぞれ表わします。
printer
にはローカル変数がないため、printer.__code__.co_varnames
は空となっています。
また、自由変数の実際の値は、関数の __closure__
アトリビュートに保持されています:
>>> printer.__closure__[0].cell_contents
'hello world'
nonlocal
文
上の例では、printer
はただ自由変数を参照するだけでした。
これに対し、自由変数の値を変更したいような場合はどうすればいいでしょうか。
次のコードを見てください:
>>> def counter(): # 問題のある実装
... count = 0
... def increment():
... count += 1
... return count
... return increment
...
>>> c = counter()
>>> c()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in increment
UnboundLocalError: local variable 'count' referenced before assignment
関数 counter
は、クロージャである increment
を返す関数です。
increment
は、実行のたびに counter
のローカル変数 count
をカウントアップし、その値を返すことを意図して実装されています。
ところが、これを実際に使ってみると UnboundLocalError
というエラーが発生します。
そのエラーの説明文を読むと、ローカル変数 count
が代入の前に参照されている、と書かれています。
これはどういうことでしょうか。
Python では、関数の内部で代入がおこなわれるとき、値が代入される変数はローカル変数であるとみなされます。
このルールにより、count += 1
の行において、Python は increment
のローカル変数 count
の値をカウントアップしようとします。
ところが、この時点で increment
のスコープにはローカル変数 count
がありません。
その結果、まだ定義されていない変数の値を読み込もうとしたとして、上記エラーが発生します。
これを回避するためには、次のように、increment
の内部の変数 count
がローカルではないことを宣言する必要があります:
>>> def counter():
... count = 0
... def increment():
... nonlocal count # countがローカルではないことを宣言
... count += 1
... return count
... return increment
...
>>> c = counter()
>>> c()
1
>>> c()
2
nonlocal count
という行に注目してください。
nonlocal
文は、与えられた変数がローカル変数ではないことを示すための文です。
nonlocal count
という文により、count += 1
という行において、変数 count
はローカル変数ではないとみなされ、ローカルスコープの外部にある count
が参照されます。
これにより、increment
の内部の count
は自由変数となり、意図通りの結果を得ることができました。
このように、クロージャの内部から外部の環境の値を変更するためには、nonlocal
により変数が自由変数であることを宣言する必要があります。
デコレータの定義
ここまでの議論により、デコレータを理解するための準備が整いました。
冒頭で述べたように、デコレータとは、関数を引数として取り、その機能を拡張した別の関数を返すような関数のことでした。
たとえば、次の関数 timethis
は、与えられた関数の実行に掛かった時間を表示するデコレータです:
import time
def timethis(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(func.__name__, elapsed)
return result
return wrapper
def countdown(n):
while n > 0:
n -= 1
countdown = timethis(countdown)
if __name__ == '__main__':
countdown(1000000)
関数 timethis
は、引数である関数を使用する別の関数 wrapper
を返す高階関数です。
wrapper
はクロージャであり、自身のローカルスコープの外側にある timethis
の引数 func
を自由変数として保持します。
wrapper
の引数は *args
と **kwargs
となっていますが、これらはそれぞれ可変長の位置引数とキーワード引数を表わしており、wrapper
に与えられた引数が func
へとそのまま受け渡されるようにしています。
wrapper
のインターフェイスが func
と整合的になるようにすることが目的です。
また、wrapper
は最後に func
の返り値を返していますが、これも func
と返り値に関する挙動を合わせるための措置となります。
ここでは例として、与えられた整数 n
が 0 となるまで 1 ずつ減算し続ける関数 countdown
を定義し、それを timethis
の引数としています。
その返り値 (timethis
の内部では wrapper
) を countdown
へと代入することで、countdown
を呼び出す際に実行時間が表示されるようになります。
これを実行すると次のような出力となります:
$ python timethis.py
countdown 0.06992554664611816
ここでは timethis
により countdown
の挙動を変更しましたが、同様の方法で timethis
を他の関数へと適用し、その実行時間を計測することができます。
このように、デコレータを定義することで、もとの関数の定義を変更することなく、そこに機能を追加していくことが可能となります。
@
記号によるデコレータの適用
ところで、
def countdown(n):
while n > 0:
n -= 1
countdown = timethis(countdown)
は
@timethis
def countdown(n):
while n > 0:
n -= 1
と書き換えることができます。より一般には、関数 deco
がデコレータであるとき、
def f(...):
...
f = deco(f)
は
@deco
def f(...):
...
とまったく同じことです。
このように、@
記号を使用することでより簡潔にデコレータを適用することができます。
実際にデコレータを利用する場合は、必ずと言っていいほどこの形式が利用されます。
よく使われるデコレータの例
ここでは、標準ライブラリが提供するデコレータの例を見ていきます。
まずは functools.wraps
を紹介します。
次のコードを見てください:
>>> @timethis
... def snooze(n):
... """Sleep for n seconds."""
... time.sleep(n)
...
>>> snooze(1)
snooze 1.001361608505249
>>> snooze.__name__ # 関数の名前
'wrapper'
>>> snooze.__doc__ # 関数のDocstring
>>>
上で定義した timethis
を、n
秒間スリープする関数 snooze
へと適用しています。
動作そのものには問題はありませんが、snooze
のアトリビュートが wrapper
のものへと書き換えられてしまっており、その結果、関数の名前 (__name__
) や Docstring (__doc__
) などの情報が失われてしまっていることがわかります。
snooze
の実体は timethis
の内部で定義されているクロージャであるため、この結果は当然であるということもできますが、もとの関数の情報が失われてしまうのは実用上不便です (doctest を書いている場合は、不便であるだけでなく、テストが実行されないという明らかな問題も生じます)。
こうした問題を解決してくれるのが functools.wraps
です:
>>> from functools import wraps
>>> import time
>>> def timethis(func):
... @wraps(func) # wrapsを適用
... def wrapper(*args, **kwargs):
... start = time.time()
... result = func(*args, **kwargs)
... elapsed = time.time() - start
... print(func.__name__, elapsed)
... return result
... return wrapper
...
>>> @timethis
... def snooze(n):
... """Sleep for n seconds."""
... time.sleep(n)
...
>>> snooze(1)
snooze 1.001321792602539
>>> snooze.__name__
'snooze'
>>> snooze.__doc__
'Sleep for n seconds.'
関数 timethis
の内部で関数を定義する際に、wraps
という、もとの関数を引数に取るデコレータを適用しています (パラメータ付きのデコレータに関してはあとで説明します)。
このデコレータの効果により、timethis
を適用した関数 snooze
のアトリビュートはもとの値を維持しています。
続いて紹介するのは、functools.lru_cache
というデコレータです。
これは、関数の結果をキャッシュする機能を付加するデコレータです。
lru
の部分は Least Recently Used の略であり、直近で最も使われていないデータを削除していくキャッシュアルゴリズムを意味しています。
>>> from functools import lru_cache
>>> @lru_cache()
... @timethis
... def slow_func(n):
... time.sleep(3)
... return n
...
>>> slow_func(3)
slow_func 3.0033774375915527
3
>>> slow_func(3)
3
実行に時間が掛かる処理を模した関数 slow_func
には、二つのデコレータが適用されています (複数のデコレータの適用についてもあとで触れます)。
一つはこれまで使用してきた timethis
、もう一つは lru_cache
です。
後者を適用することで、slow_func
の実行結果がキャッシュされます。
実際、上の結果からわかるように、引数 3 により最初に slow_func
を呼び出した際には 3 秒近くの実行時間となっていますが、二度目の実行においては slow_func
は実行されず、即座に値が返ります。
timethis
による実行時間の表示がおこなわれていないことも、キャッシュが使用されていることを示唆しています。
このように、lru_cache
を使用することにより、関数の結果をキャッシュしてパフォーマンスを向上させることが可能です。
なお、lru_cache
は dict
を使用して結果を保持しますが、関数の位置引数とキーワード引数が dict
のキーとして使われるため、lru_cache
が適用される関数は、すべての引数がハッシュ可能 (hashable) である必要があります。
つまり、たとえば int
や str
、bool
などは関数の引数として指定可能ですが、list
や set
などは指定することができません。
複数のデコレータの適用
直前の例で見たように、デコレータは複数適用することができます。
注意点としては、デコレータが適用される順番が決まっているということです。
たとえば、d1
と d2
というデコレータがあるとき、
@d1
@d2
def f():
...
は
def f():
...
f = d1(d2(f))
と同じ意味になります。 つまり、関数に近い位置のものから順に適用されていくということです。
timethis
と lru_cache
という二つのデコレータを適用した先ほどの例を考えると、timethis
を適用された関数が、さらにlru_cache
を適用されるという順番になっていることがわかります。
パラメータ付きデコレータ
最後に、もう一段階抽象度が高くなる、パラメータ付きのデコレータについて解説します。
上で見た標準ライブラリの functools.wraps
は、利用時に引数を取るデコレータでした。
このように、デコレータを利用する際に引数を取れるようにすることで、デコレータをより細かく制御することができます。
たとえば、適用対象の関数を二回実行するようなデコレータについて考えてみましょう:
>>> def repeat(func):
... def wrapper(*args, **kwargs):
... func(*args, **kwargs)
... func(*args, **kwargs)
... return wrapper
...
>>> @repeat
... def hello():
... print('hello world')
...
>>> hello()
hello world
hello world
関数 repeat
は、与えられた関数 func
を二回実行する関数を返すデコレータです。
上の例では、関数 hello
に適用することで、print
関数が二回実行されています。
ここで、たとえば繰り返しの回数を固定せず、それを repeat
のユーザーが指定できるようにするためにはどうすればいいでしょうか。
こうした場合、デコレータファクトリ (decorator factory) と呼ばれる、デコレータを返す関数を定義することが一般的です:
>>> def repeat(n):
... def repeat_decorator(func):
... def wrapper(*args, **kwargs):
... for _ in range(n):
... func(*args, **kwargs)
... return wrapper
... return repeat_decorator
...
>>> @repeat(3)
... def hello():
... print('hello world')
...
>>> hello()
hello world
hello world
hello world
ネストする階層が深いため、一見するとかなり複雑ですが、実際は今までのパターンとほぼ同一です。
まず、もっとも内側の関数だけ抜き出すと次のようになります:
def wrapper(*args, **kwargs):
for _ in range(n):
func(*args, **kwargs)
wrapper
は任意の引数を取る関数で、与えられた引数を関数 func
に引き渡します。
また、関数の実行回数は for ループにより制御されており、その回数は n
回です。
func
も n
も wrapper
のローカルスコープで定義されていないため、これらはその外部から来た変数であることがわかります。
次に、一つ外側にある関数 repeat_decorator
について見てみましょう:
def repeat_decorator(func):
def wrapper(*args, **kwargs):
...
return wrapper
これについては、今まで見てきたデコレータの定義と完全に一致しています。
wrapper
が使用する func
はこのレイヤーに存在します。
今までの議論が理解できていれば、この部分は難しいことは特にないはずです。
最後に、一番外側の関数を再度見てみましょう:
def repeat(n):
def repeat_decorator(func):
...
return repeat_decorator
上で見たように、内部の関数 repeat_decorator
はデコレータです。
よって、repeat
はデコレータを返す関数、すなわちデコレータファクトリです。
wrapper
が使用する n
はこのレイヤーに存在し、wrapper
の挙動を制御しています。
以上を理解した上で、このデコレータを使用している部分を見てみましょう:
@repeat(3)
def hello():
print('hello world')
ここまでの議論から、repeat
の返り値はデコレータであるので、@repeat(3)
における repeat(3)
の部分は、デコレータであることがわかります。
そしてその実体は、repeat_decorator
です。
つまり、上のコードは、実質的に次のコードと同じことです:
def repeat_decorator(func):
def wrapper(*args, **kwargs):
for _ in range(3):
func(*args, **kwargs)
return wrapper
@repeat_decorator
def hello():
print('hello world')
しかし、様々な n
の値に応じてこうしたデコレータを無数に定義するわけにはいきません。
そこで、可変部分である n
をパラメータとしてもつデコレータファクトリ repeat
を定義し、デコレータを動的に生成して関数に適用しているわけです。
以上、パラメータをもつデコレータ、正確にはデコレータファクトリの作り方を具体例を通じて確認しました。 これをもう少し一般化し、デコレータファクトリの一般的な構造について考えると、次のようになります:
def decorator_factory(argument):
def decorator(function):
def wrapper(*args, **kwargs):
# 前処理やargumentを使った処理
result = function(*args, **kwargs)
# 後処理やargumentを使った処理
return result
return wrapper
return decorator
上で見た repeat
と同一の構造であるので、解説は不要でしょう。
デコレータを書いている際に、一部の処理を可変とする必要がある場合は、このような構造をもつデコレータファクトリを定義することになるはずです。
まとめ
- 第一級オブジェクトとは、以下の操作が可能なオブジェクトのことである
- 変数により参照する
- 関数へ引数として渡す
- 関数から戻り値として返す
- Python では、関数は第一級オブジェクトである
- 高階関数とは、引数として関数を取る、あるいは、関数を戻り値とする関数のことである
- クロージャとは、自身が定義された環境へとアクセス可能な関数のことである
nonlocal
文は- 変数がローカル変数ではないことを示すための文である
- ローカルスコープの外部の変数の値を変更するために用いる
- デコレータは
- 関数を引数として取り、その機能を拡張した別の関数を返すような関数のことである
@
記号を用いて適用する
- デコレータを引数により制御するためには、デコレータを返す関数であるデコレータファクトリを定義する
参考
- PEP 318 — Decorators for Functions and Methods
- Glossary - decorator
- Glossary - hashable
- simeonfranklin.com - Understanding Python Decorators in 12 Easy Steps!
- Primer on Python Decorators – Real Python
- First-class citizen - Wikipedia
- Higher-order function - Wikipedia
- Closure (computer programming) - Wikipedia
- Cache replacement policies - Wikipedia
- Fluent Python
- Python Cookbook, 3rd Edition