如何自定义Flask中的响应类

有翻译或理解不对的地方,望大家指正!

如何自定义Flask中的响应类

Flask框架中的响应类,命名很贴切,叫Response。不过Flask应用中很少直接调用这个类。而是将其作为路由函数所返回响应数据的内部容器,容器里还包含了用于创建HTTP响应的其他信息。

但是没多少人知道,Flask框架其实允许应用将默认的响应类,替换为自定义类。这就给了我们研究小窍门的机会。在本文中,我将展示如何利用Flask的这个特性,简化你的代码。

Flask中的响应类是如何工作的?

大部分应用并不直接使用Flask中的响应类(Response class),但这并不是说这个类没有用武之地;实际上,Flask会为每个请求创建响应对象。那么,它是如何实现的呢?

Flask用来处理请求的函数返回时,响应周期就开始了。在网络应用中,路由通常最后会调用render_template函数,渲染引用的模板文件,将其作为字符串返回:

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

但是,你可能也知道,Flask的路由函数可以选择额外返回两个值,这两个值将被分别设为HTTP状态码和自定义的HTTP响应标头:

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

在上面的例子中,状态码被设为201,取代了Flask默认的200,即请求被成功处理的状态码。这个例子还定义了内容类型标头(Content-Type header),表明HTTP响应中包含JSON数据,因为如果你不明确设置内容类型的话,Flask会默认设置为HTML。

上面的例子介绍了HTTP响应的三个基本组成部分,即数据或正文、状态码和标头。Flask的应用实例拥有一个make_response函数,可以接受路由函数的返回值(可以是单个值,也可以是有1-3个值的元组),并将其填入响应对象(Response object)中。

你可以通过Python控制台会话(console session),看看整个过程。首先创建一个虚拟环境,并安装Flask,然后开启Python会话,并输入下面的代码:

>>> 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()方法创建响应类对象。第一次调用时,我传了一个字符串作为参数,所以响应对象中使用了默认的状态码和标头。第二次调用时,我传入了有两个值的元组,强制返回了非默认的状态码。注意,第二次调用时使用了两个括号,里层的括号将字符串和状态码包在了元组中。由于make_response()函数只接受一个参数,所以必须要这样做。

Flask在创建了代表路由函数返回值的响应对象(Response object)之后,还会做一些处理。包括将响应对象传入自定义的after_request处理程序(handlers),在这一步,应用还有有机会插入或修改标头、更改正文或状态码,如果愿意的话,甚至是启用崭新的的响应对象取而代之。最后,Flask会获取最终的响应对象,渲染成HTTP响应,并发送给客户端。

Flask中的响应类

我们来看看响应类中最有趣的特性。下面的类定义,展示了我眼中这个类所具备的灵活属性和方法:

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类,实际上衍生自Werkzeug库中的一个同名类。而Werzeug中的Response类继承的是BaseResponse类,这个类中就包含了上述定义。

charsetdefault_statusdefault_mimetype这三个类属性定义了相应的默认值。如果任何一个默认值不适用你的应用,那么你可以创建Response类的子类,定义你自己的默认值,而不必在每一个响应对象中设置自定义值。例如,如果你的应用是一个所有的路由均返回XML格式数据的API接口,你就可以在自定义的类中,将default_mimetype改为application/xml,这样Flask就会默认返回XML响应。稍后你会看到如何实现。

这里,我不会详细介绍__init__构造函数(你可以阅读Werkzeug的文档),但请注意,Flask响应对象中的三个重要元素,即响应正文、状态码和标头,是作为参数传入的。在子类中,构造函数可以改变创建响应的相应规则。

响应类中的force_type()类方法,是唯一比较复杂,但又很重要的元素。有时候,Werkzeug或是Flask需要自行创建响应对象,比如出现应用错误,并需要将其返回给客户端时。在这种情况下,响应对象不是应用提供的,而是由框架创建的。在使用自定义响应类的应用中,Flask和Werkzeug无法知道自定义类的细节,所以它们使用标准响应类来创建响应。响应类中的force_type()方法,被设计为可以接受不同响应类的实例,并会将其转换成自身的格式。

我敢肯定,你一定被force_type()方法的描述搞糊涂了。说白了,就是如果Flask碰到了一个不是其期望的响应对象,就会使用该方法进行转换。我下面要讲的第三个使用场景,就利用了这个特点,让Flask的路由函数返回诸如字典、列表或者是其他任何自定义对象,作为请求的响应对象。

好了,理论就讲这么多了。接下来,我来告诉大家如何应用上面有关响应类的小技巧。准备好了吗?

使用自定义的响应类

到现在为止,我确定你也会认为:在部分有趣的场景下,使用自定义的响应类是有利的。在给出实际例子之前,我想告诉你在Flask中设置并使用自定义的响应类是多么的简单。请看下面的这个例子:

from flask import Flask, Response

class MyResponse(Response):
    pass

app = Flask(__name__)
app.response_class = MyResponse

# ...

在上面的代码中,我定义了一个名叫MyResponse的自定义响应类。通常,自定义响应类会增加或修改默认类的行为,所以一般都会通过创建Flask中Response类的子类来实现。要想让Flask使用自定义类,我只需要设置app.response_class即可。

Flask类中的response_class是一个类属性,所以我们可以稍微修改上面的例子,创建一个设置了自定义响应类的Flask子类:

from flask import Flask, Response

class MyResponse(Response):
    pass

class MyFlask(Flask)
    response_class = MyResponse

app = MyFlask(__name__)

# ...

例1:更改响应对象的默认值

第一个例子极其简单。假设你的应用中大部分或全部端点(endpoints)都返回的是XML。对于这样的应用,将默认的内容类型设置为application/xml是合理的。可以通过下面这个仅有两行代码的响应类轻松实现:

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

容易,对吧?如果将其设为应用的默认响应类,那么你在编写返回XML的函数时,就不用担心忘记设置内容类型了。举个例子:

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

上面这个路由使用的是默认响应类,其内容类型会被设置为text/html,因为那是默认类型。使用自定义响应类,可以免去你在所有XML路由的返回语句中,额外加上标头的麻烦。另外,如果有的路由需要其他的内容类型,你仍可以替换掉默认值,就像对待一般的响应类一样。

@app.route('/')
def index():
    return '<h1>Hello, World!</h1>', {'Content-Type': 'text/html'}

例2:自动决定内容类型

下一个例子更复杂一点。假设我们的应用中HTML路由与XML路由的数量差不多,所以按照第一个例子的做法就不行了,因为不管你选用哪种默认类型,都会有一半的路由需要替换内容类型。

更好的解决办法,则是创建一个能够通过分析响应文本,决定正确的内容类型的响应类。下面的这个类实现了该功能:

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)

在这个简单的例子中,我首先确保响应对象中没有明确设置内容类型。然后,我检查响应的正文是否以<?xml开头,是的话就意味着数据是XML文档格式。如果两个条件同时成立,我会在传入父类构造函数的参数中,插入XML内容类型。

有了这个自定义响应类,任何满足XML格式要求的文档都会自动被标记为XML内容类型,而其他响应则会继续获得默认的内容类型。而且,在所有的类中,我仍然可以在必要时声明内容类型。

例3:自动返回JSON响应

最后一个例子,针对的是利用Flask创建API接口时常见的一个小问题。API接口通常返回的是JSON净负荷(JSON Payload,这就要求你使用jsonify()函数将Python字典类型转换成JSON数据,并且还得在响应对象中将内容类型设置为JSON内容类型。请看下面这个例子:

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

问题是,每个返回JSON的路由都需要这样处理,那么对接口数量众多的的API来说,你就得大量重复调用jsonify()函数。从代码可读性角度来讲,你按照下面的方式处理是不是更好?

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

下面是一个支持使用上述语法的自定义响应类,它不会影响应用中使用其他内容类型的路由正常工作:

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仅认可一小部分的类型,作为路由函数能够返回的有效响应类型。基本上,你可以返回任意与字符串和二进制相关的类型(strunicodebytesbytearray)。如果你喜欢,甚至可以返回一个已经创建好的响应对象。如果你返回的是字符串或二进制类型,Flask会发现这些是响应类知道如何处理的类型,并会将你返回的数据直接传入响应类的构造函数。

但是,如果你返回的是不支持的类型,比如说上述例子中的字典,会发生什么情况?如果返回的响应类型不是Flask预期的,那么Flask就会默认它是未知响应对象,不会以其为参数创建响应对象了,而是使用响应类的force_type()类方法,强制转换未知类型。上面的例子中,响应子类替换了该方法,但仅仅是通过调用jsonify()进行转换,之后就会让基类接手处理,就好像什么都没发生一样。

是个很好的窍门吧?尤其是这样做不会影响其他响应的正常工作。对于返回正常响应类型的路由,该子类不会做任何处理,所有的调用请求会全部传入父类中。

结语

我希望本文能够帮助大家更好地理解FLask中响应对象的工作原理。如果你知道其他使用Flask响应类的小窍门,请务必与我分享!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,517评论 18 139
  • 22年12月更新:个人网站关停,如果仍旧对旧教程有兴趣参考 Github 的markdown内容[https://...
    tangyefei阅读 35,156评论 22 257
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,810评论 6 13
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,142评论 25 707
  • 我记得, 曾经有只小飞机, 它爱飞, 它爱飞在我的哭闹中, 爱飞到我的摇篮里。 我记得, 曾经有块糖在摇篮里, 它...
    安艺微阅读 284评论 0 0