Проверка условий и обработка исключений

Когда обработка исключений более предпочтительна, чем проверка условий? Есть много ситуаций, когда я могу выбрать один или другой.

Например, это суммирующая функция, которая использует настраиваемое исключение:

# module mylibrary 
class WrongSummand(Exception):
    pass

def sum_(a, b):
    """ returns the sum of two summands of the same type """
    if type(a) != type(b):
        raise WrongSummand("given arguments are not of the same type")
    return a + b


# module application using mylibrary
from mylibrary import sum_, WrongSummand

try:
    print sum_("A", 5)
except WrongSummand:
    print "wrong arguments"

И это та же самая функция, которая позволяет избежать использования исключений

# module mylibrary
def sum_(a, b):
    """ returns the sum of two summands if they are both of the same type """
    if type(a) == type(b):
        return a + b


# module application using mylibrary
from mylibrary import sum_

c = sum_("A", 5)
if c is not None:
    print c
else:
    print "wrong arguments"

Я думаю, что использование условий всегда более читаемо и управляемо. Или я ошибаюсь? Каковы правильные случаи для определения API, которые вызывают исключения и почему?

Ответ 1

Исключения намного более управляемы, потому что они определяют общие семейства вещей, которые могут пойти не так. В вашем примере есть только одна возможная проблема, поэтому нет смысла использовать исключения. Но если у вас был другой класс, который выполняет деление, тогда он должен сигнализировать, что вы не можете делить на ноль. Просто возврат None больше не будет работать.

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

Ответ 2

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

Итак, если вы думаете о своей функции "добавить". Он НИКОГДА не должен возвращать значение null. Это не последовательный результат для добавления двух вещей. В этом случае в аргументах, которые были переданы, есть ошибка, и функция не должна пытаться притвориться, что все в порядке. Это идеальный случай для исключения исключений.

Вы хотите использовать проверку состояния и вернуть значение null, если вы находитесь в обычном или обычном случае. Например, IsEqual может быть хорошим случаем для использования условий и возвращать false, если одно из ваших условий терпит неудачу. То есть.

function bool IsEqual(obj a, obj b)
{ 
   if(a is null) return false;
   if(b is null) return false;
   if(a.Type != b.Type) return false;

   bool result = false;
   //Do custom IsEqual comparison code
   return result;
}

В этом сценарии вы возвращаете false как для случаев исключения, так и для "объектов не равного случая". Это означает, что потребитель (вызывающая сторона) не может определить, было ли сравнение неудачным или объекты просто не равны. Если эти случаи нужно различать, вы должны использовать исключения вместо условий.

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

Ответ 3

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

Во второй реализации sum_ пользователь должен каждый раз проверять, какое значение было. Это напоминает шаблон C/Fortran/other-languages ​​(и частой источник ошибок), где коды ошибок не проверяются, что мы избегаем. Вы должны писать такой код на всех уровнях, чтобы иметь возможность распространять ошибки. Это становится беспорядочным и особенно избегается в Python.

Несколько других примечаний:

  • Вам часто не нужно делать свои собственные исключения. Для многих случаев необходимы встроенные исключения, такие как ValueError и TypeError.
  • Когда я создаю новое исключение, что очень полезно, я часто пытаюсь подклассифицировать что-то более конкретное, чем Exception. Встроенная иерархия исключений здесь.
  • Я бы никогда не реализовал такую ​​функцию, как sum_, так как typechecking делает ваш код менее гибким, поддерживаемым и идиоматичным.

    Я просто напишу функцию

    def sum_(a, b):
        return a + b
    

    который работал бы, если бы объекты были совместимы, и если бы он уже не выдавал исключение, то TypeError, который все видят. Посмотрите, как работает моя реализация.

    >>> sum_(1, 4)
    5
    >>> sum_(4.5, 5.0)
    9.5
    >>> sum_([1, 2], [3, 4])
    [1, 2, 3, 4]
    >>> sum_(3.5, 5) # This operation makes perfect sense, but would fail for you
    8.5
    >>> sum_("cat", 7) # This makes no sense and already is an error.
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 1, in sum_
    TypeError: cannot concatenate 'str' and 'int' objects
    

    Мой код был короче и проще, но более надежный и гибкий, чем ваш. Вот почему мы избегаем проверки типов в Python.

