文章详情页 您现在的位置是:网站首页>文章详情
schema-让数据验证更优雅
Jeyrce.Lu 发表于:2019年6月14日 22:34 分类:【Python】 2723次阅读
有一段时间没有更新了,我并不是一个高产的博主。但是假如写出没有意义的文章,那么不如不写。
用户的行为永远是不可信的,我们一定要对用户输入的数据进行校验,让数据符合我们开发者设计的数据格式,同样对于返回给用户的数据,也需要遵循一定的格式。schema就是一种优雅、简洁的方式。
数据格式的验证
实际上,数据格式验证方式是有很多的,比如我们可以利用正则表达式、isinstance等
# 多个if elif, else来判断数据的类型,格式 if xxx: yyy elif ccc: xxx 。。。 else: xxx
这样当然可以限定数据格式,但是非常臃肿、难看,并且验证逻辑和业务逻辑放在一起,耦合。因此我们在业务层之上抽象出一个验证层,它专门用来做业务逻辑中数据格式的验证,schema
就是一种优雅的数据验证层。
事实上,验证数据有不少三方包做的不错,有的项目中叫做validate,作用和用法都是差不多的,这里只对schema进行一些介绍。
Schema的用法
pip install schema
schema的源码非常短小精悍,完全可以自己研究一遍,它提供了如下核心类,用来满足数据校验过程中的各种需要。
__all__ = [ "Schema", # 核心类,传入一个规则 "And", # 校验条件的`and`关系 "Or", # 校验条件的`or`关系 "Regex", # 用于传入一个正则 "Optional", # 用于表示可选条件 "Use", # 传入一个callable对象 "Forbidden", # 用于禁止数据出现某个名字 "Const", # 用于表示一个常量 "SchemaError", # 数据验证过程中抛出的异常 "SchemaWrongKeyError", "SchemaMissingKeyError", "SchemaForbiddenKeyError", "SchemaUnexpectedTypeError", "SchemaOnlyOneAllowedError", ]
以下所有结果我都在py3.7.3进行测试过,2.x以及3.x其他版本可能会略有不同
(零)传入具体的值:需要完全匹配
print(Schema(5).validate(6)) # 5 not match 6 print(Schema('x').validate('z')) # 'x' not match 'z'
(一)验证数据类型
int, float, list
等等builtin类型:可以得出结论:判断数据类型采用的是 isinstance(obj, type)这样的格式
type_schema = Schema(int) print(type_schema.validate(3.5)) print(type_schema.validate(3)) print(type_schema.validate('3')) print(type_schema.validate([1, 2, 3]))
(二)使用Use
传入一个callable对象:fuction
、内建类型int, float等
、实现了__call__的obj
、lambda
表达式
传入一个function
def check(obj): return obj.__class__.__name__ use_schema = Schema(Use(check)) print(use_schema.validate('sdsdsds')) # str def has_previous(obj): if hasattr(obj, 'previous'): return getattr(obj, 'previous') raise AttributeError('{} obj has no attribute {}'.format(obj, 'previous')) class PageStart(object): pass class PageEnd(object): @property def previous(self): return 0 print(Schema(Use(has_previous)).validate(PageStart())) # traceback print(Schema(Use(has_previous)).validate(PageEnd())) # 0, 可以证明validate验证后得到的是数据传入callable的返回值
传入一个类型: int, float, list等: 注意:这里要和类型判断区别,这里使用的是int(obj)这样的调用,而不是isinstance
from schema import Schema, Const, Use, Optional, Or, And, Forbidden int_schema = Schema(Use(int)) print(int_schema.validate("3")) # 3 , 这里证明了会进行 str -> int的一个隐式转换 print(int_schema.validate("3.5")) # traceback print(int_schema.validate(3)) # 3 print(int_schema.validate(3.3)) # 3 这里证明了会进行int -> float的隐式转换 print(int_schema.validate("sd")) # traceback print(int_schema.validate("d")) # traceback 同样的,我联想到c语言中'a'==65的ascii编码,可以证明python中是不会对这一值进行比较的 float_schema = Schema(Use(float, "必须是一个浮点数")) print(float_schema.validate("3")) # 3.0 print(float_schema.validate("3.5")) # 3.5 注意这里,上面的 '3.5' -> 3 会失败, 但是这里的 '3' -> 3.0 却可以成功 print(float_schema.validate(3)) # 3.0 这里也证明了 int -> float 和 'int' -> float转换 print(float_schema.validate(3.5)) # 3.5 # 可以发现str类型作为用途最广的类型,来者不拒,这也意味着 任意类型 - > str的转换 str_schema = Schema(Use(str)) print(str_schema.validate("3")) # 3 print(str_schema.validate(3)) # 3 print(str_schema.validate([1, 2, 3]), type(str_schema.validate([1, 2, 3]))) # [1, 2, 3] <class 'str'> print(str_schema.validate((1, 2, 3)), type(str_schema.validate((1, 2, 3)))) # (1, 2, 3) <class 'str'> print(str_schema.validate({1, 2, 3}), type(str_schema.validate({1, 2, 3}))) # {1, 2, 3} <class 'str'> print(str_schema.validate({'x': 1, 'y': 2}), type(str_schema.validate({'x': 1, 'y': 2}))) # {'x': 1, 'y': 2} <class 'str'> # 那么不妨我们试一下py的显式转换, 果然再次验证了之前的猜想 print(str([1, 2, 3]), type(str([1, 2, 3]))) # [1, 2, 3] <class 'str'> print(str((1, 2, 3)), type(str((1, 2, 3)))) # (1, 2, 3) <class 'str'> print(str({1, 2, 3}), type(str({1, 2, 3}))) # {1, 2, 3} <class 'str'> print(str({'x': 1, 'y': 2}), type(str({'x': 1, 'y': 2}))) # {'x': 1, 'y': 2} <class 'str'> # 复杂数据类型验证,在此以list为例,请自行验证tupple, set, dict list_schema = Schema(Use(list)) print(list_schema.validate(1)) # traceback print(list_schema.validate('a')) # ['a'] print(list_schema.validate([1, 2, 3])) # [1, 2, 3] print(list_schema.validate((1, 2, 3))) # [1, 2, 3] print(list_schema.validate({1, 2, 3})) # [1, 2, 3] print(list_schema.validate({'x': 1, 'y': 2})) # ['x', 'y']
传入一个实现了__call__方法的实例:
class Call(object): def __call__(self, *args, **kwargs): return 'called' print(Schema(Use(Call())).validate('x')) # called, 实现了__call__方法的对象就可调用
传入一个lambda表达式
print(Schema(Use(lambda obj: obj.__class__.__name__)).validate('xxx')) # str print(Schema(Use(lambda obj: isinstance(obj, dict))).validate({'x': 1})) # True
(三)传入一个实现了validate方法的对象:实际上能够使用Use
、Or
、And
等,其实就是因为他们实现了validate
方法
class DictTemplate(object): """ 自定义一个字典验证器 """ def __init__(self, **kwargs): self.dic = kwargs def validate(self, data): if not isinstance(data, dict): raise TypeError() for k, v in self.dic.items(): if k not in data: raise KeyError() if not isinstance(data[k], type(v)): raise ValueError() return data print(DictTemplate(**{'x': 1, 'y': 'sdf', 'z': [1, 2, 3]}).validate({'x': '3', 'y': 'sss', 'z': []})) # ValueError print(DictTemplate(**{'x': 1, 'y': 'sdf', 'z': [1, 2, 3]}).validate({'x': 3, 'y': 'sss'})) # KeyError print(DictTemplate(**{'x': 1, 'y': 'sdf', 'z': [1, 2, 3]}).validate({'x': 3, 'y': 'sss', 'z': []})) # {'x': 3, 'y': 'sss', 'z': []}
(四)传入一个容器对象:[]
, ()
, '{}'
print(Schema([float, int, str]).validate([3.3, 3, '333'])) # [3.3, 3, '333'] print(Schema((float, int, str)).validate((3.3, 3, []))) # Error [] should be instance of 'str', 可以得到结论:`容器中每个数据必须为float或int或str,他们是或的关系` print(Schema({float, int, str}).validate([3.3, 3, '333'])) # SchemaUnexpectedTypeError: [3.3, 3, '333'] should be instance of 'set', 可以得到结论:待验证数据必须和模板容器是一种类型
(五)传入一个字典:这在我们前后端传输数据时最为常用
print(Schema({'x': int, 'y': str, 'z': [1, 2, 3, 4]}).validate({'x': 2, 'y': 'ccc', 'z': [5, 6]})) # error, 5, 6 not match 1,2,3,4 print(Schema({'x': int, 'y': str, 'z': [1, 2, 3, 4]}).validate({'x': 2, 'y': 'ccc'})) # Missing key 'z' print(Schema({'x': int, 'y': str, 'z': [1, 2, 3, 4]}).validate({'x': 2, 'y': 'ccc', 'z': [1, 2], 'o': 'extra key'})) # wrong key 'o' print(Schema({'x': '3', 'y': 6}).validate({'x': '3', 'y': 6})) # {'x': '3', 'y': 6} print(Schema({'x': '3', 'y': 6}).validate({'x': '4', 'y': 6})) # '3' not match '4' 证明value给定一个值时必须完全一样才可匹配
要点总结:
传入字典时先验证有没有key,多了key也不行,然后验证对应的value类型或者嵌套的限制是否匹配
当模板字典的value给定一个值之后,数据必须完全匹配才可匹配
字典内规则也是可以嵌套的,规则和简单验证一致
(六)模板字典升级篇:应用扩展类Const
, Use
, Optional
, Or
, And
, Forbidden
实现更加灵活的验证规则
Const
和直接传入定值作用完全相同
print(Schema({'x': Const('5')}).validate({'x': 6})) # '5' not match 6 Const传进去啥出来的还是啥,就相当于直接给值 print(Schema({'x': '5'}).validate({'x': 5})) # '5' not match 5 和上面的作用完全相同
Optional
: 可选key, 但是一旦有对应的key,value也必须符合规则
print(Schema({'x': int, Optional('y'): str}).validate({'x': 5})) # {'x': 5} print(Schema({'x': int, Optional('y'): str}).validate({'x': 5, 'y': 6})) # error 6 is not instance of str print(Schema({'x': int, Optional('y'): str}).validate({'x': 5, 'y': 'yyy'})) # {'x': 5, 'y': 'yyy'}
And
,Or
:多个条件关系
# x必须是字符串或浮点数,或者可以转换为整形 print(Schema({'x': Or(Use(int), str, float)}).validate({'x': '5'})) # {'x': 5} # x必须是字符串或可以转换为int print(Schema({'x': Or(Use(int), str)}).validate({'x': 'xxx'})) # {'x': 'xxx'} print(Schema({'x': Or(Use(int), str)}).validate({'x': 5})) # {'x': 5} # x 必须是非空字符串 print(Schema({'x': And(str, lambda x: x != '')}).validate({'x': ''})) # error print(Schema({'x': And(str, lambda x: x != '')}).validate({'x': 'ccc'})) # {'x': 'ccc'}
Forbidden
:禁用某些key,优先级最高,并且当某些key被禁用时他的value类型无所谓
print(Schema({Forbidden('x', 'y'): int, 'z': list}).validate({'z': []})) # {'z': []} print(Schema({Forbidden('x', 'y'): int, 'z': list}).validate({'x': 5, 'z': []})) # error print(Schema({'x': int, Forbidden('x'): int, 'y': str}).validate({'x': 5, 'y': ''})) # error forbidden优先级比较高 print(Schema({Forbidden('x'): int, Optional('x'): int, 'y': str}).validate({'x': 5, 'y': ''})) # error forbidden优先级比较高
(七)Regex传入一个正则表达式
print(Schema(Regex(r'^135[a-zA-Z0-9]+$')).validate('135Ahjg')) # 135Ahjg print(Schema(Regex(r'^135[a-zA-Z0-9]+$')).validate('135444')) # 135444 print(Schema(Regex(r'^135[a-zA-Z0-9]+$')).validate('135x')) # 135x print(Schema(Regex(r'^135[a-zA-Z0-9]+$')).validate('13sx')) # error
(八)扩展篇:一些重要的参数使用
Schema可选参数:
error
,ignore_extra_keys
,name
s = Schema({'x': int}, error='必须是整形', ignore_extra_keys=True) print(s.validate({'x': 3, 'y': 5})) # {'x': 3} print(s.validate({'x': '3', 'y': 5})) # SchemaError: 必须是整形
- error:当验证不通过时抛出的自定义异常 - ignore_extra_keys: 当多出key是是否忽略异常 - name:给schema自定义一个名字,抛出异常时作为前缀
Optional中可以设定默认值:当不传入对应的key时,则默认取用, 并且可以证明:设置默认值后类型就不起作用了,但是你传了对应的key那么value就必须遵循规则了
print(Schema({'x': str, Optional('y', default=7): int}).validate({'x': ''})) # {'x': '', 'y': 7} print(Schema({'x': str, Optional('y', default=7): str}).validate({'x': ''})) # {'x': '', 'y': 7} print(Schema({'x': str, Optional('y', default=7): str}).validate({'x': '', 'y': 5})) # error
综合例子:
print(Schema({ Optional('x', default='xxx'): str, Forbidden('y', 'z'): str, 'age': And(int, lambda x: x > 0 and x < 120) }, ignore_extra_keys=True).validate({'age': 23, 'title': 'xxx'})) # {'age': 23, 'x': 'xxx'}
结语
schema确实是一种短小精悍
数据验证层工具,在一些其他的框架中也有叫做Validate, Field的, 他们使用起来确实比写多个if else强多了。
版权声明 本文属于本站 原创作品,文章版权归本站及作者所有,请尊重作者的创作成果,转载、引用自觉附上本文永久地址: http://blog.lujianxin.com/x/art/yxcf6t3z211i
上一篇:时光,你等等我
猜你喜欢
文章评论区
作者名片
- 作者昵称:Jeyrce.Lu
- 原创文章:61篇
- 转载文章:3篇
- 加入本站:2048天
作者其他文章
站长推荐
友情链接
站点信息
- 运行天数:2049天
- 累计访问:164169人次
- 今日访问:0人次
- 原创文章:69篇
- 转载文章:4篇
- 微信公众号:第一时间获取更新信息