Перейти к содержимому
k08

k08. Исключения, логирование, отладка

k08. Исключения, логирование, отладка

UAV Python Pro · Конспект K08 · Модуль M1. Python intensive: исключения, журналирование и отладка · Редакция 2.0 · После CHECKPOINT-K07

На уроке K07 вы научились читать и записывать файлы. На практике файл может отсутствовать, текст JSON может быть повреждён, а пользователь может ввести слова вместо числа. Если программа на каждую такую ситуацию просто завершается аварийно, инженер теряет время, а в более сложных сценариях — теряет понятный след причины отказа. На этом уроке вы научитесь перехватывать ожидаемые ошибки, создавать учебное исключение предметной области, вести журнал через модуль logging и отличать журнал от простого вывода print. Практика выполняется в виртуальном окружении. Полные тексты учебных файлов вложены в аккордеоны раздела практики и совпадают с файлами в каталоге code/M01_python/.

Как устроены примеры в этой редакции. В теоретических разделах каждый важный фрагмент кода приводится дважды. Сначала вы видите блок «код целиком»: в начале указана краткая цель, далее идёт сам код. Затем в раскрывающемся аккордеоне размещён тот же фрагмент с построчным разбором: после смысловой строки добавлен комментарий преподавателя с префиксом «# →». В разделе практики для каждой задачи в аккордеоне приведён полный текст соответствующего файла k08_task*.py.

Главная мысль урока состоит в следующем. Исключение — это сигнал интерпретатора Python о том, что обычный ход программы прервать необходимо. Конструкция try/except позволяет перехватить ожидаемый тип ошибки и продолжить работу осмысленно. Модуль logging записывает ход работы с уровнем важности и отметкой времени; такой журнал удобнее print для длинных сессий и последующего разбора. Отладка в PyCharm позволяет остановить программу на выбранной строке и посмотреть значения переменных, не восстанавливая их по обрывкам вывода.

Содержание

1. Цели урока

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

  • Вы будете объяснять, что такое исключение и чем оно отличается от обычного возврата значения через return.
  • Вы будете писать конструкцию try/except для ожидаемых ошибок ValueError и FileNotFoundError.
  • Вы будете понимать назначение веток else и finally у конструкции try.
  • Вы будете создавать простой класс собственного исключения и поднимать его командой raise.
  • Вы будете настраивать logging.basicConfig и записывать сообщения уровней DEBUG, INFO, WARNING и ERROR.
  • Вы будете соединять загрузку JSON с перехватом ошибок и записью в журнал.
  • Вы будете знать базовый порядок отладки в PyCharm: точка останова, запуск Debug и просмотр переменных.
Связь с разработкой программного обеспечения для БПЛА. Сценарий, который читает файл конфигурации, открывает канал связи или ожидает телеметрию, должен различать временный сбой и фатальную ошибку настройки. Журнал сессии затем помогает ответить на вопрос о том, что программа наблюдала за секунду до отказа. Без исключений и без модуля logging такой разбор превращается в восстановление событий по памяти.
↑ К оглавлению

2. Словарь урока

ТерминПростыми словами
Исключение / Exception Сигнал Python о том, что выполнить операцию обычным путём не удалось.
traceback Текст трассировки: цепочка вызовов и строка, на которой ошибка проявилась.
try / except Конструкция «попробуй выполнить блок; если случился указанный тип ошибки — обработай её».
raise Команда «поднять исключение» самостоятельно, когда данные недопустимы по смыслу задачи.
logging Стандартный модуль журналирования сообщений с уровнями важности.
Уровень журнала Степень важности сообщения: DEBUG, INFO, WARNING, ERROR, CRITICAL.
Breakpoint Точка останова в отладчике: выполнение останавливается на выбранной строке.
Отладчик Инструмент среды разработки, который выполняет программу пошагово и показывает значения переменных.
↑ К оглавлению

3. Зачем исключения и журнал в разработке для БПЛА

Представьте учебный сценарий, который читает JSON-файл с порогом минимального напряжения и затем печатает решение «GO» или «STOP». На диске файла может не оказаться. Внутри файла может быть опечатка. Пользователь может указать путь к другому каталогу. Если каждая такая ситуация завершает программу мгновенным аварийным остановом, вы теряете и частичный результат, и понятный след причины.

