Manjusaka

Manjusaka

菜鳥閱讀 Flask 源碼系列(1):Flask的router初探

前言#

沒有一個完整的開源項目的閱讀經驗的程序猿是一個不合格的程序猿,雖然曾經閱讀過部分諸如 Redis 等項目的源碼,但是還沒有過一個完整的開源項目的閱讀經驗,因此在經過某個前輩的不斷安利後,我決定用 Flask 來作為閱讀開源源碼計劃的開始。而這一個系列的文章,將作為我自己的閱讀筆記,來鞏固自己曾經所沒有重視的 Python 的很多細節。

關於 Flask#

關於 Flask 的背景知識,就不需要太多的描述了,網上已經有很多的資料了。在使用 Flask 的時候,我們經常用如下的方式來設置我們的自定義的路由:

##Flask官方Example中flaskr項目部分代碼
app = Flask(__name__)

@app.route('/')
def show_entries():
    db = get_db()
    cur = db.execute('select title, text from entries order by id desc')
    entries = cur.fetchall()
    return render_template('show_entries.html', entries=entries)


@app.route('/add', methods=['POST'])
def add_entry():
    if not session.get('logged_in'):
        abort(401)
    db = get_db()
    db.execute('insert into entries (title, text) values (?, ?)',
                 [request.form['title'], request.form['text']])
    db.commit()
    flash('New entry was successfully posted')
    return redirect(url_for('show_entries'))


@app.route('/login', methods=['GET', 'POST'])
def login():
    error = None
    if request.method == 'POST':
        if request.form['username'] != app.config['USERNAME']:
            error = 'Invalid username'
        elif request.form['password'] != app.config['PASSWORD']:
            error = 'Invalid password'
        else:
            session['logged_in'] = True
            flash('You were logged in')
            return redirect(url_for('show_entries'))
    return render_template('login.html', error=error)

那麼問題來了,上面的例子中,我們知道 app.route('xxxx',methods=['xxx']) 將會設置我們對應的方法與對應 url 的關聯,那麼這樣一種做法是怎樣生效的呢?

Flask 源碼閱讀#

讓我們看看最開始的 router 是什麼樣子的#

首先讓我們從 Flask 這裡獲取 flask 源碼,然後我們將版本號切換至最初的 0.1 版(git tag 為 8605cc310d260c3b08160881b09da26c2cc95f8d)

小 tips:閱讀開源項目時,如果當前版本太過於複雜,可以切換至項目最初發布時的版本,然後根據每次項目版本發布的 Release Note 來進行跟進。

flask.py 文件裡,我們能看到如下的的結構

講真這個時候我們就可以看到 Flask 裡的 route 的核心代碼了

def route(self, rule, **options):
    def decorator(f):
        self.add_url_rule(rule, f.__name__, **options)
        self.view_functions[f.__name__] = f
        return f
    return decorator

我們能很清楚的看到之前代碼中的 app.route('/') 本質上是調用了一个裝飾器來對我們對應的方法進行請求與方法之間進行關聯。在 route 方法被觸發後,進一步來調用 add_url_rule 來註冊我們所設定的 url。

def add_url_rule(self, rule, endpoint, **options):
    options['endpoint'] = endpoint
    options.setdefault('methods', ('GET',))
    self.url_map.add(Rule(rule, **options))

在最初版本的 Flask 中, Router 的實現就這麼簡單暴力

兩個關於裝飾器的知識點#

第一個:很多人肯定想問,在之前的代碼裡,我們沒有調用相關的方法,例如 index() ,那麼裝飾器為什麼會被觸發呢?

答:首先,請大聲告訴我,裝飾器的作用是什麼?很明顯嘛,在不修改原有代碼的基礎上,對函數進行一次封裝,然後實現為原有方法增加一些功能的特殊實現。是不是感覺很抽象?來我們看個例子

def testDe1(func):
    def de(a, b, c):
        func(a, b, c)
        print('1')
    print('2')
    return de

@testDe1
def test2(a, b, c):
    print(a+b+c)
if __name__ == '__main__':
    test2(1,2,3)

來,告訴我,這段代碼的輸出應該是什麼?答案是 2,6,1,看到這裡,你是不是感覺似乎明白了些什麼?是的沒錯,上面的例子其實等價於

def testDe1(func):
    def de(a, b, c):
        func(a, b, c)
        print('1')
    print('2')
    return de

def test2(a, b, c):
    print(a+b+c)

if __name__ == '__main__':
    testDe1(test2)(1,2,3)

那麼我們換個例子

def testDe1(func):
    def de(a, b, c):
        func(a, b, c)
        print('1')
    print('2')
    return de

@testDe1
def test2(a, b, c):
    print(a+b+c)
if __name__ == '__main__':
    pass

這段代碼的輸出會是什麼?是的沒錯,這段代碼的輸出是 2 。看到這裡你是不是感覺更明白些什麼?恩,在 Python 中,使用函數裝飾器的時候,等於先行調用了裝飾函數一次,具體來講在使用裝飾器後,裝飾器會用裝飾後的函數來進行一個替換,即在什麼也不做的情況下,會產生這樣一個調用 test2=testDe1(test2),接著如果在 __main__ 中添加一段代碼 test2(1,2,3),是不是就等價於 testDe1(test2)(1,2,3) 。看到這裡是不是徹底明白了?
恩,來,我們再來複習下前面的例子


@app.route('/')
def show_entries():
    db = get_db()
    cur = db.execute('select title, text from entries order by id desc')
    entries = cur.fetchall()
    return render_template('show_entries.html', entries=entries)

上面這段代碼裡發生了什麼?是不是有一個調用為 show_entries=app.route('/')(show_entries)?看到這裡是不是很清楚了呢?

第二個,在第一個小 tip 的基礎之上,我們來講一個關於裝飾器傳參的問題
可能很多人不清楚裝飾器傳參的使用情景,首先如前面所說裝飾器的最根本的作用在於

在不修改原有代碼的基礎上,對函數進行一次封裝,然後實現為原有方法增加一些功能的特殊實現

現在假設我們需要對函數的運行時間進行輸出,這個時候我們該怎麼辦

def testTime(func):
    def dec(*args,**kwargs):
        flag=time.time()
        func(*args,**kwargs)
        print(time.time()-flag)
    return dec

def func():
    pass

if __name__=='__main__':
    func()

如前所述,前面這段代碼等價於 test(func)(), 那麼這個時候我們想給我們時間輸出以一定的單位進行格式化怎辦,修改上面裝飾器代碼如下

def testtime(time=None):
    def dec1(func):
        def dec2(*args,**kwargs):
            flag=time.time()
            func(*args,**kwagrs)
            flag2=time.time()
            if time is not None:
                print((flag2-flag)/time)
            else:
                print(flag2-flag)
        return dec2
    return dec1

寫到這裡,大家是不是明白了帶參數的裝飾器的使用情景呢?

後記#

Flask 的路由系統相對簡單,其本質是利用帶參數的裝飾器來進行相應的路由記錄,同時利用裝飾器的包裝特性,將我們的對應的處理函數進行包裝,同時加入路由表中,一旦觸發我們所註冊的路由,便可調用我們所對應的處理函數。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。