我试图对json.JSONEncoder进行子类化,以便命名元组(使用新的Python语法定义,但可能仍然适用于collections.namedtuple的输出)序列化到JSON对象,其中元组字段对应于对象键。
例如:
from typing import NamedTuple
class MyModel(NamedTuple):
foo:int
bar:str = "Hello, World!"
a = MyModel(123) # Expected JSON: {"foo": 123, "bar": "Hello, World!"}
b = MyModel(456, "xyzzy") # Expected JSON: {"foo": 456, "bar": "xyzzy"}我的理解是,我将json.JSONEncoder子类化,并覆盖它的default方法来为新类型提供序列化。然后,该类的其他成员将在递归等方面做正确的事情。因此,我得出如下结论:
class MyJSONEncoder(json.JSONEncoder):
def default(self, o):
to_encode = None
if isinstance(o, tuple) and hasattr(o, "_asdict"):
# Dictionary representation of a named tuple
to_encode = o._asdict()
if isinstance(o, datetime):
# String representation of a datetime
to_encode = o.strftime("%Y-%m-%dT%H:%M:%S")
# Why not super().default(to_encode or o)??
return to_encode or o当它试图序列化一个cls参数(即json.dumps的datetime参数) --至少部分地证明了我的假设--但对命名元组的检查从未被击中,并且它默认将其序列化为一个元组(即JSON数组)时,这是可行的。奇怪的是,我原以为应该在我的转换对象上调用超类‘default方法’,但是当它试图序列化一个datetime时,这会引发一个异常:"TypeError:类型为'str‘的对象不是JSON序列化的“,坦率地说,这是没有意义的!
如果我使命名的tuple类型检查更加具体(例如,isinstance(o, MyModel)),我就会得到相同的行为。但是,我确实发现,如果我也重写了encode方法,通过将命名的元组检查移到那里,我几乎可以得到我想要的行为:
class AlmostWorkingJSONEncoder(json.JSONEncoder):
def default(self, o):
to_encode = None
if isinstance(o, datetime):
# String representation of a datetime
to_encode = o.strftime("%Y-%m-%dT%H:%M:%S")
return to_encode or o
def encode(self, o):
to_encode = None
if isinstance(o, tuple) and hasattr(o, "_asdict"):
# Dictionary representation of a named tuple
to_encode = o._asdict()
# Here we *do* need to call the superclass' encode method??
return super().encode(to_encode or o)这是可行的,但不是递归的:根据我的要求,它成功地将顶级命名元组序列化为JSON对象,但是存在于该命名元组中的任何命名元组都将使用默认行为(JSON数组)进行序列化。如果我将命名的tuple类型检查放在default和encode方法中,这也是一种行为。
文档意味着只有default方法应该在子类中被更改。例如,我猜想,在执行分组编码时,在AlmostWorkingJSONEncoder中覆盖AlmostWorkingJSONEncoder将导致其中断。然而,到目前为止,再多的黑客行为都没有产生我想要的结果(或者预期会发生,因为文档很少)。
我的误会在哪里?
编辑读取json.JSONEncoder的代码解释了为什么default方法在传递字符串时会引发类型错误:文档中不清楚(至少对我来说),但是default方法的目的是将某些不受支持的类型的值转换为可序列化的类型,然后返回该类型;如果不支持的类型没有转换成重写方法中的任何内容,那么您应该在最后调用super().default(o)来调用类型错误。所以就像这样:
class SubJSONEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, Foo):
return SerialisableFoo(o)
if isinstance(o, Bar):
return SerialisableBar(o)
# etc., etc.
# No more serialisation options available, so raise a type error
super().default(o)我认为我遇到的问题是,只有当编码器无法匹配任何受支持的类型时,才会调用default方法。命名元组仍然是一个元组--它是受支持的--所以它首先匹配它,然后再委托给我重写的default方法。在Python2.7中,执行这种匹配的函数是JSONEncoder对象的一部分,但是在Python3中,它们似乎被移到模块命名空间之外(因此,用户无法访问它们)。因此,我认为子类JSONEncoder不可能在不对自己的实现进行大量重写和硬耦合的情况下以通用方式序列化命名元组:(
编辑2 --我把它作为错误提交。
发布于 2019-11-03 18:07:18
坏消息
嗯,我只是看了一下来源,似乎没有一个公共挂钩来控制list或tuple实例如何被序列化。
更糟的消息
一种不安全的方法是对_make_iterencode()私有函数进行猴子修补。
好消息
另一种方法是对输入进行预处理,将命名的元组转换为dicts:
from json import JSONEncoder
from typing import NamedTuple
from datetime import datetime
def preprocess(tree):
if isinstance(tree, dict):
return {k: preprocess(v) for k, v in tree.items()}
if isinstance(tree, tuple) and hasattr(tree, '_asdict'):
return preprocess(tree._asdict())
if isinstance(tree, (list, tuple)):
return list(map(preprocess, tree))
return tree
class MD(JSONEncoder):
def default(self, o):
if isinstance(o, datetime):
return o.strftime("%Y-%m-%dT%H:%M:%S")
return super().default(o)适用于这些模型:
class MyModel(NamedTuple):
foo: int
bar: str = "Hello, World!"
class LayeredModel(NamedTuple):
baz: MyModel
fob: list
a = MyModel(123)
b = MyModel(456, "xyzzy")
c = LayeredModel(a, [a, b])
outer = dict(a=a, b=b, c=c, d=datetime.now(), e=10)
print(MD().encode(preprocess(outer)))给出这个输出:
{"a": {"foo": 123, "bar": "Hello, World!"},
"b": {"foo": 456, "bar": "xyzzy"},
"c": {"baz": {"foo": 123, "bar": "Hello, World!"},
"fob": [{"foo": 123, "bar": "Hello, World!"},
{"foo": 456, "bar": "xyzzy"}]},
"d": "2019-11-03T10:46:17",
"e": 10}发布于 2018-12-29 02:39:27
你做错了什么这是这个
if isinstance(o, tuple) and hasattr(o, "_asdict"):您的对象o不是tuple类型的。甚至连NamedTuple都没有。它的类型是MyModel或您对class MySomething(NamedTuple)的任何定义。
所以,为了做你想做的事,你必须把这个if变成
if isinstance(o, MyModel):或者,如果您有从NamedTuple定义的多个模型/类
if isinstance(o, (MyModel1, MyModel2, MyModel3, ...)):另外,不要忘记“超级”default。就像在医生们一样。
完整代码:
class MyJSONEncoder(json.JSONEncoder):
def default(self, o):
to_encode = None
if isinstance(o, MyModel):
# Dictionary representation of a named tuple
to_encode = o._asdict()
if isinstance(o, datetime):
# String representation of a datetime
to_encode = o.strftime("%Y-%m-%dT%H:%M:%S")
return json.JSONEncoder.default(self, o)https://stackoverflow.com/questions/43913256
复制相似问题