contextlib
— Утилиты для контекстов with
-операторов¶
Исходный код: Lib/contextlib.py
Этот модуль предоставляет утилиты для выполнения общих задач, связанных с оператором with
. Для получения дополнительной информации смотрите также Типы контекстных менеджеров и С помощью контекстных менеджеров заявлений.
Коммунальные услуги¶
Предоставляемые функции и классы:
- class contextlib.AbstractContextManager¶
abstract base class для классов, реализующих
object.__enter__()
иobject.__exit__()
. Предусмотрена реализация по умолчанию дляobject.__enter__()
, которая возвращаетself
, в то время какobject.__exit__()
является абстрактным методом, который по умолчанию возвращаетNone
. Смотрите также определение Типы контекстных менеджеров.Добавлено в версии 3.6.
- class contextlib.AbstractAsyncContextManager¶
abstract base class для классов, реализующих
object.__aenter__()
иobject.__aexit__()
. Предусмотрена реализация по умолчанию дляobject.__aenter__()
, которая возвращаетself
, в то время какobject.__aexit__()
является абстрактным методом, который по умолчанию возвращаетNone
. Смотрите также определение Асинхронные контекстные менеджеры.Добавлено в версии 3.7.
- @contextlib.contextmanager¶
Эта функция представляет собой decorator, которая может быть использована для определения заводской функции для
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
Оформляемая функция при вызове должна возвращать generator-итератор. Этот итератор должен выдавать ровно одно значение, которое будет привязано к целям в предложении
with
оператораas
, если таковые имеются.В момент, когда генератор выдает результат, выполняется блок, вложенный в инструкцию
with
. Затем генератор возобновляет работу после завершения работы с блоком. Если в блоке возникает необработанное исключение, оно генерируется внутри генератора в точке, где произошел сбой. Таким образом, вы можете использовать операторtry
…except
…finally
, чтобы перехватить ошибку (если таковая имеется) или обеспечить некоторую очистку. Если исключение перехватывается только для того, чтобы зарегистрировать его или выполнить какое-либо действие (а не для его полного подавления), генератор должен повторно запустить это исключение. В противном случае диспетчер контекста генератора укажет операторуwith
, что исключение обработано, и выполнение возобновится с оператором, непосредственно следующим за операторомwith
.contextmanager()
используетContextDecorator
, поэтому создаваемые им контекстные менеджеры можно использовать как декораторы, а также вwith
инструкциях. При использовании в качестве декоратора новый экземпляр генератора неявно создается при каждом вызове функции (это позволяет «одноразовым» контекстным менеджерам, созданнымcontextmanager()
, соответствовать требованию о том, чтобы контекстные менеджеры поддерживали множественные вызовы для использования в качестве декораторов).Изменено в версии 3.2: Используйте
ContextDecorator
.
- @contextlib.asynccontextmanager¶
Аналогично
contextmanager()
, но создает asynchronous context manager.Эта функция представляет собой decorator, которая может быть использована для определения заводской функции для асинхронных контекстных менеджеров инструкций
async with
без необходимости создавать класс или отдельные методы__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 ...')
Добавлено в версии 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
. Даже если произойдет ошибка,page.close()
будет вызван при выходе из блокаwith
.Примечание
Большинство типов, управляющих ресурсами, поддерживают протокол context manager, который закрывает 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
Он также может быть использован в качестве замены для asynchronous context managers:
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: 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
Этот контекстный менеджер называется reentrant.
Добавлено в версии 3.4.
- contextlib.redirect_stdout(new_target)¶
Контекстный менеджер для временного перенаправления
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
означает, что этот контекстный менеджер не подходит для использования в библиотечном коде и большинстве многопоточных приложений. Он также не влияет на вывод подпроцессов. Однако, это все еще полезный подход для многих служебных скриптов.Этот контекстный менеджер называется reentrant.
Добавлено в версии 3.4.
- contextlib.redirect_stderr(new_target)¶
Аналогично
redirect_stdout()
, но перенаправляетsys.stderr
на другой файл или файлоподобный объект.Этот контекстный менеджер называется reentrant.
Добавлено в версии 3.5.
- contextlib.chdir(path)¶
Непараллельно-безопасный контекстный менеджер для изменения текущего рабочего каталога. Поскольку при этом изменяется глобальное состояние рабочего каталога, он не подходит для использования в большинстве многопоточных или асинхронных контекстов. Он также не подходит для выполнения большинства нелинейных программ, таких как генераторы, где выполнение программы временно прекращается - если это явно не требуется, вы не должны уступать, когда этот контекстный менеджер активен.
Это простая оболочка для
chdir()
, она изменяет текущий рабочий каталог при входе и восстанавливает старый при выходе.Этот контекстный менеджер называется reentrant.
Добавлено в версии 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, который заботится о деталях корректного развертывания стека обратных вызовов exit. Он обеспечивает подходящую основу для контекстных менеджеров более высокого уровня, которые управляют стеком exit специфичными для приложения способами.
Добавлено в версии 3.3.
- enter_context(cm)¶
Вводит новый контекстный менеджер и добавляет его метод
__exit__()
в стек обратного вызова. Возвращаемое значение является результатом работы собственного метода контекстного менеджера__enter__()
.Эти контекстные менеджеры могут подавлять исключения точно так же, как они обычно это делают, если используются непосредственно как часть инструкции
with
.Изменено в версии 3.11: Вызывает
TypeError
вместоAttributeError
, если cm не является контекстным менеджером.
- push(exit)¶
Добавляет метод контекстного менеджера
__exit__()
в стек обратного вызова.Поскольку
__enter__
не вызывается, этот метод может быть использован для покрытия части реализации__enter__()
собственным методом__exit__()
контекстного менеджера.Если передается объект, который не является контекстным менеджером, этот метод предполагает, что это обратный вызов с той же сигнатурой, что и метод контекстного менеджера
__exit__()
, и добавляет его непосредственно в стек обратного вызова.Возвращая значения true, эти обратные вызовы могут подавлять исключения точно так же, как это делают методы context manager
__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¶
asynchronous context manager, аналогичный
ExitStack
, который поддерживает объединение как синхронных, так и асинхронных контекстных менеджеров, а также содержит сопрограммы для логики очистки.Метод
close()
не реализован; вместо него необходимо использоватьaclose()
.- coroutine enter_async_context(cm)¶
Аналогично
ExitStack.enter_context()
, но ожидает использования асинхронного контекстного менеджера.Изменено в версии 3.11: Вызывает
TypeError
вместоAttributeError
, если cm не является асинхронным контекстным менеджером.
- push_async_exit(exit)¶
Аналогично
ExitStack.push()
, но ожидает либо асинхронный контекстный менеджер, либо функцию сопрограммы.
- push_async_callback(callback, /, *args, **kwds)¶
Аналогично
ExitStack.callback()
, но ожидает функцию сопрограммы.
- coroutine 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
с переменной flag, указывающей, следует ли выполнять тело предложения 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()
Это позволяет заранее указать предполагаемое поведение при очистке, вместо того чтобы требовать отдельной переменной flag.
Если конкретное приложение часто использует этот шаблон, его можно еще больше упростить с помощью небольшого вспомогательного класса:
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()
Из-за того, как работает протокол decorator, функция обратного вызова, объявленная таким образом, не может принимать никаких параметров. Вместо этого доступ к любым ресурсам, которые должны быть освобождены, должен осуществляться как к переменным закрытия.
Использование контекстного менеджера в качестве декоратора функций¶
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
Как видно из выходных данных примера, повторное использование одного объекта stack в нескольких операторах 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