Pythonのwith
構文は、ファイルやソケットなどのリソースを使い終わった瞬間に自動で閉じるための仕組みです。
初心者のうちに身につけると、思わぬバグやリソース漏れを未然に防げます。
この記事では、基本から具体例、注意点までを丁寧に解説し、日々のコードで安全に活用できる知識を整理します。
Pythonのwith構文の基本
with構文とは
with構文は「コンテキストマネージャ」と呼ばれる仕組みを使い、ブロックの開始時に準備を行い、ブロック終了時に必ず後片付け(クローズなど)を実行する構文です。
ファイル、ソケット、圧縮ファイル、データベース接続など、多くの「有限な外部資源」に対して使われます。
内部的にはオブジェクトが__enter__
と__exit__
という2つのメソッドを実装しており、これがブロック開始と終了に対応しています。
ポイントとして、ブロックを抜けるタイミングは「正常終了」でも「例外発生」でも構いません。
どちらでも自動的に後片付けが行われます。
最小の例
最小限の使い方は、ファイルを開いてすぐ閉じる例で確認できます。
# 最小のwith構文の例: ファイルを開いて1行書き込み、ブロックを出たら自動で閉じます
with open("mini.txt", "w", encoding="utf-8") as f:
f.write("こんにちは、with構文!\n")
# ブロック外ではすでにクローズ済み
print("ブロック外でのf.closed:", f.closed)
ブロック外でのf.closed: True
ブロック終了で自動クローズ
ブロックを出た直後に自動でclose()
されるため、クローズ忘れが起こりません。
クローズ後に操作するとエラーになります。
# 自動クローズの動作確認
with open("auto_close.txt", "w", encoding="utf-8") as f:
f.write("自動クローズのテスト\n")
print("クローズ済みか?", f.closed)
# クローズ後に操作しようとすると例外(ValueError)が発生します
try:
f.write("もう書けないはず")
except ValueError as e:
print("エラー発生:", e)
クローズ済みか? True
エラー発生: I/O operation on closed file.
なぜ重要か
close忘れを防ぐ
クローズ忘れは、ファイルディスクリプタの枯渇、ファイルのロック解除忘れ、書き込みのフラッシュ漏れなど重大な不具合の原因になります。
特にWindowsでは開いたままのファイルを別プロセスから削除できないなどの実害が出やすいです。
with
は忘れようがない構造で、「開くスコープ」=「使うスコープ」をコードで明確にできます。
例外でも安全
ブロック内で例外が起きても、必ずクローズされます。
手動でclose()
するより、堅牢性が格段に上がります。
# 例外が発生しても確実にクローズされる
try:
with open("safe.txt", "w", encoding="utf-8") as f:
f.write("途中まで書いた後に例外を発生させる\n")
raise RuntimeError("テスト用の例外")
except RuntimeError as e:
print("例外を捕捉:", e)
# withブロックを出ているので、fはクローズ済み
print("クローズ済みか?", f.closed)
例外を捕捉: テスト用の例外
クローズ済みか? True
try/finallyとの違い
with
は、try/finally
で書くパターンを安全に短く表現したものです。
次の2つは本質的に同じ意味ですが、withの方が読みやすく、書き間違いが減ります。
# with版 (推奨)
with open("with_way.txt", "w", encoding="utf-8") as f:
f.write("withは簡潔で安全\n")
# try/finally版 (冗長でミスしやすい)
f = open("try_finally_way.txt", "w", encoding="utf-8")
try:
f.write("finallyで必ずcloseする必要がある\n")
finally:
f.close()
以下は比較表です。
観点 | with構文 | try/finally |
---|---|---|
記述量 | 少ない | 多い |
クローズ忘れ | 構文上ほぼ不可能 | finallyを書き忘れると発生 |
例外安全性 | 標準で確保 | finallyの実装に依存 |
可読性 | 高い | 低め |
複数リソース | カンマで簡潔/ExitStackで柔軟 | ネストが深くなりがち |
具体例
ファイル読み書き
ファイル操作はwith
の最重要ユースケースです。
書き込み、読み込み、追記を順に行います。
# ファイルへ書き込み
with open("data.txt", "w", encoding="utf-8") as w:
for i in range(3):
w.write(f"line-{i}\n")
# ファイルを読み込み
with open("data.txt", "r", encoding="utf-8") as r:
lines = r.readlines()
print("行数:", len(lines))
print("先頭行:", lines[0].strip())
# 追記モードで開く
with open("data.txt", "a", encoding="utf-8") as a:
a.write("追加の1行\n")
# 結果を確認
with open("data.txt", "r", encoding="utf-8") as r:
last = r.readlines()[-1].strip()
print("末尾行:", last)
行数: 3
先頭行: line-0
末尾行: 追加の1行
ここでは説明を簡単にするためencoding="utf-8"
を指定しています。
エンコーディングの詳細は別記事で扱います。
ソケット通信
ソケットもwith
で安全に扱えます。
HTTPの最小リクエストを送って、最初の行だけ読みます。
# ソケットをwithで安全に扱う例
import socket
host = "example.com"
port = 80
# 最小のHTTP/1.1リクエスト
request = (
f"GET / HTTP/1.1\r\n"
f"Host: {host}\r\n"
"Connection: close\r\n"
"User-Agent: with-demo/1.0\r\n"
"\r\n"
)
# create_connectionは接続に成功するとソケットを返す
with socket.create_connection((host, port), timeout=5) as sock:
# 送信
sock.sendall(request.encode("ascii"))
# 受信(最初の1KBだけ取り出す)
data = sock.recv(1024)
# レスポンスの先頭行を確認
first_line = data.splitlines()[0].decode("iso-8859-1", errors="replace")
print("HTTP応答の先頭行:", first_line)
出力例(環境により異なる):
HTTP応答の先頭行: HTTP/1.1 200 OK
ネットワークエラーが途中で起きても、ソケットはブロック終了時に確実に閉じられます。
複数リソースを同時に開く
2つ以上のリソースを同時に扱う場合、1行のwith
でカンマ区切りにできます。
# data.txtをコピーしてcopy.txtを作る例
with open("data.txt", "rb") as src, open("copy.txt", "wb") as dst:
bytes_copied = dst.write(src.read())
print("コピーしたバイト数:", bytes_copied)
コピーしたバイト数: 37
多数のリソースを動的に開く場合はcontextlib.ExitStack
が便利です。
# ExitStackで動的な数のファイルを安全に開く例
from contextlib import ExitStack
filenames = ["data.txt", "copy.txt"] # 実在するファイルの想定
with ExitStack() as stack:
# enter_contextで開いたリソースは、with終了時に逆順で自動クローズ
files = [stack.enter_context(open(name, "r", encoding="utf-8")) for name in filenames]
total_lines = sum(len(f.readlines()) for f in files)
print("合計行数:", total_lines)
合計行数: 5
使い方のコツと注意点
ブロック外で使わない
withブロックの外で、そのブロック内で開いたリソースを使うのはNGです。
クローズ済みのため、操作するとエラーになります。
# 悪い例: 閉じたファイルオブジェクトを返してしまう
def open_and_return_bad():
with open("temp_bad.txt", "w", encoding="utf-8") as f:
f.write("NGパターン")
return f # withを抜けるため、呼び出し側では既にクローズ済み
bad = open_and_return_bad()
print("返ってきたfはクローズ済み:", bad.closed)
try:
bad.write("追記できないはず")
except ValueError as e:
print("エラー:", e)
# 良い例: パスやデータを返す
def write_and_get_path():
path = "temp_good.txt"
with open(path, "w", encoding="utf-8") as f:
f.write("OKパターン")
return path # withの外に出る時点で安全にクローズ済み
with open(write_and_get_path(), "r", encoding="utf-8") as f:
print("内容:", f.read())
返ってきたfはクローズ済み: True
エラー: I/O operation on closed file.
内容: OKパターン
関数の戻り値としては「ファイルオブジェクトそのもの」ではなく、「読み取ったデータ」や「ファイルパス」を返す設計が安全です。
早めにクローズしたいときのブロック設計
「I/Oが必要な最小範囲だけ」をwithで囲むのがコツです。
こうすると、重い計算処理の間にファイルやソケットを握り続けることがありません。
# NG: 不要にブロックが広い
with open("wide_scope.txt", "w", encoding="utf-8") as f:
f.write("I/Oはここだけ")
# ここから先は重い計算なのに、ファイルが開いたままになる
total = sum(i * i for i in range(5_0000))
# OK: I/O部分だけを狭く囲む
with open("narrow_scope.txt", "w", encoding="utf-8") as f:
f.write("I/Oはここまで")
# 以降はリソースを持たない計算
total = sum(i * i for i in range(5_0000))
どうしてもブロック中に明示的に早く閉じたい場合はf.close()
を呼んでも構いません(ブロック終了時に二重に閉じようとしても安全です)。
ただし、基本はブロックを適切に分割して自然にクローズさせる設計が望ましいです。
with対応オブジェクトの見つけ方
- ドキュメントを確認する: 「このオブジェクトはwith文をサポート」と書かれているか、
context manager
対応の記述があればOKです。標準ライブラリではopen()
の返り値、socket.socket
、zipfile.ZipFile
、gzip.open
、tarfile.TarFile
、sqlite3.connect
の返り値などが該当します。 - その場で確認する:
hasattr(obj, "__enter__") and hasattr(obj, "__exit__")
なら、with
に対応しています。 close()
だけ持つ非対応オブジェクトはcontextlib.closing()
でラップできます。
# contextlib.closingで「close()だけ持つオブジェクト」をwith対応にする
from contextlib import closing
class LegacyResource:
def use(self):
print("使っています...")
def close(self):
print("close()が呼ばれました")
# closingで包むと、withブロックの終了時に必ずclose()が呼ばれる
with closing(LegacyResource()) as res:
res.use()
使っています...
close()が呼ばれました
with
は例外を握り潰すわけではありません。
通常、例外は発生し、__exit__
での後片付け後に外へ伝播します(一部の特殊なコンテキストは例外を抑制しますが、ファイルなどはしません)。
まとめ
with構文は「リソースの寿命をブロックに縛り付ける」ことで、close忘れや例外時の後片付け漏れを根本から防ぐ、Pythonの重要な安全装置です。
本記事では、基本の仕組み、例外時の挙動、try/finally
との違い、ファイルやソケットでの実例、複数リソース、設計のコツまでを解説しました。
これらを踏まえ、次の点を心がけると実運用でも安心です。
- 「I/Oは最小範囲で」を徹底し、不要にリソースを握り続けない設計にすること。
- withブロック外でリソースを使わないこと。データやパスを返し、必要なときに再度開くこと。
- with対応の有無はドキュメントや
__enter__/__exit__
の存在で確認し、必要ならcontextlib.closing
やExitStack
を活用すること。
毎回with
を使うのは小さな手間に見えますが、安全性・保守性・可読性の面で大きなリターンがあります。
今日からのコードで積極的に使っていきましょう。