[Python] 怖くない!デコレータ

メリークリスマス!全然関係ないけどデコレータの記事頑張って書きました。
できるだけわかりやすいように意識して書いたつもりです。

基本

デコレータとは?

「関数の処理を修飾(デコレート)する」、つまり関数の前後に処理を付け加える技術という説明が多いように思います。これでも決して間違ってはいませんが、私は正確ではないと思っています。

多くの人が思っている以上にデコレータとは簡単なものです。あまり難しく考えないでください。
デコレータの動作を一言で表すなら単に対象オブジェクトを差し替える技術です。

定義と要件

「引数を1つ受け取る呼び出し可能オブジェクト」がデコレータの最小の要件です。以下を見てください。

>>> # このように「何もしない」かつ「何も返却しない」関数でも
>>> def deco(f):
...     pass
 
>>> # 定義時にエラーは出ない。つまりデコレータとしての要件は満たしている
>>> @deco
... def test(a):
...     print(a)
 
>>> # しかしtestはNoneとなるため実行できません(decoが返却したNoneと差し替わるため)
>>> test(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable

上記の動作を見てわかるとおり、デコレータの返却値が定義対象の関数(またはクラス)とすげ変わります。動きとしては以下の代入文と同じです。デコレータは単なるシンタックスシュガーに過ぎません。

>>> test = deco(test)

単純なことですが、デコレータを理解する上ではとても重要なことです。返却する値に制限がないということは、デコレータの設定されている関数やクラスは記述されているものとは全く違う処理(引数)であったり、そもそもオブジェクトタイプ自体全く異なる可能性もあるということです。実装者はこのことに注意を払うべきですし、想定できない値が返却されるのであれば、コメント(docstring)などに記述するべきでしょう。

通常は関数デコレータなら関数を、クラスデコレータならクラスを返却します。

>>> def deco(f):
...     def _wrap(*args, **kwargs):
...         """この関数が対象関数とすげ変わる"""
...         print('前処理')
...         f(*args, **kwargs)
...         print('後処理')
...     # 返却するのを忘れずに
...     return _wrap    
 
>>> @deco
... def test(a):
...     """本来の関数"""
...     print(a)
 
>>> # decoによって作られた_wrapが実行されている
>>> test('メイン処理')
前処理
メイン処理
後処理

これでよく見る形になりました。
このように対象関数の周りにいろいろと処理を追加して装飾(デコレート)するのが関数デコレータの基本的な使い方です。
「*args, **kwargs」についてはこちらを参照してください(このために書いたんです)。

関数デコレータの場合は大抵、上記例のように本来とは違う関数が返却されるはずです。
ということは当然メタ的な情報は失われてしまいますよね。

>>> # (さっきの続きから)
>>> # 変数名こそ「test」だが、その実態は「_wrap」
>>> # 関数名やdocstringは「test」ではなく「_wrap」のものになってしまう
>>> print(test.func_name)
_wrap
>>> print(test.__doc__)
この関数が対象関数とすげ変わる

functools.wraps

これを解決するのがfunctools.wrapsデコレータです。
返却対象の関数に、本来の関数を引数に指定したwrapsを設置するだけです。

>>> import functools
>>> def deco(f):
...     @functools.wraps(f)...     def _wrap(*args, **kwargs):
...         """この関数が対象関数とすげ変わる"""
...         print('前処理')
...         f(*args, **kwargs)
...         print('後処理')
...     return _wrap
 
>>> @deco
... def test(a):
...     """本来の関数"""
...     print(a)
 
>>> # 関数名は元のまま
>>> print(test.func_name)
test
>>> # docstringも元のまま
>>> print(test.__doc__)
本来の関数

デコレータを定義するためにデコレータを使うなんてなかなか洒落てますね。
通常、関数デコレータは関数の実行をネストさせることによりその動作を変えるため、wrapsデコレータによって更にネスト(関数呼び出し)が増えてしまうと思うかもしれませんが、実はそんなことはありません。

以下のシンプルなデコレータについて考えてみましょう。

>>> def deco(f):
...     @functools.wraps(f)
...     def test2(*args, **kwargs):
...         f(*args, **kwargs)
...     return test2
 
>>> @deco
... def test():
...     pass

デコレータ記法を分解したのが以下です。

>>> import functools
 
>>> # testは上記の例で言うとtestとfに当たる
>>> def test(): pass
 
>>> # test2は上記の例で言うとtest2に当たる
>>> def test2(): pass
 
>>> # _decoは上記の例では該当なし(代入されないので)
>>> _deco = functools.wraps(test)
 
>>> # test3は上記の例では書き換え後のtestに当たる
>>> test3 = _deco(test2)
 
>>> # test2(wrapsにデコレートされた関数)とtest3(返却された関数)は同じオブジェクト
>>> test2 is test3
True

オブジェクトが等しいということはwrapsは引数と同じ関数を返却しているということですね。functools.wrapsはメタ情報のみを変更するため、関数をネストさせる必要がないんです、きっと。
本当はソースをちゃんと見て確かめたかったんですが、functools.pyの中で呼ばれている関数がビルトインだったため中まで追えませんでした。(すまんな)

「test3はtest2の返却結果だから同じなのは当たり前じゃないの?」と指摘されて誤解してしまったのですが、ここで確認したいのは「_decoによって処理された結果が同じオブジェクトか」ということなのです。

構造が変わらないのであれば、使わない手はありませんね。デコレータを実装する機会があれば是非functools.wrapsデコレータを使いましょう。

クロージャ

関数デコレータではクロージャという技術がよく使われます。一体どういうことでしょうか。
Pythonには「Local」「Enclosing(正確にはEnclosing Function’s)」「Global」「Builtin」という4つのスコープがあり、その頭文字をとって「LEGB」とか呼ばれています。「Enclosing」というあまり聞きなれないスコープは関数がネストされた場合の外側の関数を指します。これにより外側で定義した変数を内側の関数に閉じ込めることができます。これをクロージャと呼びます。デコレータに限らず結構よく使われます。

>>> def deco(f):
...     val1 = 'enclosing1'
...     def _wrap(*args, **kwargs):
...         # 外側で宣言された変数を参照できる
...         print(val1)
...         f(*args, **kwargs)
...         print(val2)
...     # 呼び出し関数より後ろで定義されていてもいい
...     val2 = 'enclosing2'
...     return _wrap
 
>>> @deco
... def test(a):
...     print(a)
 
>>> test('test')
enclosing1
test
enclosing2

閉じ込められた変数は以降、内側の関数からしか参照できないため名前空間を汚しません。

少し脱線しますが、Enclosingスコープから渡された変数を上書きする際には注意が必要です。
関数内で行われる代入は「global」など特別な宣言がなければLocal変数とみなされるためです。
Enclosingスコープで定義された変数で呼び出し回数をカウントするような例で考えてみます。

>>> def deco(f):
...     i = 0
...     def _wrap(*args, **kwargs):
...         i += 1
...         print('{0}回目の実行'.format(i))
...         f(*args, **kwargs)
...     return _wrap
 
>>> @deco
... def test(a):
...     print(a)
 
>>> test('test')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in _wrap
UnboundLocalError: local variable 'i' referenced before assignment

「i += 1」という代入が存在するためiは_wrapのLocal変数とみなされてしまったようです。_wrap関数内ではiに初期値を与えてないためエラーとなりました。ベストな対応策ではないかもしれませんが、ミュータブルなオブジェクトの書き換えで代用することもできますね。

>>> def deco(f):
...     i = [0]  # ミュータブルなら何でもいい(classなど)
...     def _wrap(*args, **kwargs):
...         i[0] += 1
...         print('{0}回目の実行'.format(i[0]))
...         f(*args, **kwargs)
...     return _wrap    
 
>>> @deco
... def test(a):
...     print(a)
 
>>> test('test')
1回目の実行
test
 
>>> test('test2')
2回目の実行
test2
 
>>> test('test3')
3回目の実行
test3

Python3系ではnonlocalを宣言しておくことで代入が行われてもLocalとみなされないようです。

>>> def deco(f):
...     i = 0
...     def _wrap(*args, **kwargs):
...         nonlocal i
...         i += 1
...         print('{0}回目の実行'.format(i))
...         f(*args, **kwargs)
...     return _wrap
 
>>> @deco
... def test(a):
...     print(a)
 
>>> test('test')
1回目の実行
test
 
>>> test('test2')
2回目の実行
test2
 
>>> test('test3')
3回目の実行
test3

(脱線終わり)

引数を受け取るデコレータ

デコレータが引数を受け取れるようになれば動的に動作を変更することができるので便利ですね(小並感)。
基本は普通のデコレータと同じなんですが、関数のネストが増えるので難しいと感じる方も多いかもしれません。

デコレータの引数で受け取った回数、対象関数を繰り返し実行する例を考えてみます。

>>> def deco(I):
...     print('{0}回繰り返し実行します'.format(I))
...     def _deco(f):
...         def _wrap(*args, **kwargs):
...             # 最外で受け取った引数をここでも参照できる
...             for i in range(I):
...                 print [i],
...                 f(*args, **kwargs)
...         return _wrap
...     return _deco
 
>>> @deco(1)
... def test1(a):
...     print(a)
1回繰り返し実行します
 
>>> @deco(2)
... def test2(a):
...     print(a)
2回繰り返し実行します
 
>>> @deco(3)
... def test3(a):
...     print(a)
3回繰り返し実行します
 
>>> test1('test')
[0] test
>>> test2('test')
[0] test
[1] test
>>> test3('test')
[0] test
[1] test
[2] test

引数を受け取る分階層が一つ増えました。でもよく見ると中の構造「関数を1つ受け取り、関数を返す」という基本的な構造は変わっていませんね。(同じように呼び出せるように関数名は少しだけ変えましたけど)

引数を受け取るデコレータの動作は以下のような感じです。

  1. デコレータをセット(@deco(1))した時点で関数として実行されてネストが剥がれる
  2. 一つ内側の「関数を受け取る」関数(_deco)がデコレータとして実行される
  3. 一番内側の関数(_wrap)が装飾後の関数として返却される
>>> #(さっきの続き)
>>> test1.func_name
'_wrap'
>>> test2.func_name
'_wrap'
>>> test3.func_name
'_wrap'

これは個人的な考え方ですが、一番外側の関数はデコレータというより「デコレータを返すクロージャ(ファクトリ)」と理解したほうがわかりやすいかもしれません。

デコレータの構文制限

このルールに沿えば、いくつでもネストさせることができるのではと思うかもしれませんが、どうやらそれは不可能なようです。

>>> def deco():
...     def _deco():
...         def __deco():
...             def ___deco(f):
...                 def _wrap(*args, **kwargs):
...                     print(0)
...                     f()
...                     print(2)
...                 return _wrap
...             return ___deco
...         return __deco
...     return _deco()
 
>>> def test(): print 1
 
>>> # 余計なネストの分だけ実行して剥がしていけば
>>> test2 = deco()()(test)
>>> # 最終的に_wrapが取れる
>>> test2
<function _wrap at 0x7f4175baca28>
>>> test2()
0
1
2
 
>>> # でも同じことをデコレータでやろうとするとエラーになる
>>> @deco()()
  File "<stdin>", line 1
    @deco()()
           ^
SyntaxError: invalid syntax

デコレータ設置の際は最初に実行された関数の後に続けて何かを書くということができないようです。
上記の例は関数を必要以上にネストさせるという意味のない例でしたが、メソッドチェーンのような書き方もできないので注意が必要です。

>>> class A(object):
...     def set_value(self, value):
...         self.value = value
...         return self
...     
...     def deco(self, f):
...         def _wrap(*args, **kwargs):
...             print(self.value)
...             f(*args, **kwargs)
...         return _wrap
 
>>> a = A()
>>> @a.set_value(0).deco
  File "<stdin>", line 1
    @a.set_value(0).deco
                   ^
SyntaxError: invalid syntax
 
>>> def test():
...     print(1)
 
>>> test()
1
 
>>> # ちょっとめんどくさいけどこのように書く
>>> a = A().set_value(0)
>>> @a.deco
... def test():
...     print(1)
 
>>> test()
0
1

クラスデコレータ

関数と同じようにクラスに対してもデコレータを設置することができます。
関数デコレータが「動作」を変更するのに対し、クラスデコレータはその属性を変更します(基本的に)。

value属性を追加するだけのクラスデコレータは次のようになります。

>>> def deco(cls):
>>>     """インスタンスメソッドも関数なのでデコレータにできる"""
...     cls.value = 100
...     return cls
 
>>> @deco
... class Test(object):
...     pass
 
>>> # decoによってvalue属性が追加されている
>>> test = Test()
100

関数デコレータとは違いネストさせる必要がないためシンプルです。関数デコレータよりわかりやすいです。
もちろん関数デコレータと同じように引数を受け取るようにすることもできます。その場合はクラスデコレータをラップする関数のネストが増えるだけです。

疲れたのでここまで。