Пакеты и структура проекта

⏱ 30 мин · 🧩 заданий: 5

Пакет — папка модулей

Когда модулей становится много, их группируют в пакеты — папки с файлом __init__.py:

shop/ ← пакет shop ├── __init__.py ← выполняется при import shop ├── payments.py ← модуль shop.payments └── orders/ ← подпакет shop.orders ├── __init__.py ├── models.py ← shop.orders.models └── service.py ← shop.orders.service

Пакет — тоже модуль: объект с атрибутами, у которого есть __path__ (где искать подмодули).

import sys, tempfile, pathlib

root = pathlib.Path(tempfile.mkdtemp())
files = {
    "shop/__init__.py": 'print("  init shop")\nVERSION = "1.0"\n',
    "shop/payments.py": 'def charge(amount):\n    return f"списано {amount} ₽"\n',
    "shop/orders/__init__.py": 'print("  init shop.orders")\nfrom .service import place_order\n__all__ = ["place_order"]\n',
    "shop/orders/models.py": 'class Order:\n    def __init__(self, total):\n        self.total = total\n',
    "shop/orders/service.py": (
        'from .models import Order          # относительный: тот же пакет\n'
        'from ..payments import charge      # относительный: родительский пакет\n'
        'def place_order(total):\n'
        '    order = Order(total)\n'
        '    return charge(order.total)\n'
    ),
}
for path, text in files.items():
    (root / path).parent.mkdir(parents=True, exist_ok=True)
    (root / path).write_text(text, encoding="utf-8")
sys.path.insert(0, str(root))

from shop.orders import place_order     # __init__.py реэкспортирует функцию
import shop
print(place_order(990), shop.VERSION)
print(shop.__path__, [m for m in sys.modules if m.startswith("shop")])

Абсолютные и относительные импорты

# внутри shop/orders/service.py
from shop.payments import charge     # абсолютный: полный путь от корня
from ..payments import charge        # относительный: на уровень выше
from .models import Order            # относительный: этот же пакет
Абсолютный Относительный
Читаемость сразу видно, откуда короче внутри глубоких пакетов
Переименование пакета нужно править импорты не затрагивает
Работает в скрипте python file.py да нет
Рекомендация PEP 8 предпочтителен допустим внутри пакета

Что писать в init.py

  • Ничего — нормальный вариант (с Python 3.3 файл даже необязателен — тогда это namespace-пакет, но для обычных проектов __init__.py лучше оставить).
  • Публичный API: реэкспорт основных имён, чтобы пользователи писали from shop import Order, а не from shop.orders.models import Order.
  • __version__, __all__.

Не стоит: тяжёлые вычисления, подключения к базе, импорт всех подмодулей «на всякий случай» — это замедляет каждый импорт пакета.

Раскладка проекта

Современный стандарт — src-layout:

myapp/ ← корень репозитория ├── pyproject.toml ← метаданные, зависимости, настройки инструментов ├── README.md ├── .gitignore ├── .env.example ← пример переменных окружения (без секретов!) ├── src/ │ └── myapp/ │ ├── __init__.py │ ├── __main__.py ← python -m myapp │ ├── config.py ← настройки из переменных окружения │ ├── api/ ← HTTP-слой (роуты, схемы) │ ├── services/ ← бизнес-логика │ ├── repositories/ ← доступ к данным │ └── models.py ├── tests/ │ ├── conftest.py │ ├── unit/ │ └── integration/ ├── migrations/ ← миграции БД (alembic) ├── Dockerfile └── docker-compose.yml

Зачем src/: из корня репозитория нельзя случайно импортировать myapp «как папку». Тесты работают с установленным пакетом (pip install -e .), и если вы забыли включить файл в пакет — тесты это поймают.

Плоская раскладка (myapp/ прямо в корне) проще и тоже распространена — например, в Django-проектах.

Точки входа

# src/myapp/__main__.py
from myapp.cli import main

main()

Теперь python -m myapp запускает приложение. А в pyproject.toml можно объявить консольную команду:

[project.scripts]
myapp = "myapp.cli:main"     # после установки появится команда `myapp`

Типичные ошибки импорта

Ошибка Частая причина Решение
ModuleNotFoundError: No module named 'myapp' запуск не из того каталога, пакет не установлен, не активировано окружение pip install -e ., python -m, активировать venv
attempted relative import with no known parent package файл с относительными импортами запущен как скрипт python -m package.module
cannot import name 'X' (most likely due to a circular import) циклический импорт вынести общее, импорт внутри функции
module 'random' has no attribute 'randint' свой файл random.py перекрыл стандартный переименовать файл, удалить __pycache__
No module named 'requests' пакет установлен в другое окружение python -m pip install requests тем же интерпретатором

Какой Python?

which python / python -c "import sys; print(sys.executable)" показывают, какой интерпретатор запускается. Устанавливайте пакеты через python -m pip, чтобы pip гарантированно относился к этому интерпретатору.

Вопрос на собеседовании

«Чем модуль отличается от пакета?» — Модуль — один файл .py. Пакет — папка с модулями (обычно с __init__.py), которая сама импортируется как модуль и позволяет строить иерархию package.subpackage.module.

Итог

  • Пакет — папка с __init__.py; при импорте подпакета сначала инициализируются родители.
  • Абсолютные импорты — по умолчанию; относительные (., ..) — внутри пакета, не работают в скриптах.
  • __init__.py — место для публичного API, а не для тяжёлой логики.
  • src-layout + pyproject.toml + python -m — современная структура проекта.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Заполнить таблицу

Файл shop/orders/service.py. Какой импорт куда ведёт?

shop/orders/models.pyshop/payments.pyshop/orders/__init__.py
from .models import Order
from ..payments import charge
from shop.payments import charge
from . import something
Отметьте ячейку — «+», пустая — «−».
Задание 2 Один вариант ответа

Почему при запуске python shop/orders/service.py падает ImportError: attempted relative import with no known parent package?

Задание 3 Один вариант ответа

Что выполняется при import shop.orders?

Задание 4 Один вариант ответа

Главное преимущество раскладки src/ (код в src/myapp/)?

Задание 5 Ввод ответа

Какой файл внутри пакета выполняется при команде python -m mypackage? Напишите имя файла.

Войдите, чтобы отмечать пройденные уроки.