文章详情页 您现在的位置是:网站首页>文章详情
schema-让数据验证更优雅
Jeyrce.Lu
发表于:2019年6月14日 22:34
分类:【Python】
3228次阅读
有一段时间没有更新了,我并不是一个高产的博主。但是假如写出没有意义的文章,那么不如不写。
用户的行为永远是不可信的,我们一定要对用户输入的数据进行校验,让数据符合我们开发者设计的数据格式,同样对于返回给用户的数据,也需要遵循一定的格式。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篇
- 加入本站:2387天
作者其他文章
站长推荐
友情链接
站点信息
- 运行天数:2388天
- 累计访问:164169人次
- 今日访问:0人次
- 原创文章:69篇
- 转载文章:4篇
- 微信公众号:第一时间获取更新信息
