Нашли ошибку?
Скачать чистый
урок 3 из 4≈ 25 минупражнения: 0 из 4

Свои исключения

Исключение — тоже класс: своя иерархия ShopError → OutOfStockError, атрибуты исключения с подробностями, перехват по базовому классу и цепочка raise … from.

Python запустится при первом выполнении

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

Исключение — это класс

Каждое исключение — экземпляр класса. Почти все классы исключений — и все, с которыми вы встречались, включая ValueError, — наследуют от Exception (выше него только BaseException для служебных вроде KeyboardInterrupt — Ctrl+C). Своё исключение — подкласс Exception; часто ему не нужно ничего, кроме имени. По соглашению имя заканчивается на Error:

[ ]Своё исключение
class RegisterClosedError(Exception):
    pass


def open_check(hour):
    if hour < 8 or hour >= 22:
        raise RegisterClosedError(f"касса закрыта в {hour}:00")
    return "чек открыт"


print(open_check(9))
try:
    open_check(23)
except RegisterClosedError as error:
    print(type(error).__name__, "—", error)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
чек открыт
RegisterClosedError — касса закрыта в 23:00

Иерархия исключений

Свои исключения удобно собрать в иерархию: общий базовый класс для всех ошибок кассы и подклассы для конкретных случаев. Тогда except ShopError перехватит любую ошибку кассы, а except OutOfStockError — только нехватку товара. Работает обычный isinstance: исключение подкласса — это и исключение базового класса.

[ ]Перехват по базовому классу
class DeliveryError(Exception):
    pass


class CourierLateError(DeliveryError):
    pass


class AddressError(DeliveryError):
    pass


for problem in [CourierLateError("курьер опаздывает"), AddressError("нет такого дома")]:
    try:
        raise problem
    except DeliveryError as error:  # перехватит оба подкласса
        print(type(error).__name__, "→", error)
print(issubclass(AddressError, DeliveryError), issubclass(AddressError, Exception))
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
CourierLateError → курьер опаздывает
AddressError → нет такого дома
True True
[ ]Упражнение 1Ошибки кассы решено

Объявите три класса исключений: ShopError — базовый для всех ошибок кассы, подкласс Exception; OutOfStockError и PaymentError — подклассы ShopError.

# объявите ShopError, OutOfStockError и PaymentError

Атрибуты исключения

Исключение — объект, и у него могут быть атрибуты: какой товар, сколько просили, сколько есть. Код, который его перехватил, достанет подробности, а не будет разбирать текст сообщения. Текст же по-прежнему передают в __init__ базового класса — его покажет str(error):

[ ]Исключение с подробностями
class PaymentDeclinedError(Exception):
    def __init__(self, amount, reason):
        super().__init__(f"оплата {amount} ₽ отклонена: {reason}")
        self.amount = amount
        self.reason = reason


try:
    raise PaymentDeclinedError(640, "нет связи с банком")
except PaymentDeclinedError as error:
    print(error)
    print(error.amount, error.reason)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
оплата 640 ₽ отклонена: нет связи с банком
640 нет связи с банком
[ ]Упражнение 2Чего не хватает решено

Допишите OutOfStockError (по-прежнему подкласс ShopError): OutOfStockError("Сироп", 2, 1) хранит название товара, сколько просили и сколько есть — в атрибутах name, requested и available, а str(error) — "мало «Сироп»: нужно 2, есть 1".

class OutOfStockError(ShopError):
    def __init__(self, name, requested, available):
        ...

[ ]Упражнение 3Продажа со склада решено

Напишите функцию sell(stock, name, qty): stock — словарь «товар → количество». Если товара меньше qty (или его нет вовсе), функция выбрасывает OutOfStockError с подробностями и склад не меняет; иначе — уменьшает количество.

def sell(stock, name, qty):
    ...

raise … from: цепочка исключений

Иногда своё исключение возникает из-за чужого: строку заказа "Латте x два" не удалось разобрать, потому что int("два") выбросил ValueError. Перехватить ValueError и выбросить своё — правильно, но причину терять не стоит. raise НовоеИсключение(…) from err создаёт цепочку исключений (exception chaining): причина сохраняется в атрибуте __cause__, и трассировка покажет обе ошибки.

[ ]Причина в __cause__
class PriceFormatError(Exception):
    pass


def parse_price(text):
    try:
        return int(text.replace("₽", ""))
    except ValueError as err:
        raise PriceFormatError(f"цена не разобрана: {text}") from err


try:
    parse_price("двести ₽")
except PriceFormatError as error:
    print(error)
    print("причина:", type(error.__cause__).__name__, "—", error.__cause__)
Выводсохранённый · нажмите «Выполнить», чтобы получить свой
цена не разобрана: двести ₽
причина: ValueError — invalid literal for int() with base 10: 'двести '
[ ]Упражнение 4Разбор количества решено

Объявите исключение OrderFormatError (подкласс ShopError) и функцию parse_qty(text): она возвращает число из строки вида "x2" — после буквы x. Если число не разбирается, функция выбрасывает OrderFormatError("неверное количество: x два") с исходным ValueError в качестве причины.

class OrderFormatError(ShopError):
    pass


def parse_qty(text):
    ...

Вопрос

OutOfStockError — подкласс ShopError. Какой блок сработает?

try:
    sell(stock, "Сироп", 10)
except ShopError:
    print("ошибка кассы")
except OutOfStockError:
    print("нет товара")

Нашли ошибку?