Я разрабатываю фрагмент кода вокруг древовидной структуры. Дерево может делать несколько вещей, одна из которых — способность сериализовать и десериализовать свои данные. Есть несколько разных типов узлов, например 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
метод вне класса и в своего рода независимую функцию решает проблему, да. Честно говоря, мне это не нравилось … это как-то кажется неправильным, потому что в моем реальном коде не только один метод, подобный этому, должен быть перемещен за пределы класса.— см
К вашему сведению, я просто добавил контекстный раздел к своему вопросу для получения дополнительных сведений. Смотрите ссылку на мой скетч. Возможно, это поможет пролить дополнительный свет на эту тему.
— см