適当のごった煮

Pythonと境界標とQGISを中心にいろいろと

デコレータの作り方

スポンサードリンク

個人のちょっとした作業でPythonを使っている状況ではデコレータを使う機会はほとんどありませんが、公開されているソースで見かける「@」が気になるので、どんな機能なのかを調べました。

公式ドキュメントの用語集では、「別の関数を返す関数」と記載されていて、以下の二つは等しいと書かれています。

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

「@」を使っていない「f = staticmethod(f)」をよく見ると、ある関数を引数にとって、引数の関数と同じ名前の関数に別の関数を代入している、と読みとれます。

一般にデコレータは、既存の関数を変更せず、関数の前後にコードを追加する目的として用いられる、と説明されます。

デコレータの定義、指定方法と使い方は以下の通りです。

# デコレータ関数の定義
def decorator_name(func):
    def wrapper(*args, **kwargs):
        # 処理
    return wrapper

# デコレータ指定
@decorator_name
def test():
    # 処理

# 実際に使う
test()

別に機能を追加しなくても、関数を引数にして関数を返す関数であればデコレータとして機能します(そのことに意味があるかどうかは別として)。

# 3つの数字の最小値を表示する関数を最大値を表示する関数に置き換え
def dec(func):
    def wrapper(*args, **kwargs):
        print(max(args)) # 最大値を表示
    return wrapper

@dec
def print_min(a, b, c):
    print(min(a, b, c)) # 最小値を表示

print_min(1, 2, 3)  => 「3」が結果として表示される

「@」記法を使わなければ、別の名前のオブジェクトに関数オブジェクトを入れることもできます。

def dec(func):
    def wrapper(*args, **kwargs):
        print(max(args))
    return wrapper

def print_min(a, b, c):
    print(min(a, b, c))

test = dec(print_min)
test(1, 2, 3)  => 「3」が結果として表示される

引数ありのデコレータ

デコレータに引数を渡すこともできます。Bottleで見かける「@route('/hello')」などです。

まず、引数なしのデコレータで、デコレータがどのように展開されるかを確認します。

def dec(func):
    print('decの中')
    def wrapper(*args, **kwargs):
        print('wrapperの中')
        func(*args, **kwargs)
    print('return直前')
    return wrapper

@dec
def test():
    print('test実行')

一見、定義だけに見えるスクリプトなので何も表示されないと感じますが、「decの中」と「return直前」が表示されます。

つまり、「@」によって「test = dec(test)」と展開され、testにwrapperが代入されるところまでは実行されているということです。

続けてシェルで「test()」を実行すると以下のようになります。

>>> test()
wrapperの中
test実行
>>> type(test)
<class 'function'>
>>> test
<function dec.<locals>.wrapper at 0x0000016EE861FC80>
>>>

「test」はデコレータ関数内で定義された「wrapper」部分のみとなっています。

ここから、「関数を引数として関数を返す」というデコレータ機能より前に引数を受け取る構造にすればデコレータに引数を渡せることが分かります。

これを実現するには、引数を受け取る関数でデコレータを包み込むように定義します。

def dec(i): # 引数を受け取る
    def dec_main(func): # デコレータメイン
        def wrapper(*args, **kwargs): # デコレータが返す関数
            for x in range(i): # 引数を参照できる
                func(*args, **kwargs)
        return wrapper
    return dec_main

@dec(3)
def test():
    print('test実行')

内側の「wrapper」から外側で受け取った引数を参照できる機能は、クロージャと呼ばれるそうです。

続けてシェルで「test()」を実行すると下記のようになります。

>>> test()
test実行
test実行
test実行
>>> test
<function dec.<locals>.dec_main.<locals>.wrapper at 0x0000021A2B9DFD08>
>>> 

これを「@」を使わずに書くと、「test = dec(3)(test)」となり、dec(3)がまずは解釈されてdec_mainに姿を変え、「test = dec_main(test)」となって機能します。

実際に「@」を使わないと以下のようになります。

# 手動でデコレート(定義)
def dec(i):
    def dec_main(func):
        def wrapper(*args, **kwargs):
            for x in range(i):
                func(*args, **kwargs)
        return wrapper
    return dec_main

def test():
    print('test実行')

# シェルで順を追って手動でデコレート
>>> x = dec(2)
>>> x
<function dec.<locals>.dec_main at 0x000001DE7142FC80>
>>> test = x(test)
>>> test
<function dec.<locals>.dec_main.<locals>.wrapper at 0x000001DE7142FD08>
>>> test()
test実行
test実行
>>> 

ここまでくると、公式ドキュメントにある、最初に見たときは意味が分からなかった下記の表記が等しいという説明も理解できます。

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

まず、func直近の「@f2」の部分が「f2(func)」と展開され、続いて関数「f2(func)」が「@f1(arg)」の引数として「f1(arg)(f2(func))」。ここから「func = f1(arg)(f2(func))」となります。

なお、普通にデコレータを使うとデコレート対象関数のコメント等が上書きされてしまうので、functoolsというモジュールがあります。

デコレータは、説明を一読しただけでは理解できないうえ、便利さを痛感できず、Python中級の入学試験のような機能だと感じます。プロとアマの間にある壁のひとつなのではないでしょうか。

参考