LoginSignup
6
10

More than 3 years have passed since last update.

Python の汎用デコレータ(decorator)フレームワークを作る

Last updated at Posted at 2020-02-07

Python の "デコレータ(decorator)" を作ってみることにします。
汎用的なデコレータの枠組み(フレームワーク)を作成します。

  1. 引数を渡さないデコレータ
  2. 引数を渡すデコレータ
  3. これらを統合した枠組みのデコレータ

を作ります。
最初にデコレータのフレームワーク(ゴール)を提示し、それに向かって作っていきます。

デコレータのフレームワーク

この記事は、読み下すように書いている(試行錯誤を繰り返しながらゴールに向かう)のと、長文なので、先にデコレータの書き方(フレームワーク、枠組み)だけ記載しておきます。テストを含めたフルバージョンは最後に記載します。忙しい方は、時間のある時にゆっくりご覧ください。

  • ※ 制限事項: デコレータの第1引数に呼び出し可能なオブジェクトを指定できません。
decorator_framework.py
from functools import wraps

def my_decorator( *args, **kwargs ):
    # 引数を取らないことが明確なデコレータはここからの部分を
    # _my_decorator の名前を変えてグローバルで define することも可
    def _my_decorator( func ):
        # _my_decorator_body() を定義する前に必要な処理があれば、ここに書く
        print( "_my_decorator_body() を定義する前に必要な処理があれば、ここに書 く" )
        @wraps(func)
        def _my_decorator_body( *body_args, **body_kwargs ):
            # 前処理はここで実行
            print( "前処理はここで実行", args, kwargs, body_args, body_kwargs )
            try:
                # デコレートした本体の実行
                ret = func( *body_args, **body_kwargs )
            except:
                raise
            # 後処理はここで実行
            print( "後処理はここで実行", args, kwargs, body_args, body_kwargs )
            return ret

        # デコレータが記載された時に処理が必要な場合にはここに書く #2
        print( "デコレータが記載された時に処理が必要な場合にはここに書く #2" )
        return _my_decorator_body

    # 引数を取らないことが明確なデコレータはここまで

    # デコレータが記載された時に処理が必要な場合にはここに書く #1
    print( "デコレータが記載された時に処理が必要な場合にはここに書く #1" )

    if len(args) == 1 and callable( args[0] ):
        # 引数無しでデコレータが呼ばれた場合はここで処理
        print( "No arguments" )
        return _my_decorator( args[0] )

    else:
        # 引数ありでデコレータが呼ばれた場合はここで処理
        print( "There are some arguments:", args )
        return _my_decorator

さらにシンプルに。

decorator_framework.py シンプル版
from functools import wraps

def my_decorator( *args, **kwargs ):
    def _my_decorator( func ):
        # _my_decorator_body() を定義する前に必要な処理があれば、ここに書く
        @wraps(func)
        def _my_decorator_body( *body_args, **body_kwargs ):
            # 前処理はここで実行
            try:
                # デコレートした本体の実行
                ret = func( *body_args, **body_kwargs )
            except:
                raise
            # 後処理はここで実行
            return ret

        # デコレータが記載された時に処理が必要な場合にはここに書く #2
        return _my_decorator_body

    # デコレータが記載された時に処理が必要な場合にはここに書く #1

    if len(args) == 1 and callable( args[0] ):
        # 引数無しでデコレータが呼ばれた場合はここで処理
        return _my_decorator( args[0] )

    else:
        # 引数ありでデコレータが呼ばれた場合はここで処理
        return _my_decorator

では、早速デコレータ(decorator)の作成に取り掛かりましょう。

デコレータ(decorator) とは

用語集にはデコレータ(decorator)について、以下のように記載されています。

decorator

(デコレータ) 別の関数を返す関数で、通常、 @wrapper 構文で関数変換として適用されます。デコレータの一般的な利用例は、 classmethod()staticmethod() です。

デコレータの文法はシンタックスシュガーです。次の2つの関数定義は意味的に同じものです:

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

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

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

(decorator - 用語集 — Python 3.8.1 ドキュメント より引用)

上の引用の例では、@staticmethod がデコレータです。

また、関数定義の項目の記載は次のようになっています。

関数定義は一つ以上の デコレータ 式でラップできます。デコレータ式は関数を定義するとき、関数定義の入っているスコープで評価されます。その結果は、関数オブジェクトを唯一の引数にとる呼び出し可能オブジェクトでなければなりません。関数オブジェクトの代わりに、返された値が関数名に束縛されます。複数のデコレータはネストして適用されます。例えば、以下のようなコード:

@f1(arg)
@f2
def func(): pass

は、だいたい次と等価です

def func(): pass
func = f1(arg)(f2(func))

ただし、前者のコードでは元々の関数を func という名前へ一時的に束縛することはない、というところを除きます。

(関数定義 - 8. 複合文 (compound statement) — Python 3.8.1 ドキュメント より引用)

デコレータの文法はシンタックスシュガーです

「シンタックスシュガー」という言葉が聞き慣れない方もいるでしょう。簡単に言ってしまえば、複雑な(わかりにくい)記述を簡単な記法に置き換える、あるいはその逆が可能なものを「シンタックスシュガー」と言います。

