Является ли оператор Python *with* точно эквивалентным блоку try - (except) - finally?

Я знаю, что это широко обсуждалось, но я до сих пор не могу найти ответ, подтверждающий это: является ли оператор with идентичным вызову того же кода в блоке try - (except) -finally, где что угодно один определяет в функции __exit__ контекстного менеджера, помещается в блок finally?

Например, эти два фрагмента кода делают одно и то же?

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")

Спасибо!


person Clara    schedule 29.09.2014    source источник
comment
вы получите две разные ошибки, диспетчер контекста выдаст ошибку ввода-вывода по неверному пути, а try/finally выдаст ошибку referenced before assignment   -  person Padraic Cunningham    schedule 29.09.2014
comment
И что такое функция __exit__ в вашем коде?   -  person Padraic Cunningham    schedule 29.09.2014
comment
@PadraicCunningham Менеджер контекста использует генератор (функцию с yield) для создания блоков __enter__ и __exit__.   -  person Dunes    schedule 29.09.2014
comment
@Dunes, я понимаю, как работают контекстные менеджеры, я спрашивал у ОП, что они считают функцией выхода.   -  person Padraic Cunningham    schedule 29.09.2014


Ответы (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, этот код выдаст ошибку.

Это не влияет заметно на поток управления.

Рассмотрим __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 для создания менеджеров контекста.

person Veedrac    schedule 29.09.2014
comment
Требуется ли вложенный try? Это эквивалентно использованию except: if not exit(mgr, *sys.exc_info()): raise else: exit(mgr, None, None, None)? Кроме того, почему exit = type(mgr).__exit__ вместо exit = mgr.__exit__? Приор кажется странно специфичным. Тем не менее, это похоже на то, что использует with: если вы делаете foo.__enter__ = lambda 'asdf', а затем делаете with foo as x: print x, он не использует 'asdf'. - person Ben; 22.05.2015
comment
@Ben Я думаю, ты прав, что try не нужно вкладывать, но тем не менее тебе нужен finally. exit = type(mgr).__exit__ используется, потому что CPython имеет историю кэширования специальных методов, и это просто делает его согласованным. - person Veedrac; 22.05.2015
comment
Разве поведение finally не охватывалось частью else поведения try/except/else? Или я что-то упускаю? - person Ben; 22.05.2015
comment
Я понимаю. Рад, что спросил :-) - person Ben; 22.05.2015
comment
Очень хорошо написанная статья, которая подчеркивает небольшие различия (которые редко проявляются) между ними, также может быть найдена здесь для тех, кто заинтересован. - person Dimitris Fasarakis Hilliard; 29.06.2017
comment
@Ben Спасибо за вопрос finally, мне было интересно то же самое здесь: stackoverflow.com/q/59322585/2326961 - person Maggyero; 13.12.2019
comment
@Veedrac Большое спасибо за этот подробный ответ, это помогает! Однако почему внешний оператор try? Не могли бы вы просто поместить предложение finally во внутренний оператор try? - person Maggyero; 13.12.2019
comment
@Veedrac Вот ответ: stackoverflow.com/a/59344104/2326961 - person Maggyero; 15.12.2019