在 Python 中使用 YAML 或 JSON 序列化 RangeDict
Serializing a RangeDict using YAML or JSON in Python
我正在使用 RangeDict 制作包含范围的字典。当我使用 Pickle 时,它很容易写入文件并稍后读取。
import pickle
from rangedict import RangeDict
rngdct = RangeDict()
rngdct[(1, 9)] = \
{"Type": "A", "Series": "1"}
rngdct[(10, 19)] = \
{"Type": "B", "Series": "1"}
with open('rangedict.pickle', 'wb') as f:
pickle.dump(rngdct, f)
但是,我想使用 YAML(如果 YAML 不起作用,则 JSON...)而不是 Pickle,因为大多数人似乎都讨厌它(而且我想要人类可读的文件,这样他们对阅读它们的人有意义)
基本上,更改代码以调用 yaml 并在 'w'
模式下打开文件,而不是在 'wb'
下对写作方面有帮助,但是当我在另一个脚本中读取文件时,我收到这些错误:
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/yaml/constructor.py", line 129, in construct_mapping
value = self.construct_object(value_node, deep=deep)
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/yaml/constructor.py", line 61, in construct_object
"found unconstructable recursive node", node.start_mark)
yaml.constructor.ConstructorError: found unconstructable recursive node
我迷路了。如何序列化 rangedict 对象并以其原始形式读回?
TL;DR;跳到此答案的底部以获取工作代码
我敢肯定有些人讨厌 pickle
,它在重构代码时肯定会让人头疼(当 pickle 对象的 classes 移动到不同的文件时)。但更大的问题是 pickle 是不安全的,只是一个 YAML 是你使用它的方式。
有趣的是,您不能 pickle 到更具可读性的 protocol level 0(Python 3 中的默认协议版本 3)如:
pickle.dump(rngdct, f, 协议=0)
将抛出:
TypeError: a class that defines slots without defining getstate cannot be pickled
这是因为 RangeDict
module/class 有点简约,如果您尝试这样做,它也会显示(或更确切地说不显示):
print(rngdict)
这将只打印 {}
您可能使用了 PyYAML dump()
例程(及其相应的不安全 load()
)。尽管这可以转储通用 Python classes,但您必须意识到它是在 Python 3.0 之前或大致同时实现的。 (并且 Python 3 支持稍后实现)。尽管 YAML 解析器没有理由可以转储和加载 pickle
所做的确切信息,但它不会挂钩到 pickle
支持例程(尽管它可以),当然也不会挂钩到信息中Python 3 个特定的酸洗协议。
无论如何,如果没有 RangeDict
对象的特定表示器(和构造函数),使用 YAML 没有任何意义:它使加载可能不安全,并且您的 YAML 包含 所有使对象高效的血腥细节。如果你这样做 yaml.dump()
:
!!python/object:rangedict.RangeDict
_root: &id001 !!python/object/new:rangedict.Node
state: !!python/tuple
- null
- color: 0
left: null
parent: null
r: !!python/tuple [1, 9]
right: !!python/object/new:rangedict.Node
state: !!python/tuple
- null
- color: 1
left: null
parent: *id001
r: !!python/tuple [10, 19]
right: null
value: {Series: '1', Type: B}
value: {Series: '1', Type: A}
IMO 在 YAML 中的 可读 表示是:
!rangedict
[1, 9]:
Type: A
Series: '1'
[10, 19]:
Type: B
Series: '1'
由于序列用作键,如果不对解析器进行重大修改,PyYAML 无法加载它。但幸运的是,那些修改已经合并到 ruamel.yaml
中(免责声明:我是那个包的作者),所以 "all" 你需要做的是 subclass RangeDict
提供合适的代表和构造函数(class)方法:
import io
import ruamel.yaml
from rangedict import RangeDict
class MyRangeDict(RangeDict):
yaml_tag = u'!rangedict'
def _walk(self, cur):
# walk tree left -> parent -> right
if cur.left:
for x in self._walk(cur.left):
yield x
yield cur.r
if cur.right:
for x in self._walk(cur.right):
yield x
@classmethod
def to_yaml(cls, representer, node):
d = ruamel.yaml.comments.CommentedMap()
for x in node._walk(node._root):
d[ruamel.yaml.comments.CommentedKeySeq(x)] = node[x[0]]
return representer.represent_mapping(cls.yaml_tag, d)
@classmethod
def from_yaml(cls, constructor, node):
d = cls()
for x, y in node.value:
x = constructor.construct_object(x, deep=True)
y = constructor.construct_object(y, deep=True)
d[x] = y
return d
rngdct = MyRangeDict()
rngdct[(1, 9)] = \
{"Type": "A", "Series": "1"}
rngdct[(10, 19)] = \
{"Type": "B", "Series": "1"}
yaml = ruamel.yaml.YAML()
yaml.register_class(MyRangeDict) # tell the yaml instance about this class
buf = io.StringIO()
yaml.dump(rngdct, buf)
data = yaml.load(buf.getvalue())
# test for round-trip equivalence:
for x in data._walk(data._root):
for y in range(x[0], x[1]+1):
assert data[y]['Type'] == rngdct[y]['Type']
assert data[y]['Series'] == rngdct[y]['Series']
buf.getvalue()
正是之前显示的可读表示。
如果你必须处理转储 RangeDict
本身(即不能 subclass 因为你使用了一些具有 RangeDict
硬编码的库),那么你可以添加属性和方法MyRangeDict
的 grafting/monkeypatching.
直接到 RangeDict
我正在使用 RangeDict 制作包含范围的字典。当我使用 Pickle 时,它很容易写入文件并稍后读取。
import pickle
from rangedict import RangeDict
rngdct = RangeDict()
rngdct[(1, 9)] = \
{"Type": "A", "Series": "1"}
rngdct[(10, 19)] = \
{"Type": "B", "Series": "1"}
with open('rangedict.pickle', 'wb') as f:
pickle.dump(rngdct, f)
但是,我想使用 YAML(如果 YAML 不起作用,则 JSON...)而不是 Pickle,因为大多数人似乎都讨厌它(而且我想要人类可读的文件,这样他们对阅读它们的人有意义)
基本上,更改代码以调用 yaml 并在 'w'
模式下打开文件,而不是在 'wb'
下对写作方面有帮助,但是当我在另一个脚本中读取文件时,我收到这些错误:
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/yaml/constructor.py", line 129, in construct_mapping
value = self.construct_object(value_node, deep=deep)
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/yaml/constructor.py", line 61, in construct_object
"found unconstructable recursive node", node.start_mark)
yaml.constructor.ConstructorError: found unconstructable recursive node
我迷路了。如何序列化 rangedict 对象并以其原始形式读回?
TL;DR;跳到此答案的底部以获取工作代码
我敢肯定有些人讨厌 pickle
,它在重构代码时肯定会让人头疼(当 pickle 对象的 classes 移动到不同的文件时)。但更大的问题是 pickle 是不安全的,只是一个 YAML 是你使用它的方式。
有趣的是,您不能 pickle 到更具可读性的 protocol level 0(Python 3 中的默认协议版本 3)如:
pickle.dump(rngdct, f, 协议=0) 将抛出:
TypeError: a class that defines slots without defining getstate cannot be pickled
这是因为 RangeDict
module/class 有点简约,如果您尝试这样做,它也会显示(或更确切地说不显示):
print(rngdict)
这将只打印 {}
您可能使用了 PyYAML dump()
例程(及其相应的不安全 load()
)。尽管这可以转储通用 Python classes,但您必须意识到它是在 Python 3.0 之前或大致同时实现的。 (并且 Python 3 支持稍后实现)。尽管 YAML 解析器没有理由可以转储和加载 pickle
所做的确切信息,但它不会挂钩到 pickle
支持例程(尽管它可以),当然也不会挂钩到信息中Python 3 个特定的酸洗协议。
无论如何,如果没有 RangeDict
对象的特定表示器(和构造函数),使用 YAML 没有任何意义:它使加载可能不安全,并且您的 YAML 包含 所有使对象高效的血腥细节。如果你这样做 yaml.dump()
:
!!python/object:rangedict.RangeDict
_root: &id001 !!python/object/new:rangedict.Node
state: !!python/tuple
- null
- color: 0
left: null
parent: null
r: !!python/tuple [1, 9]
right: !!python/object/new:rangedict.Node
state: !!python/tuple
- null
- color: 1
left: null
parent: *id001
r: !!python/tuple [10, 19]
right: null
value: {Series: '1', Type: B}
value: {Series: '1', Type: A}
IMO 在 YAML 中的 可读 表示是:
!rangedict
[1, 9]:
Type: A
Series: '1'
[10, 19]:
Type: B
Series: '1'
由于序列用作键,如果不对解析器进行重大修改,PyYAML 无法加载它。但幸运的是,那些修改已经合并到 ruamel.yaml
中(免责声明:我是那个包的作者),所以 "all" 你需要做的是 subclass RangeDict
提供合适的代表和构造函数(class)方法:
import io
import ruamel.yaml
from rangedict import RangeDict
class MyRangeDict(RangeDict):
yaml_tag = u'!rangedict'
def _walk(self, cur):
# walk tree left -> parent -> right
if cur.left:
for x in self._walk(cur.left):
yield x
yield cur.r
if cur.right:
for x in self._walk(cur.right):
yield x
@classmethod
def to_yaml(cls, representer, node):
d = ruamel.yaml.comments.CommentedMap()
for x in node._walk(node._root):
d[ruamel.yaml.comments.CommentedKeySeq(x)] = node[x[0]]
return representer.represent_mapping(cls.yaml_tag, d)
@classmethod
def from_yaml(cls, constructor, node):
d = cls()
for x, y in node.value:
x = constructor.construct_object(x, deep=True)
y = constructor.construct_object(y, deep=True)
d[x] = y
return d
rngdct = MyRangeDict()
rngdct[(1, 9)] = \
{"Type": "A", "Series": "1"}
rngdct[(10, 19)] = \
{"Type": "B", "Series": "1"}
yaml = ruamel.yaml.YAML()
yaml.register_class(MyRangeDict) # tell the yaml instance about this class
buf = io.StringIO()
yaml.dump(rngdct, buf)
data = yaml.load(buf.getvalue())
# test for round-trip equivalence:
for x in data._walk(data._root):
for y in range(x[0], x[1]+1):
assert data[y]['Type'] == rngdct[y]['Type']
assert data[y]['Series'] == rngdct[y]['Series']
buf.getvalue()
正是之前显示的可读表示。
如果你必须处理转储 RangeDict
本身(即不能 subclass 因为你使用了一些具有 RangeDict
硬编码的库),那么你可以添加属性和方法MyRangeDict
的 grafting/monkeypatching.
RangeDict