Структура кода для представления в виде дерева с несколькими типами (классами) узлов и циклическими зависимостями

Я разрабатываю фрагмент кода вокруг древовидной структуры. Дерево может делать несколько вещей, одна из которых – способность сериализовать и десериализовать свои данные. Есть несколько разных типов узлов, например 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 ответа
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))

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

    – см

  • К вашему сведению, я просто добавил контекстный раздел к своему вопросу для получения дополнительных сведений. Смотрите ссылку на мой скетч. Возможно, это поможет пролить дополнительный свет на эту тему.

    – см

Экземпляры классов 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

  • Благодарю. К вашему сведению, я просто добавил контекстный раздел к своему вопросу для получения дополнительных сведений. Смотрите ссылку на мой скетч. Возможно, это поможет пролить дополнительный свет на эту тему.

    – см

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *