руководствоPython
August 3, 2020

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

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

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

Синтаксические ошибки, также известные как ошибки синтаксического анализа, являются, пожалуй, самой распространенной жалобой, которую вы получаете, пока изучаете Python:

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

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

Исключения

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

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

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

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

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

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

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

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...

tryОператор работает следующим образом .

  • Во- первых, выполняется выражение между try except операторами
  • Если исключение не возникает, предложение исключения пропускается, и выполнение try оператора заканчивается.
  • Если во время выполнения предложения try возникает исключение, остальная часть предложения пропускается. Затем, если его тип соответствует исключению, названному после exceptключевого слова, выполняется условие исключение, а затем продолжается выполнение остального кода
  • Если возникшее исключение не соответствует исключению в операторе except, то оно вызывается интепретатором python:
>>> try:
...     1 / 0
... except NameError:
...     print("NameError")
... 
Traceback (most recent call last):
   File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero

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

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

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

import sys

try:
    f = open('myfile.txt')
    s = f.readline()
    i = int(s.strip())
except OSError as err:
    print("OS error: {0}".format(err))
except ValueError:
    print("Could not convert data to an integer.")
except:
    print("Unexpected error:", sys.exc_info()[0])
    raise # raise вызывает ошибку, которая возникла

Оператор tryexceptимеет необязательное условие else , которое, если оно присутствует, должно следовать всем, кроме предложений. Это полезно для кода, который должен быть выполнен, если предложение try не вызывает исключение. Например:

for arg in sys.argv[1:]:
    try:
        f = open(arg, 'r')
    except OSError:
        print('cannot open', arg)
    else:
        print(arg, 'has', len(f.readlines()), 'lines')
        f.close()

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

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

Исключение также может указывать на экземпляр исключения и его аргументы, хранящиеся в instance.args Для удобства это все перемещено в __str__(), чтобы все это можно было написать без ссылки .args. Можно также создать экземпляр исключения прежде, чем вызвать его, и добавить любые атрибуты к нему по желанию:

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

Если у исключения есть аргументы, они печатаются как последняя часть («деталь») сообщения для необработанных исключений.

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

>>> def this_fails():
...     x = 1/0
...
>>> try:
...     this_fails()
... except ZeroDivisionError as err:
...     print('Handling run-time error:', err)
...
Handling run-time error: division by zero

Вызов исключений

raise Оператор позволяет вам заставить определенное исключение произойдет. Например:

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: HiThere

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

raise ValueError  # shorthand for 'raise ValueError()'

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

>>> try:
...     raise NameError('HiThere')
... except NameError:
...     print('An exception flew by!')
...     raise
...
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
NameError: HiThere

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

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

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

class Error(Exception):
    """Базовый класс для исключений в этом модуле."""
    pass

class InputError(Error):
    """Исключение возникающее при ошибке во входных данных.

    Attributes:
        expression - входное выражение, в котором произошла ошибка
        message - объяснение ошибки
    """

    def __init__(self, expression, message):
        self.expression = expression
        self.message = message

class TransitionError(Error):
    """Возникает, когда операция пытается выполнить переход состояния, который не разрешается.
    allowed.

    Attributes:
        previous - состояние в начале перехода
        next - попытка нового состояния
        message - объяснение того, почему определенный переход не разрешен
    """

    def __init__(self, previous, next, message):
        self.previous = previous
        self.next = next
        self.message = message

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

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

Обнаружение действий по очистке

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

>>> try:
...     raise KeyboardInterrupt
... finally:
...     print('Goodbye, world!')
...
Goodbye, world!
KeyboardInterrupt
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>

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

  • Если во время выполнения try предложения возникает исключение, оно может быть обработано except предложением. Если исключение не обрабатывается except предложением, исключение повторно вызывается после того, как finally предложение было выполнено.
  • Исключение может возникнуть во время выполнения предложения except или else. Опять же, исключение повторно вызывается после того, как finallyпредложение было выполнено.
  • Если tryоператор достигает break, continue или return заявление, то finally условие будет выполняться непосредственно перед break, continue или return инструкциями.
  • Если finally пункт включает в себя return заявление, возвращаемое значение будет значение из finally, а не из try оператора

Например:

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

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

>>> def divide(x, y):
...     try:
...         result = x / y
...     except ZeroDivisionError:
...         print("division by zero!")
...     else:
...         print("result is", result)
...     finally:
...         print("executing finally clause")
...
>>> divide(2, 1)
result is 2.0
executing finally clause
>>> divide(2, 0)
division by zero!
executing finally clause
>>> divide("2", "1")
executing finally clause
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'str' and 'str'

Как видите, finallyпредложение выполняется в любом случае. TypeError Поднят путем деления двух строки . Except не обрабатывает это исключение. Но исключение было повторно вызвано из интерпретатора после finally оператора

Автоматический сборщик мусора

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

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

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

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

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

Заключение

Пост был создан для тг-канала @coolcoders