Инициализация пакета Python через __init__.py
Основные принципы работы __init__.py
Файл __init__.py автоматически выполняется при импорте пакета. В Python 3.3+ он может отсутствовать, если используется namespace packages, но для обычных пакетов его наличие обязательно. Основное назначение - инициализация пакета и управление импортами.
# mypackage/__init__.py
from .submodule import useful_class
from . import utils
__all__ = ['useful_class', 'utils']
Python init script (инициализация скрипта python (__init__.py))
Этот код делает useful_class и utils доступными напрямую через import mypackage. Без __init__.py пакет не будет распознан интерпретатором (в старых версиях Python).
Типичные проблемы:
- Циклические импорты - когда подмодули импортируют друг друга через пакет. Решение: отложенные импорты внутри функций или использование абсолютных путей.
- Побочные эффекты при импорте - если
__init__.pyвыполняет тяжелую инициализацию (запись в БД, загрузку файлов), это замедляет импорт. Решение: перенести такую логику в отдельную функцию, вызываемую явно.
Как определить список экспортируемых имен через __all__?
__all__ задает, какие имена будут импортированы при from package import *. Если __all__ не указан, импортируются все имена, не начинающиеся с подчеркивания. Это помогает контролировать публичный интерфейс.
# __init__.py
from .module_a import A
from ._private import _internal
__all__ = ['A'] # _internal не будет экспортирован
Ошибка: забыли указать __all__ - пользователи могут случайно импортировать внутренние модули. Решение: всегда явно определять __all__ для крупных пакетов.
Как выполнить код инициализации при импорте пакета?
Например, настроить логирование или считать конфигурацию из переменных окружения. Главное - избегать блокирующих операций.
# __init__.py
import logging
import os
logging.basicConfig(level=os.getenv('LOG_LEVEL', 'INFO'))
logger = logging.getLogger(__name__)
logger.info('Package initialized')
Проблема: двойная инициализация при импорте в нескольких местах. Решение: проверять флаг или использовать синглтон.
Как организовать ленивый импорт подмодулей?
Если подмодули тяжелые, их импорт можно отложить до первого обращения. В Python 3.7+ для этого используется __getattr__ на уровне пакета.
# __init__.py
import importlib
def __getattr__(name):
if name == 'heavy_module':
return importlib.import_module(f'{__name__}.heavy_module')
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
Теперь mypackage.heavy_module импортируется только при обращении к нему.
Ошибка: __getattr__ срабатывает для каждого обращения. Если имя совпадает с уже существующим атрибутом, функция не вызывается. Решение: проверять, что имя отсутствует в sys.modules.
Как автоматически импортировать все подмодули?
Для автоматизации импорта всех файлов в пакете можно использовать pkgutil или importlib. Полезно для плагинов или динамической регистрации.
# __init__.py
import pkgutil
import importlib
__all__ = []
for loader, module_name, is_pkg in pkgutil.walk_packages(__path__):
if not is_pkg:
module = importlib.import_module(f'{__name__}.{module_name}')
globals()[module_name] = module
__all__.append(module_name)
Проблема: порядок импорта может вызвать циклические зависимости. Решение: явно сортировать модули или использовать topological_sort.
Расширенные примеры использования __init__.py
Пример 1: Реализация lazy-импорта с помощью __getattr__
Создадим пакет mypackage с тяжелыми модулями. Вместо того чтобы импортировать их все сразу, будем загружать по требованию.
# mypackage/__init__.py
import importlib
import sys
# Список ленивых модулей
_LAZY_MODULES = ['heavy1', 'heavy2', 'heavy3']
def __getattr__(name):
if name in _LAZY_MODULES:
module = importlib.import_module(f'{__name__}.{name}')
# Кэшируем модуль в sys.modules, чтобы повторный вызов не триггерил __getattr__
sys.modules[f'{__name__}.{name}'] = module
return module
raise AttributeError(f'Модуль {__name__!r} не содержит {name!r}')
Результат:
>>> import mypackage >>> # heavy1, heavy2, heavy3 еще не импортированы >>> mypackage.heavy1 # теперь импортируется heavy1
Пример 2: Автоматическая регистрация плагинов
В пакете plugins каждый файл содержит класс с атрибутом name. В __init__.py собираем их в словарь.
# plugins/__init__.py
import pkgutil
import importlib
plugins = {}
for loader, module_name, is_pkg in pkgutil.iter_modules(__path__):
module = importlib.import_module(f'{__name__}.{module_name}')
for attr_name in dir(module):
attr = getattr(module, attr_name)
if hasattr(attr, 'name'):
plugins[attr.name] = attr
Пример модуля плагина:
# plugins/plugin_a.py
class PluginA:
name = 'A'
def run(self):
return 'Plugin A executed'
Результат:
>>> from plugins import plugins >>> plugins['A'].run() 'Plugin A executed'
Пример 3: Инициализация с конфигурацией из переменных окружения
Пакет config при импорте загружает настройки из .env или переменных системы.
# config/__init__.py
import os
from dotenv import load_dotenv
load_dotenv() # загружаем .env если есть
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///default.db')
SECRET_KEY = os.getenv('SECRET_KEY')
if not SECRET_KEY:
raise RuntimeError('SECRET_KEY не задан')
Использование:
>>> from config import DATABASE_URL, SECRET_KEY >>> print(DATABASE_URL) postgresql://user:pass@localhost/db
Пример 4: Динамическое создание публичного API
Пакет api содержит несколько модулей с функциями. В __init__.py собираем все публичные функции в единый интерфейс.
# api/__init__.py
import pkgutil
import importlib
# Собираем все функции, имя которых не начинается с подчеркивания
_public_functions = {}
for loader, name, is_pkg in pkgutil.iter_modules(__path__):
module = importlib.import_module(f'{__name__}.{name}')
for func_name in dir(module):
if not func_name.startswith('_'):
func = getattr(module, func_name)
if callable(func):
_public_functions[func_name] = func
# Экспортируем все функции через globals()
globals().update(_public_functions)
__all__ = list(_public_functions.keys())
Результат:
>>> from api import my_func >>> my_func() 'Executed'