@kolyazapoteev

Насколько допустимо с точки зрения стилистики вызывать Exception в конструкции if/else?

Допустим, есть функция avg(), принимающая на вход список/кортеж и возвращающая среднее значение:
def avg(data_set: list | tuple) -> int:
    if isinstance(data_set, list) or isinstance(data_set, tuple):
        avg = sum(data_set) / len(data_set)
    else:
        raise TypeError('argument must be list or tuple')
    return avg

Периодически бывают ситуации, когда "обрабатывать" можно как через try/catch, так и подобным способом. При том, как по мне, второй вариант здесь выглядит более "явным".

Собственно, вопрос из заголовка: можно ли так делать или я ошибаюсь, и это "плохой код"?
  • Вопрос задан
  • 279 просмотров
Решения вопроса 1
ipatiev
@ipatiev
Потомок старинного рода Ипатьевых-Колотитьевых
Вопрос не очень понятен, особенно в части "обрабатывать" можно как через try/catch".

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

В этом случае первый вариант однозначно предпочтительнее.
При ловле исключения будет более сложная логика - его надо не только поймать, но и определить что это именно то, которое мы ждём. И если это какое-то другое, то перевыбросить без изменений.
Кроме того, исключения в основном используются в исключительных, непредвиденных ситуациях. А здесь случай вполне предвиденный.

Я бы только инвертировал условие, чтобы во-первых, сделать логику более стройной (проверили - вывалились), а во-вторых, чтобы избавиться от else и убрать лишний отступ.
if not (isinstance(data_set, list) or isinstance(data_set, tuple)): 
    raise TypeError('argument must be list or tuple')
return sum(data_set) / len(data_set)


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

То есть в реальном коде я бы убрал проверку из функции вообще, а проверял данные при их получении (и выводил ошибку).
А на случай, если вдруг функция всё равно будет вызвана с неверным типом аргумента, есть системное исключение, 'type' object is not iterable
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
Vindicar
@Vindicar
RTFM!
Подход, предложенный @Ипатьев, в принципе неплох.
Но я считаю, стоит помнить, что питон - язык с динамической типизацией. Если какой-то тип умеет выполнять требуемые от него операции, стоит этот тип принимать. Иными словами, тебе необязательно принимать именно список или кортеж - тебе подойдёт любая коллекция, которую можно перечислить, и у которой есть длина.
Так что я бы посоветовал сделать иначе:
import collections.abc as abc
# Collection требует поддержки итерации, проверки вхождения и длины.
def avg(data_set: abc.Collection[int]) -> float:  # type hint для разработчика и IDE
    # если получим что-то не то, будет выкинуто TypeError - а нам это и надо.
    avg = sum(data_set) / len(data_set)  # такое деление вернёт float
    return avg


Либо, если тебе хочется своей обработки ошибки:
import collections.abc as abc

def avg(data_set: abc.Collection[int]) -> float:  # type hint для разработчика и IDE
    # протокол abc.Collection можно проверять через isinstance()
    if not isinstance(data_set, abc.Collection):  
        raise TypeError(f'data_set must be a collection of ints, got {type(data_set)}')
    avg = sum(data_set) / len(data_set)  # такое деление вернёт float
    return avg

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

И это будет работать:
>>> avg([1,2,3])  # список
2.0  # работает
>>> avg({1,2,3})  # множество
2.0  # работает
>>> avg(x for x in range(3))  # выражение-генератор
Traceback (most recent call last):  # отказ, у генераторов нет заранее известной длины
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in avg
TypeError: data_set must be a collection of ints, got <class 'generator'>
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы