昨天继续更新 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

从“本质”的角度理解装饰器,大部分装饰器问题都能迎刃而解。至于类方法使用装饰器,以前好像也碰到过问题,不过现在不想研究了,遇到再说吧...