通过PyYAML序列化命名元组

4 投票
2 回答
4595 浏览
提问于 2025-04-18 13:05

我在寻找一种合理的方法,用PyYAML将命名元组序列化为YAML格式。

有几点我不想做:

  • 不想在创建命名元组时动态调用构造函数、表示器或解析器。这些YAML文件可能会被存储并在以后重新加载,所以我不能依赖于在恢复时存在相同的运行环境。

  • 不想在全局范围内注册命名元组。

  • 不想依赖命名元组有唯一的名称。

我在想类似这样的方案:

class namedtuple(object):
    def __new__(cls, *args, **kwargs):
        x = collections.namedtuple(*args, **kwargs)

        class New(x):
            def __getstate__(self):
                return {
                    "name": self.__class__.__name__,
                    "_fields": self._fields,
                    "values": self._asdict().values()
                }
        return New

def namedtuple_constructor(loader, node):
    import IPython; IPython.embed()
    value = loader.construct_scalar(node)

import re
pattern = re.compile(r'!!python/object/new:myapp.util\.')
yaml.add_implicit_resolver(u'!!myapp.util.namedtuple', pattern)
yaml.add_constructor(u'!!myapp.util.namedtuple', namedtuple_constructor)

假设这个代码在路径myapp/util.py的应用模块中。

不过,当我尝试加载时,我没有进入构造函数:

from myapp.util import namedtuple

x = namedtuple('test', ['a', 'b'])
t = x(1,2)
dump = yaml.dump(t)
load = yaml.load(dump)

它会找不到myapp.util中的New。

我尝试了各种其他方法,这只是我认为可能效果最好的一个。

免责声明:即使我进入了正确的构造函数,我也知道我的规范需要进一步完善,关于哪些参数被保存以及它们如何传递到生成的对象中,但对我来说,第一步是将YAML表示传入我的构造函数,之后的事情应该就简单了。

2 个回答

1

我希望能有一个方法,不用重新创建 namedtuple 函数,但这个方法达到了我的目标。

给你一个方案。

简而言之

这是一个使用 PyAML 3.12 的概念验证。

import yaml

def named_tuple(self, data):
    if hasattr(data, '_asdict'):
        return self.represent_dict(data._asdict())
    return self.represent_list(data)

yaml.SafeDumper.yaml_multi_representers[tuple] = named_tuple

注意:为了保持代码的整洁,你应该使用你可以用到的 add_multi_representer() 方法,以及一个自定义的表示器/加载器,就像你之前做的那样。

这样你就能得到:

>>> import collections
>>> Foo = collections.namedtuple('Foo', 'x y z')
>>> yaml.safe_dump({'foo': Foo(1,2,3), 'bar':(4,5,6)})
'bar: [4, 5, 6]\nfoo: {x: 1, y: 2, z: 3}\n'
>>> print yaml.safe_dump({'foo': Foo(1,2,3), 'bar':(4,5,6)})                                                                                                   
bar: [4, 5, 6]
foo: {x: 1, y: 2, z: 3}

这个是怎么工作的

正如你自己发现的,namedtuple 并没有一个特殊的类;探索它会得到:

>>> collections.namedtuple('Bar', '').mro()
[<class '__main__.Bar'>, <type 'tuple'>, <type 'object'>]

所以,Python 的命名元组实例其实是 tuple 的实例,只是多了一个 _asdict() 方法。

1

我解决了我的问题,不过方式有点不太理想。

现在我的应用程序使用了自己的 namedtuple 实现;我复制了 collections.namedtuple 的源代码,创建了一个基类,让所有新的 namedtuple 类型都可以继承,并且修改了模板(下面的摘录只是为了简洁,主要突出与原来的 namedtuple 源代码的不同之处)。

class namedtupleBase(tuple): 
    pass

_class_template = '''\
class {typename}(namedtupleBase):
    '{typename}({arg_list})'

namedtuple 函数做了一个小改动,以便将新类添加到命名空间中:

namespace = dict(_itemgetter=_itemgetter, __name__='namedtuple_%s' % typename,
                 OrderedDict=OrderedDict, _property=property, _tuple=tuple,
                 namedtupleBase=namedtupleBase)

现在注册一个 multi_representer 就能解决问题了:

def repr_namedtuples(dumper, data):
    return dumper.represent_mapping(u"!namedtupleBase", {
        "__name__": data.__class__.__name__,
        "__dict__": collections.OrderedDict(
            [(k, v) for k, v in data._asdict().items()])
    })

def consruct_namedtuples(loader, node):
    value = loader.construct_mapping(node)
    cls_ = namedtuple(value['__name__'], value['__dict__'].keys())
    return cls_(*value['__dict__'].values())

yaml.add_multi_representer(namedtupleBase, repr_namedtuples)
yaml.add_constructor("!namedtupleBase", consruct_namedtuples)

感谢 在pyyaml中用相同基类表示不同类的实例 这个问题给了我灵感。

我希望能有一个不需要重新创建 namedtuple 函数的想法,但这个方法达到了我的目标。

撰写回答