Документация Python неофициальный перевод
Содержание страницы

29.6. contextlib – Утилиты для контекстов with-оператораcontextlib – Utilities for with-statement contexts

Исходный код: Lib/contextlib.py


Этот модуль предоставляет утилиты для типичных задач, связанных с оператором with. Дополнительную информацию см. также в разделах Типы менеджеров контекста и Менеджеры контекста оператора with.

29.6.1. УтилитыUtilities

Предоставляемые функции и классы:

@contextlib.contextmanager

Эта функция – декоратор, который можно использовать для определения фабричной функции для менеджеров контекста оператора with, без необходимости создавать класс или отдельные методы __enter__() и __exit__().

Простой пример (это не рекомендуется как реальный способ генерации HTML!):

from contextlib import contextmanager

@contextmanager
def tag(name):
    print("<%s>" % name)
    yield
    print("</%s>" % name)

>>> with tag("h1"):
...    print("foo")
...
<h1>
foo
</h1>

Декорируемая функция при вызове должна возвращать генератор-итератор. Этот итератор должен передать (yield) ровно одно значение, которое будет связано с целями в предложении as оператора with, если оно есть.

В точке, где генератор выполняет yield, выполняется блок, вложенный в оператор with. Затем генератор возобновляется после выхода из блока. Если в блоке возникает необработанное исключение, оно повторно возбуждается внутри генератора в точке, где произошёл yield. Таким образом, можно использовать конструкцию try...except...finally для перехвата ошибки (если она есть) или для обеспечения выполнения очистки. Если исключение перехватывается лишь для того, чтобы залогировать его или выполнить какое-либо действие (а не подавить его полностью), генератор должен возобновить (reraise) это исключение. В противном случае менеджер контекста-генератор сообщит оператору with, что исключение обработано, и выполнение продолжится с оператора, непосредственно следующего за оператором with.

contextmanager() использует ContextDecorator, поэтому создаваемые им менеджеры контекста можно использовать как в качестве декораторов, так и в операторах with. При использовании в качестве декоратора новый экземпляр генератора неявно создаётся при каждом вызове функции (это позволяет менеджерам контекста, созданным contextmanager(), которые в противном случае были бы «одноразовыми», удовлетворять требованию, что менеджеры контекста должны поддерживать множественные вызовы, чтобы использоваться в качестве декораторов).

Изменено в версии 3.2: Использование ContextDecorator.

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('http://www.python.org')) as page:
    for line in page:
        print(line)

без необходимости явно закрывать page. Даже если произойдёт ошибка, page.close() будет вызван при выходе из блока with.

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

Этот менеджер контекста является реентерабельным.

Новое в версии 3.4.

contextlib.redirect_stdout(new_target)

Менеджер контекста для временного перенаправления sys.stdout в другой файл или файлоподобный объект.

Этот инструмент добавляет гибкость существующим функциям или классам, чей вывод жестко привязан к stdout.

Например, вывод help() обычно отправляется в sys.stdout. Можно захватить этот вывод в строку, перенаправив его в объект io.StringIO:

