Является ли Python * с * оператором в точности эквивалентным блоку try - (кроме) - finally?
Я знаю, что это широко обсуждалось, но я все еще не могу найти ответ, чтобы подтвердить это: является ли оператор with идентичным вызову того же кода в блоке try - (кроме) -finally, где все, что определено в __exit__
функция контекстного менеджера находится в блоке finally?
Например, эти 2 фрагмента кода делают одно и то же?
import sys
from contextlib import contextmanager
@contextmanager
def open_input(fpath):
fd = open(fpath) if fpath else sys.stdin
try:
yield fd
finally:
fd.close()
with open_input("/path/to/file"):
print "starting to read from file..."
такой же как:
def open_input(fpath):
try:
fd = open(fpath) if fpath else sys.stdin
print "starting to read from file..."
finally:
fd.close()
open_input("/path/to/file")
Спасибо!
1 ответ
Я собираюсь отбросить упоминания о сфере применения, потому что это действительно не очень актуально.
Согласно PEP 343,
with EXPR as VAR:
BLOCK
переводит на
mgr = (EXPR)
exit = type(mgr).__exit__ # Not calling it yet
value = type(mgr).__enter__(mgr)
exc = True
try:
try:
VAR = value # Only if "as VAR" is present
BLOCK
except:
# The exceptional case is handled here
exc = False
if not exit(mgr, *sys.exc_info()):
raise
# The exception is swallowed if exit() returns true
finally:
# The normal and non-local-goto cases are handled here
if exc:
exit(mgr, None, None, None)
Как вы видете, type(mgr).__enter__
называется, как вы ожидаете, но не внутри try
,
type(mgr).__exit__
называется на выходе. Единственное отличие состоит в том, что когда есть исключение, if not exit(mgr, *sys.exc_info())
путь взят. Это дает with
способность анализировать и заглушать ошибки в отличие от того, что finally
оговорка может сделать.
contextmanager
это не сильно усложняет. Это просто:
def contextmanager(func):
@wraps(func)
def helper(*args, **kwds):
return _GeneratorContextManager(func, *args, **kwds)
return helper
Затем посмотрите на рассматриваемый класс:
class _GeneratorContextManager(ContextDecorator):
def __init__(self, func, *args, **kwds):
self.gen = func(*args, **kwds)
def __enter__(self):
try:
return next(self.gen)
except StopIteration:
raise RuntimeError("generator didn't yield") from None
def __exit__(self, type, value, traceback):
if type is None:
try:
next(self.gen)
except StopIteration:
return
else:
raise RuntimeError("generator didn't stop")
else:
if value is None:
value = type()
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopIteration as exc:
return exc is not value
except:
if sys.exc_info()[1] is not value:
raise
Неважный код был исключен.
Первое, что следует отметить, что если есть несколько yield
s, этот код будет ошибкой
Это не влияет заметно на поток управления.
Рассматривать __enter__
,
try:
return next(self.gen)
except StopIteration:
raise RuntimeError("generator didn't yield") from None
Если менеджер контекста был хорошо написан, это никогда не сломается от того, что ожидается.
Разница в том, что если генератор бросает StopIteration
, другая ошибка ( RuntimeError
) будет произведено. Это означает, что поведение не полностью идентично нормальному with
если вы используете совершенно произвольный код.
Рассмотрим без ошибок __exit__
:
if type is None:
try:
next(self.gen)
except StopIteration:
return
else:
raise RuntimeError("generator didn't stop")
Единственная разница, как и раньше; если ваш код выдает StopIteration
, это повлияет на генератор и, следовательно, contextmanager
Декоратор будет неправильно истолковывать это.
Это означает, что:
from contextlib import contextmanager
@contextmanager
def with_cleanup(func):
try:
yield
finally:
func()
def good_cleanup():
print("cleaning")
with with_cleanup(good_cleanup):
print("doing")
1/0
#>>> doing
#>>> cleaning
#>>> Traceback (most recent call last):
#>>> File "", line 15, in <module>
#>>> ZeroDivisionError: division by zero
def bad_cleanup():
print("cleaning")
raise StopIteration
with with_cleanup(bad_cleanup):
print("doing")
1/0
#>>> doing
#>>> cleaning
Что вряд ли имеет значение, но могло бы.
В заключение:
else:
if value is None:
value = type()
try:
self.gen.throw(type, value, traceback)
raise RuntimeError("generator didn't stop after throw()")
except StopIteration as exc:
return exc is not value
except:
if sys.exc_info()[1] is not value:
raise
Это поднимает тот же вопрос о StopIteration
Но интересно отметить, что последняя часть.
if sys.exc_info()[1] is not value:
raise
Это означает, что если исключение не обработано, трассировка не будет изменена. Если он был обработан, но существует новая трассировка, он будет повышен.
Это идеально соответствует спецификации.
TL; DR
with
на самом деле немного мощнее, чемtry...finally
в этомwith
может самоанализ и заглушить ошибки.Будьте осторожны
StopIteration
, но в противном случае вы в порядке, используя@contextmanager
создавать контекстные менеджеры.