Manjusaka

Manjusaka

Pythonにおけるデコレーターの理解方法

Python のデコレーターを理解する方法#

まず、またこのゴミドキュメントエンジニアがやってきました。日常の水文執筆を始めます。きっかけはこの質問Python デコレーターをどう理解するか?を見たことです。ちょうど最近誰かにこれを説明したばかりで、このゴミはまた新たなラッキー記事の執筆を始めました。

予備知識#

デコレーターを理解するためには、まず Python において非常に重要な概念である「関数はファーストクラスメンバーである」ということを理解する必要があります。この文をもう少し詳しく言うと、関数は特別なタイプの変数であり、他の変数と同様に関数に引数として渡すことができ、また戻り値として返すこともできます。


def abc():
    print("abc")

def abc1(func):
    func()

abc1(abc)

このコードの出力は、関数 abc の中で出力される abc という文字列です。プロセスは非常にシンプルで、関数 abc を引数として abc1 に渡し、次に abc1 の中で渡された関数を呼び出します。

次に別のコードを見てみましょう。


def abc1():
    def abc():
        print("abc")
    return abc
abc1()()

このコードの出力も前と同じです。ここでは、abc1 内部で定義された関数 abc を変数として返し、abc1 を呼び出して戻り値を取得した後、返された関数をさらに呼び出しています。

さて、次は考えてみましょう。関数 add を実装して、add(m)(n)m+n と等価になるようにします。この問題は、前述のファーストクラスメンバーの概念を理解していれば、非常に簡単に書けるでしょう。

def add(m):
    def temp(n):
        return m+n
    return temp
print(add(1)(2))

はい、ここでの出力は 3 です。

本文#

前の予備知識を見た後、今日のテーマを始めることができます。

まずは要件を見てみましょう#

今、私たちには次のような関数があります。


def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result

今、この関数にいくつかのコードを追加して、この関数の実行時間を計算したいと思います。

私たちはおそらく考え、次のようなコードを書きました。

import time
def range_loop(a,b):
    time_flag=time.time()
    for i in range(a,b):
        temp_result=a+b
    print(time.time()-time_flag)
    return temp_result

とりあえず、こうやって計算した時間が正確かどうかは置いておいて、次のように多くの関数に時間計算機能を追加したいと思います。

import time
def range_loop(a,b):
    time_flag=time.time()
    for i in range(a,b):
        temp_result=a+b
    print(time.time()-time_flag)
    return temp_result
def range_loop1(a,b):
    time_flag=time.time()
    for i in range(a,b):
        temp_result=a+b
    print(time.time()-time_flag)
    return temp_result
def range_loop2(a,b):
    time_flag=time.time()
    for i in range(a,b):
        temp_result=a+b
    print(time.time()-time_flag)
    return temp_result

私たちはおおよそ考え、うん、Ctrl+C,Ctrl+V。emmmm さて、今、あなたたちはこのコードが特に汚いと思いませんか?私たちはそれをもっときれいにしたいと思っていますが、どうすればいいでしょう?

私たちは考え、前述のファーストクラスメンバーの概念に従って、次のようなコードを書きました。

