7.1. Теория

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

7.1.1. Известные ошибки в ПО

История знает множество примеров, где программные ошибки стоили не только огромных денег, но и человеческих жизней [6] [7]:

7.1.1.1. 1962 г.: ракета Маринер-1

Маринер-1 - космический аппарат США для изучения Венеры (Рисунок 7.1.1).

_images/07_01_01.png

Рисунок 7.1.1 - Ракета Маринер-1 [9]

  • Описание и причина:

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

  • Примерный ущерб / потери

    Никто не погиб, однако экономические потери составили 18,3 млн. долларов.

7.1.1.2. 1985 г.: аппарат лучевой терапии Therac-25

Therac-25 - канадский аппарат лучевой терапии (Рисунок 7.1.2).

_images/07_01_02.png

Рисунок 7.1.2 - Аппарат лучевой терапии `Therac-25 [10]

  • Описание и причина:

    Неисправность была вызвана тем, что в проекте использовались библиотеки с ошибками, входящие в состав ПО аппарата Therac-20, что и привело к фатальным последствиям. В коде была найдена довольно распространенная ошибка многопоточности, называемое состоянием гонки. Тем не менее ошибку не заметили, так как Therac-20 работал исправно из-за дополнительных (аппаратных) мер предосторожности.

  • Примерный ущерб / потери

    Умерло 2 человека, 4 получили серьезное облучение.

7.1.1.3. 1991 г.: ЗРК Patriot

Patriot - американский зенитный ракетный комплекс (Рисунок 7.1.3)

_images/07_01_03.png

Рисунок 7.1.3 - ЗРК Patriot [11]

  • Описание и причина:

    Во время Войны в Персидском заливе по казармам подразделений США был нанесен ракетный удар иракскими ракетами типа Р-17 (советская баллистическая ракета). Ни одна из ракет не была перехвачена, и удар достиг цели.

    В программном обеспечении ЗРК, отвечающем за ведение и перехват цели, присутствовала ошибка, из-за которой со временем внутренние часы постепенно отходили от истинного значения времени: системное время хранилось как целое число в 24-битном регистре с точностью до 0,1 секунды; при итоговом расчете данные переводились в вещественное число.

    Проблема заключалась в том, что число \(\cfrac{1}{10}\) не имеет точного представления в двоичной системе счисления:

    • \(\cfrac{1}{10}_{10} = 0,0001100110011001100110011001100..._{2}\);

    • \(\cfrac{1}{10}_{10} = 0,00011001100110011001100_{2}\) (24-битное целое в системе Patriot);

    • ошибка 1 измерения: ~ \(0,000000095_{10}\);

    • ошибка за 100 часов работы \(0,000000095 \cdot 10 \cdot 60 \cdot 60 \cdot 100 = 0,34\) с.;

    • ракета Р-17 летит со скоростью 1676 м/c, и проходит за 0,34 с. больше полукилометра.

    Данной ошибки в измерениях было достаточно, чтобы ракета преодолела радиус поражения Patriot (Рисунок 7.1.4, Видео 7.1.1).

    _images/07_01_04.png

    Рисунок 7.1.4 - Неправильное определение зоны пролета ракеты [12]

    Видео 7.1.1 - Демонстрация ошибки ПО Patriot

  • Примерный ущерб / потери

    Погибло 28 американских солдат и еще двести получили ранения.

7.1.1.4. 2000 г.: Проблема 2000 года (Y2K)

  • Описание и причина:

    Разработчики программного обеспечения, выпущенного в XX веке, зачастую использовали два знака для представления года в датах: например, 1 января 1961 года представлялось как «01.01.61». При наступлении 1 января 2000 года при двузначном представлении года после 99 наступал 00 год (т.е. 99 + 1 = 00), что интерпретировалось многими старыми программами как 1900 год. Сложность была еще и в том, что многие программы обращались к вычислению дат вперед (например, при составлении плана закупок, планировании даты полета и т.д.) (Рисунок 7.1.5).

    _images/07_01_05.png

    Рисунок 7.1.5 - Табло показывает 3 января 1900 года, вместо 3 января 2000 года. Франция [13]

  • Примерный ущерб / потери

    30-300 млрд. долларов.

7.1.1.5. 2009-2011 г.: отзыв автомобилей Toyota

  • Описание и причина:

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

    Видео 7.1.2 - Случайное ускорение автомобиля

    В ходе десятимесячного расследования специалисты NASA выявили, что программное обеспечение не соответствует стандартам MISRA (англ. Motor Industry Software Reliability Association) и содержит 7134 нарушения. Представители Toyota ответили, что у них свои собственные стандарты.

    20 декабря 2010 года Тойота отвергнула обвинения, но выплатила 16 млрд. долларов в досудебном порядке по искам, выпустила обновление ПО для некоторых моделей машин и отозвала 5,5 млн. автомобилей [8] (Рисунок 7.1.6).

    _images/07_01_06.png

    Рисунок 7.1.6 - Lexus ES 350 2007-2010 - одна из моделей с неисправностью [14]

  • Примерный ущерб / потери

    Погибло не менее 89 человек, многомиллиардные потери компании.

Примечание

С более полным списком ошибок в программном обеспечении можно познакомиться в Википедии: https://en.wikipedia.org/wiki/List_of_software_bugs.

7.1.2. Определение и разновидности ошибок

Ошибка (также баг от англ. Software Bug) - неполадка в программе, из-за которой она ведет себя неопределенно, выдавая неожиданный результат.

Основные категории ошибок:

  • синтаксические;

  • логические;

  • ошибки времени выполнения;

  • недокументированное поведение.

7.1.2.1. Синтаксические ошибки

  • Причина:

    Несоответствие синтаксису языка программирования. Для компилируемых языков программирования синтаксическая ошибка не позволит выполнить компиляцию.

  • Пример:

    >>> for i in range(10)
      File "<stdin>", line 1
        for i in range(10)
                         ^
    SyntaxError: invalid syntax
    

7.1.2.2. Логические (семантические) ошибки

  • Причина:

    Несоответствие правильной логике работы программы.

  • Пример:

    >>> def avg_of_2(a, b):
    ...     return a + b / 2
    ...
    >>> avg_of_2(4, 8)  # Вернет 8 вместо 6
    

7.1.2.3. Ошибки времени выполнения

  • Причина:

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

  • Пример:

    >>> a = 5
    >>> b = 0
    >>> a / b
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    ZeroDivisionError: division by zero
    

7.1.2.4. Недокументированное поведение

  • Причина:

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

  • Пример:

    Одним из наиболее известных примеров являются SQL-инъекции - внедрение в запрос произвольного SQL-кода.

    -- Код на сервере
    txtUserId = getRequestString("UserId");
    txtSQL = "SELECT * FROM Users WHERE UserId = " + txtUserId;
    
    -- "Стандартный" вызов на клиенте при UserId = 105 сформирует запрос
    SELECT * FROM Users WHERE UserId = 105
    
    -- Передача в качестве 'UserId' значения
    -- "105; DROP TABLE Suppliers" приведет к удалению таблицы 'Suppliers'
    SELECT * FROM Users WHERE UserId = 105; DROP TABLE Suppliers
    

Синтаксические и ошибки времени выполнения приводят к немедленному завершению (краху (англ. Crash)) выполнения программы в отличие от логических ошибок, после которых программа может продолжить работу. Пример краха программного обеспечения приведен на Видео 7.1.3.

Видео 7.1.3 - Презентация ОС Windows 98

7.1.3. Поиск ошибок и отладка программы

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

Существуют две взаимодополняющие технологии отладки:

  • использование отладчиков: программ, которые включают в себя пользовательский интерфейс для пошагового выполнения программы: оператор за оператором, функция за функцией, с остановками на некоторых строках исходного кода или при достижении определенного условия (Рисунок 7.1.7);

    _images/07_01_07.png

    Рисунок 7.1.7 - Пример отладки в IDE PyCharm: выполнение «заморожено» на точке останова (англ. Breakpoint), при этом IDE отображает текущие значения переменных, дополнительные окна и параметры

  • вывод текущего состояния программы с помощью расположенных в критических точках программы операторов вывода — на экран, принтер, громкоговоритель или в файл. Вывод отладочных сведений в файл называется журналированием (также логгированием или аудитом) (Листинг 7.1.1).

    Листинг 7.1.1 - Пример отладочного вывода с использованием функции print() | скачать
    import random
    
    a = random.randint(1, 100)
    b = random.randint(1, 100)
    
    print(a, b)  # 44 97
    
    print(a**2 + b**2)  # 11345
    

7.1.4. Подходы к обработке ошибок

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

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

  1. «Семь раз отмерь, один раз отрежь» - LBYL (англ. Look Before You Leap);

    Суть подхода: прежде чем выполнить основное действие выполняются проверки - не получится ли деления на ноль, есть ли файл на диске и т.д.

  2. «Легче попросить прощения, чем разрешения» - EAFP (англ. «It’s Easier To Ask Forgiveness Than Permission»).

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

В Листинге 7.1.2 приведен пример сравнения двух подходов.

Листинг 7.1.2 - Псевдокод функций, использующих разные подходы к обработке ошибок: «Семь раз отмерь, один раз отрежь» и «Легче попросить прощения, чем разрешения»
Обе функции возвращают решение линейного уравнения и НИЧЕГО, если 'a' = 0


ФУНКЦИЯ найти_корень_1(a, b):
    ЕСЛИ a не равно 0
        ВЕРНУТЬ -b / a
    ИНАЧЕ
        ВЕРНУТЬ НИЧЕГО


ФУНКЦИЯ найти_корень_2(a, b):
    ОПАСНЫЙ БЛОК КОДА      # Внутри данного блока пишется код, который
        ВЕРНУТЬ -b / a     # потенциально может привести к ошибкам
    ЕСЛИ ПРОИЗОШЛА ОШИБКА  # В случае деления на 0 попадаем сюда
        ВЕРНУТЬ НИЧЕГО

Подход «Семь раз отмерь, один раз отрежь» имеет определенные минусы:

  • проверки могут уменьшить читаемость и ясность основного кода;

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

  • разработчик может легко допустить ошибку, забыв какую-либо из проверок;

  • ситуация может изменится между моментом проверки и моментом выполнения операции.

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

7.1.5. Обработка исключений в Python

7.1.5.1. Понятия исключения

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

  • BaseException (базовое исключение)

    • SystemExit (исключение, порождаемое функцией sys.exit() при выходе из программы)

    • KeyboardInterrupt (прерывании программы пользователем, Ctrl+C)

    • Exception (базовое несистемное исключение)

      • ArithmeticError (арифметическая ошибка)

        • FloatingPointError (неудачное выполнение операции с плавающей запятой)

        • OverflowError (результат арифметической операции слишком велик для представления)

        • ZeroDivisionError (деление на ноль)

      • LookupError (некорректный индекс или ключ)

        • IndexError (индекс не входит в диапазон элементов)

        • KeyError (несуществующий ключ)

      • MemoryError (недостаточно памяти)

      • NameError (не найдено переменной с таким именем)

      • OSError (ошибка, связанная с ОС - есть подклассы, например FileNotFoundError)

      • SyntaxError (синтаксическая ошибка, включает классы IndentationError и TabError)

      • SystemError (внутренняя ошибка)

      • TypeError (операция применена к объекту несоответствующего типа)

      • ValueError (аргумент правильного типа, но некорректного значения)

Пример встроенного возбуждения исключения:

>>> "я - строка" / 5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for /: 'str' and 'int'

7.1.5.2. Конструкция try

Придерживаясь идеологии «Легче попросить прощения, чем разрешения», Python предусматривает конструкцию try для обработки возникающих исключений.

try
except
else
finally
try:                              # (try строго 1)
    try_ suite                    # код, который может выполниться с ошибкой
except exception_group1 as var1:  # (except - 0 (если есть finally) и более)
    except_suite1                 # код, выполняемый в случае исключения 'exception_group1'
...                               # ссылка на исключение может быть записана в 'var1'
except exception_groupN as varN:
    except_suiteN                 # код, выполняемый в случае исключения 'exception_groupN'
...                               # except-блоков может быть произвольное кол-во
else:                             # (else - 0 или 1)
    else_suite                    # выполняется, если try не завершен преждевременно (например, break)
finally:                          # (finally - 0 или 1)
    finally_suite                 # код, который должен выполнится всегда (была ошибка выше или нет)

Ход выполнения:

  • код, который потенциально может привести к ошибке, помещается в блок try;

  • в случае ошибки, код немедленно завершается и переходит в обработчик except (если он указан для соответствующего исключения);

  • после поток выполнения переходит к else (если исключений не было) и finally (в любом случае).

На Рисунке 7.1.8 приведены общие варианты потока выполнения программы при обработке исключений.

_images/07_01_08.png

Рисунок 7.1.8 - Варианты потока выполнения программы при обработке исключений [4]

Обработка исключений (и соответственно идеология «Легче попросить прощения, чем разрешения») - предпочитаемый способ в Python, а использование блоков зависит от конкретной ситуации.

Наиболее общий вариант обработки исключений приведен в Листинге 7.1.3.

Листинг 7.1.3 - Наиболее простой способ обработки исключений | скачать
try:
    x = int(input("Введите целое число x (для вычисления 1/x): "))
    res = 1 / x

    print("1/{} = {:.2f}".format(x, res))
except:
    print("Произошла ошибка!")

# --------------
# Примеры вывода:

# Введите целое число x (для вычисления 1/x): 3
# 1/3 = 0.33

# Введите целое число x (для вычисления 1/x): qwerty
# Произошла ошибка!

Подобный вариант обработки исключений не рекомендуется, т.к. блок except будет перехватывать любое исключение, что не позволит точно определить ошибку в коде. Улучшить код можно, добавив обработку исключения по классу (Листинг 7.1.4).

Листинг 7.1.4 - Обработка общего класса исключений Exception | скачать
try:
    x = int(input("Введите целое число x (для вычисления 1/x): "))
    res = 1 / x

    print("1/{} = {:.2f}".format(x, res))
except Exception as err:
    print("Произошла ошибка!")
    print("Тип:", type(err))
    print("Описание:", err)

# --------------
# Примеры вывода:

# Введите целое число x (для вычисления 1/x): 3
# 1/3 = 0.33

# Введите целое число x (для вычисления 1/x): 5.5
# Произошла ошибка!
# Тип: <class 'ValueError'>
# Описание: invalid literal for int() with base 10: '5.5'

Рекомендуемым способом обработки исключений является как можно большая конкретизация класса исключения (Листинг 7.1.5).

Листинг 7.1.5 - Обработка конкретных классов исключений | скачать
try:
    x = int(input("Введите целое число x (для вычисления 1/x): "))
    res = 1 / x

    print("1/{} = {:.2f}".format(x, res))
except ZeroDivisionError:
    print("На ноль делить нельзя!")
except ValueError as err:  # 'err' содержит ссылку на исключение
    print("Будьте внимательны:", err)
except (FileExistsError, FileNotFoundError):  # Исключения можно перечислять в виде кортежа
    print("Этого никогда не случится - мы не работаем с файлами")
except Exception as err:
    # Все, что не обработано выше и является потомком 'Exception',
    # будет обработано здесь
    print("Произошла ошибка!")
    print("Тип:", type(err))
    print("Описание:", err)

# --------------
# Примеры вывода:

# Введите целое число x (для вычисления 1/x): 3
# 1/3 = 0.33

# Введите целое число x (для вычисления 1/x): 0
# На ноль делить нельзя!

# Введите целое число x (для вычисления 1/x): qwerty
# Будьте внимательны: invalid literal for int() with base 10: 'qwerty'

7.1.5.3. Возбуждение исключений (raise)

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

raise
raise exception(args)  # явное указание класса возбуждаемого исключения

# или

raise                  # 1) повторное возбуждение активного исключения (re-raise)
                       #    внутри блока except
                       # 2) 'TypeError' по умолчанию

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

В Листинге 7.1.6 приведен пример использование оператора raise.

Листинг 7.1.6 - Использование raise для управления потоком выполнения | скачать
MIN = 1
MAX = 10

try:
    x = int(input("Введите целое число от {} до {}: ".format(MIN, MAX)))

    if not MIN <= x <= MAX:
        # Возбудив исключение, его можно будет обработать в except
        # вместе с другими похожими исключениями
        raise ValueError("Число лежит вне интервала [{}; {}]!".format(MIN, MAX))

    print("Спасибо!")
except ValueError as err:  # 'err' содержит ссылку на исключение
    print("Будьте внимательны:", err)

# --------------
# Примеры вывода:

# Введите целое число от 1 до 10: 5
# Спасибо!

# Введите целое число от 1 до 10: 15
# Будьте внимательны: Число лежит вне интервала [1; 10]!

# Введите целое число от 1 до 10: qwerty
# Будьте внимательны: invalid literal for int() with base 10: 'qwerty'

7.1.5.4. Особенности обработки исключений внутри функций

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

Разница в обработке исключений приведена в Листинге 7.1.7.

Листинг 7.1.7 - Различная обработка исключений в функции | скачать
# Ниже представлены 3 варианта обработки исключений в функциях
# Основное правило - обработка исключений внутри возможна и нужна, однако
#                    вызывающий код должен также знать о случившемся, если
#                    влияет на дальнейшую работу


def get_1_x(x):
    """Вернуть 1/x.

    Функция не обрабатывает исключения - ответственность на вызывающем коде.
    """
    return 1/x


def get_2_x(x):
    """Вернуть 2/x.

    Функция обрабатывает исключения, "затушив" ошибку - вызывающий код
    не будет знать, сработала функция правильно или нет.

    Данный способ использовать не рекомендуется!
    """
    try:
        return 2/x
    except Exception as e:
        print("Внутри произошла ошибка...", e)


def get_3_x(x):
    """Вернуть 3/x.

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

    Внутренняя обработка исключений может быть полезна, если в целом результат
    функции не связан с внутренней ошибкой.
    """
    try:
        return 3/x
    except Exception as e:
        print("Внутри произошла ошибка...", e)
        raise

funcs = (get_1_x, get_2_x, get_3_x)
# Вызываем каждую функцию с "ошибочным" параметром
for func in funcs:
    try:
        print("-" * 50)
        print("Запущена функция:", func.__name__)
        print(func(0))
    except Exception as e:
        print("Произошла ошибка: {}.".format(e))

# -------------
# Пример вывода:

# --------------------------------------------------
# Запущена функция: get_1_x
# Произошла ошибка: division by zero.
# --------------------------------------------------
# Запущена функция: get_2_x
# Внутри произошла ошибка... division by zero
# None
# --------------------------------------------------
# Запущена функция: get_3_x
# Внутри произошла ошибка... division by zero
# Произошла ошибка: division by zero.

7.1.5.5. Утверждения (assert)

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

В Python утверждения поддерживаются оператором assert.

assert
assert boolean_expression[, optional_expression]

# boolean_expression: логическое выражение для проверки
# optional_expression: необязательное сообщение (строка)

Если boolean_expression возвращает False, возбуждается исключение AssertionError с сообщением optional_expression (если задано).

Пример использования утверждений приведен в Листинге 7.1.8.

Листинг 7.1.8 - Использование утверждений в Python | скачать
# Использование оператора assert
# поможет отследить неверно реализованную функцию


def add_to_list(x, lst=[]):
    # Использование assert здесь оправдано - список всегда
    # подразумевается пустым
    assert len(lst) == 0, "Список должен быть пуст!"

    lst.append(x)
    return lst


print(add_to_list(1))
print(add_to_list(2))

# -------------
# Пример вывода:

# [1]
# Traceback (most recent call last):
#   File "07_01_08_a.py", line 15, in <module>
#     print(add_to_list(2))
#   File "07_01_08_a.py", line 8, in add_to_list
#     assert len(lst) == 0, "Список должен быть пуст!"
# AssertionError: Список должен быть пуст!

Примечание

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

7.1.5.6. Исключения или утверждения?

При выборе, использовать исключения или утверждения, придерживайтесь правил (Таблица 7.1.1).

Таблица 7.1.1 - Использовать исключения или утверждения?

Исключения

Утверждения

Обработка ошибок, которые могут произойти во время выполнения программы (неправильный ввод пользователя и т.п.)

Проверка ситуаций, которые предположительно не могут произойти

1

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

Проверка предусловий, постусловий и инвариантов в необщедоступном коде (локальных функциях, внутреннем коде и т.д.)

2

Предоставление информации об ошибке пользователю

Предоставление информации об ошибке команде разработки

3

Восстановление из ошибочной ситуации (например, дальнейшая работа программы, если не удалось открыть файл)

Проверка невозможных ситуаций, свидетельствующих о серьезной ошибке в коде (например, выход за границы списка)

Примеры использования исключения и утверждений приведены в Листингах 7.1.9 (а-в).

Листинг 7.1.9 (а) - Использование исключений и утверждений в Python | скачать
# Исключения и утверждения могут проверять параметры функции,
# выступая в т.ч. более строгим вариантом документации


def fact(x):
    """Вернуть факториал 'x'.

    Не передавайте числа больше 15 во избежание переполнения памяти.
    """
    if x <= 1:
        return 1
    else:
        return x * fact(x-1)


def fact_save_1(x):
    assert x <= 15, \
        "Не передавайте числа больше 15 во избежание переполнения памяти"

    return fact(x)


def fact_save_2(x):
    if not x <= 15:
        raise ValueError("Не передавайте числа больше 15 во избежание "
                         "переполнения памяти")

    return fact(x)


print("{:>3} {:>20} {:>20}".format(*("x", "fact()", "fact_save()")))
for x in (5, 20):
    print("{:3}".format(x), end=" ")
    print("{:20}".format(fact(x)), end=" ")
    # print("{:20}".format(fact_save_1(x)))
    print("{:20}".format(fact_save_2(x)))

# -------------
# Пример вывода:

#   x               fact()          fact_save()
#   5                  120                  120
#  20  2432902008176640000 Traceback (most recent call last):
#   File "07_01_08_b.py", line 23, in <module>
#     print("{:20}".format(fact_save(x)))
#   File "07_01_08_b.py", line 15, in fact_save
#     assert x <= 15, "Не передавайте числа больше 15 во избежание переполнения памяти"
# AssertionError: Не передавайте числа больше 15 во избежание переполнения памяти
#
#   x               fact()          fact_save()
#   5                  120                  120
#  20  2432902008176640000 Traceback (most recent call last):
#   File "07_01_08_b.py", line 34, in <module>
#     print("{:20}".format(fact_save_2(x)))
#   File "07_01_08_b.py", line 24, in fact_save_2
#     raise ValueError("Не передавайте числа больше 15 во избежание "
# ValueError: Не передавайте числа больше 15 во избежание переполнения памяти
Листинг 7.1.9 (б) - Использование исключений и утверждений в Python | скачать
# Использование оператора assert для проверки входных и выходных данных


def make_call(accounts, account_id, mins, costs_per_min):
    """Списать со счета 'account' на 'value' баллов в случае звонка.

    Параметры:
        - accounts (dict): словарь со всеми счетами абонентов;
        - account_id (int): идентификатор абонента в словаре 'accounts';
        - mins (int): количество минут разговора;
        - costs_per_min (int): стоимость минуты разговора.
    """

    def get_costs(mins, costs_per_min):
        """Вернуть стоимость звонка.

        Параметры:
            - mins (int): количество минут разговора;
            - costs_per_min (int): стоимость минуты разговора.
        """
        return -mins * costs_per_min

    # Проверка типов
    assert isinstance(mins, int), "Параметр 'mins' имеет неверный тип!"
    assert isinstance(costs_per_min, (int, float)),\
        "Параметр 'costs_per_min' имеет неверный тип!"

    # Проверка значений
    assert mins > 0, "Параметр 'mins' должен быть > 0"
    assert costs_per_min >= 0, "Параметр 'costs' должен быть >= 0"

    # Расчет (стоимость звонка не должна быть меньше 0)
    costs_total = get_costs(mins, costs_per_min)
    assert costs_total >= 0,\
        "Расчет стоимости звонка был осуществлен неверно!"
    accounts[account_id] -= costs_total


# Словарь ID=Баланс
accounts = {"Василий Иванов": 100}
print(accounts)

try:
    make_call(accounts, "Василий Иванов", mins=4, costs_per_min=2)
except Exception as e:
    print("Во время списывания стоимости звонка произошла ошибка:", e)

print(accounts)
Листинг 7.1.9 (в) - Использование исключений и утверждений в Python | скачать
# Совместное использование исключений и утверждений

weekday_names = {
    1: "Понедельник",
    2: "Вторник",
    3: "Среда",
    4: "Четверг",
    5: "Пятница",
    6: "Суббота",
    7: "Воскресенье"
}


def weekday_name(weekday):
    """Вернуть название дня недели. Нумерация с 1.

    Параметры:
        weekday (int): номер дня недели.

    Исключения:
        - TypeError: 'weekday' не int;
        - ValueError: 'weekday' не число от 1 до 7.

    Результат:
        str: название дня недели.
    """
    # "Невозможная" ситуация - словарь 'weekday_names' может быть
    # "испорчен" - проверяется с помощью assert.
    assert weekday_names is not None and isinstance(weekday_names, dict), \
        "Внутренняя ошибка программы. Обратитесь в разрабочику."

    # Параметры функции проверяются с помощью исключений
    if not isinstance(weekday, int):
        raise TypeError("Параметр 'weekday' должен быть типа 'int'.")
    if weekday not in weekday_names:
        raise ValueError("Параметр 'weekday' должен быть целым числом "
                         "от 1 до 7.")

    return weekday_names[weekday]


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

while True:
    try:
        weekday = int(input("Введите номер дня недели (1-7): "))

        # if not 1 <= weekday <= 7:
        #     raise ValueError("Номер дня недели должен быть целым числом "
        #                      "от 1 до 7.")

        # Раскомментируйте код ниже, чтобы получить срабатывание assert
        # weekday_names = None

        print("Это -", weekday_name(weekday))

        break
    except TypeError as err:
        print("Проверьте, что введено целое число.")
    except ValueError as err:
        print("Проверьте, что введено целое число, и оно "
              "находится в допустимых границах.")
    except Exception as err:
        print("Ошибка при определении названия дня недели.")
        print(err)  # Запись в лог информации об ошибке

7.1.6. Рекомендации

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

  • код, который потенциально может привести к ошибкам, должен быть помещен в блок try;

  • блок except должен:

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

    • категорически не следует «тушить» исключения (писать пустой или бессмысленный except);

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

  • блок finally следует использовать для освобождения ресурсов (это может быть закрытие файла или сетевого соединения), независимо от того, прошла операция успешно или нет.