这个 Python 库有必要好好学学

在不少状况下,咱们会有把 Python 对象进行序列化或反序列化的需求,好比开发 REST API,好比一些面向对象化的数据加载和保存,都会应用到这个功能。编程

这里看一个最基本的例子,这里给到一个 User 的 Class 定义,再给到一个 data 数据,像这样:json

class User(object):
   def __init__(self, name, age):
       self.name = name
       self.age = age

data = [{
   'name': 'Germey',
   'age': 23
}, {
   'name': 'Mike',
   'age': 20
}]

如今我要把这个 data 快速转成 User 组成的数组,变成这样:数组

[User(name='Germey', age=23), User(name='Mike', age=20)]

你会怎么来实现?less

或者我有了上面的列表内容,想要转成一个 JSON 字符串,变成这样:ide

[{"name": "Germey", "age": 23}, {"name": "Mike", "age": 20}]

你又会怎么操做呢?post

另外若是 JSON 数据里面有各类各样的脏数据,你须要在初始化时验证这些字段是否合法,另外 User 这个对象里面 name、age 的数据类型不一样,如何针对不一样的数据类型进行针对性的类型转换,这个你有更好的实现方案吗?ui

初步思路

以前我写过一篇文章这多是 Python 面向对象编程的最佳实践介绍过 attrs 和 cattrs 这两个库,它们两者的组合能够很是方便地实现对象的序列化和反序列化。spa

譬如这样:3d

from attr import attrs, attrib
from cattr import structure, unstructure

@attrs
class User(object):
   name = attrib()
   age = attrib()

data = {
   'name': 'Germey',
   'age': 23
}
user = structure(data, User)
print('user', user)
json = unstructure(user)
print('json', json)

运行结果:code

user User(name='Germey', age=23)
json {'name': 'Germey', 'age': 23}

好,这里咱们经过 attrs 和 cattrs 这两个库来实现了单个对象的转换。

首先咱们要确定一下 attrs 这个库,它能够极大地简化 Python 类的定义,同时每一个字段能够定义多种数据类型。

但 cattrs 这个库就相对弱一些了,若是把 data 换成数组,用 cattrs 仍是不怎么好转换的,另外它的 structure 和 unstructure 在某些情景下容错能力较差,因此对于上面的需求,用这两个库搭配起来并非一个最优的解决方案。

另外数据的校验也是一个问题,attrs 虽然提供了 validator 的参数,但对于多种类型的数据处理的支持并无那么强大。

因此,咱们想要寻求一个更优的解决方案。

更优雅的方案

这里推荐一个库,叫作 marshmallow,它是专门用来支持 Python 对象和原生数据相互转换的库,如实现 object -> dict,objects -> list, string -> dict, string -> list 等的转换功能,另外它还提供了很是丰富的数据类型转换和校验 API,帮助咱们快速实现数据的转换。

要使用 marshmallow 这个库,须要先安装下:

pip3 install marshmallow

好了以后,咱们在以前的基础上定义一个 Schema,以下:

class UserSchema(Schema):
   name = fields.Str()
   age = fields.Integer()
   
   @post_load
   def make(self, data, **kwargs):
       return User(**data)

仍是以前的数据:

data = [{
   'name': 'Germey',
   'age': 23
}, {
   'name': 'Mike',
   'age': 20
}]

这时候咱们只须要调用 Schema 的 load 事件就行了:

schema = UserSchema()
users = schema.load(data, many=True)
print(users)

输出结果以下:

[User(name='Germey', age=23), User(name='Mike', age=20)]

这样,咱们很是轻松地完成了 JSON 到 User List 的转换。

有人说,若是是单个数据怎么办呢,只须要把 load 方法的 many 参数去掉便可:

data = {
   'name': 'Germey',
   'age': 23
}

schema = UserSchema()
user = schema.load(data)
print(user)

输出结果:

User(name='Germey', age=23)

固然,这仅仅是一个反序列化操做,咱们还能够正向进行序列化,以及使用各类各样的验证条件。

