Я разрабатываю фрагмент кода вокруг древовидной структуры. Дерево может делать несколько вещей, одна из которых — способность сериализовать и десериализовать свои данные. Есть несколько разных типов узлов, например NodeA и NodeB, каждый представлен классом. Различные типы узлов могут иметь существенно разные функции и могут содержать разные типы данных. Как общая база, все классы узлов наследуются от «базовой» Node класс. Любой данный тип узла может быть в корне дерева. Упрощенный пример предполагаемой структуры выглядит следующим образом:
from typing import Dict, List
from abc import ABC
class NodeABC(ABC):
pass
class Node(NodeABC):
subtype = None
def __init__(self, nodes: List[NodeABC]):
self._nodes = nodes
def as_serialized(self) -> Dict:
return {"nodes": [node.as_serialized() for node in self._nodes], "subtype": self.subtype}
@classmethod
def from_serialized(cls, subtype: str, nodes: List[Dict], **kwargs):
nodes = [cls.from_serialized(**node) for node in nodes]
if subtype == NodeA.subtype:
return NodeA(**kwargs, nodes = nodes)
elif subtype == NodeB.subtype:
return NodeB(**kwargs, nodes = nodes)
class NodeA(Node):
subtype = "A"
def __init__(self, foo: int, **kwargs):
super().__init__(**kwargs)
self._foo = foo * 2
def as_serialized(self) -> Dict:
return {"foo": self._foo // 2, **super().as_serialized()}
class NodeB(Node):
subtype = "B"
def __init__(self, bar: str, **kwargs):
super().__init__(**kwargs)
self._bar = bar + "!"
def as_serialized(self) -> Dict:
return {"bar": self._bar[:-1], **super().as_serialized()}
demo = {
"subtype": "A",
"foo": 3,
"nodes": [
{
"subtype": "B",
"bar": "ghj",
"nodes": []
},
{
"subtype": "A",
"foo": 7,
"nodes": []
},
]
}
assert demo == Node.from_serialized(**demo).as_serialized()
Итог: работает. Проблема: между фактическими типами узлов существуют «круговые» зависимости. NodeA/NodeB и база Node класс. Если весь этот код находится в одном файле Python, он работает нормально. Однако, если я попытаюсь переместить каждый класс в отдельный файл, интерпретатор Python станет недоволен из-за (теоретически необходимого) циклического импорта. Фактические классы действительно большие, поэтому я хотел бы немного структурировать свой код.
Вопрос: Согласно общепринятому мнению, если циклический импорт / зависимости становятся предметом обсуждения, тогда дизайн / структура кода отстой и в первую очередь виноваты. Я согласен с этим, но у меня действительно не так много хороших идей, как улучшить вышеперечисленное.
Я знаю, что могу устранить «ограничение» циклического импорта, выполнив «импорт вручную во время выполнения» по крайней мере для одной части круга плюс некоторые ошибки, но этого я бы хотел избежать …
КОНТЕКСТ
Я занимаюсь разработкой модуль Python zugbruecke.
Он позволяет вызывать подпрограммы в Windows DLL из кода Python, работающего в Unices / Unix-подобных системах, таких как Linux, MacOS или BSD. zugbruecke разработан как прямая замена для модуля ctypes стандартной библиотеки Python. zugbruecke построен на основе Wine. Автономный интерпретатор Windows Python, запускаемый в фоновом режиме, используется для выполнения вызываемых подпрограмм DLL.
Его код для синхронизации обоих ctypes типы данных и ctypes данные стали немного устаревшими и запыленными и могут потребовать серьезного рефакторинга. Его текущая форма действительно не объектно-ориентирована, можно найти Вот (определения типов данных), Вот (в основном фактические данные), Вот (определения синхронизации указателя) и Вот (фактическая синхронизация указателя). Рано эскиз о том, как я хотел бы ввести правильную ориентацию объекта (и какую-то правильную файловую структуру), можно найти Вот. Приведенный выше пример — это упрощенная версия моего настоящего эскиза.
2 ответа
Вы можете отделить свое определение узла от процесса сериализации, который учитывает порядок зависимости (сериализация узла) -> (реализация узла) -> (база узла). Это также снижает количество super жонглирование, которое нужно выполнять в каждом подклассе.
Если вы хотите еще больше облегчить определенные типы узлов, вы также можете удалить необходимость в **kwargs путем создания экземпляров объектов с помощью словаря, который не содержит записей для «подтипа» или «узлов».
from typing import Dict, List, Optional
from abc import ABC
# node.py
class Node(ABC):
nodes:List['Node']
subtype:Optional[str] = None
def serialize(self) -> Dict:
raise NotImplementedError()
# node_a.py (depends on node)
class NodeA(Node):
subtype = "A"
def __init__(self, foo: int, **kwargs):
self._foo = foo * 2
def serialize(self) -> Dict:
return {"foo": self._foo // 2}
# node_b.py ( depends on node)
class NodeB(Node):
subtype = "B"
def __init__(self, bar: str, **kwargs):
self._bar = bar + "!"
def serialize(self) -> Dict:
return {"bar": self._bar[:-1]}
# node_serialization.py ( depends on node_a, node_b )
NODE_TYPES = {
nodeclass.subtype:nodeclass
for nodeclass in [NodeA,NodeB]
}
def deserialize(serialized_node: Dict) -> Node:
node = NODE_TYPES[serialized_node['subtype']](**serialized_node)
node.nodes = [ deserialize(node) for node in serialized_node['nodes'] ]
return node
def serialize(node:Node) -> Dict:
return {
**node.serialize(),
'nodes': [ serialize(node) for node in node.nodes ],
'subtype': node.subtype
}
# usage
original = {
"subtype": "A",
"foo": 3,
"nodes": [
{
"subtype": "B",
"bar": "ghj",
"nodes": []
},
{
"subtype": "A",
"foo": 7,
"nodes": []
},
]
}
assert original == serialize(deserialize(original))
Экземпляры классов Python включают атрибут __class__. Node.as_serialized() использует self.__class__.__module__ и self.__class__.__name__ для сериализации подтипа узла и воссоздания узла при десериализации. В настоящее время, Node.from_serialized() не нужно ссылаться на другие классы Node, поэтому нет проблемы циклического импорта.
import sys
class Node:
def __init__(self, nodes):
self._nodes = nodes
def as_serialized(self):
return {"nodes": [node.as_serialized() for node in self._nodes],
"subtype": (self.__class__.__module__, self.__class__.__name__)}
@classmethod
def from_serialized(cls, subtype, nodes, **kwargs):
nodes = [cls.from_serialized(**node) for node in nodes]
module, klass = subtype
return getattr(sys.modules[module], klass)(**kwargs, nodes=nodes)
class NodeA(Node):
def __init__(self, foo: int, **kwargs):
super().__init__(**kwargs)
self._foo = foo * 2
def as_serialized(self):
return {"foo": self._foo // 2, **super().as_serialized()}
class NodeB(Node):
def __init__(self, bar: str, **kwargs):
super().__init__(**kwargs)
self._bar = bar + "!"
def as_serialized(self):
return {"bar": self._bar[:-1], **super().as_serialized()}
Обратите внимание, что поле «подтип» изменилось:
demo = {
"subtype": ("__main__", "NodeA"),
"foo": 3,
"nodes": [
{
"subtype": ("__main__", "NodeB"),
"bar": "ghj",
"nodes": []
},
{
"subtype": ("__main__", "NodeA"),
"foo": 7,
"nodes": []
},
]
}
Спасибо за ответ. Собственно, ваш код все еще является циклической зависимостью.
NodeAпроисходит отNode. К тому же,NodeAтакже должен быть импортирован в контекстеNodeдля тебяgetattrвызов функции для работы. Может быть, мне чего-то не хватает, но как бы вы поместили каждый из трех классов в отдельный .py-файл, а затем как бы вы решить возникающую проблему циклического импорта (которая, на мой взгляд, все еще остается)?— см
@sme, как бы вы использовали
Node,NodeA, иNodeBесли вы их куда-то не импортировали? Ваш основной (или другой) код импортирует их, чтобы построить дерево. Когда вы импортируете модуль, он кешируется вsys.modules. Вот почему мой код ищет модуль вsys.modulesа затем получает класс из модуля. Если вы импортировалиNodeAиз «fileA.py», затемgetattr(sys.modules["fileA"], "NodeA")получит соответствующий класс. Если хочешь ,Node.from_serialized()также может загрузить модуль, если он еще не был загружен. Посмотрите как код для_Loaderвpickleмодуль.— RootTwo
Да, это та «загрузка во время выполнения» / боджинг, о которой я имел в виду. Это некрасиво и имеет тенденцию к увлекательным неудачам, но да, я делал это раньше. В конце концов, это допустимый вариант.
— см
Которые должны быть
_Unpicklerне_Loader.— RootTwo
Благодарю. К вашему сведению, я просто добавил контекстный раздел к своему вопросу для получения дополнительных сведений. Смотрите ссылку на мой скетч. Возможно, это поможет пролить дополнительный свет на эту тему.
— см

Большое спасибо. Хм, просто двигая
deserializeметод вне класса и в своего рода независимую функцию решает проблему, да. Честно говоря, мне это не нравилось … это как-то кажется неправильным, потому что в моем реальном коде не только один метод, подобный этому, должен быть перемещен за пределы класса.— см
К вашему сведению, я просто добавил контекстный раздел к своему вопросу для получения дополнительных сведений. Смотрите ссылку на мой скетч. Возможно, это поможет пролить дополнительный свет на эту тему.
— см