Q1@contextmanagerをジェネレータ関数に付けるとき、ちょうど 1 回 yield する必要があるのはなぜですか?
contextlib — リソース管理と with の自作
@contextmanagerとyieldでwithをクラスなしに自作する書き方、try/finallyでの確実な後処理、suppressでの特定例外の握りつぶしを実例で学べます。
with文を自作するときに使うcontextlibを整理します。@contextmanagerによるシンプルな書き方と、suppressによる特定例外の握りつぶしの 2 つを順に見ていきます。
@contextmanager — with を簡単に自作する
Python のwith文は、ファイルopen / DB 接続 / ロック取得など「使い終わったら必ず後始末する」処理を安全に書ける構文です。自分でwith対応のクラスを作るには__enter__ / __exit__を定義する必要があり、ボイラープレートが多くなります。@contextmanagerデコレータは、ジェネレータ(yieldを含む関数)を 1 つ書くだけでwith 対応の context manager を作れる簡略記法で、yieldの前後で「処理開始」と「後始末」を表現します。
yieldの前が __enter__ 相当(前処理)、yield 後が __exit__ 相当(後処理)になる。yieldでwith の as に渡す値を返せる。from contextlib import contextmanager
# --- 参考: クラスで with 対応を作る場合 -------------------
# class Section:
# def __init__(self, name):
# self.name = name
# def __enter__(self):
# print(f"--- {self.name} 開始 ---")
# return self # with の as に渡す値
# def __exit__(self, exc_type, exc, tb):
# print(f"--- {self.name} 終了 ---")
# ----------------------------------------------------------
# @contextmanager 版: ジェネレータ関数 1 つで同じことが書ける
@contextmanager
def section(name):
print(f"--- {name} 開始 ---")
yield # ここで with ブロックの中身が動く
print(f"--- {name} 終了 ---")
# 使う側
with section("集計"):
total = sum(range(100))
print("合計:", total)
# 出力:
# --- 集計 開始 ---
# 合計: 4950
# --- 集計 終了 ---
yieldの前後がそのまま前処理・後処理になり、コード量が大幅に減る。例外でも後処理を確実に走らせるなら try/finally を入れる
yieldの後の処理は、with ブロック内で例外が起きると実行されないことがあります。ファイル close / ロック解放など必ず動かしたい後処理は、ジェネレータ関数の中でtry: yield finally: cleanup()の形で囲んでおくのが安全です。
suppress — 特定例外を握りつぶす
contextlib.suppress(例外型, ...)は「指定した例外を握りつぶして、後続の処理を続ける」ための context manager です。try: ... except KeyError: passのような「特定の例外だけ無視して続行」パターンを 1 行で書けます。複数の例外型をまとめて指定でき、指定外の例外は通常通り伝播します。
from contextlib import suppress
import os
# 「ファイルが無くてもデフォルト値で動かす」例
config = {"theme": "light", "font_size": 14}
with suppress(FileNotFoundError):
with open("user_config.txt") as f:
config["theme"] = f.read().strip()
# user_config.txt が無くても止まらず、config はデフォルトのまま続行
print(config)
# → {'theme': 'light', 'font_size': 14}
# 「すでに削除済みなら無視して進む」例
with suppress(FileNotFoundError):
os.remove("old.tmp")
print("削除完了 (元から無くても OK)")
理解度チェック
まずは1問ずつ答えてみましょう。
Q2with suppress(KeyError):のブロック内でKeyError が発生したときどうなりますか?