I am developing a piece of code around a tree structure. The tree can do a few things - one of them being its ability to serialize & de-serialize its data. There are multiple different types of nodes, e.g. NodeA and NodeB, each represented by a class. Different types of nodes may have significantly different functionality and may hold different types of data. As a common base, all node classes inherit from a "base" Node class. Any given type of node can be at the root of the tree. A simplified example of the envisioned structure looks as follows:
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()
Bottom line: It works. The problem: There are "circular" dependencies between the actual node types NodeA/NodeB and the base Node class. If all of this code resides in a single Python file, it works fine. However, if I try to move each class to a separate file, the Python interpreter will become unhappy because of (theoretically required) circular imports. The actual classes are really big, so I would like to structure my code a bit.
Question: A common wisdom says that if circular imports / dependencies become a topic of debate, then the code's design / structure sucks and is at fault in the first place. I'd agree with that but I really do not have many good ideas of how to improve the above.
I am aware that I could eliminate the circular import "limitation" by doing a "manual run-time import" at least for one part of the circle plus some botching, but this is something that I'd like to avoid ...