Skip to content

Latest commit

 

History

History
106 lines (66 loc) · 6.07 KB

更好的Python对象序列化方法.md

File metadata and controls

106 lines (66 loc) · 6.07 KB

原文:Better Python Object Serialization


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 的一个更加优雅的重新实现(从未发生过)。