寒月記

住みにくいところをどれほどか寛容て

Python のデコレータの基本:使い方から functools.wraps の利用まで

※Qiita からこちらにも記事を移しました

前回の記事 の冒頭で、LINE Bot作成中、そしてその中で分からなかった概念を勉強中である、と書きました。 前回でクロージャについては勉強できたので、次はデコレータについてまとめます。

LINE Bot の公式サンプルは Flask を利用しているので、コード中に @、すなわちデコレータが多く出てきます。 やはり少し調べた程度ではピンとこない機能だったので、まとめてみました。
なお、本来の目的に照らし、ここではデコレータの機能の一部に焦点化して解説しています*1

「Flask で LINE Bot 作ろう」
->「デコレータでルーティングしてる、デコレータ理解してないから勉強しよう」<- 今ここ
->「デコレータはクロージャを使った仕組みらしいけどクロージャ分からないから勉強しよう」

1. デコレータの理解に必要な知識・仕組み

本記事で紹介する範囲のデコレータの理解に必要な知識・仕組みを以下に挙げます。

  • クロージャ
    • 変数のスコープ
    • 第一級オブジェクトとしての関数
      • 関数を引数として渡す
      • 関数を返り値として返す
    • 関数のネスト
  • 可変長引数

クロージャ関数のネスト については、前回の記事「Pythonのクロージャについて: 関数のスコープと、関数が第一級オブジェクトであることからちゃんと考える」 で解説していますので、よろしければそちらをご覧ください。

可変長引数 については、ここでは解説しませんが割と理解しやすくかつ重要な知識ですので、ぜひ確認してみてください。 簡単に言うと名前の通りで、受け渡す実引数の数を動的に変えられる仮引数(の記述方法) です。

2. デコレータとは:概要

はじめに、「そもそもデコレータとはどんなもので、利用することでどんなメリットがあるか」を、現時点の私の理解に基づき簡単に述べます。

デコレータ
主に、既存の関数の実装自体は 変更せずに 、その関数に追加の処理を加える目的で作られる関数。処理追加対象の関数を引数として受け取り、追加処理を実装した新たな関数を返す。

メリット

  1. 関数の部品化の促進
  2. コードの重複を減らす

以上 2点により、コードの可読性・保守性を向上できる

利用例

  • 共通のロガーを実装
  • 共通のバリデータを実装 etc.

デコレータの応用はもちろん他にもあります。 例えば以下のサイトなどに多くの例が掲載されています。 https://wiki.python.org/moin/PythonDecoratorLibrary

それでは、順を追って解説していきます。

3. デコレータの基本

改めて、デコレータの定義を公式ドキュメントから引用してみます*2

decorator (デコレータ) 別の関数を返す関数で、通常、@wrapper構文で関数変換として適用されます。デコレータの一般的な利用例は、classmethod() と staticmethod() です。
デコレータの文法はシンタックスシュガーです。次の2つの関数定義は意味的に同じものです。

```
def f(...):
...
f = staticmethod(f)

@staticmethod
def f(...):
...
```

同じ概念がクラスにも存在しますが、あまり使われません。デコレータについて詳しくは、関数定義およびクラス定義のドキュメントを参照してください。

なるほど、わからん。 と思っていましたが、クロージャについて勉強した結果、少しは言いたいことが分かってきました。

この定義における重要な点は、以下の三つと考えられます。

  1. デコレータは、関数を返す関数
  2. @ を使う構文を用い、関数変換として利用されることが多い
  3. デコレータ文法はシンタックスシュガーなので、@を用いずとも等価の表現ができる より簡易な説明として、"O'Reilly 入門 Python3" では、デコレータを入力として関数を一つ取り、別の関数を返す関数としています。

では、公式ドキュメントの例だと実際の動きをイメージしづらいので、"Python3 入門" の例を見てみましょう*3

デコレータの作り方

ここでは、document_it という、引数として受け取った関数の「関数名」「引数」「実行結果」を表示するデコレータを作成します。

