Как реализовать и проверить исключения при работе с классом стека?
Что проверяют
Умеете ли вы сделать исключения частью интерфейса класса и проверить их тестом, а не только написать raise. Следом спрашивают, какое исключение выбрать, встроенное или своё, и как убедиться, что после ошибки стек в корректном состоянии.
Как отвечать
Сначала поведение. На пустом стеке pop() и peek() бросают исключение, а не возвращают None: иначе None, лежащий в стеке, не отличить от пустоты. Если у стека есть ёмкость, push() при заполнении бросает своё исключение.
- иерархия: базовый
class StackError(Exception), от негоEmptyStackErrorиStackFullError. Пустой стек можно ещё унаследовать отIndexError, тогда код, привыкший кlist.pop(), продолжит работать; - сообщение: короткое и полезное, например
"pop from empty stack"; - перевод исключений: в стиле EAFP
try: return self._items.pop()иexcept IndexError: raise EmptyStackError(...) from None.from Noneубирает внутреннюю деталь из трейсбека; - удобства:
__len__и__bool__, тогда работаетif stack:. Для типизации с Python 3.12 есть синтаксисclass Stack[T]:.
Тесты на pytest:
with pytest.raises(EmptyStackError, match="empty"):и внутри ровно один вызовstack.pop();- после блока проверить инвариант:
len(stack) == 0, а после неудачногоpushв полный стек размер не изменился; pytest.mark.parametrize, чтобы прогнатьpopиpeekодним тестом;- в
unittestто же даютassertRaisesкак контекстный менеджер иassertRaisesRegex.
Где спотыкаются
Пишут pytest.raises(Exception). Такой тест проходит на любой ошибке, включая AttributeError из-за опечатки в имени метода, и ничего не проверяет.
Кладут в блок with несколько строк и проверки после вызова, который бросает исключение. Код после него не выполнится, и эти проверки тихо не работают.
Путают переполнение своего стека с RecursionError. Список в Python растёт, пока хватает памяти, так что переполнение имеет смысл только для стека с явной ёмкостью, и проверку нужно написать самим.