Тема 04 · Аннотации типов и документация кода

Аннотации типов и документация кода · МДК.01.01 · разработка программных модулей

Глава 4.9

4.9

Разбор случаев:
двенадцать функций

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

Категория
Разбор примеров
Уровень
Начальный
Формат
Google, как принято в курсе
Зачем это знать
Документировать свой код по образцу близкого случая

§1Как пользоваться разбором

Все функции взяты из проекта темы — «Отчёт кафе» из главы 4.6, и лежат в тех же файлах, что указаны в подписи к каждому примеру. Описания записаны в формате Google.

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

Разбор читается рядом с проектом: в редакторе открыт код без описаний, в книге — этот разбор. Второй архив содержит те же функции с готовыми докстрингами.

§2Случай 1. Короткая функция без параметров

app/utils/clock.pyслучай 1
def today_label() -> str:
    """Возвращает текущую дату в виде «21.09.2026» для шапки отчёта."""
    return date.today().strftime("%d.%m.%Y")
Вывод help(today_label)эталонный архив
today_label() -> str
    Возвращает текущую дату в виде «21.09.2026» для шапки отчёта.

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

§3Случай 2. Значение по умолчанию

app/utils/formatter.pyслучай 2
def format_title(text: str, width: int = 40) -> str:
    """Центрирует заголовок отчёта, достраивая строку знаками «=».

    Args:
        text: Текст заголовка без пробелов по краям.
        width: Итоговая ширина строки в знаках. Значение по умолчанию
            совпадает с шириной отчёта в консоли.

    Returns:
        Строка ровно указанной ширины; более длинный текст не обрезается.
    """
    return text.center(width, "=")

Значение по умолчанию видно из подписи, а его смысл — нет. В докстринге записаны две вещи: откуда взялось число 40 и что происходит на границе — текст длиннее указанной ширины остаётся целым. Поведение на границе описывают всегда: из подписи и из имени функции оно не следует.

§4Случай 3. Функция ничего не возвращает

app/ui/console.pyслучай 3
def print_report(lines: list[str]) -> None:
    """Печатает готовый отчёт в консоль построчно.

    Args:
        lines: Строки отчёта в порядке вывода.

    Note:
        Функция только печатает: расчёты и форматирование выполняются
        до вызова, в services и utils.
    """
    for line in lines:
        print(line)

Раздел Returns: при возврате None не пишут: возвращать нечего, и аннотация об этом уже сообщила. Вместо него указывают действие функции — здесь это вывод в консоль. Раздел Note: отмечает границу ответственности: этот модуль не считает и не форматирует.

§5Случай 4. Функция возбуждает исключение

app/services/calculator.pyслучай 4
def calculate_average(grades: list[int]) -> float:
    """Считает среднюю оценку по списку.

    Args:
        grades: Оценки от 2 до 5, список не должен быть пустым.

    Returns:
        Среднее значение, округлённое до двух знаков.

    Raises:
        ValueError: Если список оценок пуст. Среднее по пустому списку
            не определено, поэтому нуль в этом случае не возвращается.
    """
    if not grades:
        raise ValueError("Список оценок пуст")
    return round(sum(grades) / len(grades), 2)

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

На полях: ограничения входных данных

Строка «оценки от 2 до 5» — договорённость предметной области, которую функция не проверяет. Такие ограничения в докстринге указывают прямо: читатель видит, при каких данных функция задумана работать. Когда ограничение важно соблюсти, его переносят в проверку внутри функции, и тогда в разделе Raises: появляется ещё одна строка.

§6Случай 5. Аргумент меняется на месте

app/services/orders.pyслучай 5
def sort_orders_in_place(orders: list[Order]) -> None:
    """Сортирует список заказов по убыванию суммы, изменяя сам список.

    Args:
        orders: Список заказов. Порядок элементов в нём меняется,
            новый список не создаётся.

    Note:
        Вызывающий код после вызова получает изменённый список.
        Когда исходный порядок нужно сохранить, передают его копию:
        sort_orders_in_place(orders.copy()).
    """
    orders.sort(key=lambda order: -order.kopeks)
Терминалчто происходит со списком вызывающего кода
>>> orders = [Order('Иванов Пётр', 100), Order('Петрова Анна', 900)]
>>> sort_orders_in_place(orders)
>>> orders
[Order(waiter='Петрова Анна', kopeks=900), Order(waiter='Иванов Пётр', kopeks=100)]

Функция ничего не вернула, а список у вызывающего кода стал другим: порядок элементов изменился. Это и описывает раздел Note:.

Изменение переданного объекта из подписи не видно: аннотация -> None одинакова у функции, которая печатает, и у функции, которая перестраивает список вызывающего кода. Побочное действие описывают всегда: запись файла, изменение аргумента, обращение к сети, запись в журнал.

§7Случай 6. Генератор

app/services/importer.pyслучай 6
def read_rows(path: Path) -> Iterator[dict[str, str]]:
    """Читает CSV-файл построчно, не загружая его целиком в память.

    Args:
        path: Путь к файлу с колонками waiter и kopeks.

    Yields:
        Очередную строку файла в виде словаря «колонка — значение».

    Raises:
        FileNotFoundError: Если файла по указанному пути нет.

    Note:
        Файл остаётся открытым, пока перебор не закончен. Прервав
        перебор, вызывающий код закрывает генератор сам либо
        использует его внутри цикла for до конца.
    """
    with open(path, encoding="utf-8", newline="") as file:
        yield from csv.DictReader(file)
Вывод help(read_rows)эталонный архив
read_rows(path: pathlib.Path) -> collections.abc.Iterator[dict[str, str]]
    Читает CSV-файл построчно, не загружая его целиком в память.

    Args:
        path: Путь к файлу с колонками waiter и kopeks.

    Yields:
        Очередную строку файла в виде словаря «колонка — значение».

    Raises:
        FileNotFoundError: Если файла по указанному пути нет.

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

В справке видно и тип результата — Iterator, и раздел Yields: по ним в вызывающем коде видно, что значения придут по одному при переборе.

У генератора вместо Returns: пишут Yields: и описывают, что приходит на каждом шаге перебора. Вторая особенность — время жизни ресурса: пока перебор идёт, файл открыт. Это тоже сведения для вызывающего кода, и место им в докстринге.

§8Случай 7. Несколько значений в результате

app/services/calculator.pyслучай 7
def grade_bounds(grades: list[int]) -> tuple[int, int]:
    """Находит минимальную и максимальную оценку.

    Args:
        grades: Оценки студента, список не должен быть пустым.

    Returns:
        Пара «минимум, максимум» именно в этом порядке.

    Raises:
        ValueError: Если список оценок пуст.
    """
    if not grades:
        raise ValueError("Список оценок пуст")
    return min(grades), max(grades)

Аннотация tuple[int, int] сообщает, что в результате два целых числа, и не сообщает, какое из них первое. Порядок значений в паре — обязательная строка в Returns:. Перепутанный порядок при распаковке даёт работающую программу с неверными числами в отчёте.

§9Случай 8. Работа с файлом

app/services/report_file.pyслучай 8
def save_report(path: Path, lines: list[str]) -> int:
    """Записывает отчёт в текстовый файл в кодировке UTF-8.

    Args:
        path: Путь к файлу. Существующий файл перезаписывается,
            родительская папка должна существовать.
        lines: Строки отчёта без символов перевода строки на концах.

    Returns:
        Количество записанных строк.

    Raises:
        FileNotFoundError: Если родительской папки нет.
        PermissionError: Если на запись в эту папку нет прав.
    """
    with open(path, "w", encoding="utf-8") as file:
        file.write("\n".join(lines))
    return len(lines)
Терминалрезультат и ошибка из раздела Raises
>>> save_report(Path('otchet.txt'), ['Смена', 'Иванов Пётр 1 685,00 ₽'])
2
>>> save_report(Path('net-takoy-papki/otchet.txt'), ['Смена'])
FileNotFoundError: [Errno 2] No such file or directory: 'net-takoy-papki/otchet.txt'

Первый вызов вернул 2 — столько строк записано. Второй возбудил FileNotFoundError, потому что родительской папки нет; это условие и записано в разделе Raises:.

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

§10Случай 9. Служебная функция

app/utils/text.pyслучай 9
def _squash_spaces(value: str) -> str:
    """Заменяет подряд идущие пробелы одним."""
    return " ".join(value.split())


def normalize_name(name: str) -> str:
    """Приводит имя официанта к виду «Фамилия Имя».

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

    Args:
        name: Имя в произвольном виде, например «  иванов   пётр ».

    Returns:
        Имя вида «Иванов Пётр».
    """
    return _squash_spaces(name).title()

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

§11Случай 10. Класс с методами

