Это плохая практика - не захватывать исключения внутренней функции и вместо этого делать это при вызове внешней функции? Давайте посмотрим на два примера:

Вариант а)

def foo(a, b):
    return a / b

def bar(a):
    return foo(a, 0)

try:
    bar(6)
except ZeroDivisionError:
    print("Error!")

Плюс: чище (на мой взгляд)
Против: вы не можете определить, какие исключения вызывает bar, не глядя на foo

Вариант б)

def foo(a, b):
    return a / b

def bar(a):
    try:
        ret = foo(a, 0)
    except ZeroDivisionError:
        raise

    return ret

try:
    bar(6)
except ZeroDivisionError:
    print("Error!")

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

Другие варианты?

Я понимаю, что если вы хотите сделать что-то с исключением или сгруппировать несколько исключений вместе, вариант b - единственный выбор. Но что, если вы хотите повторно вызвать только некоторые конкретные исключения как есть?

Я не смог найти ничего в PEP, что проливает свет на это.

0
skd 2 Сен 2020 в 10:41

2 ответа

Лучший ответ

Работа с ошибками

Это плохая практика? На мой взгляд: нет. В целом это ХОРОШАЯ практика:

def foo(a, b):
    return a / b

def bar(a):
    return foo(a, 0)

try:
    bar(6)
except ZeroDivisionError:
    print("Error!")

Причина проста: код, работающий с ошибкой, сосредоточен в одной точке вашей основной программы.

В некоторых языках программирования исключения, которые потенциально могут возникнуть, должны быть объявлены на уровне функции / метода. Python отличается: это язык сценариев, в котором отсутствуют подобные функции. Конечно, иногда вы можете получить исключение совершенно неожиданно, поскольку вы можете не знать, что другой код, который вы вызываете, может вызвать такое исключение. Но в этом нет ничего страшного: для разрешения этой ситуации у вас есть try...except... в вашей основной программе.

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

  • документировать исключения, которые могут возникнуть; если язык программирования сам по себе здесь не помогает, необходимо восполнить этот недостаток предоставлением более обширной документации;
  • выполнить обширные тесты;

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

Поэтому вместо ...

def bar(a):
    try:
        ret = foo(a, 0)
    except ZeroDivisionError:
        raise

    return ret

... записывать:

def bar(a):
    """
    Might raise ZeroDivisionError
    """
    return foo(a, 0)

Или, как я бы написал:

#
# @throws   ZeroDivisionError       Does not work with zeros.
#
def bar(a):
    return foo(a, 0)

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

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

Вывод ошибок

В любом случае я бы не стал следовать вашему подходу к печати простого сообщения об ошибке:

try:
    bar(6)
except ZeroDivisionError:
    print("Error!")

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

К сожалению, Python не обеспечивает удобочитаемую трассировку стека в выводе ошибок. Чтобы компенсировать это, я реализовал свою собственную обработку вывода ошибок (повторно используемую в качестве модуля), которая даже использует цвета, но это другой вопрос, который также может выходить за рамки этого вопроса.

1
Regis May 2 Сен 2020 в 10:58

Если вы следуете книге «Чистый код» от дяди Боба, вы всегда должны разделять логику и обработку ошибок. Это сделало бы вариант а.) Предпочтительным решением.

Лично мне нравится называть функции так:

def _foo(a, b):
    return a / b

def try_foo(a, b):
    try:
        return _foo(a, b)
    except ZeroDivisionError:
        print('Error')


if __name__ == '__main__':
    try_foo(5, 0)        
1
tilman151 2 Сен 2020 в 08:34