Pythonでデコレータ&with文の裏側を見る
デコレータ
デコレータの例
デコレータは以下のコードで登場する@で始まる表記のことです。外部ライブラリだとFastAPIやLangChain, LangSmithなどでちょくちょく登場する印象があります。関数やクラスの直前に付けることで、その関数やクラスの振る舞いが変わります。
// dataclass
// Personクラスに__init__メソッド等が勝手に付く
@dataclass
class Person:
name: str
age: int
// FastAPI
// GETリクエストを受け付けるAPIが作成される
app = FastAPI()
@app.get("/items/{item_id}")
async def get_item(item_id: int):
return {"item_id": item_id}
// langsmith
// LLMとのやり取りが記録される
@traceable(run_type="llm")
def invoke_llm(messages):
return openai.chat.completions.create(
messages=messages, model="gpt-4o-mini", temperature=0
)
デコレータの定義
デコレータの仕組みを見る前に、なんとなく定義を確認しておきます。
Pythonの公式ドキュメントには以下のように定義されています。
Decorator expressions are evaluated when the function is defined, in the scope that contains the function definition. The result must be a callable, which is invoked with the function object as the only argument. The returned value is bound to the function name instead of the function object.
The evaluation rules for the decorator expressions are the same as for function decorators. The result is then bound to the class name.
https://docs.python.org/3/reference/compound_stmts.html
ざっくりまとめると、以下の通りです。
- 関数定義時に評価(実行)される
- 関数を唯一の引数として受け取る
- callableな戻り値を返す
- 戻り値が元の関数名にバインドされる
- classに対しても同様に使える
デコレータの仕組み
デコレータの仕組みを理解するには、実装を確認するのが手っ取り早いです。
以下は、シンプルなデコレータの例です。
def around_start_finish(func):
def wrapper(*args, **kwargs):
print(f"[start] {func.__name__}") # 関数呼び出し直前
result = func(*args, **kwargs) # デコレータを付けた関数
print(f"[finish] {result} {args[0]}!") # 関数実行後
return result
return wrapper
@around_start_finish
def say_hello(name):
print(f"Hello, {name}!")
return "Hello"
[start] around_start_finish
Hello, Alice!
[finish] Hello, Alice!
この例からわかるデコレータの特徴は、
- 関数の前後に、適当な処理を置ける。
- 引数、戻り値を好き勝手に扱える。
といったところでしょう。
関数の実行時間の計測やログ出力、一時的な定数の変更、などと便利な使い道がありそうです。
with文
with文の例
with文は以下のコードで登場するwithキーワードを使った構文のことです。ファイル操作やデータベース接続などでよく登場します。リソースの取得と解放を勝手にやってくれるのがありがたいです。
# ファイル操作
# ファイルを開いて、処理が終わったら自動的に閉じる
with open('data.txt', 'r') as f:
content = f.read()
# sqlite3
# データベース接続を自動的にクローズ
with sqlite3.connect('database.db') as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
# requests
# HTTPセッションの管理
with requests.Session() as session:
response = session.get('https://api.example.com/data')
another_response = session.get('https://api.example.com/other')
with文の仕組み
with文も仕組みを把握するときは、実装を確認するのが手っ取り早いです。
デコレータのときと同じお題で、シンプルな例を作ってみました。
class AroundStartFinish:
def __init__(self, func_name): # with文の1行目で実行
self.func_name = func_name
self.args = None
self.result = None
def __enter__(self): # with文に入る直前に実行
print(f"[start] {self.func_name}")
return self
def __exit__(self, exc_type, exc_val, exc_tb): # with文が終わった直後に実行
if self.result and self.args:
print(f"[finish] {self.result} {self.args[0]}!")
return False
name = "Alice"
with AroundStartFinish("say_hello") as context:
print("出力例")
print(f"Hello, {name}!")
context.result = "Hello"
context.args = (name,)
[start] say_hello
Hello, Alice!
[finish] Hello Alice!
この例からわかるwith文の特徴は、
- 処理の前後に、適当な処理を置ける
- 引数や戻り値(に相当するもの)は、
contextを経由すれば扱える
といったところでしょうか。
...おや。デコレータの時と同じようなものが出来上がりました。
文法は違えど、出来ることはほぼ同じのようです。
おまけ: ContextDecorator
デコレータとwith文が似た機能であることが分かりました。
そして、Pythonにはそんな2つを同時に実装する機能も用意されていました。
from contextlib import ContextDecorator
class AroundStartFinish(ContextDecorator):
def __enter__(self):
print("[start]")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("[finish]")
return False
# 使用例1: with文として使用
with AroundStartFinish():
print("Hello, Alice!")
# 使用例2: デコレータとして使用
@AroundStartFinish()
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice")
ContextDecoratorを使えば、デコレータとwith文両方で使えるクラスを作ることができます。上記のように前後に処理を挟む程度であれば、なかなか簡単に書けます。ただ、引数・戻り値を参照しようとなると__call__()を実装する必要があったり少し面倒になります。
おわり
おわり
Discussion