总结:Python 轻量级序列化和反序列化包 marshmallow 详细使用指南

第一部分、marshmallow 基本的数据类型及基础属性介绍

1、基础数据类型

fields.String(*, default, missing, data_key, …) 字符串类型

fields.UUID(*, default, missing, data_key, …) UUID字符串类型

fields.Integer(*, strict, **kwargs) 整数类型

fields.Decimal(places, rounding, *, allow_nan, …) 支持Python中的decimal类型,常用语金额类字段

fields.Boolean(*, truthy, falsy, **kwargs) 布尔类型

fields.Float(*, allow_nan, as_string, **kwargs) 浮点类型

fields.DateTime(format, **kwargs) 日期时间类型

fields.Date(format, **kwargs) 日期类型

fields.Email(*args, **kwargs) 邮箱字符串类型

fields.List(cls_or_instance, type], **kwargs) 列表类型,常用于接收数组数据

fields.Dict(keys, type] = None, values, …) 字典类型,常用于接收json类型数据

2、特殊一点的数据类型

fields.Nested(nested, type, str, Callable[[], …) 类似于django中的外键序列化类型,用于使用额外的Schema序列化外键对象

fields.Method(serialize, deserialize, **kwargs) 一个采用Schema方法返回值的字段。类似于django序列化器中的函数字段,可以通过方法构造一个完整的字段返回,该字段可以不是模型中存在的。

fields.Function(serialize, Any], Callable[[Any, …) 接受函数返回值的字段。可将字段通过函数处理后将函数返回值作为该字段的值序列化返回。

3、数据类型的常用通用属性

default 如果设置,则缺少输入值时,将在反序列化(输入数据验证)过程中使用此值。如果未设置,则如果缺少输入值,则该字段将从串行化输出中排除。可以是值或可调用。

missing 该字段用于序列化时,如果字段未在数据中找到,则使用此值。可以是值或可调用。

validate 反序列化期间调用的验证器或验证器集合。验证程序将字段的输入值作为其唯一参数,并返回一个布尔值。如果返回False,ValidationError则会引发异常。

required 必传验证,如果没有传递该字段将引发默认字段异常。

allow_none 将其设置为在验证/反序列化期间True是否None应被视为有效值。如果missing=None和allow_none未设置,则默认为True。否则,默认值为False。

load_only 如果True在序列化过程中跳过此字段,则其值将出现在序列化数据中。

dump_only 如果True在反序列化过程中跳过此字段,则其值将出现在反序列化的对象中。在HTTP API的上下文中,这有效地将该字段标记为“只读”。

error_messages 值为字典类型,可以用来替代默认的字段异常提示语,例如error_messages={“required”: “功能名称为必传项。”}将默认的必传验证异常语句换成中文。

metadata 要存储为元数据的额外参数。

4、常见的内置验证器

validate.Email(*, error) 邮箱验证,error 表示可以替换内置的异常提示语,传入值为字符串。

validate.Equal(comparable, *, error) 相等验证,验证输入值是否等于给定值

validate.Length(min, max, *, equal, error) 长度验证,验证输入值的最大最小

validate.OneOf(choices, labels, *, error) 选项验证,验证输入值是否属于选项

validate.Range([min, max]) 范围验证

validate.Regexp(regex, bytes, Pattern][, flags]) 正则验证

validate.URL(*, relative, schemes, Set[str]] = None, …) 验证是否为URL

第二部分、使用marshmallow进行数据验证

1、安装marshmallow包

pip install marshmallow

2、反序列化:输入数据验证

会对输入数据进行验证,如果数据不符合给定的格式,就会触发ValidationError的异常,marshmallow就会将验证异常的信息输出,在代码中就可以捕获这个异常,将异常的信息友好的返回给调用处。在实际web场景的应用中,这也是很常见的,例如对用户输入的数据进行验证,如果数据不符合规范,就将异常信息返回给调用处。

数据验证中还有很多高级的用法,例如定制验证消息、自定义验证器、内置验证器、未知字段的处理等等,本例中都会一一列出日常开发中最常用到的东西,读者在测试时按需打开对应的测试注释即可得到测试的输出。

示例代码如下:

from marshmallow import Schema, fields, ValidationError, validates_schema, validates, EXCLUDE, validate
import datetime


# 将原本的英文验证提示转为中文提示
TranslateToCnErrorMessages = {
    
    
    'required': '该字段必传。',
    'null': '该字段必能为空。',
    'validator_failed': '该字段验证失败。',
    'required': '该字段必传。',
    'type': '该字段类型无效。',
    'invalid': '该字段不是期待的数据类型。'
}

def validate_null(data):
    '''
    一个通用的非空字符串验证器
    '''
    if not data:
        raise ValidationError("该字段字段不能为空字符串")

def validate_name(data):
    '''
    自定义验证条件
    '''
    validate_null(data)
    if len(data) <= 1:
        raise ValidationError("name长度必须大于1位")
    if len(data) >= 6:
        raise ValidationError("name长度不能大于6位")


# 创建一个图式
class TestSchema(Schema):
    # name = fields.String(required=True) # 此时虽然对name进行了必传验证,但是空字符串也能传
    name = fields.String(required=True, validate=validate_name, error_messages=dict(TranslateToCnErrorMessages)) # 可以额外自定义一个验证条件,用来防止空字符和长度验证
    # sex = fields.String(required=True, error_messages=dict(TranslateToCnErrorMessages, **{'invalid': '期待字符串类型'}))
    sex = fields.String(required=True, error_messages=dict(TranslateToCnErrorMessages, **{
    
    'invalid': '期待字符串类型'}), validate=validate.Length(min=1,max=1, error='sex字段字符串长度为1。')) # 使用内置验证器
    # age = fields.Number(required=True) # 可能出现浮点数
    age = fields.Integer(required=True) # 整数
    addr = fields.String(missing='上海', validate=validate_null) # 给一个默认值,在某字段以数据库角度是有默认值时,可以使用missing指定。表示该字段可以不传,不传时使用missing的值。
    # addr = fields.String(default='北京') # default是在序列化时使用的,例如在序列化时,addr字段不存在时就返回该默认值。在下一章节介绍使用。
    status = fields.Integer(validate=validate.Equal(comparable=1, error='该字段为固定值1。')) # 整数
    email = fields.Email(error_messages=dict(TranslateToCnErrorMessages, **{
    
    'invalid': '期待的字符串格式为邮箱类型。'}))
    password = fields.Str(load_only=True) # 设置密码只写属性,只能被写入,不能被读出
    created_time = fields.DateTime(dump_only=True) # 设置创建时间只读属性,只能被读取,不等被写入

    class Meta:
        strict = True # 定义strict=True,则一次可对多个字段进行验证。官方是这么说的,但我试了好几遍也没弄明白怎么用
        unknown = EXCLUDE # 对未知字段的处理,EXCLUDE表示直接扔掉
        # ordered = True # 可以对验证数据进行排序,当ordered为真时,的到的已验证数据字段按照类中的顺序排列,但是返回的是一个OrderedDict

    @validates_schema
    def validate_func(self, data, **kwargs):
        # 此种方法进行验证时,无法同时对多个字段发出异常提示,只能通过匹配逐个提示
        # 如果需要多字段同时发出异常提示,需要自定义validate或使用validates装饰器,但是推荐使用装饰器的方式。这样都在同一个类内,便于管理。
        if data['sex'] == '':
            raise ValidationError("sex不能为空字符串")
        if len(data['sex']) > 100:
            raise ValidationError("sex非法")
        if len(data['email']) <= 10:
            raise ValidationError("email非法")
    
    # 在内部使用装饰器的方式验证name字段,实现效果和ame内的validate是一致的。实际中建议使用装饰器的方法实现。
    # @validates('name')
    # def validate_name(self, data):
    #     if not data:
    #         raise ValidationError("name字段不能为空字符串")
    #     if len(data) <= 1:
    #         raise ValidationError("name长度必须大于1位")
    #     if len(data) >= 6:
    #         raise ValidationError("name长度不能大于6位")

    # @validates('sex')
    # def validate_sex(self, data):
    #     if data == '':
    #         raise ValidationError("sex不能为空字符串")
    
    # @validates('email')
    # def validate_email(self, data):
    #     if len(data) <= 10:
    #         raise ValidationError("email非法")
        

unvalidate_datas = [
    {
    
    "name": "张三", "sex": "男", "age": 16, "email": "[email protected]"},
    {
    
    "sex":"男女", "age": 16, "email": "[email protected]", "status":0}, # no name
    {
    
    "name": "张三", "age": 16, "email": "[email protected]"}, # no sex
    {
    
    "name": "张三", "sex": None, "age": 16, "email": "[email protected]"}, # sex is null
    {
    
    "name": "张三", "sex": "", "age": 16, "email": "[email protected]"}, # sex is ''
    {
    
    "name": "张三", "sex": 1, "age": 16, "email": "test.com"}, # sex is not str
    {
    
    "name": "张三", "sex": "男", "age": 16}, # no email
    {
    
    "name": "张三", "sex": "男", "age": 16, "email": "[email protected]", "act": "爱好"},
    {
    
    "name": "张三", "sex": "男", "age": 16, "email": "[email protected]", "addr": "杭州"},
]

def main():
    try:
        # validataed_data = TestSchema(many=True).load(unvalidate_datas) # 验证多个数数据集
        # validataed_data = TestSchema().load(unvalidate_datas[4]) # 验证单个数据集
        # validataed_data = TestSchema().load(unvalidate_datas[1]) # 测试自定义验证器、内置验证器、验证提示信息转中文
        # validataed_data = TestSchema().load(unvalidate_datas[7]) # 测试是未知字段的处理方式:本例是直接丢弃,会发现结果中没有act字段
        # validataed_data = TestSchema().load(unvalidate_datas[8]) # 测试default和missing的使用逻辑:missing在没有传递目标字段时会被使用。default在序列化时使用。
        validataed_data = TestSchema().load(unvalidate_datas[1], partial=("name",)) # 忽略部分字段不验证的方式,本例将name字段的验证忽略。
        print('得到的已验证数据:', validataed_data, type(validataed_data))
    except ValidationError as err:
        print('验证异常')
        print(err.messages)


if __name__ == '__main__':
    main()
  1. 反序列化的一些高级操作

在反序列时可以使用官方提供的一些常见的装饰器完成特定的操作,例如:
post_dump([fn,pass_many,pass_original]) 注册要在序列化对象后调用的方法,它会在对象序列化后被调用。
post_load([fn,pass_many,pass_original]) 注册反序列化对象后要调用的方法,它会在验证数据之后被调用。
pre_dump([fn,pass_many]) 注册要在序列化对象之前调用的方法,它会在序列化对象之前被调用。
pre_load([fn,pass_many]) 在反序列化对象之前,注册要调用的方法,它会在验证数据之前调用。
validates(field_name) 注册一个字段验证器,对指定字段设置验证器。这个的使用方法在上面可以测试。

下面示例是在反序列化后将传入的json数据直接转换为需要的对象。

from marshmallow import Schema, fields, ValidationError, validates_schema, validates, EXCLUDE, post_load
import datetime


# 可以使用的对象1
user_datas = [
    {
    
    "name": "张三", "sex": "男", "age": 16, "email": "[email protected]", "password":"123456"},
    {
    
    "name": "李四", "sex": "女", "age": 17, "email": "[email protected]", "created_time" : datetime.datetime.now()},
]

# 可以使用的对象2
class UserModel(object):
    def __init__(self, name, sex, age, email, password):
        self.name = name
        self.sex = sex
        self.age = age
        self.email = email
        self.password = password
        self.created_time = datetime.datetime.now()

    def __repr__(self):
        return '<User(name={self.name!r})>'.format(self=self)


# 创建一个返回序列化对象使用的图式
class UserModelSchema(Schema):
    name = fields.String()
    sex = fields.String()
    age = fields.Integer()
    email = fields.Email()
    password = fields.Str(load_only=True)
    created_time = fields.DateTime(format='%Y-%m-%d %H:%M:%S', dump_only=True)

    class Meta:
        ordered = True

    @post_load
    def make_user_obj(self, data, **kwargs):
        return UserModel(**data)


def main():
    '''
    post_load的使用
    '''
    try:
        res = UserModelSchema().load(user_datas[0])
        print(res, type(res)) # 测试直接返回一个对象。原来没有使用post_load时返回的是字典对象
    except Exception as err:
        print(err)


if __name__ == '__main__':
    main()

第三部分、使用marshmallow进行序列化操作

