contextlib — Вспомогательные средства для контекстов оператора with
Исходный код: Lib/contextlib.py
Этот модуль предоставляет вспомогательные средства для распространённых задач, связанных с оператором with. Дополнительные сведения см. также в разделах Типы менеджеров контекста и Менеджеры контекста оператора with.
Вспомогательные средства
Предоставляемые функции и классы:
-
class contextlib.AbstractContextManager -
Абстрактный базовый класс для классов, реализующих
__enter__()и__exit__(). Предоставляется реализация по умолчанию для__enter__(), которая возвращаетself, тогда как__exit__()является абстрактным методом, который по умолчанию возвращаетNone. См. также определение типов менеджеров контекста.Добавлено в версии 3.6.
-
class contextlib.AbstractAsyncContextManager -
Абстрактный базовый класс для классов, реализующих
__aenter__()и__aexit__(). Предоставляется реализация по умолчанию для__aenter__(), которая возвращаетself, тогда как__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
Декорируемая функция при вызове должна возвращать генератор-итератор. Этот итератор должен выдавать ровно одно значение, которое будет присвоено целям в предложении
asоператораwith, если оно есть.В момент, когда генератор выдаёт значение, выполняется блок, вложенный в оператор
with. После выхода из блока выполнение генератора возобновляется. Если в блоке возникает необработанное исключение, оно повторно возбуждается внутри генератора в точке, где произошло yield. Таким образом, можно использовать операторtry…except…finally, чтобы перехватить ошибку (если она возникла) или обеспечить выполнение очистки. Если исключение перехватывается лишь для записи в журнал или выполнения какого-либо действия (а не для его полного подавления), генератор должен повторно возбудить это исключение. В противном случае менеджер контекста-генератор сообщит операторуwith, что исключение обработано, и выполнение продолжится с оператора, следующего непосредственно за операторомwith.@contextmanagerиспользуетContextDecorator, поэтому созданные ею менеджеры контекста можно применять как декораторы, а также в операторахwith. При использовании в качестве декоратора новый экземпляр генератора неявно создаётся при каждом вызове функции (это позволяет созданным с помощью@contextmanagerодноразовым менеджерам контекста соответствовать требованию многократного вызова, необходимому для использования в качестве декораторов).Изменено в версии 3.2: использование
ContextDecorator.
-
@contextlib.asynccontextmanager -
Аналогична
@~contextlib.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://www.python.org')) as page: for line in page: print(line)не закрывая
pageявно. Даже если возникает ошибка, при выходе из блока оператораwithбудет вызванpage.close().Примечание
Большинство типов, управляющих ресурсами, поддерживают протокол менеджера контекста, который закрывает thing при выходе из оператора
with. Поэтомуclosing()особенно полезен для сторонних типов, не поддерживающих менеджеры контекста. Этот пример приведён исключительно для иллюстрации, посколькуurlopen()обычно используется в менеджере контекста.
-
contextlib.aclosing(thing) -
Возвращает асинхронный менеджер контекста, который по завершении блока вызывает метод
aclose()объекта thing. Это в основном эквивалентно следующему:from contextlib import asynccontextmanager @asynccontextmanager async def aclosing(thing): try: yield thing finally: await thing.aclose()Важно, что
aclosing()обеспечивает детерминированную очистку асинхронных генераторов, если они завершаются досрочно из-заbreakили исключения. Например: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_result из
__enter__(), но в остальном ничего не делает. Он предназначен для использования вместо необязательного менеджера контекста, например: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__сохраняет возможность обработки исключений, даже если используется как декоратор.ContextDecoratorиспользуется функцией@contextmanager, поэтому эта функциональность доступна автоматически.Пример использования
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как класс-миксин: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__не вызывается, этот метод можно использовать, чтобы охватить часть реализации__enter__()собственным методом__exit__()менеджера контекста.Если передан объект, не являющийся менеджером контекста, этот метод считает его обратным вызовом с той же сигнатурой, что и у метода
__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.
Одноразовые, повторно используемые и реентерабельные менеджеры контекста
Большинство менеджеров контекста устроены так, что их можно эффективно использовать в операторе with только один раз. Такие одноразовые менеджеры контекста необходимо создавать заново при каждом использовании: повторная попытка использования вызовет исключение или приведёт к некорректной работе.
Из-за этого распространённого ограничения обычно рекомендуется создавать менеджеры контекста непосредственно в заголовке оператора with, в котором они используются (как показано во всех приведённых выше примерах).
Файлы — пример фактически одноразовых менеджеров контекста: первый оператор with закрывает файл, препятствуя дальнейшим операциям ввода-вывода с этим файловым объектом.
Менеджеры контекста, созданные с помощью @contextmanager, также являются одноразовыми. При повторной попытке использования они сообщат об ошибке, если генератор, лежащий в их основе, не вернул значение:
>>> 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
© 2001 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.14/library/contextlib.html