つまり、デコレータは書き換え可能な置き換え、ということで、他の言語では「マクロ」などと呼んでいるものに近いものになります。(ただし、置き換え方が決まっています)

用語集の引用内の例で考えてみましょう。

例1
def f(...):
    ...
f = staticmethod(f)  

この例1と

例2
@staticmethod
def f(...):
    ...

この例2が同じものだと書いてあります。どういうことでしょうか。

例1 が行っていることは、
1. f() という関数を定義する。
2. 定義した関数 f() (関数オブジェクト) を引数として staticmethod() を実行する。
3. staticmethod() からの戻り値を、改めて関数 f にする。
ということになります。

これを簡略化して書いたのが例2になり、@staticmethod を「デコレータ(decorator)」と呼びます。

その後、実際に関数 f() が呼び出されると、staticmethod() の戻り値として再定義された関数が呼び出されるという仕組みです。

デコレータの構造

@staticmethod は実際に標準で定義されたデコレータなので、サンプルとしては都合が悪いため、以下は @my_decorator として話を進めます。

上の例だけでわかることがいくつかあります。

  1. デコレータ my_decorator は関数である。(正確には、「呼び出し可能なオブジェクト」)
  2. デコレータ(関数) my_decorator() は関数オブジェクトを引数とする。
  3. デコレータ(関数) my_decorator() は、関数を戻り値とする。

ということです。

最初の一歩

上でわかったことをコードにすると以下になります。

my_decorator 最初の一歩
def my_decorator(func):
    return func

何もせずに、引数で受け取った関数をそのまま返しています。
@ を使わない呼び出しは sample 001 のようになります。

sample 001
>>> def my_decorator_001(func):
...     return func
...
>>> def base_func():
...     print( "in base_func()" )
...
>>> base_func = my_decorator_001( base_func )
>>> base_func()
in base_func()
>>>

@ を使って、デコレータとして使う場合は、sample 002 のようになります。

sample 002
>>> def my_decorator_001(func):
...     return func
...
>>> @my_decorator_001
... def base_func():
...     print( "in base_func()" )
...
>>> base_func()
in base_func()
>>>

これでは何が起こっているのかわかりませんね。しかし、@my_decorator_001 を入力したところで、継続入力待ちになっているところは注目ポイントです。

my_decorator() の中で、表示を追加してみましょう。

sample 003
>>> def my_decorator_002(func):
...     print( "in my_decorator_002()" )
...     return func
...
>>> def base_func():
...     print( "in base_func()" )
...
>>> base_func = my_decorator_002(base_func)
in my_decorator_002()
>>> base_func()
in base_func()
>>>

次にデコレータを使ってみます。

sample 004
>>> def my_decorator_002(func):
...     print( "in my_decorator_002()" )
...     return func
...
>>> @my_decorator_002
... def base_func():
...     print( "in base_func()" )
...
in my_decorator_002()
>>> base_func()
in base_func()
>>>

@my_decorator_002 を書いたところで関数 my_decorator_002() が実行されず、その下の def base_func(): の定義が終わったところで in my_decorator() が表示されています。これは sample 003 で base_func = my_decorator_002(base_func) を実行したときと同じことが起きている、というのが注目ポイントになります。

戻り値の関数は何でもいいのか

これまでは、デコレータの引数に与えられた関数をそのまま return で返していました。
この戻り値の関数は何でもいいのでしょうか。

別な関数を返すとどうなるか、まずは、グローバルな関数を使って試してみましょう。

sample 005
>>> def global_func():
...     print( "in global_func()" )
...
>>> def my_decorator_005(func):
...     print( "in my_decorator_005()" )
...     return global_func
...
>>> @my_decorator_005
... def base_func():
...     print( "in base_func()" )
...
in my_decorator_005()
>>> base_func()
in global_func()
>>>

base_func() を実行することで、問題なく global_func() が呼び出されました。
しかし、当然ですが、元の base_func() は実行されていません。

処理を追加して元の関数を実行する

新しい関数を実行しつつ、元の関数を実行することを考えます。

元の関数はデコレータ関数の引数で渡されているので、これを呼び出せば元の関数が実行されます。

sample 006
>>> def global_func():
...     print( "in global_func()" )
...
>>> def my_decorator_006(func):
...     print( "in my_decorator_006()" )
...     func()  # 元の関数の呼び出し
...     return global_func
...
>>> @my_decorator_006
... def base_func():
...     print( "in base_func()" )
...
in my_decorator_006()
in base_func()      # ここで元の関数を呼び出してしまっている
>>> base_func()     # ※ ここで元の関数を呼び出したい
in global_func()
>>>

元の関数 base_func() は呼び出されましたが、これは、@my_decorator_006 が指定されたときです。
base_func() を呼び出したいタイミングは、「※」で base_func() を呼び出したときです。
つまり、global_func() の中で、base_func() を呼び出したいわけです。

少し修正を加えて、global_func() の中で呼び出せるようにしてみましょう。

sample 007
>>> # 元の関数を保持しておく
... original_func = None
>>>
>>> def global_func():
...     global original_func
...     print( "in global_func()" )
...     original_func() # オリジナル関数が呼び出されるはず
...
>>> def my_decorator_007(func):
...     global original_func
...     print( "in my_decorator_007()" )
...     original_func = func # 引数に渡された func をグローバル original_func へ
...     return global_func
...
>>> @my_decorator_007
... def base_func():
...     print( "in base_func()" )
...
in my_decorator_007()
>>> base_func()     # ※ ここで元の関数を呼び出したい
in global_func()
in base_func()
>>>

少しややこしいですが、グローバル変数 original_func を用意しておいて、ここに引数で渡された func を保存しておくことで、global_func() から呼び出すことができました。

ところが、これでは一つ問題があります。
デコレータを複数の関数に対して使おうとすると、期待通りに動きません。

sample 008
>>> # 元の関数を保持しておく
... original_func = None
>>>
>>> def global_func():
...     global original_func
...     print( "in global_func()" )
...     original_func() # オリジナル関数が呼び出されるはず
...
>>> def my_decorator_007(func):
...     global original_func
...     print( "in my_decorator_007()" )
...     original_func = func # 引数に渡された func をグローバル original_func へ
...     return global_func
...
>>> @my_decorator_007
... def base_func():
...     print( "in base_func()" )
...
in my_decorator_007()
>>> @my_decorator_007
... def base_func_2():
...     print( "in base_func_2()" )
...
in my_decorator_007()
>>> base_func()     # "in base_func()" が表示されてほしい
in global_func()
in base_func_2()     "in base_func()" ではなく "in base_func_2()" が表示された
>>> base_func_2()   # "in base_func_2()" が表示されてほしい
in global_func()
in base_func_2()
>>>

base_func() の呼び出しで、base_func_2() が実行されてしまいました。
グローバル変数 global_func は一つしかないので、最後に代入された base_func_2 が実行されています。

デコレータを複数回使用できるようにする (1)

デコレータを複数回使用できるようにするために、「クロージャ (Closure)」 の話をします。

次のような sample_009.py を見てみましょう。

sample_009.py
 1  def outer(outer_arg):
 2      def inner(inner_arg):
 3          # outer_arg と inner_arg を出力する
 4          print( "outer_arg: " + outer_arg + ", inner_arg: " + inner_arg )
 5      return inner    # inner() 関数オブエクトを返す
 6
 7  f1 = outer("first")
 8  f2 = outer("second")
 9
10  f1("f1")
11  f2("f2")

7行目の f1 = outer("first") の後に、8行目で f2 = outer("second") を実行します。
それぞれ inner 関数オブジェクトが代入されますが、10行目の f1("f1") と 11行目の f2("f2") はどうなるでしょう。

8行目で f2 = outer("second") としていますので、4 行目の print() では、outer_arg"second" になります。

10 行目の f1("f1") と 11行目の f2("f2") の出力は、それぞれ

outer_arg: second, inner_arg: f1
outer_arg: second, inner_arg: f2

となりそうですが……

実行してみると次のようになります。

sample 009
>>> def outer(outer_arg):
...     def inner(inner_arg):
...         # outer_arg と inner_arg を出力する
...         print( "outer_arg:", outer_arg, ", inner_arg:", inner_arg )
...     return inner    # inner() 関数オブエクトを返す
...
>>> f1 = outer("first")
>>> f2 = outer("second")
>>>
>>> f1("f1")
outer_arg: first, inner_arg: f1
>>> f2("f2")
outer_arg: second, inner_arg: f2
>>>

これは、
1. def inner(): ... という inner() 関数定義は、outer() 関数が呼ばれるたびに「実行」される。
2. inner() 関数が定義されている(定義を実行している)時に参照している外側の outer_arg は確定して(評価されて) inner によって保持される。
という動きをするためです。これをクロージャ(Closure)と言います。
外側のスコープにある変数は、実行時ではなく、定義時に評価されます。

デコレータを複数回使用できるようにするために、これを利用します。

デコレータを複数回使用できるようにする (2)

sample 009 ではグローバル関数を使いましたが、クロージャの機能を使うためにデコレータ関数の内部で関数を定義します。

sample_010.py
def my_decorator_010(func):
    print( "in my_decorator_010()" )
    def inner():
        print( "in inner() and calling", func )
        func()
        print( "in inner() and returnd from ", func )
    print( "in my_decorator_010(), leaving ..." )
    return inner

@my_decorator_010
def base_func():
    print( "in base_func()" )

@my_decorator_010
def base_func_2():
    print( "in base_func_2()" )

base_func()     # "in base_func()" が表示されてほしい
base_func_2()   # "in base_func_2()" が表示されてほしい

これを実行(インタラクティブに入力)してみます。

sample 010
>>> def my_decorator_010(func):
...     print( "in my_decorator_010()" )
...     def inner():
...         print( "in inner() and calling", func )
...         func()
...         print( "in inner() and returnd from ", func )
...     print( "in my_decorator_010(), leaving ..." )
...     return inner
...
>>> @my_decorator_010
... def base_func():
...     print( "in base_func()" )
...
in my_decorator_010()
in my_decorator_010(), leaving ...
>>> @my_decorator_010
... def base_func_2():
...     print( "in base_func_2()" )
...
in my_decorator_010()
in my_decorator_010(), leaving ...
>>> base_func()     # "in base_func()" が表示されてほしい
in inner() and calling <function base_func at 0x769d1858>
in base_func()
in inner() and returnd from  <function base_func at 0x769d1858>
>>> base_func_2()   # "in base_func_2()" が表示されてほしい
in inner() and calling <function base_func_2 at 0x769d1930>
in base_func_2()
in inner() and returnd from  <function base_func_2 at 0x769d1930>
>>>

期待通りの結果が得られました。

ここまでを整理すると、デコレータは次のようにコーディングすればよいことがわかりました。

sample 011
def my_decorator_011(func):
    def inner():
        # 呼び出し前の処理をここに書く
        func()
        # 呼び出し後の処理をここに書く
    return inner

戻り値を返す関数に対するデコレータ

ここまでのデコレータは、
1. 値を返さない
2. 引数がない
という関数に対して使用できました。

汎用的なデコレータを作るのであれば、上の2つを満たす必要があります。

戻り値を考慮したプログラムは以下のようになります。

sample_012.py
def my_decorator_012(func):
    def inner():
        # 呼び出し前の処理をここに書く
        ret = func()
        # 呼び出し後の処理をここに書く
        return ret
    return inner

@my_decorator_012
def base_func_1():
    print( "in base_func_1()" )
    return 1

@my_decorator_012
def base_func_2():
    print( "in base_func_2()" )

r1 = base_func_1()
print( r1 )
base_func_2()

実行結果は以下の通り。

sample 012
>>> def my_decorator_012(func):
...     def inner():
...         # 呼び出し前の処理をここに書く
...         ret = func()
...         # 呼び出し後の処理をここに書く
...         return ret
...     return inner
...
>>> @my_decorator_012
... def base_func_1():
...     print( "in base_func_1()" )
...     return 1
...
>>> @my_decorator_012
... def base_func_2():
...     print( "in base_func_2()" )
...
>>> r1 = base_func_1()
in base_func_1()
>>> print( r1 )
1
>>> base_func_2()
in base_func_2()
>>>

戻り値がない場合 (base_func_2()) でも動作しました。

引数を取る関数に対するデコレータ

参照: parameter - 用語集 — Python 3.8.1 ドキュメント

関数の引数は、可変長位置パラメータと可変長キーワードパラメータによって、引数の個数、キーワード指定が定まらないものを仮引数として受け取ることができます。

  • 可変長位置: (他の仮引数で既に受けられた任意の位置引数に加えて) 任意の個数の位置引数が与えられることを指定します。このような仮引数は、以下の args のように仮引数名の前に * をつけることで定義できます:

    def func(*args, **kwargs): ...
    
  • 可変長キーワード: (他の仮引数で既に受けられた任意のキーワード引数に加えて) 任意の個数のキーワード引数が与えられることを指定します。このような仮引数は、上の例の kwargs のように仮引数名の前に ** をつけることで定義できます。

(parameter - 用語集 — Python 3.8.1 ドキュメントより引用)

簡単に言ってしまえば、関数の引数として (*args, **kwargs) を指定すれば、可変の引数を受け取ることができるということです。

これを利用すると、引数を取る関数に対するデコレータは、次のように書くことができます。

sample_013.py
def my_decorator_013(func):
    def inner( *args, **kwargs ):
        # 呼び出し前の処理をここに書く
        ret = func( *args, **kwargs )
        # 呼び出し後の処理をここに書く
        return ret
    return inner

@my_decorator_013
def base_func_1(arg1, arg2, arg3="arg3"):
    print( "in base_func_1({}, {}, {})".format(arg1, arg2, arg3 ) )
    return 1

@my_decorator_013
def base_func_2():
    print( "in base_func_2()" )

r1 = base_func_1("arg1","arg2")
print( r1 )
base_func_2()

以下が実行結果です。

sample 013
>>> def my_decorator_013(func):
...     def inner( *args, **kwargs ):
...         # 呼び出し前の処理をここに書く
...         ret = func( *args, **kwargs )
...         # 呼び出し後の処理をここに書く
...         return ret
...     return inner
...
>>> @my_decorator_013
... def base_func_1(arg1, arg2, arg3="arg3"):
...     print( "in base_func_1({}, {}, {})".format(arg1, arg2, arg3 ) )
...     return 1
...
>>> @my_decorator_013
... def base_func_2():
...     print( "in base_func_2()" )
...
>>> r1 = base_func_1("arg1","arg2")
in base_func_1(arg1, arg2, arg3)
>>> print( r1 )
1
>>> base_func_2()
in base_func_2()
>>>

例外処理

例外についても考慮しておきます。

sample_014.py
def my_decorator_014(func):
    def inner( *args, **kwargs ):
        # 呼び出し前の処理をここに書く
        try:
            ret = func( *args, **kwargs )
        except:
            raise
        # 呼び出し後の処理をここに書く
        return ret
    return inner

