Изявлението *with* на Python точно еквивалентно ли е на блок try - (except) - finally?

Знам, че това беше широко обсъждано, но все още не мога да намеря отговор, за да потвърдя това: изразът with идентичен ли е с извикването на същия код в блок try - (except) -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")

Благодаря!


person Clara    schedule 29.09.2014    source източник
comment
ще получите две различни грешки, контекстният мениджър ще даде io грешка на невалиден път и try/finally ще даде referenced before assignment грешка   -  person Padraic Cunningham    schedule 29.09.2014
comment
Също така каква е функцията __exit__ във вашия код?   -  person Padraic Cunningham    schedule 29.09.2014
comment
@PadraicCunningham Контекстният мениджър използва генератора (функция с добив), за да създаде блоковете __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

Неважният код е премахнат.

Първото нещо, което трябва да се отбележи е, че ако има няколко yields, този код ще доведе до грешка.

Това не се отразява забележимо на контролния поток.

Помислете за __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