Параллельный импорт между моделями SQLAlchemy
У меня был модуль, описывающий модели данных:
Основные классы:
- ActionType: Таблица-Справочник отслеживаемых действий
- AuditLog: Логирование действий пользователей.
- Assembly: Описание сборочных чертежей.
- Part: Описание деталей.
- AssemblyPart: Связь между сборками и деталями.
- AssemblyAssembly: Связь между сборками и другими сборками.
- Material: Таблица-Справочник материалов.
- Thick: Таблица-Справочник толщин материалов.
- ProgramX5: Программы обработки для станков Х5.
- PartProgramX5: Связь между деталями и программами обработки.
- Role: Таблица-Справочник ролей пользователей.
- User: Пользователи системы.
Учитывая, что это минимальная структура, я решил сразу разделить его на 3 части:
- модуль с основными моделями (Сборки, детали, пользователи)
- модуль со справочниками (роли, материалы и т.д.)
- модуль связей (все модели, организующие связь многие-ко-многим)
И тут я столкнулся с перекрестным импортом между справочниками / связями и основными моделями.
Как это решается? Интересует best practices декларативного стиля SQLAlchemy.
Ответы (1 шт):
Скорее вопрос о "Круговом" а не "Параллельном" импорте.
Если это так, то: один модуль(файл) A не может завершить инициализацию, так как требует зависимый модуль B, который, в свою очередь, требует A.
Такое невозможно и попытка решить нерешаемое не принесет успехов.
TYPE_CHECKING
Если речь о импорте типов(как аннотаций), то это решает TYPE_CHECKING.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from my_module import MyType
def foo(instance: "MyType"):
data = instance.get_data()
Такой тип нельзя использовать явно, например создать экземпляр класса ins = MyType(), но это не мешает вызывать его методы и использовать в аннотациях. Попросту говоря - строки в TYPE_CHECKING интерпретатор проигнорирует, но они видны статическим анализаторам.
Что касается библиотек SQLModel/SQLAlchemy, используется тот же TYPE_CHECKING, и так же окружаем ссылку на символ в скобки, тык SQLModel:
team: Optional["SomeClass"] = Relationship(back_populates="xyz")
... точно так же как это происходит с именем класса, когда он полностью не определен:
class SomeClass:
def try_get(self) -> "None | SomeClass":...
Protocol
Для некоторых вспомогательных типов можно вынести в отдельные файлы классы протоколов. Почти как абстрактный класс, но немного гибче и менее зависим от других модулей. Это очень напоминает определение интерфейсов:
// types.py
from typing import Protocol
class LoggerProtocol(Protocol):
def error(self, msg: str) -> None:...
... после реализовать такой класс-протокол LoggerImpl в любом месте(наследовать его необязательно), но импортировать в зависимые модули именно протокол:
from .types import LoggerProtocol
class MyModule:
def __init__(self, logger: LoggerProtocol):...
Конечно, такой подход требует возможности создать LoggerImpl и передать его методам зависимого модуля:
logger = LoggerImpl()
my_module = MyModule(logger)
Протоколы работают так же как типы TypeScript: структурная типизация - главное чтобы были все свойства и методы, а что передается в качестве этого типа не имеет значения.
Грязные методы
<pre>(Вместо спойлера которого тут нет)
На самом деле любую зависимость можно получить налету используя нечто подобное:
```py
if TYPE_CHECKING:
from .my_module import SomeClass
# так
def create_instance(class_name: str, *args: Any, **kwargs: Any) -> Any:
module = importlib.import_module("my_project.my_module")
cls = getattr(module, class_name)
return cls(*args, **kwargs)
# ... или так
def createSomeClass() -> "SomeClass":
from .my_module import SomeClass
return SomeClass()
```
... но это хорошо для условных `if sys.platform == "win32"`, а не круговых зависимостей.
Такие фокусы с круговыми зависимостями приведут к повторному выполнению кода модуля.
</pre>
PS
Чаще всего таких подходов TYPE_CHECKING/Interface-Protocol достаточно чтобы разрешить круговые зависимости, а что такое best practices в SQLAlchemy я не знаю.
PPS: Иногда проще оставить 1000_строк в одном файле не выдумывая лишних имен файлов и не решая проблем с круговыми зависимостями.