@my_decorator_014
def base_func_1(arg1, arg2, arg3="arg3"):
    print( "in base_func_1({}, {}, {})".format(arg1, arg2, arg3 ) )
    return 1

@my_decorator_014
def base_func_2():
    print( "in base_func_2()" )
    na = 1 / 0      # ゼロ除算の例外が発生する

r1 = base_func_1("arg1","arg2")
print( r1 )

try:
    base_func_2()
except ZeroDivisionError:
    print( "Zero Division Error" )

以下が実行結果です。

sample 014
>>> def my_decorator_014(func):
...     def inner( *args, **kwargs ):
...         # 呼び出し前の処理をここに書く
...         try:
...             ret = func( *args, **kwargs )
...         except:
...             raise
...         # 呼び出し後の処理をここに書く
...         return ret
...     return inner
...
>>> @my_decorator_014
... def base_func_1(arg1, arg2, arg3="arg3"):
...     print( "in base_func_1({}, {}, {})".format(arg1, arg2, arg3 ) )
...     return 1
...
>>> @my_decorator_014
... def base_func_2():
...     print( "in base_func_2()" )
...     na = 1 / 0      # ゼロ除算の例外が発生する
...
>>> r1 = base_func_1("arg1","arg2")
in base_func_1(arg1, arg2, arg3)
>>> print( r1 )
1
>>>
>>> try:
...     base_func_2()
... except ZeroDivisionError:
...     print( "Zero Division Error" )
...
in base_func_2()
Zero Division Error
>>>

デコレータに引数を渡す

デコレータは関数(呼び出し可能なオブジェクト)なので、これに引数を渡すことを考えます。

最初に引用した関数定義の記載でも引数があるデコレータの例が書かれていました。

@f1(arg)
@f2
def func(): pass
def func(): pass
func = f1(arg)(f2(func))

デコレータがネストしていますので、単純化のために一つのデコレータだけで考えます。

@my_decorator('引数')
def func(arg1, arg2, arg3):
    pass

これは、下と等価です。

def func(arg1, arg2, arg3):
    pass
func = my_decorator('引数')(func)

my_decorator('引数') の呼び出しで返ってきた関数(仮に _my_decorator とします)を使って _my_decorator(func) を実行するということです。

ネストが一段深くなり、一番外側の関数(デコレータ)が引数を受け取り、その中に今までのデコレータが入る形になります。

sample015.py
def my_decorator_015( arg1 ):
    def _my_decorator( func ):
        def inner( *args, **kwargs ):
            print( "in inner, arg1={}, func={}".format(arg1, func.__name__) )
            ret = func( *args, **kwargs )
            print( "in inner leaving ..." )
            return ret
        return inner
    return _my_decorator

以下が実行結果です。

sample 015
>>> def my_decorator_015( arg1 ):
...     def _my_decorator( func ):
...         def inner( *args, **kwargs ):
...             print( "in inner, arg1={}, func={}".format(arg1, func.__name__) )
...             ret = func( *args, **kwargs )
...             print( "in inner leaving ..." )
...             return ret
...         return inner
...     return _my_decorator
...
>>> @my_decorator_015('引数')
... def f_015( arg1, arg2, arg3 ):
...     print( "in func( {}, {}, {} )".format(arg1, arg2, arg3) )
...
>>> f_015( "引数1", "引数2", "引数3" )
in inner, arg1=引数, func=f_015
in func( 引数1, 引数2, 引数3 )
in inner leaving ...
>>>

引数付きデコレータを呼び出した時には、下の部分の arg1 と func が _my_decorator_body に保存されています。

             print( "in _my_decorator_body, arg1={}, func={}".format(arg1, func.__name__) )

そのため、f_015() を呼び出した時には、保存された arg1 と func が参照されています。

一旦、まとめ

引数をとらないデコレータ

引数をとらないデコレータは以下のようになります。

sample_014.py
def my_decorator_014(func):
    def inner( *args, **kwargs ):
        # 呼び出し前の処理をここに書く
        try:
            ret = func( *args, **kwargs )
        except:
            raise
        # 呼び出し後の処理をここに書く
        return ret
    return inner

引数をとるデコレータ

引数を渡すデコレータは以下のようになります。

sample_016.py
def my_decorator_016( arg1 ):
    def _my_decorator( func ):
        def inner( *args, **kwargs ):
            # 呼び出し前の処理をここに書く
            try:
                ret = func( *args, **kwargs )
            except:
                raise
            # 呼び出し後の処理をここに書く
            return ret
        return inner
    return _my_decorator

引数の有無を吸収するデコレータ

引数を前提にしたデコレータに、引数を与えずに使用するとどうなるでしょうか。

sample_017.py
def my_decorator_017( arg1 ):
    def _my_decorator( func ):
        def inner( *args, **kwargs ):
            # 呼び出し前の処理をここに書く
            try:
                ret = func( *args, **kwargs )
            except:
                raise
            # 呼び出し後の処理をここに書く
            return ret
        return inner
    return _my_decorator

@my_decorator_017
def f_017( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_017.__name__, arg1, arg2, arg3) )

f_017( "引数1", "引数2", "引数3" )
実行結果
$ python sample_017.py
Traceback (most recent call last):
  File "sample_017.py", line 21, in <module>
    f_017( "引数1", "引数2", "引数3" )
TypeError: _my_decorator() takes 1 positional argument but 3 were given
$

これは、@my_decorator_017 に引数を渡していないため、f_017 = my_decorator_017(f_017) として呼び出されているためです。
これを避ける方法が2つあります。

  1. デコレータに引数を指定しない場合でも必ず () をつけ、可変長引数で受け取る。
  2. デコレータに引数を指定しない場合には、単一の関数オブジェクトが渡されるので、それで判定する。

1つ目の例です。

sample_018.py
def my_decorator_018( *args, **kwargs ):
    def _my_decorator( func ):
        def inner( *inner_args, **inner_kwargs ):
            # 呼び出し前の処理をここに書く
            try:
                ret = func( *inner_args, **inner_kwargs )
            except:
                raise
            # 呼び出し後の処理をここに書く
            return ret
        return inner
    return _my_decorator

@my_decorator_018()
def f_018( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_018.__name__, arg1, arg2, arg3) )

f_018( "引数1", "引数2", "引数3" )
実行結果
$ python sample_018.py
in inner( 引数1, 引数2, 引数3 )

2つ目の方法は、デコレータの第1引数を判定に使います。

sample_019.py
def my_decorator_019( *args, **kwargs ):
    def _my_decorator( func ):
        def inner( *inner_args, **inner_kwargs ):
            # 呼び出し前の処理をここに書く
            try:
                ret = func( *inner_args, **inner_kwargs )
            except:
                raise
            # 呼び出し後の処理をここに書く
            return ret
        return inner
    if len(args) == 1 and callable(args[0]):
        return _my_decorator( args[0] )
    else:
        return _my_decorator

@my_decorator_019
def f_019_1( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_019_1.__name__, arg1, arg2, arg3) )

@my_decorator_019()
def f_019_2( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_019_2.__name__, arg1, arg2, arg3) )

@my_decorator_019('arg')
def f_019_3( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_019_3.__name__, arg1, arg2, arg3) )

f_019_1( "引数1", "引数2", "引数3" )
f_019_2( "引数1", "引数2", "引数3" )
f_019_3( "引数1", "引数2", "引数3" )

デコレータに渡される引数と f_019_*() が呼び出される際に渡される引数を区別するために、それぞれ ( *args, **kwargs )( *inner_args, **inner_kwargs ) で区別しています。

実行結果
$ python sample_019.py
in inner( 引数1, 引数2, 引数3 )
in inner( 引数1, 引数2, 引数3 )
in inner( 引数1, 引数2, 引数3 )

これで、汎用的なデコレータの枠組みが出来上がった…… と思いたいところですが…… 呼び出した関数名 ( f_019_*.__name__ ) がすべて inner になってしまいました。

最後の仕上げ

元の関数の代わりに、inner という関数が呼ばれていることはわかります。
しかし、元の関数では、名前 (__name__) やドキュメント (__doc__) などの属性を参照するかもしれません。

これを回避することができる、@wraps というデコレータが用意されています。

参照: @functools.wraps() - functools --- 高階関数と呼び出し可能オブジェクトの操作 — Python 3.8.1 ドキュメント
参照: functools.update_wrapper() - functools --- 高階関数と呼び出し可能オブジェクトの操作 — Python 3.8.1 ドキュメント

これを def inner の前にデコレートすると、外側の関数 (_my_decorator()) に渡された引数 func の属性をラップ(wrap)して、保持することができます。

sample_020.py
from functools import wraps

def my_decorator_020( *args, **kwargs ):
    def _my_decorator( func ):
        @wraps(func)
        def inner( *inner_args, **inner_kwargs ):
            # 呼び出し前の処理をここに書く
            try:
                ret = func( *inner_args, **inner_kwargs )
            except:
                raise
            # 呼び出し後の処理をここに書く
            return ret
        return inner
    if len(args) == 1 and callable(args[0]):
        return _my_decorator( args[0] )
    else:
        return _my_decorator

@my_decorator_020
def f_020_1( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_020_1.__name__, arg1, arg2, arg3) )

@my_decorator_020()
def f_020_2( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_020_2.__name__, arg1, arg2, arg3) )

@my_decorator_020('arg')
def f_020_3( arg1, arg2, arg3 ):
    print( "in {}( {}, {}, {} )".format(f_020_3.__name__, arg1, arg2, arg3) )

f_020_1( "引数1", "引数2", "引数3" )
f_020_2( "引数1", "引数2", "引数3" )
f_020_3( "引数1", "引数2", "引数3" )

sample_019.py に対して、from functools import wrapsdef inner() の前に @wraps(func) を追加したものです。
func_my_decorator() の引数として渡された関数です。

実行結果
$ python sample_020.py
in f_020_1( 引数1, 引数2, 引数3 )
in f_020_2( 引数1, 引数2, 引数3 )
in f_020_3( 引数1, 引数2, 引数3 )
$

関数の名前 (f_019_*.__name__) が inner ではなく、元の関数の名前として保持されています。

長い道のりでしたが、ようやくデコレータ(decorator)のフレームワークが出来上がりました。

汎用的に作成したので、def my_decorator( *args, **kwargs ): となっていますが、受け取る引数が確定している場合には、明確化した方がよいでしょう。

decorator_framework.py Full Version

decorator_framework.py Full Version
#!/usr/bin/env python
# -*- coding: utf-8 -*-

###########################################
#   デコレータ (decorator)
###########################################
from functools import wraps

def my_decorator( *args, **kwargs ):
    """
    for doctest

    >>> @my_decorator
    ... def f1( arg1 ):
    ...     print( arg1 )
    ...
    デコレータが記載された時に処理が必要な場合にはここに書く #1
    No arguments
    _my_decorator_body() を定義する前に必要な処理があれば、ここに書く
    デコレータが記載された時に処理が必要な場合にはここに書く #2
    >>> @my_decorator('mytest1')
    ... def f2( arg2 ):
    ...     print( arg2 )
    ...
    デコレータが記載された時に処理が必要な場合にはここに書く #1
    There are some arguments: ('mytest1',)
    _my_decorator_body() を定義する前に必要な処理があれば、ここに書く
    デコレータが記載された時に処理が必要な場合にはここに書く #2
    >>> @my_decorator
    ... def f3( arg1 ):
    ...     print( arg1 )
    ...     a = 1/0
    ...
    デコレータが記載された時に処理が必要な場合にはここに書く #1
    No arguments
    _my_decorator_body() を定義する前に必要な処理があれば、ここに書く
    デコレータが記載された時に処理が必要な場合にはここに書く #2
    >>> @my_decorator('mytest2')
    ... def f4( arg2 ):
    ...     print( arg2 )
    ...     a = 1/0
    ...
    デコレータが記載された時に処理が必要な場合にはここに書く #1
    There are some arguments: ('mytest2',)
    _my_decorator_body() を定義する前に必要な処理があれば、ここに書く
    デコレータが記載された時に処理が必要な場合にはここに書く #2
    >>> try:
    ...     f1( "Hello, World! #1" )
    ... except:
    ...     print( "error #1" )
    ...
    前処理はここで実行 (<function f1 at 0x...>,) {} ('Hello, World! #1',) {}
    Hello, World! #1
    後処理はここで実行 ... {} ('Hello, World! #1',) {}
    >>> try:
    ...         f2( "Hello, World! #2" )
    ... except:
    ...         print( "error #2" )
    ...
    前処理はここで実行 ('mytest1',) {} ('Hello, World! #2',) {}
    Hello, World! #2
    後処理はここで実行 ('mytest1',) {} ('Hello, World! #2',) {}
    >>> try:
    ...         f3( "Hello, World! #3" )
    ... except:
    ...         print( "error #3" )
    ...
    前処理はここで実行 (<function f3 at 0x...>,) {} ('Hello, World! #3',) {}
    Hello, World! #3
    error #3
    >>> try:
    ...         f4( "Hello, World! #4" )
    ... except:
    ...         print( "error #4" )
    ...
    前処理はここで実行 ('mytest2',) {} ('Hello, World! #4',) {}
    Hello, World! #4
    error #4
    >>>
    """

    # 引数を取らないことが明確なデコレータはここから
    # _my_decorator の名前を変えてグローバルで define する
    def _my_decorator( func ):
        # _my_decorator_body() を定義する前に必要な処理があれば、ここに書く
        print( "_my_decorator_body() を定義する前に必要な処理があれば、ここに書 く" )
        @wraps(func)
        def _my_decorator_body( *body_args, **body_kwargs ):
            # 前処理はここで実行
            print( "前処理はここで実行", args, kwargs, body_args, body_kwargs )
            try:
                # デコレートした本体の実行
                ret = func( *body_args, **body_kwargs )
            except:
                raise
            # 後処理はここで実行
            print( "後処理はここで実行", args, kwargs, body_args, body_kwargs )
            return ret

        # デコレータが記載された時に処理が必要な場合にはここに書く #2
        print( "デコレータが記載された時に処理が必要な場合にはここに書く #2" )
        return _my_decorator_body

    # 引数を取らないことが明確なデコレータはここまで

    # デコレータが記載された時に処理が必要な場合にはここに書く #1
    print( "デコレータが記載された時に処理が必要な場合にはここに書く #1" )

    if len(args) == 1 and callable( args[0] ):
        # 引数無しでデコレータが呼ばれた場合はここで処理
        print( "No arguments" )
        return _my_decorator( args[0] )

    else:
        # 引数ありでデコレータが呼ばれた場合はここで処理
        print( "There are some arguments:", args )
        return _my_decorator


###########################################
#   unitttest
###########################################
import unittest
from io import StringIO
import sys