f = io.StringIO()
with redirect_stdout(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.

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():
        # Выполнить действия

ContextDecorator позволяет вместо этого написать:

@cm()
def f():
    # Выполнить действия

Это даёт понять, что 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.ExitStack

Менеджер контекста, предназначенный для упрощения программного комбинирования других менеджеров контекста и функций очистки, особенно тех, которые являются необязательными или определяются входными данными.

Например, набор файлов можно легко обработать в одном операторе with следующим образом:

with ExitStack() as stack:
    files = [stack.enter_context(open(fname)) for fname in filenames]
    # Все открытые файлы будут автоматически закрыты в конце
    # оператор with, даже если попытки открыть файлы, идущие далее
    # в списке, вызывают исключение

Каждый экземпляр поддерживает стек зарегистрированных колбэков, которые вызываются в обратном порядке при закрытии экземпляра (явно или неявно в конце оператора with). Обратите внимание, что колбэки не вызываются неявно при сборке мусора экземпляра стека контекстов.

Эта модель стека используется, чтобы менеджеры контекста, которые получают свои ресурсы в методе __init__ (например, файловые объекты), могли быть обработаны корректно.

Поскольку зарегистрированные колбэки вызываются в обратном порядке регистрации, в итоге это ведёт себя так, как если бы с зарегистрированным набором колбэков использовались несколько вложенных операторов with. Это распространяется даже на обработку исключений: если внутренний колбэк подавляет или заменяет исключение, то внешние колбэки получат аргументы на основе этого обновлённого состояния.

Это относительно низкоуровневый API, который заботится о деталях корректной размотки стека колбэков выхода. Он предоставляет подходящую основу для менеджеров контекста более высокого уровня, которые манипулируют стеком выхода специфичным для приложения образом.

Новое в версии 3.3.

enter_context(cm)

Входит в новый менеджер контекста и добавляет его метод __exit__() в стек колбэков. Возвращаемое значение – результат собственного метода менеджера контекста __enter__().

Эти менеджеры контекста могут подавлять исключения так же, как если бы они использовались непосредственно как часть оператора with.

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]
    # Сохраните метод close, но пока не вызывайте его.
    close_files = stack.pop_all().close
    # Если открытие любого файла завершается неудачей, все ранее открытые файлы будут
    # закрыты автоматически. Если все файлы открыты успешно,
    # они останутся открытыми даже после завершения оператора with.
    # close_files() затем можно вызвать явно, чтобы закрыть их все.
close()

Немедленно разворачивает стек колбэков, вызывая колбэки в порядке, обратном регистрации. Для любых зарегистрированных менеджеров контекста и колбэков выхода переданные аргументы будут указывать, что исключения не произошло.

29.6.2. Примеры и рецептыExamples and Recipes

В этом разделе описаны некоторые примеры и рецепты для эффективного использования инструментов, предоставляемых модулем contextlib.

29.6.2.1. Поддержка переменного числа менеджеров контекстаSupporting a variable number of context managers

Основной вариант использования ExitStack описан в документации класса: поддержка переменного числа менеджеров контекста и других операций очистки в одном операторе with. Вариативность может быть обусловлена тем, что количество необходимых менеджеров контекста определяется пользовательским вводом (например, открытие указанной пользователем коллекции файлов), или тем, что некоторые менеджеры контекста являются необязательными:

with ExitStack() as stack:
    for resource in resources:
        stack.enter_context(resource)
    if need_special_resource():
        special = acquire_special_resource()
        stack.callback(release_special_resource, special)
    # Выполните операции, использующие полученные ресурсы

Как показано, ExitStack также позволяет довольно легко использовать операторы with для управления произвольными ресурсами, которые изначально не поддерживают протокол управления контекстом.

29.6.2.2. Упрощение поддержки одного опционального менеджера контекстаSimplifying support for single optional context managers

В частном случае одного необязательного менеджера контекста экземпляры ExitStack можно использовать как «ничего не делающий» менеджер контекста, что позволяет легко опустить менеджер контекста, не влияя на общую структуру исходного кода:

def debug_trace(details):
    if __debug__:
        return TraceContext(details)
    # В релизном режиме никаких особых действий с контекстом не выполняется
    return ExitStack()

with debug_trace():
    # В режиме отладки набор трассируется, в остальных случаях работает обычным образом

29.6.2.3. Перехват исключений из методов __enter__Catching exceptions from __enter__ methods

Иногда бывает желательно перехватывать исключения из реализации метода __enter__, не перехватывая случайно исключения из тела оператора with или метода __exit__ менеджера контекста. Используя ExitStack, можно немного разделить шаги протокола управления контекстом, чтобы добиться этого:

stack = ExitStack()
try:
    x = stack.enter_context(cm)
except Exception:
    # обработать исключение __enter__
else:
    with stack:
        # Обработка нормального случая

Необходимость в этом, вероятно, указывает на то, что базовый API должен предоставлять прямой интерфейс управления ресурсами для использования с операторами try/except/finally, но не все API хорошо спроектированы в этом отношении. Когда менеджер контекста является единственным предоставляемым API управления ресурсами, ExitStack может упростить обработку различных ситуаций, которые невозможно обработать напрямую в операторе with.

29.6.2.4. Очистка в реализации __enter__Cleaning up in an __enter__ implementation

Как отмечено в документации ExitStack.push(), этот метод может быть полезен для очистки уже выделенного ресурса, если последующие шаги в реализации __enter__() завершатся неудачей.

Вот пример реализации этого для менеджера контекста, который принимает функции захвата и освобождения ресурса, а также опциональную функцию проверки, и отображает их на протокол управления контекстом:

from contextlib import contextmanager, ExitStack

class ResourceManager:

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

    @contextmanager
    def _cleanup_on_error(self):
        with ExitStack() as stack:
            stack.push(self)
            yield
            # Проверка прошла успешно и не вызвала исключение
            # Соответственно, мы хотим сохранить ресурс и передать его
            # обратно вызывающему коду
            stack.pop_all()

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

    def __exit__(self, *exc_details):
        # Нам не нужно дублировать логику освобождения ресурсов
        self.release_resource()

29.6.2.5. Замена любого использования try-finally и флаговых переменныхReplacing any use of try-finally and flag variables

Иногда можно встретить шаблон с оператором 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(Callback, self).__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()

Из-за того, как работает протокол декоратора, функция обратного вызова, объявленная таким образом, не может принимать никаких параметров. Вместо этого любые ресурсы, которые необходимо освободить, должны быть доступны как переменные замыкания.

29.6.2.6. Использование менеджера контекста в качестве декоратора функцииUsing a context manager as a function 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: {}'.format(self.name))

    def __exit__(self, exc_type, exc, exc_tb):
        logging.info('Exiting: {}'.format(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 0343 – оператор «with»
Спецификация, предыстория и примеры для оператора with в Python.

29.6.3. Одноразовые, повторно используемые и реентерабельные менеджеры контекстаSingle use, reusable and reentrant context managers

Большинство менеджеров контекста написаны так, что их можно эффективно использовать в операторе with только один раз. Эти одноразовые менеджеры контекста должны создаваться заново каждый раз при использовании – попытка использовать их второй раз вызовет исключение или не будет работать правильно.

Это распространённое ограничение означает, что обычно рекомендуется создавать менеджеры контекста непосредственно в заголовке оператора with, где они используются (как показано во всех примерах выше).

Файлы являются примером фактически одноразовых менеджеров контекста, поскольку первый оператор with закроет файл, предотвращая любые дальнейшие операции ввода-вывода с использованием этого файлового объекта.

Контекстные менеджеры, созданные с помощью 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

29.6.3.1. Реентерабельные менеджеры контекстаReentrant context managers

Более сложные контекстные менеджеры могут быть «реентерабельными». Такие контекстные менеджеры можно не только использовать в нескольких инструкциях with, но и внутри инструкции with, которая уже использует тот же контекстный менеджер.

threading.RLock является примером реентерабельного контекстного менеджера, как и suppress() и redirect_stdout(). Вот очень простой пример реентерабельного использования:

>>> 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 к другому потоку данных.

29.6.3.2. Повторно используемые менеджеры контекстаReusable context managers

Отличающиеся от одноразовых и реентерабельных менеджеров контекста – «многократно используемые» менеджеры контекста (или, если быть совсем точным, «многократно используемые, но не реентерабельные» менеджеры контекста, поскольку реентерабельные менеджеры контекста также являются многократно используемыми). Эти менеджеры контекста поддерживают многократное использование, но завершатся ошибкой (или иным образом будут работать некорректно), если конкретный экземпляр менеджера контекста уже был использован во внешней инструкции 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