Ответ 4

Моя основная причина предпочтительности исключений для возврата статуса связана с рассмотрением того, что произойдет, если программист забывает выполнять свою работу. За исключением случаев, вы можете забыть об исключении. В этом случае ваша система будет явно терпеть неудачу, и у вас будет возможность рассмотреть, где добавить улов. С возвратом статуса, если вы забудете проверить возврат, он будет молча игнорировать, и ваш код будет продолжен, возможно, позже не так загадочно. Я предпочитаю видимый отказ невидимому.

Есть и другие причины, которые я объяснил здесь: Исключения против статуса Возврат.

Ответ 5

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

Например, вот код, который я встретил в подготовленном операторе, когда разработчик устанавливает значения параметров (Java, а не Python):

// Variant A
try {
  ps.setInt(1, enterprise.getSubRegion().getRegion().getCountry().getId());
} catch (Exception e) {
  ps.setNull(1, Types.INTEGER);
}

С условной проверкой это будет написано так:

// Variant B
if (enterprise != null && enterprise.getSubRegion() != null
  && enterprise.getSubRegion().getRegion() != null
  && enterprise.getSubRegion().getRegion().getCountry() != null) {
  ps.setInt(1, enterprise.getSubRegion().getRegion().getCountry().getId()); 
} else {
  ps.setNull(1, Types.INTEGER);
}

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

Вариант B можно улучшить с помощью вспомогательных функций в EnterpriseBean, которые немедленно возвратят регион и страну:

public RegionBean getRegion() {
  if (getSubRegion() != null) {  
    return getSubRegion().getRegion();
  } else {
    return null;
  }
}

public CountryBean getCountry() {
  if (getRegion() != null) {
    return getRegion().getCountry();
  } else {
    return null;
  }
}

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

// Variant C
if (enterprise != null && enterprise.getCountry() != null) {
  ps.setInt(1, enterprise.getCountry().getId());
} else {
  ps.setNull(1, Types.INTEGER);
}

Кроме того, прочитайте эту статью статью Joel о том, почему исключения не следует злоупотреблять. И эссе Раймона Чена.

Ответ 6

Вы должны исключить исключение, когда параметр содержит неожиданное значение.

В ваших примерах я бы рекомендовал исключить исключение, если два параметра имеют разные типы.

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

Ответ 7

Может быть, sum_ выглядит в одиночестве. Что, если, знаете ли, это действительно используется?

#foo.py
def sum_(a, b):
    if type(a) == type(b):
        return a + b

#egg.py
from foo import sum_:
def egg(c = 5):
  return sum_(3, c)

#bar.py
from egg import egg
def bar():
  return len(egg("2"))
if __name__ == "__main__":
  print bar()

Если вы запустили bar.py, вы получили бы:

Traceback (most recent call last):
  File "bar.py", line 6, in <module>
    print bar()
  File "bar.py", line 4, in bar
    return len(egg("2"))
TypeError: object of type 'NoneType' has no len()

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

Кто хочет исправить эту ошибку, придется сначала дважды проверить bar.py, а затем проанализировать egg.py, пытаясь выяснить, откуда именно это произошло. После чтения egg.py им нужно будет прочитать sum_.py и, надеюсь, заметить неявный возврат None; только тогда они понимают проблему: они не выполнили проверку типа из-за параметра egg.py, установленного для них.

Положите немного реальной сложности в этом, и вещь станет уродливой очень быстрой.

Python, в отличие от C, написан с помощью "Просто попросить прощение, чем разрешение" : помните: если что-то пойдет не так, я получить исключение. Если вы передадите мне None, где я ожидаю фактическое значение, все будет ломаться, исключение произойдет далеко от линии, фактически вызывающей его, и люди будут ругаться в вашем общем направлении на 20 разных языках, а затем изменить код, чтобы выбросить подходящее исключение (TypeError("incompatible operand type")).