[Python]デコレータを理解する

Python

デコレータは関数やクラスの前後に処理を追加するものですが、その動きや書き方がいまいち腑に落ちず、放置していました。

しかし、DjangoやFlaskなど、他のパッケージを触っていると出てきますし、よくわからないまま触るのも気持ちが悪く、この際ちゃんと理解しようと思ってまとめました。

 Androidアプリを作成しました。
 感情用のメモ帳です。

スポンサーリンク
スポンサーリンク

デコレータとは何か

デコレータの役割は、関数やクラスを書き換えずに処理を追加することです。

デコレータは関数であり、その構造は、基本的には引数として関数やクラスを受け取ります。そして関数やクラスを返します。

次のような単純な関数に処理を付け加えたい、と思ったとします。

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()
$ python hello.py
----------
Hello
----------

上記「sample.py」で定義した「print_line関数」は、デコレータではありません。

デコレータとして新しく関数を作るなら、関数を受け取り、関数を返すものにする必要があります。

そのために関数内関数を定義し、戻り値として返します。
※ただし、戻り値となる関数には()をつけないこと。

実際に書き換えてみましょう。

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

そしてデコレート(装飾)される側の関数定義の上に「@関数名」をつけます。

@装飾する関数
def 装飾される関数:
内容

装飾される関数()

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

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


hello()

これで「hello()」が実行されるときには、「print_line」が自動的に呼ばれるようになります。

$ python hello.py
----------
Hello
----------
ポイント
  • 機能追加のための関数を定義する。ただし、その関数は引数に関数を受け取り、関数を返すもの。
  • 元の関数の上に「@関数名」
  • 呼び出すときには元の関数だけで良い。

デコレータのメリット

デコレータの関数定義はわかりづらく、その中身の処理を追っていくのが難しいものです。

先に例示した「sample.py」のように書く方がはるかにわかりやすいでしょう。

なんでこんな書き方をするのだろう、そもそもデコレータにメリットはあるのだろうかと考えていました。

私なりに、デコレータが元の関数やクラスを装飾するものであるなら、関数やクラスを受け取って返す書き方は理にかなっている、という結論に至りました。

また「@」をつけて表記することで、ここでなにかしらの機能を追加しているのだな、とすぐ目につきます

現状はまだ使いこなせていないためこのような感想しか出てきませんが、複数のライブラリでも使われていることから使い道は多いのだろうということはわかります。

処理の流れを追う

デコレータがひとつの場合

デコレータでつまずきやすいのは、処理の流れだと思うので、その流れを追っていきたいと思います。

またさっきの「hello.py」を使用し、デコレータと「hello()」の呼び出しをコメントアウトします。

デコレータを使わずにデコレータを使ったのと同じ結果を得ようとすると、次のような実行文を書きます。

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

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


# hello()
print_line(hello)()
print_line(hello)()

あまり見たことがない形です。

この文が実行されたときと、デコレータを使って「hello()」が呼ばれたときは、ほぼ同じ処理をしているので、この文の実行の流れを見ることでデコレータを理解していきましょう。

次の順序で実行されます。

  1. print_line(hello)が実行されます。
  2. そしてこのprint_lineは、内部でwrapperを返します。
  3. 呼び出しもとに帰ってきたwrapperはそこで()と合わさり実行されます。
  4. wrapper内部で、その外側の関数――print_line――の引数であるhello実行されます。
print_line(hello)()
|---------------|
     wrapper  +  ()
     |------|
      hello()

2.の補足。
関数に()をつけないと関数は実行されません。関数オブジェクトが返されます。

