月曜日, 4月 28, 2025
Google search engine
ホームニューステックニュース【保存版】 2 万文字で語る Python の with 文で始めるリソース管理 ── C++/Go/TypeScript の技法を横断

【保存版】 2 万文字で語る Python の with 文で始めるリソース管理 ── C++/Go/TypeScript の技法を横断



本記事では、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 は、withusingを使用してリソース管理を行うことはできますが、
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 系の言語では、
withusing,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 型の言語は、 withusing, 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、finallywith など一切実行されない
os.abort() 自プロセスに SIGABRT を送信して異常終了する。コアダンプを生成し、後処理は一切行われない
Python インタプリタの異常終了 CPython 自体がクラッシュ(バグや内部エラー)した場合、finally 文は実行されない
ハードウェア障害 電源断・メモリ故障・ディスク故障などによりプログラムが強制的に停止する

つまり、プロセスレベルで “即死” するような終了方法に対しては、Python の finallywith 文は機能しません。
プロセスが終了する際に「後片付けの猶予」が与えられないため、
リソース解放(ファイル 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 など)では、
アプリケーションのデプロイやロールアウト時に、プロセスが強制終了される 場面が出てきます。

このとき、プロセスに対しては次のような流れでシグナルが送られます。

デプロイ時のプロセス終了フロー

  1. まず SIGTERM が送られて例外が発生する(優しく終了してね、という合図)
  2. アプリケーションがこれを受け取り、できるだけ速やかにリソース解放や後処理 (finally, with) を行い、自発的に終了する
  3. 一定時間内(例: 30 s)に終了しなければ SIGKILL が送られる (finally, with 文は実行されません)

つまり、猶予はあるが、永遠には待ってくれないということです。

このため、

SIGTERM を受け取ったらできるだけすぐ終了する
長時間かかるリソース解放(DB フラッシュ、大量ファイル保存など)は注意する

「SIGKILL される前に終了できる設計」 を意識する といった考え方が重要になります。
SIGKILL は先程述べたように、 OS カーネルレベルでの即時終了処理になりますので、
finallywith 文は一切実行されません。
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文を使用することで、リソース管理の意図を明確にし、コードの可読性と安全性を向上させることができます。

この文章を書いた理由

https://findy-code.io/engineer-lab/techtensei-curekoshimizu

こちらで、 「もしいま、Python をイチから学び直すとしたら?」 というテーマで Findy さんの Engineer Lab に寄稿させていただきました。
これは初心者の方がどのように Python を学ぶと良いか、という内容を書いたものだったのですが、
その中で with 文や contextlib.contextmanager によるリソース管理 についても触れたので、
こちらで詳しく記載させていただきました。

こちらの内容で、Python を学んだばかりの方が Python のリソース管理について何か初心者の方の参考になれば幸いです。

Recustomerでは新しいメンバーを大大大募集しています!
Recustomerではこの記事で書いてあるようなことをコードレビューなどで議論しながら、
いつも楽しく技術についてお話しています。
今回の記事に関することや、Recustomerに興味を持っていただいた方はカジュアル面談でぜひお話ししましょう!

https://findy-code.io/companies/1720/jobs/ZTKi_6WF3GBgA

フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -
Google search engine

Most Popular

Recent Comments