理解JSONEncoder

时间:2017-05-11 10:48:41

标签: python json subclass python-3.6 namedtuple

我正在尝试子类化json.JSONEncoder,这样命名的元组(使用新的Python 3.6+语法定义,但它可能仍适用于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.dumpsdatetime值 - 至少部分证明我的假设 - 但检查命名元组永远不会被命中,它默认将其序列化为元组(即,到JSON数组)。奇怪的是,我假设我应该在我的变换对象上调用超类'default方法,但是当它尝试序列化datetime时会引发异常:“TypeError:'str'类型的对象不是JSON可序列化的“,坦率地说没有意义!

如果我使命名的元组类型检查更具体(例如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数组)进行序列化。如果我在default encode方法中都使用命名的元组类型检查,这也是一种行为。

文档暗示只应在子类中更改default方法。例如,我认为在encode中覆盖AlmostWorkingJSONEncoder会导致它在进行分块编码时中断。然而,到目前为止,没有多少hackery产生了我想要的东西(或者期望发生,因为文档很少)。

我的误解在哪里?

编辑阅读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方法之前首先匹配。在Python 2.7中,执行此匹配的函数是JSONEncoder对象的一部分,但在Python 3中,它们似乎已移到模块命名空间外(因此,用户空间无法访问)。因此,我认为不可能将JSONEncoder子类化为以通用方式序列化命名元组,而无需对您自己的实现进行大量重写和硬耦合:(

编辑2 我将其作为bug提交。

2 个答案:

答案 0 :(得分:1)

坏消息

嗯,我只是看过the source,似乎没有一个公共的钩子来控制list或tuple实例如何序列化。

更糟的消息

一种不安全的方法是猴子修补 _make_iterencode()私有函数。

好消息

另一种方法是预处理输入,将命名的元组转换为字典:

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}

答案 1 :(得分:-3)

这是你在做什么错

 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。就像在the docs中一样。

完整代码:

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)