【Python】デコレーターを使って関数を修飾する

Python

この記事では、Pythonのデコレーターの使い方を解説します。

デコレーターとは、関数をデコる(死語?)ための仕組みです。もう少し真面目に説明すると、関数にデコレーターを付けることで、その関数を実行した際にデコレーター定義関数に実行した関数を渡し、デコレーター定義関数を実行します。

言葉ではわかりにくいと思うのでサンプルを見てみましょう。
試しに以下のtest関数にデコレータを修飾してみます。

def test():
    print('test関数')

この関数にデコレーターを使って修飾してみます。

# デコレーター定義関数
def decorator(func):
    def wrapper(*args, **kwargs):
        print('decorator start')
        func(*args, **kwargs)
        print('decorator end')
    return wrapper


@decorator
def test():
    print('test関数')


test()
# decorator start
# test関数
# decorator end

デコレーター定義関数を作成し、test関数を修飾しています。それにより、test関数を実行した際に、decorator関数が処理されているのがわかります。

このように、デコレーター定義関数を作成し、関数にデコレーターを付与することで、その関数の実行時にデコレーター定義関数を実行します。

デコレーターの基本

デコレーターのことが何となく解ったところで、記述方法を見ていきましょう。

書式

まずはデコレーターの定義関数の作成方法を見ていきましょう。

def デコレーター名(func):
    def wrapper(*args, **kw):
        # 処理
    return wrapper

wrapperという関数名に文法的な意味はありません。
このように作成したデコレーターを以下のように関数に付与します。

@デコレーター名
def 何らかの関数():
    # 処理

デコレーターが付与された関数は、実行時にデコレーター定義関数の引数funcに渡されてデコレーター定義関数が実行されます。

同等の呼び出し

簡単な処理の関数を2つ作成し、1つにはデコレーターを付与した。この2つの関数が同じ結果になるように呼び出してみる。

def deco(func):
    def wrapper(*args, **kw):
        print('deco - start')
        func(*args, **kw)
        print('deco - end')
    return wrapper


@deco
def func1(text):
    print(text)


def func2(text):
    print(text)


# デコレーター有り
func1('func1')
# deco - start
# func1
# deco - end

# デコレーター無し
deco(func2)('func2')
# deco - start
# func2
# deco - end

22行目の呼び出し方法をデコレーターを使うことで表現できる。

何だか紛らわしいが、deco関数を実行すると(func2)を引数として受け取り、wrapper関数を返します。そして、返されたwrapper関数が(‘func2’)引数を受け取り実行します。

引数を使用するデコレーター

引数が定義されている関数にデコレーターを付与させることもできます。

通常の引数と可変長引数を定義した関数にデコレーターを付与させ、動作を確認してみます。

def decorator(func):
    def wrapper(*args, **kw):
        func(*args, **kw)
    return wrapper


# 通常の引数と可変長引数を取る関数
@decorator
def test(text, *args, **kw):
    print(text)
    print(args)
    print(kw)


test('test関数です', 1, 2, 3, one=1, two=2, three=3)
# test関数です
# (1, 2, 3)
# {'one': 1, 'two': 2, 'three': 3}

3行目でfuncの引数に*argsと**kwを指定しています。これによって、引数を正常に引き渡せています。

アンパック

リストなどにアスタリスクをつけることで要素を分解して渡すことができます。

以下のコードはタプルをアンパックして出力しています。

args = ('arg', 1, 2, 3)

print(*args)
# arg 1 2 3

これは以下のコードと同等です。

print(args[0], args[1], args[2], args[3])

アンパックと可変長引数を組み合わせることで便利に使用できます。

例えば以下のような関数があったとします。

def func(arg, *args):
    print(arg)
    print(args)

この関数に、先ほどのargsの値をアンパックを使わず、渡そうと思ったら以下のようになります。

func(args[0], args[1], args[2], args[3])

この記述はめんどくさいし、インデックスを指定してしまっているので可変長引数の意味がなくなってしまいます。

しかし、アンパックを使用することで簡潔に指定でき、配列の長さが変わっても問題ありません。

func(*args)

