$ sudo teach IT
МОДУЛЬ 9 · УРОК 4

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

Мы научились создавать пакеты и модули. Теперь разберём, как правильно импортировать код между файлами пакета. Существуют два принципиально разных способа — абсолютный и относительный. Разберём каждый до деталей, узнаем о коварных циклических импортах и типичной ошибке новичков.

⏱ ~50 минут 🎓 Для новичков 🐍 Python 3

🗂️ Зачем вообще нужны разные виды импортов?

Представьте: вы пишете библиотеку для работы с интернет-магазином. В ней есть пакет shop с несколькими файлами — товары, корзина, пользователи. Файл cart.py («корзина») хочет использовать класс Product из файла products.py («товары»). Как написать этот импорт?

Вот где начинается развилка. В Python есть два способа:

🏠 Абсолютный импорт

Полный путь от корня проекта: from shop.products import Product. Всегда понятно, откуда берётся код.

📍 Относительный импорт

Путь относительно текущего файла: from .products import Product. Точка означает «тот же пакет, что и я».

Начнём с абсолютных импортов — они проще и используются чаще.

🌳 Структура нашего учебного пакета

Чтобы примеры были понятны, давайте зафиксируем структуру проекта. Мы будем работать с пакетом shop («магазин»):

my_project/
├── main.py                  ← точка входа, запускаем этот файл
└── shop/                    ← наш пакет
    ├── __init__.py
    ├── products.py          ← класс Product (товар)
    ├── cart.py              ← класс Cart (корзина)
    ├── user.py              ← класс User (пользователь)
    └── utils/               ← вложенный пакет с вспомогательными функциями
        ├── __init__.py
        ├── formatter.py     ← форматирование цен
        └── validator.py     ← проверка данных

Содержимое файлов для примеров:

# shop/products.py
class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __repr__(self):
        return f"Product('{self.name}', {self.price})"
# shop/utils/formatter.py
def format_price(amount, currency="RUB"):
    return f"{amount:,.2f} {currency}"

🏠 Абсолютные импорты — полный адрес от корня

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

💡 Аналогия: абсолютный импорт — это как полный почтовый адрес: «Россия, Москва, ул. Ленина, д. 5». Вы всегда знаете, где это находится, независимо от того, откуда смотрите.

Вот как используются абсолютные импорты в нашем проекте:

# main.py — запускаем из корня проекта
from shop.products import Product       # пакет shop → файл products → класс Product
from shop.cart import Cart              # пакет shop → файл cart → класс Cart
from shop.utils.formatter import format_price  # вложенный пакет utils → файл formatter

p = Product("Ноутбук", 50000)
print(format_price(p.price))  # 50,000.00 RUB

Абсолютные импорты также используются внутри самого пакета. Например, если cart.py нужен класс Product:

# shop/cart.py — абсолютный импорт внутри пакета
from shop.products import Product   # полный путь: shop → products → Product

class Cart:
    def __init__(self):
        self.items = []

    def add(self, product: Product, quantity: int = 1):
        self.items.append((product, quantity))

    def total(self):
        return sum(p.price * q for p, q in self.items)

    def __repr__(self):
        return f"Cart({len(self.items)} items, total={self.total()})"

✅ PEP 8 рекомендует абсолютные импорты — официальный стандарт стиля Python говорит, что абсолютные импорты предпочтительнее. Они более явные, читаемые и не вызывают путаницы.

📍 Относительные импорты — точка как компас

Относительный импорт — это когда путь к модулю указывается относительно текущего файла. Вместо полного адреса от корня вы говорите: «возьми это из соседнего файла» или «возьми из папки-родителя».

Ключевой символ — точка. Одна точка — это «тот же пакет», две точки — «пакет-родитель» (на уровень выше).

Синтаксис точек:

from .module import X — одна точка: импорт из того же пакета (соседний файл)

from ..module import X — две точки: импорт из пакета на уровень выше

from ...module import X — три точки: ещё на уровень выше (редко используется)

Пример с одной точкой (.)

Мы в файле shop/cart.py. Хотим импортировать Product из соседнего файла shop/products.py. Одна точка означает «в той же папке/пакете, где нахожусь я»:

# shop/cart.py — относительный импорт (одна точка)
from .products import Product   # . = текущий пакет (shop), ищем products.py рядом

