トラック
/
Python
Python
/
シラバス
/
デコレーター
デコ

デコレーター の Python

{other: "%{count}個の演習"}

デコレーターについて

デコレーターとは、渡された関数の振る舞いを拡張したり置き換えたりするために、別の関数を引数として受け取る関数です。 関数Aがデコレーターで、関数Bがその引数であるとき、関数Aは関数Bのコードを_変更することなく_、関数Bの振る舞いを変更、拡張、置き換えします。 デコレーター関数Aが関数Bを_ラップする_と言います。 「振る舞いを変更する」と言っていますが、ラップされた関数自体は_実際には変更されません_。 振る舞いは、ラップされた関数の_まわり_(と、それが返すもの)に追加されるか、その振る舞いが別の振る舞いに_置き換わる_かのどちらかです。

デコレーターは高階関数です

高階関数とは、1つ以上の関数を引数として受け取ったり、1つ以上の関数を返したりする関数です。 引数として使ったり、別の関数から返したりできる関数を、第一級関数と呼びます。 Pythonの関数は、呼び出し可能オブジェクトとして、他の値やオブジェクトと同じように、変数に代入したり引数として使ったりできる第一級関数です。 高階関数と第一級関数が組み合わさることで、デコレーターが実現できます。

デコレーターの使い方

@記号は、デコレーター関数の名前の前に付けて、デコレートしたい関数のすぐ上に書きます。

@decorator
def decorated_function():
    pass

引数を受け取るデコレーターもあります。

@decorator_with_arg(name="Bob")
def decorated_function2():
    pass

デコレーターにデフォルト引数が定義されている場合は、他の関数を呼び出すときと同じように、デコレーターを機能させるには@decorator()のように括弧(())を付けて呼び出さなければなりません。

@decorator_with_default_arg()
def decorated_function3():
    pass

デコレーターが_位置引数_を取る場合、その引数を渡さないと、次のようなエラーになります。

TypeError: decorator_with_pos_arg() missing 1 required positional argument: 'name'

@decoratorという構文は、_デコレートする側の関数_を呼び出し、_デコレートされる関数_を引数として渡すための糖衣構文(シンタックスシュガー)、つまり省略記法です。 デコレーターを呼び出す別の方法の例を、次に示します。

def function():
    pass

function = decorator(function)


def function2():
    pass

function2 = decorator_with_arg(name="Bob")(function2)


def function3():
    pass

function3 = decorator_with_default_arg()(function3)

シンプルなデコレーターを書く

ほとんどのデコレーターは、別の関数の振る舞いを_拡張_したり_置き換え_たりするためのものですが、ラップしている関数をそのまま返すだけのデコレーターもあります。

デコレーターは、少なくとも1つの引数、つまりラップする関数を受け取る関数です。 通常は、ラップした関数そのものか、その関数を使った式の結果を返します。

シンプルなデコレーター、つまりラップした関数をそのまま返すだけのデコレーターは、次のように書けます。

>>> def do_nothing(func):
...     return func
...
... @do_nothing
... def function4():
...     return 4
...
>>> print(function4())
4

デコレーターは、ログに使う追加情報のような副作用だけを加えることもあります。

>>> def my_logger(func):
...     print(f"Entering {func.__name__}")
...     return func
...
... @my_logger
... def my_func():
...     print("Hello")
...
>>> my_func()
Entering my_func
Hello

デコレーターは自分自身を返すわけではありません。 自分の関数引数を返すこともあれば、別の関数を返すこともあり、渡された関数(デコレートした関数)の戻り値を置き換える1つ以上の値を返すこともあります。 デコレーターが別の関数を返す場合、通常は内部関数を返します。

内部関数

関数は、関数の中でも定義できます。 このように入れ子になった関数を、内部関数と呼びます。 デコレーターは、内部関数を使って自分の関数引数をラップできます。 デコレーターは、その内部関数を返します。 内部関数は、元の関数引数を返すこともあります。

内部関数を使った検証デコレーター

