# 8. Ошибки и исключения

До настоящего момента сообщения об ошибках упоминались лишь вскользь, но если вы пробовали запускать примеры с кодом, вы, вероятно, их встречали. Существует (по крайней мере) два различных типа ошибок: синтаксические ошибки и исключения

## 8.1. Синтаксические ошибки

Пока вы только изучаете Python, чаще всего вам будут попадаться синтаксически ошибки, или ошибки разбора (парсинга):

    >>> while True print('Hello world')
      File "<stdin>", line 1
        while True print('Hello world')
                   ^^^^^
    SyntaxError: invalid syntax

Анализатор повторяет строку с ошибкой и отображает маленькие стрелки, указывающие на место, где была обнаружена ошибка. Обратите внимание, что это не всегда то место, которое нужно исправить. В примере ошибка обнаружена в функции `print()`, поскольку двоеточие (`':'`) отсутствует непосредственно перед ней.

Имя файла (`<stdin>` в нашем примере) и номер строки выводятся для того, чтобы вы знали, где искать ошибку, если ввод поступал из файла.

## 8.2. Исключения

Даже если инструкция или выражение корректны синтаксически, они могут привести к ошибке при попытке их исполнить. Ошибки, появляющиеся во время выполнения программы, называются исключениями, и они не всегда фатальны: скоро вы узнаете, как их обрабатывать. Однако большинство исключений не обрабатываются программами и приводят к сообщениями об ошибке, как это:

    >>> 10 * (1/0)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        10 * (1/0)
              ~^~
    ZeroDivisionError: division by zero
    >>> 4 + spam*3
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        4 + spam*3
            ^^^^
    NameError: name 'spam' is not defined
    >>> '2' + 2
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        '2' + 2
        ~~~~^~~
    TypeError: can only concatenate str (not "int") to str

Последняя строка сообщения об ошибке указывает, что произошло. Исключения бывают разных типов, и тип исключения выводится как часть сообщения: в примере появились ошибки типов `ZeroDivisionError`, `NameError` и `TypeError`. Строка, напечатанная в качестве типа исключения — это имя встроенного исключения, которое произошло. Это верно для всех встроенных исключений, но может быть неверно для пользовательских (хотя такое соответствие — полезная практика). Имена стандартных исключений — встроенные идентификаторы (не зарезервированные ключевые слова).

Остаток строки содержит уточняющую информацию, которая зависит от типа исключения и от причины его возникновения.

Начальная часть сообщения об ошибке показывает контекст, в котором произошло исключение, в виде трассировки стека. Обычно в ней содержится трассировка со строками исходного кода; однако она не показывает строки, прочитанные из стандартного ввода.

В разделе Built-in Exceptions перечислены встроенные исключения и их смысл.

## 8.3. Обработка исключений

В Python можно писать программы, которые обрабатывают выбранные исключения. Посмотрите на следующий пример, который запрашивает у пользователя ввод до тех пор, пока не будет введено правильное целое число, но при этом позволяет пользователю прервать программу (с помощью `Control`-`C` или другого способа, поддерживаемого операционной системой). Обратите внимание, что прерывание, инициированное пользователем, сигнализируется с помощью исключения `KeyboardInterrupt`.

    >>> while True:
    ...     try:
    ...         x = int(input("Пожалуйста, введите число: "))
    ...         break
    ...     except ValueError:
    ...         print("Упс! Это было невалидное число. Попробуйте снова...")
    ...

Инструкция `try` работает следующим образом.

- Первым делом, выполняется ветка try, то есть инструкции между ключевыми словами `try` и `except`.

- Если исключение не возникает, ветка except пропускается, и выполнение инструкции `try` завершается.

- Если исключение возникает во время выполнения ветки `try`, оставшаяся её часть пропускается. Затем, если его тип совпадает с именем исключения, указанным после ключевого слова `except`, выполняется ветка except, а затем выполнение продолжается после блока try/except.

- Если возникает исключение, которое не совпадает с именем исключения в ветке except, оно передается внешним инструкциям `try`; если обработчик не найден, это необработанное исключение, и выполнение останавливается с сообщением об ошибке.

Инструкция `try` может содержать более одной ветви except, чтобы указать обработчики для разных исключений. Выполнится не более одного обработчика. Обработчики обрабатывают только исключения, возникающие в соответствующей ветке try, а не в других обработчиках той же инструкции `try`. Ветка except может указывать несколько исключений, например:

    ... except RuntimeError, TypeError, NameError:
    ...     pass

