Вилов винятку під час використання оператора Python 'with'


293

На жаль, я не можу зрозуміти, як обробити виняток для оператора python 'with'. Якщо у мене є код:

with open("a.txt") as f:
    print f.readlines()

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

with open("a.txt") as f:
    print f.readlines()
except:
    print 'oops'

і не можу писати

with open("a.txt") as f:
    print f.readlines()
else:
    print 'oops'

додавання "з" до оператора спробу / крім іншого не працює: виняток не підвищується. Що я можу зробити, щоб обробити відмову всередині оператора "з" пітонічним способом?


Що ви маєте на увазі, що "додавання" до "в операції спробувати / за винятком не працює інше: виняток не підвищується" ? withЗаява не чарівно розірвати огороджувальну try...exceptзаяву.
Аран-Фей

4
Цікаво, примірочних з-ресурсів заяву в Java робить підтримку саме цей випадок використання ви хочете. docs.oracle.com/javase/tutorial/essential/exceptions/…
Наюкі

Відповіді:


256
from __future__ import with_statement

try:
    with open( "a.txt" ) as f :
        print f.readlines()
except EnvironmentError: # parent of IOError, OSError *and* WindowsError where available
    print 'oops'

Якщо ви хочете по-різному звертатися за помилками від відкритого виклику та робочого коду, ви можете зробити:

try:
    f = open('foo.txt')
except IOError:
    print('error')
else:
    with f:
        print f.readlines()

3
Як зазначається в stackoverflow.com/questions/5205811/… , блок спроб тут справді занадто широкий. Не створюється різниця між винятками під час створення контекстного менеджера та тих, що знаходяться в тілі оператора з оператором, тому це може бути неправдивим рішенням для всіх випадків використання.
ncoghlan

@ncoghlan Але ви можете додати зайві try...exceptблоки всередину, withщоб бути ближче до джерела винятку, який не має нічого спільного open().
rbaleksandar

1
@rbaleksandar Якщо я добре пам’ятаю, мій коментар суворо посилався на перший приклад у відповіді, де весь із заявою знаходиться всередині блоку спробу / за винятком блоку (так що навіть якщо у вас є внутрішні блоки спробувати / очікуйте, будь-які винятки, які дозволяють уникнути, будуть все одно потрапив у зовнішній). Згодом Дуглас додав другий приклад для вирішення випадків, коли це розрізнення має значення.
ncoghlan

3
Чи закриється файл у цьому прикладі? Я запитую, тому що він був відкритий поза рамками "з".
Майк Коллінз

6
@MikeCollins Вихід із пункту "з" закриє відкритий файл, навіть коли файл відкритий перед "з".
користувач7938784

75

Найкращий "пітонічний" спосіб зробити це, використовуючи withвисловлювання, вказаний як Приклад №6 в PEP 343 , який дає передумови твердження.

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

38
Мені це подобається, але мені здається, що це занадто багато чорної магії. Це не зовсім явно для читача
Пол Зеєб

