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

これでずっとクリアになりましたね!

さらに拡張#

さて、今、私たちには新しい要件があります。

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 のルーター初探

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

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