📖 Теория: Документация и аннотации типов

⚡ Кратко

Docstring — строка в тройных кавычках сразу после объявления. Она сохраняется в атрибуте __doc__ и показывается help().

Аннотации типов пишутся через двоеточие после параметра и стрелку -> для результата. Они помогают IDE и статическим анализаторам, но не останавливают выполнение.

Изменяемые объекты (list, dict, set) передаются по ссылке, поэтому изменения внутри функции видны снаружи. Неизменяемые (int, str, tuple) — нет.

Что такое документация и docstring

Документация — это описание кода, которое помогает разработчикам понимать, как работают функции, классы и модули. Хорошая документация снижает время на поддержку и упрощает совместную работу.

Docstring (документирующая строка) — это строка, заключённая в тройные кавычки (""" или '''), расположенная сразу после объявления функции, класса или в начале модуля. В отличие от обычного комментария, docstring доступен через help() и атрибут __doc__.

Стандартный формат reST/Epytext

В исходном материале используется формат с полями :param и :return. Это распространённый reST-стиль, который понимают IDE и генераторы документации.

def greet(name: str) -> str:
    """
    Функция принимает имя и возвращает строку приветствия.

    :param name: Имя пользователя.
    :return: Приветственное сообщение.
    """
    return f"Hello, {name}!"

Где размещать docstring

  • Функция: первая строка тела функции.
  • Класс: первая строка тела класса.
  • Модуль: в самом начале файла, до импортов или сразу после них.
⚠️ Проверить по документации: существуют и другие популярные форматы docstring — Google Style и NumPy Style. Они отличаются разметкой секций, но решают ту же задачу. Выбор формата часто определяется правилами команды или проекта.

Справка в Python: help() и __doc__

Функция help() выводит документацию объекта. Если передать в неё пользовательскую функцию с docstring, Python покажет именно её.

def add(a: int, b: int) -> int:
    """Возвращает сумму двух чисел."""
    return a + b

help(add)        # покажет docstring функции
print(add.__doc__)  # тот же текст одной строкой

Без аргументов help() запускает интерактивный справочный режим: можно вводить имена объектов и получать информацию о них.

💡 На заметку: IDE, такие как PyCharm и VS Code, умеют автоматически генерировать шаблон docstring, если после объявления функции ввести """ и нажать Enter.

Аннотации типов

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

Зачем нужны type hints

  • Упрощение понимания кода. Сразу видно, какие данные приходят на вход и что возвращается.
  • Помощь в отладке. Статический анализатор (например, mypy) находит потенциальные ошибки до запуска программы.
  • Автодокументирование. IDE используют аннотации для подсказок и автодополнения.

Базовый синтаксис

def function_name(param1: type1, param2: type2) -> return_type:
    # тело функции
    return value

variable: type = value

Аннотации для базовых типов

Для большинства встроенных типов название аннотации совпадает с названием типа:

def factorial(n: int) -> int:
    """Возвращает факториал числа."""
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result

def convert_to_celsius(fahrenheit: float) -> float:
    """Конвертирует температуру из Фаренгейта в Цельсий."""
    return (fahrenheit - 32) * 5 / 9

def is_even(number: int) -> bool:
    """Определяет, является ли число чётным."""
    return number % 2 == 0

def log_message(message: str) -> None:
    """Выводит сообщение в консоль, но не возвращает значения."""
    print(f"LOG: {message}")
⚠️ Важно: аннотации не влияют на выполнение кода. Если передать строку в функцию, ожидающую int, Python не выдаст ошибку, но код может упасть позже из-за несовместимых операций.

Аннотации структур данных

В Python 3.9+ встроенные коллекции можно использовать как generic-типы: в квадратных скобках указывается тип содержимого.

Список

def process_numbers(numbers: list[int]) -> list[int]:
    # Список чисел
    return [n ** 2 for n in numbers]

Кортеж

У кортежа есть два случая: фиксированная длина и произвольная длина.

def get_info() -> tuple[str, float]:
    # Первый элемент str, второй элемент float
    return "Bob", 4.91

def variable_tuple() -> tuple[int, ...]:
    # Кортеж произвольной длины, но только целые числа
    return 5, 8, 2

Множество, frozenset и словарь

def unique_chars(text: str) -> set[str]:
    # Множество уникальных символов
    return set(text)

def frozen_example() -> frozenset[int]:
    # Множество уникальных чисел
    return frozenset([1, 2, 3])

def count_words(text: str) -> dict[str, int]:
    """Принимает строку и возвращает словарь с подсчётом каждого слова."""
    words = text.split()
    return {word: words.count(word) for word in words}

Модуль typing: Any, Union, Optional, Callable

Когда одного типа недостаточно, используют специальные конструкции из модуля typing.

Any — любой тип

from typing import Any

def process_data(data: Any) -> str:
    """Принимает данные любого типа и возвращает строку с их представлением."""
    return f"Данные: {data}"

Union и оператор | — несколько типов

from typing import Union

# Старый стиль
def calculate(value: Union[int, float]) -> float:
    return value ** 2

# Современный стиль Python 3.10+
def calculate_new(value: int | float) -> float:
    return value ** 2

Optional — значение может быть None

from typing import Optional

def get_user_name(user_id: int) -> Optional[str]:
    """Возвращает имя пользователя или None, если пользователь не найден."""
    users = {1: "Alice", 2: "Bob"}
    return users.get(user_id)

Callable — функция как аргумент

from typing import Callable

def execute_function(
    func: Callable[[int, int], int],
    nums1: list[int],
    nums2: list[int]
) -> list[int]:
    return [func(a, b) for a, b in zip(nums1, nums2)]
⚠️ Проверить по документации: в typing есть и более продвинутые инструменты, например TypeVar для параметрических типов и Protocol для структурной типизации. Они используются в больших проектах для повышения гибкости аннотаций.

Передача изменяемых и неизменяемых объектов

В Python аргументы передаются по ссылке, но поведение зависит от того, является ли объект изменяемым.

Неизменяемые типы

Когда внутри функции пытаемся изменить неизменяемый объект, Python создаёт новый объект, а оригинал остаётся прежним.

def modify_value(n: int) -> None:
    print(f"Внутри до: {n}, id: {id(n)}")
    n += 1
    print(f"Внутри после: {n}, id: {id(n)}")

num = 10
modify_value(num)
print(f"Снаружи: {num}, id: {id(num)}")

Изменяемые типы

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

def modify_list(lst: list[int]) -> None:
    print(f"Внутри до: {lst}, id: {id(lst)}")
    lst.append(99)
    print(f"Внутри после: {lst}, id: {id(lst)}")

my_list = [1, 2, 3]
modify_list(my_list)
print(f"Снаружи: {my_list}, id: {id(my_list)}")

Как избежать нежелательных изменений

Если нужно работать с изменяемым объектом, но не менять оригинал, создайте копию:

def safe_modify_list(lst: list[int]) -> list[int]:
    """Работает с копией списка, оставляя оригинальный неизменным."""
    copy_lst = lst.copy()
    copy_lst.append(99)
    return copy_lst
⚠️ Проверить по документации: для вложенных изменяемых структур поверхностной копии .copy() бывает недостаточно. В таких случаях используют copy.deepcopy() из стандартной библиотеки.
⚠️ Проверить по документации: статический анализ типов выполняется отдельными инструментами, например mypy. Установка и запуск: pip install mypy, затем mypy your_file.py. Это помогает находить расхождения между аннотациями и фактическим использованием до запуска программы.