Pythonデコレーターの詳しい使い方紹介(コード例)

不言
リリース: 2019-02-25 10:33:34
転載
2711 人が閲覧しました

この記事では、Python デコレータの詳細な使用方法 (コード例) を紹介します。一定の参考価値があります。必要な友人は参照してください。お役に立てば幸いです。

Python では、通常、デコレータは関数を修飾してパブリック関数を実装し、コードの再利用を実現するために使用されます。関数定義の前に @xxxx を追加すると、関数は特定の動作を挿入します。これは驚くべきことです。ただし、これは単なる糖衣構文です。

シナリオ

データを別の方法で処理するために使用される機能する関数がいくつかあると仮定します。

def work_bar(data):
    pass


def work_foo(data):
    pass
ログイン後にコピー

これを関数呼び出しの前後に実行したいと考えています。ログを出力したいのですがどうすればいいですか?

愚かな解決策

logging.info('begin call work_bar')
work_bar(1)
logging.info('call work_bar done')
ログイン後にコピー

複数のコード呼び出しがある場合はどうなりますか?考えるだけで怖いです!

関数のパッケージ化

愚かな解決策は、コードの冗長性を高めすぎることであり、各関数呼び出しを再度記述する必要がありますロギング。冗長ロジックのこの部分は、新しい関数にカプセル化できます:

def smart_work_bar(data):
    logging.info('begin call: work_bar')
    work_bar(data)
    logging.info('call doen: work_bar')
ログイン後にコピー

このようにして、smart_work_bar を毎回呼び出すことができます:

smart_work_bar(1)

# ...

smart_work_bar(some_data)
ログイン後にコピー

General Closure

完璧に見えます...しかし、work_foo にも同じニーズがある場合、smart_work_foo を再度実装する必要がありますか?これは明らかに非科学的です!

心配しないでください。クロージャを使用できます。

def log_call(func):
    def proxy(*args, **kwargs):
        logging.info('begin call: {name}'.format(name=func.func_name))
        result = func(*args, **kwargs)
        logging.info('call done: {name}'.format(name=func.func_name))
        return result
    return proxy
ログイン後にコピー

この関数は、関数オブジェクト (プロキシ関数) をパラメータとして受け取り、プロキシ関数を返します。プロキシ関数を呼び出すと、最初にログが出力され、次にプロキシ関数が呼び出され、呼び出しが完了してからログが出力され、最後に呼び出し結果が返されます。このようにして、一般化の目的は達成されるのではないでしょうか? ——任意のプロキシ関数 funclog_call を簡単に処理できます。

smart_work_bar = log_call(work_bar)
smart_work_foo = log_call(work_foo)

smart_work_bar(1)
smart_work_foo(1)

# ...

smart_work_bar(some_data)
smart_work_foo(some_data)
ログイン後にコピー

1 で、log_call はパラメータ work_bar を受け取り、プロキシ関数 proxy を返し、それを割り当てます。 smart_work_barまで。 4 行目では、プロキシ関数 proxy である smart_work_bar を呼び出し、最初にログを出力し、次に func を呼び出します。 work_bar、最後にログを出力します。プロキシ関数では、func が渡された work_bar オブジェクトと密接に関連していることに注意してください。これは クロージャ です。

繰り返しますが、プロキシ関数名を上書きできます。新しい名前の前に smart_ を付けるのは、まだ少し面倒です:

work_bar = log_call(work_bar)
work_foo = log_call(work_foo)

work_bar(1)
work_foo(1)
ログイン後にコピー

構文シュガー

まず次のコードを見てください。

def work_bar(data):
    pass
work_bar = log_call(work_bar)


def work_foo(data):
    pass
work_foo = log_call(work_foo)
ログイン後にコピー

コードは冗長ではありませんが、まだ十分に直観的ではありません。このとき、構文シュガーが登場します~~~

@log_call
def work_bar(data):
    pass
ログイン後にコピー

ですので、1 つのことに注意してください (強調 )。ここでの @log_call の関数は次のとおりです。 Pythonコンパイラはコードwork_bar = log_call(work_bar)を挿入します。

評価デコレータ

まずはデコレータ eval_now が何をするのか推測してみましょう。

def eval_now(func):
    return func()
ログイン後にコピー

奇妙に思えます。プロキシ関数が定義されていません。これはデコレータとみなされますか?

@eval_now
def foo():
    return 1

print foo
ログイン後にコピー

このコードは、関数を呼び出して評価するための 1 を出力します。それで何の役に立つの? foo = 1 と直接書くことはできないのでしょうか?この簡単な例では、このように書くことももちろん可能です。より複雑な例を見てみましょう - ログ オブジェクトの初期化:

# some other code before...

# log format
formatter = logging.Formatter(
    '[%(asctime)s] %(process)5d %(levelname) 8s - %(message)s',
    '%Y-%m-%d %H:%M:%S',
)

# stdout handler
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setFormatter(formatter)
stdout_handler.setLevel(logging.DEBUG)

# stderr handler
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setFormatter(formatter)
stderr_handler.setLevel(logging.ERROR)

# logger object
logger = logging.Logger(__name__)
logger.setLevel(logging.DEBUG)
logger.addHandler(stdout_handler)
logger.addHandler(stderr_handler)

# again some other code after...
ログイン後にコピー

Use eval_now:

# some other code before...

@eval_now
def logger():
    # log format
    formatter = logging.Formatter(
        '[%(asctime)s] %(process)5d %(levelname) 8s - %(message)s',
        '%Y-%m-%d %H:%M:%S',
    )

    # stdout handler
    stdout_handler = logging.StreamHandler(sys.stdout)
    stdout_handler.setFormatter(formatter)
    stdout_handler.setLevel(logging.DEBUG)

    # stderr handler
    stderr_handler = logging.StreamHandler(sys.stderr)
    stderr_handler.setFormatter(formatter)
    stderr_handler.setLevel(logging.ERROR)

    # logger object
    logger = logging.Logger(__name__)
    logger.setLevel(logging.DEBUG)
    logger.addHandler(stdout_handler)
    logger.addHandler(stderr_handler)

    return logger

# again some other code after...
ログイン後にコピー

2 つのコードの目的は同じですが、後者の方が明らかに明瞭で、コード ブロックのスタイルを持っています。さらに重要なのは、一時変数 (formatter など) が外部名前空間 (グローバルなど) を汚染するのを避けるために、関数呼び出しはローカル名前空間で初期化されることです。

パラメータを使用したデコレータ

遅い関数呼び出しを記録するためのデコレータを定義します:

def log_slow_call(func):
    def proxy(*args, **kwargs):
        start_ts = time.time()
        result = func(*args, **kwargs)
        end_ts = time.time()

        seconds = start_ts - end_ts
        if seconds > 1:
        logging.warn('slow call: {name} in {seconds}s'.format(
            name=func.func_name,
            seconds=seconds,
        ))

        return result

    return proxy
ログイン後にコピー

35 行は、関数呼び出しの前後で現在時刻を取得し、7 行で呼び出し時間を計算しており、1 秒以上かかる場合は警告ログが出力されます。

@log_slow_call
def sleep_seconds(seconds):
    time.sleep(seconds)

sleep_seconds(0.1)  # 没有日志输出

sleep_seconds(2)    # 输出警告日志
ログイン後にコピー

ただし、しきい値の設定は常に状況に依存し、機能ごとに異なる値が設定される場合があります。しきい値をパラメータ化する方法があれば良いでしょう:

def log_slow_call(func, threshold=1):
    def proxy(*args, **kwargs):
        start_ts = time.time()
        result = func(*args, **kwargs)
        end_ts = time.time()

        seconds = start_ts - end_ts
        if seconds > threshold:
        logging.warn('slow call: {name} in {seconds}s'.format(
            name=func.func_name,
            seconds=seconds,
        ))

        return result

    return proxy
ログイン後にコピー

ただし、 @xxxx 構文シュガーは常に、パラメータとして装飾された関数を使用してデコレータを呼び出します。 しきい値 パラメータを渡す可能性はありません。どうやってするの? ——クロージャを使用して、threshold パラメータをカプセル化します:

def log_slow_call(threshold=1):
    def decorator(func):
        def proxy(*args, **kwargs):
            start_ts = time.time()
            result = func(*args, **kwargs)
            end_ts = time.time()

            seconds = start_ts - end_ts
            if seconds > threshold:
            logging.warn('slow call: {name} in {seconds}s'.format(
                name=func.func_name,
                seconds=seconds,
            ))

            return result

        return proxy

    return decorator


@log_slow_call(threshold=0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)
ログイン後にコピー

このように、log_slow_call(threshold=0.5) は戻り関数 decorator## を呼び出します。 #、関数にはクロージャ変数 threshold があり、値は 0.5 です。 decorator模様替えsleep_秒

デフォルトのしきい値を使用すると、関数呼び出しを省略できません:

@log_slow_call()
def sleep_seconds(seconds):
    time.sleep(seconds)
