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).
Описание и причина:
Неисправность была вызвана тем, что в проекте использовались библиотеки с ошибками, входящие в состав ПО аппарата 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).
Примерный ущерб / потери
Погибло 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).
Примерный ущерб / потери
30-300 млрд. долларов.
7.1.1.5. 2009-2011 г.: отзыв автомобилей Toyota¶
Описание и причина:
Неисправность в дроссельной заслонке (регулирует количество горючей смеси, поступающей в цилиндры двигателя внутреннего сгорания) с электронным контролем (ETC) приводила к случайным ускорениям автомобиля (Видео 7.1.2).
В ходе десятимесячного расследования специалисты NASA выявили, что программное обеспечение не соответствует стандартам MISRA (англ. Motor Industry Software Reliability Association) и содержит 7134 нарушения. Представители Toyota ответили, что у них свои собственные стандарты.
20 декабря 2010 года Тойота отвергнула обвинения, но выплатила 16 млрд. долларов в досудебном порядке по искам, выпустила обновление ПО для некоторых моделей машин и отозвала 5,5 млн. автомобилей [8] (Рисунок 7.1.6).
Примерный ущерб / потери
Погибло не менее 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. Поиск ошибок и отладка программы¶
Поиск ошибок выполняется на этапе тестирования программного обеспечения, в процессе которого происходит обнаружение, локализация и устранение ошибок - или отладка. В процессе отладки происходит определение текущих значений переменных и путей выполнения программы, приведших к сбою.
Существуют две взаимодополняющие технологии отладки:
использование отладчиков: программ, которые включают в себя пользовательский интерфейс для пошагового выполнения программы: оператор за оператором, функция за функцией, с остановками на некоторых строках исходного кода или при достижении определенного условия (Рисунок 7.1.7);
вывод текущего состояния программы с помощью расположенных в критических точках программы операторов вывода — на экран, принтер, громкоговоритель или в файл. Вывод отладочных сведений в файл называется журналированием (также логгированием или аудитом) (Листинг 7.1.1).
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. Подходы к обработке ошибок¶
При написании кода необходимо предусматривать, что в определенном месте программы может возникнуть ошибка и дополнять код на случай ее возникновения.
Существует два ключевых подхода программирования реакции на возможные ошибки:
«Семь раз отмерь, один раз отрежь» - 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 приведены общие варианты потока выполнения программы при обработке исключений.
Обработка исключений (и соответственно идеология «Легче попросить прощения, чем разрешения») - предпочитаемый способ в 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
следует использовать для освобождения ресурсов (это может быть закрытие файла или сетевого соединения), независимо от того, прошла операция успешно или нет.