Исключения дают язык для описания ожидаемых сбоев. Журнал даёт хронологию: что программа пыталась сделать, что она получила и на чём остановилась. В модулях, посвящённых MAVLink, те же приёмы понадобятся для таймаутов, обрывов связи и разбора сессии. Сейчас вы осваиваете их на файлах и числах — на безопасной учебной почве.

↑ К оглавлению

4. Конструкция try, except, else, finally

Базовый try/except

Исключение — это объект-сигнал о сбое. Когда интерпретатор Python не может продолжить обычный ход программы, например при вызове float("текст"), он создаёт исключение. Если его никто не перехватил, программа печатает traceback и завершается.

Конструкция try / except означает следующее. Сначала выполняется блок try. Если во время выполнения возникло исключение указанного типа, управление переходит в блок except, где ситуацию обрабатывают.

# Пример: перехват ValueError при преобразовании строки в число
def read_min_alt(raw: str) -> float:
    """Преобразовать строку в float. При ошибке вернуть 10.0."""
    try:
        value = float(raw)
    except ValueError:
        print(f"Не могу превратить {raw!r} в число. Беру 10.0")
        return 10.0
    return value

print(read_min_alt("12.5"))
print(read_min_alt("двенадцать"))
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    def read_min_alt(raw: str) -> float:
    # → Объявляем функцию: на входе строка, на выходе число float.
    
        """Преобразовать строку в float. При ошибке вернуть 10.0."""
    # → Краткое описание функции (docstring).
    
        try:
    # → Начинается блок, в котором мы ожидаем возможную ошибку.
    
            value = float(raw)
    # → Пытаемся превратить строку в число. Если raw = "двенадцать", возникнет ValueError.
    
        except ValueError:
    # → Если возникло исключение типа ValueError, управление переходит сюда.
    
            print(f"Не могу превратить {raw!r} в число. Беру 10.0")
    # → Сообщаем о проблеме. !r показывает значение в кавычках.
    
            return 10.0
    # → Возвращаем запасное учебное значение вместо падения программы.
    
        return value
    # → Если ошибки не было, возвращаем успешно полученное число.
    
    print(read_min_alt("12.5"))
    # → Первый вызов: строка корректна, ожидаем 12.5.
    
    print(read_min_alt("двенадцать"))
    # → Второй вызов: строка нечисловая, сработает except.
Не перехватывайте все исключения без необходимости. Конструкция except Exception или голый except скрывает настоящие ошибки программиста. На курсе перехватывайте конкретные типы, которые вы действительно ожидаете: ValueError, FileNotFoundError, json.JSONDecodeError и подобные им.
# Пример: FileNotFoundError при чтении файла
from pathlib import Path

