contextlib --- with 文コンテキスト用ユーティリティ

ソースコード: Lib/contextlib.py


このモジュールは with 文に関わる一般的なタスクのためのユーティリティを提供します。詳しい情報は、 コンテキストマネージャ型 と with文とコンテキストマネージャ を参照してください。

ユーティリティ

以下の関数とクラスを提供しています:

class contextlib.AbstractContextManager

__enter__() と __exit__() を実装するクラスのための abstract base class です。 __enter__() には self を返すデフォルトの実装があり、__exit__() はデフォルトで None を返す抽象メソッドです。 コンテキストマネージャ型 の定義も参照してください。

Added in version 3.6.

class contextlib.AbstractAsyncContextManager

__aenter__() と __aexit__() を実装するクラスのための abstract base class です。 __aenter__() には self を返すデフォルトの実装があり、__aexit__() はデフォルトで None を返す抽象メソッドです。 非同期コンテキストマネージャ (Asynchronous Context Manager) の定義も参照してください。

Added in version 3.7.

@contextlib.contextmanager

この関数は with 文コンテキストマネージャのファクトリ関数を定義するために利用できる デコレータ です。新しいクラスや __enter__() と __exit__() メソッドを別々に定義しなくても、ファクトリ関数を定義することができます。

多くのオブジェクトは with 文での使用をサポートしていますが、コンテキストマネージャではなく、contextlib.closing で使うための close() メソッドも実装していないリソースを管理する必要が生じることもあります。

リソースを正しく管理するよう保証する抽象的な例は以下のようなものでしょう:

from contextlib import contextmanager

@contextmanager
def managed_resource(*args, **kwds):
    # リソースを確保するコードの例:
    resource = acquire_resource(*args, **kwds)
    try:
        yield resource
    finally:
        # リソースを解放するコードの例:
        release_resource(resource)

このとき、関数は次のように使うことができます:

>>> with managed_resource(timeout=3600) as resource:
...     # ブロック内のコードが例外を送出しても、
...     # ブロックの終わりでリソースは解放されます

デコレート対象の関数は呼び出されたときに ジェネレータ-イテレータを返す必要があります。このイテレータは必ず値を1つ yield しなければなりません。 with 文の as 節が存在するなら、その値は as 節のターゲットへ束縛されることになります。

ジェネレータが yield を実行した箇所で with 文のネストされたブロックが実行されます。ブロックから抜けた後でジェネレータは再開されます。ブロック内で処理されない例外が発生した場合は、ジェネレータ内部の yield を実行した箇所で例外が再送出されます。なので、(もしあれば) エラーを捕捉したり、クリーンアップ処理を確実に実行したりするために、try...except...finally 構文を使用できます。例外を捕捉する目的が、(完全に例外を抑制してしまうのではなく) 単に例外のログをとるため、もしくはあるアクションを実行するためなら、ジェネレータはその例外を再送出しなければなりません。例外を再送出しない場合、ジェネレータのコンテキストマネージャは with 文に対して例外が処理されたことを示し、with 文の直後の文から実行を再開します。

@contextmanager は ContextDecorator を使用するため、作成したコンテキストマネージャは with 文でもデコレータとしても使用できます。 デコレータとして使う場合は、関数を呼び出すたびに、新しいジェネレータインスタンスが暗黙に作成されます (これにより、@contextmanager が作成する、本来は一度しか使えないコンテキストマネージャも、デコレータとして使うための複数回の呼び出しをサポートするという要件を満たせます)。

バージョン 3.2 で変更: ContextDecorator の使用。

@contextlib.asynccontextmanager

@~contextlib.contextmanager に似ていますが、非同期コンテキストマネージャ を作成します。

この関数は async with 文のための非同期コンテキストマネージャのファクトリ関数を定義するために利用できるデコレータ (decorator) です。新しいクラスや __aenter__() と __aexit__() メソッドを個別に定義する必要はありません。このデコレータは非同期ジェネレータ (asynchronous generator) 関数に適用しなければなりません。

簡単な例:

from contextlib import asynccontextmanager

@asynccontextmanager
async def get_connection():
    conn = await acquire_db_connection()
    try:
        yield conn
    finally:
        await release_db_connection(conn)

async def get_all_users():
    async with get_connection() as conn:
        return conn.query('SELECT ...')

Added in version 3.7.

@asynccontextmanager で定義したコンテキストマネージャは、デコレータとしても async with 文でも使用できます。

import time
from contextlib import asynccontextmanager

@asynccontextmanager
async def timeit():
    now = time.monotonic()
    try:
        yield
    finally:
        print(f'it took {time.monotonic() - now}s to run')

@timeit()
async def main():
    # ... 非同期のコード ...

デコレータとして使う場合は、関数を呼び出すたびに、新しいジェネレータインスタンスが暗黙に作成されます。 これにより、@asynccontextmanager が作成する、本来は一度しか使えないコンテキストマネージャも、デコレータとして使うための複数回の呼び出しをサポートするという要件を満たせます。

バージョン 3.10 で変更: @asynccontextmanager で作成した非同期コンテキストマネージャをデコレータとして使用できるようになりました。

contextlib.closing(thing)

ブロックの完了時に thing を close するコンテキストマネージャを返します。これは基本的に以下と等価です:

from contextlib import contextmanager

@contextmanager
def closing(thing):
    try:
        yield thing
    finally:
        thing.close()

そして、明示的に page を close する必要なしに、次のように書くことができます:

from contextlib import closing
from urllib.request import urlopen

with closing(urlopen('https://www.python.org')) as page:
    for line in page:
        print(line)

page を明示的に close する必要は無く、エラーが発生した場合でも、 with ブロックを出るときに page.close() が呼ばれます。

注釈

リソースを管理するほとんどの型は context manager プロトコルをサポートし、with 文を抜けるときに thing を閉じます。 そのため、closing() はコンテキストマネージャをサポートしないサードパーティー製の型で特に役立ちます。 urlopen() は通常、コンテキストマネージャとして使用するため、この例は説明のためのものにすぎません。

contextlib.aclosing(thing)

ブロックの完了時に thing の aclose() メソッドを呼び出すような非同期コンテキストマネージャを返します。これは基本的に以下と等価です:

from contextlib import asynccontextmanager

@asynccontextmanager
async def aclosing(thing):
    try:
        yield thing
    finally:
        await thing.aclose()

重要なことは、たとえば次の例のように break や例外によって早期にブロックが終了した場合に、 aclosing() は非同期ジェネレータの決定論的なクリーンアップをサポートすることです:

from contextlib import aclosing

async with aclosing(my_generator()) as values:
    async for value in values:
        if value == 42:
            break

このパターンは、ジェネレータの非同期な終了のコードがイテレーション処理と同じコンテキストの中で実行されることを保証します (すなわち例外とコンテキスト変数は期待通りに動作し、またジェネレータが依存するタスクの寿命が尽きたあとに終了のコードが実行されることもありません)。

Added in version 3.10.

contextlib.nullcontext(enter_result=None)

__enter__() から enter_result を返し、それ以外は何もしないコンテキストマネージャを返します。 省略可能なコンテキストマネージャの代役として使用することを意図しています。 例えば次のように使います。

def myfunction(arg, ignore_exceptions=False):
    if ignore_exceptions:
        # suppress を使ってすべての例外を無視します。
        cm = contextlib.suppress(Exception)
    else:
        # 例外を無視せず、cm は何も行いません。
        cm = contextlib.nullcontext()
    with cm:
        # 何らかの処理

enter_result を使った例です:

def process_file(file_or_path):
    if isinstance(file_or_path, str):
        # 文字列の場合はファイルを開きます
        cm = open(file_or_path)
    else:
        # ファイルを閉じる責任は呼び出し側にあります
        cm = nullcontext(file_or_path)

    with cm as file:
        # ファイルを処理します

非同期コンテキストマネージャ の代役として使うこともできます:

async def send_http(session=None):
    if not session:
        # HTTP セッションがなければ aiohttp で作成します
        cm = aiohttp.ClientSession()
    else:
        # セッションを閉じる責任は呼び出し側にあります
        cm = nullcontext(session)

    async with cm as session:
        # session を使って HTTP リクエストを送信します

Added in version 3.7.

バージョン 3.10 で変更: 非同期コンテキストマネージャ (asynchronous context manager) のサポートが追加されました。

contextlib.suppress(*exceptions)

with 文の内部で指定された例外の発生を抑えるコンテキストマネージャを返します。 with 文の後に続く最初の文から処理が再開されます。

ほかの完全に例外を抑制するメカニズム同様、このコンテキストマネージャは、黙ってプログラム実行を続けることが正しいことであるとわかっている、非常に限定的なエラーをカバーする以上の使い方はしてはいけません。

例えば:

from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove('somefile.tmp')

with suppress(FileNotFoundError):
    os.remove('someotherfile.tmp')

これは以下と等価です:

try:
    os.remove('somefile.tmp')
except FileNotFoundError:
    pass

try:
    os.remove('someotherfile.tmp')
except FileNotFoundError:
    pass

このコンテキストマネージャは 再入可能(リエントラント) です。

with ブロック内のコードが BaseExceptionGroup を送出した場合、抑制対象の例外はグループから取り除かれます。 グループ内の抑制されなかった例外は、元のグループの derive() メソッドで作成した新しいグループにまとめて再送出されます。

Added in version 3.4.

バージョン 3.12 で変更: suppress は、BaseExceptionGroup の一部として送出された例外の抑制をサポートするようになりました。

contextlib.redirect_stdout(new_target)

sys.stdout を一時的に別の file object にリダイレクトするコンテキストマネージャです。

このツールは、出力先が sys.stdout に固定されている既存の関数やクラスを柔軟に使えるようにします。

例えば、help() の出力は通常 sys.stdout に送られます。 出力先を io.StringIO オブジェクトにリダイレクトすれば、出力を文字列として取得できます。 置き換え先のストリームは __enter__() メソッドから返されるので、with 文のターゲットとして使用できます。

with redirect_stdout(io.StringIO()) as f:
    help(pow)
s = f.getvalue()

help() の出力をディスク上のファイルに送るためには、出力を通常のファイルにリダイレクトします:

with open('help.txt', 'w') as f:
    with redirect_stdout(f):
        help(pow)

help() の出力を標準エラー出力 (sys.stderr) に送るには以下のようにします:

with redirect_stdout(sys.stderr):
    help(pow)

sys.stdout のシステム全体にわたる副作用により、このコンテキストマネージャはライブラリコードやマルチスレッドアプリケーションでの使用には適していません。また、サブプロセスの出力に対しても効果がありません。そのような制限はありますが、それでも多くのユーティリティスクリプトに対して有用なアプローチです。

このコンテキストマネージャは 再入可能(リエントラント) です。

Added in version 3.4.

contextlib.redirect_stderr(new_target)

redirect_stdout() に似ていますが、グローバルな sys.stderr を別の file object にリダイレクトします。

このコンテキストマネージャは 再入可能(リエントラント) です。

Added in version 3.5.

contextlib.chdir(path)

現在の作業ディレクトリを変更するパラレル非安全なコンテキストマネージャです。グローバルな状態である作業ディレクトリを変更するため、ほとんどのマルチスレッドまたは非同期のコンテキストに対する利用は適切ではありません。また、プログラムの実行権限を一時的に放棄するジェネレータのような、直線的でないコードを実行する場合も適切ではありません -- 明確に必要でないかぎり、このコンテキストマネージャがアクティブな状態で yield すべきではありません。

これは chdir() の単純なラッパーで、コンテキストに入るときに現在の作業ディレクトリを変更し、終了時に元の作業ディレクトリを復元します。

このコンテキストマネージャは 再入可能(リエントラント) です。

Added in version 3.11.

class contextlib.ContextDecorator

コンテキストマネージャをデコレータとしても使用できるようにする基底クラスです。

ContextDecorator を継承するコンテキストマネージャは、通常どおり __enter__() と __exit__() を実装する必要があります。 デコレータとして使用した場合も、__exit__ で任意に例外処理を行えます。

@contextmanager は ContextDecorator を使用するため、この機能を自動的に利用できます。

ContextDecorator の例:

from contextlib import ContextDecorator

class mycontext(ContextDecorator):
    def __enter__(self):
        print('Starting')
        return self

    def __exit__(self, *exc):
        print('Finishing')
        return False

このとき、クラスは次のように使うことができます:

>>> @mycontext()
... def function():
...     print('The bit in the middle')
...
>>> function()
Starting
The bit in the middle
Finishing

>>> with mycontext():
...     print('The bit in the middle')
...
Starting
The bit in the middle
Finishing

これは次のような形のコードに対するシンタックスシュガーになります:

def f():
    with cm():
        # 処理を行います

ContextDecorator を使うと代わりに次のように書けます:

@cm()
def f():
    # 処理を行います

デコレーターを使うと、cm が関数の一部ではなく全体に適用されていることが明確になります (インデントレベルを1つ節約できるのもメリットです)。

すでに基底クラスを持っているコンテキストマネージャーも、ContextDecorator を mixin クラスとして利用することで拡張できます:

from contextlib import ContextDecorator

class mycontext(ContextBaseClass, ContextDecorator):
    def __enter__(self):
        return self

    def __exit__(self, *exc):
        return False

注釈

デコレートされた関数が複数回呼び出せるように、内部のコンテキストマネージャーは複数の with 文に対応する必要があります。そうでないなら、明示的な with 文を関数内で利用するべきです。

デコレートする呼び出し可能オブジェクトがジェネレータ関数、コルーチン関数、非同期ジェネレータ関数の場合、返されるラッパーも同じ種類になります。 コンテキストマネージャは、ジェネレータやコルーチンオブジェクトを作成する呼び出しの間だけでなく、反復や await の間ずっと開いたままになります。 ラップされたジェネレータと非同期ジェネレータは、反復の終了時に closing() や aclosing() を使った場合と同様に明示的に閉じられます。

注釈

非同期ジェネレータのラッパーは、async for を使って各値を再度 yield します。 asend() で送った値や athrow() で投げ込んだ例外は、ラップされたジェネレータには転送されません。

Added in version 3.2.

バージョン 3.15 で変更: ジェネレータ関数、コルーチン関数、非同期ジェネレータ関数をデコレートすると、反復や await の間もコンテキストマネージャを開いたままにするようになりました。 以前は、ジェネレータやコルーチンオブジェクトを作成するとすぐにコンテキストマネージャを終了していました。

class contextlib.AsyncContextDecorator

ContextDecorator に似ていますが、コンテキストマネージャへの出入りに async with を使います。 このクラスは、コルーチン関数と非同期ジェネレータ関数のデコレートに使ってください。 返されるラッパーも同じ種類になります。

注釈

同期関数と同期ジェネレータも受け付けますが、ラッパーは常に非同期になるため、デコレートした呼び出し可能オブジェクトは await するか、async for で反復する必要があります。 呼び出し方を変更する意図がない場合は、代わりに ContextDecorator を使ってください。

AsyncContextDecorator の使用例:

from asyncio import run
from contextlib import AsyncContextDecorator

class mycontext(AsyncContextDecorator):
    async def __aenter__(self):
        print('Starting')
        return self

    async def __aexit__(self, *exc):
        print('Finishing')
        return False

このとき、クラスは次のように使うことができます:

>>> @mycontext()
... async def function():
...     print('The bit in the middle')
...
>>> run(function())
Starting
The bit in the middle
Finishing

>>> async def function():
...    async with mycontext():
...         print('The bit in the middle')
...
>>> run(function())
Starting
The bit in the middle
Finishing

Added in version 3.10.

バージョン 3.15 で変更: 非同期ジェネレータ関数をデコレートすると、反復の間もコンテキストマネージャを開いたままにするようになりました。 以前は、ジェネレータオブジェクトを作成するとすぐにコンテキストマネージャを終了していました。 同期関数と同期ジェネレータ関数も受け付けるようになり、その場合は非同期のラッパーを返します。

class contextlib.ExitStack

他の、特にオプションであったり入力に依存するようなコンテキストマネージャーやクリーンアップ関数を動的に組み合わせるためのコンテキストマネージャーです。

例えば、複数のファイルを1つの with 文で簡単に扱うことができます:

with ExitStack() as stack:
    files = [stack.enter_context(open(fname)) for fname in filenames]
    # リストの後半にあるファイルを開く際に例外が発生しても、
    # 開いたすべてのファイルは with 文の終わりで
    # 自動的に閉じられます

__enter__() メソッドは ExitStack インスタンスを返すだけで、追加の処理は行いません。

