到達点は、共通処理を関数へ付け加え、資源の利用範囲を明示することです。前提は第06・09回です。
関数も値として渡せる
デコレータは関数を受け取り、別の関数などを返して振る舞いを包みます。ログや計測に便利ですが、何が起きるか読めないほど重ねると保守が難しくなります。まず手動で包む処理を理解します。
from contextlib import contextmanager
from functools import wraps
def traced(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"開始: {function.__name__}")
try:
return function(*args, **kwargs)
finally:
print(f"終了: {function.__name__}")
return wrapper
@contextmanager
def section(name):
print(f"入る: {name}")
try:
yield
finally:
print(f"出る: {name}")
@traced
def total(values):
return sum(values)
with section("集計"):
print(total([10, 20]))
print(total.__name__)
main.pyでは、集計へ入る、total開始、total終了、30、集計を出る、totalという順で表示されます。wrapsは元の関数名や説明などを保つ助けになります。*args と **kwargs は位置引数とキーワード引数を受け渡します。
後始末は成功時だけではない
contextmanagerのyield前が開始処理、後ろが終了処理です。例外でも終了処理を走らせるため、try/finallyで囲みます。中の例外を捕まえて再送出しなければ、失敗を抑止する動きになるので意図を明確にします。
今回のデコレータは同期関数向けです。async関数にそのまま付けると、コルーチンを作った時点しか囲まず、実際の非同期処理全体を計測できません。非同期版ではラッパー自体をasyncにしてawaitする必要があります。
便利な抽象化を使う前に、呼び出し順、戻り値、例外、資源の寿命が変わらないか確認します。認証やトランザクションなど重要な処理を隠す場合は、特にテストで契約を確かめます。
練習と解答
練習:totalの中でValueErrorを投げると、どの終了メッセージが出るでしょうか。
解答:totalの終了と集計の終了の両方が表示され、例外は外側へ伝わります。finallyがあるからです。例外を無視して30を返すような動作にはなりません。
公式資料
contextlibとfunctoolsが参照先です。
Python全20回の目次 | 前の回 | 次の回