def read_text_or_warn(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8")
    except FileNotFoundError:
        print(f"Файл не найден: {path}")
        return ""
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    from pathlib import Path
    # → Импортируем Path для работы с путями к файлам.
    
    def read_text_or_warn(path: Path) -> str:
    # → Функция принимает путь и возвращает текст либо пустую строку.
    
        try:
    # → Пытаемся прочитать файл.
    
            return path.read_text(encoding="utf-8")
    # → Успешное чтение: сразу возвращаем содержимое.
    
        except FileNotFoundError:
    # → Если файла нет, Python возбуждает FileNotFoundError.
    
            print(f"Файл не найден: {path}")
    # → Сообщаем о причине.
    
            return ""
    # → Возвращаем пустую строку, чтобы вызывающий код мог продолжить работу.

Ветки else и finally

У конструкции try есть две дополнительные ветки, которые часто путают.

  • Ветка else выполняется только в том случае, если блок try завершился без исключения.
  • Ветка finally выполняется почти всегда: и после успеха, и после except. Её используют для обязательных завершающих действий. В учебном примере это служебное сообщение. В прикладном коде это может быть закрытие ресурса, если вы не использовали оператор with.
# Пример: ветки else и finally
from pathlib import Path

def load_demo(p: Path) -> str:
    print("start load")
    try:
        text = p.read_text(encoding="utf-8")
    except FileNotFoundError:
        print("except: файла нет")
        return ""
    else:
        print("else: чтение прошло без исключения")
        return text
    finally:
        print("finally: служебный хвост")
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    def load_demo(p: Path) -> str:
    # → Учебная функция загрузки текста файла.
    
        print("start load")
    # → Метка начала: поможет увидеть порядок сообщений.
    
        try:
    # → Основная операция, которая может завершиться ошибкой.
    
            text = p.read_text(encoding="utf-8")
    # → Чтение файла.
    
        except FileNotFoundError:
    # → Обработка отсутствия файла.
    
            print("except: файла нет")
    # → Сообщение о перехваченной ошибке.
    
            return ""
    # → Возврат пустой строки.
    
        else:
    # → Эта ветка выполняется только если except не сработал.
    
            print("else: чтение прошло без исключения")
    # → Подтверждение успешного чтения.
    
            return text
    # → Возвращаем прочитанный текст.
    
        finally:
    # → Эта ветка выполняется почти всегда: и после успеха, и после except.
    
            print("finally: служебный хвост")
    # → Место для обязательных завершающих действий.
Связь с практикой. Полный текст файла k08_task2_else_finally.py приведён в задаче 8.2. Внимательно прочитайте порядок печатных строк для случая, когда файл существует, и для случая, когда файла нет.
↑ К оглавлению

5. Команда raise и собственное исключение

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

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

# Пример: своё исключение и raise
class TelemetryError(Exception):
    """Учебная ошибка: данные телеметрии непригодны для решения."""

def require_positive_altitude(altitude_m: float) -> float:
    if altitude_m < 0:
        raise TelemetryError(f"Отрицательная высота: {altitude_m}")
    return altitude_m

try:
    print(require_positive_altitude(-3.0))
except TelemetryError as exc:
    print("domain error:", exc)
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    class TelemetryError(Exception):
    # → Объявляем собственный класс исключения. Он наследует Exception.
    
        """Учебная ошибка: данные телеметрии непригодны для решения."""
    # → Краткое описание смысла ошибки.
    
    def require_positive_altitude(altitude_m: float) -> float:
    # → Функция проверяет высоту на допустимость.
    
        if altitude_m < 0:
    # → Условие нарушения предметного правила.
    
            raise TelemetryError(f"Отрицательная высота: {altitude_m}")
    # → raise поднимает исключение и прерывает обычный ход функции.
    
        return altitude_m
    # → Если высота корректна, возвращаем её как обычный результат.
    
    try:
    # → Вызываем функцию в защищённом блоке.
    
        print(require_positive_altitude(-3.0))
    # → Передаём заведомо неверное значение.
    
    except TelemetryError as exc:
    # → Перехватываем именно наш тип ошибки. as exc сохраняет объект ошибки.
    
        print("domain error:", exc)
    # → Печатаем текст исключения.
Зачем объявлять собственный класс исключения. Когда блок except ловит именно TelemetryError, вы отделяете ошибку смысла данных от ошибки файловой системы. Позднее это упростит разбор журналов и автоматические проверки.
Связь с практикой. Полный текст файла k08_task3_custom_error.py приведён в задаче 8.3.
↑ К оглавлению

6. Модуль logging

Уровни сообщений

logging — стандартный модуль Python для журналирования. В отличие от print, запись в журнал имеет уровень важности и обычно содержит отметку времени.

УровеньТипичный смысл
DEBUG Подробности для разработчика; в рабочем режиме их часто отключают
INFO Нормальный ход работы: сессия началась, файл загружен
WARNING Ситуация подозрительна, но программа ещё может продолжать
ERROR Операция не удалась
CRITICAL Очень тяжёлый сбой; используют реже

Настройка basicConfig

# Пример: настройка logging в файл и в консоль
import logging
from pathlib import Path

log_path = Path("k08_data") / "session_demo.log"
log_path.parent.mkdir(exist_ok=True)

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler(log_path, encoding="utf-8"),
        logging.StreamHandler(),
    ],
)

