💭

Pythonのデコレータとは何か

2023/02/08に公開

1.デコレータとは

1-1.役割

デコレータの役割とは、関数やクラスの中身を書き換えずに処理を追加することである。デコレータは関数であり、引数として関数やクラスを受け取り、関数やクラスで処理を返す。

1-2.具体例1

次の関数に処理を追加する場合を想定する。

def hello():
    print('Hello')

hello()

出力されるHello前後に「----」を加える場合、下記のようなコードが書ける

def print_line():
    print('----------')
    hello()
    print('----------')

def hello():
    print('Hello')

#  def hello():
#     print('----------')
#     print('Hello')
#     print('----------')

# hello()
print_line()

ただし、print_line関数はデコレータではない。デコレータを新しく作る場合、関数を受け取り、関数で返す必要がある。

下記のように装飾したい関数を@print_lineと定義する。

def print_line(func):
    def wrapper():
        print('----------')
        func()
        print('----------')
    return wrapper

@print_line
def hello():
    print('Hello')

hello()
  • hello()が実行されると、def hello()が処理される。
  • def hello()はprint_line関数を装飾するので、def print_lineで処理が始まる
  • def print_lineは引数としてfuncが定義されている。
  • このfuncが、引数としてdef hello()を受け取るため、出力結果は、以下の通りとなる
$ python hello.py
----------
Hello
----------

1-3.具体例2

下記の流れで実行される。

  • myfunc("Blabla")が処理される。
  • 上記は、def myfunc関数のprintにて出力処理される。
  • def myfunc関数はsample_decoratorにデコレートされるため、def sample_decoratorが処理される。
  • ここでdef myfunc関数が引数のmyfuncとして受け取られる。
  • myfunc関数は、print(text)という処理があり、そこではBlablaが出力される。
  • その結果、一番下の実行結果が出力される。

コード

def sample_decorator(myfunc):
    def inner_func(*args):
        print("I am the decorator!")
        myfunc(*args)
    return inner_func
 
@sample_decorator
def myfunc(text):
    print(text)
 
myfunc("Blabla")

実行結果

I am the decorator!
Blabla

参考:複数の引数を渡したい場合、下記の実装も可能

def sample_decorator(myfunc):
    def inner_func(*args):
        print("I am the decorator!")
        myfunc(*args)

    return inner_func
 
@sample_decorator
def myfunc(text, text2):
    print(text)
    print(text2)
 
myfunc("Blabla", "test")

1-4.具体例3

コード

def sample_decorator(myfunc):
    def inner_func(a,b):
        return "I am the decorator!" + " " + a + " " + b
    return inner_func
 
@sample_decorator
def myfunc(text):
    return text
 
print(myfunc("test1","test2"))

出力結果

I am the decorator! test1 test2

下記に処理の流れを記載する。

  • まず、print(myfunc("test1","test2"))が実行される。
  • 実行されたmyfuncを参照するため、def myfunc(text):が実行される。
  • de myfunc(text)はsample_decoratorにデコレートされるため、def sample_decorator(myfunc)が実行される。
  • その中のinner_funcは、変数aとbを持つ。最初にmyfunc("test1","test2")と変数を代入しているため、最終的にこれらが変数として出力される。

2.デコレータの応用的な使い方

2-1.元の関数のメタデータを維持する

先ほどまで説明したデコレータの定義だと、元の関数のメタデータが、デコレータで使用された関数のメタデータにおきかわってしまった。元の関数のメタデータを維持するため、nameメソッドで元の関数名をチェックする

def sample_decorator(myfunc):
    def inner_func():
        return "I am the decorator!"
    return inner_func
 
@sample_decorator
def myfunc(text):
    return text
 
print(myfunc.__name__)

実行結果

inner_func

本来、myfuncと表示したいが、元データのinner_funcが表示される。myfuncのメタデータに変更を加えたくない場合、functools.wrapsにより解決できる。functools.wrapsは、元の関数のメタデータに変更を加えないためのデコレータである。inner_funcの前に変更を加えたくない関数(myfunc)を引数として渡すことで、メタデータの変更をしないように処理できる。

from functools import wraps
def sample_decorator(myfunc):
    @wraps(myfunc)
    def inner_func():
        return "I am the decorator!"
    return inner_func
 
@sample_decorator
def myfunc(text):
    return text
 
print(myfunc.__name__)

実行結果

myfunc

2-2.複数のデコレータを使う

