Python API 类型系统的设计与演变(2)类型系统实践
- UID
- 1066743
|
Python API 类型系统的设计与演变(2)类型系统实践
类型系统实践下面以 Python 2.7 为例,详细介绍下如何在一个在线服务上实现类型系统,以及类型系统可以帮助研发人员做哪些有意义的事情。
marshmallowPython 2 中没有一个官方的类型系统实现,所以在 API 参数的验证中,往往是通过外挂第三方 Schema 实现的。
marshmallow 是本文选用的一个对类型系统进行建模的 Python 库,它有着极高的流行程度,提供了基本的类型定义、参数验证功能和序列化 / 反序列化机制。
现在假设研发团队要开发一个用户相关的接口,首先要对用户这个服务资源进行抽象定义,一个基本的 Schema 定义如下:
清单 1. 一个用户接口参数模式定义1
2
3
4
5
6
7
8
9
10
11
12
13
| # -*- coding: utf-8 -*-
import re
from marshmallow import Schema, fields, validate
from myapp import fields as myfields
class UserSchema(Schema):
user_id = myfields.UserId(required=True, help=u'用户的唯一 ID')
nickname = fields.Str(required=True,
validate=validate.Length(min=2, max=20),
help=u'用户的昵称')
email = fields.Email(required=True, u'用户的邮箱,不可重复')
|
marshmallow 自带了许多内建类型,比如 Email,URL,UUID 等,研发人员也可以根据业务来定制自定义类型,比如上文的 UserId 可以像这样定义:
清单 2. 自定义类型示例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| # -*- coding: utf-8 -*-
import re
class UserId(fields.Field):
""" 长度为 10 - 17 的,由字母、数字、下划线组成的 ID """
pattern = re.compile(r'^[a-zA-Z0-9\_]{10-17}$')
# 必选的
default_error_messages = {
'invalid': u'不是一个有效的用户 ID',
'format': u'{value} 无法被格式化为 ID 字符串',
}
def _serialize(self, value, attr, obj):
return value
def _deserialize(self, value, attr, data):
# 可以使用任何验证方式,而不仅仅是正则表达式
if not self.pattern.match(value):
self.fail('invalid', value=value)
return value
|
服务开发人员也可以自己写装饰器或使用开源的库,比如 来根据这个 Schema 做参数验证(以 Flask 为例):
清单 3. Web 框架集成示例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # -*- coding: utf-8 -*-
from flask import Flask, jsonify
from webargs.flaskparser import use_args
from myapp.schema import UserSchema
app = Flask(__name__)
@app.route('/', methods=('GET',))
@use_args(UserSchema)
def echo_user(args):
return jsonify(**args)
if __name__ == '__main__':
app.run()
|
在生产环境的服务中,通常会选择重载 API 注册用的装饰器(比如 @app.route 和 @use_args)来收集 API 的定义存储到一个全局的对象里(可能是远程对象),来实现框架级的 API 反射机制,以允许服务实例在运行时拿到所有已注册的 API 的声明,以给第三方工具 / RPC 客户端提供最新的 Schema。
在上面的代码定义里,大家可以发现 API 类型系统中几个重要的功能都已经存在了:
- Schema 允许以接口为粒度定义类型声明
- fields 允许自定义类型(包括类型的校验规则,描述和错误信息)
- validate 允许自定义校验规则
- webargs 帮助类型系统与框架进行集成
但仅仅有这些就够了吗?
validator 和枚举在繁忙的业务系统开发过程中,通常需要一定程度的抽象来增强代码的可重用性,比如正则表达式和枚举等。
枚举是一种特殊的类型,在线服务对它的可描述性有着更多的诉求。在阅读一个 API 的定义时,人们看到枚举字段,不仅仅想看到这个字段期望什么样的枚举值,更想看到每一个枚举值所代表的涵义,这就要求类型系统扩展(或许是约束)枚举值的定义。
Python 内置的枚举类型有它的优势,但枚举值使用了包装类型,取值时需要通过 .value 函数来获取,而本文所描述的服务已经在线上运行许久了,改造工程浩大,于是采用了类似于 Flask Config Object 的定义风格。
清单 4. 一种可选的枚举声明定义1
2
3
4
5
6
7
8
| class UserStateEnum(object):
OK = 0
PENDING = 1
__desc__ = {
OK: u'有效用户',
PENDING: u'封禁用户'
}
|
通过定义一个类,约定类属性名大写为枚举属性,描述信息放在特殊的字段里,以此来表示枚举类型。
这是一个关键的思维模式:在线服务在扩展时必须要考虑 API 的可解释性。
异常和 RFC 4918在线服务对于异常系统的诉求是将异常按照危重等级进行分离,保证高危异常的可追溯性,以及低危异常的可解释性。
在理想的情况下,可以把异常简单分为三类:
- 系统异常,由于系统故障或程序 Bug 导致的,应及时发送到 Issue Tracking 的系统中并发送警报。
- 业务异常,由于用户的输入不符合业务逻辑导致的异常,比如用户不存在。可以从日志中审计,可能会需要进行 Issue Tracking,无需报警。
- 参数错误,用户的输入不符合文档约定(契约),比如期望参数是一个 URL,但传来一个普通字符串。同样可以从日志中审计,但无需进行 Issue Tracking,无需报警。
在责权划分上,类型系统应该只包含了第三类异常,不涉及业务逻辑和系统异常的处理。
由于本文所描述的 Web 层遵循 REST 语义来进行服务开发,最早的 HTTP Status 使用了 500,随着类型系统的完善,响应状态码也逐渐细分,上面三类异常分别对应 500、400、422 三种 Status Code。
关于 422 状态码的选取,可以参考 和参考文献中一些有益的讨论。 |
|
|
|
|
|