ログイン後にコピー
乙女座は最初の行のかっこのペアに満足していない可能性があるため、次のように改善できます:

def log_slow_call(func=None, threshold=1):
    def decorator(func):
        def proxy(*args, **kwargs):
            start_ts = time.time()
            result = func(*args, **kwargs)
            end_ts = time.time()

            seconds = start_ts - end_ts
            if seconds > threshold:
            logging.warn('slow call: {name} in {seconds}s'.format(
                name=func.func_name,
                seconds=seconds,
            ))

            return result

        return proxy

    if func is None:
        return decorator
    else:
        return decorator(func)
ログイン後にコピー
This この書き込み方法は、使用法

A デフォルトしきい値 (呼び出しなし) と使用法 B カスタムしきい値 (呼び出しあり) という 2 つの異なる使用法と互換性があります。

# Case A
@log_slow_call
def sleep_seconds(seconds):
    time.sleep(seconds)


# Case B
@log_slow_call(threshold=0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)
ログイン後にコピー
Usage

A、何が起こるかというと log_slow_call(sleep_seconds) です。つまり、func パラメータは空ではありません。これは直接的なものです。調整decoratorラップして返します (しきい値はデフォルトです)。

用法B中,先发生的是log_slow_call(threshold=0.5)func参数为空,直接返回新的装饰器decorator,关联闭包变量threshold,值为0.5;然后,decorator再装饰函数sleep_seconds,即decorator(sleep_seconds)。注意到,此时threshold关联的值是0.5,完成定制化。

你可能注意到了,这里最好使用关键字参数这种调用方式——使用位置参数会很丑陋:

# Case B-
@log_slow_call(None, 0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)
ログイン後にコピー

当然了,函数调用尽量使用关键字参数是一种极佳实践,含义清晰,在参数很多的情况下更是如此。

智能装饰器

上节介绍的写法,嵌套层次较多,如果每个类似的装饰器都用这种方法实现,还是比较费劲的(脑子不够用),也比较容易出错。

假设有一个智能装饰器smart_decorator,修饰装饰器log_slow_call,便可获得同样的能力。这样,log_slow_call定义将变得更清晰,实现起来也更省力啦:

@smart_decorator
def log_slow_call(func, threshold=1):
    def proxy(*args, **kwargs):
        start_ts = time.time()
        result = func(*args, **kwargs)
        end_ts = time.time()

        seconds = start_ts - end_ts
        if seconds > threshold:
        logging.warn('slow call: {name} in {seconds}s'.format(
            name=func.func_name,
            seconds=seconds,
        ))

        return result

    return proxy
ログイン後にコピー

脑洞开完,smart_decorator如何实现呢?其实也简单:

def smart_decorator(decorator):

    def decorator_proxy(func=None, **kwargs):
        if func is not None:
            return decorator(func=func, **kwargs)

        def decorator_proxy(func):
            return decorator(func=func, **kwargs)

        return decorator_proxy

    return decorator_proxy
ログイン後にコピー

smart_decorator实现了以后,设想就成立了!这时,log_slow_call,就是decorator_proxy(外层),关联的闭包变量decorator是本节最开始定义的log_slow_call(为了避免歧义,称为real_log_slow_call)。log_slow_call支持以下各种用法:

# Case A
@log_slow_call
def sleep_seconds(seconds):
    time.sleep(seconds)
ログイン後にコピー

用法A中,执行的是decorator_proxy(sleep_seconds)(外层),func非空,kwargs为空;直接执行decorator(func=func, **kwargs),即real_log_slow_call(sleep_seconds),结果是关联默认参数的proxy

# Case B
# Same to Case A
@log_slow_call()
def sleep_seconds(seconds):
    time.sleep(seconds)
ログイン後にコピー

用法B中,先执行decorator_proxy()funckwargs均为空,返回decorator_proxy对象(内层);再执行decorator_proxy(sleep_seconds)(内层);最后执行decorator(func, **kwargs),等价于real_log_slow_call(sleep_seconds),效果与用法A一致。

# Case C
@log_slow_call(threshold=0.5)
def sleep_seconds(seconds):
    time.sleep(seconds)
ログイン後にコピー

用法C中,先执行decorator_proxy(threshold=0.5)func为空但kwargs非空,返回decorator_proxy对象(内层);再执行decorator_proxy(sleep_seconds)(内层);最后执行decorator(sleep_seconds, **kwargs),等价于real_log_slow_call(sleep_seconds, threshold=0.5),阈值实现自定义!

以上がPythonデコレーターの詳しい使い方紹介(コード例)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:segmentfault.com
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート