contextlib --- 用於 with 語句上下文的工具

原始碼: Lib/contextlib.py


本模組提供了用於 with 語句相關常見任務的工具。更多資訊請參閱 上下文管理器型別With 語句上下文管理器

工具

提供的函式和類

class contextlib.AbstractContextManager

一個為實現了 object.__enter__()object.__exit__() 的類提供的 抽象基類。為 object.__enter__() 提供了預設實現,它會返回 self,而 object.__exit__() 是一個抽象方法,預設返回 None。另請參閱 上下文管理器型別 的定義。

在 3.6 版本加入。

class contextlib.AbstractAsyncContextManager

一個為實現了 object.__aenter__()object.__aexit__() 的類提供的 抽象基類。為 object.__aenter__() 提供了預設實現,它會返回 self,而 object.__aexit__() 是一個抽象方法,預設返回 None。另請參閱 非同步上下文管理器 的定義。

在 3.7 版本加入。

@contextlib.contextmanager

此函式是一個 裝飾器,可用於為 with 語句上下文管理器定義一個工廠函式,而無需建立一個類或單獨的 __enter__()__exit__() 方法。

雖然許多物件本身就支援在 with 語句中使用,但有時需要管理的資源本身不是上下文管理器,也沒有實現可用於 contextlib.closingclose() 方法。

一個抽象的例子如下,以確保正確的資源管理:

from contextlib import contextmanager

@contextmanager
def managed_resource(*args, **kwds):
    # Code to acquire resource, e.g.:
    resource = acquire_resource(*args, **kwds)
    try:
        yield resource
    finally:
        # Code to release resource, e.g.:
        release_resource(resource)

然後可以像這樣使用該函式:

>>> with managed_resource(timeout=3600) as resource:
...     # Resource is released at the end of this block,
...     # even if code in the block raises an exception

被裝飾的函式在被呼叫時必須返回一個 生成器-迭代器。此迭代器必須只 yield 一個值,該值將被繫結到 with 語句的 as 子句中的目標(如果有的話)。

在生成器 yield 的地方,with 語句中巢狀的程式碼塊將被執行。程式碼塊退出後,生成器將恢復執行。如果在程式碼塊中發生未處理的異常,它將在生成器內部 yield 發生的地方被重新引發。因此,你可以使用 try...except...finally 語句來捕獲錯誤(如果有),或確保進行一些清理工作。如果捕獲異常僅僅是為了記錄日誌或執行某些操作(而不是完全抑制它),生成器必須重新引發該異常。否則,生成器上下文管理器將向 with 語句表明異常已處理,程式將從緊跟在 with 語句之後的語句繼續執行。

contextmanager() 使用 ContextDecorator,因此它建立的上下文管理器既可以作為裝飾器使用,也可以在 with 語句中使用。當用作裝飾器時,每次函式呼叫都會隱式建立一個新的生成器例項(這使得由 contextmanager() 建立的原本“一次性”的上下文管理器能夠滿足作為裝飾器使用的上下文管理器必須支援多次呼叫的要求)。

在 3.2 版更改: 使用了 ContextDecorator

@contextlib.asynccontextmanager

contextmanager() 類似,但建立的是一個 非同步上下文管理器

此函式是一個 裝飾器,可用於為 async with 語句非同步上下文管理器定義一個工廠函式,而無需建立一個類或單獨的 __aenter__()__aexit__() 方法。它必須應用於一個 非同步生成器 函式。

一個簡單的例子:

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 ...')

在 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():
    # ... async code ...

當用作裝飾器時,每次函式呼叫都會隱式地建立一個新的生成器例項。這使得由 asynccontextmanager() 建立的原本“一次性”的上下文管理器能夠滿足作為裝飾器使用的上下文管理器必須支援多次呼叫的要求。

在 3.10 版更改: asynccontextmanager() 建立的非同步上下文管理器可以用作裝飾器。

contextlib.closing(thing)

返回一個上下文管理器,它會在程式碼塊完成時關閉 thing。這基本上等同於:

from contextlib import contextmanager

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

這樣你就可以這樣寫程式碼:

from contextlib import closing
from urllib.request import urlopen

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

而無需顯式關閉 page。即使發生錯誤,當 with 程式碼塊退出時,page.close() 也會被呼叫。

備註

大多數管理資源的型別都支援 上下文管理器 協議,該協議會在離開 with 語句時關閉 thing。因此,closing() 對於不支援上下文管理器的第三方型別最為有用。此示例純粹用於說明目的,因為 urlopen() 通常會在上下文管理器中使用。

contextlib.aclosing(thing)

返回一個非同步上下文管理器,它會在程式碼塊完成時呼叫 thingaclose() 方法。這基本上等同於:

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

這種模式確保了生成器的非同步退出程式碼在其迭代的相同上下文中執行(這樣異常和上下文變數就能按預期工作,並且退出程式碼不會在其所依賴的某個任務的生命週期結束後執行)。

在 3.10 版本加入。

contextlib.nullcontext(enter_result=None)

返回一個上下文管理器,它從 __enter__ 返回 enter_result,但除此之外什麼也不做。它旨在用作可選上下文管理器的替代品,例如:

