最近在學習Flask, 其中遇到了一個錯誤, 發現這個問題和Flask, 路由有關系, 所以就記了下來
錯誤代碼:
from flask import Flask, render_template, request, redirect, session app = Flask(__name__) app.secret_key = "wang" def confirm(func): # 負責確認用戶有沒有登陸的裝飾器 def inner(*args, **kwargs): if session.get("auth"): # 判斷用戶的session中沒有user return func(*args, **kwargs) # 通過 else: # 跳轉登陸頁面, 並攜帶當前訪問的url next_url = request.path return redirect(f'/login?next={next_url}') return inner @app.route('/') @confirm def index(): return "index" @app.route('/login', methods=["GET", "POST"]) def login(): msg = '' if request.method == "POST": auth = request.form.get('auth') if auth == 'wang': # 簡單認證 session['auth'] = auth # 設置session next_url = request.args.get('next_url', "/") # 獲取用戶之前訪問的url, 進行跳轉 return redirect(next_url) else: msg = "口令錯誤" return render_template("login.html", msg=msg) @app.route('/shopping') @confirm def shopping(): return "購物" if __name__ == '__main__': app.run(debug=True)
報錯:
詭異的是, 我不啟動flask, 只是解釋一遍, 也會報錯
報錯分析
分析報錯提示
根據報錯的提示, 說我的代碼存在重復的函數, 然后就開始檢查我的函數, 發現函數名並沒有重復, 難道就這樣排除函數名的嫌疑嗎? NONONO
可能是我對裝飾器的理解還不夠, 找了好半天才發現這個問題, 原來是裝飾器的原因, 為什么呢?
為什么說是因為裝飾器, 才會出現函數覆蓋的問題?
再來溫習一下裝飾器
def test(func): # 裝飾器 """ test :param func: 其實就是要裝飾的函數 :return: """ def inner(*args, **kwargs): start = time.time() func(*args, **kwargs) end = time.time() return end - start return inner def outer(a, b): # 被裝飾的函數 for n in range(a): for j in range(b): a = n + j outer = test(outer) # 這里因為使用語法糖, 這種方式更能表示出問題 # 在這一步可以說對outer進行了重新的賦值, # 現在outer就等於test這個函數的返回值, 並且將原本的outer傳了進去 # test函數的返回值是一個inner # 在inner函數中就包括了原本的outer, 並且這個outer在inner函數中是加了括號的 # 也就是說, 當inner被調用的時候, 原本的outer也會被調用 # 剛剛說test函數返回的是inner函數 # 當outer = test(outer)執行完之后, 新的outer就等於inner了 # 到這只需要知道現在的outer一樣不是原來的outer了, 而是指向了inner, 在inner內部調用原來的outer print(outer(100000, 200)) # 這是調用函數, 不能改變這個調用方式
再來看flask中app.route中的源碼
flask使用裝飾器來綁定一個url和視圖的關系, 帶着遇到的問題來看看源碼中做了些什么
@app.route('/shopping') ① @confirm def shopping(): return "購物" def route(self, rule, **options): def decorator(f): endpoint = options.pop('endpoint', None) # ② 從參數中彈出endpoint, 沒有的話就是None self.add_url_rule(rule, endpoint, f, **options) # ③ return f @setupmethod def add_url_rule(self, rule, endpoint=None, view_func=None, provide_automatic_options=None, **options): if endpoint is None: # ④ endpoint如果為None endpoint = _endpoint_from_view_func(view_func) # ⑤ 將試圖視圖函數傳了進去, 返回視圖函數的__name__ def _endpoint_from_view_func(view_func): assert view_func is not None, 'expected view func if endpoint is not provided.' ~~~~ 其實執行的就是 inner.__name__, 因為此時的shopping, 已經不是原來的shopping, 而是裝飾器內部返回的函數 return view_func.__name__ # ⑥ 因為沒有定義endpoint, 所以在返回視圖函數的名字
看上面的代碼應該就知道是哪里重復了, 原因就是, 都是調用了inner.__name__, 所以拿到的endpoint值都是一樣的才會提示重復了函數名
就是源碼中的這一段拋出的異常
if view_func is not None: old_func = self.view_functions.get(endpoint) if old_func is not None and old_func != view_func: raise AssertionError('View function mapping is overwriting an ' 'existing endpoint function: %s' % endpoint) self.view_functions[endpoint] = view_func
如何解決這個問題
方法一
根據源碼可以看看出是因為, endpoint重復才導致的, 當endpoint為None時, 就會調用inner.__name__, 就會導致重復, 那么我們不讓endpoint為空, 那就不會調用inner.__name__了
也就不會出現重復的問題了.
現在來看看在哪里可以定義這個endpoint.
還記得源碼中是從哪里去endpoint的嗎, 是不是下面這里
endpoint = options.pop('endpoint', None)
是從option中pop出去的, option就是route(self, rule, **options)的一個形參, 也就是說你在使用app.route的時候可以傳一個endpoint的鍵值對
只需要對兩個視圖的route裝飾器添加一個參數即可, 看代碼吧, 紅色部分是新添加的
@app.route('/', endpoint="index") @confirm def index(): return "index" @app.route('/shopping', endpoint="shopping") @confirm def shopping(): return "購物"
這樣既可以了, 不信你試試
方法二
使用functools.wraps
不太推薦這種方法, 因為flask本身的不穩定性, 所以要盡可能的少用第三方的模塊, 下面只提供了代碼
def confirm(func, *args, **kwargs): @wraps(func) def inner(): if session.get('user'): return func(*args, **kwargs) else: next_url= request.path return redirect('/login?next=%s' % (next_url,)) return inner