$ python
Python 3.10.0 (default, Oct 12 2021, 16:02:08) [GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def func():
...     print(1)
... 
>>> func()
1
>>> func
<function func at 0x7fc87d1615a0>

4.の補足。
関数内関数は、その外側の関数のものを使うことができます。

>>> def print_number(num):
...     def inner():
...             print(f'number: {num}')
...     inner()
... 
>>> print_number(100)
number: 100

デコレータがふたつあった場合

今度はデコレータがふたつの場合の流れを追ってみましょう。

「hello.py」ではなく、別のものを作りました。

def decorator1(func):
    def wrapper1():
        print('wrapper1内で関数の実行前')
        func()
        print('wrapper1内で関数の実行後')
    print('decorator1がwrapper1を返す')
    return wrapper1

def decorator2(func):
    def wrapper2():
        print('wrapper2内で関数の実行前')
        func()
        print('wrapper2内で関数の実行後')
    print('decorator2がwrapper2を返す')
    return wrapper2

@decorator1
@decorator2
def myfunc():
    print('被修飾関数の実行')


if __name__ == '__main__':
    myfunc()

「decorator1」の内部には「wrapper1」、「decorator2」の内部では「wrapper2」が定義されていて、myfuncが実行されます。

出力がどういうものになるか想像できるでしょうか?

実際にやってみましょう。

$ python deco.py 
decorator2がwrapper2を返す
decorator1がwrapper1を返す
wrapper1内で関数の実行前
wrapper2内で関数の実行前
被修飾関数の実行
wrapper2内で関数の実行後
wrapper1内で関数の実行後

今度も前項のようにデコレータを使わずに呼び出そうとすると、「myfunc()」の部分を次のように変えます。

decorator1(decorator2(myfunc))()

処理の流れは、

  1. decorator2が実行され、wrapper2を返します
  2. decorator1が実行され、wrapper1を返します
  3. wrapper1が実行され、その外側のdecorator1の引数であるwrapper2を実行します
  4. wrapper2が実行されると、その外側のdecorator2の引数であるmyfuncを実行します
decorator1(decorator2(myfunc))()
           |------------------|
decorator1(     wrapper2     )
|-------------------------------|
            wrapper1             ()

このようにデコレータの実行順は、装飾される関数に近い方から、コードから見ると下から上に向かって順に実行されます。

# decorator2 → decorator1の順
@decorator1
@decorator2
def myfunc():
    print('被装飾関数の実行')


if __name__ == '__main__':
    myfunc()

注意が必要な仕様

デコレータを使うときに注意が必要なことが2つありました。

  • デコレータは定義されたところで実行される
  • 元の関数の名前が置き換わる

定義されたところで実行される

つぎのモジュールを実行すると何が起きると思いますか?

def print_test(func):
    print('test')
    def wrapper():
        func()
    return wrapper

@print_test
def myfunc():
    print('myfunc')

定義しか書いてないから何も起きない。
というのはちょっと違います。

$ python test.py 
test

print_test内の「print('test')」が実行されています。

デコレータ定義時に実行されたようで、最初に気づいたときには、予期しないものだったので驚きました。

myfunc()」と呼び出し文を末尾に追加して実行すると、

$ python test.py 
test
myfunc

意図通りの出力にはなりますが、printなどの実行文はwrapper内で定義した方が良いかもしれません。

関数の名前が置き換わる

対話モードで起動して実例を見てもらいます。

$ python
Python 3.10.0 (default, Oct 12 2021, 16:02:08) [GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def test():
...     pass
... 
>>> test.__name__
'test'

test関数の名前は「test」です。
これは何もおかしくありません。

しかし、デコレータを使うと、この名前が置き換わります。

>>> def decorator(func):
...     def wrapper():
...             func()
...     return wrapper
... 
>>> @decorator
... def test():
...     pass
... 
>>> test.__name__
'wrapper'
>>> test(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: decorator.<locals>.wrapper() takes 0 positional arguments but 1 was given

「wrapper」に変わってしまいました。
これではエラー処理のときなど不便です。

これを解消するための標準ライブラリがあります。
デコレータの「functools.wraps」です。

>>> from functools import wraps
>>> def decorator(func):
...     @wraps(func)
...     def wrapper():
...             func()
...     return wrapper
... 
>>> @decorator
... def test():
...     pass
... 
>>> test.__name__
'test'
>>> test(4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: test() takes 0 positional arguments but 1 was given

デコレータの引数の関数をwrapsデコレータに渡しています。

元の関数名もそのままですね。

タイトルとURLをコピーしました