各インスタンスは登録されたコールバックのスタックを管理し、インスタンスが (明示的に、あるいは with 文の終わりに暗黙的に) close されるときに逆順でそれを呼び出します。コンテキストスタックのインスタンスが暗黙的にガベージコレクトされたときには callback は呼び出され ません 。

このスタックモデルは、(file オブジェクトのように) __init__ メソッドでリソースを確保するコンテキストマネージャーを正しく扱うためのものです。

登録されたコールバックが登録の逆順で実行されるので、複数のネストされた with 文を利用するのと同じ振る舞いをします。これは例外処理にも適用されます。内側のコールバックが例外を抑制したり置き換えたりした場合、外側のコールバックには更新された状態に応じた引数が渡されます。

これは正しく exit callback の stack を巻き戻すための、比較的低レベルな API です。アプリケーション独自のより高レベルなコンテキストマネージャーを作るための基板として使うのに適しています。

Added in version 3.3.

enter_context(cm)

新しいコンテキストマネージャに入り、その __exit__() メソッドをコールバックスタックに追加します。 戻り値は、コンテキストマネージャ自身の __enter__() メソッドの結果です。

コンテキストマネージャーは、普段 with 文で利用された時と同じように、例外を抑制することができます。

バージョン 3.11 で変更: cm がコンテキストマネージャでなかった場合、 AttributeError の代わりに TypeError 例外を送出します。

バージョン 3.15 で変更: __enter__() と __exit__() に任意のデスクリプタを使用できるようになりました。

push(exit)

コンテキストマネージャの __exit__() メソッドをコールバックスタックに追加します。

__enter__ は呼び出され ない ため、このメソッドを使えば、__enter__() の実装の一部を、コンテキストマネージャ自身の __exit__() メソッドによる終了処理の対象にできます。

コンテキストマネージャでないオブジェクトが渡された場合、このメソッドは、そのオブジェクトをコンテキストマネージャの __exit__() メソッドと同じシグネチャを持つコールバックと見なし、コールバックスタックに直接追加します。

これらのコールバックは、真の値を返すことで、コンテキストマネージャの __exit__() メソッドと同様に例外を抑制できます。

この関数はデコレータとしても使えるように、受け取ったオブジェクトをそのまま返します。

バージョン 3.15 で変更: __exit__() に任意のデスクリプタを使用できるようになりました。

callback(callback, /, *args, **kwds)

任意の関数と引数を受け取り、コールバックスタックに追加します。

他のメソッドと異なり、このメソッドで追加されたコールバックは例外を抑制しません (例外の詳細も渡されません)。

この関数はデコレータとしても使えるように、受け取った callback をそのまま返します。

pop_all()

コールバックスタックを新しい ExitStack インスタンスに移して、それを返します。このメソッドは callback を実行しません。代わりに、新しい stack が (明示的に、あるいは with 文の終わりに暗黙的に) close されるときに実行されます。

例えば、複数のファイルを "all or nothing" に開く処理を次のように書けます:

with ExitStack() as stack:
    files = [stack.enter_context(open(fname)) for fname in filenames]
    # close メソッドを保持しますが、まだ呼び出しません。
    close_files = stack.pop_all().close
    # どれかのファイルを開くのに失敗すると、それまでに開いた
    # すべてのファイルが自動的に閉じられます。
    # すべてのファイルを
    # 開けた場合は、with 文が終わった後も開いたままになります。
    # その後、close_files() を明示的に呼んですべて閉じられます。
close()

すぐにコールバックスタックを巻き戻し、コールバック関数を登録の逆順に呼び出します。登録されたすべてのコンテキストマネージャーと終了 callback に、例外が起こらなかった場合の引数が渡されます。

class contextlib.AsyncExitStack

ExitStack に似た 非同期コンテキストマネージャ です。スタック上で同期と非同期の両方のコンテキストマネージャの組み合わせをサポートします。また、後処理のためのコルーチンも持っています。

close() メソッドは実装されていません。 代わりに aclose() を使用する必要があります。

async enter_async_context(cm)

ExitStack.enter_context() に似ていますが、非同期コンテキストマネージャを受け取ります。

バージョン 3.11 で変更: cm が非同期コンテキストマネージャでなかった場合、 AttributeError の代わりに TypeError 例外を送出します。

