昨天继续更新 bili-auth 项目,做了个安全性更新,写装饰器时遇到了点问题,这里记录下。
普通的装饰器
这里假设有一个打印函数执行时间的装饰器,比较典型的是长这样的:
def timer(func):
def wrapper(*args, **kw)
start_time = time.time()
result = func(*args, **kw)
end_time = time.time()
cost_time = end_time - start_time
print(f'execute function in {cost_time:.3f}s')
return result
return wrapper
使用时仅需这样:
@timer
def helloworld():
time.sleep(1)
print('execution finished')
带参数装饰器
假如现在我们需要在打印时间的 log 上加个前缀。我一开始想当然地以为是这样的:
# 装饰器函数定义形式是这样的
def decorator(func):
pass
# 想到类方法定义是这样的:
def class_method(self):
pass
# 然后类方法参数定义是这样的:
def class_method(self, arg):
pass
# 类比可知,带参数装饰器形式是这样的:
def decorator(func, arg):
pass
# 类比论证,Q.E.D.(确信)
试过以后就知道,这样显然不行。
搜索后知道了带参数装饰器的用法是这样的:
def timer_factory(prefix):
def timer(func):
def wrapper(*args, **kw)
start_time = time.time()
result = func(*args, **kw)
end_time = time.time()
cost_time = end_time - start_time
print(f'[{prefix}] execute function in {cost_time:.3f}s')
return result
return wrapper
return timer
@timer_factory('example')
def helloworld():
pass
一开始真是百思不得其解,为什么 Python 带参和无参装饰器有两种定义?这样很不 Pythonic 啊,而且解释器怎么知道 "timer_factory" 不是用来传递函数的?而且它也只会返回个装饰器,而不会返回装饰后的函数,怎么会事呢?
装饰器的“本质”
仿佛回到刚开始理解装饰器这个东西的时候了。一开始把装饰器这个东西理解成一种奇怪的语法糖,一种固定写法,很不好懂只能死记的玩意儿。后来使用中逐渐理解了它的内涵,越发接受了这种写法。
它的骨架就是这样:
@deco
def func():
pass
可以理解成,解释器在处理这个函数定义时,“本质上”是这样装饰函数的:
func = deco(func)
我没有阅读过解释器的实现,只是从黑盒之外观察罢了。不过这个“本质”足够解释装饰器的很多特性了。
这样,就很好理解装饰器了。
接下来我们考虑带参数的装饰器。它的形式是这样:
@timer_factory('example')
def helloworld():
pass
从“本质”的角度考虑,“带参数装饰器”的例子就是这样:
helloworld = timer_factory('example')(helloworld)
解释器会先处理 timer_factory('example') ,它的执行结果是一个闭包内的 timer。于是原来的“本质”就变成这样:
helloworld = timer(helloworld)
这就转化成一个普通的装饰器形式了。只不过,参数通过闭包传递了,这样便实现了带参数的装饰器。还是十分巧妙而 Pythonic 的。
嵌套装饰器
有了这个“本质”,我们也好理解嵌套的装饰器了:
@deco
@another_deco
def func():
pass
我们也可以理解为:
func = deco(another_deco(func))
Flask 中使用装饰器的问题
这也是我遇到的一点问题。既然和装饰器擦点边,也放在这篇文章里吧。
以普通的方式使用装饰器,很容易碰到这样的报错:
AssertionError: View function mapping is overwriting an existing endpoint function: wrapper
Flask 提示我们,路由处理函数重名了。我们的装饰器在“包装”了原函数之后,名字并没有变成原函数的样子,就是说伪装不够到位。
因此我们需要更改装饰器自己的名字。这样即可:
def decorator(func):
def wrapper(*args, **kw):
result = func()
return result
# 通过 "__name__" 属性,可以访问函数名,同样可以修改函数名
wrapper.__name__ = func.__name__
return wrapper
从“本质”的角度理解装饰器,大部分装饰器问题都能迎刃而解。至于类方法使用装饰器,以前好像也碰到过问题,不过现在不想研究了,遇到再说吧...