更好的 Python 对象序列化方法

发布于 2025-01-06 23:18:13 字数 5315 浏览 9 评论 0

Python 标准库充满了蒙尘的宝石。其中一个允许基于参数类型的简单优雅的函数调度。这使得它对任意对象的序列化是完美的 —— 例如,web API 和结构化日志的 JSON 化。

谁没有看过它:

TypeError: datetime.datetime(...) is not JSON serializable

虽然这应该不是一个大问题,但是它是。 json 模块 —— 从 simplejson 继承了其 API —— 提供了两种序列化对象的方法:

  1. 实现一个 default() 函数,该函数接收一个对象,然后返回 JSONEncoder 能够理解的东东。
  2. 自己实现或子类化 JSONEncoder ,然后将其当做 cls 传递给 dump 方法。你可以自己实现它,或者重载 JSONEncoder.default() 方法

而由于替代实现想要混进去,所以它们不同程度地模仿了 json 模块的 API。[1]

可扩展性

这两种方法的共同点是,它们是不可扩展的:未提供新类型的添加支持。你单一的 default() 备用必须知道所有你想要序列化的类型。这意味着你要么写像这样的函数:

def to_serializable(val):
    if isinstance(val, datetime):
        return val.isoformat() + "Z"
    elif isinstance(val, enum.Enum):
        return val.value
    elif attr.has(val.__class__):
        return attr.asdict(val)
    elif isinstance(val, Exception):
        return {
            "error": val.__class__.__name__,
            "args": val.args,
        }
    return str(val)

这很痛苦,因为你必须在同一个地方为所有对象添加序列化。[2]

或者,你可以尝试自己拿出解决方案,就如 Pyramid 的 JSON 渲染器在 JSON.add_adapter 中做的那样,它使用了待在冷宫中的 zope.interface 的适配器注册表。[3]

另一方面,Django 使用了一个 DjangoJSONEncoder 来解决,这是 json.JSONEncoder 的一个子类,并且它知道如何解码日期、时间、UUID,并保证(可以)。但除此之外,你又要靠自己了。如果你想更进一步使用 Django 和 web API,那么,反正你可能已经使用 Django REST 框架了。它们提出了一个完整的 序列化系统 ,这个系统可不仅仅做了让数据准备好 json.dumps()

最后,为了完整起见,我觉得我必须提一提自己在 structlog 的解决方法,这一方法从一开始我就深深地讨厌:添加一个 __structlog__ 方法到你的类中,它按照 __str__ 返回一个序列化表示。请不要重蹈我的覆辙;标签 软件小丑


鉴于 JSON 相当普遍,令人惊讶的是,目前,我们只有孤立的解决方案。我个人希望的是,有一种方法,可以在一个地方统一注册序列器,但是以一种分散的方式,而无需对我的(或者更糟糕:第三方)类进行任何改变。

进入 PEP 443

原来,对这个问题,Python 3.4 想出了一个很好的解决方法,参见 PEP 443 : functools.singledispatch (对于 Python 遗留版本,也可见 PyPI )。

简单地说,定义一个默认的函数,然后基于第一个参数类型,注册该函数的额外版本:

from datetime import datetime
from functools import singledispatch

@singledispatch
def to_serializable(val):
    """Used by default."""
    return str(val)

@to_serializable.register(datetime)
def ts_datetime(val):
    """Used if *val* is an instance of datetime."""
    return val.isoformat() + "Z"

现在,你也可以在 datetime 实例上调用 to_serializable() ,而单一的调度将选择正确的函数:

>>> json.dumps({"msg": "hi", "ts": datetime.now()},
...            default=to_serializable)
'{"ts": "2016-08-20T13:08:59.153864Z", "msg": "hi"}'

这给了你将你的序列器改造成你所想要的权力:和类一起,在一个单独的模块,或者和 JSON 相关的代码放在一起?任君选择!但是你的保持干净,你的项目之间没有庞大的 if-elif-else 分支。

更进一步

显然, @singledispatch 的适用范围不仅是 JSON。一般的绑定不同行为到不同类型上 ,以及特别的对象序列化是普遍有用的[4]。我的一些校对人员提到,他们使用在可调用对象上使用类的 dict ,尝试了贫民窟近似和其他类似的暴行。(Ele 注,原文是“Some of my proofreaders mentioned they tried a ghetto approximation using dicts of classes to callables and other similar atrocities.”。有更好的翻译,欢迎贡献~~)

换句话说, @singledispatch 只可能是那个你一直想要的函数,虽然它一直都在。

P.S. 当然,在 PyPI 上,还有一个 *multiple*dispatch

脚注


  1. 然而,有个流行的替代实现: UltraJSON 完全不支持自定义对象序列化,而 python-rapidjson 只支持 default() 函数。
  2. 虽然你可以看到,使用 attrs 可管理;也许 你应该使用 attrs !
  3. 不幸的是,在从 zope.component 移植过来后,当前 API Pyramid 的使用是 无正式文档的
  4. 有人告诉我,添加单一调度到标准库的原始动力是 pprint 的一个更加优雅的重新实现(从未发生过)。

原文: Better Python Object Serialization

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

文章
评论
27 人气
更多

推荐作者

笑脸一如从前

文章 0 评论 0

mnbvcxz

文章 0 评论 0

真是无聊啊

文章 0 评论 0

旧城空念

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文