import time
def time_count(func,a,b):
    time_flag=time.time()
    temp_result=func(a,b)
    print(time.time()-time_flag)
    return temp_result
    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop1(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop2(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
time_count(range_loop,a,b)
time_count(range_loop1,a,b)
time_count(range_loop2,a,b)

うん、見た目はそれっぽいですが、さて、今度は新しい問題が出てきました。私たちは今、すべての関数が 2 つの引数だけを受け取ると仮定していますが、任意の引数をサポートしたい場合はどうすればいいでしょう?私たちは眉をひそめ、次のようなコードを書きました。


import time
def time_count(func,*args,**kwargs):
    time_flag=time.time()
    temp_result=func(*args,**kwargs)
    print(time.time()-time_flag)
    return temp_result
    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop1(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop2(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
time_count(range_loop,a,b)
time_count(range_loop1,a,b)
time_count(range_loop2,a,b)

さて、今見てみると、少しそれっぽくなりましたが、もう一度考えてみましょう。このコードは実際には私たちの関数呼び出しの方法を変更しました。たとえば、range_loop(a,b) を直接実行しても関数の実行時間を取得することはできません。さて、もし関数の呼び出し方法を変更せずに関数の実行時間を取得したい場合はどうすればいいでしょう?

とても簡単です。置き換えればいいのです。


import time
def time_count(func):
    def wrap(*args,**kwargs):
        time_flag=time.time()
        temp_result=func(*args,**kwargs)
        print(time.time()-time_flag)
        return temp_result
    return wrap
    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop1(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
def range_loop2(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
range_loop=time_count(range_loop)
range_loop1=time_count(range_loop1)
range_loop2=time_count(range_loop2)
range_loop(1,2)
range_loop1(1,2)
range_loop2(1,2)

emmmm、こう見ると、ずいぶん快適に感じますね?元の実行方法を変更することなく、関数の実行時間を出力しました。

しかし。。。あなたたちは手動で置き換えるのがあまりにも面倒だと思いませんか???他に簡略化できる方法はありませんか?

さて、Python は私たちが甘いものが好きな子供であることを知っていて、新しい構文糖を提供してくれました。これが今日の主役、デコレーターです。

デコレーターについて#

私たちは前述のように、関数の特性を変更することなく、既存のコードに新しい機能を追加しましたが、手動での置き換えがあまりにも面倒だと感じています。そう、Python の公式もそれがとても面倒だと感じているので、新しい構文糖が登場しました。

私たちの上記のコードは次のように書き換えることができます。


import time
def time_count(func):
    def wrap(*args,**kwargs):
        time_flag=time.time()
        temp_result=func(*args,**kwargs)
        print(time.time()-time_flag)
        return temp_result
    return wrap
@time_count    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
@time_count
def range_loop1(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
@time_count
def range_loop2(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result
range_loop(1,2)
range_loop1(1,2)
range_loop2(1,2)

わあ、ここまで書いて、あなたはひらめいたのではないでしょうか!?まさか???そう、実際には @ 記号は構文糖であり、私たちが以前手動で置き換えたプロセスを環境に実行させるのです。さて、簡単に言うと、@ の役割は、包まれた関数をデコレータ関数 / クラスに変数として渡し、デコレータ関数 / クラスが返す値で元の関数を置き換えることです。

@decorator
def abc():
    pass

前述のように、実際には特別な置き換えプロセスが発生しています abc=decorator(abc) 。さて、いくつかの問題を解決して練習してみましょう。


def decorator(func):
    return 1
@decorator
def abc():
    pass
abc()

このコードは何が起こるでしょうか?答え:例外が発生します。なぜですか?答え:デコレータの際に置き換えが発生し、abc=decorator(abc) となり、置き換え後の abc の値は 1 になります。整数はデフォルトで関数として呼び出すことはできません。


def time_count(func):
    def wrap(*args,**kwargs):
        time_flag=time.time()
        temp_result=func(*args,**kwargs)
        print(time.time()-time_flag)
        return temp_result
    return wrap

def decorator(func):
    def wrap(*args,**kwargs):
        temp_result=func(*args,**kwargs)
        return temp_result
    return wrap

def decorator1(func):
    def wrap(*args,**kwargs):
        temp_result=func(*args,**kwargs)
        return temp_result
    return wrap

@time_count
@decorator
@decorator1    
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result

このコードはどのように置き換えられるのでしょうか?答え:time_count(decorator(decorator1(range_loop)))

うん、今、デコレーターについて基本的な理解ができたのではないでしょうか?

拡張#

さて、私は前に書いた time_count 関数を修正して、flag パラメータを受け取れるようにしたいと思います。flagTrue のときは関数の実行時間を出力し、False のときは出力しないようにします。

私たちは一歩一歩進んでいきます。新しい関数を time_count_plus と呼ぶと仮定しましょう。

私たちが実現したい効果は次のようなものです。

@time_count_plus(flag=True)
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result

さて、私たちはまず time_count_plus(flag=True) を一度呼び出し、それが返す値をデコレータ関数として range_loop を置き換えます。OK、ではまず time_count_plus はパラメータを受け取り、関数を返す必要がありますよね。

def time_count_plus(flag=True):
    def wrap1(func):
        pass
    return wrap1

さて、関数をデコレータ関数として返すことができました。次に、私たちは @ が実際には一度の置き換えプロセスを引き起こすことを知っています。さて、私たちの置き換えは range_loop=time_count_plus(flag=True)(range_loop) ではないでしょうか。さて、皆さんは非常に明確に理解しているはずです。wrap1 の中にも関数が必要で、戻り値を返す必要があります。

うん、最終的なコードは次のようになります。

def time_count_plus(flag=True):
    def wrap1(func):
        def wrap2(*args,**kwargs):
            if flag:
                time_flag=time.time()
                temp_result=func(*args,**kwargs)
                print(time.time()-time_flag)
            else:
                temp_result=func(*args,**kwargs)
            return temp_result
        return wrap2
    return wrap1
@time_count_plus(flag=True)
def range_loop(a,b):
    for i in range(a,b):
        temp_result=a+b
    return temp_result

これでずいぶんクリアになったのではないでしょうか!

拡張その 2#

さて、私たちには新しい要件が出てきました。

m=3
n=2
def add(a,b):
    return a+b

def sub(a,b):
    return a-b

def mul(a,b):
    return a*b

def div(a,b):
    return a/b

今、文字列 a があり、a の値は +-*/ のいずれかです。さて、a の値に基づいて対応する関数を呼び出したいのですが、どうすればいいでしょう?

私たちは考え、うん、論理判断ですね。


m=3
n=2
def add(a,b):
    return a+b

def sub(a,b):
    return a-b

def mul(a,b):
    return a*b

def div(a,b):
    return a/b
a=input('任意の + - * / を入力してください\n')
if a=='+':
    print(add(m,n))
elif a=='-':
    print(sub(m,n))
elif a=='*':
    print(mul(m,n))
elif a=='/':
    print(div(m,n))

しかし、このコードは if else が多すぎるのではないでしょうか?私たちはよく考え、ファーストクラスメンバーの特性を使い、dict を使って演算子と関数の関連付けを実現しました。

m=3
n=2
def add(a,b):
    return a+b

def sub(a,b):
    return a-b

def mul(a,b):
    return a*b

def div(a,b):
    return a/b
func_dict={"+":add,"-":sub,"*":mul,"/":div}
a=input('任意の + - * / を入力してください\n')
func_dict[a](m,n)

emmmm、見た目は良さそうですが、登録プロセスをさらに簡略化できる方法はありませんか?この時、デコレーターの構文特性を利用できます。

m=3
n=2
func_dict={}
def register(operator):
    def wrap(func):
        func_dict[operator]=func
        return func
    return wrap
@register(operator="+")
def add(a,b):
    return a+b
@register(operator="-")
def sub(a,b):
    return a-b
@register(operator="*")
def mul(a,b):
    return a*b
@register(operator="/")
def div(a,b):
    return a/b

a=input('任意の + - * / を入力してください\n')
func_dict[a](m,n)

さて、私たちが前述のように @ 構文を使用する際、実際には置き換えプロセスが引き起こされることを覚えていますか?ここでは、この特性を利用して、デコレーターがトリガーされるときに関数マッピングを登録しています。これにより、私たちは直接 'a' の値に基づいて関数を取得できます。また、注意すべき点は、ここでは元の関数を変更する必要がないため、第三層の関数を書く必要はありません。

Flask に精通している方は、route メソッドを呼び出してルートを登録する際にもこの特性が使用されていることを知っているでしょう。別の記事を参考にしてください。 初心者が Flask のソースコードを読むシリーズ(1):Flask のルーターの初探

まとめ#

実際、この記事全体を通して、皆さんは次のようなことを理解できたはずです。Python におけるデコレーターは、実際にはファーストクラスメンバーの概念のさらに一歩進んだ応用であり、関数を他の関数に渡し、新しい機能を包み込んで返すことです。@ は実際にはこのプロセスを簡略化しただけです。Python では、デコレーターは至る所に存在し、多くの公式ライブラリの実装もデコレーターに依存しています。例えば、以前に書いたこのゴミ文 初心者が Flask のソースコードを読むシリーズ(1):Flask のルーターの初探 などがあります。

さて、今日はここまでにしましょう!

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。