flask 高级编程
一. flask基本原理
视图函数
定义视图函数©
通过装饰器来给函数定义一个路由,从而可以通过http请求来访问到这个函数;
基于函数的视图很难去实现代码的复用;
from flask import Flask
app = Flask(__name__)
@app.route('/hello')
def hello()
return 'hello'
app.run()
唯一url原则
需要保证唯一url,不加/也可以保证唯一url,但是不兼容用户的输入,所以可以在规则中兼容用户的输入:
@app.route('/hello/')
新更改后的代码必须重启下服务器,将新更改的代码生效;
唯一url原则的本质-重定向
兼容带不带/的原理:处理不同的url的实质是做了一次重定向,重定向的原理是浏览器与服务器之间的访问:
-
在正常的访问流程中,浏览器访问url1访问的资源,服务器在接受到请求后,把url1的资源返回到浏览器,并且将状态码设置为200;
- 但在重定向过程中,浏览器访问url1的资源,出于某种原因,服务器没有url1或者不想让你访问url1,此时会在服务器返回给浏览器的信息中的location字段设置为url2的地址,并且把返回的状态码改为301或者302,到浏览器接受时,会根据状态码来获取重定向url的信息,并再次向url2发送请求。
开启flask服务器自动重启
将flask的模式设置为调试模式,就可以使flask服务器开始自动重启。打开调试模式的方法就是在app.run的方法内传入debug参数,并将其设置为true:
app.run(debug=True)
开启调试模式的另一个好处是其可以把运行的异常来进行访问;
路由注册
方法一通过装饰器来注册(装饰器不用添加view_func参数)(推荐使用)
@app.route('/hello')
def hello()
return 'hello'
方法二通过app的方法来进行注册(如果是使用即插视图,那么就要使用add的方式来进行注册)
def hello()
return 'hello'
# 第一个参数指定路由链接,第二个参数指定视图函数
app.add_url_rule('/hello',view_func=hello)
两种方法的区别就是通过装饰器方法来进行路由注册无需指定view_func,因为装饰器本身就是定义在要装饰的函数的前面。如果是使用基于类的视图,即即插视图的方法,则需要使用add_url_rule来进行注册。
app.run的相关参数
app.run默认服务器运行的,只能是本地或局域网内的机器能够访问,可以通过app.run方法中的host属性,来对外网中的url进行访问。
# host如果定为固定的ip地址,则只有那一个可以访问
app.run(host='0.0.0.0',debug=True,port=81)
flask配置文件
部署的最基本原则,就是要保证我们开发环境下的源代码和生产环境下的源代码一定要是镜像关系,即两份源代码一样。
DEBUG = True
方法一:使用配置文件时,将其当作模块导入进来即可:
from config import DEBUG
app.run(host='0.0.0.0',debug=DEBUG,port=81)
方法二:使用app方法来进行导入:
# 配置文件载入:方法需要接受一个模块的路径
app.config.from_object('config')
# 配置文件读取:
# config本身就是字典dict的一个子类
app.run(host='0.0.0.0',debug=app.config['DEBUG'],port=81)
注:如果是采用from_object来载入的话,要求DEBUG需要大写。但是如果你在配置文件中没有对DEBUG参数进行定义时,运行也不会报错,因为DEBUG在flask中的默认值为false。
if__name__ == 'main’的含义:
在开发环境下,运行的服务器是flask自带的非常简单的服务器,但是将项目部署到生产环境时,我们通常是不会使用flask自带的服务器的,通常使用nginx+uwsgi,nginx作为前置服务器用来接受浏览器发来的请求,接着会将请求转发给我们的uwsgi。在项目中,不是通过run直接执行程序,而是通过uwsgi加载fisher模块来启动flask的相关代码。
if __name__ == '__main__':
app.run(host='0.0.0.0',bebug=app.config['DEBUG'],port=81)
在生产环境下fisher文件不再是入口文件,它只是被uwsgi加载的模块文件,所以在生产环境下,app.run是根本不会被执行的。如果没有了name=main的判断,在生产环境下一旦加载了fisher文件之后,app.run就会执行,我们已经启动了uwsgi作为我们的服务器,接着我们又启动了app.run作为我们的内置服务器,两个服务器是不可以的。但是加入了main之后,我们可以保证在生产环境下不会启动flask自带的web服务器。(测试环境下运行才能进入到main函数)
响应对象Response
视图函数的return和普通函数的return有什么区别吗?
视图函数的return,flask会在其背后做一系列的封装,即除了返回定义的字符串的内容之外,还会返回status code,content-type;
content-type是放置于http的headers的属性中,content-type告诉http请求的接收方该如何解析我们的主体内容。
对于视图函数,content-type默认值为"text/html",其会指导浏览器将html标签当作html来进行解析。
当你在视图函数中返回一个很简单的字符串时,flask会将字符串当作响应的主体内容,同时把主题内容封装为Response对象,所以对于视图函数而言,返回的永远都是一个Response对象。
from flask import Flask,make_response
def hello():
# 修改content-type让其显示
headers = {
'content-type':'text/plain'
}
response = make_response('<html></html>',404)
response.headers = headers
return response
web返回的本质都是字符串,其控制的本质是content-type;
也可以不去自己创建response对象:
from flask import Flask,make_response
def hello():
return '<html></html>',301
二. 数据与flask路由
基本目标与功能
目标是完成:作为一个赠送书的人,你到底要赠送哪本书;
书籍搜索的功能包括两部分,一个是关键字搜索,另一个是根据isbn号进行检索。而书籍的书籍信息不是本地的数据源,而是依赖于外部的数据源,通过外部的API来获取数据。
考虑客户端传递参数
需要传递什么参数
思考下客户端需要传递给我们什么样的参数,客户端传递给我们的参数最终要使用到外部的API,所以首先要看外部的API需要接受什么样的参数。
那么在这里,关键字搜索的API接口为:
http://t.yushu.im/v2/book/search?q={}&start={}&count={}
isbn搜索的接口为:
http://t.yushu.im/v2/book/isbn/{isbn}
区分不同的搜索接口的方法
了解过接口后,那是否需要用户来通过区分不同的搜索呢?即用户主观选择使用isbn或关键字搜索后,再输入进行搜索。显然这样是不必要的,我们可以通过自己内部的逻辑判断处理,统一化搜索接口。
在视图函数中接受用户传来的参数
可以先通过最简单的方式,即通过url路径的方式来进行参数传递,通过在路由中定义<>的形式,就可以将内容识别为参数而不是固定的字符串。
注:在书写多条件的语句时,有两个原则:第一个原则是:应该把很大概率出现假的条件放在前面,这样一旦前面判断出是假的之后,后面的判断就不会再执行了,从而让代码执行效率加快;第二个原则是:把比较耗时的操作放在后面;
@app.route('/book/searc h/<q>/<page>')
def search(q,page):
# q:普通关键字,isbn关键字
# page
#isbn有isbn13和isbn10两种方式:13表示由13个0-9 之间的数字组成,isbn10表示是10个0到9数字组成,含有一些'-';
isbn_or_key = 'key'
if len(q) == 13 and q.isdigit():
isbn_or_key = 'isbn'
short_q = q.replace('-','')
if '-' in q and len(short_q) == 10 and short_q.isdigit:
isbn_or_key = 'isbn'
pass
代码重构
上述代码不好的地方:
-
条件逻辑判断语句太多;
-
无法实现复用;
-
通常读代码都是从控制器或者是视图函数开始的,所以视图函数或控制器应该把这块是做什么的体现出来;
那么比较好的解决方法就是将这些逻辑功能风爪给你为一个函数,然后在视图函数中调用它。
在包中新建模块helper.py文件,构造方法:
def is_isbn_or_key(word)
isbn_or_key = 'key'
if len(q) == 13 and q.isdigit():
isbn_or_key = 'isbn'
short_q = q.replace('-','')
if '-' in q and len(short_q) == 10 and short_q.isdigit:
isbn_or_key = 'isbn'
return isbn_or_key
然后在fisher.py文件中调用这个逻辑函数:
from helper import is_isbn_or_key
视图函数是开始一个web项目的起点,所以一定要保证视图函数里的代码是简洁宜读的。
看源代码要分层的去看,第一层就是知道函数做什么就够了,理清楚整个源代码的结构和线索。
requests发送http请求及代码简化手段
在py中访问API,即向url发送http请求,我们可以定义http模块,在模块中封装类来实现这个功能。
鱼书的API还是按照restful的标准做的API,凡是restful的API都遵循着一定的规律。
在同模块下定义http.py文件
from urllib import request
import requests
class HTTP:
def get(self, url, return_json=True):
r = requests.get(url)
if r.status_code == 200:
if return_json
return r.json()
else:
return r.text
else:
if return_json:
return {}
else:
return ''
简化写法:
class HTTP:
def get(self, url, return_json=True):
r = requests.get(url)
if r.status_code != 200:
return {} if rerurn_json else ''
return r.json() if return_json else r.text
不要直接直白的去写if-else,这样会让代码显得过于冗长,总结下简化if-else的方法:
-
利用三元表达式来简化if-else的写法;
-
使用if+return来简化代码的写法;
if+2个return中后面的return可以理解为正常流程中的一种特例处理情况;
在视图函数中调用http请求类
在视图函数中平铺的写业务逻辑代码并不是一个很好的习惯,所以可以把整个的请求过程封装为一个类:在fisher包下新建一个模块文件yushu_book.py,并建新类YuShuBook,接着把业务逻辑封装到这个类中。
因为查询时有关键字查询和isbn查询,而这两个查询的方法又都不一样,因此我们可以定义两个方法来完成。
既然定义了一个类,那么它就有职责自身去完成API的请求,所以对方法来传入参数并不是一个很好的做法(search_by_isbn(self,url)不好)。那么我们就可以把url定义在类属性或者是类变量里进行处理,在这里如果我们用实例方法来进行考虑,则self这个参数本身没有意义,因此我们可以将其改为静态方法。
from http import HTTP
class YuShuBook:
isbn_url = 'http://t.yushu.im/v2/book/isbn/{}'
keyword_url = 'http://t.yushu.im/v2/book/search?q={}&start={}&count={}'
@classmethod
def search_by_isbn(cls, isbn):
# 通过类名访问类变量
url = YuShuBook.isbn_url.format(isbn)
# 定义为类方法后,就可以通过cls来访问方法或变量
url = cls.isbn_url.format(isbn)
result = HTTP.get(url)
return result
@classmethod
def search_by_keyword(cls,keyword,count=15,start=0):
url = YuShuBook.keyword_url.format(keyword,count,start)
result = HTTP.get(url)
return result
接下来在视图函数中使用我们封装的两个方法:
(通过alt+enter键来进行快速导入提示的包)
def search(q,page):
isbn_or_key = is_isbn_or_key(q)
if isbn_or_key == 'isbn':
result = YuShuBook.search_by_isdn(q)
else:
result = YuShuBook.search_by_keyword(q)
# dict序列化
return json.dumps(result),200,{'content-type':'application/json'}
jsonify
前面在传递json的内容比较冗长,这里我们用flask特定的jsonify的方法来对json的内容进行处理,这有点类似于API的功能:把数据通过json格式返回到客户端去。
return jsonify(result)
将视图函数拆分到单独的文件中
不推荐把所有视图函数放在一个文件中:
- 放到一个文件中,代码太长,不容易维护;
- 从业务模型的角度来讲,不同的业务模型应该分配到不同的文件中去;
现在我们来设计下文件的目录结构:
-fisher
fisher.py
– app
— web
---- book.py
接着把视图函数移到我们的web目录下面来,对于web.py:
@app.route('/book/searc h/<q>/<page>')
def search(q,page):
isbn_or_key = is_isbn_or_key(q)
if isbn_or_key == 'isbn':
result = YuShuBook.search_by_isdn(q)
else:
result = YuShuBook.search_by_keyword(q)
# dict序列化
return jsonify(result)
那么现在有一个问题,我们如何把核心对象app导入到book这个文件来使用呢?
我们回顾下之前的思路:之前把视图函数放在fisher,py这个启动文件中,然后我们在启动文件里实例化了flask核心对象,接着就可以直接在下面使用我们的核心对象。但是现在因为移动了文件,没有办法再直接使用核心对象了,那么接下来我们的考虑是:
- 把启动文件的核心对象导入到book中来使用(结果->会报404错误)
但是这里一定要清楚的一点是,我们是以fisher.py作为启动文件的,而不是book.py文件,那么在book中引用核心对象,只能保证book中关联了fisher,但是在fisher运行时,book文件依然会得不到执行。所以执行后仍然会报404错误。 - 再把视图函数导入启动文件的模块(结果->依然404错误)
在fisher.py中实例化flask对象之后加上语句:from app.web import book,启动文件确实会运行了视图函数,但是仍然会报404错误。
# fisher.py
from flask import jsonify
from helper import is_isbn_or_key
from yushu_book import YuShuBook
from fisher import app
app = Flask(__name__)
app.config.from_object('config')
from app.web import book
if __name__ == '__main__':
print(id(app))
app.run(host='0.0.0.0', debug=app.config['DEBUG'], port=81)
清楚这个404的错误问题,我们需要对flask路由机制有一个深入的了解:
首先,对于客户端(或说postman),我们是如何通过url地址,最终访问到Search这个视图函数的。很容易想到的就是通过字典来映射,即字典的key表示的是url,字典的value表示的是视图函数。
而flask除了url和对应的视图函数之外,中间还有一个endpoint。endpoint是用来反向构造url的。
如何才能算成功的注册路由呢?首先urlmap,这个map对象中必须有我们的search的一 个指向,同时view-functions必须要记录search on point它所指向的一个视图函数。这样当一个请求进到flask的内部,它才能通过url最终找到我们的视图函数,从而调用视图函数。
出现第二个404错误的原因在于,我们在调用时是一个循环引用。