Структура проектов: модули и подпакеты в Python
Создание пакета с помощью каталога и файла __init__.py
Как организовать модули в пакет, чтобы импортировать их единым именем?
Пакет в Python это директория, содержащая файл __init__.py (может быть пустым) и другие модули. При импорте пакета исполняется __init__.py. Рассмотрим структуру:
project/
mypackage/
__init__.py
module_a.py
module_b.py
Python py package (пакеты python)
Содержимое module_a.py:
def func_a():
return "A"
Теперь импорт:
import mypackage.module_a
print(mypackage.module_a.func_a())
A
Для упрощения доступа можно импортировать функции в __init__.py:
# __init__.py
from .module_a import func_a
from .module_b import func_b
Тогда импорт:
import mypackage
print(mypackage.func_a())
A
Типичные ошибки
- Импорт приводит к ошибке ModuleNotFoundError. Решение: проверить текущий рабочий каталог или добавить путь в sys.path.
- Ошибка ImportError при циклическом импорте. Избегать взаимного импорта модулей внутри пакета.
Как управлять списком импортируемых имен при использовании from package import *?
Используйте переменную __all__ в __init__.py. Определите какие имена должны экспортироваться.
# __init__.py
__all__ = ['func_a', 'func_b']
from .module_a import func_a
from .module_b import func_b
Теперь при from mypackage import * будут доступны только func_a и func_b.
Если __all__ не определен, то import * импортирует все имена, не начинающиеся с подчеркивания. Но рекомендуется явно задавать __all__ для контроля.
Как импортировать модули из подпакетов без указания полного пути?
С помощью относительных импортов.
# mypackage/subpackage/module_c.py
from ..module_a import func_a
Относительные импорты допустимы только внутри пакета. При запуске скрипта напрямую они работать не будут. Используйте python -m mypackage.script.
Ошибка ImportError: attempted relative import with no known parent package возникает, если скрипт запущен как главный. Решение: запускать через -m или перестроить структуру.
Как создать пакет, который не требует __init__.py (namespace package)?
Этот подход используется для распределенных пакетов. Если в директории нет __init__.py и она находится в sys.path, она становится namespace package. Однако для большинства проектов рекомендуется использовать обычные пакеты с __init__.py.
# две отдельные директории в разных местах, но объединённые под одним именем
dir1/mypackage/module_a.py
dir2/mypackage/module_b.py
Если обе директории добавлены в sys.path, то mypackage будет namespace package, объединяющим модули из обоих мест.
Трудно отследить, откуда берутся модули. Редко используется.
Как сделать пакет устанавливаемым с помощью pip?
Создается файл setup.py (или pyproject.toml) с описанием метаданных. После выполнения pip install . пакет копируется в site-packages и становится доступным для импорта из любого места.
# setup.py
from setuptools import setup, find_packages
setup(
name='mypackage',
version='1.0',
packages=find_packages(),
)
Затем команда:
pip install .
После установки пакет можно импортировать как обычно.
Ошибка при неправильном указании packages. Используйте find_packages() или явный список.
Расширенные примеры работы с пакетами
Пример пакета с вложенными подпакетами и динамическим импортом.
# структура:
app/
__init__.py
core/
__init__.py
utils.py
models/
__init__.py
user.py
services/
__init__.py
auth.py
Содержимое app/core/utils.py:
def hash_password(pwd):
import hashlib
return hashlib.sha256(pwd.encode()).hexdigest()
В app/models/user.py относительный импорт:
from ..core.utils import hash_password
class User:
def __init__(self, name, password):
self.name = name
self.password_hash = hash_password(password)
В app/services/auth.py:
from ..models.user import User
class AuthService:
@staticmethod
def login(name, password):
user = User(name, password)
return user.password_hash == expected_hash
Файл app/__init__.py для удобства:
from .models.user import User
from .services.auth import AuthService
Теперь клиентский код:
from app import User, AuthService
user = User("Alice", "secret")
print(user.password_hash)
2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b5
Динамический импорт модуля по имени строки:
import importlib
module_path = 'app.models.user'
user_module = importlib.import_module(module_path)
UserClass = getattr(user_module, 'User')
print(UserClass)
Перечисление всех модулей в пакете с использованием pkgutil:
import pkgutil
import app
for importer, modname, ispkg in pkgutil.walk_packages(path=app.__path__, prefix=app.__name__ + '.'):
print(modname, ispkg)
app.core True app.core.utils False app.models True app.models.user False app.services True app.services.auth False
Загрузка ресурсов из пакета с помощью importlib.resources (Python 3.9+):
import importlib.resources as resources
text = resources.read_text('app.core', 'config.json')
# предполагается, что в core есть config.json
При использовании pkgutil.walk_packages возможно, что некоторые модули не обнаружатся, если они не импортированы. Решение: предварительно импортировать корневой пакет и использовать его __path__.
Относительные импорты внутри пакета при запуске через -m работают корректно. При прямом запуске скрипта из пакета возникает ошибка. Рекомендуется запускать с помощью -m.