次は、検証にデコレーターを使った例です。

def my_validator(func):
    def my_wrapper(world):
        print(f"Entering {func.__name__} with {world} argument")
        
        if world == "Pluto":
            print("Pluto is not a planet!")
        else:
            return func(world)
    return my_wrapper

@my_validator
def my_func(planet):
    print(f"Hello, {planet}!")

my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!

my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Pluto is not a planet!

最初の行では、デコレーターのfunc引数が定義されています。 次の行では、func引数をラップする、デコレーターの_内部関数_が定義されています。 _内部関数_はデコレーターのfunc引数をラップするので、funcに渡されるのと同じ引数を受け取ります。 なお、ラッパーは、funcで定義された引数と同じ名前を使う必要はありません。 元の関数ではplanet、デコレーターではworldという名前を使っていますが、デコレーターはちゃんと動きます。

内部関数は、funcを返すか、world == "Pluto"の場合にはPlutoは惑星ではないと出力します。 代わりにValueErrorを発生させるように書くこともできます。 つまり、_内部関数_はfuncをラップし、funcを返すか、funcがするはずのことの代わりになる何かを行います。 デコレーターは、自分の_内部関数_を返します。 _内部関数_が、渡された元の関数を返すこともあれば、返さないこともあります。 ラッパー関数または_内部関数_の中で条件によってどのコードが実行されるかによって、funcが返されたり、エラーが発生したり、funcの戻り値と同じ型の値が返されたりします。

任意の数の引数を取る関数をデコレートする

*argsと**kwargsの構文を使えば、任意の数の引数を取る関数のためのデコレーターも書けます。

次は、任意の数の引数を取る関数のためのデコレーターの例です。

def double(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs) * 2
    return wrapper

@double
def add(*args):
    return sum(args)

print(add(2, 3, 4))
#-> 18

print(add(2, 3, 4, 5, 6))
#-> 40

これは、関数引数の戻り値を2倍にする仕組みです。 戻り値を3倍、4倍などにしたい場合は、次のセクションで示すように、デコレーター自体に仮引数を追加します。

独自の仮引数を持つデコレーター

次は、デコレートした関数の戻り値を好きな倍数にできるよう設定できるデコレーターの例です。

def multi(factor=1):
     if factor == 0:
        raise ValueError("factor must not be 0")

     def outer_wrapper(func):
         def inner_wrapper(*args, **kwargs):
             return func(*args, **kwargs) * factor
         return inner_wrapper
     return outer_wrapper

@multi(factor=3)
def add(*args):
    return sum(args)

print(add(2, 3, 4))
#-> 27

print(add(2, 3, 4, 5, 6))
#-> 60

最初の行で、factorが0でないことを検証しています。 次に、外側のラッパーを定義します。 これは、仮引数を取らないデコレーターに期待されるのと同じシグネチャーです。 外側のラッパーには、元の関数と同じシグネチャーを持つ内部関数があります。 内側のラッパーが、元の関数の戻り値に、デコレーターに渡された引数を掛ける処理を行います。 外側のラッパーは内側のラッパーを返し、デコレーターは外側のラッパーを返します。

次は、元の関数に渡された引数を検証するかどうかを制御できる、仮引数を持つデコレーターの例です。

def check_for_pluto(check=True):
    def my_validator(func):
        def my_wrapper(world):
            print(f"Entering {func.__name__} with {world} argument")
            if check and world == "Pluto":
                print("Pluto is not a planet!")
            else:
                return func(world)
        
        return my_wrapper
    return my_validator

@check_for_pluto(check=False)
def my_func(planet):
    print(f"Hello, {planet}!")

my_func("World")
#-> Entering my_func with World argument
#-> Hello, World!

my_func("Pluto")
#-> Entering my_func with Pluto argument
#-> Hello, Pluto!

これで、Plutoをチェックするかどうかを簡単に切り替えられ、しかもmy_funcを変更する必要はありません。

GitHubで編集 リンクは新しいウィンドウまたはタブで開きます