Класс в ветке `except` соответствует исключениям, которые являются экземплярами самого класса или одного из его производных классов (но не наоборот — ветка except, перечисляющая производный класс, не соответствует экземплярам его базовых классов). Например, следующий код выведет B, C, D в таком порядке:

    class B(Exception):
        pass

    class C(B):
        pass

    class D(C):
        pass

    for cls in [B, C, D]:
        try:
            raise cls()
        except D:
            print("D")
        except C:
            print("C")
        except B:
            print("B")

Обратите внимание, что если развернуть порядок обработчиков (поставив `except B` на первое место), код напечатает B, B, B — сработает первай совпадающая ветка except.

Когда возникает исключение, оно может иметь связанные значения, также известные как аргументы исключения. Наличие и типы аргументов зависят от типа исключения.

Ветка except может указать переменную после имени исключения. Переменная привязывается к экземпляру исключения, который обычно имеет атрибут `args`, в котором хранятся аргументы. Для удобства, встроенные типы исключений определяют метод `__str__()`, чтобы печатать все аргументы без явного доступа к `.args`.

    >>> try:
    ...     raise Exception('спам', 'яйца')
    ... except Exception as inst:
    ...     print(type(inst))    # тип исключения
    ...     print(inst.args)     # аргументы хранятся в .args
    ...     print(inst)          # __str__ позволяет вывести args напрямую,
    ...                          # но может быть переопределён в подклассах исключений
    ...     x, y = inst.args     # распаковка args
    ...     print('x =', x)
    ...     print('y =', y)
    ...
    <class 'Exception'>
    ('спам', 'яйца')
    ('спам', 'яйца')
    x = спам
    y = яйца

Вывод `__str__()` исключения печатается в виде последней части („detail“) сообщения для необработанных исключений.

`BaseException` — это общий базовый класс всех исключений. Одним из его подклассов является `Exception`, базовый класс всех некритических исключений. Исключения, которые не являются подклассами `Exception`, обычно не обрабатываются, потому что они используются для указания на то, что программа должна завершиться. В частности, это `SystemExit`, которое возбуждается методом `sys.exit()`, и `KeyboardInterrupt`, которое возбуждается, когда пользователь хочет прервать программу.

`Exception` можно использовать в качестве подстановочного знака, который перехватывает (почти) всё. Однако хорошей практикой является быть максимально конкретным в отношении типов исключений, которые мы собираемся обрабатывать, и разрешать любым неожиданным исключениям распространяться дальше.

Самый распространенный шаблон для обработки `Exception` — это печать или логирование исключения, а затем повторное его возбуждение (позволяя вызывающей стороне также обработать его):

    import sys

    try:
        f = open('myfile.txt')
        s = f.readline()
        i = int(s.strip())
    except OSError as err:
        print("Ошибка OS:", err)
    except ValueError:
        print("Не удалось преобразовать данные в целое число.")
    except Exception as err:
        print(f"Неожиданная ошибка {err=}, {type(err)=}")
        raise

У инструкции `try` … `except` есть опциональная ветка else, которая, если существует, должна следовать после всех блоков except. Её используют, когда нужно выполнить какой-то код, если блок try не вызывал исключение. Например:

    for arg in sys.argv[1:]:
        try:
            f = open(arg, 'r')
        except OSError:
            print('не удалось открыть', arg)
        else:
            print(arg, 'имеет', len(f.readlines()), 'строк')
            f.close()

Использование ветки `else` предпочтительнее добавления дополнительного кода в ветку `try`, потому что это предотвращает случайное перехватывание исключения, которое не было вызвано кодом, защищённым инструкцией `try` … `except`.

Обработчики исключений обрабатывают не только исключения, которые возникают непосредственно в ветке try, но также и те, которые возникают внутри функций, вызываемых (даже косвенно) в ветке try. Например:

    >>> def this_fails():
    ...     x = 1/0
    ...
    >>> try:
    ...     this_fails()
    ... except ZeroDivisionError as err:
    ...     print('Обработка ошибки во время выполнения:', err)
    ...
    Обработка ошибки во время выполнения: division by zero

## 8.4. Выбрасывание исключений

Инструкция `raise` позволяет программисту вызвать желаемое исключение. Например:

    >>> raise NameError('ПриветТам')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        raise NameError('ПриветТам')
    NameError: ПриветТам

