Response是Flask中响应客户端请求的类,然而在Flask应用中很少直接使用Response。Flask使用Response作为响应数据的容器,在响应客户端的请求时会添加一些创建HTTP响应所需要的附加信息。flask的响应数据是由应用的路由函数返回给客户端。然而,Flask也为应用提供了一种选择,来使开发者自己定义一些response类。本文将利用这一点来展示如何简化你应用的代码。

Flask的Response是如何工作的?

大多数的应用不会直接使用Flask的Response类,但是这不意味着框架中不使用Response。实际上,Flask为每个请求都创建响应对象。于是问题来了,它到底是如何工作的呢?
响应周期的起始时刻即:当Flask调用一个函数去处理请求的返回值的这个时间点。在web应用中,路由处理的最后都会调用render_template函数,render_template的主要功能是渲染引用的模板文件并作为字符串返回给客户端:

@app.route('/index')
def index():
    # ...
    return render_template('index.html')

但是你可能知道,一个Flask路由处理函数有两个可选的附加参数返回,一个是响应状态的编码,另一个是自定义的HTTP的报文类型:

@app.route('/data')
def index():
    # ...
    return render_template('data.json'), 201, {'Content-Type': 'application/json'}

Flask默认的Content-Type值是HTML,请求处理成功的默认编码值是200,在上边的例子中,Flaks将状态编码设置为201,响应内容的类型设置为JSON格式。
响应的内容由三个基本的单元组成:数据或者html的body,状态编码,Http报文类型。Flask应用实例中的make_response()函数实现了将路由函数中的返回值存储在Response对象中的功能。
你可以在Python的console中输入如下的代码来观察上述的处理过程:

>>> from flask import Flask
>>> app = Flask(__name__)
>>> app.make_response('Hello, World')
<Response 12 bytes [200 OK]>
>>> app.make_response(('Hello, World', 201))
<Response 12 bytes [201 CREATED]>

这里我创建了一个Flask应用实例,并调用make_response()方法创建了两个Response对象。第一个例子中,将一个字符串作为参数,response中的状态码和报文类型使用的是默认值。第二个例子中,参数是一个包含两个值的元组参数,并将状态码设置成为非默认值。
Flask将Response对象作为路由函数的响应的同时,有许多细节可以处理。例如,在after_request函数中可以用来插入或者修改http报文类型,改变html的内容或者状态码,甚至可以自定义一个完全不同的响应class来替代默认的Response。最后,Flask会将修改之后的响应对象返回给客户端。

Flask的Response类

下面我们来看看响应类中最核心的部分。下面这个类给出了我所认为的最核心的属性和方法:

class Response:
    charset = 'utf-8'
    default_status = 200
    default_mimetype = 'text/html'
    def __init__(self, response=None, status=None, headers=None,
                 mimetype=None, content_type=None, direct_passthrough=False):
        pass
    @classmethod
    def force_type(cls, response, environ=None):
        pass

这里需要注意的是,当你去看Flask源码时,看到的和上面的代码不一致。Flask中的Response 实际上非常简洁,这是由于Response 的核心功能是由Werkzeug框架中的Response 实现的。Werkzeug中的Response类继承自BaseResponse 类,
上边的属性和方法是由BaseResponse 定义的。

这三个类属性:charset, default_status 和default_mimetype定义了一些默认值。如果你的应用配置和默认值不符,可以通过继承Response 类来自定义属性,这样可以避免每次在响应时修改这些值。例如,如果你的应用是API,所有的路由返回的都是XML,你可以在自己定制的响应类中修改default_mimetype 为application/xml,这样Flask将默认的返回XML格式的响应。

我不会再详细的介绍init构造方法,但是需要注意的是Response 的构造方法接收三个重要的参数,响应主体,状态码和报头。在子类中,构造方法可以改变这个规则来创建特定的响应。

force_type()方法在响应类中扮演了十分重要的作用。在某些情况下,Werkzeug或Flask需要创建自己的响应对象,例如应用程序发生错误时,需要将错误响应返回给客户端。在这种情况下,响应对象是由框架创建的,而不是由应用产生的。当你在一个应用中创建特定的响应类型时,Flask和Werkzeug并不知道这个特定的响应类型的详细信息,因而框架还是默认的创建标准的响应类型。响应类中的force_type()方法负责将不同的响应实例转换成自己需要的形式。

我相信你一定会对force_type()的作用感到迷惑。实际上,每当Flask遇到响应对象与期望的类不符时,使用这个方法可以转换响应对象。在下边的第三个例子中,我将展示如何利用这个方法使得Flask的路由函数为每一个请求都返回一个支持诸如字典,列表,或者其他任何形式的特定对象的响应。
上边已经讲述了足够多的原理,在下边的章节中我会将这些编程技巧用代码实现。

使用定制的Response类
 为一个Flask应用配置一个定制的响应类是非常简单的。让我们看看下边这个例子:
from flask import Flask, Response
class MyResponse(Response):
    pass
app = Flask(__name__)
app.response_class = MyResponse
# …

这里我自己定义了一个名字是MyResponse 的响应类。通常我们定义自己的响应类时,只需要自定义一个Flask的Response 类的子类,并在此基础上添加或者修改某些行为。之后将app.response_class属性指定为自定义的类,就可以使Flask使用我们自己的响应类型。

Flask 类的属性response_class是类属性,因此作为从上边例子的变体,你可以创建一个Flask的子类来使用你自己的响应类:

from flask import Flask, Response
class MyResponse(Response):
    pass
class MyFlask(Flask)
    response_class = MyResponse
app = MyFlask(__name__)
例子#1:改变Response 的默认值

第一个例子十分简单。比如你的应用在所有的endpoints都返回XML.对于这种情形,我们只需要将默认的content type设置为application/xml即可。这个只需要两行代码就可以实现。

class MyResponse(Response):
    default_mimetype = 'application/xml'

很简单,对不对?如果将这个类设置为应用的默认响应,你可以在路由函数中返回XML而不必设置
content type。例如:

@app.route('/data')
def get_data():
    return '''<?xml version="1.0" encoding="UTF-8"?>
<person>
    <name>John Smith</name>
</person>
'''

如果使用默认的响应类,路由将会默认接收 application/xml格式的内容类型。如果你需要不同的
content type,你只需要在正常的路由函数返回值里覆盖一下默认值即可,

@app.route('/')
def index():
    return '<h1>Hello, World!</h1>', {'Content-Type': 'text/html'}
例子#2:自动地决定Content Type

下边的这个例子有一些复杂。假定这个应用中的HTML 和XML路由数量相当,这时使用第一种方法就十分低效,因为你无论怎么设置默认的格式,都有一半数量的路由需要覆盖默认的content type。
一个比较好的方案是:创建一个响应类型可以依据响应的内容自行决定使用的content type。下边给出一个实例:

class MyResponse(Response):
    def __init__(self, response, **kwargs):
        if 'mimetype' not in kwargs and 'contenttype' not in kwargs:
            if response.startswith('<?xml'):
                kwargs['mimetype'] = 'application/xml'
        return super(MyResponse, self).__init__(response, **kwargs)

这个例子中,一开始response中没有指定content type。接下来,如果response文本是以<?xml
开头,这说明数据是以XML文档格式编码。如果上边的两个条件均为真,将XML content type以
字典的形式作为参数发送给父类的构造方法.
在这个response类中,任何XML格式的文档都会自动的接收XML content type,而其他的响应将继续使用默认的content type。而且在所有的类中,我们仍然可以自动的指定我们需要的content type类型。

例子#3:自动地设定JSON响应

最后的例子将会处理Flask的中非常麻烦的情形。使用Flask的API返回JSON格式是一种非常普遍的情形,这需要你调用jsonify()函数将Python 字典转换成JSON 格式,并在响应中设置content type为JSON.下边是一个路由处理的例子:

@app.route('/data')
def get_data():
    return jsonify({'foo': 'bar'})

遗憾的是,所有需要返回JSON的路由函数都需要这么做,这会导致大量API的结尾都会重复的调用jsonify()函数。从代码可读性的角度考虑,是否有方法可以替代?

@app.route('/data')
def get_data():
    return {'foo': 'bar'}

这里定义了一个响应类,可以支持上边的语法,使得所有的路由函数不需要再返回JSON:

class MyResponse(Response):
    @classmethod
    def force_type(cls, rv, environ=None):
        if isinstance(rv, dict):
            rv = jsonify(rv)
        return super(MyResponse, cls).force_type(rv, environ)

Flask 中路由handler 函数只能处理有限的几种类型。这些类型包括str, unicode, bytes, bytearray
但是当你返回一些不支持的类型,例如上面例子中的字典,Flask会如何处理呢?如果返回的响应类型与期望的类型不符,Flask将其认定为未知的响应类型,此时不会创建response对象返回信息,而是调用类方法force_type()强制转换这个未知类型。在这个例子中,子类自定义的响应类覆盖重写这个方法,自动转换返回值是字典的情形。转换是通过在调用父类方法之前,先调用jsonify()方法来实现。
上面的这种技巧不会影响其他正常的响应函数的功能。这是由于对于任何返回正常响应类来说,子类没有做任何事情,所有的请求透明的传递给父类。

原文链接

https://blog.miguelgrinberg.com/post/customizing-the-flask-response-class

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