Импорты и пакеты: import, __init__.py, sys.path
Импорты и пакеты: import, init.py, sys.path
В первых уроках мы импортировали стандартные модули - import os, from json import dumps (сами эти модули разберём в уроках про os и pathlib и datetime и json). Но как это работает под капотом? Где Python ищет модули, что такое пакет, чем отличаются абсолютные импорты от относительных? В этом уроке - подробный разбор системы импортов.
Модуль vs пакет
- Модуль - один файл
.pyс Python-кодом - Пакет - директория с файлом
__init__.py, содержащая модули и/или подпакеты
Пример структуры:
myapp это пакет, myapp.utils подпакет, myapp.utils.http модуль.
init.py - маркер пакета
Файл __init__.py делает директорию пакетом. Может быть пустым, может содержать код инициализации:
# myapp/__init__.py
__version__ = "1.0.0"
from .models.user import User # реэкспорт
from .models.order import Order
Тогда снаружи можно:
from myapp import User, Order, __version__
С Python 3.3 появились namespace packages - директории без __init__.py могут быть пакетами для определённых сценариев. Но для обычных проектов всегда добавляй __init__.py - это явно и предсказуемо.
import - формы
# Импорт модуля целиком
import os
import json as J # с alias
# Импорт конкретных объектов
from os import path, getcwd
from os.path import join, exists
# Импорт всего (плохая практика)
from os import *
# Импорт пакета
import myapp.utils.http
myapp.utils.http.get(...)
# С alias
from myapp.utils.http import get as http_get
Конвенция: предпочитай явные импорты from x import y для краткости в коде, но import x для модулей с короткими часто используемыми именами.
Где Python ищет модули - sys.path
При import foo Python ищет:
- Built-in модули (compiled in)
- Frozen модули (компиляция в исполняемый файл)
- По
sys.path- список директорий
import sys
print(sys.path)
# ['', '/usr/lib/python3.12', '/usr/lib/python3.12/site-packages', ...]
sys.path обычно содержит (и именно его подменяет активированное виртуальное окружение):
- Пустая строка
''- текущая директория (где запущен скрипт) - Путь к каталогу запущенного скрипта
PYTHONPATHпеременная окружения- Стандартные пути site-packages
Можно модифицировать в runtime:
sys.path.insert(0, "/my/custom/path")
import my_module # будет искаться в добавленном пути тоже
Кеширование импортов
Python кеширует импортированные модули в sys.modules:
import sys
import json
print("json" in sys.modules) # True
# Повторный import не перезагружает
import json # уже в кеше, просто привязка имени
Это значит:
- Изменения в исходнике модуля не подхватываются автоматически
- Для перезагрузки -
importlib.reload(module) - Циклические импорты могут странно вести себя
Абсолютные импорты
Полный путь от корня пакета:
# myapp/models/user.py
from myapp.utils.string import normalize
from myapp.utils.http import get
Преимущества:
- Явные - сразу видно откуда модуль
- Не ломаются при переименовании файла
- Работают везде
Это рекомендуемый стиль в современном Python (PEP 8).
Относительные импорты
Путь относительно текущего модуля:
# myapp/models/user.py
from ..utils.string import normalize # .. означает родитель
from .order import Order # . означает текущий пакет
. это текущий пакет, .. родитель, ... дедушка.
Преимущества:
- Короче для глубоко вложенных модулей
- Удобно при перемещении пакета (внутренние импорты не ломаются)
Недостатки:
- Только внутри пакета (не работают в скриптах)
- Менее явные
- Усложняют рефакторинг
В сообществе разногласия. Многие предпочитают абсолютные везде для consistency.
Запуск модуля как скрипта - python -m
python -m myapp.main
Запускает myapp/main.py как скрипт. Это лучше чем python myapp/main.py - правильно настраивает sys.path и __package__ для относительных импортов.
Также через -m запускают модули стандартной библиотеки:
python -m http.server 8000
python -m json.tool < data.json # форматирование JSON
python -m venv .venv
python -m pip install requests
name == "main"
При импорте модуля его __name__ равно имени модуля (например "myapp.main"). При прямом запуске - "__main__". Это позволяет различать:
# myapp/main.py
def run():
print("running app")
if __name__ == "__main__":
run()
Когда модуль импортируется, run() не вызывается. При python -m myapp.main - вызывается.
Циклические импорты
# a.py
from b import B
class A:
def use_b(self):
return B()
# b.py
from a import A # циклическая зависимость!
class B:
def use_a(self):
return A()
При импорте a Python начинает загружать его, доходит до from b import B, начинает загружать b, тот пытается импортировать a который ещё не завершил загрузку - получаем неполный модуль или ImportError.
Решения:
- Reorganize код - выделить общее в третий модуль
- Lazy import внутри функции (импортируем при первом вызове):
class A:
def use_b(self):
from b import B # импорт отложен
return B()
- TYPE_CHECKING для type hints:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from b import B # импорт только для типов, не в runtime
class A:
def use_b(self) -> "B":
from b import B
return B()
ImportError vs ModuleNotFoundError
# Модуль не найден
import nonexistent
# ModuleNotFoundError: No module named 'nonexistent'
# Объект не найден в существующем модуле
from os import nonexistent_function
# ImportError: cannot import name 'nonexistent_function' from 'os'
ModuleNotFoundError это подкласс ImportError (с Python 3.6). Различие помогает в отладке.
Поиск конкретного импорта
import importlib.util
spec = importlib.util.find_spec("requests")
if spec is None:
print("requests not installed")
else:
print(f"Found at: {spec.origin}")
Удобно для опциональных зависимостей или диагностики.
all - публичный API модуля
# myapp/utils/__init__.py
from .http import get, post, delete
from .string import normalize, slugify
from .internal import _private_helper
__all__ = ["get", "post", "delete", "normalize", "slugify"]
__all__ контролирует что экспортируется при from myapp.utils import *. Подчёркивание-приватные не включаются по умолчанию, но __all__ делает интерфейс явным.
Также используется типизаторами и tools для понимания публичного API.
site-packages - где живут зависимости
python -c "import site; print(site.getsitepackages())"
# ['/usr/lib/python3.12/site-packages', '~/.local/lib/python3.12/site-packages']
site-packages - стандартная директория для third-party библиотек. pip install ставит туда. В venv своя site-packages внутри .venv/lib/python3.X/site-packages/.
Editable install для разработки
pip install -e .
Устанавливает текущий проект в editable mode: изменения в исходниках сразу видны без переустановки. Под капотом - линк в site-packages указывает на твой код. Удобно для разработки пакетов которые сам же пишешь.
Распространённые ошибки
1. import * от модуля
from utils import * # тащит ВСЁ
Импортирует всё что не начинается с _. Загрязняет namespace, ломает явность, конфликты имён. Используй только в очень специфических случаях (interactive REPL, REPL).
2. Относительный импорт в запускаемом скрипте
# myapp/main.py - запускаем как `python myapp/main.py`
from .utils import helper # ImportError: attempted relative import with no known parent
Относительные импорты работают только если модуль импортирован как часть пакета. Запускай через python -m myapp.main или используй абсолютные импорты.
3. Циклический import
Обсудили выше. Reorganize или lazy import.
4. Подмена встроенных модулей
# project/json.py - своя реализация
# project/main.py
import json # импортирует НАШУ json.py, не stdlib!
Не называй свои модули как стандартные библиотеки. На многих системах работает (импорт через sys.path), но создаёт путаницу и ломает code completion.
5. Большие импорты на module level
# на module level - выполнится при любом импорте модуля
import slow_library # 2 секунды импорта
heavy_initialization() # ещё 5 секунд
Импорты модуля выполняются один раз при первом import, но это первый раз может быть медленным. Тяжёлую инициализацию выноси в функции или lazy import.
Сравнение с Go
В Go импорт похож но строже:
import (
"fmt"
"encoding/json"
"myapp/utils"
)
- Все импорты явные в начале файла
- Нет циклических импортов (compile error)
- Нет относительных импортов (всегда абсолютные)
- Неиспользуемые импорты - compile error
Python более гибкий, но требует дисциплины. Go более строгий, что упрощает поддержку больших проектов.
Реальный пример структуры
Структура src/my_backend/ (src-layout) - современная практика. Тесты отдельно, чтобы не попадали в пакет.
Мини-задание
- Создай свой мини-пакет:
# mypackage/__init__.py
from .core import main_function
__all__ = ["main_function"]
# mypackage/core.py
from .utils.helpers import normalize
def main_function(text):
return normalize(text) + "!"
# mypackage/utils/helpers.py
def normalize(text):
return text.strip().lower()
Использование:
from mypackage import main_function
print(main_function(" Hello ")) # hello!
- Запуск модуля как скрипта:
# mypackage/__main__.py
from .core import main_function
if __name__ == "__main__":
print(main_function("Hello from script"))
Запуск: python -m mypackage
- Lazy import:
def heavy_operation():
# Тяжёлая библиотека загружается только когда функция вызвана
import pandas as pd
return pd.DataFrame({"a": [1, 2, 3]})
Что дальше
Освоили систему импортов. В следующем уроке - packaging: pyproject.toml углублённо, build wheel, публикация в PyPI, entry points.