Единственный аргумент, принимаемый `raise`, указывает на исключение, которое нужно породить. Тут можно указать либо экземпляр исключения, либо класс исключения (класс, который наследуется от `BaseException`, такой как `Exception` или один из его подклассов). Если передаётся класс исключения, экземпляр будет неявно создан с помощью вызова конструктора класса без аргументов:

    raise ValueError  # сокращение для 'raise ValueError()'

Если вы хотите узнать, возникло исключение или нет, но не планируете его обрабатывать, есть более простая форма инструкции `raise`. Она позволяет повторно выбросить исключение:

    >>> try:
    ...     raise NameError('ПриветТам')
    ... except NameError:
    ...     print('Мимо пролетело исключение!')
    ...     raise
    ...
    Мимо пролетело исключение!
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
        raise NameError('ПриветТам')
    NameError: ПриветТам

## 8.5. Цепочка исключений

Если внутри секции `except` возникает необработанное исключение, оно будет иметь прикрепленное к нему обрабатываемое исключение и будет включено в сообщение об ошибке:

    >>> try:
    ...     open("database.sqlite")
    ... except OSError:
    ...     raise RuntimeError("unable to handle error")
    ...
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
        open("database.sqlite")
        ~~~~^^^^^^^^^^^^^^^^^^^
    FileNotFoundError: [Errno 2] No such file or directory: 'database.sqlite'

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
      File "<stdin>", line 4, in <module>
        raise RuntimeError("unable to handle error")
    RuntimeError: unable to handle error

Для указания того, что исключение является прямым следствием другого, инструкция `raise` позволяет использовать необязательное ключевое слово `from`:

    # exc должен быть экземпляром исключения или None.
    raise RuntimeError from exc

Это может быть полезно при преобразовании одного исключения в другое. Например:

    >>> def func():
    ...     raise ConnectionError
    ...
    >>> try:
    ...     func()
    ... except ConnectionError as exc:
    ...     raise RuntimeError('Failed to open database') from exc
    ...
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
        func()
        ~~~~^^
      File "<stdin>", line 2, in func
    ConnectionError

    The above exception was the direct cause of the following exception:

    Traceback (most recent call last):
      File "<stdin>", line 4, in <module>
        raise RuntimeError('Failed to open database') from exc
    RuntimeError: Failed to open database

Оно также позволяет отключить автоматическое связывание исключений с использованием идиомы `from None`:

    >>> try:
    ...     open('database.sqlite')
    ... except OSError:
    ...     raise RuntimeError from None
    ...
    Traceback (most recent call last):
      File "<stdin>", line 4, in <module>
        raise RuntimeError from None
    RuntimeError

Для получения дополнительной информации о механизмах связывания, см. Built-in Exceptions.

## 8.6. Пользовательские исключения

Программы могут создавать собственные исключения, определив новый класс исключения (см. Классы, чтобы узнать больше про классы в Python). Исключения обычно наследуются от класса `Exception` напрямую или косвенно.

Классы исключений могут быть определены таким образом, чтобы делать всё то же самое, что и любой другой класс. Но обычно они остаются простыми, часто предлагая только несколько атрибутов, которые позволяют обработчикам извлекать информацию об ошибке.

Большинство исключений определены с именем, оканчивающимся на «Error», подобно стандартным исключениям.

Многие стандартные модули определяют свои собственные исключения для сообщения об ошибках, которые могут возникнуть в функциях этого модуля.

## 8.7. Определение действий по очистке

Инструкция `try` может содержать ещё одну необязательную ветку, предназначенную для определения действий по очистке, которые должны быть выполнены при любых обстоятельствах. Например:

    >>> try:
    ...     raise KeyboardInterrupt
    ... finally:
    ...     print('До свидания, мир!')
    ...
    До свидания, мир!
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
        raise KeyboardInterrupt
    KeyboardInterrupt

Если ветка `finally` присутствует, код в ней выполнится в последнюю очередь перед заверешением инструкции `try`. Ветка `finally` выполняется независимо от того, произошло исключение или нет. Следующие пункты описывают более сложные случаи возникновения исключений:

- Если исключение происходит в ветка `try`, оно может быть обработано в ветке `except`. Если исключение не было обработано в ветке `except`, оно повторно выбрасывается после выполнения ветки `finally`.

- Исключение может произойти в ветках `except` или `else`. Как и в предыдущем случае, исключение порождается заново после выполнения ветки `finally`.

- Если в ветке `finally` выполняются инструкции `break`, `continue` или `return`, то исключение повторно не выбрасывается. Это может запутать и поэтому не рекомендуется. Начиная с версия 3.14 компилятор выдаёт для этого случая `SyntaxWarning` (см. PEP 765).