class Cart:
    def __init__(self):
        self.items = []

    def add(self, product: Product, quantity: int = 1):
        self.items.append((product, quantity))

    def total(self):
        return sum(p.price * q for p, q in self.items)

Пример с двумя точками (..)

Мы в файле shop/utils/formatter.py. Хотим импортировать Product из shop/products.py. Нам нужно «выйти» из папки utils в папку shop — это уровень выше, поэтому две точки:

# shop/utils/formatter.py — выходим на уровень выше
from ..products import Product   # .. = пакет shop (родитель utils)

def format_product(product: Product) -> str:
    return f"{product.name}: {product.price:,.0f} руб."

# Для сравнения — абсолютный вариант того же импорта:
# from shop.products import Product

⚠️ Когда использовать относительные импорты? Главным образом — внутри одного пакета при рефакторинге (переименовании пакета). Если пакет shop переименовать в store, то все абсолютные импорты типа from shop.products import ... нужно исправлять. А относительные (from .products import ...) продолжат работать без изменений, потому что они не знают имени пакета.

⚖️ Абсолютный vs Относительный — что выбрать?

Давайте сравним оба подхода для одного и того же случая: мы в shop/cart.py и хотим импортировать из соседних файлов.

# shop/cart.py — ДВА способа написать одно и то же

# --- Абсолютный стиль (рекомендуется PEP 8) ---
from shop.products import Product
from shop.utils.formatter import format_price

# --- Относительный стиль (допустим внутри пакета) ---
from .products import Product
from .utils.formatter import format_price

🏠 Абсолютные

✅ Рекомендует PEP 8

✅ Легко читать — сразу понятно, откуда код

✅ Работают при прямом запуске файла

❌ При переименовании пакета — нужно обновлять

📍 Относительные

✅ Устойчивы к переименованию пакета

✅ Коротко при глубокой вложенности

❌ Нельзя использовать в скриптах

❌ Иногда сложнее понять, что именно импортируется

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

🔄 Циклические импорты — ловушка для новичков

Циклический импорт — это ситуация, когда модуль A импортирует модуль B, а модуль B импортирует модуль A. Получается замкнутый круг, и Python не знает, что загружать первым.

🚨 Проблема: Python начинает загружать A, видит импорт B, начинает загружать B, видит импорт A... но A ещё не до конца загружен! Это приводит к ошибке ImportError или к тому, что нужный класс ещё не определён.

Пример проблемы

Представим: user.py хочет знать о корзине пользователя, а cart.py хочет знать о пользователе-владельце:

# shop/user.py — ПРОБЛЕМА: циклический импорт
from shop.cart import Cart   # импортируем Cart из cart.py

class User:
    def __init__(self, name):
        self.name = name
        self.cart = Cart()   # у каждого пользователя есть корзина
# shop/cart.py — ПРОБЛЕМА: циклический импорт
from shop.user import User   # импортируем User из user.py

class Cart:
    def __init__(self):
        self.owner: User = None   # у корзины есть владелец-пользователь
        self.items = []

Запустим и получим:

# Вывод при запуске:
# ImportError: cannot import name 'Cart' from partially initialized module 'shop.cart'
# (most likely due to a circular import)

Python честно говорит: модуль shop.cart «частично инициализирован» — он начал загружаться, но не закончил, потому что попал в петлю.

Три способа решения

Способ 1: Поздний импорт (внутри функции/метода) — самый простой для новичков. Импорт переносится внутрь функции, где он действительно нужен. К моменту вызова функции оба модуля уже загружены:

# shop/cart.py — РЕШЕНИЕ: поздний импорт внутри метода
class Cart:
    def __init__(self):
        self.items = []

    def set_owner(self, user):
        # Импорт здесь, а не на уровне модуля!
        # К моменту вызова set_owner() модуль user.py уже полностью загружен
        from shop.user import User
        if not isinstance(user, User):
            raise TypeError("owner must be a User instance")
        self.owner = user

Способ 2: Реструктуризация кода — лучшее долгосрочное решение. Если возникает циклический импорт, это часто сигнал, что ответственность распределена неправильно. Вынесите общие зависимости в третий модуль:

# shop/base.py — общие типы без зависимостей
class BaseEntity:
    """Базовый класс, от которого наследуют User и Cart"""
    def __init__(self, entity_id):
        self.id = entity_id
