Ваша версия 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
, однако это не тривиально, уродливо и есть лучший способ минимизировать dumpB
(и dumpA
) в функции, которые возвращают значение, возвращенное некоторым методом 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-маркер , а не документ-старт-маркер по уважительным причинам.И что за ...
, маркером окончания документа, могут следовать только директивы или
---
, тогда как спецификация явно указывает на то, что за ним могут следовать комментарии, а также голые документы.