app/domain/shift.pyслучай 10
class Shift:
    """Смена кафе: заказы одного рабочего дня.

    Объект накапливает заказы и считает по ним итоги. Заказы хранятся
    в порядке добавления.

    Attributes:
        day: Дата смены.
        orders: Добавленные заказы в порядке поступления.
    """

    def __init__(self, day: date) -> None:
        """Создаёт пустую смену на указанную дату."""
        self.day = day
        self.orders: list[Order] = []

    def add(self, order: Order) -> None:
        """Добавляет заказ в конец списка смены.

        Args:
            order: Заказ с уже нормализованным именем официанта.
        """
        self.orders.append(order)

    def total_kopeks(self) -> int:
        """Возвращает выручку смены в копейках; у пустой смены — 0."""
        return sum(order.kopeks for order in self.orders)
Терминал VS Code: вывод help для класса Shift с описанием, разделом Attributes и перечнем методов
Так выглядит докстринг класса, прочитанный через help(Shift). Сверху подпись создания объекта, затем краткая строка и пояснение, ниже раздел Attributes с двумя полями. За ним идёт перечень методов: у каждого своя подпись и своё описание.

Докстринг класса описывает, что представляет объект, и перечисляет атрибуты в разделе Attributes:. У __init__ здесь хватает одной строки: проверок нет, параметр один и он аннотирован. Метод total_kopeks описан однострочно с указанием поведения на пустой смене — это тот пограничный случай, из-за которого читатель иначе открыл бы тело метода.

§12Случай 11. Dataclass

app/domain/order.pyслучай 11
@dataclass(frozen=True)
class Order:
    """Один заказ из файла смены.

    Объект неизменяемый: после создания сумму и официанта поменять
    нельзя, поэтому заказы можно безопасно передавать между слоями.

    Attributes:
        waiter: Имя официанта в виде «Фамилия Имя».
        kopeks: Сумма заказа в копейках, неотрицательная.
    """

    waiter: str
    kopeks: int

    def __post_init__(self) -> None:
        """Проверяет данные сразу после создания заказа.

        Raises:
            ValueError: Если сумма отрицательная.
        """
        if self.kopeks < 0:
            raise ValueError("Сумма заказа не может быть отрицательной")
        object.__setattr__(self, "waiter", normalize_name(self.waiter))

У dataclass поля объявлены в теле класса, а метод __init__ создаётся автоматически — писать в нём докстринг негде. Поэтому сведения о полях идут в раздел Attributes: докстринга класса, а проверки выносятся в __post_init__, и уже у него свой докстринг с разделом Raises:.

Последняя строка примера меняет поле через object.__setattr__: у неизменяемого объекта обычное присваивание запрещено, а имя официанта нужно привести к одному виду один раз при создании. Такие неочевидные места объясняют в докстринге класса или комментарием рядом.

§13Случай 12. Модуль и пакет

app/services/orders.pyслучай 12: начало файла
"""Заказы смены: чтение данных и расчёты по ним.

Модуль работает только с данными: читает CSV, собирает объекты Order
и считает итоги. Печать и форматирование — в app/ui и app/utils.
Суммы во всём модуле хранятся в копейках, целыми числами.
"""

import csv
from pathlib import Path
app/__init__.pyслучай 12: докстринг пакета
"""Отчёт кафе: выручка по официантам за смену.

Пакеты:
    domain — объекты предметной области: заказ, смена.
    services — чтение данных и расчёты.
    utils — форматирование строк и сумм.
    ui — вывод в консоль.

Точка входа — main.py в корне проекта.
"""

Докстринг модуля отвечает на вопрос, за что отвечает файл, и называет границу: чего в этом модуле нет. Докстринг пакета в __init__.py даёт карту: перечень вложенных пакетов с одной строкой о каждом и указание точки входа. С такой картой человек, открывший репозиторий впервые, понимает раскладку до чтения кода.

§14Сокращение многословного описания

Длина докстринга не показатель качества. Пример ниже описывает функцию из случая 4 в двадцать строк, повторяя подпись, пересказывая тело и оценивая код.

Двадцать строк

"""Эта очень важная функция calculate_average предназначена для того, чтобы вычислять среднее арифметическое значение оценок студента.

Она принимает на вход параметр grades типа список целых чисел int и возвращает значение типа float.

Внутри функции сначала проверяется, пустой ли список. Затем вызывается встроенная функция sum, результат делится на len, после чего round округляет до двух знаков.

Функция написана эффективно и не требует доработок."""

→
Семь строк

"""Считает среднюю оценку по списку.

Args:
    grades: Оценки от 2 до 5, список не должен быть пустым.

Returns:
    Среднее значение, округлённое до двух знаков.

Raises:
    ValueError: Если список оценок пуст."""

Из левого варианта убрано четыре вида лишнего текста: оценка кода («очень важная», «написана эффективно»), повтор имени функции, повтор типов из аннотаций и пересказ тела построчно. Пересказ тела вреднее прочего: он устаревает при первой же правке реализации, хотя поведение функции остаётся тем же.

