关于Flask中View function mapping is overwriting an existing endpoint function

首次编辑:24/3/10/11:03
最后编辑:24/3/10/11:57

引子

背景

本来是在写个人网站,以前的代码中,几乎每个视图函数都有类似于:

@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")
  1. 第二种是在@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,所以实际上“/”路由的endpointwrapper
下面的“/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装饰器放在最外层是完全不起作用的(目前我还没研究明白具体原因)。
所以还是根据实际开发中的需求来决定吧,这或许也是个好办法。

评论