Github上最受歡迎的Python輕量級框架Flask入門

老錢發表於2018-06-06

Github上最受歡迎的Python輕量級框架Flask入門
flask最近終於釋出了它的1.0版本更新,從專案開源到最近的1.0版本flask已經走過了8個年頭。

# app.py
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run()
複製程式碼

執行python app.py,開啟瀏覽器訪問http://localhost:5000/就可以看到頁面輸出了Hello World!

flask的誕生於2010年的愚人節,本來它只是作者無意間寫的一個小玩具,沒想到它卻悄悄流行起來了。漫長的8年時間,flask一直沒有釋出一個嚴肅的正式版本,但是卻不能阻擋它成了github上最受好評的Python Web框架。

flask核心內建了兩個最重要的元件,所有其它的元件都是通過易擴充套件的外掛系統整合進來的。這兩個內建的元件分別是werkzeug和jinja2。

Github上最受歡迎的Python輕量級框架Flask入門

werkzeug是一個用於編寫Python WSGI程式的工具包,它的結構設計和程式碼質量在開源社群廣受褒揚,其原始碼被尊為Python技術領域最值得閱讀的開源庫之一。

# wsgi.py
from werkzeug.wrappers import Request, Response

@Request.application
def application(request):
    return Response('Hello World!')

if __name__ == '__main__':
    from werkzeug.serving import run_simple
    run_simple('localhost', 4000, application)
複製程式碼

執行python wsgi.py開啟瀏覽器訪問http://localhost:4000/就可以看到頁面輸出了Hello World!

Have you looked at werkzeug.routing? It's hard to find anything that's simpler, more self-contained, or purer-WSGI than Werkzeug, in general — I'm quite a fan of it!

by Alex Martelli, the author of 《Python in a Nutshell》 && 《Python Cookbook》

Github上最受歡迎的Python輕量級框架Flask入門

jinja2是一個功能極為強大的模板系統,它完美支援unicode中文,每個模板都執行在安全的沙箱環境中,使用jinja2編寫的模板程式碼非常優美。

{% extends "layout.html" %}
{% block body %}
  <ul>
  {% for user in users %}
    <li><a href="{{ user.url }}">{{ user.username }}</a></li>
  {% endfor %}
  </ul>
{% endblock %}
複製程式碼

werkzeug和jinja2這兩個庫的共同特點是編寫的程式碼賞心悅目,作者Armin Ronacher選擇這兩個庫來作為flask的基石說明作者有非常挑剔的程式碼品味。那麼作者是誰呢,鐺!他是一位來自澳大利亞的帥哥!

Github上最受歡迎的Python輕量級框架Flask入門

好,閒話少說言歸正傳,接下來我們開始體驗flask的神奇魅力。

安裝flask

pip install flask

圓周率計算API

Github上最受歡迎的Python輕量級框架Flask入門

圓周率可以使用正整數的平方倒數之和求得,當這個級數趨於無限時,值會越來越接近圓周率。

# flask_pi.py
import math

from flask import Flask, request

app = Flask(__name__)

@app.route("/pi")
def pi():
    # 預設引數
    n = int(request.args.get('n', '100'))
    s = 0.0
    for i in range(1, n):
        s += 1.0/i/i
    return str(math.sqrt(6*s))

if __name__ == '__main__':
    app.run()
複製程式碼

執行python flask_pi.py,開啟瀏覽器訪問http://localhost:5000/pi?n=1000000,可以看到頁面輸出3.14159169866,這個值同圓周率已經非常接近。

注意pi()的返回值不能是浮點數,所以必須使用str轉換成字串

再仔細觀察程式碼,你還會注意到一個特殊的變數request,它看起來似乎是一個全域性變數。從全域性變數裡拿當前請求引數,這非常奇怪。如果在多執行緒環境中,該如何保證每個執行緒拿到的都是當前執行緒正在處理的請求引數呢?所以它不能是全域性變數,它是執行緒區域性變數,執行緒區域性變數外表上和全域性變數沒有差別,但是在訪問執行緒區域性變數時,每個執行緒得到的都是當前執行緒內部共享的物件。

快取計算結果

為了避免重複計算,我們將已經計算的pi(n)值快取起來,下次就可以直接查詢。同時我們不再只返回一個單純的字串,我們返回一個json串,裡面有一個欄位cached用來標識當前的結果是否從快取中直接獲取的。

import math
import threading

from flask import Flask, request
from flask.json import jsonify

app = Flask(__name__)


class PiCache(object):

    def __init__(self):
        self.pis = {}
        self.lock = threading.RLock()

    def set(self, n, pi):
        with self.lock:
            self.pis[n] = pi

    def get(self, n):
        with self.lock:
            return self.pis.get(n)


cache = PiCache()


@app.route("/pi")
def pi():
    n = int(request.args.get('n', '100'))
    result = cache.get(n)
    if result:
        return jsonify({"cached": True, "result": result})
    s = 0.0
    for i in range(1, n):
        s += 1.0/i/i
    result = math.sqrt(6*s)
    cache.set(n, result)
    return jsonify({"cached": False, "result": result})

if __name__ == '__main__':
    app.run()
複製程式碼

執行python flask_pi.py,開啟瀏覽器訪問http://localhost:5000/pi?n=1000000,可以看到頁面輸出

{
  "cached": false,
  "result": 3.141591698659554
}
複製程式碼

再次重新整理頁面,我們可以觀察到cached欄位變成了true,說明結果確實已經快取了

{
  "cached": true,
  "result": 3.141591698659554
}
複製程式碼

讀者也許會問,為什麼快取類PiCache需要使用RLock呢?這是因為考慮到多執行緒環境下Python的字典讀寫不是完全執行緒安全的,需要使用鎖來保護一下資料結構。

分散式快取

上面的快取僅僅是記憶體快取,程式重啟後,快取結果消失,下次計算又得重新開始。

if __name__ == '__main__':
    app.run('127.0.0.1', 5001)
複製程式碼

如果開啟第二個埠5001來提供服務,那這第二個程式也無法享受第一個程式的記憶體快取,而必須重新計算。所以這裡要引入分散式快取Redis來共享計算快取,避免跨程式重複計算,避免重啟重新計算。

import math
import redis

from flask import Flask, request
from flask.json import jsonify

app = Flask(__name__)


class PiCache(object):

    def __init__(self, client):
        self.client = client

    def set(self, n, result):
        self.client.hset("pis", str(n), str(result))

    def get(self, n):
        result = self.client.hget("pis", str(n))
        if not result:
            return
        return float(result)


client = redis.StrictRedis()
cache = PiCache(client)


@app.route("/pi")
def pi():
    n = int(request.args.get('n', '100'))
    result = cache.get(n)
    if result:
        return jsonify({"cached": True, "result": result})
    s = 0.0
    for i in range(1, n):
        s += 1.0/i/i
    result = math.sqrt(6*s)
    cache.set(n, result)
    return jsonify({"cached": False, "result": result})

if __name__ == '__main__':
    app.run('127.0.0.1', 5000)
複製程式碼

執行python flask_pi.py,開啟瀏覽器訪問http://localhost:5000/pi?n=1000000,可以看到頁面輸出

{
  "cached": false,
  "result": 3.141591698659554
}
複製程式碼

再次重新整理頁面,我們可以觀察到cached欄位變成了true,說明結果確實已經快取了

{
  "cached": true,
  "result": 3.141591698659554
}
複製程式碼

重啟程式,再次重新整理頁面,可以看書頁面輸出的cached欄位依然是true,說明快取結果不再因為程式重啟而丟失。

MethodView

寫過Django的朋友們可能會問,Flask是否支援類形式的API編寫方式,回答是肯定的。下面我們使用Flask原生支援的MethodView來改寫一下上面的服務。

import math
import redis

from flask import Flask, request
from flask.json import jsonify
from flask.views import MethodView

app = Flask(__name__)


class PiCache(object):

    def __init__(self, client):
        self.client = client

    def set(self, n, result):
        self.client.hset("pis", str(n), str(result))

    def get(self, n):
        result = self.client.hget("pis", str(n))
        if not result:
            return
        return float(result)


client = redis.StrictRedis()
cache = PiCache(client)


class PiAPI(MethodView):

    def __init__(self, cache):
        self.cache = cache

    def get(self, n):
        result = self.cache.get(n)
        if result:
            return jsonify({"cached": True, "result": result})
        s = 0.0
        for i in range(1, n):
            s += 1.0/i/i
        result = math.sqrt(6*s)
        self.cache.set(n, result)
        return jsonify({"cached": False, "result": result})


# as_view提供了引數可以直接注入到MethodView的構造器中
# 我們不再使用request.args,而是將引數直接放進URL裡面,這就是RESTFUL風格的URL
app.add_url_rule('/pi/<int:n>', view_func=PiAPI.as_view('pi', cache))


if __name__ == '__main__':
    app.run('127.0.0.1', 5000)
複製程式碼

我們實現了MethodView的get方法,說明該API僅支援HTTP請求的GET方法。如果要支援POST、PUT和DELETE方法,需要使用者自己再去實現這些方法。

flask預設的MethodView挺好用,但是也不夠好用,它無法在一個類裡提供多個不同URL名稱的API服務。所以接下來我們引入flask的擴充套件flask-classy來解決這個問題。

小試flask擴充套件flask-classy

使用擴充套件的第一步是安裝擴充套件pip install flask-classy,然後我們在同一個類裡再加一個新的API服務,計算斐波那契級數。

Github上最受歡迎的Python輕量級框架Flask入門

import math
import redis

from flask import Flask
from flask.json import jsonify
from flask_classy import FlaskView, route  # 擴充套件

app = Flask(__name__)

# pi的cache和fib的cache要分開
class PiCache(object):

    def __init__(self, client):
        self.client = client

    def set_fib(self, n, result):
        self.client.hset("fibs", str(n), str(result))

    def get_fib(self, n):
        result = self.client.hget("fibs", str(n))
        if not result:
            return
        return int(result)

    def set_pi(self, n, result):
        self.client.hset("pis", str(n), str(result))

    def get_pi(self, n):
        result = self.client.hget("pis", str(n))
        if not result:
            return
        return float(result)


client = redis.StrictRedis()
cache = PiCache(client)


class MathAPI(FlaskView):

    @route("/pi/<int:n>")
    def pi(self, n):
        result = cache.get_pi(n)
        if result:
            return jsonify({"cached": True, "result": result})
        s = 0.0
        for i in range(1, n):
            s += 1.0/i/i
        result = math.sqrt(6*s)
        cache.set_pi(n, result)
        return jsonify({"cached": False, "result": result})

    @route("/fib/<int:n>")
    def fib(self, n):
        result, cached = self.get_fib(n)
        return jsonify({"cached": cached, "result": result})

    def get_fib(self, n): # 遞迴,n不能過大,否則會堆疊過深溢位stackoverflow
        if n == 0:
            return 0, True
        if n == 1:
            return 1, True
        result = cache.get_fib(n)
        if result:
            return result, True
        result = self.get_fib(n-1)[0] + self.get_fib(n-2)[0]
        cache.set_fib(n, result)
        return result, False


MathAPI.register(app, route_base='/')  # 註冊到app


if __name__ == '__main__':
    app.run('127.0.0.1', 5000)
複製程式碼

訪問http://localhost:5000/fib/100,我們可以看到頁面輸出了

{
  "cached": false,
  "result": 354224848179261915075
}
複製程式碼

訪問http://localhost:5000/pi/10000000,計算量比較大,所以多轉了一回,最終頁面輸出了

{
  "cached": false,
  "result": 3.141592558095893
}
複製程式碼

閱讀更多高階文章,關注微信訂閱號「碼洞

擴充套件閱讀

廖雪峰教你ThreadLocal的正確用法

Python字典是否是執行緒安全的

Hello Flask知乎專欄

相關文章