この記事では、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)
完璧に見えます...しかし、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
この関数は、関数オブジェクト (プロキシ関数) をパラメータとして受け取り、プロキシ関数を返します。プロキシ関数を呼び出すと、最初にログが出力され、次にプロキシ関数が呼び出され、呼び出しが完了してからログが出力され、最後に呼び出し結果が返されます。このようにして、一般化の目的は達成されるのではないでしょうか? ——任意のプロキシ関数 func
、log_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
3
、5
行は、関数呼び出しの前後で現在時刻を取得し、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()
,func
及kwargs
均为空,返回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 サイトの他の関連記事を参照してください。