7.1. Теория¶
Создание программного обеспечения - сложный процесс, осуществляемый коллективом специалистов (реже - одним человеком), а людям свойственно допускать ошибки (человеческий фактор). В связи с этим, код необходимо писать так, чтобы сводить возможные ошибки к минимуму, а также иметь возможность их эффективно определять, искать и обрабатывать, что является одним из признаков качественного программного обеспечения.
7.1.1. Известные ошибки в ПО¶
История знает множество примеров, где программные ошибки стоили не только огромных денег, но и человеческих жизней [6] [7]:
7.1.1.1. 1962 г.: ракета Маринер-1¶
Маринер-1 - космический аппарат США для изучения Венеры (Рисунок 7.1.1).
Описание и причина:
Программист сделал ошибку, когда переводил рукописные математические формулы в код. Символ логического отрицания ¬ он принял за минус, и это привело к тому, что ракета воспринимала нормальные скорости как критические и из-за этого сбилась с курса.
Примерный ущерб / потери
Никто не погиб, однако экономические потери составили 18,3 млн. долларов.
7.1.1.2. 1985 г.: аппарат лучевой терапии Therac-25¶
Therac-25 - канадский аппарат лучевой терапии (Рисунок 7.1.2).
Рисунок 7.1.2 - Аппарат лучевой терапии `Therac-25 [10]¶
Описание и причина:
Неисправность была вызвана тем, что в проекте использовались библиотеки с ошибками, входящие в состав ПО аппарата Therac-20, что и привело к фатальным последствиям. В коде была найдена довольно распространенная ошибка многопоточности, называемое состоянием гонки. Тем не менее ошибку не заметили, так как Therac-20 работал исправно из-за дополнительных (аппаратных) мер предосторожности.
Примерный ущерб / потери
Умерло 2 человека, 4 получили серьезное облучение.
7.1.1.3. 1991 г.: ЗРК Patriot¶
Patriot - американский зенитный ракетный комплекс (Рисунок 7.1.3)
Описание и причина:
Во время Войны в Персидском заливе по казармам подразделений США был нанесен ракетный удар иракскими ракетами типа Р-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).
Рисунок 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).
Рисунок 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).
Рисунок 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);
Рисунок 7.1.7 - Пример отладки в IDE PyCharm: выполнение «заморожено» на точке останова (англ. Breakpoint), при этом IDE отображает текущие значения переменных, дополнительные окна и параметры¶
вывод текущего состояния программы с помощью расположенных в критических точках программы операторов вывода — на экран, принтер, громкоговоритель или в файл. Вывод отладочных сведений в файл называется журналированием (также логгированием или аудитом) (Листинг 7.1.1).
7.1.4. Подходы к обработке ошибок¶
При написании кода необходимо предусматривать, что в определенном месте программы может возникнуть ошибка и дополнять код на случай ее возникновения.
Существует два ключевых подхода программирования реакции на возможные ошибки:
«Семь раз отмерь, один раз отрежь» - LBYL (англ. Look Before You Leap);
Суть подхода: прежде чем выполнить основное действие выполняются проверки - не получится ли деления на ноль, есть ли файл на диске и т.д.
«Легче попросить прощения, чем разрешения» - EAFP (англ. «It’s Easier To Ask Forgiveness Than Permission»).
Суть подхода: сначала выполняется основное действие, а затем, если возникнут, обрабатываются ошибки. Данный механизм поддерживается в различных языках программирования и называется обработкой исключений, а сама ошибочная ситуация - исключением.
В Листинге 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 приведены общие варианты потока выполнения программы при обработке исключений.
Рисунок 7.1.8 - Варианты потока выполнения программы при обработке исключений [4]¶
Обработка исключений (и соответственно идеология «Легче попросить прощения, чем разрешения») - предпочитаемый способ в Python, а использование блоков зависит от конкретной ситуации.
Наиболее общий вариант обработки исключений приведен в Листинге 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).
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).
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.
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.
# Ниже представлены 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.
# Использование оператора 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).
№ |
Исключения |
Утверждения |
|---|---|---|
Обработка ошибок, которые могут произойти во время выполнения программы (неправильный ввод пользователя и т.п.) |
Проверка ситуаций, которые предположительно не могут произойти |
|
1 |
Проверка параметров общедоступных функций (исключения не могут быть отключены - проверка будет осуществлена всегда, а также класс исключения говорит о конкретном типе ошибки в отличие от утверждения) |
Проверка предусловий, постусловий и инвариантов в необщедоступном коде (локальных функциях, внутреннем коде и т.д.) |
2 |
Предоставление информации об ошибке пользователю |
Предоставление информации об ошибке команде разработки |
3 |
Восстановление из ошибочной ситуации (например, дальнейшая работа программы, если не удалось открыть файл) |
Проверка невозможных ситуаций, свидетельствующих о серьезной ошибке в коде (например, выход за границы списка) |
Примеры использования исключения и утверждений приведены в Листингах 7.1.9 (а-в).
# Исключения и утверждения могут проверять параметры функции,
# выступая в т.ч. более строгим вариантом документации
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 во избежание переполнения памяти
# Использование оператора 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)
# Совместное использование исключений и утверждений
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следует использовать для освобождения ресурсов (это может быть закрытие файла или сетевого соединения), независимо от того, прошла операция успешно или нет.