class Test_My_Decorator(unittest.TestCase):
    def setUp(self):
        self.saved_stdout = sys.stdout
        self.stdout = StringIO()
        sys.stdout = self.stdout

    def tearDown(self):
        sys.stdout = self.saved_stdout

    def test_decorator_noarg(self):
        @my_decorator
        def t1(arg0):
            print( arg0 )

        t1("test_decorator_noarg")

        import re

        s = re.sub('0x[0-9a-f]+', '0x', self.stdout.getvalue())

        self.assertEqual(s,
            "デコレータが記載された時に処理が必要な場合にはここに書く #1\n" +
            "No arguments\n" +
            "_my_decorator_body() を定義する前に必要な処理があれば、ここに書く\n" +
            "デコレータが記載された時に処理が必要な場合にはここに書く #2\n" +
            "前処理はここで実行 (<function Test_My_Decorator.test_decorator_noarg.<locals>.t1 at 0x>,) {} ('test_decorator_noarg',) {}\n" +
            "test_decorator_noarg\n" +
            "後処理はここで実行 (<function Test_My_Decorator.test_decorator_noarg.<locals>.t1 at 0x>,) {} ('test_decorator_noarg',) {}\n"
            )

    def test_decorator_witharg(self):
        @my_decorator('with arg')
        def t1(arg0):
            print( arg0 )

        t1("test_decorator_witharg")

        self.assertEqual(self.stdout.getvalue(),
            "デコレータが記載された時に処理が必要な場合にはここに書く #1\n" +
            "There are some arguments: ('with arg',)\n" +
            "_my_decorator_body() を定義する前に必要な処理があれば、ここに書く\n" +
            "デコレータが記載された時に処理が必要な場合にはここに書く #2\n" +
            "前処理はここで実行 ('with arg',) {} ('test_decorator_witharg',) {}\n" +
            "test_decorator_witharg\n" +
            "後処理はここで実行 ('with arg',) {} ('test_decorator_witharg',) {}\n"
            )

    def test_functionname(self):
        @my_decorator
        def t1():
            return t1.__name__

        f_name = t1()

        self.assertEqual( f_name, "t1" )

    def test_docattribute(self):
        @my_decorator
        def t1():
            """Test Document"""
            pass

        self.assertEqual( t1.__doc__, "Test Document" )


###########################################
#   main
###########################################
if __name__ == '__main__':

    @my_decorator
    def f1( arg1 ):
        print( arg1 )

    @my_decorator('mytest1')
    def f2( arg2 ):
        print( arg2 )

    @my_decorator
    def f3( arg1 ):
        print( arg1 )
        a = 1/0

    @my_decorator('mytest2')
    def f4( arg2 ):
        print( arg2 )
        a = 1/0

    try:
        f1( "Hello, World! #1" )
    except:
        print( "error #1" )

    try:
        f2( "Hello, World! #2" )
    except:
        print( "error #2" )

    try:
        f3( "Hello, World! #3" )
    except:
        print( "error #3" )

    try:
        f4( "Hello, World! #4" )
    except:
        print( "error #4" )

    import doctest
    doctest.testmod(optionflags=doctest.ELLIPSIS)

    unittest.main()
サンプル実行
$ python decorator_framework.py
デコレータが記載された時に処理が必要な場合にはここに書く #1
No arguments
_my_decorator_body() を定義する前に必要な処理があれば、ここに書く
デコレータが記載された時に処理が必要な場合にはここに書く #2
デコレータが記載された時に処理が必要な場合にはここに書く #1
There are some arguments: ('mytest1',)
_my_decorator_body() を定義する前に必要な処理があれば、ここに書く
デコレータが記載された時に処理が必要な場合にはここに書く #2
デコレータが記載された時に処理が必要な場合にはここに書く #1
No arguments
_my_decorator_body() を定義する前に必要な処理があれば、ここに書く
デコレータが記載された時に処理が必要な場合にはここに書く #2
デコレータが記載された時に処理が必要な場合にはここに書く #1
There are some arguments: ('mytest2',)
_my_decorator_body() を定義する前に必要な処理があれば、ここに書く
デコレータが記載された時に処理が必要な場合にはここに書く #2
前処理はここで実行 (<function f1 at 0x76973a08>,) {} ('Hello, World! #1',) {}
Hello, World! #1
後処理はここで実行 (<function f1 at 0x76973a08>,) {} ('Hello, World! #1',) {}
前処理はここで実行 ('mytest1',) {} ('Hello, World! #2',) {}
Hello, World! #2
後処理はここで実行 ('mytest1',) {} ('Hello, World! #2',) {}
前処理はここで実行 (<function f3 at 0x7685bd20>,) {} ('Hello, World! #3',) {}
Hello, World! #3
error #3
前処理はここで実行 ('mytest2',) {} ('Hello, World! #4',) {}
Hello, World! #4
error #4
....
----------------------------------------------------------------------
Ran 4 tests in 0.005s

OK
$
unittest
$ python -m unittest -v decorator_framework.py
test_decorator_noarg (decorator_framework.Test_My_Decorator) ... ok
test_decorator_witharg (decorator_framework.Test_My_Decorator) ... ok
test_docattribute (decorator_framework.Test_My_Decorator) ... ok
test_functionname (decorator_framework.Test_My_Decorator) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.006s

OK
$
6
10
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
6
10