log = logging.getLogger("k08.demo")
log.info("сессия началась")
log.error("не удалось прочитать учебный файл")
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    import logging
    # → Подключаем стандартный модуль журналирования.
    
    from pathlib import Path
    # → Нужен для пути к файлу журнала.
    
    log_path = Path("k08_data") / "session_demo.log"
    # → Путь к файлу журнала сессии.
    
    log_path.parent.mkdir(exist_ok=True)
    # → Создаём каталог, если его ещё нет.
    
    logging.basicConfig(
    # → Однократная настройка корневого журнала при старте программы.
    
        level=logging.DEBUG,
    # → Минимальный уровень: DEBUG и важнее будут обработаны.
    
        format="%(asctime)s [%(levelname)s] %(message)s",
    # → Формат строки: время, уровень, текст сообщения.
    
        handlers=[
    # → Куда писать сообщения.
    
            logging.FileHandler(log_path, encoding="utf-8"),
    # → Запись в файл в кодировке UTF-8.
    
            logging.StreamHandler(),
    # → Дублирование в консоль.
    
        ],
    # → Конец списка обработчиков.
    
    )
    # → Конец basicConfig.
    
    log = logging.getLogger("k08.demo")
    # → Именованный логгер для сообщений этого учебного модуля.
    
    log.info("сессия началась")
    # → Сообщение уровня INFO о нормальном ходе работы.
    
    log.error("не удалось прочитать учебный файл")
    # → Сообщение уровня ERROR о неудачной операции.
Когда достаточно print, а когда нужен logging. Для короткой проверки на несколько минут вывод print допустим. Для сессии, которую затем будут разбирать, нужен logging: у сообщений есть уровни, время и, как правило, файл. В разделах курса, посвящённых MAVLink, мы будем опираться именно на журнал сессии.
# Пример: JSON + try/except + logging
import json
import logging
from pathlib import Path

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger("k08.loader")

def load_telemetry_json(path: Path) -> dict:
    log.info("loading %s", path)
    try:
        text = path.read_text(encoding="utf-8")
        data = json.loads(text)
    except FileNotFoundError:
        log.error("file not found: %s", path)
        return {}
    except json.JSONDecodeError as exc:
        log.error("bad json in %s: %s", path, exc)
        return {}
    else:
        log.info("loaded keys=%s", list(data.keys()))
        return data
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    import json
    # → Модуль для разбора JSON.
    
    import logging
    # → Модуль журнала.
    
    from pathlib import Path
    # → Пути к файлам.
    
    logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
    # → Краткая настройка журнала: INFO и выше, с временем.
    
    log = logging.getLogger("k08.loader")
    # → Логгер для загрузчика.
    
    def load_telemetry_json(path: Path) -> dict:
    # → Функция загружает словарь телеметрии из JSON-файла.
    
        log.info("loading %s", path)
    # → Фиксируем начало операции. %s подставит path.
    
        try:
    # → Ожидаем возможные ошибки чтения и разбора.
    
            text = path.read_text(encoding="utf-8")
    # → Читаем файл как текст.
    
            data = json.loads(text)
    # → Превращаем текст JSON в объект Python.
    
        except FileNotFoundError:
    # → Файла нет на диске.
    
            log.error("file not found: %s", path)
    # → Пишем ошибку в журнал.
    
            return {}
    # → Возвращаем пустой словарь как безопасный запасной результат.
    
        except json.JSONDecodeError as exc:
    # → Текст есть, но JSON повреждён.
    
            log.error("bad json in %s: %s", path, exc)
    # → Журналируем причину разбора.
    
            return {}
    # → Снова безопасный пустой результат.
    
        else:
    # → Сюда попадаем только если try прошёл без исключений.
    
            log.info("loaded keys=%s", list(data.keys()))
    # → Отмечаем успех и состав ключей.
    
            return data
    # → Возвращаем загруженный словарь.
Связь с практикой. Полные тексты файлов k08_task4_logging_basic.py и k08_task5_logged_loader.py приведены в задачах 8.4 и 8.5.
↑ К оглавлению

7. Отладка в PyCharm

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

Breakpoint, или точка останова, — это метка на строке исходного текста. Когда выполнение доходит до этой строки, программа переходит в режим паузы.

Базовый порядок работы в PyCharm состоит в следующем.

  1. Откройте нужный файл с расширением .py.
  2. Щёлкните слева от номера строки. Появится красная точка — breakpoint.
  3. Запустите не команду Run, а команду Debug.
  4. Когда выполнение остановится, откройте панель Variables и просмотрите значения.
  5. Клавишами Step Over и Step Into продвигайтесь по коду.
  6. Когда закончите, нажмите Stop.
