Сериализация Yaml через верблюда: использование базового класса load / dump и доступ к типу (self) в декораторе - PullRequest
0 голосов
/ 20 октября 2018

TL; DR : как использовать type(self) в декораторе функции-члена?

Я хотел бы сделать сериализацию производных классов и поделиться некоторой логикой сериализации в базекласс в Python.Поскольку pickle и простой yaml, по-видимому, не в состоянии справиться с этим надежно, я затем наткнулся на camel, что я считаю довольно аккуратным решением проблемы см. Эту ссылку .

Рассмотрим два чрезвычайно упрощенных класса B и A, где B наследуется от A.Я хочу иметь возможность сериализации B в моей основной функции следующим образом:

from camel import Camel, CamelRegistry
serializable_types = CamelRegistry()

# ... define A and B with dump and load functions ...

if __name__ == "__main__":
    serialization_interface = Camel([serializable_types])
    b = B(x=3, y=4)
    s = serialization_interface.dump(b)
    print(s)

Я предложил два решения, которые работают:

Версия 1 :сброс и загрузка выполняются в автономных функциях вне класса.Проблемы: не очень элегантные, функция dumpA не доступна автоматически для наследования класса в dumpB, более громоздкие имена функций, область действия функций больше, чем необходимо

# VERSION 1 - dump and load in external functions
class A:

    def __init__(self, x):
        self._x = x


@serializable_types.dumper(A, 'object_A', version=None)
def dumpA(a):
    return {'x': a._x}


@serializable_types.loader('object_A', version=None)
def loadA(data, version):
    return A(data.x)


class B(A):

    def __init__(self, x, y):
        super().__init__(x)
        self._y = y


@serializable_types.dumper(B, 'object_B', version=None)
def dumpB(b):
    b_data = dumpA(b)
    b_data.update({'y': b._y})
    return b_data


@serializable_types.loader('object_B', version=None)
def loadB(data, version):
    return B(data.x)

Версия 2 : функциидля загрузки и сброса определяются непосредственно в конструкторе.Функция все еще недоступна в подклассе: /

# VERSION 2 - dump and load functions defined in constructor
class A:

    def __init__(self, x):
        self._x = x

        @serializable_types.dumper(A, 'object_A', version=None)
        def dump(a):
            a.to_dict()

        @serializable_types.loader('object_A', version=None)
        def load(data, version):
            return A(data.x)

    def to_dict(self):
        return {'x': self._x}


class B(A):

    def __init__(self, x, y):
        super().__init__(x)
        self._y = y

        @serializable_types.dumper(B, 'object_B', version=None)
        def dump(b):
            b_data = b.to_dict()
            return b_data

        @serializable_types.loader('object_B', version=None)
        def load(data, version):
            return B(data.x)

    def to_dict(self):
        b_data = super().to_dict()
        b_data.update({'y': b._y})
        return b_data

Я хотел бы получить реализацию, которая выглядит следующим образом:

# VERSION 3 - dump and load functions are member functions
# ERROR: name 'A' is not defined
class A:

    def __init__(self, x):
        self._x = x

    @serializable_types.dumper(A, 'object_A', version=None)
    def dump(a):
        return {'x': a._x}

    @serializable_types.loader('object_A', version=None)
    def load(data, version):
        return A(data.x)


class B(A):

    def __init__(self, x, y):
        super().__init__(x)
        self._y = y

    @serializable_types.dumper(B, 'object_B', version=None)
    def dump(b):
        b_data = super().dump(b)
        b_data.update({'y': b._y})
        return b_data

    @serializable_types.loader('object_B', version=None)
    def load(data, version):
        return B(data.x)

Это не будет работать причина в определенииdump функции, A и B не определены.Однако с точки зрения разработки программного обеспечения я считаю, что это самое чистое решение с наименьшим количеством строк кода.
Есть ли способ заставить определения типов A и B работать в декораторе?Или кто-то решил проблему по-другому?Я наткнулся на этот , но не мог найти простой способ применить его к своему случаю использования.

1 Ответ

0 голосов
/ 20 октября 2018

Ваша версия 3 не будет работать, потому что, как вы, вероятно, заметили, во время вызова декоратора, A еще не определено.

Если вы напишите свой декоратор до того, как синтаксический сахар @ был добавлен в Python:

def some_decorator(fun):
    return fun

@some_decorator
def xyz():
    pass

, то есть:

def some_decorator(fun):
    return fun

def xyz():
    pass

some_decorator(xyz)

, тогдаэто должно быть сразу ясно.


Ваша версия 2 откладывает регистрацию ваших подпрограмм загрузчика и дампера до тех пор, пока экземпляр как A, так и B не будет создан каким-либо иным способом, чем загрузка, прежде чем вы сможетеделать загрузку.Это может сработать, если вы создали экземпляры обоих классов, а затем сделали dump, а затем загрузку из одной программы.Но если вы только создаете B и хотите сбросить его, функции для A не зарегистрированы и A.dump() недоступны.И в любом случае, если программа выполняет как дамп, так и загрузку данных, гораздо чаще сначала выполнить загрузку из некоторого постоянного хранилища, а затем выполнить дамп, и во время загрузки регистрация еще не состоялась бы.Поэтому вам понадобится дополнительный механизм регистрации для всех ваших классов и создание хотя бы одного экземпляра для каждого из этих классов.Возможно, это не то, что вам нужно.


