contextlibwith 語句上下文的實用工具

原始碼: 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 語句中使用,但有時需要管理一個資源,它本身不是上下文管理器,並且沒有實現 close() 方法以與 contextlib.closing 一起使用。

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

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

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

在生成器產生值的位置,執行巢狀在 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 類來擴充套件已經有基類的現有上下文管理器:

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__() 方法相同簽名的回撥,並將其直接新增到回撥堆疊。

透過返回 true 值,這些回撥可以像上下文管理器 __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()

coroutine 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(),但期望一個協程函式。

coroutine aclose()

類似於 ExitStack.close(),但能正確處理 awaitables。

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