Когда отладчик особенно полезен. Ветка except сработала по неясной причине; в словаре оказался неожиданный ключ; функция float получила не ту строку. Сначала воспроизведите ошибку обычным запуском Run, затем поставьте breakpoint выше места аварийного останова.
↑ К оглавлению

8. Типичные ошибки

ОшибкаЧто происходитКак следует рассуждать
Блок except без указания типа Глотаются даже программные дефекты Указывайте конкретный тип исключения
Ошибку перехватили и промолчали Позднее невозможно понять причину Запишите хотя бы log.error или log.exception
basicConfig вызывают многократно без системы Конфигурация журнала становится непредсказуемой Настраивайте журнал один раз при старте программы
Путают return и raise Либо появляется тихий None, либо неожиданный traceback return отдаёт нормальный результат; raise сигнализирует о сбое
Отладку ведут только через print в десяти местах Много шума, нет уровней, нет файла сессии Используйте logging и точечный запуск Debug
↑ К оглавлению

9. Практика

В каждой карточке задачи ниже в раскрывающемся блоке приведён полный текст соответствующего учебного файла. Этот текст совпадает с файлами в каталоге code/M01_python/. Рекомендуется такой порядок работы. Сначала внимательно прочитайте условие. Затем раскройте аккордеон, скопируйте код в файл проекта под тем же именем, запустите его в виртуальном окружении и сверьте вывод с ожидаемым результатом. Каталог k08_data/ учебные сценарии создадут самостоятельно.

Задача 8.1. Конструкция try и except

Файл практики: k08_task1_try_except.py.

Реализуйте перехват ValueError при преобразовании строки в число и перехват FileNotFoundError при чтении отсутствующего файла. В обоих случаях программа не должна завершаться аварийно.

Ожидаемый результат. Для строки «12.5» будет напечатано число 12.5. Для строки «двенадцать» появится предупреждение и значение 10.0. Для отсутствующего файла функция вернёт пустую строку.

  • Полный текст файла k08_task1_try_except.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k08_task1_try_except.py в проекте PyCharm (виртуальное окружение) и запустите программу. Ниже приведён полный учебный текст файла с комментариями.

    # k08_task1_try_except.py
    # Цель: перехватить ValueError при преобразовании строки в число
    # и FileNotFoundError при чтении несуществующего файла.
    
    from pathlib import Path
    
    HERE = Path(__file__).resolve().parent
    
    
    def read_min_alt(raw: str) -> float:
        """Преобразовать ввод пользователя в float.
    
        Если строка не является числом, перехватываем ValueError
        и возвращаем запасное учебное значение.
        """
        try:
            value = float(raw)
        except ValueError:
            print(f"Не могу превратить {raw!r} в число. Беру 10.0")
            return 10.0
        return value
    
    
    def read_text_or_warn(path: Path) -> str:
        """Прочитать текст файла. Если файла нет — вернуть пустую строку."""
        try:
            return path.read_text(encoding="utf-8")
        except FileNotFoundError:
            print(f"Файл не найден: {path}")
            return ""
    
    
    print("case1:", read_min_alt("12.5"))
    print("case2:", read_min_alt("двенадцать"))
    
    missing = HERE / "k08_data" / "no_such_file.txt"
    print("file content empty?:", read_text_or_warn(missing) == "")

Задача 8.2. Ветки else и finally

Файл практики: k08_task2_else_finally.py.

Проследите порядок сообщений start load, except или else, finally для существующего файла и для отсутствующего файла.

Ожидаемый результат. При наличии файла вы увидите else и finally. При отсутствии файла вы увидите except и finally. В обоих случаях finally выполняется.

  • Полный текст файла k08_task2_else_finally.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k08_task2_else_finally.py в проекте PyCharm (виртуальное окружение) и запустите программу. Ниже приведён полный учебный текст файла с комментариями.

    # k08_task2_else_finally.py
    # Цель: понять ветки else и finally у try.
    
    from pathlib import Path
    
    HERE = Path(__file__).resolve().parent
    OUT = HERE / "k08_data"
    OUT.mkdir(exist_ok=True)
    path = OUT / "demo.txt"
    path.write_text("ok\n", encoding="utf-8")
    
    
    def load_demo(p: Path) -> str:
        """Показать try / except / else / finally на чтении файла."""
        print("start load")
        try:
            text = p.read_text(encoding="utf-8")
        except FileNotFoundError:
            print("except: файла нет")
            return ""
        else:
            # else выполняется только если except НЕ сработал
            print("else: чтение прошло без исключения")
            return text
        finally:
            # finally выполняется почти всегда: и при успехе, и при ошибке
            print("finally: этот блок служебный (закрытие ресурсов, метка в лог)")
    
    
    print("result:", repr(load_demo(path)))
    print("---")
    print("result missing:", repr(load_demo(OUT / "missing.txt")))