在前后端分离的开发模式过程中,前后端之间使用什么数据通讯、怎么通讯是很重要的问题,目前使用最多的就是restfull规范,即使用json作为通讯的数据交换媒介。前端通过json向后端进行请求,后端响应后向前端返回json作为response。所以在开发过程中后端势必要将对象、字典等复杂的数据全部转化为json来和前端或其他后端进行通讯。

所谓序列化:就是将对象、字典等数据转成json字符串的过程。那么在Python中为什么不用json.dumps呢?这是因为json.dumps的序列化能力很有限,例如你要序列化的字典中存在一个key的值是时间类型,这个时候json.dumps就不好使了,会抛出异常。因此需要一种专业序列化工具,可以将给定的复杂数据转化为json字符串的能力。

marshmallow能提供的序列化能力不亚于Django drf那一套的序列化能力,例如将model序列化为json,model内嵌model序列化为json等。

示例代码如下:

from marshmallow import Schema, fields, ValidationError, validates_schema, validates, EXCLUDE
import datetime


# 创建一个返回序列化对象使用的图式
class ReturnTestSchema(Schema):
    name = fields.String()
    sex = fields.String()
    age = fields.Integer()
    email = fields.Email()
    password = fields.Str(load_only=True) # 设置密码只写属性,只能被写入,不能被读出
    created_time = fields.DateTime(format='%Y-%m-%d %H:%M:%S', default=datetime.datetime.now()) # format 可以格式化时间字符串的格式,default用来设置序列化时如果某个对象不存在该字段,那么就使用default值。

    # class Meta:
    #     ordered = True # 输出结果时,需要按照顺序字段顺序进行输出,将该值设为真即可


# 可以使用的对象1
user_datas = [
    {
    
    "name": "张三", "sex": "男", "age": 16, "email": "[email protected]", "password":"123456"},
    {
    
    "name": "李四", "sex": "女", "age": 17, "email": "[email protected]", "created_time" : datetime.datetime.now()},
]

# 可以使用的对象2
class UserModel(object):
    def __init__(self, name, sex, age, email, password):
        self.name = name
        self.sex = sex
        self.age = age
        self.email = email
        self.password = password
        self.created_time = datetime.datetime.now()

    def __repr__(self):
        return '<User(name={self.name!r})>'.format(self=self)


def main():
    '''
    序列化对象
    '''
    try:
        # user_data = UserModel(**user_datas[0])
        # res = ReturnTestSchema().dumps(user_data) # 将实例对象进行序列化
        # res = ReturnTestSchema().dumps(user_datas[0]) # 也可以将字典对象进行序列化
        # res = ReturnTestSchema(many=True).dumps(user_datas) # 还可以对一个数据集进行序列化,OR ReturnTestSchema().dump(user_datas, many=True)
        # res = ReturnTestSchema(many=True).dump(user_datas) # dump返回的是一个可以支持的Python内对象,如字典、列表等。dumps返回的是一个josn格式的字符串
        # res = ReturnTestSchema(only=('name', 'sex')).dump(user_datas[0]) # 过滤输出,只输出某几个字段
        res = ReturnTestSchema(exclude=('name', 'sex')).dump(user_datas[0]) # 过滤输出,指定字段不输出,其他的都输出。
        print(res, type(res))
    except Exception as err:
        print(err)

if __name__ == '__main__':
    print(main())

第四部分、一些高级操作

  1. 函数字段的使用

    方法字段,在序列化时可以对已存在或不存在的字段进行处理,和drf的方法字段使用基本一样

    并且使用函数字段还可以对输入数据进行格式化或完成一些额外处理

from marshmallow import Schema, fields, ValidationError, validates_schema, validates, EXCLUDE, validate
import datetime


# 创建一个返回序列化对象使用的图式
class ReturnTestSchema(Schema):
    name = fields.String()
    sex = fields.String()
    age = fields.Integer()
    email = fields.Email()
    password = fields.Str(load_only=True)
    created_time = fields.DateTime(format='%Y-%m-%d %H:%M:%S', default=datetime.datetime.now())
    # 1、方法字段,在序列化时可以对已存在或不存在的字段进行处理,和drf的方法字段使用基本一样
    act = fields.Method("get_act") 
    #  使方法字段既可以序列化又可以对输入数据进行格式化
    balance = fields.Method("get_balance", deserialize="load_balance")

    def get_balance(self, obj):
        return obj.get('balance') * 2

    def load_balance(self, value):
        return float(value)

    def get_act(self, obj):
        print(obj.get('sex')) # 使用序列化中的对象,可以做一些额外操作。
        return '计算字段'


