本来是在写个人网站,以前的代码中,几乎每个视图函数都有类似于:
@app.route("/")
def index():
try:
return send_file("index.html")
except FileNotFoundError as e:
abort(404)
我就在想怎么能减少重复书写同样的代码,于是想到了装饰器。
def not_found(func):
def wrapper(*args, **kwargs):
print("进入装饰器")
try:
func(*args, **kwargs)
except:
print("失败!文件未找到")
return "失败!文件未找到"
return wrapper
@app.route("/", methods=["GET"])
@not_found
def index():
return send_file("index.html")
写到这里还没什么问题,但当我再加一个视图函数,同样也使用了这个@not_found装饰器的时候,报错出现了。
AssertionError: View function mapping is overwriting an existing endpoint function: wrapper
解决办法网上很容易搜到,
1. 第一种是给装饰器加上@wraps()
from functools import wraps
def not_found(func):
@wraps(func) # 新增的代码
def wrapper(*args, **kwargs):
# 省略装饰器内容
return wrapper
@app.route("/", methods=["GET"])
@not_found
def index():
return send_file("index.html")
@app.route("/data", methods=["GET"])
@not_found
def data():
return send_file("data.html")
@app.route()中加上endpoint参数def not_found(func):
@wraps(func) # 新增的代码
def wrapper(*args, **kwargs):
# 省略装饰器内容
return wrapper
@app.route("/", methods=["GET"], endpoint="data")
@not_found
def index():
return send_file("index.html")
@app.route("/data", methods=["GET"], endpoint="data")
@not_found
def data():
return send_file("data.html")
要引发这个报错,最简单的方法是定义两个不同的路由,但它们拥有相同的视图函数名:
'''
路由1:“/”,视图函数名为“index”
路由2:“/data”,视图函数名为“index”
'''
@app.route("/", methods=["GET"])
def index():
return send_file("index.html")
@app.route("/data", methods=["GET"])
def index():
return send_file("data.html")
出现这个报错是因为,路由是通过endpoint这个变量来区分视图函数的,而默认情况下,endpoint的值就是视图函数的名字。
在上面这种情况下,两个视图函数的名字都是index,所以路由“/”的endpoint默认为index,当路由“/data”也要用默认的视图函数名index来作为endpoint的值时,发现这个值已经被路由“/”占用了,因此报错。
这种情况最简单,只需要把路由“/data”的视图函数名改成其它名字就行了。
可这并不是我遇到的情况,所以引出了下面的问题。
要想明白这个问题,首先要懂一点装饰器的知识。
我们先看个例子:
def decorator(func):
def wrapper(*args, **kwargs):
print(f"{decorator.__name__}开始执行")
func(*args, **kwargs)
print(f"{decorator.__name__}执行完毕")
return wrapper
@decorator
def func():
print(f"{func.__name__}开始执行")
print(f"{func.__name__}执行完毕")
func()
输出为:
decorator开始执行
wrapper开始执行
wrapper执行完毕
decorator执行完毕
可以看到,在func函数中,输出了自己的函数名func.__name__,但终端打印出来的却不是“func”,而是“wrapper”。
也就说,被装饰器装饰过的函数,其函数名(.__name__)其实就已经变成了装饰器的内层函数名(在本例中为wrapper)。
于是答案呼之欲出了。
在上面的例子中:
def not_found(func):
def wrapper(*args, **kwargs):
# 省略装饰器内容
return wrapper
@app.route("/", methods=["GET"])
@not_found
def index():
return send_file("index.html")
@app.route("/data", methods=["GET"])
@not_found
def data():
return send_file("data.html")
从代码执行顺序来看,@app.route装饰了一个被@not_found装饰过的index函数,而因为被装饰过的index函数的函数名(__name__属性)已经变成了wrapper,所以实际上“/”路由的endpoint是wrapper。
下面的“/data”路由也是同理,所以@app.route("/data")这个路由装饰的是被@not_found装饰过的data函数,所以路由“/data”的endpoint也会是wrapper,但这个endpoint值已经被路由“/”占用了,因此报错:View function mapping is overwriting an existing endpoint function: wrapper。
直接在路由中手动加endpoint的办法就不用多说了,非常直观。
而在装饰器中加上@wraps又是如何奏效的呢。
前面说到,被装饰器装饰过的函数,其函数名(__name__属性)就不是函数本身的名字了,而是装饰器内层函数(其实就是装饰器返回的函数)的名字(__name__属性),而@wraps的作用就是让被装饰的函数的名字仍然保持为其本身的名字(参考Python 使用wraps和不使用wraps的装饰器的区别?),这样@app.route再装饰被@not_found装饰过的视图函数,其endpoint的默认取得的值就不再是wrapper,而是视图函数本身的名字了,此时只要视图函数的名字本身不重复,就不会出现这个报错。
可能会有些小伙伴说,搞那么麻烦,直接把@app.route和@not_found的顺序调换一下,这样@app.route装饰到的就是视图函数本身了,就不会有什么重复的endpoint了。
确实是这个样子,但这两个装饰器的顺序调换照成的影响不止这一点,实际上在开头引子中,@not_found装饰器放在最外层是完全不起作用的(目前我还没研究明白具体原因)。
所以还是根据实际开发中的需求来决定吧,这或许也是个好办法。