- Если внутри `try` выполняется инструкция `break`, `continue` или `return`, ветка `finally` выполнится прямо перед выполнением инструкции `break`, `continue` или `return`.

- Если ветка `finally` содержит инструкции `return`, то возвращаемое значение будет браться из инструкций `return` ветки `finally`, а не из инструкций `return` ветки `try`. Это может запутать, поэтому такая практика не рекомендуется. Начиная с версия 3.14 компилятор выдаёт для этого случая `SyntaxWarning` (см. PEP 765).

Например:

    >>> def bool_return():
    ...     try:
    ...         return True
    ...     finally:
    ...         return False
    ...
    >>> bool_return()
    False

Более сложный пример:

    >>> def divide(x, y):
    ...     try:
    ...         result = x / y
    ...     except ZeroDivisionError:
    ...         print("деление на ноль!")
    ...     else:
    ...         print("результат равен", result)
    ...     finally:
    ...         print("выполняется блок finally")
    ...
    >>> divide(2, 1)
    результат равен 2.0
    выполняется блок finally
    >>> divide(2, 0)
    деление на ноль!
    выполняется блок finally
    >>> divide("2", "1")
    выполняется блок finally
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        divide("2", "1")
        ~~~~~~^^^^^^^^^^
      File "<stdin>", line 3, in divide
        result = x / y
                 ~~^~~
    TypeError: unsupported operand type(s) for /: 'str' and 'str'

Как можно заметить, ветка `finally` выполняется при любом исходе. Исключение `TypeError`, возникшее при делении двух строк, не обрабатывается блоком `except` и выбрасывается заново после выполнения блока `finally`.

В реальных приложениях блок `finally` может быть полезен для освобождения внешних ресурсов (например, файлов или сетевых соединений) независимо от того, было их использование успешным или нет.

## 8.8. Предопределённые действие по очистке

Некоторые объекты определяют стандартные действия по очистке, которые выполняются, когда объект больше не нужен, независимо от того, была ли операция с объектом успешной или нет. Рассмотрим следующий пример, который пытается открыть файл и вывести его содержимое на экран.

    for line in open("myfile.txt"):
        print(line, end="")

Проблема этого кода в том, что файл остаётся открытым неопределённое время после завершения этой части кода. В небольших скриптах это не вызывает проблем, но в крупных приложениях может привести к неприятностям. Инструкция `with` позволяет использовать объекты таким образом, чтобы они всегда очищали свои ресурсы незамедлительно и корректно.

    with open("myfile.txt") as f:
        for line in f:
            print(line, end="")

После выполнения этой инструкции файл f всегда будет закрыт, даже если при обработке строк возникла ошибка. Объекты, которые, как файлы, предоставляют предопределённые действия по очистке, указывают это в своей документации.

## 8.9. Выбрасывание и обработка нескольких несвязанных исключений

Есть ситуации, когда необходимо сообщить о нескольких возникших исключениях. Это часто встречается во фреймворках, поддерживающих параллелизм, когда несколько задач могут завершиться с ошибкой параллельно, но есть и другие случаи, когда желательно продолжить выполнение и собрать несколько ошибок, а не возбуждать первое исключение.

Встроенный `ExceptionGroup` оборачивает список экземпляров исключений, чтобы их можно было выбросить вместе. Он сам является исключением, поэтому его можно перехватить, как и любое другое исключение.

    >>> def f():
    ...     excs = [OSError('ошибка 1'), SystemError('ошибка 2')]
    ...     raise ExceptionGroup('возникли проблемы', excs)
    ...
    >>> f()
      + Exception Group Traceback (most recent call last):
      |   File "<stdin>", line 1, in <module>
      |     f()
      |     ~^^
      |   File "<stdin>", line 3, in f
      |     raise ExceptionGroup('возникли проблемы', excs)
      | ExceptionGroup: возникли проблемы (2 sub-exceptions)
      +-+---------------- 1 ----------------
        | OSError: ошибка 1
        +---------------- 2 ----------------
        | SystemError: ошибка 2
        +------------------------------------
    >>> try:
    ...     f()
    ... except Exception as e:
    ...     print(f'перехвачено {type(e)}: {e}')
    ...
    перехвачено <class 'ExceptionGroup'>: возникли проблемы (2 sub-exceptions)
    >>>