Задача 8.3. Собственное исключение

Файл практики: k08_task3_custom_error.py.

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

Ожидаемый результат. Для значения 12.0 будет напечатан успешный результат. Для значения -3.0 будет напечатано domain error с текстом об отрицательной высоте.

  • Полный текст файла k08_task3_custom_error.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k08_task3_custom_error.py в проекте PyCharm (виртуальное окружение) и запустите программу. Ниже приведён полный учебный текст файла с комментариями.

    # k08_task3_custom_error.py
    # Цель: своё исключение для учебной «ошибки предметной области».
    
    class TelemetryError(Exception):
        """Учебная ошибка: данные телеметрии непригодны для решения."""
    
    
    def require_positive_altitude(altitude_m: float) -> float:
        """Вернуть высоту, если она корректна; иначе поднять TelemetryError."""
        if altitude_m < 0:
            # raise создаёт исключение и прерывает обычный ход функции
            raise TelemetryError(f"Отрицательная высота: {altitude_m}")
        return altitude_m
    
    
    for value in (12.0, -3.0):
        try:
            alt = require_positive_altitude(value)
            print("ok altitude:", alt)
        except TelemetryError as exc:
            # as exc — сохранить объект ошибки, чтобы напечатать текст
            print("domain error:", exc)

Задача 8.4. Журнал logging

Файл практики: k08_task4_logging_basic.py.

Настройте basicConfig так, чтобы сообщения писались и в файл session_demo.log, и в консоль. Запишите сообщения нескольких уровней.

Ожидаемый результат. В консоли и в файле k08_data/session_demo.log появятся строки с временем, уровнем и текстом сообщений.

  • Полный текст файла k08_task4_logging_basic.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k08_task4_logging_basic.py в проекте PyCharm (виртуальное окружение) и запустите программу. Ниже приведён полный учебный текст файла с комментариями.

    # k08_task4_logging_basic.py
    # Цель: модуль logging — уровни и вывод в файл + консоль.
    
    import logging
    from pathlib import Path
    
    HERE = Path(__file__).resolve().parent
    OUT = HERE / "k08_data"
    OUT.mkdir(exist_ok=True)
    log_path = OUT / "session_demo.log"
    
    # basicConfig настраивает корневой логгер ОДИН раз при старте программы
    logging.basicConfig(
        level=logging.DEBUG,
        format="%(asctime)s [%(levelname)s] %(message)s",
        handlers=[
            logging.FileHandler(log_path, encoding="utf-8"),
            logging.StreamHandler(),  # дублировать в консоль
        ],
    )
    
    log = logging.getLogger("k08.demo")
    
    log.debug("подробности для разработчика")
    log.info("сессия началась")
    log.warning("напряжение учебное ниже комфортного порога")
    log.error("не удалось прочитать учебный файл")
    
    print("log file:", log_path)
    print("log tail:")
    print(log_path.read_text(encoding="utf-8"))

Задача 8.5. JSON, except и журнал

Файл практики: k08_task5_logged_loader.py.

Загрузите корректный JSON, повреждённый JSON и отсутствующий файл. Для ошибок используйте log.error, для успеха — log.info.