§15Что писать и чего не писать

Попадает в докстрингОстаётся вне докстринга
Действие функции одним предложениемИмя функции и перечень её параметров словами
Единицы измерения: копейки, знаки после запятой, кодировкаТипы, уже указанные в аннотациях
Ограничения на входные данныеПересказ тела построчно
Поведение на границе: пустой список, нуль, превышение шириныВыбор конкретных строк реализации — это комментарий
Порядок значений в результатеОценка кода и планы на будущее
Возбуждаемые исключения и их условияИсключения из глубины сторонних библиотек
Побочные действия: запись файла, изменение аргумента, сетьСведения, которые меняются чаще кода: сроки, фамилии, номера задач

§16Мини-практика: десять минут

Работа идёт на стартовом архиве проекта темы: код в нём тот же, описаний нет.

  1. Откройте app/services/orders.py и найдите функцию без докстринга, которая возвращает значение. Задокументируйте её по случаю 2 или 7.
  2. Найдите функцию, которая возбуждает ValueError, и добавьте раздел Raises: с условием ошибки.
  3. Найдите функцию, которая печатает или пишет файл, и опишите её действие без раздела Returns:.
  4. Добавьте докстринг модуля первой строкой файла: за что отвечает и чего в нём нет.
  5. Добавьте докстринг пакета в app/__init__.py с перечнем вложенных пакетов.
  6. Выполните python -c "import app.services.orders as m; help(m)" и проверьте, что описания на месте, а порядок разделов совпадает с образцами главы.

§17Проверьте себя

Функция возвращает None. Нужен ли раздел Returns:?

Нет. Возвращать нечего, и аннотация -> None об этом сообщила. Вместо результата описывают действие функции: печать, запись файла, изменение переданного объекта.

Почему изменение аргумента на месте обязательно указывать в докстринге?

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

Какой раздел ставят генератору и что ещё у него описывают?

Раздел Yields: — что приходит на каждом шаге перебора. Дополнительно описывают время жизни ресурса: пока перебор не закончен, открытый файл остаётся открытым, и прерванный перебор вызывающему коду нужно закрыть.

Функция возвращает tuple[int, int]. Что нужно написать в Returns:?

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

Чем докстринг dataclass отличается от докстринга обычного класса?

У dataclass метод __init__ создаётся автоматически, поэтому его докстринг писать негде: сведения о полях идут в раздел Attributes: докстринга класса. Проверки, вынесенные в __post_init__, документируются в этом методе, с разделом Raises:.

Что из этого лишнее в докстринге: единицы измерения, пересказ тела, порядок значений в результате, оценка кода?

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

§18Шпаргалка по главе

Однострочный докстринг хватает нет параметров нет исключений служебная функция с _
Нужен Args: ограничения на данные смысл значения по умолчанию единицы измерения
Вместо Returns: Yields: у генератора ничего при -> None Attributes: у класса
Всегда описывать запись и перезапись файла изменение аргумента порядок значений в паре
Raises: класс ошибки + условие только свои исключения
Убрать из текста повтор подписи и типов пересказ тела оценку кода

Итог главы

Двенадцать разобранных случаев дают образцы для большей части кода в учебном проекте. Короткой функции без параметров хватает одной строки. Значение по умолчанию требует пояснения смысла. При возврате None раздел о результате опускают и описывают действие. Исключения, которые функция возбуждает сама, перечисляют с условиями. Изменение переданного объекта, запись файла и другие побочные действия описывают всегда, потому что из подписи они не видны. У генератора вместо результата описывают выдаваемые значения и время жизни открытого ресурса. У класса сведения о полях идут в раздел атрибутов, у dataclass — туда же, поскольку метод создания генерируется автоматически. Докстринг модуля называет ответственность файла, докстринг пакета в __init__.py даёт карту вложенных пакетов. Лишними в докстринге оказываются повтор подписи, пересказ тела и оценка кода. В следующей главе эти описания начинает создавать редактор: разобраны расширение-генератор для VS Code, настройка формата и автоматическая проверка, которая находит недокументированные функции.

Откуда взяты факты
  1. Разделы и их порядок в формате Google — руководство по стилю Python от Google, раздел о комментариях и докстрингах.
  2. Требования к первой строке и оформлению — PEP 257.
  3. Поведение __post_init__ и автоматический __init__ — документация модуля dataclasses.
  4. Код функций — проект темы «Отчёт кафе», архивы к главе 4.6.
Глава 4.9 · Разбор случаев: двенадцать функций · МДК.01.01 Автор: Макаров Максим Николаевич