バージョン 3.15 で変更: __aenter__() と __aexit__() に任意のデスクリプタを使用できるようになりました。

push_async_exit(exit)

ExitStack.push() に似ていますが、非同期コンテキストマネージャまたはコルーチン関数を受け取ります。

バージョン 3.15 で変更: __aexit__() に任意のデスクリプタを使用できるようになりました。

push_async_callback(callback, /, *args, **kwds)

ExitStack.callback() に似ていますが、コルーチン関数を受け取ります。

async aclose()

ExitStack.close() に似ていますが、待機可能オブジェクトを適切に処理します。

@asynccontextmanager の例の続きです。

async with AsyncExitStack() as stack:
    connections = [await stack.enter_async_context(get_connection())
        for i in range(5)]
    # リストの後半で接続を開く際に例外が発生しても、
    # 開いたすべての接続は async with 文の終わりで
    # 自動的に解放されます。

Added in version 3.7.

例とレシピ

この節では、contextlib の提供するツールを効果的に使うための例やレシピを紹介します。

可変数個のコンテキストマネージャーをサポートする

ExitStack の第一のユースケースは、クラスのドキュメントにかかれている通り、一つの with 文で可変数個のコンテキストマネージャーや他のクリーンアップ関数をサポートすることです。ユーザーの入力 (指定された複数個のファイルを開く場合など) に応じて複数個のコンテキストマネージャーが必要となる場合や、いくつかのコンテキストマネージャーがオプションとなる場合に、可変数個のコンテキストマネージャーが必要になります:

with ExitStack() as stack:
    for resource in resources:
        stack.enter_context(resource)
    if need_special_resource():
        special = acquire_special_resource()
        stack.callback(release_special_resource, special)
    # 確保したリソースを使う操作を行います

上の例にあるように、 ExitStack はコンテキストマネージャープロトコルをサポートしていないリソースの管理を with 文を使って簡単に行えるようにします。

__enter__ メソッドからの例外をキャッチする

with 文の本体やコンテキストマネージャの __exit__() メソッドからの例外を誤って捕捉 せずに、__enter__() メソッドの実装からの例外だけを捕捉したい場合があります。 ExitStack を使えば、コンテキスト管理プロトコルの手順を少し分離することで、これを実現できます。

stack = ExitStack()
try:
    x = stack.enter_context(cm)
except Exception:
    # __enter__ の例外を処理します
else:
    with stack:
        # 通常の場合を処理します

実際のところ、このようなコードが必要になるのならば、利用している API 側で try/except/finally 文を使った直接的なリソース管理インターフェースを提供するべきです。しかし、すべての API がそのようによく設計されているとは限りません。もしコンテキストマネージャーが提供されている唯一のリソース管理APIであるなら、 ExitStack を使って with 文を使って処理することができない様々なシチュエーションの処理をすることができます。

__enter__ 実装内のクリーンアップ

ExitStack.push() のドキュメントで説明したとおり、このメソッドは __enter__() の実装の後半で処理が失敗した場合に、すでに確保したリソースをクリーンアップするのに役立ちます。

次の例では、リソースの確保と開放の関数に加えて、オプションのバリデーション関数を受け取るコンテキストマネージャーで、この方法を使ってコンテキストマネージャープロトコルを提供しています:

from contextlib import contextmanager, AbstractContextManager, ExitStack

class ResourceManager(AbstractContextManager):

    def __init__(self, acquire_resource, release_resource, check_resource_ok=None):
        self.acquire_resource = acquire_resource
        self.release_resource = release_resource
        if check_resource_ok is None:
            def check_resource_ok(resource):
                return True
        self.check_resource_ok = check_resource_ok

    @contextmanager
    def _cleanup_on_error(self):
        with ExitStack() as stack:
            stack.push(self)
            yield
            # 検証に成功し、例外は発生しませんでした。
            # したがって、リソースを保持し、
            # 呼び出し側に返します。
            stack.pop_all()

    def __enter__(self):
        resource = self.acquire_resource()
        with self._cleanup_on_error():
            if not self.check_resource_ok(resource):
                msg = "Failed validation for {!r}"
                raise RuntimeError(msg.format(resource))
        return resource

    def __exit__(self, *exc_details):
        # リソースの解放処理を重複して記述する必要はありません
        self.release_resource()

