Обработка ошибок

Исключения

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

try/except — перехватывает исключения, возбужденные интерпретатором или вашим программным кодом, и выполняет восстановительные операции.

try/finally выполняет заключительные операции независимо от того, возникло исключение или нет.

raise — дает возможность возбудить исключение программно.

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

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

Назначение исключений

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

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

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

  4. Заключительные операции. Как будет показано далее, инструкция try/finally позволяет гарантировать выполнение завершающих операций независимо от наличия исключений.

  5. Необычное управление потоком выполнения. И, наконец, так как исключения – это своего рода оператор «goto», их можно использовать как основу для экзотического управления потоком выполнения программы.

Примеры исключений

Предположим, что у нас имеется следующая функция:

def fetcher(obj, index):
    return obj[index]
s = "some_string"
fetcher(s, 3)
'e'

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

fetcher(s, 20)
IndexError: string index out of range

Иногда это совсем не то, что нам требуется. Например, серверные программы обычно должны оставаться активными даже после появления внутренних ошибок.

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

try:
    fetcher(s, 20)
except IndexError:
    print("got exception")
print("continuing")
got exception
continuing

Finally

Инструкции try могут включать блоки finally. Эти блоки выглядят точно так же, как обработчики except. Комбинация try/finally определяет завершающие действия, которые всегда выполняются «на выходе», независимо от того, возникло исключение в блоке try или нет.

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

try/finally можно использовать вместо with/as.

try:
    f = open("some_file.txt", "w")
    # что то делаем и получаем ошибку
except IOError as err:
    print("Получаем ошибку и печатаем её: ", err)
finally:
    # но файл закрыть не забываем
    f.close()
    print("Этот код выполнится")
Этот код выполнится

Типы исключений

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

BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- GeneratorExit
 +-- Exception
      +-- StopIteration
      +-- AssertionError
      +-- AttributeError
      +-- LookupError
           +-- IndexError
           +-- KeyError
      +-- OSError
      +-- SystemError
      +-- TypeError
      +-- ValueError

Существуют несколько системных исключений, например, SystemExit (генерируется, если мы вызвали функцию OSExit), KeyboardInterrupt (генерируется, если мы нажали сочетание клавиш Ctrl + C) и так далее. Все остальные исключения генерируется от базового класса Exception. Именно от этого класса нужно порождать свои исключения.

Попробуем преобразовать строку в целое число, видим ValueError:

int("asdf")
ValueError: invalid literal for int() with base 10: 'asdf'

Получим TypeError при попытке сложить целое число со строкой:

1 + "10"
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Чтобы отлавливать любые ошибки программы, возникающеие в коде, можно отлавливать ошибку Exception

try:
    """ some code """
except Exception:
    print("got all exceptions")
print("continuing")
continuing

Внимание! Не стоит отливливать BaseException или KeyboardInterrupt, т.к. иногда вы даже не сможете выйти из программы. Python добускает возможность не указывать тип ошибки после except, что равнозначно except BaseException.

while True:
    try:
        raw = input("введите число: ")
        number = int(raw)
        break
    except:
        # не указали тип исключения, значит, обрабатываем все
        print("некорректное значение")

raise

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

try:
    raw = input("введите число: ")
    if not raw.isdigit():
        raise ValueError
except ValueError:
    print("некорректное значение!")
введите число: в
некорректное значение!

AssertionError

Говоря об исключениях, нельзя не затронуть инструкцию assert. По умолчанию, если выполнить инструкцию assert с логическим выражением, результат которого равен True, ничего не произойдет. Но если попробовать выполнить инструкцию assert с логическим выражением, которое равно False, то будет сгенерировано исключение AssertionError. Также мы можем передать дополнительную строку в сам объект AssertionError.

user_input = input("Введите ваше имя: ")
assert user_input, "Пустая строка!" # введено ли имя
print("Выполнится только если введено")
Введите ваше имя:
AssertionError: Пустая строка!

Исключения AssertionError предназначены скорее для программистов. При написании наших программ на этапе разработки мы должны видеть, что делаем что-то не так (например, передали в функцию некорректное значение). Не нужно, например, обрабатывать пользовательский ввод и пытаться обработать исключение AssertionError блоком try except. Если таких мест будет очень много, то это затронет и производительность нашей программы. Однако, есть возможность отключить все инструкции assert при помощи флага −O. Тогда AssertionError не будет сгенерирована. Этим и отличаются исключения AssertionError от обычных пользовательских исключений и исключений стандартной библиотеки.

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

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

import os
import requests

ex_path = os.path.split(requests.__file__)[0] + "/exceptions.py"
print(ex_path)
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/requests/exceptions.py
with open(ex_path) as f:
    print(f.read())
# -*- coding: utf-8 -*-

"""
requests.exceptions
~~~~~~~~~~~~~~~~~~~

This module contains the set of Requests' exceptions.
"""
from urllib3.exceptions import HTTPError as BaseHTTPError


class RequestException(IOError):
"""There was an ambiguous exception that occurred while handling your
request.
"""

def __init__(self, *args, **kwargs):
"""Initialize RequestException with `request` and `response` objects."""
response = kwargs.pop('response', None)
self.response = response
self.request = kwargs.pop('request', None)
if (response is not None and not self.request and
hasattr(response, 'request')):
self.request = self.response.request
super(RequestException, self).__init__(*args, **kwargs)


class HTTPError(RequestException):
"""An HTTP error occurred."""


class ConnectionError(RequestException):
"""A Connection error occurred."""


class ProxyError(ConnectionError):
"""A proxy error occurred."""


class SSLError(ConnectionError):
"""An SSL error occurred."""


class Timeout(RequestException):
"""The request timed out.

Catching this error will catch both
:exc:`~requests.exceptions.ConnectTimeout` and
:exc:`~requests.exceptions.ReadTimeout` errors.
"""


class ConnectTimeout(ConnectionError, Timeout):
"""The request timed out while trying to connect to the remote server.

Requests that produced this error are safe to retry.
"""


class ReadTimeout(Timeout):
"""The server did not send any data in the allotted amount of time."""


class URLRequired(RequestException):
"""A valid URL is required to make a request."""


class TooManyRedirects(RequestException):
"""Too many redirects."""


class MissingSchema(RequestException, ValueError):
"""The URL schema (e.g. http or https) is missing."""


class InvalidSchema(RequestException, ValueError):
"""See defaults.py for valid schemas."""


class InvalidURL(RequestException, ValueError):
"""The URL provided was somehow invalid."""


class InvalidHeader(RequestException, ValueError):
"""The header value provided was somehow invalid."""


class InvalidProxyURL(InvalidURL):
"""The proxy URL provided is invalid."""


class ChunkedEncodingError(RequestException):
"""The server declared chunked encoding but sent an invalid chunk."""


class ContentDecodingError(RequestException, BaseHTTPError):
"""Failed to decode response content"""


class StreamConsumedError(RequestException, TypeError):
"""The content for this response was already consumed"""


class RetryError(RequestException):
"""Custom retries logic failed"""


class UnrewindableBodyError(RequestException):
"""Requests encountered an error when trying to rewind a body"""

# Warnings


class RequestsWarning(Warning):
"""Base warning for Requests."""
pass


class FileModeWarning(RequestsWarning, DeprecationWarning):
"""A file was opened in text mode, but Requests determined its binary length."""
pass


class RequestsDependencyWarning(RequestsWarning):
"""An imported dependency doesn't match the expected version range."""
pass
url = "https://github.com/not_found"

try:
    response = requests.get(url, timeout=30)
    response.raise_for_status()
except requests.Timeout:
    print("ошибка timeout, url:", url)
except requests.HTTPError as err:
    code = err.response.status_code
    print("ошибка url: {0}, code: {1}".format(url, code))
except requests.RequestException:
    print("ошибка скачивания url: ", url)
else:
    print(response.content)
ошибка url: https://github.com/not_found, code: 404
  1. Назовите три области, где можно было бы использовать операции с исключениями.
  2. Что произойдет с программой в случае исключения, если вы не предусмотрите его обработку?
  3. Как можно реализовать восстановление нормальной работы сценария после исключения?
  4. Назовите два способа возбуждения исключений в сценариях.
  5. Назовите два способа, с помощью которых можно было бы организовать выполнение заключительных операций независимо от того, возникло исключение или нет.

Comments