もちろん、ディクショナリな可変長引数もアンパック出来ます。

こちらのアンパックはアスタリスクを2つ付けます。

kw = {'one': 1, 'two': 2, 'three': 3}


def func(**kw):
    print(kw)


func(**kw)
# {'one': 1, 'two': 2, 'three': 3}

kwをアンパックすると「one=1, two=2, three=3」と言う形で取り出されます。関数に使用することで引数名を指定して値を引き渡すこともできます。

kw = {'one': 1, 'two': 2, 'three': 3}


def func(one, two, three):
    print(one, two, three)


func(**kw)
# 1 2 3

この呼び出しは以下と同等です。

func(one=1, two=2, three=3)

戻り値を返すデコレーター

戻り値が定義されている関数のデコレーターの定義方法を見ていきます。

例えば、以下のように定義します。

def decorator(func):
    def wrapper(*args, **kw):
        result = func(*args, **kw)
        twice = result * 2
        return result, twice
    return wrapper


@decorator
def add(x, y):
    return x + y


r = add(1, 2)
print(r)
# (3, 6)

wrapper関数内でfunc関数の戻り値を返すことで戻り値を定義できました。

返す前に値を加工することもでき、これによりadd関数の結果を加工して、全く別物として返すこともできます。

デコレーターに引数を渡す

デコレーターに引数を渡して、デコレーター定義関数内で使用することができます。

以下のコードは、関数のスピードを測るデコレーターです。
デコレーターに指定した引数の回数だけ繰り返し処理され、その平均値を返します。

import time


def func_speed(count):
    def _func_speed(func):
        def wrapper(*args, **kw):
            t = 0
            for i in range(count):
                s_t = time.time()
                func(*args, **kw)
                t += time.time() - s_t
            return t / count
        return wrapper
    return _func_speed


@func_speed(10)
def func(second):
    # sleepで引数に指定された秒数だけ、実行を停止
    time.sleep(second)


print(func(0.1))
# 0.10340278148651123

実行結果が約0.1秒だったので正常に速度を測れているのがわかります。しかし、このままではデコレーターに引数を渡さなかった場合に正常に動作しません。

なので、デコレーターに引数が渡されなかった場合の処理も追加しておきます。

# 引数名をargに変更
def func_speed(arg):
    def _func_speed(func):
        def wrapper(*args, **kw):
            t = 0
            for i in range(count):
                s_t = time.time()
                func(*args, **kw)
                t += time.time() - s_t
            return t / count
        return wrapper

    # 追加
    if isinstance(arg, int):
        count = arg
        return _func_speed
    elif callable(arg):
        count = 1
        return _func_speed(arg)

14行目でargがintかどうかを判別しています。argがintならばcountにargを代入して_func_speed関数を返しています。

17行目ではargが関数かどうかを判別しています。argが関数ならばデコレーターに引数を渡していないということなので、countに1を代入し、_func_speed関数にargを引数として渡し、その実行結果を返しています。

デコレーターを複数付ける

デコレーターを複数つけることもできます。

以下のコードはtest関数に複数のデコレーターを付けています。

def deco1(func):
    def wrapper(*args, **kw):
        print('deco1 start')
        func(*args, *kw)
        print('deco1 end')
    return wrapper


def deco2(func):
    def wrapper(*args, **kw):
        print('deco2 end')
        func(*args, **kw)
        print('deco2 end')
    return wrapper


@deco1
@deco2
def test():
    print('test関数')


test()
# deco1 start
# deco2 start
# test関数
# deco2 end
# deco1 end

実行結果を見ると、初めにdeco1が処理されてfuncが実行されたらdeco2が処理されています。deco2が処理されたらtest関数が処理されているのでtest関数はdeco2のfuncに渡されているのがわかります。そして、deco2が処理し終わったらdeco1の残りの処理をしています。

同等の呼び出し

覚える必要はないですが一応参考までに…。

先ほどの複数のデコレーターを付けた関数と同等の呼び出し方法です。

deco1(deco2(test))()

うん、めちゃくちゃわかりずらい!!
デコレーターを使うことでシンプルに記述できているのがわかります。

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