def document_it(func):
    def new_function(*args,**kwargs):
        print('Running function: ', func.__name__)
        print('Positional arguments: ', args)
        print('Keyword arguments: ', kwargs)
        result = func(*args,**kwargs)
        print('Result: ', result)
        return result
    return new_function

document_it.py

この document_it という関数が、デコレータ、すなわち引数として受け取った関数をデコレート (修飾) する関数です。
上の定義で見た通り、関数 (func) を受け取って関数 (new_function) を返しています。

デコレータの使い方

今度は実際に、今作った document_it という関数を、デコレータとして使ってみます。
デコレータの使い方としては、公式ドキュメントの定義にあるように @ を使うことが多いのですが、この構文はあくまでシンタックスシュガーなので、実際の動きを追いやすいよう @ を使わないで書いてみます。

なお、ここでは、2つの引数の和を返すだけの add_ints という関数をデコレートします。

def document_it(func):
    def new_function(*args,**kwargs):
        print('Runnig function: ', func.__name__)
        print('Positional arguments: ', args)
        print('Keyword arguments: ', kwargs)
        result = func(*args,**kwargs)
        print('Result: ', result)
        return result
    return new_function


def add_ints(a, b):
    return a + b


decorated_add_ints = document_it(add_ints)  # document_it() に add_intsを渡し、
                                            # その結果返される new_functionを
                                            # decorated_add_intsに格納

decorated_add_ints1.py

@ を使わずに書くとこんな感じです。 前回の記事を見ていただいている方などは見覚えがある形かと思うのですが、これはクロージャです。
引数もローカル変数のように振舞うので、デコレータは クロージャを利用したテクニック と言えます。

では実際の実行結果を見てみます。

>>> decorated_add_ints(3, 5)  # new_functionは可変長引数を受け取るので、すべての引数を受け取れる
Runnig function:  add_ints    # ただし、new_function内で document_itが受け取った funcを実行しているので、
Positional arguments:  (3, 5) # funcが処理できる引数でないと resultの定義時にエラーとなることに注意
Keyword arguments:  {}
Result:  8
8
>>>

result-decorated_add_ints1

このように、デコレータを利用した decorated_add_ints を実行することで、単に引数の和を返すだけでなく、document_it の処理も実行しています。

ただし、被修飾関数の add_ints の処理が実行されているのは、new_function 内の result = func(*args,**kwargs)add_ints を実行し、return result で結果を返しているためです。
この処理がないと、単に new_function の処理を行うだけになるので、実装時は気をつけないと、以下の例の様に単なる異なる関数の実行となってしまいます (公式ドキュメントも「関数処理の追加」ではなく「関数変換」と書いています)。

def document_it(func):
    def new_function(*args,**kwargs):
        print('Runnig function: ', func.__name__)
        print('Positional arguments: ', args)
        print('Keyword arguments: ', kwargs)
#         result = func(*args,**kwargs)
#         print('Result: ', result)
#         return result
    return new_function


def add_ints(a, b):
    return a + b


decorated_add_ints = document_it(add_ints)

decorated_add_ints1_not_add_but_change.py

>>> decorated_add_ints(3, 5)
Runnig function:  add_ints
Positional arguments:  (3, 5)
Keyword arguments:  {}
>>>

result-decorated_add_ints1_not_add_but_change

デコレータのシンタックスシュガー

上の記述で、デコレータの実装は完了です。

しかし、既に書いたように、よく目にするデコレータの書き方は、@ を使うものです。 これは、可読性を高めるためのものであり、@ を使うことでデコレータがすっきり書けます。

では、@ を使って document_it デコレータを add_ints に適用してみます。

def document_it(func):
    def new_function(*args,**kwargs):
        print('Runnig function: ', func.__name__)
        print('Positional arguments: ', args)
        print('Keyword arguments: ', kwargs)
        result = func(*args,**kwargs)
        print('Result: ', result)
        return result
    return new_function


@document_it          # 被修飾関数定義の上に @{デコレータ} でデコレータ適用
def add_ints(a, b):
    return a + b

decorated_add_ints2.py

