Как обработать ошибку деления на ноль

Improve Article

Save Article

Like Article

  • Read
  • Discuss
  • Improve Article

    Save Article

    Like Article

    We use Exception Handling to overcome exceptions occurred in execution of a program in a systematic manner.

    Dividing a number by Zero is a mathematical error (not defined) and we can use exception handling to gracefully overcome such operations. If you write a code without using exception handling then the output of division by zero will be shown as infinity which cannot be further processed.

    Consider the code given below, the Division function returns the result of numerator divided by denominator which is stored in the variable result in the main and then displayed. This Division function does not have any rumination for denominator being zero.

    #include <iostream>

    using namespace std;

    float Division(float num, float den)

    {

        return (num / den);

    }

    int main()

    {

        float numerator = 12.5;

        float denominator = 0;

        float result;

        result = Division(numerator, denominator);

        cout << "The quotient of 12.5/0 is "

             << result << endl;

    }

    Output:

    The quotient of 12.5/0 is inf
    

    We can handle this exception in a number of different ways, some of which are listed below

    • 1) Using the runtime_error class

      The runtime_error class is a derived class of Standard Library class exception, defined in exception header file for representing runtime errors.
      Now we consider the exact same code but included with handling the division by zero possibility. Here, we have the try block inside main that calls the Division function. The Division function checks if the denominator passed is equal to zero if no it returns the quotient, if yes it throws a runtime_error exception. This Exception is caught by the catch block which prints the message “Exception occurred” and then calls the what function with runtime_error object e. The what() function {used in the code given below} is a virtual function of the class Standard exception defined in stdexcept header file, it is used to identify the exception. This prints the message “Math error: Attempted to divide by Zero”, after which the program resumes the ordinary sequence of instructions.

      #include <iostream>

      #include <stdexcept> // To use runtime_error

      using namespace std;

      float Division(float num, float den)

      {

          if (den == 0) {

              throw runtime_error("Math error: Attempted to divide by Zeron");

          }

          return (num / den);

      }

      int main()

      {

          float numerator, denominator, result;

          numerator = 12.5;

          denominator = 0;

          try {

              result = Division(numerator, denominator);

              cout << "The quotient is "

                   << result << endl;

          }

          catch (runtime_error& e) {

              cout << "Exception occurred" << endl

                   << e.what();

          }

      }

      Output:

      Exception occurred
      Math error: Attempted to divide by Zero
      
    • 2) Using User defined exception handling

      Here we define a class Exception that publicly inherits from runtime_error class. Inside the class Exception, we define only a constructor that will display the message “Math error: Attempted to divide by Zero” when called using the class object. We define the Division function that calls the constructor of class Exception when denominator is zero otherwise returns the quotient. Inside of main we give some values to numerator and denominator, 12.5 and 0 respectively. Then we come to the try block that calls the Division function which will either return the quotient or throw an exception. The catch block catches the exception of type Exception, displays the message “Exception occurred” and then calls the what function. After the exception is handled the program resumes.

      #include <iostream>

      #include <stdexcept>

      using namespace std;

      class Exception : public runtime_error {

      public:

          Exception()

              : runtime_error("Math error: Attempted to divide by Zeron")

          {

          }

      };

      float Division(float num, float den)

      {

          if (den == 0)

              throw Exception();

          return (num / den);

      }

      int main()

      {

          float numerator, denominator, result;

          numerator = 12.5;

          denominator = 0;

          try {

              result = Division(numerator, denominator);

              cout << "The quotient is " << result << endl;

          }

          catch (Exception& e) {

              cout << "Exception occurred" << endl

                   << e.what();

          }

      }

      Output:

      Exception occurred
      Math error: Attempted to divide by Zero
      
    • 3) Using Stack Unwinding

      In stack unwinding we have the main inside which the try block calls the Division function which in turn calls the CheckDenominator function. The CheckDenominator function checks if denominator is zero, if true throws an exception otherwise returns the value of denominator. The Division function calculates the value of quotient {if non-zero value of denominator was passed} and returns the same to the main. The catch block catches any exception thrown and displays the message “Exception occurred” and calls the what function which prints “Math error: Attempted to divide by zero”. After this the program resumes.

      #include <iostream>

      #include <stdexcept>

      using namespace std;

      float CheckDenominator(float den)

      {

          if (den == 0) {

              throw runtime_error("Math error: Attempted to divide by zeron");

          }

          else

              return den;

      }

      float Division(float num, float den)

      {

          return (num / CheckDenominator(den));

      }

      int main()

      {

          float numerator, denominator, result;

          numerator = 12.5;

          denominator = 0;

          try {

              result = Division(numerator, denominator);

              cout << "The quotient is "

                   << result << endl;

          }

          catch (runtime_error& e) {

              cout << "Exception occurred" << endl

                   << e.what();

          }

      }

      Output:

      Exception occurred
      Math error: Attempted to divide by zero
      
    • 4) Using try and catch(…)

      In this code the try block calls the CheckDenominator function. In CheckDenominator function we check if denominator is zero, if true throw an exception by passing a string “Error”. This string is caught by the catch block and therefore prints the message “Exception occurred”. The catch block here is capable of catching exception of any type.

      #include <iostream>

      #include <stdexcept>

      using namespace std;

      float CheckDenominator(float den)

      {

          if (den == 0)

              throw "Error";

          else

              return den;

      }

      int main()

      {

          float numerator, denominator, result;

          numerator = 12.5;

          denominator = 0;

          try {

              if (CheckDenominator(denominator)) {

                  result = (numerator / denominator);

                  cout << "The quotient is "

                       << result << endl;

              }

          }

          catch (...) {

              cout << "Exception occurred" << endl;

          }

      }

      Output:

      Exception occurred
      

    Last Updated :
    23 Jan, 2019

    Like Article

    Save Article

    Сложность в «элегантной» обработке исключений заключается в том, что она сильно зависит от контекста.

    Вот, например, в одном приложении, нужно выделить UI контрол и попросить пользователя ввести валидное значение. В другом приложении (если оно серверное), нужно бросить исключение, которое должно пересечь границы приложения тем или иным способом. А в третьем приложении, проверка аргументов происхоидт на более высоком уровне и передача 0 в параметр b является багом и должно привести к крэшу приложения.

    Все это приводит к следующим возможностям:

    1. Добавить предусловие в метод Calculate, что b != 0: Contract.Requires(b != 0). В этом случае мы переклаыдваем ответственность за валидацию кода на вызывающую сторону. Мы говорим, что 0 — это невалидное значение, и просим клиента нас не трогать в этом случае.

    2. Ничего не делать и пробрасывать DivideByZeroException. Еще одна альтернатива: задокументировать метод Calculate и «сказать», что из него может прилететь это исключение.

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

    1. Добавить новый уровень косвенности. Еще, можно добавить специальный тип исключения, например, CalculationException или OperationException. В этом случае, новый тип исключения будет описывать семейство ошибок, которое может произойти при работе с абстрактной операцией.

    Это решение является более расширяемым, поскольку новые наследники могут бросать новые и новые типы исключений, а конкретный тип исключения может содержать более специфичную информацию:

    public abstract class OperationException { }
    
    public class DivideByZeroOperationException : OperationException {}
    
    public class Division : BinaryOperation
    {
        protected override decimal Calculate(decimal a, decimal b)
        {
            if (b == 0) {throw new DivideByZeroOperationException();}
    
            return a / b;
        }
    }
    

    Теперь клиент может решить, нужно ли ему обрабатывать базовое исключение или же более специфичное.

    1. Еще одна альтернатива, вернуться к умным кодам возврата и уйти от исклчюений.

    Пример:

    public enum Error { DivideByZero, YetAnotherError }
    
    // Это такой себе 'Either' тип, который может содержать либо ошибку (код и,
    // возможно, сообщение), либо валидный результат
    public class OperationResult {
      public decimal? Result {get;}
      public Error? Error {get;}
    
      public static OperationResult Success(decimal result) {
      }
    
      public static OperationResult Failure(Error error) {
      // Может быть стоит добавить сообщение или другую контекстную инфомрацию
      }
    }
    
    public class Division : BinaryOperation
    {
        protected override OperationResult Calculate(decimal a, decimal b)
        {
            if (b == 0) {return OperationResult.Error(Error.DivideByZero);}
            return a / b;
        }
    
    }
    

    Это более «функциональный» подход, в то время, как предыдущие являются более каноническими в ОО языках, таких как C#.

    Главное заключение: не существует вменяемого способа полностью обработать исключение внутри метода Calculate. Информация об ошибке должна быть передана вызывающему коду, поскольку только на более высоком уровне существует достаточно инфомрации (контекста), чтобы обработать эту проблему полноценным образом.

    Содержание:развернуть

    • Как устроен механизм исключений
    • Как обрабатывать исключения в Python (try except)
    • As — сохраняет ошибку в переменную

    • Finally — выполняется всегда

    • Else — выполняется когда исключение не было вызвано

    • Несколько блоков except

    • Несколько типов исключений в одном блоке except

    • Raise — самостоятельный вызов исключений

    • Как пропустить ошибку

    • Исключения в lambda функциях
    • 20 типов встроенных исключений в Python
    • Как создать свой тип Exception

    Программа, написанная на языке Python, останавливается сразу как обнаружит ошибку. Ошибки могут быть (как минимум) двух типов:

    • Синтаксические ошибки — возникают, когда написанное выражение не соответствует правилам языка (например, написана лишняя скобка);
    • Исключения — возникают во время выполнения программы (например, при делении на ноль).

    Синтаксические ошибки исправить просто (если вы используете IDE, он их подсветит). А вот с исключениями всё немного сложнее — не всегда при написании программы можно сказать возникнет или нет в данном месте исключение. Чтобы приложение продолжило работу при возникновении проблем, такие ошибки нужно перехватывать и обрабатывать с помощью блока try/except.

    Как устроен механизм исключений

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

    💁‍♂️ Пример: напишем скрипт, в котором функция ожидает число, а мы передаём сроку (это вызовет исключение «TypeError»):

    def b(value):
    print("-> b")
    print(value + 1) # ошибка тут

    def a(value):
    print("-> a")
    b(value)

    a("10")

    > -> a
    > -> b
    > Traceback (most recent call last):
    > File "test.py", line 11, in <module>
    > a("10")
    > File "test.py", line 8, in a
    > b(value)
    > File "test.py", line 3, in b
    > print(value + 1)
    > TypeError: can only concatenate str (not "int") to str

    В данном примере мы запускаем файл «test.py» (через консоль). Вызывается функция «a«, внутри которой вызывается функция «b«. Все работает хорошо до сточки print(value + 1). Тут интерпретатор понимает, что нельзя конкатенировать строку с числом, останавливает выполнение программы и вызывает исключение «TypeError».

    Далее ошибка передается по цепочке в обратном направлении: «b» → «a» → «test.py«. Так как в данном примере мы не позаботились обработать эту ошибку, вся информация по ошибке отобразится в консоли в виде Traceback.

    Traceback (трассировка) — это отчёт, содержащий вызовы функций, выполненные в определенный момент. Трассировка помогает узнать, что пошло не так и в каком месте это произошло.

    Traceback лучше читать снизу вверх ↑

    Пример Traceback в Python

    В нашем примере Traceback содержится следующую информацию (читаем снизу вверх):

    1. TypeError — тип ошибки (означает, что операция не может быть выполнена с переменной этого типа);
    2. can only concatenate str (not "int") to str — подробное описание ошибки (конкатенировать можно только строку со строкой);
    3. Стек вызова функций (1-я линия — место, 2-я линия — код). В нашем примере видно, что в файле «test.py» на 11-й линии был вызов функции «a» со строковым аргументом «10». Далее был вызов функции «b». print(value + 1) это последнее, что было выполнено — тут и произошла ошибка.
    4. most recent call last — означает, что самый последний вызов будет отображаться последним в стеке (в нашем примере последним выполнился print(value + 1)).

    В Python ошибку можно перехватить, обработать, и продолжить выполнение программы — для этого используется конструкция try ... except ....

    Как обрабатывать исключения в Python (try except)

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

    Например, вот как можно обработать ошибку деления на ноль:

    try:
    a = 7 / 0
    except:
    print('Ошибка! Деление на 0')

    Здесь в блоке try находится код a = 7 / 0 — при попытке его выполнить возникнет исключение и выполнится код в блоке except (то есть будет выведено сообщение «Ошибка! Деление на 0»). После этого программа продолжит свое выполнение.

    💭 PEP 8 рекомендует, по возможности, указывать конкретный тип исключения после ключевого слова except (чтобы перехватывать и обрабатывать конкретные исключения):

    try:
    a = 7 / 0
    except ZeroDivisionError:
    print('Ошибка! Деление на 0')

    Однако если вы хотите перехватывать все исключения, которые сигнализируют об ошибках программы, используйте тип исключения Exception:

    try:
    a = 7 / 0
    except Exception:
    print('Любая ошибка!')

    As — сохраняет ошибку в переменную

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

    try:
    file = open('ok123.txt', 'r')
    except FileNotFoundError as e:
    print(e)

    > [Errno 2] No such file or directory: 'ok123.txt'

    В примере выше мы обращаемся к объекту класса «FileNotFoundError» (при выводе на экран через print отобразится строка с полным описанием ошибки).

    У каждого объекта есть поля, к которым можно обращаться (например если нужно логировать ошибку в собственном формате):

    import datetime

    now = datetime.datetime.now().strftime("%d-%m-%Y %H:%M:%S")

    try:
    file = open('ok123.txt', 'r')
    except FileNotFoundError as e:
    print(f"{now} [FileNotFoundError]: {e.strerror}, filename: {e.filename}")

    > 20-11-2021 18:42:01 [FileNotFoundError]: No such file or directory, filename: ok123.txt

    Finally — выполняется всегда

    При обработке исключений можно после блока try использовать блок finally. Он похож на блок except, но команды, написанные внутри него, выполняются обязательно. Если в блоке try не возникнет исключения, то блок finally выполнится так же, как и при наличии ошибки, и программа возобновит свою работу.

    Обычно try/except используется для перехвата исключений и восстановления нормальной работы приложения, а try/finally для того, чтобы гарантировать выполнение определенных действий (например, для закрытия внешних ресурсов, таких как ранее открытые файлы).

    В следующем примере откроем файл и обратимся к несуществующей строке:

    file = open('ok.txt', 'r')

    try:
    lines = file.readlines()
    print(lines[5])
    finally:
    file.close()
    if file.closed:
    print("файл закрыт!")

    > файл закрыт!
    > Traceback (most recent call last):
    > File "test.py", line 5, in <module>
    > print(lines[5])
    > IndexError: list index out of range

    Даже после исключения «IndexError», сработал код в секции finally, который закрыл файл.

    p.s. данный пример создан для демонстрации, в реальном проекте для работы с файлами лучше использовать менеджер контекста with.

    Также можно использовать одновременно три блока try/except/finally. В этом случае:

    • в try — код, который может вызвать исключения;
    • в except — код, который должен выполниться при возникновении исключения;
    • в finally — код, который должен выполниться в любом случае.

    def sum(a, b):
    res = 0

    try:
    res = a + b
    except TypeError:
    res = int(a) + int(b)
    finally:
    print(f"a = {a}, b = {b}, res = {res}")

    sum(1, "2")

    > a = 1, b = 2, res = 3

    Else — выполняется когда исключение не было вызвано

    Иногда нужно выполнить определенные действия, когда код внутри блока try не вызвал исключения. Для этого используется блок else.

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

    b = int(input('b = '))
    c = int(input('c = '))
    try:
    a = b / c
    except ZeroDivisionError:
    print('Ошибка! Деление на 0')
    else:
    print(f"a = {a}")

    > b = 10
    > c = 1
    > a = 10.0

    В этом случае, если пользователь присвоит переменной «с» ноль, то появится исключение и будет выведено сообщение «‘Ошибка! Деление на 0′», а код внутри блока else выполняться не будет. Если ошибки не будет, то на экране появятся результаты деления.

    Несколько блоков except

    В программе может возникнуть несколько исключений, например:

    1. Ошибка преобразования введенных значений к типу float («ValueError»);
    2. Деление на ноль («ZeroDivisionError»).

    В Python, чтобы по-разному обрабатывать разные типы ошибок, создают несколько блоков except:

    try:
    b = float(input('b = '))
    c = float(input('c = '))
    a = b / c
    except ZeroDivisionError:
    print('Ошибка! Деление на 0')
    except ValueError:
    print('Число введено неверно')
    else:
    print(f"a = {a}")

    > b = 10
    > c = 0
    > Ошибка! Деление на 0

    > b = 10
    > c = питон
    > Число введено неверно

    Теперь для разных типов ошибок есть свой обработчик.

    Несколько типов исключений в одном блоке except

    Можно также обрабатывать в одном блоке except сразу несколько исключений. Для этого они записываются в круглых скобках, через запятую сразу после ключевого слова except. Чтобы обработать сообщения «ZeroDivisionError» и «ValueError» в одном блоке записываем их следующим образом:

    try:
    b = float(input('b = '))
    c = float(input('c = '))
    a = b / c
    except (ZeroDivisionError, ValueError) as er:
    print(er)
    else:
    print('a = ', a)

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

    Raise — самостоятельный вызов исключений

    Исключения можно генерировать самостоятельно — для этого нужно запустить оператор raise.

    min = 100
    if min > 10:
    raise Exception('min must be less than 10')

    > Traceback (most recent call last):
    > File "test.py", line 3, in <module>
    > raise Exception('min value must be less than 10')
    > Exception: min must be less than 10

    Перехватываются такие сообщения точно так же, как и остальные:

    min = 100

    try:
    if min > 10:
    raise Exception('min must be less than 10')
    except Exception:
    print('Моя ошибка')

    > Моя ошибка

    Кроме того, ошибку можно обработать в блоке except и пробросить дальше (вверх по стеку) с помощью raise:

    min = 100

    try:
    if min > 10:
    raise Exception('min must be less than 10')
    except Exception:
    print('Моя ошибка')
    raise

    > Моя ошибка
    > Traceback (most recent call last):
    > File "test.py", line 5, in <module>
    > raise Exception('min must be less than 10')
    > Exception: min must be less than 10

    Как пропустить ошибку

    Иногда ошибку обрабатывать не нужно. В этом случае ее можно пропустить с помощью pass:

    try:
    a = 7 / 0
    except ZeroDivisionError:
    pass

    Исключения в lambda функциях

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

    20 типов встроенных исключений в Python

    Иерархия классов для встроенных исключений в Python выглядит так:

    BaseException
    SystemExit
    KeyboardInterrupt
    GeneratorExit
    Exception
    ArithmeticError
    AssertionError
    ...
    ...
    ...
    ValueError
    Warning

    Все исключения в Python наследуются от базового BaseException:

    • SystemExit — системное исключение, вызываемое функцией sys.exit() во время выхода из приложения;
    • KeyboardInterrupt — возникает при завершении программы пользователем (чаще всего при нажатии клавиш Ctrl+C);
    • GeneratorExit — вызывается методом close объекта generator;
    • Exception — исключения, которые можно и нужно обрабатывать (предыдущие были системными и их трогать не рекомендуется).

    От Exception наследуются:

    1 StopIteration — вызывается функцией next в том случае если в итераторе закончились элементы;

    2 ArithmeticError — ошибки, возникающие при вычислении, бывают следующие типы:

    • FloatingPointError — ошибки при выполнении вычислений с плавающей точкой (встречаются редко);
    • OverflowError — результат вычислений большой для текущего представления (не появляется при операциях с целыми числами, но может появиться в некоторых других случаях);
    • ZeroDivisionError — возникает при попытке деления на ноль.

    3 AssertionError — выражение, используемое в функции assert неверно;

    4 AttributeError — у объекта отсутствует нужный атрибут;

    5 BufferError — операция, для выполнения которой требуется буфер, не выполнена;

    6 EOFError — ошибка чтения из файла;

    7 ImportError — ошибка импортирования модуля;

    8 LookupError — неверный индекс, делится на два типа:

    • IndexError — индекс выходит за пределы диапазона элементов;
    • KeyError — индекс отсутствует (для словарей, множеств и подобных объектов);

    9 MemoryError — память переполнена;

    10 NameError — отсутствует переменная с данным именем;

    11 OSError — исключения, генерируемые операционной системой:

    • ChildProcessError — ошибки, связанные с выполнением дочернего процесса;
    • ConnectionError — исключения связанные с подключениями (BrokenPipeError, ConnectionResetError, ConnectionRefusedError, ConnectionAbortedError);
    • FileExistsError — возникает при попытке создания уже существующего файла или директории;
    • FileNotFoundError — генерируется при попытке обращения к несуществующему файлу;
    • InterruptedError — возникает в том случае если системный вызов был прерван внешним сигналом;
    • IsADirectoryError — программа обращается к файлу, а это директория;
    • NotADirectoryError — приложение обращается к директории, а это файл;
    • PermissionError — прав доступа недостаточно для выполнения операции;
    • ProcessLookupError — процесс, к которому обращается приложение не запущен или отсутствует;
    • TimeoutError — время ожидания истекло;

    12 ReferenceError — попытка доступа к объекту с помощью слабой ссылки, когда объект не существует;

    13 RuntimeError — генерируется в случае, когда исключение не может быть классифицировано или не подпадает под любую другую категорию;

    14 NotImplementedError — абстрактные методы класса нуждаются в переопределении;

    15 SyntaxError — ошибка синтаксиса;

    16 SystemError — сигнализирует о внутренне ошибке;

    17 TypeError — операция не может быть выполнена с переменной этого типа;

    18 ValueError — возникает когда в функцию передается объект правильного типа, но имеющий некорректное значение;

    19 UnicodeError — исключение связанное с кодирование текста в unicode, бывает трех видов:

    • UnicodeEncodeError — ошибка кодирования;
    • UnicodeDecodeError — ошибка декодирования;
    • UnicodeTranslateError — ошибка перевода unicode.

    20 Warning — предупреждение, некритическая ошибка.

    💭 Посмотреть всю цепочку наследования конкретного типа исключения можно с помощью модуля inspect:

    import inspect

    print(inspect.getmro(TimeoutError))

    > (<class 'TimeoutError'>, <class 'OSError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>)

    📄 Подробное описание всех классов встроенных исключений в Python смотрите в официальной документации.

    Как создать свой тип Exception

    В Python можно создавать свои исключения. При этом есть одно обязательное условие: они должны быть потомками класса Exception:

    class MyError(Exception):
    def __init__(self, text):
    self.txt = text

    try:
    raise MyError('Моя ошибка')
    except MyError as er:
    print(er)

    > Моя ошибка


    С помощью try/except контролируются и обрабатываются ошибки в приложении. Это особенно актуально для критически важных частей программы, где любые «падения» недопустимы (или могут привести к негативным последствиям). Например, если программа работает как «демон», падение приведет к полной остановке её работы. Или, например, при временном сбое соединения с базой данных, программа также прервёт своё выполнение (хотя можно было отловить ошибку и попробовать соединиться в БД заново).

    Вместе с try/except можно использовать дополнительные блоки. Если использовать все блоки описанные в статье, то код будет выглядеть так:

    try:
    # попробуем что-то сделать
    except (ZeroDivisionError, ValueError) as e:
    # обрабатываем исключения типа ZeroDivisionError или ValueError
    except Exception as e:
    # исключение не ZeroDivisionError и не ValueError
    # поэтому обрабатываем исключение общего типа (унаследованное от Exception)
    # сюда не сходят исключения типа GeneratorExit, KeyboardInterrupt, SystemExit
    else:
    # этот блок выполняется, если нет исключений
    # если в этом блоке сделать return, он не будет вызван, пока не выполнился блок finally
    finally:
    # этот блок выполняется всегда, даже если нет исключений else будет проигнорирован
    # если в этом блоке сделать return, то return в блоке

    Подробнее о работе с исключениями в Python можно ознакомиться в официальной документации.

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

    Деление целых чисел на ноль

    Описывать процесс создания калькулятора я не буду, т.к. процесс, по сути, ни чём не отличается от калькулятора, в котором используются только целые числа, а сразу перенесёмся в конец таких уроков, когда начинают разбирать допущенные ошибки. Среди которых самая распространенная, это необработанное исключение, которое возникает в результате деления целого числа на ноль, например:

    static void Main (string [] args)
    {
    int a = 0;
    int b = 5 / a;
    int c = b;
    Console.Write(c);
    Console.ReadLine();
    }

    В данном примере при делении целого числа на ноль возникает исключение DivideByZeroException, после чего выполнение программы прекращается.

    исключение DivideByZeroException

    Обычно в такой ситуации нам предлагают использовать блоки try catch, чтобы отловить исключение, а затем выдать сообщение об ошибке, либо предложить какую-то иную логику, например:

    static void Main (string [] args)
    {
    try
    {
    int a = 0;
    int b = 5 / a;
    int c = b;
    Console.Write(c); 
    }
    catch (DivideByZeroException ex)
    {
    Console.WriteLine(ex.Message);
    }
    }

    В этот раз при возникновении исключения попадаем в блок catch.

    attempted to divide by zero

    Посмотрев этот видеоурок, начинающие программисты очень часто делают для себя вывод, что блоки try catch, не зависимо от того, делите вы на ноль целое или дробное число, являются универсальным средством решения данной проблемы. Но, так ли это на самом деле?

    Деление дробных чисел на ноль

    Предлагаю взглянуть на следующий код:

    static void Main( string [] args)
    {
    try
    {
    int a = 0;
    float b = 5.2F / a;
    float c = b;
    Console.Write(c);
    Console.ReadLine(); 
    }
    catch (DivideByZeroException ex)
    {
    Console.Write(ex.Message);
    }
    }

    Казалось бы, всё учтено, в коде добавлены блоки try catch, и теперь можно делить на ноль любые числа и ничего не боятся. Но, давайте разберём этот пример. Поставим точку остановы, и посмотрим, чему будет равно значение переменной b после операции деления на ноль. Многие скажут, что и так всё понятно возникнет исключение, которое будет обработано блоком catch. Но давайте всё-таки взглянем на результат.

    шаг1

    продолжим выполнение программы

    шаг2

    И так что мы видим. Главное это то, что ожидаемого выброса исключения при делении дробного числа на ноль не происходит, вместо этого переменной b было присвоено значение Infinity (бесконечность), после чего программа успешно продолжила свое выполнение. Установленные же блоки try catch не помогли нам выявить деление на ноль, потому что их работа связана с обработкой возникающих исключений, которых в данном случае — нет, как в более раннем примере с целыми числами. Поэтому в качестве результата получаем следующую картинку:

    результат

    Судя по коду, программист явно не ожидал такого результата и не предполагал, что дальнейшие расчеты будут происходить со значением бесконечность. Хотя при этом, казалось бы, все сделано так, как объясняли в уроке с целыми числами, в чём же проблема?

    Стандарт IEEE 754

    На самом деле всё просто. Для арифметических операций с плавающей точкой используется стандарт IEEE 754 (IEEE floating point) или (The IEEE Standard for Floating-Point Arithmetic), в котором описываются: правила округления, арифметические форматы, операции, обработка исключений и многое другое.

    Возникновение исключения, при делении дробного числа на ноль, так же описано в стандарте и попадает в список пяти исключений, при возникновении которых вместо exception возвращается значение по умолчанию. В данном случае таким значением является ±Infinity (которое и было получено ранее в примере). Дополнительная обработка таких ситуаций так же не требуется, поэтому добавленные блоки try catch, хоть и указаны в коде, но никакой роли не играют.

    Вывод: при делении на ноль типов с плавающей точкой никогда не генерируется исключение, вместо этого всегда возвращается значение, по умолчанию описанное в стандарте, то есть ±Infinity, но так же возможно значение NaN.

    Как определить, было ли выполнено деление на ноль?

    Например, можно воспользоваться следующим способом: cначала позволим программе выполнить деление на ноль, в результате получаем +-infinity, а затем добавим проверку с методом float.IsInfinity() либо double.IsInfinity() в зависимости от типа. Проверка ловит, как минус, так и плюс бесконечность.

    например:

    static void Main (string [] args)
    {
    int a = 0;
    float b = 5.2F / a;	 //Получаем Infinity
    if (float.IsInfinity(b))
    {
    //будет выведено это сообщение
    Console.Write("деление на ноль!");
    }
    else
    {
    float c = b;
    Console.Write(c);
    }
    }

    Но, здесь есть один нюанс, текст сообщения:

    Console.Write("деление на ноль!");

    Данное сообщение может быть не верным, в ситуации арифметического переполнения переменной b, возникновение которого так же описано в списке пяти исключений стандарта IEEE754, когда вместо выброса исключения возвращают значение по умолчанию, которым опять же является ±Infinity.

    например:

    float a = 0.1F;
    //переполнение
    float b = float.MinValue / a;
    Console.Write(b);
    Console.ReadLine();     //-Infinity

    Установленная ранее проверка float.IsInfinity() успешно отработает и на консоль будет выведено сообщение:»деление на ноль», что, конечно же, не верно.

    NaN

    Ещё один нюанс возникает, если переменная b будет иметь значение 0.

    было:

    float b = 5,2F / a;

    стало:

    float b = 0.0F / a;

    То есть в данном примере будем делить ноль на ноль. Всё остальное оставим как есть, нажимаем F5

    NaN

    Как видно на картинке в результате деления переменная b содержит NaN (Not-a-Number) или (значение, не являющееся числом). Это ещё одно значение, которое можно получить в результате операции деления дробного числа на ноль. При этом никаких исключений не выбрасывается. Добавленная проверка float.IsInfinity() в данном случаи выдает false, а значит код в блоке else выполнится успешно.

    результат

    Чтобы отловить этот момент в коде, можно воспользоваться методом float.IsNaN()

    if (float.IsInfinity(b) | float.IsNaN(b))
    {
    Console.Write(b); //+-Infinity либо NaN
    }

    На этом всё. Надеюсь, это статья поможет Вам избежать подобных ошибок в вашем коде.

    Добавлено:

    В языке c# существует только два типа с плавающей точкой (IEEE floating point) — это float и double, которые реализованы на основе стандарта IEEE 754. Поэтому при делении на ноль числа типа Decimal будет выброшено исключение DivideByZeroException.

    Читайте также:

    • Как получить исходный код страницы с помощью класса WebBrowser?
    • WebBrowser работа с контентом
    • Удаление строки в DataGridView


    Обработка исключений

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

    Напишем программу, которая будет считать обратные значения для целых чисел из заданного диапазона и выводить их в одну строку с разделителем ‘;’. Один из вариантов кода для решения этой задачи выглядит так:

    print(";".join(str(1 / x) for x in range(int(input()), int(input()) + 1)))
    

    Программа получилась в одну строчку за счёт использования списочных выражений. Однако при вводе диапазона чисел, включающего в себя 0 (например, от -1 до 1), программа выдаст следующую ошибку:

    ZeroDivisionError: division by zero

    В программе произошла ошибка «деление на ноль». Такая ошибка, возникающая при выполнении программы и останавливающая её работу, называется исключением.

    Попробуем в нашей программе избавиться от возникновения исключения деления на ноль. Пусть при попадании 0 в диапазон чисел обработка не производится и выводится сообщение «Диапазон чисел содержит 0». Для этого нужно проверить до списочного выражения наличие нуля в диапазоне:

    interval = range(int(input()), int(input()) + 1)
    if 0 in interval:
        print("Диапазон чисел содержит 0.")
    else:
        print(";".join(str(1 / x) for x in interval))
    

    Теперь для диапазона, включающего в себя 0, например от -2 до 2, исключения ZeroDivisionError не возникнет. Однако при вводе строки, которую невозможно преобразовать в целое число (например, «a»), будет вызвано другое исключение:

    ValueError: invalid literal for int() with base 10: 'a'

    Произошло исключение ValueError. Для борьбы с этой ошибкой нам придётся проверить, что строка состоит только из цифр. Сделать это нужно до преобразования в число. Тогда наша программа будет выглядеть так:

    start = input()
    end = input()
    # Метод lstrip("-"), удаляющий символы "-" в начале строки, нужен для учёта
    # отрицательных чисел, иначе isdigit() вернёт для них False
    if not (start.lstrip("-").isdigit() and end.lstrip("-").isdigit()):
        print("
        ввести два числа.")
    else:
        interval = range(int(start), int(end) + 1)
        if 0 in interval:
            print("Диапазон чисел содержит 0.")
        else:
            print(";".join(str(1 / x) for x in interval))
    

    Теперь наша программа работает без ошибок и при вводе строк, которые нельзя преобразовать в целое число.

    Подход, который был нами применён для предотвращения ошибок, называется Look Before You Leap (LBYL), или «Посмотри перед прыжком». В программе, реализующей такой подход, проверяются возможные условия возникновения ошибок до исполнения основного кода.

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

    Существует другой подход для работы с ошибками: Easier to Ask Forgiveness than Permission (EAFP), или «Проще попросить прощения, чем разрешения». В этом подходе сначала исполняется код, а в случае возникновения ошибок происходит их обработка. Подход EAFP реализован в Python в виде обработки исключений.

    Исключения в Python являются классами ошибок. В Python есть много стандартных исключений. Они имеют определённую иерархию за счёт механизма наследования классов. В документации Python версии 3.10.8 приводится следующее дерево иерархии стандартных исключений:

    BaseException
     +-- SystemExit
     +-- KeyboardInterrupt
     +-- GeneratorExit
     +-- Exception
          +-- StopIteration
          +-- StopAsyncIteration
          +-- ArithmeticError
          |    +-- FloatingPointError
          |    +-- OverflowError
          |    +-- ZeroDivisionError
          +-- AssertionError
          +-- AttributeError
          +-- BufferError
          +-- EOFError
          +-- ImportError
          |    +-- ModuleNotFoundError
          +-- LookupError
          |    +-- IndexError
          |    +-- KeyError
          +-- MemoryError
          +-- NameError
          |    +-- UnboundLocalError
          +-- OSError
          |    +-- BlockingIOError
          |    +-- ChildProcessError
          |    +-- ConnectionError
          |    |    +-- BrokenPipeError
          |    |    +-- ConnectionAbortedError
          |    |    +-- ConnectionRefusedError
          |    |    +-- ConnectionResetError
          |    +-- FileExistsError
          |    +-- FileNotFoundError
          |    +-- InterruptedError
          |    +-- IsADirectoryError
          |    +-- NotADirectoryError
          |    +-- PermissionError
          |    +-- ProcessLookupError
          |    +-- TimeoutError
          +-- ReferenceError
          +-- RuntimeError
          |    +-- NotImplementedError
          |    +-- RecursionError
          +-- SyntaxError
          |    +-- IndentationError
          |         +-- TabError
          +-- SystemError
          +-- TypeError
          +-- ValueError
          |    +-- UnicodeError
          |         +-- UnicodeDecodeError
          |         +-- UnicodeEncodeError
          |         +-- UnicodeTranslateError
          +-- Warning
               +-- DeprecationWarning
               +-- PendingDeprecationWarning
               +-- RuntimeWarning
               +-- SyntaxWarning
               +-- UserWarning
               +-- FutureWarning
               +-- ImportWarning
               +-- UnicodeWarning
               +-- BytesWarning
               +-- EncodingWarning
               +-- ResourceWarning

    Для обработки исключения в Python используется следующий синтаксис:

    try:
        <код , который может вызвать исключения при выполнении>
    except <классисключения_1>:
        <код обработки исключения>
    except <классисключения_2>:
        <код обработки исключения>
    ...
    else:
        <код выполняется, если не вызвано исключение в блоке try>
    finally:
        <код , который выполняется всегда>

    Блок try содержит код, в котором нужно обработать исключения, если они возникнут.
    При возникновении исключения интерпретатор последовательно проверяет, в каком из блоков except обрабатывается это исключение.
    Исключение обрабатывается в первом блоке except, обрабатывающем класс этого исключения или базовый класс возникшего исключения.
    Необходимо учитывать иерархию исключений для определения порядка их обработки в блоках except. Начинать обработку исключений следует с более узких классов исключений. Если начать с более широкого класса исключения, например Exception, то всегда при возникновении исключения будет срабатывать первый блок except.
    Сравните два следующих примера. В первом порядок обработки исключений указан от производных классов к базовым, а во втором — наоборот.

    Первый пример:

    try:
        print(1 / int(input()))
    except ZeroDivisionError:
        print("Ошибка деления на ноль.")
    except ValueError:
        print("Невозможно преобразовать строку в число.")
    except Exception:
        print("Неизвестная ошибка.")
    

    При вводе значений «0» и «a» получим ожидаемый, соответствующий возникающим исключениям вывод:

    Невозможно преобразовать строку в число.

    и

    Ошибка деления на ноль.

    Второй пример:

    try:
        print(1 / int(input()))
    except Exception:
        print("Неизвестная ошибка.")
    except ZeroDivisionError:
        print("Ошибка деления на ноль.")
    except ValueError:
        print("Невозможно преобразовать строку в число.")
    

    При вводе значений «0» и «a» получим в обоих случаях неинформативный вывод:

    Неизвестная ошибка.

    Необязательный блок else выполняет код в случае, если в блоке try не вызвано исключение. Добавим блок else в пример для вывода сообщения об успешном выполнении операции:

    try:
        print(1 / int(input()))
    except ZeroDivisionError:
        print("Ошибка деления на ноль.")
    except ValueError:
        print("Невозможно преобразовать строку в число.")
    except Exception:
        print("Неизвестная ошибка.")
    else:
        print("Операция выполнена успешно.")
    

    Теперь при вводе корректного значения, например «5», вывод программы будет следующим:

    2.0
    Операция выполнена успешно.

    Блок finally выполняется всегда, даже если возникло какое-то исключение, не учтённое в блоках except, или код в этих блоках сам вызвал какое-либо исключение. Добавим в нашу программу вывод строки «Программа завершена» в конце программы даже при возникновении исключений:

    try:
        print(1 / int(input()))
    except ZeroDivisionError:
        print("Ошибка деления на ноль.")
    except ValueError:
        print("Невозможно преобразовать строку в число.")
    except Exception:
        print("Неизвестная ошибка.")
    else:
        print("Операция выполнена успешно.")
    finally:
        print("Программа завершена.")
    

    Перепишем код, созданный с применением подхода LBYL, для первого примера из этой главы с использованием обработки исключений:

    try:
        print(";".join(str(1 / x) for x in range(int(input()), int(input()) + 1)))
    except ZeroDivisionError:
        print("Диапазон чисел содержит 0.")
    except ValueError:
        print("Необходимо ввести два числа.")
    

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

    Исключения можно принудительно вызывать с помощью оператора raise. Этот оператор имеет следующий синтаксис:

    raise <класс исключения>(параметры)

    В качестве параметра можно, например, передать строку с сообщением об ошибке.

    Создание собственных исключений

    В Python можно создавать свои собственные исключения. Синтаксис создания исключения такой же, как и у создания класса. При создании исключения его необходимо наследовать от какого-либо стандартного класса-исключения.

    Напишем программу, которая выводит сумму списка целых чисел и вызывает исключение, если в списке чисел есть хотя бы одно чётное или отрицательное число. Создадим свои классы исключений:

    • NumbersError — базовый класс исключения;
    • EvenError — исключение, которое вызывается при наличии хотя бы одного чётного числа;
    • NegativeError — исключение, которое вызывается при наличии хотя бы одного отрицательного числа.
    class NumbersError(Exception):
        pass
    
    
    class EvenError(NumbersError):
        pass
    
    
    class NegativeError(NumbersError):
        pass
    
    
    def no_even(numbers):
        if all(x % 2 != 0 for x in numbers):
            return True
        raise EvenError("В списке не должно быть чётных чисел")
    
    
    def no_negative(numbers):
        if all(x >= 0 for x in numbers):
            return True
        raise NegativeError("В списке не должно быть отрицательных чисел")
    
    
    def main():
        print("Введите числа в одну строку через пробел:")
        try:
            numbers = [int(x) for x in input().split()]
            if no_negative(numbers) and no_even(numbers):
                print(f"Сумма чисел равна: {sum(numbers)}.")
        except NumbersError as e:  # обращение к исключению как к объекту
            print(f"Произошла ошибка: {e}.")
        except Exception as e:
            print(f"Произошла непредвиденная ошибка: {e}.")
    
            
    if __name__ == "__main__":
        main()
    

    Модули

    Обратите внимание: в программе основной код выделен в функцию main. А код вне функций содержит только условный оператор и вызов функции main при выполнении условия __name__ == "__main__". Это условие проверяет, запущен ли файл как самостоятельная программа или импортирован как модуль.

    Любая программа, написанная на языке программирования Python, может быть импортирована как модуль в другую программу. В идеологии Python импортировать модуль — значит полностью его выполнить. Если основной код модуля содержит вызовы функций, ввод или вывод данных без использования указанного условия __name__ == "__main__", то произойдёт полноценный запуск программы. А это не всегда удобно, если из модуля нужна только отдельная функция или какой-либо класс.

    При изучении модуля itertools мы говорили о том, как импортировать модуль в программу. Покажем ещё раз два способа импорта на примере собственного модуля.

    Для импорта модуля из файла, например example_module.py, нужно указать его имя, если он находится в той же папке, что и импортирующая его программа:

    import example_module
    

    Если требуется отдельный компонент модуля, например функция или класс, то импорт можно осуществить так:

    from example_module import some_function, ExampleClass
    

    Обратите внимание: при втором способе импортированные объекты попадают в пространство имён новой программы. Это означает, что они будут объектами новой программы и в программе не должно быть других объектов с такими же именами.

    Рассмотрим написанное выше на примере. Пусть имеется программа module_hello.py, в которой находится функция hello(name), возвращающая строку приветствия пользователя по имени. В самой программе кроме функции присутствует вызов этой функции и печать результата её работы. Импортируем из модуля module_hello.py функцию hello(name) в другую программу program.py и также используем для вывода приветствия пользователя.

    Код программы module_hello.py:

    def hello(name):
        return f"Привет, {name}!"
    
    
    print(hello(input("Введите своё имя: ")))
    

    Код программы program.py:

    from module_hello import hello
    
    print(hello(input("Добрый день. Введите имя: ")))
    

    При выполнении program.py нас ожидает неожиданное действие. Программа сначала запросит имя пользователя, а затем сделает это ещё раз, но с приветствием из program.py.

    Введите своё имя: Андрей
    Привет, Андрей!
    Добрый день. Введите имя: Андрей
    Привет, Андрей!

    Наша ошибка заключается в том, что программа module_hello.py выполняется полностью, включая основной код с вызовом функции и выводом результата. Исправим программу module_hello.py, добавив проверку, запущена программа или импортирована как модуль:

    def hello(name):
        return f"Привет, {name}!"
    
    
    if __name__ == "__main__":
        print(hello(input("Введите своё имя: ")))
    

    Теперь при импорте модуля module_hello.py код в теле условного оператора выполняться не будет. А основной код этой программы выполнится только при запуске файла как отдельной программы.
    Для большего удобства обычно в теле указанного условного оператора вызывают функцию main(), а основной код программы оформляют уже внутри этой функции.
    Тогда наш модуль можно переписать так:

    def hello(name):
        return f"Привет, {name}!"
    
    
    def main():
        print(hello(input("Введите своё имя: ")))
    
    
    if __name__ == "__main__":
        main()
    

    Обратите внимание: при импорте модуля мы можем с помощью символа * указать, что необходимо импортировать все объекты. Например, так:

    from some_module import *
    

    Однако делать так крайне не рекомендуется, потому что все объекты модуля добавляются в пространство имён нашей программы, что может приводить к конфликтам.

    Понравилась статья? Поделить с друзьями:
  • Как обработать ошибку в питоне
  • Как обойти ошибку недоступно в вашем регионе
  • Как обработать ошибку в python
  • Как обойти ошибку недоступно в вашей стране
  • Как обработать ошибку в excel