# shop/user.py — теперь не нужен импорт cart.py на уровне модуля
from shop.base import BaseEntity

class User(BaseEntity):
    def __init__(self, name):
        super().__init__(id(self))
        self.name = name
        self.cart = None   # Cart создаётся снаружи и присваивается сюда
# shop/cart.py — не нужен импорт user.py на уровне модуля
from shop.base import BaseEntity

class Cart(BaseEntity):
    def __init__(self):
        super().__init__(id(self))
        self.owner = None   # User присваивается снаружи
        self.items = []

Способ 3: TYPE_CHECKING — элегантный способ для аннотаций типов. Импорт нужен только для подсказок типов в IDE, но не во время выполнения:

# shop/cart.py — TYPE_CHECKING для аннотаций
from __future__ import annotations   # обязательно! включает отложенные аннотации
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    # Этот блок выполняется ТОЛЬКО при статическом анализе (mypy, IDE)
    # При реальном запуске программы он ПРОПУСКАЕТСЯ
    from shop.user import User

class Cart:
    def __init__(self):
        self.owner: "User" = None   # строка в кавычках — отложенная аннотация
        self.items = []

    def set_owner(self, user: "User") -> None:
        self.owner = user

✅ Итог по циклическим импортам: для новичков рекомендуется поздний импорт внутри функции — это быстро и понятно. Если цикл возникает часто, стоит переосмыслить архитектуру (Способ 2). TYPE_CHECKING — инструмент для более зрелого кода с аннотациями типов.

💥 Самая частая ошибка: запуск файла с относительными импортами напрямую

Есть одна ошибка, которую совершают почти все новички. Вы написали в cart.py относительный импорт и решили запустить этот файл напрямую, чтобы проверить код:

# Ошибочный запуск — прямо из папки shop/
python shop/cart.py

# ИЛИ так (тоже ошибка):
cd shop
python cart.py

Вы получите:

ImportError: attempted relative import with no known parent package

Почему так происходит? Когда Python запускает файл напрямую (как скрипт), он не считает его частью пакета — для него этот файл является «главным», у него нет «родительского пакета». Относительный импорт строится на понятии «я нахожусь внутри пакета X», а при прямом запуске такого контекста нет.

Воспроизводим ошибку:

# shop/cart.py
from .products import Product   # относительный импорт

class Cart:
    pass
# Запускаем напрямую — ОШИБКА:
$ python shop/cart.py
ImportError: attempted relative import with no known parent package

# Правильно — запускать main.py из корня проекта:
$ python main.py    # ✅ работает!

Три правильных способа запуска

# Способ 1: запускать главный файл из корня (самый простой)
$ python main.py

# Способ 2: запускать как модуль с флагом -m (Python сам понимает структуру пакета)
$ python -m shop.cart    # запустить cart.py как часть пакета shop

# Способ 3: если нужно тестировать файл напрямую — используй if __name__ == "__main__"
# и абсолютные импорты внутри этого блока
# shop/cart.py — безопасная версия для тестирования
from .products import Product   # нормальный относительный импорт для пакета

class Cart:
    def __init__(self):
        self.items = []

# Блок для прямого тестирования файла (запускается только при python cart.py)
if __name__ == "__main__":
    # Внутри этого блока используем абсолютный импорт
    import sys, os
    sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    from shop.products import Product as P
    c = Cart()
    print("Cart работает!")   # Cart работает!

💡 Простое правило: если в файле есть относительные импорты (from .что_то import ...), его нельзя запускать напрямую через python имя_файла.py. Запускайте только через главный файл (main.py) или через флаг -m.

🏁 Итоги урока

🏠 Абсолютный импорт

Полный путь от корня: from shop.products import Product. Рекомендован PEP 8. Использовать по умолчанию.

📍 Относительный импорт

from .module import X — тот же пакет. from ..module import X — уровень выше. Удобен при рефакторинге имён пакета.

🔄 Циклические импорты

A импортирует B, B импортирует A — ошибка. Решение: поздний импорт внутри функции, реструктуризация или TYPE_CHECKING.

💥 ImportError при запуске

Файл с относительными импортами нельзя запускать напрямую. Только через main.py из корня или python -m package.module.

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