Оператор with
в Python поддерживает концепцию контекста среды выполнения, определенного контекстным менеджером. Типичные области применения контекстных менеджеров включают сохранение и восстановление различных типов глобального состояния, блокировку и разблокировку ресурсов, закрытие открытых файлов и т. д.
with
;with
:with EXPRESSION as TARGET: SUITE
Семантически эквивалентен:
manager = (EXPRESSION) enter = type(manager).__enter__ exit = type(manager).__exit__ value = enter(manager) hit_except = False try: TARGET = value SUITE except: hit_except = True if not exit(manager, *sys.exc_info()): raise finally: if not hit_except: exit(manager, None, None, None)
Выражение EXPRESSION
, непосредственно следующее за ключевым словом with
является "выражением контекста", так как это выражение обеспечивает основной ключ к среде выполнения, которую менеджер контекста устанавливает для продолжительности тела выражения.
with
:EXPRESSION
) оценивается для получения менеджера контекста.__enter__()
для последующего использования.__exit__()
для последующего использования.__enter__()
.TARGET
была включена в оператор with
, то ей присваивается возвращаемое значение из метода __enter__()
.with
гарантирует, что если метод __enter__()
возвращается без ошибки, то всегда будет вызываться метод __exit__()
. Таким образом, если ошибка возникает во время присваивания значения через оператор as
, то она будет обрабатываться так же, как и ошибка, возникающая внутри with
.__exit__()
. Если исключение вызвало выход из последовательности команд, то его тип exc_type
, значение exc_val
и информация о трассировке exc_tb
передаются в качестве аргументов __exit__()
. В противном случае предоставляется три аргумента None
.Если последовательность команд была завершена из-за исключения, а возвращаемое значение из метода __exit__()
было False
, то исключение вызывается повторно. Если возвращаемое значение было True
, то исключение подавляется и выполнение продолжается с оператора, следующего за оператором with
.
Если последовательность команд была завершена по любой причине, кроме исключения, то возвращаемое значение из __exit__()
игнорируется, и выполнение продолжается.
При наличии нескольких контекстных менеджеров, они обрабатываются так, как если бы несколько операторов with
были вложенными:
with A() as a, B() as b: SUITE # Эквивалентно with A() as a: with B() as b: SUITE
С версии Python 3.10 поддерживается использование круглых скобок для написания нескольких диспетчеров. Это позволяет форматировать длинную коллекцию диспетчеров контекста в несколько строк аналогично тому, как это можно с операторами импорта. Например, теперь действительны все эти примеры:
with (CtxManager() as example): ... with ( CtxManager1(), CtxManager2() ): ... with (CtxManager1() as example, CtxManager2()): ... with (CtxManager1(), CtxManager2() as example): ... with ( CtxManager1() as example1, CtxManager2() as example2 ): ...
Допускается использовать конечную запятую в конце заключенной группы:
with ( CtxManager1() as example1, CtxManager2() as example2, CtxManager3() as example3, ): ...
Протокол контекстных менеджеров реализован с помощью пары методов, которые позволяют определяемым пользователем классам определять контекст среды выполнения, который вводится до выполнения тела инструкции и завершается при завершении инструкции:
contextmanager.__enter__()
:Метод contextmanager.__enter__()
вводит контекст среды выполнения и возвращает либо себя, либо другой объект, связанный с контекстом среды выполнения. Значение, возвращаемое этим методом, привязывается к идентификатору в предложении as
оператора with
, использующего этот контекстный менеджер.
Ярким примером контекстного менеджера, который возвращает себя, является объект file
. Файловые объекты возвращают себя из __enter__()
, чтобы разрешить использование встроенной функции open()
в качестве контекстного выражения в операторе with
.
with open('/etc/passwd') as fp: for line in fp: print line.rstrip()
contextmanager.__exit__(exc_type, exc_val, exc_tb)
:Метод contextmanager.__exit__()
предоставляет выход из контекста среды выполнения и возвращает логический флаг, указывающий, следует ли подавлять любое возникшее исключение. При возникновении исключения во время выполнения тела оператора with
, аргументы содержат тип исключения exc_type
, значение exc_val
и информацию о трассировке exc_tb
. В противном случае все три аргумента - это None
.
Если у метода contextmanager.__exit__()
установить возвращаемое значение в return True
, то это приведет к тому, что оператор with
будет подавлять возникающие исключения внутри себя и продолжит выполнение с оператора, непосредственно следующим за оператором with
. В противном случае исключение exc_type
продолжает распространяться после завершения выполнения этого метода. Исключения, возникающие во время выполнения этого метода, заменят все исключения, возникшие в теле оператора with
.
Передаваемое исключение exc_type
никогда не следует повторно вызывать явно, вместо этого метод contextmanager.__exit__()
должен возвращать return False
, чтобы указать, что метод завершился успешно и не хочет подавлять возникшее исключение. Это позволяет коду управления контекстом легко определять, действительно ли метод contextmanager.__exit__()
потерпел неудачу.
Пример работы контекстного менеджера:
# файл test_with.py class Example: def __enter__(self): print("enter") def __exit__(self, exc_type, exc_val, exc_tb): print("exit")
Запускаем: python3 -i test_with.py
>>> with Example(): ... print("Python!") # enter # Python! # exit
Также смотрите материал "Создание собственного менеджера контекста".
Полезный контекстный менеджер, который временно изменяет значение переменной среды environ
:
# файл test.py import os class SetEnv(): def __init__(self, var_name, new_value): # переменные инициализации менеджера контекста self.var_name = var_name self.new_value = new_value def __enter__(self): # при входе в менеджер # получаем и сохраняем оригинальное значение `environ` self.original_value = os.environ.get(self.var_name) # применяем новое значение `environ` os.environ[self.var_name] = self.new_value def __exit__(self, exc_type, exc_val, exc_tb): # при выходе из менеджера if self.original_value is None: # удаляем новое значение `environ` del os.environ[self.var_name] else: # применяем сохраненное (старое) значение `environ` os.environ[self.var_name] = self.original_value
Запускаем файл примера в интерактивном режиме: python3 -i test.py
>>> os.environ["USER"] # 'test' (у вас будет своё значение) # запускаем менеджер контекста >>> with SetEnv("USER", "super"): ... print("USER env from `with`:", os.environ["USER"]) # USER env from `with`: super # смотрим `environ` после выхода из блока `with` >>> os.environ["USER"] # 'test'
Поддержка упрощенного создания менеджеров контекста предоставляется модулем contextlib
.
Многие контекстные менеджеры, например, файлы и контексты на основе генераторов будут одноразовыми объектами. После вызова метода __exit__()
менеджер контекста больше не будет находиться в работоспособном состоянии (например, файл был закрыт или базовый генератор завершил выполнение).
Необходимость создания нового объекта менеджера контекста для каждого оператора with
- это самый простой способ избежать проблем с многопоточным кодом и вложенными операторами, пытающимися использовать один и тот же контекстный менеджер. Не случайно, что все стандартные менеджеры контекста модуля contextlib
, поддерживающие повторное использование, происходят из модуля threading
и все они уже разработаны для решения проблем, создаваемых потоковым и вложенным использованием.
Это означает, что для сохранения менеджера контекста с определенными аргументами инициализации, которые будут использоваться в нескольких операторах with
, как правило, необходимо будет сохранить его в вызываемом объекте с нулевым аргументом, который затем вызывается в выражении контекста каждого оператора, а не кэшировать непосредственно менеджер контекста. Если это ограничение не применяется, это должно быть ясно указано в документации соответствующего контекстного менеджера.
@contextmanager def locked(lock): lock.acquire() try: yield finally: lock.release()
Использование:
with locked(myLock): # Здесь код выполняется с удержанным myLock. # lock.release() гарантированно будет выполнен, когда блок # будет завершен (даже по необработанному исключению).
@contextmanager def opened(filename, mode="r"): f = open(filename, mode) try: yield f finally: f.close()
Использование:
with opened("/etc/passwd") as f: for line in f: print(line.rstrip())
@contextmanager def transaction(db): db.begin() try: yield None except: db.rollback() raise else: db.commit()
@contextmanager def stdout_redirected(new_stdout): save_stdout = sys.stdout sys.stdout = new_stdout try: yield None finally: sys.stdout = save_stdout
Использование:
with opened(filename, "w") as f: with stdout_redirected(f): print "Hello world"
open()
, который также возвращает условие ошибки:@contextmanager def opened_w_error(filename, mode="r"): try: f = open(filename, mode) except IOError, err: yield None, err else: try: yield f, None finally: f.close()
Использование:
with opened_w_error("/etc/passwd", "a") as (f, err): if err: print "IOError:", err else: f.write("guido::0:0::/:/bin/sh\n")