Python:デコレータを飾る。 再び

昨年、Habréでデコレータに関する2つの パートで非常に詳細な記事がすでにありました。 この新しい記事の目的は、前の記事よりもさらに洗練された例を理解するための時間を持たせるために、興味を引く意味のある例にすぐに取り掛かり 、すぐに従事することです。
対象読者は、高階関数やクロージャーに既に慣れているプログラマー(C#など)ですが、関数内の注釈は「メタ情報」であり、それ自体がリフレクションでのみ現れるという事実に慣れています。 このようなプログラマの目をすぐに引くPythonの機能は、関数を宣言する前にデコレータが存在することで、この関数の動作を変更できることです。



どのように機能しますか? トリッキーなことは何もありません。デコレータは、関数を修飾するための引数を取り、「修正された」ものを返す単なる関数です。

def timed(fn): def decorated(*x): start = time() result = fn(*x) print "Executing %s took %d ms" % (fn.__name__, (time()-start)*1000) return result return decorated @timed def cpuload(): load = psutil.cpu_percent() print "cpuload() returns %d" % load return load print "cpuload.__name__==" + cpuload.__name__ print "CPU load is %d%%" % cpuload() 
完全なソース
 cpuload .__ name __ ==デコレーション
 cpuload()は16を返します
 cpuloadの実行には105ミリ秒かかりました
 CPU負荷は16%です
宣言@timed def cpuload(): ...def cpuload(): ...; cpuload=timed(cpuload)展開されdef cpuload(): ...; cpuload=timed(cpuload) def cpuload(): ...; cpuload=timed(cpuload)であるため、結果として、グローバル名cpuloadtimed内のdecorated関数に関連付けられ、 fn変数を介して元のcpuload関数に閉じられます。 その結果、 cpuload.__name__==decorated

値が関数を取り、関数を返す関数である式は、デコレーターとして使用できます。 したがって、「パラメーター付きのデコレーター」(実際には、デコレーターファクトリー)を作成することができます。

 def repeat(times): """   times ,     """ def decorator(fn): def decorated2(*x): total = 0 for i in range(times): total += fn(*x) return total / times return decorated2 return decorator @repeat(5) def cpuload(): """   cpuload    """ print "cpuload.__name__==" + cpuload.__name__ print "CPU load is %d%%" % cpuload() 
完全なソース
 cpuload .__ name __ == decorated2
 cpuload()は7を返します
 cpuload()は16を返します
 cpuload()は0を返します
 cpuload()は0を返します
 cpuload()は33を返します
 CPU負荷は11%です
repeat(5)式の値は、 times=5閉じられるdecorator関数です。 この値はデコレーターとして使用されます。 実際にはdef cpuload(): ...; cpuload=repeat(5)(cpuload) def cpuload(): ...; cpuload=repeat(5)(cpuload)

1つの関数で複数のデコレータを組み合わせると、自然な順序で右から左に適用されます。 前の2つの例を@timed @repeat(5) def cpuload():に結合すると、-出力が得られます
 cpuload .__ name __ ==デコレーション
 cpuload()は28を返します
 cpuload()は16を返します
 cpuload()は0を返します
 cpuload()は0を返します
 cpuload()は0を返します
 decor2の実行には503ミリ秒かかりました
 CPU負荷は9%です
そして、デコレータの順序を変更すると- @repeat(5) @timed def cpuload(): -取得します
 cpuload .__ name __ == decorated2
 cpuload()は16を返します
 cpuloadの実行には100ミリ秒かかりました
 cpuload()は14を返します
 cpuloadの実行には109ミリ秒かかりました
 cpuload()は0を返します
 cpuloadの実行には101ミリ秒かかりました
 cpuload()は0を返します
 cpuloadの実行には100ミリ秒かかりました
 cpuload()は0を返します
 cpuloadの実行には99ミリ秒かかりました
 CPU負荷は6%です
最初のケースでは、広告はcpuload=timed(repeat(5)(cpuload))に展開され、2番目のケースではcpuload=repeat(5)(timed(cpuload)) 。 印刷された関数名に注意してください。どちらの場合でも、呼び出しのチェーンを追跡できます。

パラメトリックデコレーションの制限ケースは、 デコレータをパラメータとして取るデコレータです
 def toggle(decorator): """  ""  ""  """ def new_decorator(fn): decorated = decorator(fn) def new_decorated(*x): if decorator.enabled: return decorated(*x) else: return fn(*x) return new_decorated decorator.enabled = True return new_decorator @toggle(timed) def cpuload(): """   cpuload    """ print "cpuload.__name__==" + cpuload.__name__ print "CPU load is %d%%" % cpuload() timed.enabled = False print "CPU load is %d%%" % cpuload() 
完全なソース
 cpuload .__ name __ == new_decorated
 cpuload()は28を返します
 cpuloadの実行には101ミリ秒かかりました
 CPU負荷は28%です
 cpuload()は0を返します
 CPU負荷は0%です
デコレータの接続/切断を制御する値は、装飾された関数のenabled属性に保存されます。Pythonでは、任意の関数に任意の属性を「固定」できます。

結果のtoggle関数は、 デコレータのデコレータとしても使用できtoggle

 @toggle def timed(fn): """   timed    """ @toggle def repeat(times): """   repeat    """ @timed @repeat(5) def cpuload(): """   cpuload    """ print "cpuload.__name__==" + cpuload.__name__ print "CPU load is %d%%" % cpuload() timed.enabled = False print "CPU load is %d%%" % cpuload() 
完全なソース
 cpuload .__ name __ == new_decorated
 cpuload()は28を返します
 cpuload()は0を返します
 cpuload()は0を返します
 cpuload()は0を返します
 cpuload()は0を返します
 decor2の実行には501ミリ秒かかりました
 CPU負荷は5%です
 cpuload()は0を返します
 cpuload()は16を返します
 cpuload()は14を返します
 cpuload()は16を返します
 cpuload()は0を返します
 decor2の実行には500ミリ秒かかりました
 CPU負荷は9%です
ええと...いや、うまくいきませんでした! しかし、なぜですか?
なぜcpuload 2回cpuload呼び出されても、 timedデコレータがオフにならないのですか?

timedのグローバル名は、装飾されたデコレータに関連付けられていることを思い出してください。 new_decorated関数を使用。 これは、 timed.enabled = Falseの行が実際にnew_decorated関数の属性を変更することを意味します-両方のデコレーターの共通の「ラッパー」です。 if decorator.enabled:代わりにnew_decorated内でnew_decorated if decorator.enabled:if new_decorator.enabled:を確認することif new_decorator.enabled:が、 timed.enabled = Falseの行は両方のデコレーターを一度に無効にします。

このバグを修正しましょう。「内部」デコレータでenabled属性を使用するために、以前と同様に、 new_decorated関数にいくつかのメソッドをnew_decoratedます。

 def toggle(decorator): """  ""  ""  """ def new_decorator(fn): decorated = decorator(fn) def new_decorated(*x): #   if decorator.enabled: return decorated(*x) else: return fn(*x) return new_decorated def enable(): decorator.enabled = True def disable(): decorator.enabled = False new_decorator.enable = enable new_decorator.disable = disable enable() return new_decorator print "cpuload.__name__==" + cpuload.__name__ print "CPU load is %d%%" % cpuload() timed.disable() print "CPU load is %d%%" % cpuload() 
完全なソース
希望する結果が達成されます-切断されたtimedますが、 repeat続けます:
 cpuload .__ name __ == new_decorated
 cpuload()は14を返します
 cpuload()は16を返します
 cpuload()は0を返します
 cpuload()は0を返します
 cpuload()は0を返します
 decor2の実行には503ミリ秒かかりました 
 CPU負荷は6%です
 cpuload()は0を返します
 cpuload()は0を返します
 cpuload()は7を返します
 cpuload()は0を返します
 cpuload()は0を返します
 CPU負荷は1%です
これはPythonの最も魅力的な機能の1つです。関数だけでなく、任意の関数メソッドにも属性を追加できます。 関数は関数に置かれ、関数を駆動します。

Source: https://habr.com/ru/post/J187482/


All Articles