【2025新版・MFi認証・自動接続】 iPhone hdmi変換ケーブル ライトニング 設定不要・ APP不要・ 給電不要 ・1080PプルHD TV大画面 音声同期出力 ライトニング hdmi iphone tv 変換ケーブル テレビに映す 遅延なし簡単接続 iPhone/iPad などに対応 日本語取説付き(iOS13 - iOS18対応)
¥1,699 (2025年4月26日 13:09 GMT +09:00 時点 - 詳細はこちら価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。本商品の購入においては、購入の時点で当該の Amazon サイトに表示されている価格および発送可能時期の情報が適用されます。)【Amazon.co.jp限定】 バッファロー WiFi 無線LAN 中継機 Wi-Fi 5 11ac 866 + 300 Mbps ハイパワー コンセント直挿し コンパクトモデル 簡易パッケージ 日本メーカー 【 iPhone 16 / 15 / 14 / 13 / Nintendo Switch / PS5 動作確認済み 】 エコパッケージ WEX-1166DHPL/N
¥2,479 (2025年4月26日 13:07 GMT +09:00 時点 - 詳細はこちら価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。本商品の購入においては、購入の時点で当該の Amazon サイトに表示されている価格および発送可能時期の情報が適用されます。)
本記事では、Python の with
文を起点に、多言語の with
に相当する概念を横断的に比較し、
リソース管理という “地味だけど重要” なテーマ を一気に理解できるように整理しました。
「〇〇をしたら必ず △△ する」 をコードで保証する ── リソース管理とは?
A. 「そのタスク終わったら Slack で私に連絡してくださいね!」
B. 「はい、わかりました!」
(数日後…)
A. 「あれ、Slack で連絡来てないな。」
B. 「違う仕事していたら、Slack で連絡するの忘れてました!」
そんな経験、ありませんか?
「〇〇をしたら絶対にこれをやる」ということを強制する。
それを実現するのが、Python の with
文です。
〇〇をしたら絶対にこれをやるというのは、
例えばファイルを open したら必ず close をするなどのリソースに対して行うことは特に多く、
リソース管理のために with
文を使用することが多いです。
「最後にこれをするための約束をする」、そのための技法を一緒に整理しましょう!
TL;DR
-
with
文はtry…finally
の糖衣構文 - Python が 2006 年に with 構文を導入し、最近では TypeScript も 2023 年に using 構文を採用
- 多くの GC 言語で RAII 的安全性 を得るには明示的にスコープを作るしかない。だからこそ Python では
with
文という構文 -
with
文は__enter__
/__exit__
やcontextlib.contextmanager
で自作できる -
with
を使った簡単な活用事例 - SIGKILL などの OS に直接作用するイベントは
finally
が呼び出されないケースがある。特にデプロイなどではこのような事にならないように注意
2 万文字と長いブログ記事になってしまったのですが、
リソース管理技術について、
丁寧に説明してみましたので困った時などに読んでいただければと思います。
第 1 章. Python のリソース管理の核心:「with」構文を多言語比較で理解する
ファイルというものは開いたら必ず、 close をするべきです。
この例を元に、まずはリソース解放の歴史を、他の言語と比較しながら見ていきましょう。
必ず close が呼ばれるようにするとは?
file = open('example.txt', 'r')
content: str = file.read()
...
file.close()
このコードは … のところなどで例外が発生すると、file.close()
が呼び出さません。
open をしたら必ず close が呼び出されるようにするという行為を保証したいのですが、
これでは保証できていないことになるのです。
ここでは、まずファイルの open というのを例にしましたが、
- スレッドの Lock を取得したら解放をする
- User を作成したあとに削除する
- 一時的なファイルを作成してそのファイルを削除する
など、挙げればきりが無いほどに、
リソースを作ったあとに「後処理が行われることを保証する」ことを契約したいことがあります。
これを、このブログではリソース管理と呼んでおり、
最も簡単な例としてファイルの open/close の例を示します。
Python におけるリソース管理 : with 文の使い方
Python ではこのように記述します。
with open('example.txt', 'r') as file:
content: str = file.read()
...
このコードは、example.txt
というファイルを開いて内容を読み取るものです。with
文を使用することで、リソースの取得と解放を自動的に行うことができ、途中でエラーが発生してもリソース解放を保証します。
このコードと同等な内容は次のようなtry...finally
を使用してリソース管理を行う方法になります。
file = open('example.txt', 'r')
try:
content = file.read()
...
finally:
file.close()
これは等価なコードであり、どちらを選ぶべきかという問題があります。
try…finally から with への変革歴史
元々の Python では、リソース管理を行うためにtry...finally
を使った記述が行われていました。
つまり、
file = open('example.txt', 'r')
try:
content = file.read()
...
finally:
file.close()
という形のコードです。しかしながら、その後 2006 年にリリースされた Python 2.5 においてwith
文が導入されました。
つまり、
with open('example.txt', 'r') as file:
content: str = file.read()
...
という形のコードになります。この機能は PEP 343 によって提案され、リソース管理を簡潔かつ安全に行うための構文として設計されました。
try...finally
型の大きなデメリットは何かというと、
対応するリソース解放処理のメソッドが何かをユーザーが調べて実装する必要があるということ です。
例えば、open に対しては close, create に対してはもしかすると delete もしくは release という名前のメソッドかもしれません。
この処理の呼び出しを間違えると、リソース解放漏れを引き起こす可能性があります。
一方で、 with
は、その終了処理についても包含してくれているので、その呼び出し側が対応を忘れることができます。
Go における open/close の例 と try…finally の比較
Go では、defer
を使用してリソース解放を記述し、解放漏れを防ぎます。
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Open("example.txt")
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
content := make([]byte, 100)
file.Read(content)
fmt.Println(string(content))
}
Go のこの構文は Python のtry...finally
に似ています。defer
節が finally
節に相当し、この箇所にリソース解放処理を記述します。
また、Python の例外のような panic と呼ばれるときにでも実行されることを保証しています。
defer を登録しておくと、関数終了時に逆順 (LIFO 順) で実行されます。
Python でこのような LIFO 順で実行する挙動は、後述する contextlib.ExitStack
で登場します。
Python の try...finally
型で説明した通り、
open を呼び出した側が忘れずに、defer
に対応するリソース処理を書く必要があります。
つまり、open に対応するコードは close であるという知識をもって書き、呼び出し側が間違えずに実装をする必要があるところが、
この記法の特徴であります。
個人的には、Go 言語に with
文のような仕様があれば、対応する処理を忘れることがなくなったり、不適切な実装をする間違いが減るので、
このような記法があると良いなと思っています。
しかしながら、このシンプルな設計思想がとても Go らしさでもあります!
TypeScript における open/close の例 と using への進化
TypeScript では、過去ではtry-finally
を使用してリソース解放を行っていました。
import * as fs from "fs";
fileDescriptor = fs.openSync("example.txt", "r");
try {
const buffer = Buffer.alloc(100);
fs.readSync(fileDescriptor, buffer, 0, 100, 0);
console.log(buffer.toString());
} finally {
fs.closeSync(fileDescriptor);
}
しかしながら、2023 年に TypeScript 5.2 において using
構文が導入されました。
この構文は、Python の with 文のようにリソース管理をより簡潔に記述できるように設計されています。
await using file = await fs.open("example.txt", "r");
const buffer = Buffer.alloc(100);
await file.read(buffer, 0, 100, 0);
console.log(buffer.toString());
using を使用することで、リソースの取得と解放を自動的に行うことができ、途中でエラーが発生してもリソース解放を保証します。
このように、歴史としては Python が導入した try-finally からwith
のように try-finally からusing
へと進化してきたことがわかります。
ちなみに補足すると、2024/04 現在、TypeScript の using は、
仕様(ECMAScript Stage 3 draft)ベースで TypeScript 5.2 に先行実装されてる段階です。
C++/Rust の RAII との比較 : RAII の本質 = 言語機能 + 破棄タイミングがコンパイルで決まる
今まで紹介した言語とは違い、GC を採用していない C++や Rust では、RAII (Resource Acquisition Is Initialization) という考え方が採用されています
(なお、C++や Rust のスマートポインタは、RAII を実現する代表的な仕組みですが、本記事では省略しています。)
オブジェクトを生成する瞬間にリソースを獲得し、スコープを抜けた瞬間に必ず破棄する
「コンパイラがデストラクタ呼び出しを強制的に挿入する」ため、開発者が後処理を忘れることは原理的にありません。
たとえ例外が発生しても、安全に確実にリソース解放が行われます。
例えば C++や Rust では次のように書きます。
#include
void read_file() {
std::ifstream file("example.txt");
...
}
use std::fs::File;
fn read_file() -> std::io::Result()> {
let file = File::open("example.txt")?;
...
Ok(())
}
Python や先ほどの TypeScript は、with
やusing
を使用してリソース管理を行うことはできますが、
RAII のようにコンパイラが強制的にリソース解放を行うことはできません。
Python や TypeScript、Go などの言語は GC (ガーベジコレクション) を採用しているため、
RAII のようにコンパイラの機能によって、強制的にリソース解放を行うことはできません。
なぜなら、GC 言語においては、他からもリソース参照されている可能性が否定できず、
スコープを抜けたからといって参照カウントが 0 になっているとは限らず破棄できないからです
(参照カウント方式の場合を例にしていますが別の実装方式でも似た問題が発生します)。
どういうことかというと、例えば次ようなコードを考えてみてください。
def create_file():
file = open("example.txt", "r")
return file
def main():
f = create_file()
content = f.read()
print(content)
main()
file というオブジェクトは create_file
のスコープが終了した後も生きているため、
関数スコープが終了したからと言って、close
をすることを保証することはできないのです。
そのため、Python や TypeScript などの GC 系の言語では、with
やusing
,defer
などを使用して「明示的に」
コードでリソースはこれ以上使わないという意図を表現し、リソース解放処理を行う必要があります。
項目 | C++/Rust (RAII) | Python などの GC 言語 |
---|---|---|
リソース破棄タイミング | スコープ終了時(コンパイラ保証) |
with などのキーワードでブロック終了時(コード挿入) |
オブジェクト破棄と一致? | はい(必ず破棄) | いいえ(コードで明示) |
この表から、Python の with
文は、C++/Rust の RAII と異なり、明示的にスコープを定義してリソース管理を行うことがわかります。
__del__
によるリソース解放は可能か?
Python にも一応、「オブジェクトが破棄されるときに実行される」特殊メソッドとして__del__
があります。
しかしながら、これを利用してリソース解放を行うことは 推奨されていません。
この __del__
は GC がオブジェクトを破棄することが決まったタイミングで呼び出されるときに呼ばれます。
例えば、次のようなコードを考えてみましょう。
class FileManager:
def __init__(self, path: Path) -> None:
self._f = open(path, "r")
def read(self) -> str:
return self._f.read()
def __del__(self) -> None:
print("Closing file...")
self._f.close()
def main() -> None:
file = FileManager(Path("example.txt"))
content = file.read()
main() 関数が終了した後、file
オブジェクトはガーベジコレクションによって破棄されるため、__del__
メソッドが呼び出され、ファイルが閉じられます。
しかし次のような問題があります。
- GC のタイミングに依存するため、破棄される時刻が予測できない
- コードに循環参照コードが存在すると、参照カウントが 0 にならないために
__del__
は呼ばれないことがある
Python における __del__
によるリソース解放処理は 「最後の非常手段」であって、通常のリソース管理には使うべきではないというのが原則です。
色々な言語とのリソース解放キーワードの歴史
こうした理由もあって、GC 型の言語は、 with
や using
, try
, defer
のようなリソース管理のためのキーワードを導入してきました。
それまでは try...finally
型のコード構文を利用して、リソース解放を行ってきました。
年代 | 言語 | 構文/イディオム | 典型的な書き方 | 備考 |
---|---|---|---|---|
1980s 後半 ~1990 年代 | C++ | RAII(デストラクタ) | { std::ifstream f("…"); … } |
構文ではなく仕組み。スコープ終了時に自動解放が保証される |
2002 | C# 1.0 |
using statement |
using(var f = …){…} |
.NET 最初期から存在 |
2006 | Python 2.5 |
with statement |
with open(…) as f: |
PEP 343 が採択され 2.5 で正式採用 |
2011 | Java SE 7 | try-with-resources |
try (var f = …){ … } |
AutoCloseable 実装で自動解放 |
2012 | Go 1.0 | defer |
defer f.Close() |
関数退出時に逆順実行。 |
2015 | Swift 2.0 | defer |
defer { fclose(f) } |
Xcode 7 で追加 |
2015 | Python 3.5 | async with |
async with aiofiles.open(…) |
非同期リソース対応 |
2023 | TypeScript 5.2 |
using / await using
|
await using file = await fs.open(…) |
ECMAScript「Explicit Resource Management」対応 (2024-04 現在 正式標準化前) |
この表は、各言語におけるリソース管理構文機能の歴史をまとめたものですが、
Python は歴史的にも早くにwith
文を導入しているように見えます。
自分としては、TypeScript においても、最近 using
構文が導入されたことは非常に嬉しいことでした。
第 2 章. Python で“自分専用 with を実装する
これまでの説明で、Python におけるリソース管理の歴史と他の言語との比較を行いました。
Python のリソース管理は、with
文を使用してリソースの取得と解放を自動的に行うことができ、途中でエラーが発生してもリソース解放を保証することを学びました。
紹介した file の open
メソッドは、with
文をサポートしていますが、自分で実装した関数やクラスをどのように with
文サポートする必要があるのでしょうか。
そのため、この with
文を使用することができるようにするコードを生成する方法を学ぶ必要があります。
__enter__
と __exit__
メソッドを実装する方法
Python では、with
文を使用するために、クラスに __enter__
と __exit__
メソッドを実装する必要があります。
from pathlib import Path
from typing import IO, Self
from types import TracebackType
class FileManager:
def __init__(self, path: Path, mode: str = "r") -> None:
self._f: IO[str] = open(path, mode)
def __enter__(self) -> Self:
return self
def read(self) -> str:
return self._f.read()
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
self._f.close()
with FileManager(Path("example.txt")) as sf:
print(sf.read())
先ほど、__del__
メソッドを使用してリソース解放を行うことは推奨されていないと説明しましたが、
class FileManager:
def __init__(self, path: Path) -> None:
self._f = open(path, "r")
def read(self) -> str:
return self._f.read()
def __del__(self) -> None:
print("Closing file...")
self._f.close()
def main() -> None:
file = FileManager(Path("example.txt"))
content = file.read()
とするのではなく、先程のように __enter__
と __exit__
メソッドを実装することで、
def main() -> None:
with FileManager(Path("example.txt")) as sf:
print(sf.read())
とwith
文を使用して宣言的にリソースを管理することが GC 言語である Python の特徴です。
__enter__
・ __exit__
メソッドに関する注意点
この__enter__
と __exit__
メソッドへの注意点がいくつかあるので、注意書きを記載しておきます。
__enter__
が呼ばれない限り __exit__
メソッドは呼び出されない
例えば、このコードにおいて、
from pathlib import Path
from types import TracebackType
from typing import IO, Self
class FileManager:
def __init__(self, path: Path, mode: str = "r") -> None:
self._f: IO[str] = open(path, mode)
def __enter__(self) -> Self:
return self
def read(self) -> str:
return self._f.read()
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
self._f.close()
def main() -> None:
with FileManager(Path("not_existing.txt")):
pass
として main()
を呼び出すとファイルが存在せずに __init__
メソッドで例外が発生するため、__enter__
メソッドは呼び出されず、__exit__
メソッドも呼び出されません。
なぜなら、このコードはもう少し分解すると、
manager = FileManager(Path("not_existing.txt"))
with manager:
pass
という文と等価であり、with
文の前の部分で例外が発生しているためです。
__exit__
メソッドの引数に関する注意点
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
という引数を持ちます。これは、with
文のスコープ内で例外が発生しなかった場合は、この 3 つの引数はすべてNone
になります。
一方で、例外が発生した場合には、exc_type
には例外の型、exc
には例外オブジェクト、tb
にはトレースバックオブジェクトが渡されます。
__exit__
メソッドの戻り値を True
にすると例外を無視する
__exit__
メソッドの戻り値を True
にすると、例外が無視されます。
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> bool:
return True
一方で、__exit__
メソッドの戻り値を False
または None
にすると、
例外は無視されず呼び出し元に波及します。
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> bool:
return False
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
return
これはどういう意味かというと、次の例を考えてみましょう
from pathlib import Path
from types import TracebackType
from typing import IO, Self
class FileManager:
def __init__(self, path: Path, mode: str = "r") -> None:
self._f: IO[str] = open(path, mode)
def __enter__(self) -> Self:
return self
def read(self) -> str:
data = self._f.read()
if not data:
raise ValueError("File is empty.")
if len(data) > 1_000_000:
raise MemoryError("File is too large.")
return data
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> bool:
print(exc_type)
print(exc)
print(tb)
self._f.close()
if isinstance(exc, MemoryError):
return True
return False
def main() -> None:
with FileManager(Path("empty.txt")) as f:
f.read()
print("done!")
File is empty.
Traceback (most recent call last):
File "/xxx/hoge.py", line 44, in
main()
File "/xxx/hoge.py", line 39, in main
f.read()
File "/xxx/hoge.py", line 18, in read
raise ValueError("File is empty.")
ValueError: File is empty.
このようになり、最後の print("done!")
は実行されません。
これは、ValueError
が発生したため、__exit__
メソッドが呼び出され、例外が無視されなかったためです。
一方で、
with FileManager(Path("too_large_file.py")) as f:
f.read()
print("done!")
と大きなファイルを読み込むと、MemoryError が発生しますが、__exit__
メソッドの中で例外を無視するようにしているため、with
を終えても例外は呼び出されず、with
文の後の print("done!")
が実行されます。
このコードは try-finally として書くと次のようになります。
from pathlib import Path
def main() -> None:
path = Path("valid_file.txt")
file = open(path, "r")
try:
data = file.read()
if not data:
raise ValueError("File is empty.")
if len(data) > 1_000_000:
raise MemoryError("File is too large.")
except MemoryError as e:
pass
finally:
file.close()
print("done!")
そのため、finally の処理を終えたあとに、MemoryError
の場合には例外が発生されず print("done!")
が呼ばれ、ValueError
の場合には、finally が終わったあとに例外が発生してプログラム自体が終了してしまい、print("done!")
が呼ばれなかったということになります。
こういう例外を無視するような処理をする場合には、__exit__
の引数を気をつける必要があるのですが、
通常そのようなことはあまりしないと思いますので、
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
tb: TracebackType | None,
) -> None:
return
と覚えておけば十分な場合が多いです。
contextlib.contextmanager
モジュール
ここまで見ると、__enter__
と __exit__
メソッドを実装することで、with
文を使用してリソース管理を行うことができることがわかりましたが、
このようにクラスを実装するのは、行数が多く冗長で面倒な場合もあります。
そのために続く、contextlib
モジュールのcontextmanager
デコレータを使用することで、より簡潔にリソース管理を行うことができます。
from contextlib import contextmanager
from typing import Iterator, TextIO
@contextmanager
def open_file(path: Path, mode: str = "r") -> Iterator[TextIO]:
f = open(path, mode)
try:
yield f
finally:
f.close()
with open_file(Path("example.txt")) as f:
print(f.read())
このように書くことができます。
これは非常に直感的で、
最初の章で with
文は try-finally
と同等なコードであることを思い出すと、
この文は非常に自然であることがわかります。
こちらはとても簡潔にかけることもあり、__enter__
と __exit__
メソッドを実装するよりも、
こちら側で実装することは私は多いです。
第 3 章. Python で複数のリソースを管理する
多重のwith
文を使用することもできます。
with open("example.txt", "r") as f1, open("example2.txt", "r") as f2:
content1 = f1.read()
content2 = f2.read()
これは
with open("example.txt", "r") as f1:
with open("example2.txt", "r") as f2:
content1 = f1.read()
content2 = f2.read()
と同じ意味になります。
1 段ネストにできるのためシンプルに書けるのが良い点だと思います。
contextlib.ExitStack
クラス
複数のリソースを管理する場合、contextlib.ExitStack
クラスを使用することで、より柔軟にリソース管理を行うことができます。
from contextlib import ExitStack
filenames = ["example1.txt", "example2.txt", "example3.txt"]
with ExitStack() as stack:
files = [stack.enter_context(open(name, "r")) for name in filenames]
contents = [f.read() for f in files]
これにより、 登録したすべてのリソースが “逆順” でクローズされます。
この例では、stack が構築された with
文のスコープを抜けると、
stack に enter_context
でその時まで登録されていたリソースがすべてクローズされます。
順番としては example1.txt → example2.txt → example3.txt の順番で open され登録されたので、
順番としては example3.txt → example2.txt → example1.txt の順番で close されます。
第 4 章. 非同期の with
文
Python 3.5 からは、非同期処理を行うための async with
構文が導入されました。
非同期処理についてはここでは深くは触れませんが、async with
文を使用することで、非同期処理の中でもリソース管理を行うことができます。
例えば、
import aiofiles
from pathlib import Path
async def main() -> None:
async with aiofiles.open(Path("example.txt"), mode="r") as f:
content = await f.read()
print(content)
このように書くことができます。
これを try-finally で書くと次のようになります。
import aiofiles
from pathlib import Path
async def main() -> None:
try:
f = await aiofiles.open(Path("example.txt"), mode="r")
content = await f.read()
print(content)
finally:
await f.close()
となります。
__enter__
と __exit__
メソッドに対応するのが __aenter__
・__aexit__
になり、async with
文を利用できるようになります。
また、contextlib
モジュールにも非同期処理用のデコレータが用意されています。contextlib.contextmanager
に相当するもののが contextlib.asynccontextmanager
です。contextlib.ExitStack
に相当するものが contextlib.AsyncExitStack
となります。
第 5 章. with
文をサポートする主なライブラリと型
以下は、Python 標準ライブラリで with
文をサポートする主な標準ライブラリです。
ライブラリ/型名 | 役割 | 説明 |
---|---|---|
open (builtins) |
ファイルオープン |
with open(...) as f: で使うと安全にクローズできる |
threading.Lock , threading.RLock
|
ロック制御 |
with lock: と書けば確実に unlock される |
threading.Condition , threading.Semaphore
|
同期オブジェクト |
with cond: や with sem: でロック取得 |
tempfile.TemporaryFile |
一時ファイル |
with TemporaryFile() as f: で自動削除される |
tempfile.NamedTemporaryFile |
名前つき一時ファイル | 同上(close すると自動でファイル消える) |
zipfile.ZipFile |
ZIP ファイル操作 |
with ZipFile(...) as zipf: で自動クローズ |
tarfile.TarFile |
TAR ファイル操作 | with TarFile.open(...) as tar: |
sqlite3.connect |
SQLite 接続 |
with sqlite3.connect(...) as conn: でトランザクション管理 |
subprocess.Popen |
プロセス制御 |
with Popen(...) as proc: で終了処理自動化(Python 3.2〜) |
socket.socket |
ソケット通信 |
with socket(...) as s: で自動 close
|
このようにファイルやソケット、スレッドロック,DB など、さまざまなリソース管理に利用されています。
このようにリソース管理を行い、安全に最後のリソースを解放を行うことができます。
第 6 章. 応用例
時間計測
import time
from typing import Iterator
from contextlib import contextmanager
@contextmanager
def measure_time(task_name: str) -> Iterator[None]:
start = time.time()
try:
yield
finally:
end = time.time()
print(f"{task_name} にかかった時間: {end - start:.3f} 秒")
def main() -> None:
with measure_time("ファイル読み込み処理"):
...
とすることで、with 文を利用して、スコープの開始と終了を計測することができます。
これはリソース管理という使い方というよりは、finally が必ず呼ばれるという契約を利用している使い方になります。
AWS DynamoDB を使ったリソース lock
AWS DynamoDB を使って、ここでは、複数プロセス間でのリソースロックを実装する例を示します。
DynamoDB は、AWS の NoSQL データベースサービスで、スケーラブルなデータストレージを提供します。
この例では、DynamoDB テーブルを利用して、同一の リソース ID に対しては同時にアクセスできないようにロックを取得します。
ConditionExpression を利用して、すでにそのリソース ID が存在する場合には失敗するようにしていて、
単一のプロセスしか with スコープに入れないようにすることができます。
import logging
import boto3
from contextlib import contextmanager
from typing import Iterator
from botocore.exceptions import ClientError
logger = logging.getLogger(__name__)
dynamodb = boto3.resource("dynamodb")
lock_table = dynamodb.Table("locks")
@contextmanager
def process_lock(resource_id: str) -> Iterator[None]:
try:
lock_table.put_item(
Item={"resource_id": resource_id},
ConditionExpression="attribute_not_exists(resource_id)"
)
logger.info(f"ロック取得成功: {resource_id}")
yield
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
logger.warning(f"ロック取得失敗(すでに他のプロセスがリソース確保中です): {resource_id}")
raise RuntimeError(f"リソース {resource_id} はロックされています") from e
else:
raise
finally:
try:
lock_table.delete_item(Key={"resource_id": resource_id})
logger.info(f"ロック解放成功: {resource_id}")
except Exception as e:
logger.error(f"ロック解放失敗: {resource_id} - {e}")
このような形で、contextlib を利用して、DynamoDB を利用したプロセス間のロックを実装することができます。
(実際に運用す際には、DynamoDB のテーブルの TTL (Time To Live) を利用して、ロックの有効期限を設定するなど、色々考慮する必要がありますが、参考となる考え方かなと思い掲載しておきました.
)
第 7 章. 補足
補足: with
文や finally
でも必ずしも動かない場合があるケース
ここまで、Python において with
文や try...finally
を使うことでリソース管理が安全にできる、という話をしてきました。
しかしながら、“必ず” 最後にリソース解放処理が走るとは限らない状況も存在します。
これはソフトウェアとして動いている以上仕方ない問題であったりします。
この章では、そうした 「防げないケース」 について整理し、
さらにこれは Python 特有の話ではなく、他言語にも共通する問題 であることを解説します。
Python において finally
が動作できないケース
ケース | 説明 |
---|---|
SIGKILL |
kill -9 などで送られる強制終了シグナルにより、OS カーネルがプロセスを即時終了させる |
OOM Killer |
メモリ不足時にカーネルがシステム保護のため特定プロセスを強制終了する |
os._exit() |
Python プロセスを即時終了させる。GC、finally 、with など一切実行されない |
os.abort() |
自プロセスに SIGABRT を送信して異常終了する。コアダンプを生成し、後処理は一切行われない |
Python インタプリタの異常終了 | CPython 自体がクラッシュ(バグや内部エラー)した場合、finally 文は実行されない |
ハードウェア障害 | 電源断・メモリ故障・ディスク故障などによりプログラムが強制的に停止する |
つまり、プロセスレベルで “即死” するような終了方法に対しては、Python の finally
や with
文は機能しません。
プロセスが終了する際に「後片付けの猶予」が与えられないため、
リソース解放(ファイル close・ロック解除・DB コネクション切断など)が実行されずに終わります。
これは Python だけの問題ではない
実はこの問題は Python に限りません。
例えば Go でも、os.Exit()
を呼び出すと defer 文が一切実行されず に即座にプロセスが終了します。
また、SIGKILL
シグナルを受けた場合も同様です。
つまり、多くの言語に共通する OS・プロセス管理レベルの限界 に由来する問題なのです。
逆にこれはちゃんと with
文や finally
が動作するよというケース
- 普通の 例外 (
try...except
でキャッチできる例外) が発生した場合 - SIGTERM や SIGINT などの一般的な終了シグナルを受け取った場合
-
sys.exit()
を呼び出して終了する場合
このため、Python でプログラムを正常終了させたい場合はos._exit()
のような「即死関数」ではなく、sys.exit()
を使うことが推奨されます。
なお、os._exit()
は名前にアンダースコア _ が含まれているとおり、
内部用途向け(private な関数) という位置づけです。
通常のアプリケーションコードでは、使わない方が安全です。
補足 : デプロイ時に発生する SIGTERM と SIGKILL ── プロセス終了のフロー
実際の運用環境(特に AWS ECS / Kubernetes / systemd / Docker など)では、
アプリケーションのデプロイやロールアウト時に、プロセスが強制終了される 場面が出てきます。
このとき、プロセスに対しては次のような流れでシグナルが送られます。
デプロイ時のプロセス終了フロー
- まず SIGTERM が送られて例外が発生する(優しく終了してね、という合図)
- アプリケーションがこれを受け取り、できるだけ速やかにリソース解放や後処理 (
finally
,with
) を行い、自発的に終了する - 一定時間内(例: 30 s)に終了しなければ SIGKILL が送られる (
finally
,with
文は実行されません)
つまり、猶予はあるが、永遠には待ってくれないということです。
このため、
SIGTERM を受け取ったらできるだけすぐ終了する
長時間かかるリソース解放(DB フラッシュ、大量ファイル保存など)は注意する
「SIGKILL される前に終了できる設計」 を意識する といった考え方が重要になります。
SIGKILL は先程述べたように、 OS カーネルレベルでの即時終了処理になりますので、finally
や with
文は一切実行されません。
SIGTERM を受けたら速やかに終了処理を行う必要があります。
SIGTERM の猶予時間はどれくらい?
これは実行環境によって違いますが、よくある例では:
実行環境 | SIGTERM -> SIGKILL までの デフォルト猶予時間 |
---|---|
Docker (docker stop) | 10 秒 (docker stop -t ) |
Kubernetes | 30 秒 (terminationGracePeriodSeconds) |
systemd | 90 秒 (TimeoutStopSec, ※ディストリビューション依存のため参考値) |
AWS ECS/Fargate | 30 秒 |
我々の会社では、AWS ECS/Fargate でのデプロイを行っているのですが、
30 秒制約を満たせるように設計しています。
まとめ
長くなりましたが、ここまで読んでいただきありがとうございました。
実際、ファイル、データベース、ネットワーク接続など、実務で扱うリソースの多くは明示的な管理が欠かせません。
Python のリソース管理は、単に便利なだけでなく、意図を明示し、コードを安全に保つための文化を根ざしています。
The Zen of Python の有名な言葉
Explicit is better than implicit. (明示的であることは暗黙的であることに勝る)
という言葉があるように、with
文を使用することで、リソース管理の意図を明確にし、コードの可読性と安全性を向上させることができます。
この文章を書いた理由
こちらで、 「もしいま、Python をイチから学び直すとしたら?」 というテーマで Findy さんの Engineer Lab に寄稿させていただきました。
これは初心者の方がどのように Python を学ぶと良いか、という内容を書いたものだったのですが、
その中で with 文や contextlib.contextmanager によるリソース管理 についても触れたので、
こちらで詳しく記載させていただきました。
こちらの内容で、Python を学んだばかりの方が Python のリソース管理について何か初心者の方の参考になれば幸いです。
Recustomerでは新しいメンバーを大大大募集しています!
Recustomerではこの記事で書いてあるようなことをコードレビューなどで議論しながら、
いつも楽しく技術についてお話しています。
今回の記事に関することや、Recustomerに興味を持っていただいた方はカジュアル面談でぜひお話ししましょう!