Flask 接口响应封装及自定义json返回类型

tips:

  • 本文主要解决统一响应文本封装及json响应文本类型错误问题
  • 本文基于python3编写
  • 代码仓库

问题重现

  • 前文《Flask后端实践 连载三 接口标准化》实现了响应文本的封装,即:
from response import ResMsg
@app.route("/", methods=["GET"])
def test():
    res = ResMsg()
    test_dict = dict(name="zhang", age=18)
    # 此处只需要填入响应状态码,即可获取到对应的响应消息
    res.update(code=ResponseCode.SUCCESS, data=test_dict)
    return jsonify(res.data)
  • 其中的jsonify是必不可少的,但是我们希望一个函数直接返回响应文本,实现jsonify的自动封装,不必重复书写,类似return res.data这样的格式。
  • 当响应数据中存在datetimeDecimal等类型的时候,使用jsonify转换时会出错,报TypeError: Object of type {} is not JSON serializable
    python与json数据类型对应转换表:

python

json

dict

object

list,tuple

array

str

string

int,float

float

True

true

False

false

None

null

统一封装,减少重复代码

  1. Python装饰器
    Python装饰器在网上有许多教程,请读者自行学习装饰的原理以及用法。知乎上抄的这一段,装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。知乎详解
  2. 分析Flask 路由响应内容
    Flask视图函数支持返回三个值:第一个是返回的数据,第二个是状态码,第三个是头部字典。默认第二个参数为200,第三个参数也
@app.route('/')
def test():
    return 'Hello, World!', 200, {'X-Foo': 'bar'}
  1. 编写route装饰器(util.py)
def route(bp, *args, **kwargs):
    """
    路由设置,统一返回格式
    :param bp: 蓝图
    :param args:
    :param kwargs:
    :return:
    """
    kwargs.setdefault('strict_slashes', False)

    def decorator(f):
        @bp.route(*args, **kwargs)
        @wraps(f)
        def wrapper(*args, **kwargs):
            rv = f(*args, **kwargs)
            # 响应函数返回整数和浮点型
            if isinstance(rv, (int, float)):
                res = ResMsg()
                res.update(data=rv)
                return jsonify(res.data)
            # 响应函数返回元组
            elif isinstance(rv, tuple):
                # 判断是否为多个参数
                if len(rv) >= 3:
                    return jsonify(rv[0]), rv[1], rv[2]
                else:
                    return jsonify(rv[0]), rv[1]
            # 响应函数返回字典
            elif isinstance(rv, dict):
                return jsonify(rv)
            # 响应函数返回字节
            elif isinstance(rv, bytes):
                rv = rv.decode('utf-8')
                return jsonify(rv)
            else:
                return jsonify(rv)

        return wrapper

    return decorator
  1. 使用route装饰器(test.py)
from flask import Blueprint
from util import route
from response import ResMsg
from code import ResponseCode

bp = Blueprint(service_name, __name__, url_prefix="/")

@route(bp, '/packed_response', methods=["GET"])
def test_packed_response():
    """
    测试响应封装
    :return:
    """
    res = ResMsg()
    test_dict = dict(name="zhang", age=18)
    # 此处只需要填入响应状态码,即可获取到对应的响应消息
    res.update(code=ResponseCode.SUCCESS, data=test_dict)
    # 此处不再需要用jsonify,如果需要定制返回头或者http响应如下所示
    # return res.data,200,{"token":"111"}
    return res.data

解决json类型错误

  1. Flask json转换类如下,只需要我们重新写default函数,定义转换规则,便能到达我们想要的效果。
class JSONEncoder(_json.JSONEncoder):
    """The default Flask JSON encoder.  This one extends the default simplejson
    encoder by also supporting ``datetime`` objects, ``UUID`` as well as
    ``Markup`` objects which are serialized as RFC 822 datetime strings (same
    as the HTTP date format).  In order to support more data types override the
    :meth:`default` method.
    """

    def default(self, o):
        """Implement this method in a subclass such that it returns a
        serializable object for ``o``, or calls the base implementation (to
        raise a :exc:`TypeError`).

        For example, to support arbitrary iterators, you could implement
        default like this::

            def default(self, o):
                try:
                    iterable = iter(o)
                except TypeError:
                    pass
                else:
                    return list(iterable)
                return JSONEncoder.default(self, o)
        """
        if isinstance(o, datetime):
            return http_date(o.utctimetuple())
        if isinstance(o, date):
            return http_date(o.timetuple())
        if isinstance(o, uuid.UUID):
            return str(o)
        if hasattr(o, '__html__'):
            return text_type(o.__html__())
        return _json.JSONEncoder.default(self, o)
  1. 自定义Flask json解析类(core.py)
import datetime
import decimal
import uuid

from flask.json import JSONEncoder as BaseJSONEncoder


class JSONEncoder(BaseJSONEncoder):
	"""
    重新default方法,支持更多的转换方法
    """
    def default(self, o):
        """
        如有其他的需求可直接在下面添加
        :param o: 
        :return:
        """
        if isinstance(o, datetime.datetime):
            # 格式化时间
            return o.strftime("%Y-%m-%d %H:%M:%S")
        if isinstance(o, datetime.date):
            # 格式化日期
            return o.strftime('%Y-%m-%d')
        if isinstance(o, decimal.Decimal):
            # 格式化高精度数字
            return str(o)
        if isinstance(o, uuid.UUID):
            # 格式化uuid
            return str(o)
        if isinstance(o, bytes):
            # 格式化字节数据
            return o.decode("utf-8")
        return super(JSONEncoder, self).default(o)
  1. 使用自定义Flask json解析类(app.py)
from flask import Flask
from core import JSONEncoder

app = Flask(__name__)

# 返回json格式转换
app.json_encoder = JSONEncoder

if __name__ == "__main__":
    app.run()
  1. 测试(test.py)
from datetime import datetime
from decimal import Decimal
from flask import Blueprint
from util import route
from response import ResMsg
from code import ResponseCode

bp = Blueprint(service_name, __name__, url_prefix="/")

@route(bp, '/type_response', methods=["GET"])
def test_type_response():
    """
    测试返回不同的类型
    :return:
    """
    res = ResMsg()
    now = datetime.now()
    date = datetime.now().date()
    num = Decimal(11.11)
    test_dict = dict(now=now, date=date, num=num)
    # 此处只需要填入响应状态码,即可获取到对应的响应消息
    res.update(code=ResponseCode.SUCCESS, data=test_dict)
    # 此处不再需要用jsonify,如果需要定制返回头或者http响应如下所示
    # return res.data,200,{"token":"111"}
    return res.data
  1. 响应消息:
{
    "code":0,
    "data":{
        "date":"2019-03-27",
        "now":"2019-03-27 11:05:40",
        "num":"11.1099999999999994315658113919198513031005859375"
    },
    "lang":"zh_CN",
    "msg":"成功"
}

总结

  • 利用装饰器和重写类的方法,解决了前文提出的问题。
  • 下一篇文章将介绍Flask与SQLAlchemy的集成和简单使用。