5
@PaulSeeb Чому б ти не визначив це і не врятував себе від цього кожного разу, коли потрібно? Він визначений на вашому додатку, і він такий же магічний, як і будь-який інший менеджер контексту. Я думаю, що хтось, що використовує оператор with, зрозумів би це (ім'я функції також може бути більш виразним, якщо вам це не подобається). Сам оператор "з" був розроблений таким чином, щоб визначити "захищений" блок коду та делегувати функції перевірки менеджерам контексту (щоб зробити код більш зрозумілим).

9
Вся ця неприємність лише для того, щоб не записати остаточний блок в код користувача. Я починаю вважати, що всі ми страждаємо на тривалий симптом на висловлювання з заявою.
jgomo3

1
Найкращий спосіб обробляти винятки в python - це записати функцію, яка захоплює та повертає їх? Серйозно? Пітонічний спосіб обробляти винятки - використовувати try...exceptвисловлювання.
Аран-Фей

58

Вилов винятку під час використання оператора Python 'with'

Оператор with доступний без __future__імпорту з Python 2.6 . Ви можете отримати його вже на Python 2.5 (але на цей час прийшов час оновлення!) За допомогою:

from __future__ import with_statement

Ось найближче, що слід виправити. Ви майже там, але withне має exceptзастереження:

with open("a.txt") as f: 
    print(f.readlines())
except:                    # <- with doesn't have an except clause.
    print('oops')

Метод контекстного менеджера __exit__, якщо він повертається False, повторно оцінить помилку після її завершення. Якщо він повернеться True, він придушить його. openВбудоване це __exit__; не повертається True, так що вам просто потрібно гніздо його в спробі, за винятком блоку:

try:
    with open("a.txt") as f:
        print(f.readlines())
except Exception as error: 
    print('oops')

І стандартний котлоагрегат: не використовуйте голий, except:який ловить, BaseExceptionі будь-який інший можливий виняток та попередження. Будьте принаймні настільки ж конкретними, як Exceptionі за цю помилку, можливо, зловити IOError. Лише помилки, які ви готові впоратися.

Отже, у цьому випадку ви зробите:

>>> try:
...     with open("a.txt") as f:
...         print(f.readlines())
... except IOError as error: 
...     print('oops')
... 
oops

2

Розмежування між можливими джерелами винятків, зібраних із складеного withтвердження

Розрізняти винятки, які трапляються у withвисловлюванні, є складним, оскільки вони можуть виникнути в різних місцях. Винятки можна збільшити з будь-якого з наступних місць (або функцій, що називаються в ньому):

  • ContextManager.__init__
  • ContextManager.__enter__
  • тіло with
  • ContextManager.__exit__

Докладніше див. Документацію про типи диспетчерів контекстів .

Якщо ми хочемо розрізнити між цими різними випадками, достатньо лише withперетворити їх у форму try .. except. Розглянемо наступний приклад (використовуючи ValueErrorяк приклад, але, звичайно, його можна замінити будь-яким іншим винятком):

try:
    with ContextManager():
        BLOCK
except ValueError as err:
    print(err)

Тут exceptволя буде виловлювати винятки, що виникають у всіх чотирьох різних місцях, і, таким чином, не дозволяє розрізняти їх. Якщо ми переміщаємо екземпляр об'єкта контексту менеджера за межами with, ми можемо розрізняти __init__і BLOCK / __enter__ / __exit__:

try:
    mgr = ContextManager()
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        with mgr:
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
    except ValueError as err:
        # At this point we still cannot distinguish between exceptions raised from
        # __enter__, BLOCK, __exit__ (also BLOCK since we didn't catch ValueError in the body)
        pass

Ефективно це лише допомогло з __init__частиною, але ми можемо додати додаткову змінну сторожу, щоб перевірити, чи почалося тіло withзапущеного (тобто розмежування між __enter__іншими):

try:
    mgr = ContextManager()  # __init__ could raise
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        entered_body = False
        with mgr:
            entered_body = True  # __enter__ did not raise at this point
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
    except ValueError as err:
        if not entered_body:
            print('__enter__ raised:', err)
        else:
            # At this point we know the exception came either from BLOCK or from __exit__
            pass

Складна частина полягає в тому, щоб розмежувати винятки, що виникають із цього приводу, BLOCKі __exit__тому, що withбуде винесено виняток, який уникне тіла заповіту, __exit__який може вирішити, як з цим поводитися (див . Документи ). Якщо все-таки __exit__зростає, оригінальний виняток буде замінено новим. Для вирішення цих випадків ми можемо додати загальне exceptположення в тілі withмагазину, щоб зберігати будь-який потенційний виняток, який інакше уникне би непоміченим, і порівняти його з тим, що пізніше потрапив у самий крайній край except- якщо вони однакові, це означає, що походження було BLOCKінакше це було __exit__(у випадку __exit__придушення винятку шляхом повернення справжнього значення найбільш віддаленимexcept просто не буде виконано).

try:
    mgr = ContextManager()  # __init__ could raise
except ValueError as err:
    print('__init__ raised:', err)
else:
    entered_body = exc_escaped_from_body = False
    try:
        with mgr:
            entered_body = True  # __enter__ did not raise at this point
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
            except Exception as err:  # this exception would normally escape without notice
                # we store this exception to check in the outer `except` clause
                # whether it is the same (otherwise it comes from __exit__)
                exc_escaped_from_body = err
                raise  # re-raise since we didn't intend to handle it, just needed to store it
    except ValueError as err:
        if not entered_body:
            print('__enter__ raised:', err)
        elif err is exc_escaped_from_body:
            print('BLOCK raised:', err)
        else:
            print('__exit__ raised:', err)

Альтернативний підхід з використанням еквівалентної форми, згаданої в PEP 343

PEP 343 - Заява "з" вказує еквівалентну версію withтвердження "не з" . Тут ми можемо охопити різні частини try ... exceptта, таким чином, розрізняти різні потенційні джерела помилок:

import sys

try:
    mgr = ContextManager()
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        value = type(mgr).__enter__(mgr)
    except ValueError as err:
        print('__enter__ raised:', err)
    else:
        exit = type(mgr).__exit__
        exc = True
        try:
            try:
                BLOCK
            except TypeError:
                pass
            except:
                exc = False
                try:
                    exit_val = exit(mgr, *sys.exc_info())
                except ValueError as err:
                    print('__exit__ raised:', err)
                else:
                    if not exit_val:
                        raise
        except ValueError as err:
            print('BLOCK raised:', err)
        finally:
            if exc:
                try:
                    exit(mgr, None, None, None)
                except ValueError as err:
                    print('__exit__ raised:', err)

Зазвичай простіший підхід буде добре

Потреба в такому спеціальному обробці винятків повинна бути досить рідкісною, і звичайно загортання цілого withв try ... exceptблок буде достатньою. Особливо, якщо різні джерела помилок позначені різними (користувацькими) типами винятків (менеджери контекстів повинні бути розроблені відповідно), ми можемо легко їх розрізняти. Наприклад:

try:
    with ContextManager():
        BLOCK
except InitError:  # raised from __init__
    ...
except AcquireResourceError:  # raised from __enter__
    ...
except ValueError:  # raised from BLOCK
    ...
except ReleaseResourceError:  # raised from __exit__
    ...
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.