Update 2023.12.27

Python Decoratorの徹底解説
Mark Summerfield『実践 Python 3』デザインパターンのサンプルコード
Python3(3.10)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」

著作権の問題があるので,本に書いてないことだけを解説します。つまり,視点を変えて解説します。

言語仕様のDecoratorの方がデザインパターンのDecoratorパターンよりも有用であると思われ,言語仕様のDecoratorを徹底解説します。

◆◆言語仕様のDecoratorの使われる場面◆◆

Decoratorの形式は関数内関数であり,機能としては,ラッパー関数であり,その機能とは,元の関数を書き換えることなく、機能を追加する関数です。

その形式と機能が良く判るサンプルが巻末に添付した【decorator1.py】です。

Decoratorの欠陥を修正する方法を【decorator2.py】で解説します。


◆◆【decorator1.py】の解説◆◆

【decorator1.py】は3つのプログラムより成っています。同じ名前が何回か出てくるが,下のものが上のものを上書きしていますので,何の問題もありません。途中で一時停止した場合は注意が必要です。

さて,5行目からがDecoratorデコレータです。decorated functionデコられている関数は12行目からです。デコられていると判るのが16行目です。これがデコレータとデコられている関数の関係性を示しています。

メインルーチンが19行目です。呼ばれているデコられている関数は単に平均を算出するものです。その引数のtypeはfloatであり,個数は2個以上何個でも受け付けます。*アスタリスクが付いた引数はlistです。デコレータはその引数をint整数またはstr数文字からfloatに変換します。

デコレータの引数はデコられている関数そのものです。デコレータは関数内関数の形式になっていて,内部関数wrapperの引数はデコられている関数の引数です。何でも受け付けられるように(*args, **kwargs)となっています。*アスタリスクが付いた引数はlistです。**が付いた引数は辞書です。これで何でも受け付けられることになっています。

内部関数wrapperはデコられている関数を戻し,外部関数デコレータは内部関数wrapperを戻しています。この関数内関数の形式が,デコられている関数の機能を拡張することができるのです。


◆◆デコレータ宣言◆◆

2番目のプログラムのデコレータが23行目です。デコられている関数のすぐ上の同じインデントに@アットマークが先頭にありデコレータの関数名が続きます。16行目のデコレータ宣言と同じ働きをします。デコレータの定義は前のものを流用しています。動作は1番目のプログラムと全く同じです。


◆◆デコレータの欠陥◆◆

デコられている関数の機能がwrapper関数で拡張されているのですが,デコられている関数の__name__属性と__doc__属性(docstring)が引き継がれていません。その現象は【decorator2.py】のサンプルでお見せしますが,修正方法は【decorator1.py】の3番目のプログラムに書きます。

3番目のプログラムは2番目のそれと1行を除いて同じです。異なる1行は37行目であり,wrapper関数の直上で,@functools.wrapsによりデコレータ宣言をしています。このサンプルでは2番目と3番目は同じ動作をしているように見えます。その目に見えな違いは【decorator2.py】のサンプルでお見せします。


◆◆【decorator2.py】の解説◆◆

【decorator2.py】は5つのプログラムから成っていますが,同じ名前があっても下のものが上のものを上書きしていますので問題ありません。自己テストが5つもありますので外部ファイルとして使えません。

1番目から3番目のプログラムは同じプログラムです。Decoratorデコレータは引数をそのまま戻しているだけです。debugのためrunした証拠のprint文とdocstringを書いてあります。decorated functionデコられている関数はrunした証拠のprint文とdocstringしかありません。

1番目のプログラムの17行目の自己テストはデコられている関数を呼ぶだけです。デコレータとそれから戻されるデコられている関数が呼ばれます。出力は自己テスト直後にコメントアウトで書かれています。

2番目のプログラムの36行目の自己テストはデコられている関数を呼んだあと,デコられている関数の__name__(関数名)__doc__(docstring)を呼んでいます。が出力はwrapperとそれのdocstringです。

3番目のプログラムの62行目の自己テストはデコられている関数を呼んだあと,デコられている関数の__name__(関数名)__doc__(docstring)を呼んでいるのは2番目と同じです。違いは50行目のデコレータ宣言「@functools.wraps」だけです。これがデコレータの欠陥を修正します。

出力の関数名はデコられている関数functionAであり,docstringもデコられている関数のものです。

きちんと修正できました。「@functools.wraps」は常に付けておくべきものだと思います。

◆◆doctest◆◆

doctestはdocstringを使っているので,上の2番目のプログラムのようにデコられている関数のdocstringがデコレータに引き継がれないとバグります。

4番目と5番目のプログラムは同じです。デコレータは引数をそのまま戻すだけです。デコられている関数は引数を2乗して戻すだけです。違いは111行目のデコレータ宣言「@functools.wraps」だけです。

4番目のプログラムの88行目の自己テストは「doctest.testmod()」であり,doctestだけがrunします。普通はデコられている関数を引数を付けて呼ぶのですが,これは何もしていません。出力は見事に失敗しています。