В версии 1 вы не можете легко найти dumpA, находясь в dumpB, хотя должна быть возможность заглянуть во внутренние органы serializable_types и найти родителя.класс B, однако это не тривиально, уродливо и есть лучший способ минимизировать dumpBdumpA) в функции, которые возвращают значение, возвращенное некоторым методом B (соответственно A)), с соответствующим именем dump:

from camel import CamelRegistry, Camel

serializable_types = CamelRegistry()

# VERSION 1 - dump and load in external functions
class A:
    def __init__(self, x):
        self._x = x

    def dump(self):
        return {'x': self._x}

@serializable_types.dumper(A, 'object_A', version=None)
def dumpA(a):
    return a.dump()

@serializable_types.loader('object_A', version=None)
def loadA(data, version):
    return A(data.x)


class B(A):
    def __init__(self, x, y):
        super().__init__(x)
        self._y = y

    def dump(self):
        b_data = A.dump(self)
        b_data.update({'y': b._y})
        return b_data

@serializable_types.dumper(B, 'object_B', version=None)
def dumpB(b):
    return b.dump()

@serializable_types.loader('object_B', version=None)
def loadB(data, version):
    return B(data.x)

if __name__ == "__main__":
    serialization_interface = Camel([serializable_types])
    b = B(x=3, y=4)
    s = serialization_interface.dump(b)
    print(s)

, что дает:

!object_B
x: 3
y: 4

Это работает, потому что к моменту вызова dumpB у вас есть экземпляр типа B(иначе вы не могли бы получить его атрибуты), и методы класса B знают о классе A.

Обратите внимание, что выполнение return B(data.x) не будет работать ни в одной из ваших версий, так какB '__init__ ожидает два параметра.

Я считаю вышеупомянутое довольно нечитаемым.


Вы указываете, что "простой yaml, похоже, не смог справиться с этим надежно".Я не знаю, почему это так, но есть много недоразумений относительно YAML¹

. Я рекомендую вам взглянуть на ruamel.yaml (заявление об отказе: я являюсь автором этого пакета).Он требует регистрации классов для выгрузки и загрузки, использует предварительно определенные имена методов для загрузки и выгрузки (from_yaml соответственно to_yaml), и «офис регистрации» вызывает эти методы, включая информацию о классе.Поэтому нет необходимости откладывать определение этих методов до тех пор, пока вы не создадите объект, как в вашей версии 2.

Вы можете либо явно зарегистрировать класс, либо украсить класс, как только будет доступен декоратор (т.е. один разу вас есть YAML экземпляр).Поскольку B наследуется от A, вам нужно только предоставить to_yaml и from_yaml в A и вы можете повторно использовать методы dump из предыдущего примера:

import sys

class A:
    yaml_tag = u'!object_A'

    def __init__(self, x):
        self._x = x

    @classmethod
    def to_yaml(cls, representer, node):
        return representer.represent_mapping(cls.yaml_tag, cls.dump(node))

    @classmethod
    def from_yaml(cls, constructor, node):
        instance = cls.__new__(cls)
        yield instance
        state = ruamel.yaml.constructor.SafeConstructor.construct_mapping(
              constructor, node, deep=True)
        instance.__dict__.update(state)

    def dump(self):
        return {'x': self._x}

import ruamel.yaml  # delayed import so A cannot be decorated
yaml = ruamel.yaml.YAML()

@yaml.register_class
class B(A):
    yaml_tag = u'!object_B'

    def __init__(self, x, y):
        super().__init__(x)
        self._y = y

    def dump(self):
        b_data = A.dump(self)
        b_data.update({'y': b._y})
        return b_data


yaml.register_class(A)
# B not registered, because it is already decorated
b = B(x=3, y=4)
yaml.dump(b, sys.stdout)
print('=' * 20)
b = yaml.load("""\
!object_B
x: 42
y: 196
""")
print('b.x: {.x}, b.y: {.y}'.format(b, b))

который дает:

!object_B
x: 3
y: 4
====================
b.x: 42, b.y: 196

* * * * * yield в приведенном выше коде необходим для работы с экземплярами, которые имеют (косвенные) циклические ссылки на себя и для которых, очевидно, не все аргументы могут быть доступны навремя создания объекта.

¹ Например, одна ссылка YAML 1.2 указывает , что документ YAML начинается с ---, где это на самом деле называется directives-end-маркер , а не документ-старт-маркер по уважительным причинам.И что за ..., маркером окончания документа, могут следовать только директивы или
---, тогда как спецификация явно указывает на то, что за ним могут следовать комментарии, а также голые документы.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...