>>> add_ints(3, 5)
Runnig function:  add_ints
Positional arguments:  (3, 5)
Keyword arguments:  {}
Result:  8
8
>>>

result-decorated_add_ints2

実行結果も同じものとなっています。 こちらの方が、確かに見た目上はすっきりしています。

4. デコレータの応用

以上の基本を踏まえ、少しだけ応用の紹介をします。

複数のデコレータの適用

一つの関数に対し、デコレータを複数適用することができます。
実際の例として、以下を見てみます。

def document_it(func):
    def new_function_doc(*args,**kwargs):
        print('Runnig function: ', func.__name__)
        print('Positional arguments: ', args)
        print('Keyword arguments: ', kwargs)
        result = func(*args,**kwargs)
        print('Result: ', result)
        return result
    return new_function_doc


def square_it(func):
    def new_function_sq(*args,**kwargs):
        result = func(*args,**kwargs)
        return result * result
    return new_function_sq


@document_it
@square_it
def add_ints(a, b):
    return a + b

two_decorators1.py

add_ints に、document_it と、関数実行結果を二乗する square_it の 2つの関数がデコレータとして適用されています。

実行結果を見てみましょう。

>>> add_ints(3, 5)
Runnig function:  new_function_sq
Positional arguments:  (3, 5)
Keyword arguments:  {}
Result:  64
64
>>>

result-two_decorators1

確かに、add_ints(3, 5) の結果が二乗されたものになっており、かつ document_it の処理も行われています。

こんな風に、単純にデコレータを重ねるだけで、複数のデコレータの適用が可能です。

複数デコレータの適用順

ところで、複数のデコレータを適用した場合、適用順はどうなるのでしょうか。 書き方に関わらず、結果は変わらないのか、それとも一定の実行順があるのでしょうか。

two_decorators1.pyの、document_itsquare_it の記述順を変えて実行してみます。

def document_it(func):
    def new_function_doc(*args,**kwargs):
        print('Runnig function: ', func.__name__)
        print('Positional arguments: ', args)
        print('Keyword arguments: ', kwargs)
        result = func(*args,**kwargs)
        print('Result: ', result)
        return result
    return new_function_doc


def square_it(func):
    def new_function_sq(*args,**kwargs):
        result = func(*args,**kwargs)
        return result * result
    return new_function_sq


@square_it
@document_it
def add_ints(a, b):
    return a + b

two_decorators2.py

実行結果は以下です。

>>> add_ints(3, 5)
Runnig function:  add_ints
Positional arguments:  (3, 5)
Keyword arguments:  {}
Result:  8
64
>>>

result-two_decorators2

実行結果を比較すると、Result: の後に続く値が、two_decorators1.py では 64two_decorators2.py では 8と、異なった結果となっています。

print 文を仕込んで、実行順を確認してみます。

def document_it(func):
    print('inside of document_it')
    def new_function_doc(*args,**kwargs):
        print('Runnig function: ', func.__name__)
        print('Positional arguments: ', args)
        print('Keyword arguments: ', kwargs)
        result = func(*args,**kwargs)
        print('Result: ', result)
        print('new_function_doc ended')
        return result
    return new_function_doc


def square_it(func):
    print('inside of square_it')
    def new_function_sq(*args,**kwargs):
        result = func(*args,**kwargs)
        print('new_function_sq ended')
        return result * result
    return new_function_sq


@square_it
@document_it
def add_ints(a, b):
    return a + b

two_decorators2_add_print.py

>>> @square_it
... @document_it
... def add_ints(a, b):
...     return a + b
...
inside of document_it         #1 これと
inside of square_it           #2 これは add_intsにデコレータを適用した時点で出力
>>>
>>> add_ints(3, 5)
Runnig function:  add_ints
Positional arguments:  (3, 5)
Keyword arguments:  {}
Result:  8                    #3 先に document_itが実行されているので resultは 8
new_function_doc ended        #4 これと
new_function_sq ended         #5 これはそれぞれの new_func処理時に出力
64
>>>

result-two_decorators2_add_print