Используя `except*` вместо `except`, мы можем выборочно обрабатывать только те исключения в группе, которые совпадают с определенным типом. В следующем примере, который показывает вложенную группу исключений, каждый блок `except*` извлекает из группы исключения определенного типа, позволяя всем остальным исключениям продолжить распространение к другим блокам и в итоге быть повторно выброшенными.

    >>> def f():
    ...     raise ExceptionGroup(
    ...         "группа1",
    ...         [
    ...             OSError(1),
    ...             SystemError(2),
    ...             ExceptionGroup(
    ...                 "группа2",
    ...                 [
    ...                     OSError(3),
    ...                     RecursionError(4)
    ...                 ]
    ...             )
    ...         ]
    ...     )
    ...
    >>> try:
    ...     f()
    ... except* OSError as e:
    ...     print("Были OSErrors")
    ... except* SystemError as e:
    ...     print("Были SystemErrors")
    ...
    Были OSErrors
    Были SystemErrors
      + Exception Group Traceback (most recent call last):
      |   File "<stdin>", line 2, in <module>
      |     f()
      |     ~^^
      |   File "<stdin>", line 2, in f
      |     raise ExceptionGroup(
      |     ...<12 lines>...
      |     )
      | ExceptionGroup: группа1 (1 sub-exception)
      +-+---------------- 1 ----------------
        | ExceptionGroup: группа2 (1 sub-exception)
        +-+---------------- 1 ----------------
          | RecursionError: 4
          +------------------------------------
    >>>

Обратите внимание, что вложенные в группу исключений элементы должны быть экземплярами, а не типами. Это связано с тем, что на практике это обычно исключения, которые уже были выброшены и пойманы программой, по следующему образцу:

    >>> excs = []
    ... for test in tests:
    ...     try:
    ...         test.run()
    ...     except Exception as e:
    ...         excs.append(e)
    ...
    >>> if excs:
    ...    raise ExceptionGroup("Сбой тестов", excs)
    ...

## 8.10. Дополнение исключений заметками

Когда исключение создается для того, чтобы быть выброшенным, оно обычно инициализируется информацией, которая описывает возникшую ошибку. Есть случаи, когда полезно добавить информацию после того, как исключение было перехвачено. Для этой цели у исключений есть метод `add_note(note)`, который принимает строку и добавляет её в список заметок исключения. Стандартное отображение трассировки стека включает все заметки в порядке их добавления, после исключения.

    >>> try:
    ...     raise TypeError('неверный тип')
    ... except Exception as e:
    ...     e.add_note('Добавлена дополнительная информация')
    ...     e.add_note('Добавлено больше дополнительной информации')
    ...     raise
    ...
    Traceback (most recent call last):
      File "<stdin>", line 2, in <module>
        raise TypeError('bad type')
    TypeError: неверный тип
    Добавлена дополнительная информация
    Добавлено больше дополнительной информации
    >>>

Например, при сборе исключений в группу исключений мы можем добавить контекстную информацию для отдельных ошибок. В следующем примере каждое исключение в группе имеет заметку, указывающую, когда произошла эта ошибка.

    >>> def f():
    ...     raise OSError('операция не выполнена')
    ...
    >>> excs = []
    >>> for i in range(3):
    ...     try:
    ...         f()
    ...     except Exception as e:
    ...         e.add_note(f'Произошло в итерации {i+1}')
    ...         excs.append(e)
    ...
    >>> raise ExceptionGroup('Возникли проблемы', excs)
      + Exception Group Traceback (most recent call last):
      |   File "<stdin>", line 1, in <module>
      |     raise ExceptionGroup('Возникли проблемы', excs)
      | ExceptionGroup: We have some problems (3 sub-exceptions)
      +-+---------------- 1 ----------------
        | Traceback (most recent call last):
        |   File "<stdin>", line 3, in <module>
        |     f()
        |     ~^^
        |   File "<stdin>", line 2, in f
        |     raise OSError('операция не выполнена')
        | OSError: операция не выполнена
        | Произошло в итерации 1
        +---------------- 2 ----------------
        | Traceback (most recent call last):
        |   File "<stdin>", line 3, in <module>
        |     f()
        |     ~^^
        |   File "<stdin>", line 2, in f
        |     raise OSError('операция не выполнена')
        | OSError: операция не выполнена
        | Произошло в итерации 2
        +---------------- 3 ----------------
        | Traceback (most recent call last):
        |   File "<stdin>", line 3, in <module>
        |     f()
        |     ~^^
        |   File "<stdin>", line 2, in f
        |     raise OSError('операция не выполнена')
        | OSError: операция не выполнена
        | Произошло в итерации 3
        +------------------------------------
    >>>