首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

Python API 类型系统的设计与演变(2)类型系统实践

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 状态码的选取,可以参考   和参考文献中一些有益的讨论。
返回列表