$ sudo teach IT
МОДУЛЬ 10 · УРОК 5

Практика: документируем классы из предыдущих уроков

Теория позади. Теперь откроем два класса, которые вы уже знаете — BankAccount и Vector — и добавим к ним полноценную документацию: docstring в Google Style и type hints. Потом проверим результат через help(), pydoc и mypy.

⏱ ~40 минут 🛠️ Урок-практика 🐍 Python 3

🎯 Что мы будем делать

В прошлых модулях мы написали два класса. Они работают правильно, но у них нет документации — ни docstring, ни type hints. Любой, кто откроет этот код, будет вынужден читать каждую строку, чтобы понять, что делают методы.

Цель урока: взять готовые классы и привести их к профессиональному стандарту — чтобы IDE понимала типы, help() давал полезный вывод, а mypy находил ошибки ещё до запуска.
🏦
BankAccount
Модуль 5 — банковский счёт с балансом, пополнением и снятием
📐
Vector
Модуль 7 — математический вектор с операциями сложения и умножения
✅
Проверка
help(), pydoc и mypy — убеждаемся, что документация работает

🏦 Шаг 1 — BankAccount: исходный код

Вот как выглядит класс до документирования. Код работает, но ничего не объясняет:

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Сумма должна быть положительной")
        self.balance += amount
        return self.balance

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Сумма должна быть положительной")
        if amount > self.balance:
            raise ValueError("Недостаточно средств")
        self.balance -= amount
        return self.balance

    def get_info(self):
        return f"{self.owner}: {self.balance} руб."

Посмотрите: что делает deposit? Возвращает что-то — но что именно? Какой тип у balance? Без чтения тела не скажешь. Именно это мы и исправим.

✏️ Шаг 2 — BankAccount: добавляем документацию

Добавляем Google Style docstring и type hints. Создайте файл bank_account.py с таким содержимым:

class BankAccount:
    """Банковский счёт клиента.

    Хранит текущий баланс и позволяет выполнять базовые
    финансовые операции: пополнение и снятие средств.

    Attributes:
        owner (str): Имя владельца счёта.
        balance (float): Текущий баланс в рублях.

    Example:
        >>> account = BankAccount("Алиса", 1000.0)
        >>> account.deposit(500.0)
        1500.0
        >>> account.withdraw(200.0)
        1300.0
        >>> print(account.get_info())
        Алиса: 1300.0 руб.
    """

    def __init__(self, owner: str, balance: float = 0.0) -> None:
        """Инициализирует банковский счёт.

        Args:
            owner: Имя владельца счёта.
            balance: Начальный баланс в рублях. По умолчанию 0.0.
        """
        self.owner = owner
        self.balance = balance

    def deposit(self, amount: float) -> float:
        """Пополняет счёт на указанную сумму.

        Args:
            amount: Сумма пополнения в рублях. Должна быть положительной.

        Returns:
            Новый баланс после пополнения.

        Raises:
            ValueError: Если сумма меньше или равна нулю.

        Example:
            >>> account = BankAccount("Боб", 100.0)
            >>> account.deposit(50.0)
            150.0
        """
        if amount <= 0:
            raise ValueError("Сумма должна быть положительной")
        self.balance += amount
        return self.balance

    def withdraw(self, amount: float) -> float:
        """Снимает деньги со счёта.

        Args:
            amount: Сумма снятия в рублях. Должна быть положительной
                и не превышать текущий баланс.

        Returns:
            Новый баланс после снятия.

        Raises:
            ValueError: Если сумма меньше или равна нулю,
                или если на счёте недостаточно средств.

        Example:
            >>> account = BankAccount("Боб", 200.0)
            >>> account.withdraw(50.0)
            150.0
        """
        if amount <= 0:
            raise ValueError("Сумма должна быть положительной")
        if amount > self.balance:
            raise ValueError("Недостаточно средств")
        self.balance -= amount
        return self.balance

    def get_info(self) -> str:
        """Возвращает строку с информацией о счёте.

        Returns:
            Строка вида «Имя: баланс руб.»

        Example:
            >>> account = BankAccount("Алиса", 500.0)
            >>> account.get_info()
            'Алиса: 500.0 руб.'
        """
        return f"{self.owner}: {self.balance} руб."
Что изменилось: у каждого параметра теперь есть тип (str, float), каждый метод описывает что возвращает (Returns) и что может бросить (Raises). Класс получил пример использования прямо в docstring.

📐 Шаг 3 — Vector: исходный код

