Структура проектов: модули и подпакеты в Python

Раздел: Структура проекта -> пакеты 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.

пакеты Python - comments

En
Python py package (python)