之前在http://python.jobbole.com/86068/,看到关于装饰器的一些知识。
1. 函数式装饰器:
Decorator是一个函数,它以一个函数对象A为参数,返回另一个函数对象B。对象B定义在Decorator体内,形成一个闭包。函数A和函数B接受的参数相同。每当程序调用函数A时,实际上会转换为对函数B的调用
1 def func_cache(func): 2 cache = {} 3 def inner_deco(*args): 4 if args in cache: 5 print('func {} is already cached with arguments {}'.format( 6 func.__name__, args)) 7 return cache[args] 8 else: 9 print('func {} is not cached with arguments {}'.format( 10 func.__name__, args)) 11 res = func(*args) 12 cache[args] = res 13 return res 14 15 return inner_deco 16 17 @func_cache 18 def add_two_number(a, b): 19 return a + b 20 21 if __name__ == "__main__": 22 print('1. add_two_number(1, 2)') 23 add_two_number(1, 2) 24 print('2. add_two_number(2, 3)') 25 add_two_number(2, 3) 26 print('3. add_two_number(1, 2)') 27 add_two_number(1, 2)
func_cache就是我们实现的Decorator,它以一个函数对象(func)作为参数,返回另一个函数对象(inner_deco),因此,当我们每次调用被func_cache装饰过的函数(add_two_number)时,调用的其实是inner_deco,也即:
add_two_number(1, 2) --> inner_deco(1, 2)
而inner_deco,它内部实现的就是函数返回值缓存的逻辑,并打印了一些调试信息
但是这里有一个明显的问题:inner_deco只能接受*arg,也就是列表参数,这就限制了这个Decorator的使用范围。下面这个版本就添加了**kwargs的支持。需要注意的是,kwargs不能进行hash,也就不能直接作为python中字典的key值,因此这里现将其转成一个frozenset(frozenset是冻结的集合,它是不可变的,存在哈希值,好处是它可以作为字典的key,也可以作为其它集合的元素。缺点是一旦创建便不能更改,没有add,remove方法)
# -*- coding: utf-8 -*- def func_cache(func): cache = {} def inner_deco(*args, **kwargs): key = (args, frozenset(kwargs.items())) print key if key not in cache: print "func {0} is not cached with arguments {1}, {2}".format(func.__name__, args, kwargs) res = func(*args, **kwargs) cache[key] = res return cache[key] else: print "func {0} is alread cached with arguments {1}, {2}".format(func.__name__, args, kwargs) return cache[key] return inner_deco @func_cache def add_two_number(a, b): return a + b @func_cache def product_two_number(a, b): return a * b if __name__ == "__main__":
print('add_two_number func name is {}'.format(add_two_number.__name__)) add_two_number(1, 2) add_two_number(1, b=3) add_two_number(1, 2) product_two_number(2, 3) product_two_number(2, 3)
新增加了一个product_two_number函数,用于测试func_cache中的字典cache是否对于每个被装饰的函数都分配了一个,即不同函数调用是否都会调用func_cache(func)
这里的Decorator还有一个问题,它改变了被装饰函数add_two_number的签名,比如:
print('add_two_number func name is {}'.format(add_two_number.__name__))
# 输出 add_two_number func name is inner_deco
这不是我们想要的,而且在复杂项目中,对于Bug的追踪也将是灾难性的。
好在Python为我们提供了functools模块,其中的wraps装饰器可以帮助我们解决这个问题。
# -*- coding: utf-8 -*- from functools import wraps def func_cache(func): cache = {} @wraps(func) def inner_deco(*args, **kwargs): key = (args, frozenset(kwargs.items())) if key not in cache: print('func {} is not cached with arguments {} {}'.format( func.__name__, args, kwargs)) res = func(*args, **kwargs) cache[key] = res return cache[key] return inner_deco
带参数Decorator
目前我们实现的函数缓存装饰器,会缓存所有遇到的函数返回值。我们希望能够对缓存数量上限做一个限制,从而在内存消耗和运行效率上取得折中。但是同时,对于不同的函数,我们希望做到缓存上限不同,例如对于运行一次比较耗时的函数,我们希望缓存上限大一些;反之,则小一些。这时,需要用到带参数的Decorator。
# -*- coding: utf-8 -*- from functools import wraps import random def outer_deso(size=10): def func_cache(func): cache = {} @wraps(func) def inner_deco(*args, **kwargs): key = (args, frozenset(kwargs.items())) print key if key not in cache: print('func {} is not cached with arguments {} {}'.format(func.__name__, args, kwargs)) res = func(*args, **kwargs) if len(cache) >= size: luck_key = random.choice(list(cache.keys())) print('func {} cache pop {}'.format(func.__name__, luck_key)) cache.pop(luck_key, None) cache[key] = res return cache[key] else: print "func {0} is alread cached with arguments {1}, {2}".format(func.__name__, args, kwargs) return cache[key] return inner_deco return func_cache @outer_deso(size=10) def add_two_number(a, b): return a + b @outer_deso(size=10) def product_two_number(a, b): return a * b if __name__ == "__main__": print('add_two_number func name is {}'.format(add_two_number.__name__)) add_two_number(1, 2) add_two_number(1, b=3) add_two_number(1, 2) product_two_number(2, 3) product_two_number(2, 3)
无参数的装饰器,@符号后面接的是一个可做Decorator的函数对象;而有参数的装饰器,@符号后面接的是一个函数调用,此函数调用返回的是一个可做Decorator的函数对象
但是,从上面的代码中也可以看出,到了带参数的Decorator这一步,Decorator的实现已经有了两层的函数嵌套,难于理解且不够优雅。这就需要引用decorator模块。这个模块可以不仅可以减少实现Decorator过程中的函数嵌套,还可以完美的保持函数签名不被更改。
首先实现最简单的无参数Decorator:
def func_cache(func): func._cache = {} func._cache_size = 3 return decorate(func, _cache) def _cache(func, *args, **kwargs): key = (args, frozenset(func.__name__)) if key not in func._cache: print "func {} not in cache".format(func.__name__) res = func(*args, **kwargs) if len(func._cache)>=func._cache_size: lucky_key = random.choice(list(func._cache.keys())) func._cache.pop(lucky_key, None) print "func {} pop cache key {}".format(func.__name__, lucky_key) func._cache[key] = res return func._cache[key] @func_cache def add_two_number(a, b): return a + b
decorator模块实现有参装饰器:
import random from decorator import decorate def func_cache(size=10): def wrapped_cache(func): func._cache = {} func._cache_size = size return decorate(func, _cache) return wrapped_cache def _cache(func, *args, **kwargs): key = (args, frozenset(kwargs.items())) if key not in func._cache: print('func {} not hit cache'.format(func.__name__)) res = func(*args, **kwargs) if len(func._cache) >= func._cache_size: lucky_key = random.choice(list(func._cache.keys())) func._cache.pop(lucky_key, None) print('func {} pop cache key {}'.format(func.__name__, lucky_key)) func._cache[key] = res return func._cache[key] @func_cache(size=3) def add_two_number(a, b): return a + b
实现带参数的装饰器的方式是相同的:在之前不带参数的装饰器外面再包一层函数,通过闭包将参数绑定到装饰器上,并将装饰器返回。