閉じる

Pythonのwith構文とは?ファイルやソケットを安全に閉じる理由

Pythonのwith構文は、ファイルやソケットなどのリソースを使い終わった瞬間に自動で閉じるための仕組みです。

初心者のうちに身につけると、思わぬバグやリソース漏れを未然に防げます。

この記事では、基本から具体例、注意点までを丁寧に解説し、日々のコードで安全に活用できる知識を整理します。

Pythonのwith構文の基本

with構文とは

with構文は「コンテキストマネージャ」と呼ばれる仕組みを使い、ブロックの開始時に準備を行い、ブロック終了時に必ず後片付け(クローズなど)を実行する構文です。

ファイル、ソケット、圧縮ファイル、データベース接続など、多くの「有限な外部資源」に対して使われます。

内部的にはオブジェクトが__enter____exit__という2つのメソッドを実装しており、これがブロック開始と終了に対応しています。

ポイントとして、ブロックを抜けるタイミングは「正常終了」でも「例外発生」でも構いません

どちらでも自動的に後片付けが行われます。

最小の例

最小限の使い方は、ファイルを開いてすぐ閉じる例で確認できます。

Python
# 最小の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()されるため、クローズ忘れが起こりません。

クローズ後に操作するとエラーになります。

Python
# 自動クローズの動作確認
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()するより、堅牢性が格段に上がります。

Python
# 例外が発生しても確実にクローズされる
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の方が読みやすく、書き間違いが減ります

Python
# with版 (推奨)
with open("with_way.txt", "w", encoding="utf-8") as f:
    f.write("withは簡潔で安全\n")
Python
# 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の最重要ユースケースです。

書き込み、読み込み、追記を順に行います。

Python
# ファイルへ書き込み
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の最小リクエストを送って、最初の行だけ読みます。

Python
# ソケットを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でカンマ区切りにできます。

Python
# 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が便利です。

Python
# 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です。

クローズ済みのため、操作するとエラーになります。

Python
# 悪い例: 閉じたファイルオブジェクトを返してしまう
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で囲むのがコツです。

こうすると、重い計算処理の間にファイルやソケットを握り続けることがありません。

Python
# 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.socketzipfile.ZipFilegzip.opentarfile.TarFilesqlite3.connectの返り値などが該当します。
  • その場で確認する: hasattr(obj, "__enter__") and hasattr(obj, "__exit__")なら、withに対応しています。
  • close()だけ持つ非対応オブジェクトはcontextlib.closing()でラップできます。
Python
# 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.closingExitStackを活用すること。

毎回withを使うのは小さな手間に見えますが、安全性・保守性・可読性の面で大きなリターンがあります。

今日からのコードで積極的に使っていきましょう。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!