result-two_decorators2_add_print を見ると分かる通り、この例ではdocument_it -> square_it の順で処理が実行されています。 このように、デコレータを複数適用した場合、被修飾関数に近いものから順に処理されます。 result を二乗する square_it より先に document_it が実行されているので、 #3では result が 8となっています。

なお、デコレータの複数適用を @ を使わず記述すると、以下のようになり、可読性が下がります。 しかし、分解したことで処理順などは分かりやすくなっています。

def document_it(func):
    print('inside of document_it')
    def new_function_doc(*args,**kwargs):
        print('Runnig function: ', func.__name__)
        print('Positional arguments: ', args)
        print('Keyword arguments: ', kwargs)
        result = func(*args,**kwargs)
        print('Result: ', result)
        print('new_function_doc ended')
        return result
    return new_function_doc


def square_it(func):
    print('inside of square_it')
    def new_function_sq(*args,**kwargs):
        result = func(*args,**kwargs)
        print('new_function_sq ended')
        return result * result
    return new_function_sq


def add_ints(a, b):
    return a + b

add_ints = square_it(document_it(add_ints)) #1

# または以下:
# add_ints = document_it(add_ints)
# add_ints = square_it(add_ints)

two_decorators2_for_tracing_procedure.py

ちなみに、#1の処理を順を追うと、以下となります。
これが自力で分解できれば、前提知識を含め、デコレータの基礎は理解できていると言えるのではないでしょうか。


  1. document_it に関数オブジェクト add_ints を渡す
  2. document_it 内で定義されている new_function_doc が返される
  3. この時点で、#1は add_ints = square_it(new_function_doc) となる
  4. square_it(new_function_doc) で、square_it 内で定義されている new_function_sq が返る
  5. すなわち、add_ints = new_function_sq となる
  6. add_ints(3, 5) すなわち new_function_sq(3, 5) が実行開始
  7. result = func(*args,**kwargs) が展開されて result = new_function_doc(3, 5) となる     ※変数 add_ints に束縛したことで #1はクロージャとなるため、square_it(new_function_doc) で渡した実引数 new_function_doc が保持されている
  8. new_function_doc(3, 5) が実行開始
  9. print が走り、かつ result = func(*args,**kwargs) が展開されて result = add_ints(3, 5) となる     ※7同様、変数 add_ints に束縛したことで #1はクロージャとなるため、document_it(add_ints) で渡した実引数 add_ints が保持されている
  10. add_ints(3, 5) が実行された結果 result = 8 となるため、print('Result: ', result)Result: 8 となり、また result = 8 が呼び出し元である new_function_sq に返される
  11. print('new_function_sq ended') が実行され、result * result、すなわち (8 * 8) が返される

この流れと、result-two_decorators2_add_print を見比べてみてください。 スッキリしない場合はクロージャなどの前提知識の理解も含め、ぜひじっくり確認してみてください。

メタ情報の書き換えへの対策

ところで、ここまで触れずにいましたが、よく見ると result-two_decorators1result-two_decorators2 で、print('Runnig function: ', func.__name__) の結果が異なっています。

>>> add_ints(3, 5)    # square_it -> document_itの順に適用
Runnig function:  new_function_sq
Positional arguments:  (3, 5)
Keyword arguments:  {}
Result:  64
64
>>>

result-two_decorators1

>>> add_ints(3, 5)    # document_it -> square_itの順に適用
Runnig function:  add_ints
Positional arguments:  (3, 5)
Keyword arguments:  {}
Result:  8
64
>>>

result-two_decorators2

一見不思議な結果ですが、先ほど詳しく追った、複数デコレータ適用時の処理の流れを考えてみると理解できます。 分かりやすい方から見てみましょう。

result-two_decorators2 では、document_it -> square_it の順で実行されます。 すなわち、add_ints = square_it(document_it(add_ints)) となり、func.__name__ を出力している document_it に渡される関数オブジェクトは add_ints であるため、Runnig function: add_ints となります。

一方、result-two_decorators1 では、square_it -> document_it の順で実行されます。 こちらは add_ints = document_it(square_it(add_ints)) となり、func.__name__ を出力している document_it に渡される関数オブジェクトは square_it(add_ints)、すなわち new_function_sq であるため、Runnig function: new_function_sq となります。

この様にデコレータを使うと、引数として渡す元の関数とは異なる新しい関数(デコレータ内で returnしている関数) を扱うことになるため、元の関数自体が持っていたアトリビュート (メタ情報) は参照されなくなってしまいます。 今回出力したものは func.__name__ のみでしたが、例えば func.__doc__ なんかももちろん元の関数のものではなくなってしまいます。

これを解決するために、functoolsモジュールの wraps関数 を利用します。 ちょっと説明が難しいので百聞は一見に如かず、利用例を見てみましょう。

from functools import wraps


def document_it(func):
    def new_function_doc(*args,**kwargs):
        print('Runnig function: ', func.__name__)
        print('Positional arguments: ', args)
        print('Keyword arguments: ', kwargs)
        result = func(*args,**kwargs)
        print('Result: ', result)
        return result
    return new_function_doc


def square_it(func):
    @wraps(func)
    def new_function_sq(*args,**kwargs):
        result = func(*args,**kwargs)
        return result * result
    return new_function_sq


@document_it
@square_it
def add_ints(a, b):
    return a + b

decorator_wraps_use.py

two_decorators1.py での問題は、square_it -> document_it の順でデコレータを適用した際に、func.__name__ というメタ情報を出力する document_it元の関数を受け取れなくなっていた事でした。 すなわち、square_it デコレータの処理が終わった時点で、元々の関数 add_ints ではなく new_func_sq が返され、これが実引数として、document_it に渡されていたことが問題でした。

そこで、decorator_wraps_use.py にあるように、デコレータ (ここでは square_it) が返す関数 (ここでは new_func_sq) を、元の関数を引数として受け取る wraps 関数でデコレートすることで、元々の関数のメタ情報を引き継ぐことができます。

実行結果を見てみます。

>>> add_ints(3, 5)
Runnig function:  add_ints
Positional arguments:  (3, 5)
Keyword arguments:  {}
Result:  64
64
>>>

result-decorator_wraps_use

確かに、functools.wrap 関数を利用したことで、two_decorators1.py とは異なり、func.__name__ が元々の関数名となっていることが分かります。 なお、square_it の内部で @wraps を利用しており、一方で document_it の内部では利用していない理由は、既に見たように処理の流れを追うと分かります。

@wraps を利用した結果、new_function_sq 実行後に返される new_function_sq のメタ情報が、square_it に引数として渡した元の関数、add_ints のものとなります。 つまり、次の document_it 実行時、これに渡される引数 funcnew_function_sq ですがそのメタ情報は add_ints のものであり、関数名の出力文は func.__name__ なので、document_itadd_ints のメタ情報を持つ引数を渡せた時点で問題はなくなっています。

少し複雑になりました。 デコレータの利用時には、メタ情報の扱いも意識し、必要に応じて functools.wrap 関数を利用できるとよいですね。

5. まとめ

長くなりましたが、最後に本記事の解説のまとめを書いておきます。

  • 定義

    デコレータ
    主に、既存の関数の実装自体は 変更せずに 、その関数に追加の処理を加える目的で作られる関数。処理追加対象の関数を引数として受け取り、追加処理を実装した新たな関数を返す。

  • @ を利用した構文はあくまでシンタックスシュガー、実態はクロージャ

  • 複数のデコレータの適用時は、被修飾関数に近い順に実行される
  • デコレータは新しい関数を返すものなので、元々の関数のメタ情報を保持して利用したい場合は、functoools.wrap 関数を用いる
  • 分からなくなったら @ を使わない構文に直し、処理を追ってみると分かりやすいです

入門 Python 3

入門 Python 3

*1:ここでは基本・関数に焦点化しているので、staticmethod やクラスのデコレータには触れません。

*2:https://docs.python.org/ja/3/glossary.html#term-decorator

*3:以降利用しているコードは、"O'Reilly 入門 Python3" を一部改変したものです。