28. with 文とコンテキストマネージャ
第27章では、ファイルを扱うためにすでに with 文を使いました。これにより、あとでファイルを明示的に閉じることを心配せずに、データの読み書きができました。ただしその時点では、焦点は with の使い方であって、それが本当は何を意味するのかではありませんでした。
この章では一度立ち止まり、より大きな全体像を見ていきます。コンテキストマネージャ(context manager)とは何か、なぜリソースを手動で管理すると危険になり得るのか、そして with 文が Python でリソースを扱うための安全で信頼できるパターンをどのように提供するのかを学びます。さらに、with はファイルだけに限定されないこと、そして内部でどのように動いているのかを概念的に理解できるようになります。
28.1) 概念としてのコンテキストマネージャ
コンテキストマネージャ(context manager) とは、コード内で特定のコンテキストに入るときと出るときに何が起きるべきかを定義するオブジェクトです。部屋に出入りすることをイメージしてください。入ったら照明をつけ、出るときに消す——中にいる間に何が起きても関係なく、です。
28.1.1) リソース管理の問題
多くのプログラミング作業では、リソースを取得し、それを使い、そして解放します。
# ファイルを開くとリソース(ファイルハンドル)を取得します
file = open("data.txt", "r")
content = file.read()
# ファイルを使う...
file.close() # リソースを解放するこのパターンは頻繁に登場します:
- ファイルのオープンとクローズ
- 並行プログラミングにおけるロックの取得と解放
- データベース接続のオープンとクローズ
- メモリバッファの割り当てと解放
課題は、何かがうまくいかなかった場合でも、リソースが必ず解放されることを保証することです。
28.1.2) 何がオブジェクトをコンテキストマネージャにするのか
コンテキストマネージャとは、次の2つの特殊メソッドを実装している任意のオブジェクトです。
__enter__(): コンテキストに入るとき(withブロックの開始時)に呼ばれます__exit__(): コンテキストから出るとき(withブロックの終了時。エラーが起きても)に呼ばれます
コンテキストマネージャを使うために、これらのメソッドを自分で実装する必要はありません。ファイルオブジェクトのような Python の組み込み型はすでにこれらを持っています。この概念を理解しておくと、コンテキストマネージャを扱っている場面を見分けやすくなります。
# ファイルオブジェクトはコンテキストマネージャです
# __enter__ と __exit__ メソッドを持っています
file = open("example.txt", "r")
print(hasattr(file, "__enter__")) # Output: True
print(hasattr(file, "__exit__")) # Output: True
file.close()28.1.3) 基本パターン: セットアップ、使用、後片付け
コンテキストマネージャは3段階のパターンに従います:
セットアップフェーズ: リソースを取得します(例: ファイルを開く、データベースに接続する、ロックを取得する)
使用フェーズ: リソースを使って作業します(例: ファイルの読み書き、データベースのクエリ、共有データへのアクセス)
後片付けフェーズ: リソースを解放します(例: ファイルを閉じる、データベース接続を切る、ロックを解放する)
重要な洞察: 後片付けフェーズは常に実行されます。使用フェーズの途中で何が起きても関係ありません。
28.2) 手動のリソース管理が危険な理由
with 文を学ぶ前に、手動のリソース管理がなぜ失敗し得て問題を引き起こすのかを理解しましょう。
28.2.1) クローズし忘れ
最もよくあるミスは、単純にリソースを閉じ忘れることです。
# 設定ファイルを読み込む
config_file = open("config.txt", "r")
settings = config_file.read()
# あっ!ファイルを閉じるのを忘れた
# ファイルハンドルが開いたままになるPython は最終的にプログラム終了時にファイルを閉じますが、ファイルを開いたままにしておくと問題が起きる可能性があります:
- リソース枯渇: OS は開けるファイル数に上限があります
- ファイルロック: 他のプログラムがファイルにアクセスできなくなることがあります
- データ損失: バッファされた書き込みがディスクにフラッシュされない可能性があります
28.2.2) エラーによって後片付けが実行されない
リソースを閉じることを覚えていても、エラーによって後片付けコードが実行されないことがあります。
# ファイルを処理しようとする
data_file = open("data.txt", "r")
content = data_file.read()
result = process_data(content) # ここでエラーが起きたら?
data_file.close() # process_data() が失敗するとこの行は実行されません!process_data() が例外を送出すると、プログラムは直接エラーハンドリングに移り、close() 呼び出しをスキップします。ファイルは無期限に開いたままになります。
28.2.3) 複数の終了地点
return 文が複数ある関数では、後片付けはさらに難しくなります。
def read_first_valid_line(filename):
file = open(filename, "r")
for line in file:
line = line.strip()
if line and not line.startswith("#"):
# 有効な行を見つけた - しかしファイルはまだ開いたまま!
return line
file.close() # 有効な行が見つからなかった場合にだけ到達する
return Noneこの関数は有効な行を見つけると早期 return し、ファイルを開いたままにします。すべての return 文の前に file.close() を入れる必要があり、忘れやすく保守もしづらいです。
28.2.4) 複雑なエラーハンドリング
後片付けを確実にするために、try-except-finally を使おうとするかもしれません。
# エラーを適切に扱おうとする
file = None
try:
file = open("data.txt", "r")
content = file.read()
result = process_data(content)
except FileNotFoundError:
print("File not found")
except ValueError:
print("Invalid data format")
finally:
if file is not None:
file.close()これは動きますが、冗長でミスが入り込みやすいです。次のことを行う必要があります:
- try ブロックの前に変数を初期化する
- リソースの取得に成功したかどうかを、閉じる前に確認する
- finally ブロックを入れることを忘れない
- すべてのリソースに対してこのパターンを繰り返す
28.2.5) 実世界での影響
これらの問題は理論だけの話ではありません。何千ものファイルを処理するプログラムを考えてみましょう。
# WARNING: Resource leak - for demonstration only
# PROBLEM: Files are never closed
def process_many_files(filenames):
results = []
for filename in filenames:
file = open(filename, "r") # ファイルを開く
data = file.read()
results.append(analyze(data))
# MISTAKE: ファイルを一度も閉じない
return results
# 1000個のファイルを処理した後、1000個のファイルハンドルが開いたままになります!
# やがて、OS はこれ以上ファイルを開くことを拒否します出力(多くの反復の後):
OSError: [Errno 24] Too many open files: 'file_1001.txt'システムのファイルハンドル上限を使い果たしたため、プログラムがクラッシュします。これはリソースリーク(resource leak) です。リソースを取得しているのに、解放していません。
28.3) ファイル以外での with の使用
with 文は、ファイルだけでなく任意のコンテキストマネージャで動作します。ここまでに挙げた問題をどう解決するのかを見ていき、さまざまなコンテキストで使われる例を確認しましょう。
28.3.1) with 文の基本構文
with 文はシンプルな構造です。
with expression as variable:
# リソースを使うコードブロック
# with 文の下にインデントする
# ここでリソースが自動的に解放されるexpression はコンテキストマネージャオブジェクトとして評価されなければなりません。as variable 部分は任意ですが、通常は付けます。リソースを参照するための名前が得られるからです。
28.3.2) ファイル操作での with の使用
with 文がファイル処理をどのように変えるかを見てください。
# 手動の方法(危険)
file = open("data.txt", "r")
content = file.read()
file.close()
# with 文の方法(安全)
with open("data.txt", "r") as file:
content = file.read()
# ここでファイルが自動的に閉じられる(エラーが起きても)with ブロックが終わるとき、コードが通常どおり完了した場合でも、例外が送出された場合でも、ファイルは必ず閉じられます。
28.3.3) 複数のコンテキストマネージャ
1つの with 文で複数のリソースを管理できます。
# あるファイルから読み込み、別のファイルに書き込む
with open("input.txt", "r") as input_file, open("output.txt", "w") as output_file:
for line in input_file:
processed = line.upper()
output_file.write(processed)
# ここで両方のファイルが自動的に閉じられるこれは with 文をネストするのと同等ですが、より簡潔です。
# ネストした with 文(同等だがより冗長)
with open("input.txt", "r") as input_file:
with open("output.txt", "w") as output_file:
for line in input_file:
processed = line.upper()
output_file.write(processed)どちらの方法でも、処理中にエラーが起きても、両方のファイルが正しく閉じられることが保証されます。
28.3.4) 圧縮ファイルの扱い
Python の gzip モジュールは、圧縮ファイルを読み書きするためのコンテキストマネージャを提供します。
import gzip
# 圧縮データを書き込む
with gzip.open("data.txt.gz", "wt") as compressed_file:
compressed_file.write("This text will be compressed\n")
compressed_file.write("Saving space on disk\n")
# ファイルが自動的に閉じられ、圧縮処理が完了する
# 圧縮データを読み込む
with gzip.open("data.txt.gz", "rt") as compressed_file:
content = compressed_file.read()
print(content)Output:
This text will be compressed
Saving space on diskwith 文によって、圧縮ファイルが適切に完了処理されることが保証されます。これは圧縮にとって重要で、不完全な圧縮は壊れたファイルにつながる可能性があります。
28.3.5) 一時的なディレクトリ変更
現在の作業ディレクトリを一時的に変更する必要がある場合、手動管理は危険になり得ます:
import os
# 現在のディレクトリ
print(f"Starting in: {os.getcwd()}")
# 手動でディレクトリを変更する(危険)
original_dir = os.getcwd()
os.chdir("/tmp")
print(f"Now in: {os.getcwd()}")
process_files() # ここでエラーが起きると original_dir に戻れないかもしれない
os.chdir(original_dir)process_files() が例外を送出すると、プログラムは元のディレクトリに戻れず、その後のコードで予期しない挙動を引き起こす可能性があります。
Python 3.11 では、元のディレクトリに戻ることを保証するコンテキストマネージャ contextlib.chdir() が導入されました:
import os
from contextlib import chdir
print(f"Starting in: {os.getcwd()}")
# コンテキストマネージャを使う(安全)
with chdir("/tmp"):
print(f"Temporarily in: {os.getcwd()}")
process_files() # これがエラーを送出しても、元のディレクトリに戻る
print(f"Back in: {os.getcwd()}")
# 自動的に元のディレクトリに戻ったディレクトリ変更は with ブロックが終わると自動的に元に戻ります。コードが通常どおり完了した場合でも、例外が送出された場合でも同様です。
28.3.6) 並行プログラミングのためのスレッドロック
並行プログラミング(上級トピックで扱います)では、ロックはコンテキストマネージャです。
# 概念例(threading は上級トピックで学びます)
import threading
lock = threading.Lock()
# 手動のロック管理(危険)
lock.acquire()
# クリティカルセクション - ここでエラーが起きたら?
lock.release() # 実行されないかもしれない
# with 文(安全)
with lock:
# クリティカルセクション
# エラーが起きてもロックが自動的に解放される
pass28.4) with 文の内部動作(概念のみ)
with 文が内部でどう動くかを理解すると、その強力さを実感でき、コンテキストマネージャを扱っているときに気づきやすくなります。このセクションは概念的な概要です。これらの詳細を自分で実装する必要はありません。
28.4.1) 2つの特殊メソッド
すべてのコンテキストマネージャは、Python が自動的に呼び出す2つの特殊メソッドを実装しています。
__enter__(self): with ブロックが始まるときに呼ばれます
- セットアップ操作を行います(ファイルを開く、ロックを取得する、など)
asの後の変数に代入されるリソースオブジェクトを返しますas句がない場合、戻り値は無視されます
__exit__(self, exc_type, exc_value, traceback): with ブロックが終わるときに呼ばれます
- 後片付け操作を行います(ファイルを閉じる、ロックを解放する、など)
- 発生した例外の情報を受け取ります
- 例外が送出された場合でも、常に呼ばれます
Trueを返すことで例外を抑制できます(行われることはまれです)
28.4.2) Python が with 文を実行する方法
Python が with 文を実行するときに何が起きるかを追ってみましょう。
with open("data.txt", "r") as file:
content = file.read()
print(content)ステップごとの実行は次のとおりです。
ステップ 1: Python は open("data.txt", "r") を評価し、ファイルオブジェクトを作成します
ステップ 2: Python はファイルオブジェクトの __enter__() メソッドを呼び出します
ステップ 3: __enter__() はファイルオブジェクト自身を返し、それが file に代入されます
ステップ 4: Python はインデントされたコードブロックを実行します
ステップ 5: ブロックが終わると(通常終了でも例外でも)、Python は __exit__() を呼び出します
ステップ 6: __exit__() がファイルを閉じ、後片付けを行います
ステップ 7: 例外が発生していた場合、Python は後片付けの後でそれを再送出します
28.4.3) コンテキストマネージャにおける例外処理
with ブロック内で例外が発生すると、Python はその情報を __exit__() に渡します。
# エラーが起きたときに何が起こるか
try:
with open("data.txt", "r") as file:
content = file.read()
result = int(content) # ValueError を送出する可能性がある
print(result)
except ValueError as e:
print(f"Invalid data: {e}")
# except ブロックが実行される前にファイルは閉じられるValueError が発生したときの実行フロー:
重要な点: 例外が伝播する前に __exit__() が呼ばれます。これにより、エラーが起きたときでも後片付けが確実に実行されます。
28.4.4) シンプルなメンタルモデル
with 文を「保証」として捉えてください。
with resource_manager as resource:
# リソースを使う
pass
# Python が後片付けが行われたことを保証するブロック内で何が起きても——通常終了、return 文、例外、あるいはシステムエラーでさえ——Python は __exit__() を呼んで後片付けを行います。この保証こそが with を強力にしており、リソースを扱うときに with を使うべき理由です。
この章の重要ポイント:
- コンテキストマネージャ(context manager) は、リソースに対するセットアップと後片付け操作を定義します
- 手動のリソース管理は、後片付けのし忘れ、エラー、複数の終了地点のために危険です
with文は、エラーが起きても後片付けが行われることを保証します- ファイルや後片付けが必要な他のあらゆるリソースには
withを使いましょう - 複数のリソースは、1つの
with文で管理できます - 内部的には、
withは__enter__()と__exit__()メソッドを自動的に呼び出します __exit__()は必ず実行されるため、リソースが適切に解放されます
with 文は、エラーが入り込みやすい手動作業だったリソース管理を、自動で信頼できる後片付けへと変えてくれます。ファイル、データベース接続、ロック、または適切な後片付けが必要なその他のリソースを扱うときは、いつでも使ってください。コードはより安全で、よりクリーンで、よりプロフェッショナルになります。