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 を呼び出す前に文字列を出力します。 言い換えれば、wrapperfunc に、「実行前に文字列を出力する」という機能を付加しています。 最後に、target = deco(target) により、関数 targetdeco が適用され、その結果が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 が処理を終えたあとでも、関数 printermake_printer のローカル変数である msg へとアクセスできるということです。 通常、関数のローカル変数は、その関数の処理が終わると削除されます。 上の例では、msgmake_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_varnamesprinter.__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_cachedict を使用して結果を保持しますが、関数の位置引数とキーワード引数が dict のキーとして使われるため、lru_cache が適用される関数は、すべての引数がハッシュ可能 (hashable) である必要があります。 つまり、たとえば intstrbool などは関数の引数として指定可能ですが、listset などは指定することができません。

複数のデコレータの適用

直前の例で見たように、デコレータは複数適用することができます。 注意点としては、デコレータが適用される順番が決まっているということです。 たとえば、d1d2 というデコレータがあるとき、

@d1
@d2
def f():
    ...

def f():
    ...

f = d1(d2(f))

と同じ意味になります。 つまり、関数に近い位置のものから順に適用されていくということです。

timethislru_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 回です。 funcnwrapper のローカルスコープで定義されていないため、これらはその外部から来た変数であることがわかります。

次に、一つ外側にある関数 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 と同一の構造であるので、解説は不要でしょう。 デコレータを書いている際に、一部の処理を可変とする必要がある場合は、このような構造をもつデコレータファクトリを定義することになるはずです。

まとめ

参考