了解 JSONEncoder 的子类化

Posted

技术标签:

【中文标题】了解 JSONEncoder 的子类化【英文标题】:Understanding subclassing of JSONEncoder 【发布时间】:2017-10-10 07:53:33 【问题描述】:

我正在尝试子类化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

当它尝试序列化(即,作为json.dumpscls 参数)datetime 值时,这是有效的——至少部分证明了我的假设——但对命名元组的检查永远不会被命中,并且它默认将其序列化为一个元组(即,一个 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 方法应该在子类中更改。例如,我假设在 AlmostWorkingJSONEncoder 中覆盖 encode 会导致它在进行分块编码时中断。然而,到目前为止,没有多少黑客能够产生我想要的(或期望发生的,因为文档很少)。

我的误会在哪里?


编辑阅读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 子类化以以通用方式序列化命名元组:(

EDIT 2 我以bug 提交了此内容。

【问题讨论】:

请注意 this answer 表现出预期的行为,但它依赖于 Python 3 中不再存在的 Python 2.7 API 调用。覆盖新的 iterencode 方法似乎并不比覆盖encode 您可能希望查看 thisansweer 作为解决问题的方法。 【参考方案1】:

你做错了什么是这个

 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)

【讨论】:

实际上isinstance() 确实为超类返回 True,并且 tuple 是 MyModel 类的超类(我检查过;奇怪的是它不是 NamedTuple 的超类;我没有甚至不想知道是什么奇怪的魔法造成的)。 所以isinstance(o, tuple) 完全没问题。 isinstance(o, tuple) 很好,如果 o 是一个实际的元组,而不是 NamedTuplenamedtuples。这些只是生成给定类结构的“工厂”。 在解释器中检查。这是一个元组。 奇怪,我知道我在写这篇文章的时候查了一下,是False。再次检查,它是True【参考方案2】:

坏消息

嗯,我刚刚查看了the source,似乎没有公共挂钩来控制列表或元组的实例如何被序列化。

坏消息

一种不安全的方法是对 _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

【讨论】:

@heemayl 我刚刚更新了答案并包含了 super()

以上是关于了解 JSONEncoder 的子类化的主要内容,如果未能解决你的问题,请参考以下文章

我可以取消 JSONEncoder Swift 吗?

为啥我的自定义 JSONEncoder.default() 忽略布尔值?

在 Swift 中将 JSONEncoder 用于 Equatable 方法

JSONDecoder和JSONEncoder类是线程安全的吗?

使用 Swift 的 Encodable 将可选属性编码为 null 而无需自定义编码

子类化 UITextField