通过 camel 进行 Yaml 序列化:使用 base class load/dump 并在装饰器中访问 type(self)

Yaml serialization through camel: using base class load/dump and accessing type(self) in decorator

TL;DR: 如何在成员函数的装饰器中使用type(self)

我想对派生的 classes 进行序列化,并在 Python 中共享基础 class 中的一些序列化逻辑。 由于 pickle 和简单的 yaml 似乎无法可靠地处理这个问题,然后我偶然发现了 camel,我认为这是解决问题 see this link 的一个非常巧妙的解决方案。

考虑两个极其简化的 classes BA,其中 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:转储和加载是在 class 之外的独立函数中完成的。问题:不是很优雅,函数dumpA不能自动用于继承dumpB中的class,函数命名比较麻烦,函数范围比必要的大

# 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: 加载和转储函数直接在构造函数中定义。函数在 subclass :/

中仍然不可用
# 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 函数的定义中,AB 未定义。然而,从软件设计的角度来看,我认为这是代码行最少的最干净的解决方案。
有没有办法让 AB 的类型定义在装饰器中工作?或者有没有人以不同的方式解决了这个问题? 我遇到了 但看不到将它应用到我的用例的直接方法。

您的版本 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,推迟注册你的装载机和倾卸机 例程,直到 AB 的实例在某些情况下被创建 除了在加载之前加载之外。那可能有用 如果您创建了两个 classes 的实例,然后转储,然后加载, 从一个程序中。但是如果你只创建 B 并想转储 它,那么 A 的功能还没有注册并且 A.dump() 是 无法使用。无论如何,如果一个程序同时转储和加载数据, 从一些持久存储中加载更为常见 首先,然后进行倾销,并在加载注册时 还没有发生。所以你需要一些额外的 所有 classes 的注册机制和至少创建 每个 classes 一个实例。可能不是你想要的。


在版本 1 中,您无法轻松找到 dumpA,而在 dumpB 中, 尽管应该可以查看的内部结构 serializable_types 并找到 B 的父级 class,然而这是 非平凡的,丑陋的,并且有一个更好的方法通过最小化 dumpB (和 dumpA) 转化为 return 值 returned 某些方法 B 的函数 (resp. 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 的实例 (否则你无法获得它的属性),以及 class B 了解 class A.

请注意,return B(data.x) 不适用于您的任何版本 因为 B__init__ 需要两个参数。

我觉得上面的内容相当难读。


您表示“简单yaml似乎无法处理 这可靠”。我不知道为什么这是真的,但有 很多关于 YAML 的误解¹

我建议你看看 ruamel.yaml(免责声明:我是那个包的作者)。 它需要为转储和加载注册 classes,使用预定义的方法名称 用于加载和转储(from_yaml resp. to_yaml),以及 "registration office" 调用 这些方法包括 class 信息。所以没有必要推迟定义 这些方法,直到您像在版本 2 中那样构造一个对象。

您可以显式注册 class 或将 class 修饰为 一旦装饰器可用(即一旦你有了 YAML 实例)。由于 B 继承自 A,您只需提供 to_yamlfrom_yaml in 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 参考 states YAML 文档以 --- 开头,实际调用的地方 一种 directives-end-marker 而不是 document-start-marker 有充分的理由。而那个..., 文档结束标记,后面只能跟指令或
---, 而规范清楚地表明它后面可以有注释 以及裸文件。