重温Python装饰器

589 阅读4分钟

Python装饰器可谓是一个老生常谈的话题了,随便Google一下都有大量的Python装饰器教程,而且花样繁多,高档中档低档都有。本文的定位是面向会使用装饰器,了解闭包,但是时不时需要翻阅文档或教程的新手(比如像我这种),如果你是希望从这篇博客里面解锁一些使用装饰器的高级姿势,那么直接点击关闭网页即可。

@就是一个语法糖

先给大家补习一下基本功课,Python装饰器的使用是通过@符号来完成的,一个常见的装饰器例子如下:

@cache
def rpc_invoke():
    pass

上述的例子等价于:

def rpc_invoke():
    pass

rpc_invoke = cache(rpc_invoke)

因此,@符号其实是将下一行的函数作为参数传入@后面的装饰器函数(以函数作为参数的函数)中。

一层装饰器比较简单,但大家在工作也可能会经常碰到二层装饰器,还是以上述的cache decorator举例:

# 添加缓存时间
@cache(3600)
def rpc_inovke():
    pass

等价于:

def rpc_inovke():
    pass

rpc_invoke = (cache(3600))(rpc_invoke)

二层装饰器与一层装饰器的原理类似,只是在一层装饰器中,cache函数本身就是一个装饰器函数,而在二层装饰器中,调用cache函数会返回一个装饰器函数,即cache函数是一个装饰器函数的Generator。

从上述的例子中可以看出,其实@符号可有可无,只是为了程序员们编写方便而已,这种情况下@就是一个Syntactic Sugar。

编写装饰器的姿势

温习完使用装饰器的知识,下面来看下如何写装饰器。比如写一个最简单的一层cache装饰器,就起码需要两个def。

def cache(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        key = func.__name__
        cache_result = cache.get(key)
        if cache_result:
            return cache_result
        else:
            result = func(*args, **kwargs)
            cache.set(key, result)
                return result
    return wrapper

@cache
def rpc_invoke():
    pass

最外面的一层def就是装饰器函数,当调用cache函数的时候,它返回wrapper函数用于替换原来的rpc_invoke函数。

最里面的一层def可以视为被装饰函数(在这里是rpc_invoke)的替身,即当调用rpc_invoke函数的时候,其实是在调用wrapper函数,而rpc_invoke函数作为wrapper函数的闭包变量在wrapper函数内部被调用,即func变量。

通俗点说,装饰的过程就是偷梁换柱,用wrapper函数取代rpc_invoke函数,而functools.wraps则是为了使这一个偷梁换柱更加逼真。

OK,那么是不是每一个装饰器都需要起码两个def呢?这倒不一定,具体取决于装饰器的用途,比如你是希望偷梁换柱,那基本就得需要两个def,但是如果只是通过装饰器收集信息呢?

下面介绍一下装饰器的另一种常用姿势,比如要收集程序中使用了某装饰器的所有函数:

functions = []

def decorator(func):
    functions.append(func)
    return func

@decorator
def func1():
    pass

当启动程序的时候,func1就会自动被添加到functions变量中。

这个例子可能大家会觉得没啥卵用,事实上也的确没卵用,只是一个example。使用一层装饰器收集信息的场景比较少,但是如果使用二层装饰器就能玩出一些新花样了,比如Flask的路由设计:

class App(object):
    ROUTE_MAP = {}

    def route(url):
    	def decorator(func):
    	    ROUTE_MAP[url] = func
    	    return func
    	return decorator

@app.route('/user/info')
def get_user_info(user_id):
    pass

通过装饰器收集每个路由对应的handler,并记录在app的一个字典中,那么当请求到达app的时候,app就能根据路由字典迅速找到handler并处理请求。

N层装饰器怎么办

一层装饰器好办,无非是要么一个def或者两个def,那要是写N层装饰器呢?

先不论这N层装饰器的合理性,这里提一个简单粗暴的计数方法:每增加一次参数传递,就加一层def

以能够控制时间缓存的二层cache装饰器为例:

def cache(expiration=300)
    def decorator(func):
    	@functools.wrap(func)
	    def wrapper(*args, **kwargs):
	    	key = func.__name__
	    	cache_result = cache.get(key)
	    	if cache_result:
	    		return cache_result
	    	else:
	    		result = func(*args, **kwargs)
	    		cache.set(key, result)
	    		return result
	    return wrapper
    return decorator

有不同想法欢迎指出,祝大家周末快乐,不用加班~