def myfunction(arg, ignore_exceptions=False):
    if ignore_exceptions:
        # Use suppress to ignore all exceptions.
        cm = contextlib.suppress(Exception)
    else:
        # Do not ignore any exceptions, cm has no effect.
        cm = contextlib.nullcontext()
    with cm:
        # Do something

使用 enter_result 的示例:

def process_file(file_or_path):
    if isinstance(file_or_path, str):
        # If string, open file
        cm = open(file_or_path)
    else:
        # Caller is responsible for closing file
        cm = nullcontext(file_or_path)

    with cm as file:
        # Perform processing on the file

它也可以用作 非同步上下文管理器 的替代品:

async def send_http(session=None):
    if not session:
        # If no http session, create it with aiohttp
        cm = aiohttp.ClientSession()
    else:
        # Caller is responsible for closing the session
        cm = nullcontext(session)

    async with cm as session:
        # Send http requests with session

在 3.7 版本加入。

在 3.10 版更改: 增加了對 非同步上下文管理器 的支援。

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() 方法建立的。

在 3.4 版本加入。

在 3.12 版更改: suppress 現在支援抑制作為 BaseExceptionGroup 一部分引發的異常。

contextlib.redirect_stdout(new_target)

用於將 sys.stdout 臨時重定向到另一個檔案或類檔案物件的上下文管理器。

此工具為那些輸出被硬編碼到 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 的全域性副作用意味著此上下文管理器不適合在庫程式碼和大多數多執行緒應用程式中使用。它對子程序的輸出也沒有影響。然而,對於許多實用工具指令碼來說,它仍然是一種有用的方法。

此上下文管理器是 可重入的

在 3.4 版本加入。

contextlib.redirect_stderr(new_target)

redirect_stdout() 類似,但重定向的是 sys.stderr 到另一個檔案或類檔案物件。

此上下文管理器是 可重入的

在 3.5 版本加入。

contextlib.chdir(path)

一個非並行安全的上下文管理器,用於更改當前工作目錄。由於這會改變一個全域性狀態,即工作目錄,因此它不適合在大多數執行緒化或非同步上下文中使用。它也不適合大多數非線性程式碼執行,如生成器,在其中程式執行會暫時放棄——除非明確需要,否則當此上下文管理器處於活動狀態時,不應 yield。

這是對 chdir() 的一個簡單包裝,它在進入時更改當前工作目錄,並在退出時恢復舊目錄。

此上下文管理器是 可重入的

在 3.11 版本中新增。

class contextlib.ContextDecorator

一個基類,使上下文管理器也可以用作裝飾器。

繼承自 ContextDecorator 的上下文管理器必須像往常一樣實現 __enter____exit__。即使作為裝飾器使用,__exit__ 也保留其可選的異常處理功能。

ContextDecoratorcontextmanager() 使用,所以你會自動獲得這個功能。

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():
        # Do stuff

ContextDecorator 讓你改為這樣寫:

@cm()
def f():
    # Do stuff

這清楚地表明 cm 應用於整個函式,而不僅僅是其中的一部分(而且節省一個縮排級別也不錯)。

已經有基類的現有上下文管理器可以透過使用 ContextDecorator 作為混入類(mixin class)來擴充套件:

from contextlib import ContextDecorator

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

    def __exit__(self, *exc):
        return False

備註

由於被裝飾的函式必須能夠被多次呼叫,底層的上下文管理器必須支援在多個 with 語句中使用。如果不是這種情況,那麼應該使用在函式內部帶有顯式 with 語句的原始結構。

在 3.2 版本加入。

class contextlib.AsyncContextDecorator

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

在 3.10 版本加入。

class contextlib.ExitStack

一個上下文管理器,旨在簡化以程式設計方式組合其他上下文管理器和清理函式的過程,特別是那些可選的或由輸入資料驅動的。

例如,一組檔案可以很容易地在單個 with 語句中處理,如下所示:

with ExitStack() as stack:
    files = [stack.enter_context(open(fname)) for fname in filenames]
    # All opened files will automatically be closed at the end of
    # the with statement, even if attempts to open files later
    # in the list raise an exception

__enter__() 方法返回 ExitStack 例項,並且不執行任何額外操作。

每個例項都維護一個已註冊回撥的堆疊,當例項關閉時(無論是在 with 語句結束時顯式或隱式關閉),這些回撥會以相反的順序被呼叫。請注意,當上下文堆疊例項被垃圾回收時,回撥*不會*被隱式呼叫。

使用這種堆疊模型是為了正確處理那些在它們的 __init__ 方法中獲取資源的上下文管理器(例如檔案物件)。

由於註冊的回撥是按註冊順序的相反順序呼叫的,這最終的行為就好像使用了多個巢狀的 with 語句,並帶有已註冊的回撥集。這甚至擴充套件到了異常處理——如果一個內部回撥抑制或替換了一個異常,那麼外部回撥將被傳遞基於該更新狀態的引數。

這是一個相對底層的 API,它負責正確展開退出回撥堆疊的細節。它為以應用程式特定方式操作退出堆疊的更高級別上下文管理器提供了合適的基礎。