try-finally + flag 変数パターンを置き換える

try-finally 文に、finally 句の内容を実行するかどうかを示すフラグ変数を組み合わせたパターンを目にすることがあるかもしれません。一番シンプルな (単に except 句を使うだけでは処理できない) ケースでは次のようなコードになります:

cleanup_needed = True
try:
    result = perform_operation()
    if result:
        cleanup_needed = False
finally:
    if cleanup_needed:
        cleanup_resources()

try 文を使ったコードでは、セットアップとクリーンアップのコードが任意の長さのコードで分離してしまうので、開発者やレビューアにとって問題になりえます。

ExitStack を使えば、代わりに with 文の終わりに実行されるコールバックを登録し、後でそのコールバックをスキップするかどうかを決定できます:

from contextlib import ExitStack

with ExitStack() as stack:
    stack.callback(cleanup_resources)
    result = perform_operation()
    if result:
        stack.pop_all()

これにより、別途フラグ変数を用意せずに、意図したクリーンアップの動作をあらかじめ明示できます。

もしあるアプリケーションがこのパターンを多用するのであれば、小さいヘルパークラスを導入してよりシンプルにすることができます:

from contextlib import ExitStack

class Callback(ExitStack):
    def __init__(self, callback, /, *args, **kwds):
        super().__init__()
        self.callback(callback, *args, **kwds)

    def cancel(self):
        self.pop_all()

with Callback(cleanup_resources) as cb:
    result = perform_operation()
    if result:
        cb.cancel()

もしリソースのクリーンアップが単体の関数にまとまってない場合でも、 ExitStack.callback() のデコレーター形式を利用してリソース開放処理を宣言することができます:

from contextlib import ExitStack

with ExitStack() as stack:
    @stack.callback
    def cleanup_resources():
        ...
    result = perform_operation()
    if result:
        stack.pop_all()

デコレータープロトコルの使用上、このように宣言されたコールバック関数は引数を取ることができません。その代わりに、リリースするリソースをクロージャー変数としてアクセスできる必要があります。

コンテキストマネージャーを関数デコレーターとして使う

ContextDecorator はコンテキストマネージャーを通常の with 文に加えて関数デコレーターとしても利用できるようにします。

例えば、関数やまとまった文を、そこに入った時と出た時の時間をトラックするロガーでラップしたい場合があります。そのために関数デコレーターとコンテキストマネージャーを別々に書く代わりに、 ContextDecorator を継承すると1つの定義で両方の機能を提供できます:

from contextlib import ContextDecorator
import logging

logging.basicConfig(level=logging.INFO)

class track_entry_and_exit(ContextDecorator):
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        logging.info('Entering: %s', self.name)

    def __exit__(self, exc_type, exc, exc_tb):
        logging.info('Exiting: %s', self.name)

このクラスのインスタンスはコンテキストマネージャーとしても利用でき:

with track_entry_and_exit('widget loader'):
    print('Some time consuming activity goes here')
    load_widget()

また関数デコレーターとしても利用できます:

@track_entry_and_exit('widget loader')
def activity():
    print('Some time consuming activity goes here')
    load_widget()

コンテキストマネージャを関数デコレータとして使う場合には、もう 1 つ制限があります。 __enter__() の戻り値にアクセスする方法がありません。 その値が必要な場合は、明示的な with 文を使う必要があります。

参考

PEP 343 - "with" ステートメント

Python の with 文の仕様、背景、および例が記載されています。

単回使用、再利用可能、およびリエントラントなコンテキストマネージャ

ほとんどのコンテキストマネージャは、 with 文の中で一度だけ使われるような場合に効果的になるように書かれています。これら単回使用のコンテキストマネージャは毎回新規に生成されなければなりません - それらを再利用しようとすると、例外を引き起こすか、正しく動作しません。

この共通の制限が意味することは、コンテキストマネージャは (上記すべての使用例に示すとおり) 一般に with 文のヘッダ部分で直接生成することが推奨されるということです。

ファイルオブジェクトは単回使用のコンテキストマネージャ有効に利用した例です。最初の with 文によりファイルがクローズされ、それ以降そのファイルオブジェクトに対するすべての IO 操作を防止します。