5番目のプログラムの123行目の自己テストは「doctest.testmod()」であり,doctestだけがrunします。デコられている関数のdocstringつまりdoctestが認識でき,テスト結果が出力されています。

ターミナルから起動する場合は,モジュール名の後に「-v」を付けます。PyCharmでは,1回runした後に,左上4本線メニュー→Runプルダウンメニュー→Edit Configurations...→Scriptの下の右上矢印左下矢印の枠→「-v」→OK

その後,doctestのオプションを使わず,普通にrunしてください。


◆◆ソースコード◆◆

このWebの左上隅からダウンロードできます。

ソースファイルは2つです。
・decorator1.py;Decoratorサンプル(Decoratorの動作の解説)
・decorator2.py;Decoratorサンプル(@functools.wrapsのありなしの差)


【decorator1.py】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Decorator
def float_args_and_return(function):
    def wrapper(*args, **kwargs):
        args = [float(arg) for arg in args]  # list
        return float(function(*args, **kwargs))
    return wrapper

# Decorated function
def mean(first, second, *rest):
    numbers = (first, second) + rest         # tuple
    return sum(numbers) / len(numbers)
# make decorated function
mean = float_args_and_return(mean)


print(mean(5, "6", "7.5"))
# 6.166666666666667


@float_args_and_return                  # decorator
def mean(first, second, *rest):         # decorated function
    numbers = (first, second) + rest
    return sum(numbers) / len(numbers)


print(mean(5, "6", "7.5", 8))
# 6.625


import functools

# Decorator
def float_args_and_return(func):
    @functools.wraps(func)              # decorate wrapper function
    def wrapper(*args, **kwargs):
        args = [float(arg) for arg in args]
        return float(func(*args, **kwargs))
    return wrapper

@float_args_and_return                  # decorator
def mean(first, second, *rest):         # decorated function
    numbers = (first, second) + rest
    return sum(numbers) / len(numbers)


print(mean(5, "6", "7.5"))
# 6.166666666666667

【decorator2.py】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#!/usr/bin/env python3
# -*- coding: utf-8 -*-


def decoratorA(fnuc):
    def wrapper(*args, **kwargs):
        """docstring of decorator"""
        print("I am decorator")
        return fnuc(*args, **kwargs)
    return wrapper

@decoratorA
def functionA():
    """docstring of decorated function"""
    print("I am decorated function")

if __name__ == '__main__':
    functionA()
# I am decorator
# I am decorated function

print('====')

def decoratorA(fnuc):
    def wrapper(*args, **kwargs):
        """docstring of decorator"""
        print("I am decorator")
        return fnuc(*args, **kwargs)
    return wrapper

@decoratorA
def functionA():
    """docstring of decorated function"""
    print("I am decorated function")

if __name__ == '__main__':
    functionA()
    print(functionA.__name__)
    print(functionA.__doc__)
# I am decorator
# I am decorated function
# wrapper
# docstring of decorator

print('====')

import functools

def decoratorA(fnuc):
    @functools.wraps(fnuc)
    def wrapper(*args, **kwargs):
        """docstring of decorator"""
        print("I am decorator")
        return fnuc(*args, **kwargs)
    return wrapper

@decoratorA
def functionA():
    """docstring of decorated function"""
    print("I am decorated function")

if __name__ == '__main__':
    functionA()
    print(functionA.__name__)
    print(functionA.__doc__)
# I am decorator
# I am decorated function
# functionA
# docstring of decorated function

print('====')

import functools
import doctest
doctest.testmod()
def decoratorA(fnuc):
    def wrapper(*args, **kwargs):
        return fnuc(*args, **kwargs)
    return wrapper

@decoratorA
def functionA(n):
    """docstring of decorated function
    >>> functionA(2)
    4
    """
    return n ** 2
if __name__ == '__main__':
    doctest.testmod()
# 3 items had no tests:
#     __main__
#     __main__.decoratorA
#     __main__.functionA
# 0 tests in 3 items.
# 0 passed and 0 failed.
# Test passed.
# 3 items had no tests:
#     __main__
#     __main__.decoratorA
#     __main__.functionA
# 0 tests in 3 items.
# 0 passed and 0 failed.
# Test passed.

print('====')

import functools
import doctest

def decoratorA(fnuc):
    @functools.wraps(fnuc)
    def wrapper(*args, **kwargs):
        return fnuc(*args, **kwargs)
    return wrapper

@decoratorA
def functionA(n):
    """docstring of decorated function
    >>> functionA(2)
    4
    """
    return n ** 2
if __name__ == '__main__':
    doctest.testmod()
# Trying:
#     functionA(2)
# Expecting:
#     4
# ok
# 2 items had no tests:
#     __main__
#     __main__.decoratorA
# 1 items passed all tests:
#    1 tests in __main__.functionA
# 1 tests in 3 items.
# 1 passed and 0 failed.
# Test passed.

以上

トップページに戻る
inserted by FC2 system