# 可以使用的对象1
user_datas = [
    {
    
    "name": "张三", "sex": "男", "age": 16, "email": "[email protected]", "password":"123456"},
    {
    
    "name": "张三", "sex": "男", "age": 16, "email": "[email protected]", "password":"123456", "balance": 10},
    {
    
    "name": "李四", "sex": "女", "age": 17, "email": "[email protected]", "created_time" : datetime.datetime.now()},
]

# 可以使用的对象2
class UserModel(object):
    def __init__(self, name, sex, age, email, password, balance):
        self.name = name
        self.sex = sex
        self.age = age
        self.email = email
        self.password = password
        self.balance = balance
        self.created_time = datetime.datetime.now()

    def __repr__(self):
        return '<User(name={self.name!r})>'.format(self=self)


def main():
    '''
    序列化对象
    '''
    try:
        # res = ReturnTestSchema().dump(user_datas[0])
        res = ReturnTestSchema().dump(user_datas[1]) # 测试序列化balance
        print(res, type(res))
        # validate_data = ReturnTestSchema().load(user_datas[0])
        validate_data = ReturnTestSchema().load(user_datas[1]) # 测试反序列化balance
        print(validate_data, type(validate_data))
    except Exception as err:
        print(err)

if __name__ == '__main__':
    print(main())
  1. Nested数据类型的使用

在使用MySQL这种结构化的数据库时,不可避免的会使用到外键。Nested数据类型就是用来处理外键字段的。它会自动的将外键类型的数据转换为json,一对多或多对多时,还可以通过配置将外键转为一个json数据集。

下面就看具体操作,运行测试

from marshmallow import Schema, fields, ValidationError, validates_schema, validates, EXCLUDE, validate
import datetime as dt


class User(object):
    def __init__(self, name, email):
        self.name = name
        self.email = email
        self.created_at = dt.datetime.now()
        self.friends = []
        self.employer = None
        self.books = []


class Blog(object):
    def __init__(self, title, author):
        self.title = title
        self.author = author # 用来代替MySQL中的外键关系


class BlogSchema(Schema):
    title = fields.String()
    # 使用lambda匿名函数,使只有在使用author时才会找UserSchema对象,不然可能出现先后调用,对象找不到的情况
    author = fields.Nested(lambda: UserSchema()) # 用来序列化外键对象,可以将外键对象按照指定的图式进行序列化,并且外键指定的图式可以完成任何支持的marshmallow操作

class UseUserSchema(Schema):
    name = fields.String()
    email = fields.String()

class UserSchema(Schema):
    name = fields.String()
    email = fields.Email()
    created_at = fields.DateTime(format='%Y-%m-%d %H:%H:%S')
    # 双向嵌套,例如找出所有属于该用户的所有书籍。注意:在大数据量时不要这样做,否则你的接口返回将非常慢
    books = fields.List(fields.Nested(BlogSchema(exclude=("author",))))
    # friends = fields.Nested(UseUserSchema(many=True)) # 方式一:使用一个外部的图式,可以指点序列化哪些字段
    friends = fields.Nested("self",only=("name", ), many=True) # 方式二:使用自身图式作为外键的图式,并指定序列化的字段
    # friends = fields.Nested(lambda: UserSchema(many=True, only=("name",)), dump_only=True) # 方式二的一种:使用自身图式作为外键的图式,并指定序列化的字段
    # friends = fields.Pluck("self", "name", many=True) # 方式三:使用Pluck字段可以用单个值来替换嵌套的数据。


def main():
    '''
    序列化对象
    '''
    try:
        user0 = User(name="南派三叔", email="[email protected]")
        user1 = User(name="刘慈欣", email="[email protected]")
        user2 = User(name="天下霸唱", email="[email protected]")
        blog = Blog(title="盗墓笔记", author=user0)
        res = BlogSchema().dump(blog)
        print(res, type(res))
        user0.books = [blog]
        user0.friends.append(user1)
        user0.friends.append(user2)
        res = UserSchema().dump(user0)
        print(res, type(res))
    except Exception as err:
        print(err)

if __name__ == '__main__':
    print(main())

猜你喜欢

转载自blog.csdn.net/haeasringnar/article/details/109339949
今日推荐