下面咱们再来看看吧。

更方便的序列化

上面的例子咱们实现了序列化操做,输出了 users 为:

[User(name='Germey', age=23), User(name='Mike', age=20)]

有了这个数据,咱们也能轻松实现序列化操做。

序列化操做,使用 dump 方法便可

result = schema.dump(users, many=True)
print('result', result)

运行结果以下:

result [{'age': 23, 'name': 'Germey'}, {'age': 20, 'name': 'Mike'}]

因为是 List,因此 dump 方法须要加一个参数 many 为 True。

固然对于单个对象,直接使用 dump 一样是能够的:

result = schema.dump(user)
print('result', result)

运行结果以下:

result {'name': 'Germey', 'age': 23}

这样的话,单个、多个对象的序列化也再也不是难事。

通过上面的操做,咱们完成了 object 到 dict 或 list 的转换,即:

object <-> dict
objects <-> list

验证

固然,上面的功能其实并不足以让你以为 marshmallow 有多么了不得,其实就是一个对象到基本数据的转换嘛。但确定不止这些,marshmallow 还提供了更增强大啊功能,好比说验证,Validation。

好比这里咱们将 age 这个字段设置为 hello,它没法被转换成数值类型,因此确定会报错,样例以下:

data = {
   'name': 'Germey',
   'age': 'hello'
}

from marshmallow import ValidationError
try:
   schema = UserSchema()
   user, errors = schema.load(data)
   print(user, errors)
except ValidationError as e:
   print('e.message', e.messages)
   print('e.valid_data', e.valid_data)

这里若是加载报错,咱们能够直接拿到 Error 的 messages 和 valid_data 对象,它包含了错误的信息和正确的字段结果,运行结果以下:

e.message {'age': ['Not a valid integer.']}
e.valid_data {'name': 'Germey'}

所以,好比咱们想要开发一个功能,好比用户注册,表单信息就是提交过来的 data,咱们只须要过一遍 Validation,就能够轻松得知哪些数据符合要求,哪些不符合要求,接着再进一步进行处理。

固然验证功能确定不止这一些,咱们再来感觉一下另外一个示例:

from pprint import pprint
from marshmallow import Schema, fields, validate, ValidationError

class UserSchema(Schema):
   name = fields.Str(validate=validate.Length(min=1))
   permission = fields.Str(validate=validate.OneOf(['read', 'write', 'admin']))
   age = fields.Int(validate=validate.Range(min=18, max=40))


in_data = {'name': '', 'permission': 'invalid', 'age': 71}
try:
   UserSchema().load(in_data)
except ValidationError as err:
   pprint(err.messages)

好比这里的 validate 字段,咱们分别校验了 name、permission、age 三个字段,校验方式各不相同。

如 name 咱们要判断其最小值为 1,则使用了 Length 对象。permission 必需要是几个字符串之一,这里又使用了 OneOf 对象,age 又必须是介于某个范围之间,这里就使用了 Range 对象。

下面咱们故意传入一些错误的数据,看下运行结果:

{'age': ['Must be greater than or equal to 18 and less than or equal to 40.'],
'name': ['Shorter than minimum length 1.'],
'permission': ['Must be one of: read, write, admin.']}

能够看到,这里也返回了数据验证的结果,对于不符合条件的字段,一一进行说明。

另外咱们也能够自定义验证方法:

from marshmallow import Schema, fields, ValidationError

def validate_quantity(n):
   if n < 0:
       raise ValidationError('Quantity must be greater than 0.')
   if n > 30:
       raise ValidationError('Quantity must not be greater than 30.')

class ItemSchema(Schema):
   quantity = fields.Integer(validate=validate_quantity)

in_data = {'quantity': 31}
try:
   result = ItemSchema().load(in_data)
except ValidationError as err:
   print(err.messages)

经过自定义方法,一样能够实现更灵活的验证,运行结果:

{'quantity': ['Quantity must not be greater than 30.']}

对于上面的例子,还有更优雅的写法:

from marshmallow import fields, Schema, validates, ValidationError


class ItemSchema(Schema):
   quantity = fields.Integer()
   
   @validates('quantity')
   def validate_quantity(self, value):
       if value < 0:
           raise ValidationError('Quantity must be greater than 0.')
       if value > 30:
           raise ValidationError('Quantity must not be greater than 30.')

经过定义方法并用 validates 修饰符,使得代码的书写更加简洁。

必填字段

若是要想定义必填字段,只须要在 fields 里面加入 required 参数并设置为 True 便可,另外咱们还能够自定义错误信息,使用 error_messages 便可,例如:

from pprint import pprint
from marshmallow import Schema, fields, ValidationError

class UserSchema(Schema):
   name = fields.String(required=True)
   age = fields.Integer(required=True, error_messages={'required': 'Age is required.'})
   city = fields.String(
       required=True,
       error_messages={'required': {'message': 'City required', 'code': 400}},
   )
   email = fields.Email()

try:
   result = UserSchema().load({'email': 'foo@bar.com'})
except ValidationError as err:
   pprint(err.messages)

默认字段

对于序列化和反序列化字段,marshmallow 还提供了默认值,并且区分得很是清楚!如 missing 则是在反序列化时自动填充的数据,default 则是在序列化时自动填充的数据。

例如:

from marshmallow import Schema, fields
import datetime as dt
import uuid

class UserSchema(Schema):
   id = fields.UUID(missing=uuid.uuid1)
   birthdate = fields.DateTime(default=dt.datetime(2017, 9, 29))

print(UserSchema().load({}))
print(UserSchema().dump({}))

这里咱们都是定义的空数据,分别进行序列化和反序列化,运行结果以下:

{'id': UUID('06aa384a-570c-11ea-9869-a0999b0d6843')}
{'birthdate': '2017-09-29T00:00:00'}

能够看到,在没有真实值的状况下,序列化和反序列化都是用了默认值。

这个真的是解决了我以前在 cattrs 序列化和反序列化时候的痛点啊!

指定属性名

在序列化时,Schema 对象会默认使用和自身定义相同的 fields 属性名,固然也能够自定义,如:

class UserSchema(Schema):
   name = fields.String()
   email_addr = fields.String(attribute='email')
   date_created = fields.DateTime(attribute='created_at')

user = User('Keith', email='keith@stones.com')
ser = UserSchema()
result, errors = ser.dump(user)
pprint(result)

运行结果以下:

{'name': 'Keith',
'email_addr': 'keith@stones.com',
'date_created': '2014-08-17T14:58:57.600623+00:00'}

反序列化也是同样,例如:

class UserSchema(Schema):
   name = fields.String()
   email = fields.Email(load_from='emailAddress')

data = {
   'name': 'Mike',
   'emailAddress': 'foo@bar.com'
}
s = UserSchema()
result, errors = s.load(data)

运行结果以下:

{'name': u'Mike',
'email': 'foo@bar.com'}

嵌套属性

对于嵌套属性,marshmallow 固然也不在话下,这也是让我以为 marshmallow 很是好用的地方,例如:

from datetime import date
from marshmallow import Schema, fields, pprint

class ArtistSchema(Schema):
   name = fields.Str()

class AlbumSchema(Schema):
   title = fields.Str()
   release_date = fields.Date()
   artist = fields.Nested(ArtistSchema())

bowie = dict(name='David Bowie')
album = dict(artist=bowie, title='Hunky Dory', release_date=date(1971, 12, 17))

schema = AlbumSchema()
result = schema.dump(album)
pprint(result, indent=2)

这样咱们就能充分利用好对象关联外键来方便地实现不少关联功能。

以上介绍的内容基本算在平常的使用中是够用了,固然以上都是一些基本的示例,对于更多功能,能够参考 marchmallow 的官方文档:https://marshmallow.readthedocs.io/en/stable/,强烈推荐你们用起来。

相关文章
相关标签/搜索