前言
本文通过实际项目的经历,经过搜索与比较,终于找到了好用的 Python 序列化库:marshmallow。
它拥有类似 django 的语法,支持详细的自定义设置,适合各种各样的序列化/反序列化情景。
问题
我们都知道,Python 自带了一些序列化的库,例如 json、pickle、marshal 等。现在的问题是,要向一个 HTTP 服务器提交一个 POST 请求,附带一个 JSON 作为 payload,假设这个 payload 是这样的:
{ "name": "abc", "price": 1.23456, "date": "2021-06-26" }
那么,它对应的 Python 定义是
from dataclasses import dataclass import datetime @dataclass class Item: name: str price: float date: datetime.date
所以,这个 POST 请求会是这样发送的:
import json import requests item = Item('abc', Decimal('1.23456'), '2021-06-26') requests.post(url, data=json.dumps(item))
这样一跑,马上给报错:TypeError: Object of type Item is not JSON serializable
。嗯?????怎么报错了?才知道 Python 自带的 json 库不支持序列化一个自定义的 class,仅仅支持那几个 Python 内置的类,如 dict, str, float, list, bool
。对此,网上是有一些解决方法,但其实都是妥协之举:如果服务器返回一个 {"name": "abc","price": 1.23456,"date":"2021-06-26"}
,能方便地反序列化成实例么?
遇到这种问题,首先就想起“不要自己造轮子”的原则。Python 作为一门非常成熟的语言,就没有什么 pythonic 的解决方案?然后我去百度、Google,似乎还真没有,marshmallow 已经是我能找到的最好的了。
手上还有 JVM 的项目,可以用 jackson/fastjson
等库,那才是方便啊,类型传进去后,正反序列化一气呵成。
如果自己给这个类写一个 to_json()
方法,手动构造一个 dict 呢?想想还是不行,如果未来要修改这个类的属性,那么还得对应改。什么?你说连这个类都不要了,post 的时候直接现场构造一个 dict?这绝对是在给自己挖坑啊。
所以,marshmallow 赶紧学起来。
Marshmallow
Schema
最重要的,是定义 schema。对于刚才的 Item 类,对应的 schema 会这样写:
@dataclass class Item: name: str price: Decimal date: datetime.date # 对于其他的所有 fields, 可以参考文档 # https://marshmallow.readthedocs.io/en/stable/marshmallow.fields.html#api-fields from marshmallow import Schema, fields class ItemSchema(Schema): name = fields.String() price = fields.Decimal() date = fields.Date()
如果以前接触过 django,应该对这种写法很熟悉,不过不熟悉也没关系。
序列化
然后,就可以用 dump()
序列化了。
item = Item('abc', Decimal('1.23456'), datetime.date(2021, 6, 26)) schema = ItemSchema() result_obj = schema.dump(item) print(result_obj) # 会输出 {'price': Decimal('1.23456'), 'name': 'abc', 'date': '2021-06-26'}
可以看到,在定义好的 schema 的帮助下,item 实例变成了一个 dict。
如果想输出一个字符串呢?可以把 schema.dump
换成 schema.dumps
。(不过由于 price
是一个 Decimal
实例,而自带的 json 不支持 Decimal 会报错,可以考虑另外装一个 simplejson
库来解决。这个问题在后文会再讲到)
反序列化
使用 load() 进行反序列化:
input_data = { 'name': 'abc', 'price': Decimal('1.23456'), 'date': '2021-06-26'} load_result = schema.load(input_data) print(load_result) # 输出 {'name': 'abc', 'date': datetime.date(2021, 6, 26), 'price': Decimal('1.23456')}
注意到 date
属性变成了一个 Python 的 datetime.date
实例,这是我们希望的。
这里只是反序列化成了一个 dict,那么怎样才能返回一个真正的 Item
实例?可以给 schema 添加一个返回 Item
的方法,并打上 post_load
的注解(装饰器)。
from marshmallow import Schema, fields, post_load class ItemSchema(Schema): name = fields.String() price = fields.Decimal() date = fields.Date() @post_load def make_item(self, data, **kwargs): return Item(**data) result_obj = schema.dump(item) print(result_obj) # 返回 Item(name='abc', price=Decimal('1.23456'), date=datetime.date(2021, 6, 26))
可以,有点样子了。
数据校验
既然有了 schema,当然可以顺便在 load 时做校验了。例如输入的字段有没有多,有没有缺,属性的类型对不对,值的范围有没有超,哪些属性可以填 None(null)等等。这里不再赘述,见文档。
自定义字段
Marshmallow 库自带的一些类型可能不够满足需求,例如想加一个 省份、身份证 什么的,用字符串的话好像感觉欠缺了一点校验。而刚才提到的 Decimal 比较难处理,我觉得也可以用自定义字段来解决。
例如,可以用 Method 字段来自定义正反序列化时所使用的方法。
class ItemSchema(Schema): name = fields.String() price = fields.Method('price_decimal_2_float', deserialize='float_2_decimal') date = fields.Date() @post_load def make_item(self, data, **kwargs): return Item(**data) def price_decimal_2_float(self, item: Item): return float(item.price) def float_2_decimal(self, float): return decimal.Decimal(str(float))
这样就可以正常用 dumps(), loads()
了:
result_str = schema.dumps(item) print(result_str) # {"date": "2021-06-26", "name": "abc", "price": 1.23456} input_str = '{"date": "2021-06-26", "name": "abc", "price": 1.23456}' load_result = schema.loads(input_str) print(load_result) # Item(name='abc', price=Decimal('1.23456'), date=datetime.date(2021, 6, 26))
太棒了,输入的 JSON 的 date 和 price,在反序列化后,自动变成了 datetime.date
和 Decimal
。
小结
Marshmallow 的核心是 schema,数据类型、校验等都记录在 schema 中,从而支持复杂对象的序列化和反序列化。如果说它有什么缺点,那必须是它仍然需要专门定义一个 schema 才能使用。如果是 JVM 系列的 jackson 等库,可以直接使用 data class 作为 model/schema,额外的配置通过注解等方式引入,这样会少一个对应的 schema 类。marshmallow 的做法使类的数量双倍了。总体来说,它仍然是不造轮子的情况下的好选择。
至于更深入的用法,还是需要参考官方文档,见文末。
不可以直接用反射获取python类中的成员变量吗?比如这种:https://stackoverflow.com/questions/1398022/looping-over-all-member-variables-of-a-class-in-python
变量取名的时候取m_name、m_price和m_date,然后写个to_json(),按照上面的方法然后filter。
说起来枫哥还是彻底转码了吗。。。
序列化不是问题,但是反序列化会比较麻烦..?
是啊😂互联网金融码农