Ожидаемый результат. Для good будет словарь с mode и altitude_m. Для bad и missing будет пустой словарь, а в журнале — сообщения об ошибках.

  • Полный текст файла k08_task5_logged_loader.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k08_task5_logged_loader.py в проекте PyCharm (виртуальное окружение) и запустите программу. Ниже приведён полный учебный текст файла с комментариями.

    # k08_task5_logged_loader.py
    # Цель: соединить JSON-загрузку (K07) с try/except и logging (K08).
    
    import json
    import logging
    from pathlib import Path
    
    HERE = Path(__file__).resolve().parent
    OUT = HERE / "k08_data"
    OUT.mkdir(exist_ok=True)
    
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(message)s",
    )
    log = logging.getLogger("k08.loader")
    
    
    def load_telemetry_json(path: Path) -> dict:
        """Загрузить JSON-словарь телеметрии с журналированием ошибок."""
        log.info("loading %s", path)
        try:
            text = path.read_text(encoding="utf-8")
            data = json.loads(text)
        except FileNotFoundError:
            log.error("file not found: %s", path)
            return {}
        except json.JSONDecodeError as exc:
            log.error("bad json in %s: %s", path, exc)
            return {}
        else:
            log.info("loaded keys=%s", list(data.keys()))
            return data
    
    
    # Готовим корректный файл
    good = OUT / "telemetry_ok.json"
    good.write_text('{"mode": "GUIDED", "altitude_m": 11.0}\n', encoding="utf-8")
    
    # Готовим битый JSON
    bad = OUT / "telemetry_bad.json"
    bad.write_text("{mode: GUIDED, broken}\n", encoding="utf-8")
    
    print("good:", load_telemetry_json(good))
    print("bad:", load_telemetry_json(bad))
    print("missing:", load_telemetry_json(OUT / "nope.json"))

Задача 8.6. Отладка в PyCharm

Откройте файл k08_task5_logged_loader.py. Поставьте точку останова внутри блока except json.JSONDecodeError или на строке с log.error для повреждённого JSON. Запустите программу командой Debug и просмотрите путь к файлу и объект ошибки в панели Variables.

Задача 8.7. Сохранение в Git

Сохраните практику в истории Git. Каталог k08_data/ с выходными учебными файлами в репозиторий обычно не включают.

git add code/M01_python/k08_*.py
git commit -m "K08: try except raise logging practice"
↑ К оглавлению

10. Проверьте себя

  1. Чем исключение отличается от обычного return None?
  2. Зачем в блоке except указывают конкретный тип, а не перехватывают все исключения подряд?
  3. Когда выполняется ветка else у конструкции try?
  4. Зачем нужна ветка finally, если для файлов часто используют оператор with?
  5. В каком случае уместна команда raise для собственного TelemetryError?
  6. Чем вызов logging.info отличается от print с точки зрения последующего разбора сессии?
  7. Что означает уровень WARNING по сравнению с уровнем ERROR?
  8. Какой минимальный порядок действий в PyCharm Debug вы назовёте достаточным для учебной задачи?
  9. Зачем в уроке один и тот же пример кода показан дважды: целиком и с построчным разбором?
↑ К оглавлению

11. Чек-лист самопроверки

  • Я объясняю исключение и traceback своими словами законченными предложениями.
  • Я пишу try/except для ValueError и FileNotFoundError.
  • Я понимаю назначение веток else и finally.
  • Я создаю класс исключения и выполняю raise.
  • Я настраиваю logging.basicConfig и пишу сообщения в файл.
  • Я различаю уровни DEBUG, INFO, WARNING и ERROR.
  • Я связал загрузку JSON с except и записями в журнал.
  • Я поставил breakpoint и хотя бы один раз запускал Debug.
  • Я открывал аккордеоны практики и сверял полный код с запуском в виртуальном окружении.
  • Я запускал k08_task1 … k08_task5 в venv.
  • Я сделал git commit по K08.
Результат. Когда пункты закрыты, напишите ментору: K08 чек-лист закрыт. Далее следует урок K09, посвящённый классам, dataclasses и перечислениям Enum.
↑ К оглавлению

12. Что дальше

  • Предыдущий урок: K07 · Файлы, JSON, CSV, YAML
  • Текущий урок: K08 · Исключения, журналирование и отладка (редакция 2.0)
  • Следующий урок: K09 · Классы, dataclasses, Enum
  • Горизонт модуля M1: K12 и контрольная точка G1
Стиль изложения. Текст следует голосу внимательного преподавателя: законченные предложения, явные определения, отраслевая терминология БПЛА, без сленга и без обрубков. Первые определения терминов отмечены классом uk-marker. Примеры кода даны в двойном виде. Практики содержат полный исходный текст в аккордеоне. Стандарт: docs/05_EDITOR_STYLE_GUIDE.md.
↑ К оглавлению

 

 

Среда, 19 августа 2026
k08. Исключения, логирование, отладка