28
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

今更聞けない(?)Python知識シリーズ -デコレータ-

Last updated at Posted at 2017-02-27

導入

クラス、メソッド、イテレータ、コンストラクタ...
Python(に限らないですが)でプログラミングをしていると専門用語や概念がたくさん出てきます。
ここでは自分の頭の整理と炎上ラーニングの意味を込めて、今更聞けない用語・使い方を説明していきます。

第一回は__デコレータ__です。(第二回やるかは未定)

デコレータとは

一言で言うと、__関数に機能を付与する__ものです。
ともかく一つサンプルコードを見てみましょう。

print_args_decorator.py
# -*- coding: utf-8 -*-
# 引数を表示するデコレータ
def print_args(func):
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        print('<args is ...>')
        for arg in args:
            print(arg)
        for kwarg in kwargs:
            print('{0}:{1}'.format(kwarg, kwargs[kwarg]))
        return res
    return wrapper

このデコレータで関数をデコレーションしましょう。関数を用意します。

seimei.py
# -*- coding: utf-8 -*-
def seimei(LastName, FirstName):
    """引数を氏名として表示する"""
    print('氏名:{0} {1}'.format(FirstName, LastName))

if __name__ == '__main__':
    seimei(LastName='Jackson', FirstName='Michael')
氏名:Michael Jackson

名前と苗字を受け取って氏名として出力しています。これに先ほど作成したデコレータを付けるとどうなるでしょうか。

seimei_deco.py
# -*- coding: utf-8 -*-
from print_args_decorator import print_args

@print_args  # デコレータ
def seimei(LastName, FirstName):
    """引数を氏名として表示する"""
    print('氏名:{0} {1}'.format(FirstName, LastName))

if __name__ == '__main__':
    seimei(LastName='Jackson', FirstName='Michael')
氏名:Michael Jackson
<args is ...>
LastName:Jackson
FirstName:Michael

本来の関数の出力と一緒に、引数の値を出力するようになりました。これは、氏名を返す関数に__引数を出力する機能を付与__したためです。なんかわからんけど便利そう

デコレータの作り方

デコレータの内部で何が行われているかは、以下の記事で詳しく解説しています。

この記事と上記サンプルコードをもとに、デコレータの基本的な動作を説明します。(上記事はプログラミングにおける重要な概念であるスコープなどについても触れているので、Python初学者は一読をお勧めします。)

関数を受け取り、関数を返す

ます、デコレータとして機能する部分を作ります。サンプルコードだとdef print_args(func):の階層です。
この関数は、引数に関数を指定しています。また同階層のreturnステートメントを見ると、wrapperという関数を返していることがわかります。関数を受け取って、その関数を加工して(ラッパーにして)返しているわけですね。

関数実行と別に行う操作を定義する

次に、付与する機能を定義する部分を作ります。サンプルコードだとdef wrapper(*args, **kwargs):の階層です。
この関数では、まず上層で受け取ったfuncを実行しています。funcの引数に制約を設けたくないため、*args, **kwargsを引数としています。デコレータは複数の関数に機能して真価を発揮するので、*args, **kwargsを引数とすることが多いです。
funcを実行した後に、引数をprintしています。**kwargsは引数をdict型で格納するため、引数が関数作成時の順番ではなくdict準拠のソート順になることに注意してください。

デコレータの使い方

デコレータを作ったら関数をデコレーションしましょう。
デコレータは、関数定義の直前でアットマークを付けて呼び出します。サンプルコードのseimei_deco.pyでは、デコレータprint_argsをsemei関数の直前で@print_argsによって呼び出しています。
これでseimei関数はもともとの動作に加えて「関数実行後に引数をprintする」ようになりました。

上記例とは別の関数にデコレータを付与してみましょう。
デコレータはどんな関数にも付与可能なことが良さなので、引数に*args, **kwargsを指定するなど、なるべく汎用的な利用が可能なように作成するといいと思います。

sum_deco.py
from print_args_decorator import print_args

@print_args
def sum_print_args(*args):
    return sum(args)

if __name__ == '__main__':
    sum_num = sum_print_args(1, 3)
    print('SUM = {0}'.format(sum_num))
<args is ...>
1
3
SUM = 4

functools.wrapsの利用

デコレータを使って関数を生成すると、ドキュメント文字列などの情報が失われてしまいます。
これは、デコレータを作成する際に__functools.wraps__を使うことで防げます。
先ほど作成したseimei_deeco.pyのドキュメント文字列を確認しましょう。

>>> from seimei_deco import seimei
>>> print(seimei.__doc__)
None

ドキュメント文字列はNoneになってしまいます。
functools.wrapsを使ってデコレータを定義し直し、生成した関数のドキュメント文字列を確認してみます。

seimei_deco_use_functools.py
# -*- coding: utf-8 -*-
# 引数を表示するデコレータ
def print_args(func):
    import functools
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        print('<args is ...>')
        for arg in args:
            print(arg)
        for kwarg in kwargs:
            print('{0}:{1}'.format(kwarg, kwargs[kwarg]))
        return res
    return wrapper

@print_args  # デコレータ
def seimei(LastName, FirstName):
    """引数を氏名として表示する"""
    print('氏名:{0} {1}'.format(FirstName, LastName))

if __name__ == '__main__':
    seimei(LastName='Jackson', FirstName='Michael')
>>> from seimei_deco_use_functools import seimei
>>> print(seimei.__doc__)
引数を氏名として表示する

もとの関数のドキュメント文字列が表示されました。ここでは、functools.wraps自体をデコレータとして利用していることがポイントです。(このページを参考にしました)
先にも述べましたが、デコレータはなるべく汎用的な利用ができるように作成するべきなので、基本的にfunctools.wrapsは利用した方がいいと思います。

使いどころ

デコレータはどんな関数にも付与することができます。そのため、コードの中で繰り返し同じ動作をする際に威力を発揮します
デコレータではなく関数を作成し、関数定義の中でいちいち呼び出すことで同様の操作ができますが、関数定義の1行前にアットマークでちょろっと書いてあることで、コードの可読性を上げることができます。モジュール内の各メソッドについて、エラーログを収集したいときなどに非常に有効です。

また、モジュールのコードを見ていると、@propertyというデコレータがたまに出てきます。これは関数をプロパティ化するものですが、ここでは詳細を省きます。このページなどを参考にしてください。

参考

28
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?