Context managers created using @contextmanager are also single use context managers, and will complain about the underlying generator failing to yield if an attempt is made to use them a second time:

>>> from contextlib import contextmanager
>>> @contextmanager
... def singleuse():
...     print("Before")
...     yield
...     print("After")
...
>>> cm = singleuse()
>>> with cm:
...     pass
...
Before
After
>>> with cm:
...     pass
...
Traceback (most recent call last):
    ...
RuntimeError: generator didn't yield

リエントラントなコンテキストマネージャ

より洗練されたコンテキストマネージャには"リエントラント"なものがあります。そのようなコンテキストマネージャは、複数の with 文で使えるだけでなく、同じコンテキストマネージャをすでに使っている with 文の 内部 でも使うことができます。

threading.RLock はリエントラントなコンテキストマネージャの例であり、また suppress(), redirect_stdout(), そして chdir() もリエントラントです。以下はリエントラントな利用法の非常に単純な例です:

>>> from contextlib import redirect_stdout
>>> from io import StringIO
>>> stream = StringIO()
>>> write_to_stream = redirect_stdout(stream)
>>> with write_to_stream:
...     print("This is written to the stream rather than stdout")
...     with write_to_stream:
...         print("This is also written to the stream")
...
>>> print("This is written directly to stdout")
This is written directly to stdout
>>> print(stream.getvalue())
This is written to the stream rather than stdout
This is also written to the stream

リエントラントな性質の実例はお互いを呼び出しあう複数の関数を含んでいる可能性が高く、したがってこの例よりもはるかに複雑です。

リエントラントであることはスレッドセーフであることと同じ ではない ことには注意が必要です。たとえば redirect_stdout() は、 sys.stdout を異なるストリームに束縛することによりシステムの状態に対してグローバルな変更を行うことから、明らかにスレッドセーフではありません。

再利用可能なコンテキストマネージャ

単回使用のコンテキストマネージャとリエントラントなコンテキストマネージャのいずれとも異なるタイプに "再利用可能" なコンテキストマネージャがあります (あるいは、より明確には、"再利用可能だがリエントラントでない" コンテキストマネージャです。 リエントラントなコンテキストマネージャもまた再利用可能だからです)。 再利用可能なコンテキストマネージャは複数回利用をサポートしますが、同じコンテキストマネージャのインスタンスが外側の with 文ですでに使用中の場合には失敗します (もしくは正しく動作しません)。

threading.Lock は再利用可能だがリエントラントでないコンテキストマネージャの例です (リエントラントなロックのためには threading.RLock を代わりに使う必要があります)。

再利用可能だがリエントラントでないコンテキストマネージャのもう一つの例は ExitStack です。 これを使うどの with 文を抜けるときにも、現在登録されている すべての コールバックを呼び出します。 コールバックをどこで登録したかは関係ありません。

>>> from contextlib import ExitStack
>>> stack = ExitStack()
>>> with stack:
...     stack.callback(print, "Callback: from first context")
...     print("Leaving first context")
...
Leaving first context
Callback: from first context
>>> with stack:
...     stack.callback(print, "Callback: from second context")
...     print("Leaving second context")
...
Leaving second context
Callback: from second context
>>> with stack:
...     stack.callback(print, "Callback: from outer context")
...     with stack:
...         stack.callback(print, "Callback: from inner context")
...         print("Leaving inner context")
...     print("Leaving outer context")
...
Leaving inner context
Callback: from inner context
Callback: from outer context
Leaving outer context

例における出力が示すように、ひとつのスタックオブジェクトを複数の with 文で再利用しても正しく動作します。しかし入れ子にして使った場合は、一番内側の with 文を抜ける際にスタックが空になります。これは望ましい動作とは思えません。

ひとつの ExitStack インスタンスを再利用する代わりに複数のインスタンスを使うことにより、この問題は回避することができます:

>>> from contextlib import ExitStack
>>> with ExitStack() as outer_stack:
...     outer_stack.callback(print, "Callback: from outer context")
...     with ExitStack() as inner_stack:
...         inner_stack.callback(print, "Callback: from inner context")
...         print("Leaving inner context")
...     print("Leaving outer context")
...
Leaving inner context
Callback: from inner context
Leaving outer context
Callback: from outer context