Введение

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

В этой лабораторной работе вы изучите несколько фундаментальных методов отладки в Python. Мы рассмотрим, как:

  • Вручную вызывать (raise) и обрабатывать исключения (exceptions) для управления потоком выполнения программы во время возникновения ошибок.
  • Захватывать подробные трассировки ошибок (tracebacks) в виде строк для целей логирования.
  • Использовать утверждения (assertions) в качестве проверок корректности (sanity checks) для раннего обнаружения логических ошибок.
  • Реализовать гибкую систему логирования (logging) для отслеживания выполнения вашей программы и диагностики проблем без прерывания её потока.

Вызов исключений

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

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

Примечание: Если вы не знакомы с использованием WebIDE (похожего на VS Code), пожалуйста, изучите его сначала в этой лаборатории.

Давайте попробуем это на примере.

  1. Откройте файл raise_exception.py в проводнике файлов слева.
  2. Добавьте следующий код в файл. Этот код определяет функцию box_print, которая вызывает исключения для недопустимых аргументов, а затем вызывает эту функцию с допустимыми и недопустимыми входными данными, перехватывая исключения.
def box_print(symbol, width, height):
    if len(symbol) != 1:
      raise Exception('Symbol must be a single character string.')
    if width <= 2:
      raise Exception('Width must be greater than 2.')
    if height <= 2:
      raise Exception('Height must be greater than 2.')
    print(symbol * width)
    for i in range(height - 2):
        print(symbol + (' ' * (width - 2)) + symbol)
    print(symbol * width)

## Handle exceptions when calling the function
for sym, w, h in (('*', 4, 4), ('O', 20, 5), ('x', 1, 3), ('ZZ', 3, 3)):
    try:
        box_print(sym, w, h)
    except Exception as err:  ## Catch exception and print error message
        print('An exception happened: ' + str(err))
  1. Теперь запустите скрипт из терминала, чтобы увидеть, как вызываются и обрабатываются исключения.
python3 raise_exception.py

Вы увидите следующий вывод. Первые два вызова box_print будут успешными, а последние два завершатся с ошибкой, инициируя блок except для вывода пользовательских сообщений об ошибках.

****
*  *
*  *
****
OOOOOOOOOOOOOOOOOOOO
O                  O
O                  O
O                  O
OOOOOOOOOOOOOOOOOOOO
An exception happened: Width must be greater than 2.
An exception happened: Symbol must be a single character string.

Получение трассировки в виде строки

Когда происходит необработанное исключение, Python выводит трассировку (traceback) — подробный отчет о вызовах функций, которые привели к ошибке. Иногда вы хотите корректно обработать исключение, но при этом записать полную трассировку для последующей отладки. Модуль traceback позволяет это сделать.

На этом шаге вы узнаете, как использовать traceback.format_exc() для захвата трассировки в виде строки и сохранения её в файл.

  1. Откройте файл get_traceback.py в проводнике файлов.
  2. Добавьте следующий код. Этот скрипт импортирует модуль traceback, а затем использует блок try...except для перехвата исключения. Внутри блока except он записывает полную трассировку в файл с именем errorInfo.txt.
import traceback

try:
    raise Exception('This is the error message.')
except:
    with open('errorInfo.txt', 'w') as error_file:
        error_file.write(traceback.format_exc())  ## Write traceback to file
    print('The traceback info was written to errorInfo.txt.')
  1. Выполните скрипт в терминале.
python3 get_traceback.py

Скрипт выведет подтверждающее сообщение:

The traceback info was written to errorInfo.txt.
  1. Теперь проверьте содержимое только что созданного файла errorInfo.txt. Вы можете открыть его в проводнике файлов или использовать команду cat в терминале.
cat errorInfo.txt

Файл будет содержать полную трассировку, похожую на следующую:

Traceback (most recent call last):
  File "/home/labex/project/get_traceback.py", line 4, in <module>
    raise Exception('This is the error message.')
Exception: This is the error message.

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

Использование утверждений (Assertions)

Утверждения (assertions) — это проверки здравого смысла, предназначенные для того, чтобы убедиться, что ваш код не делает ничего заведомо неправильного. Оператор assert проверяет истинность условия. Если условие ложно, он вызывает AssertionError, что в идеале должно привести к аварийному завершению вашей программы. Такой подход "быстрого отказа" (fail-fast) помогает быстрее определить корень ошибки.

Оператор assert состоит из ключевого слова assert, проверяемого условия и необязательного сообщения об ошибке.

На этом шаге вы будете использовать утверждение для проверки состояния программы.

  1. Откройте файл use_assertions.py в проводнике файлов.
  2. Добавьте следующий код. Он проверяет статус pod_bay_door_status. Первое утверждение проходит без проблем, а второе завершится неудачей и вызовет AssertionError.
## assert statement: check condition, raise AssertionError if False
pod_bay_door_status = 'open'
assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'  ## Passes

pod_bay_door_status = 'I\'m sorry, Dave. I\'m afraid I can\'t do that.'
assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'  ## Raises AssertionError
  1. Запустите скрипт в терминале.
python3 use_assertions.py

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

Traceback (most recent call last):
  File "/home/labex/project/use_assertions.py", line 6, in <module>
    assert pod_bay_door_status == 'open', 'The pod bay doors need to be "open".'
AssertionError: The pod bay doors need to be "open".

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

Реализация логирования (Logging)

Хотя операторы print() можно использовать для отладки, более мощным и гибким подходом является использование встроенного модуля Python logging. Логирование позволяет отслеживать события во время выполнения программы и категоризировать сообщения по уровню важности (severity).

На этом шаге вы добавите логирование в программу, чтобы помочь диагностировать ошибку.

  1. Сначала откройте файл factorial_log.py в проводнике файлов.
  2. Добавьте следующий код. Эта программа пытается вычислить факториал числа, но содержит ошибку. Мы добавили вызовы logging.debug(), чтобы отслеживать значения переменных на каждом шаге. Функция logging.basicConfig() настраивает систему логирования для отображения сообщений уровня DEBUG и выше.
import logging
logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(levelname)s - %(message)s')
logging.debug('Start of program')

def factorial(n):
    logging.debug('Start of factorial(%s)' % (n))
    total = 1
    for i in range(1, n + 1):
        total *= i
        logging.debug('i is ' + str(i) + ', total is ' + str(total))
    logging.debug('End of factorial(%s)' % (n))
    return total

print(factorial(5))
logging.debug('End of program')
  1. Запустите скрипт в терминале.
python3 factorial_log.py

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

 %(asctime)s - DEBUG - Start of program
 %(asctime)s - DEBUG - Start of factorial(5)
 %(asctime)s - DEBUG - i is 1, total is 1
 %(asctime)s - DEBUG - i is 2, total is 2
 %(asctime)s - DEBUG - i is 3, total is 6
 %(asctime)s - DEBUG - i is 4, total is 24
 %(asctime)s - DEBUG - i is 5, total is 120
 %(asctime)s - DEBUG - End of factorial(5)
120
 %(asctime)s - DEBUG - End of program

(Примечание: В исходном markdown-файле в примере кода была ошибка. Исправленный код предоставлен здесь для лучшего обучения. Исходная ошибка заключалась в использовании range(n + 1), которое начинается с 0, из-за чего общее значение всегда становилось 0. Исправление: range(1, n + 1).)

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

Настройка поведения логирования

Модуль logging обладает высокой степенью настраиваемости. Вы можете контролировать уровень важности (severity level) отображаемых сообщений, полностью отключать логирование или перенаправлять сообщения логов в файл вместо экрана.

Уровни логирования

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

Уровень Функция логирования Описание
DEBUG logging.debug() Для мелких деталей, полезно только при диагностике проблем.
INFO logging.info() Для записи общих событий или подтверждения того, что всё работает как ожидалось.
WARNING logging.warning() Для указания на потенциальную проблему, которая не останавливает программу.
ERROR logging.error() Для записи ошибки, из-за которой конкретная операция завершилась неудачей.
CRITICAL logging.critical() Для фатальной ошибки, которая вот-вот приведет к завершению работы программы.

Когда вы устанавливаете уровень логирования в basicConfig, Python будет отображать сообщения этого уровня и всех более высоких уровней. Например, установка level=logging.INFO покажет сообщения INFO, WARNING, ERROR и CRITICAL, но не DEBUG.

Отключение логирования

После отладки вы можете захотеть отключить все сообщения логов, не удаляя вызовы logging из вашего кода. Это можно сделать с помощью logging.disable(). Она принимает уровень в качестве аргумента и отключает все сообщения логов на этом уровне и ниже. Например, logging.disable(logging.CRITICAL) подавит каждое сообщение лога.

Логирование в файл

Вместо вывода логов в консоль вы можете сохранять их в файл, передав аргумент ключевого слова filename в logging.basicConfig().

Давайте попрактикуемся.

  1. Откройте файл configure_logging.py.
  2. Добавьте следующий код. Он настраивает логирование для записи всех сообщений уровня DEBUG и выше в файл с именем myProgramLog.txt.
import logging

## Configure logging to write to a file
logging.basicConfig(filename='myProgramLog.txt', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')

logging.debug('This is a debug message.')
logging.info('This is an info message.')
logging.warning('This is a warning message.')
  1. Запустите скрипт в терминале.
python3 configure_logging.py

На этот раз в терминале вы не увидите никакого вывода.

  1. Проверьте содержимое файла myProgramLog.txt.
cat myProgramLog.txt

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

2023-10-27 10:30:00,123 - DEBUG - This is a debug message.
2023-10-27 10:30:00,123 - INFO - This is an info message.
2023-10-27 10:30:00,123 - WARNING - This is a warning message.

На этом наш обзор основных методов отладки в Python завершен.

Резюме

В этой лабораторной работе вы изучили и отработали несколько основных методов отладки в Python. Вы начали с использования оператора raise для создания и вызова пользовательских исключений (custom exceptions) и обработки их с помощью блоков try...except. Затем вы узнали, как получить полную трассировку исключения (traceback) в виде строки с помощью traceback.format_exc() для целей логирования.

Далее вы изучили операторы assert как способ выполнения проверок на корректность (sanity checks) и "быстрого выхода из строя" (fail fast), когда ваша программа переходит в неожиданное состояние. Наконец, вы погрузились в модуль logging, научившись настраивать его для отображения сообщений, управления выводом с помощью различных уровней логирования и записи логов в файл для постоянного хранения записей.

Эти навыки являются основополагающими для написания надежных, поддерживаемых и более легких в отладке приложений на Python.