Теперь второй класс. Исходный вариант без документации:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    def length(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

Что принимает __mul__ — вектор или число? Что возвращает length — и в каких единицах? Без документации непонятно.

✏️ Шаг 4 — Vector: добавляем документацию

Создайте файл vector.py:

class Vector:
    """Двумерный математический вектор.

    Представляет вектор на плоскости с координатами x и y.
    Поддерживает сложение двух векторов и умножение на скаляр.

    Attributes:
        x (float): Координата по горизонтальной оси.
        y (float): Координата по вертикальной оси.

    Example:
        >>> v1 = Vector(1.0, 2.0)
        >>> v2 = Vector(3.0, 4.0)
        >>> v1 + v2
        Vector(4.0, 6.0)
        >>> v1 * 3
        Vector(3.0, 6.0)
        >>> v1.length()
        2.23606797749979
    """

    def __init__(self, x: float, y: float) -> None:
        """Инициализирует вектор с заданными координатами.

        Args:
            x: Координата по горизонтальной оси.
            y: Координата по вертикальной оси.
        """
        self.x = x
        self.y = y

    def __add__(self, other: "Vector") -> "Vector":
        """Складывает два вектора покоординатно.

        Args:
            other: Второй вектор для сложения.

        Returns:
            Новый вектор, являющийся суммой двух векторов.

        Example:
            >>> Vector(1.0, 2.0) + Vector(3.0, 4.0)
            Vector(4.0, 6.0)
        """
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar: float) -> "Vector":
        """Умножает вектор на скалярное число.

        Args:
            scalar: Число, на которое умножается вектор.

        Returns:
            Новый вектор, координаты которого умножены на scalar.

        Example:
            >>> Vector(2.0, 3.0) * 2
            Vector(4.0, 6.0)
        """
        return Vector(self.x * scalar, self.y * scalar)

    def length(self) -> float:
        """Вычисляет длину (модуль) вектора.

        Длина рассчитывается по теореме Пифагора: sqrt(x² + y²).

        Returns:
            Длина вектора в виде числа с плавающей точкой.

        Example:
            >>> Vector(3.0, 4.0).length()
            5.0
        """
        return (self.x ** 2 + self.y ** 2) ** 0.5

    def __repr__(self) -> str:
        """Возвращает строковое представление вектора.

        Returns:
            Строка вида «Vector(x, y)», пригодная для отладки.

        Example:
            >>> repr(Vector(1.0, 2.0))
            'Vector(1.0, 2.0)'
        """
        return f"Vector({self.x}, {self.y})"
Обратите внимание: тип "Vector" написан в кавычках — это называется forward reference. Когда Python читает аннотацию __add__, класс Vector ещё не до конца определён, поэтому используем строку. Начиная с Python 3.10 это можно делать без кавычек через from __future__ import annotations.

🔍 Шаг 5 — Запускаем help()

Теперь проверим результат. Откройте терминал, запустите Python и введите:

from bank_account import BankAccount
help(BankAccount)

Python выведет примерно такой результат:

Help on class BankAccount in module bank_account:

class BankAccount(builtins.object)
 |  BankAccount(owner: str, balance: float = 0.0) -> None
 |
 |  Банковский счёт клиента.
 |
 |  Хранит текущий баланс и позволяет выполнять базовые
 |  финансовые операции: пополнение и снятие средств.
 |
 |  Attributes:
 |      owner (str): Имя владельца счёта.
 |      balance (float): Текущий баланс в рублях.
 |
 |  Example:
 |      >>> account = BankAccount("Алиса", 1000.0)
 |      >>> account.deposit(500.0)
 |      1500.0
 |
 |  Methods defined here:
 |
 |  deposit(self, amount: float) -> float
 |      Пополняет счёт на указанную сумму.
 |
 |      Args:
 |          amount: Сумма пополнения в рублях. Должна быть положительной.
 |
 |      Returns:
 |          Новый баланс после пополнения.
 |
 |      Raises:
 |          ValueError: Если сумма меньше или равна нулю.
 |  ...

Отлично! Python сам собрал все docstring и показал их в удобном виде. Именно это вы увидите в IDE, когда наведёте курсор на метод.

📄 Шаг 6 — Генерируем документацию через pydoc

pydoc — это встроенный инструмент Python, который генерирует документацию прямо из исходного кода. Перейдите в папку с файлом и выполните:

# Текстовый вывод в терминал
python -m pydoc bank_account

# HTML-файл (откроется в браузере)
python -m pydoc -w bank_account

После второй команды появится файл bank_account.html. Откройте его в браузере — увидите страницу документации с заголовком, всеми методами и их описаниями. Именно так это выглядит в профессиональных проектах.

python -m pydoc bank_account
Выводит текст прямо в терминал. Удобно для быстрой проверки.
python -m pydoc -w bank_account
Генерирует HTML-файл. Можно открыть в браузере или отправить коллеге.
python -m pydoc -b
Запускает локальный сервер. В браузере откроется каталог всех модулей.

🔬 Шаг 7 — Проверяем типы через mypy

mypy — статический анализатор типов. Он читает ваши type hints и проверяет, нет ли где несоответствий — без запуска кода. Установите и запустите:

# Установка (один раз)
pip install mypy

# Проверка файла
mypy bank_account.py

Если типы расставлены правильно, mypy выведет:

Success: no issues found in 1 source file

Теперь намеренно сломаем тип — добавим в bank_account.py такой вызов:

# bank_account.py (в конце файла добавьте)
account = BankAccount("Тест")
account.deposit("сто рублей")   # строка вместо float!

Снова запустите mypy bank_account.py. Теперь вы увидите:

bank_account.py:XX: error: Argument 1 to "deposit" has incompatible type "str"; expected "float"
Found 1 error in 1 file (checked 1 source file)
mypy нашёл ошибку до запуска! Без type hints вы бы узнали об этой ошибке только в runtime — и только если тест специально передал бы строку. С type hints ошибка обнаружена немедленно.

Уберите сломанный код после проверки — пусть файл остаётся чистым.

🏁 Итоги урока

В этом уроке мы взяли два реальных класса и превратили их в профессионально задокументированный код:

  • Написали Google Style docstring для классов и каждого метода
  • Добавили type hints ко всем параметрам и возвращаемым значениям
  • Использовали forward reference ("Vector") для аннотации внутри класса
  • Проверили результат через help() — документация отображается корректно
  • Сгенерировали HTML-документацию через pydoc -w
  • Убедились, что mypy находит ошибки типов до запуска программы

Теперь у вас есть полный рабочий процесс: написал класс → задокументировал → проверил типы. Этот подход используют во всех серьёзных Python-проектах.