在 3.3 版本加入。

enter_context(cm)

進入一個新的上下文管理器,並將其 __exit__() 方法新增到回撥堆疊中。返回值是該上下文管理器自己的 __enter__() 方法的結果。

這些上下文管理器可以像直接作為 with 語句的一部分使用時一樣抑制異常。

在 3.11 版更改: 如果 cm 不是上下文管理器,則會引發 TypeError 而不是 AttributeError

push(exit)

將上下文管理器的 __exit__() 方法新增到回撥堆疊中。

由於 __enter__ *沒有*被呼叫,此方法可用於透過上下文管理器自己的 __exit__() 方法覆蓋 __enter__() 實現的一部分。

如果傳遞的物件不是上下文管理器,此方法會假定它是一個與上下文管理器的 __exit__() 方法具有相同簽名的回撥,並將其直接新增到回撥堆疊中。

透過返回真值,這些回撥可以像上下文管理器的 __exit__() 方法一樣抑制異常。

傳入的物件會從函式中返回,從而允許此方法用作函式裝飾器。

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

接受任意回撥函式和引數,並將其新增到回撥堆疊中。

與其他方法不同,以這種方式新增的回撥不能抑制異常(因為它們永遠不會被傳遞異常詳情)。

傳入的回撥會從函式中返回,從而允許此方法用作函式裝飾器。

pop_all()

將回調堆疊轉移到一個新的 ExitStack 例項並返回它。此操作不會呼叫任何回撥——相反,它們現在將在新堆疊關閉時被呼叫(無論是在 with 語句結束時顯式或隱式關閉)。

例如,可以按如下方式將一組檔案作為“要麼全部成功,要麼全部失敗”的操作開啟:

with ExitStack() as stack:
    files = [stack.enter_context(open(fname)) for fname in filenames]
    # Hold onto the close method, but don't call it yet.
    close_files = stack.pop_all().close
    # If opening any file fails, all previously opened files will be
    # closed automatically. If all files are opened successfully,
    # they will remain open even after the with statement ends.
    # close_files() can then be invoked explicitly to close them all.
close()

立即展開回調堆疊,按註冊順序的相反順序呼叫回撥。對於任何已註冊的上下文管理器和退出回撥,傳入的引數將表明沒有發生異常。

class contextlib.AsyncExitStack

一個 非同步上下文管理器,類似於 ExitStack,支援組合同步和非同步上下文管理器,以及用於清理邏輯的協程。

close() 方法未實現;必須使用 aclose() 代替。

async enter_async_context(cm)

類似於 ExitStack.enter_context(),但期望一個非同步上下文管理器。

在 3.11 版更改: 如果 cm 不是非同步上下文管理器,則會引發 TypeError 而不是 AttributeError

push_async_exit(exit)

類似於 ExitStack.push(),但期望一個非同步上下文管理器或一個協程函式。

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)]
    # All opened connections will automatically be released at the end of
    # the async with statement, even if attempts to open a connection
    # later in the list raise an exception.

在 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)
    # Perform operations that use the acquired resources

如圖所示,ExitStack 也使得使用 with 語句來管理那些本身不支援上下文管理協議的任意資源變得相當容易。

捕獲來自 __enter__ 方法的異常

有時需要從 __enter__ 方法實現中捕獲異常,同時*不*無意中捕獲來自 with 語句體或上下文管理器的 __exit__ 方法的異常。透過使用 ExitStack,上下文管理協議中的步驟可以稍微分開,以允許這樣做:

stack = ExitStack()
try:
    x = stack.enter_context(cm)
except Exception:
    # handle __enter__ exception
else:
    with stack:
        # Handle normal case

實際上需要這樣做可能表明底層的 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
            # The validation check passed and didn't raise an exception
            # Accordingly, we want to keep the resource, and pass it
            # back to our caller
            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):
        # We don't need to duplicate any of our resource release logic
        self.release_resource()

替代 try-finally 和旗標變數的所有用法

你有時會看到一種模式,即使用一個帶有旗標變數的 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 繼承,在單個定義中提供這兩種功能:

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()

請注意,當將上下文管理器用作函式裝飾器時,還有一個額外的限制:無法訪問 __enter__() 的返回值。如果需要該值,則仍需使用顯式的 with 語句。

參見

PEP 343 - “with” 語句

Python with 語句的規範、背景和示例。

一次性、可重用與可重入的上下文管理器

大多數上下文管理器的編寫方式意味著它們只能在 with 語句中有效使用一次。這些一次性上下文管理器每次使用時都必須重新建立——試圖第二次使用它們會觸發異常或無法正常工作。

這一常見限制意味著,通常建議在使用上下文管理器的 with 語句的頭部直接建立它們(如上述所有用法示例所示)。

檔案是有效的一次性上下文管理器的一個例子,因為第一個 with 語句將關閉檔案,從而阻止使用該檔案物件進行任何進一步的 IO 操作。

使用 contextmanager() 建立的上下文管理器也是一次性上下文管理器,如果試圖第二次使用它們,它們會抱怨底層生成器未能 yield:

>>> 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