下記の流れで処理が行われる。

  • まずdef myfuncに近い順で処理されるため、@A→@Bという順になる
  • @Aは下記の通り、出力される。
    • I am the A decorator!
    • Hello, decorator
    • I am the A decorator!
  • 次に、@Bが出力される。
    • I am the B decorator!
    • (Aの実行結果)
    • I am the B decorator!
  • (Aの実行結果)は、上記出力結果で代入されるため、一番下の実行結果が出力される。
def A(myfunc):
    def inner_func():
        print("I am the A decorator!")
        myfunc()
        print("I am the A decorator!")
    return inner_func
 
def B(myfunc):
    def inner_func():
        print("I am the B decorator!")
        myfunc()
        print("I am the B decorator!")
    return inner_func
 
@B
@A
def myfunc():
    print("Hello, decorator")
 
myfunc()

実行結果

I am the B decorator!
I am the A decorator!
Hello, decorator
I am the A decorator!
I am the B decorator!

3.Pythonのデコレータにwrapsを使うべき理由

「2-1.元の関数のメタデータを維持する」にて、下記事例を確認した。

  • @wrapsをつけずにprint(myfunc.name)を実行するとinner_funcが表示される。
  • @wrapsをつけてprint(myfunc.name)を実行するとmyfuncが表示される。

上記の何が問題かというと、実際動かしている関数と関数名が一致していないため、doctestが動かなくなる、という事態が発生する(*通常動作では問題ない)。別事例で説明しよう。

def hoge_decorator(f):
    def hoge_wrapper(*args, **kwargs):
        """デコレータのDocstringだよ"""
        print("デコレータだよ")
        return f(*args, **kwargs)
    return hoge_wrapper

@hoge_decorator
def hoge_function():
    """デコってる関数のDocstringだよ"""
    print("これがデコってる関数だ!")

if __name__ == '__main__':
    hoge_function()

結果

デコレータだよ
これがデコってる関数だ!

上記は、問題なく動作する。ただ、下記print出力を加えるとどうなるか。

def hoge_decorator(f):
    def hoge_wrapper(*args, **kwargs):
        """デコレータのDocstringだよ"""
        print("デコレータだよ")
        return f(*args, **kwargs)
    return hoge_wrapper

@hoge_decorator
def hoge_function():
    """デコってる関数のDocstringだよ"""
    print("これがデコってる関数だ!")

if __name__ == '__main__':
    hoge_function()
    print(hoge_function.__name__)
    print(hoge_function.__doc__)

結果

デコレータだよ
これがデコってる関数だ!
hoge_wrapper
デコレータのDocstringだよ

nameはhogewrapperだが、docはhoge_functionのデコレータを出力するため、実際に動かしている関数が、意図したものとは異なるという事態になる。そのため、通常動作では問題ないが、doctestだと動かなくなってしまう。

参考:doctestとは

doctestとは、簡易的なテストを実行するためのpython標準ライブラリである。下記の通り、docstringの実行内容と正しい返り値をセットで書くことにより、テストを実行できる。テストは、「python hoge.py」で実行できる。

def add(a, b):
    '''
    >>> add(1, 2)
    3
    >>> add(-8, -2)
    -10
    '''
    pass

if __name__ == '__main__':
    import doctest
    doctest.testmod()

実行結果

**********************************************************************
File "__main__", line 3, in __main__.add
Failed example:
    add(1, 2)
Expected:
    3
Got nothing
**********************************************************************
File "__main__", line 5, in __main__.add
Failed example:
    add(-8, -2)
Expected:
    -10
Got nothing
**********************************************************************
1 items had failures:
   2 of   2 in __main__.add
***Test Failed*** 2 failures.
TestResults(failed=2, attempted=2)

上記の通り、エラーが発生する。return a+bとしていなかったことが原因なので、コードを修正する。

def add(a, b):
    '''
    >>> add(1, 2)
    3
    >>> add(-8, -2)
    -10
    '''
    return a + b

if __name__ == '__main__':
    import doctest
    doctest.testmod()

正しくテストが実行される。テストが全て成功すれば、出力はされない。

参考文献

https://fuji-pocketbook.net/python-decorator/#:~:text=名前が置き換わる-,デコレータとは何か,やクラスを返します。
https://www.sejuku.net/blog/25130
https://qiita.com/moonwalkerpoday/items/9bd987667a860adf80a2
https://qiita.com/studio_haneya/items/d44fc73781e88694cd3e

Discussion