一、装饰器
装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。
在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现。
由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。先看看一些实例, 然后再来分析下原理。假设我们有如下的基本函数:
1 2 3 |
def rge(x): for i in range(x): pass |
把函数赋值给一个变量:
1 2 |
>>> f = rge >>> f(10000000) |
函数对象有一个 __name__ 属性,可以拿到函数的名字:
1 2 3 4 |
>>> rge.__name__ 'rge' >>> f.__name__ 'rge' |
现在,假设我们要增强rge()函数的功能,比如,统计函数的执行时间,但又不希望修改rge()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
本质上,decorator就是一个返回函数的高阶函数,接受一个函数作为参数,并且返回一个函数。所以,我们要定义一个能打印执行时间的decorator,可以定义如下:
1 2 3 4 5 6 |
import time def timeit(fun, *args, **kw): #可变参数; start = time.time() ret = fun(*args, **kw) #参数解构; print time.time() - start return ret |
timeit()函数的参数定义是(*args, **kw),因此,timeit()函数可以接受任意参数的调用。
执行效果如下:
1 2 |
>>> timeit(rge,1000000) #发生在调用的时候; 0.0733699798584 |
这种实现看上去还可以,但是每次调用的是decorator,还要把函数作为一个参数传入。这样需要修改调用的地方,使用起来就不方便了。重新定义一下装饰器:
1 2 3 4 5 6 7 8 |
import time def timeit(fun, *args, **kw): def wrap(*args, **kw): start = time.time() ret = fun(*args, **kw) print(time.time() - start) return ret return wrap |
执行效果如下:
1 2 3 |
>>> f = timeit(rge) #可以提前定义好; >>> f(10000000) 0.694827079773 |
观察上面的timeit,因为它是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:
1 2 3 4 5 6 7 8 9 |
# 装饰rge函数; >>> @timeit ... def rge(x): ... for i in range(x): ... pass # 调用rge函数; >>> rge(1000000) 0.0770568847656 |
效果等效于上面的执行方法。
另外要说明的一点就是,一个装饰器在装饰一个函数或者类的时候就会进行实例化,然后返回函数。而函数只有在被装饰的函数或类做实例化时才会调用。如下测试,在要返回的函数前面输出一个字符:
1 2 3 4 5 6 7 8 9 |
import time def timeit(fun, *args, **kw): print("init") def wrap(*args, **kw): start = time.time() ret = fun(*args, **kw) print(time.time() - start) return ret return wrap |
然后同样去装饰一个函数,我们看一下被装饰的函数创建完成时, print("init") 会不会输出:
1 2 3 4 5 6 7 8 9 10 11 |
# 装饰rge函数; >>> @timeit ... def rge(x): ... for i in range(x): ... pass ... init # 调用rge函数; >>> rge(1000000) 0.0554578304291 |
可以看出创建完被装饰的函数之后,timeit中定义的 print("init") 执行了,而wrap函数被返回了。当 rge() 函数被执行时才会执行 wrap() 函数。
二、带参数的装饰器
如果装饰器本身需要传入参数,那就需要编写一个返回装饰器的高阶函数。写出来会更复杂。比如,要自定义log的文本:
1 2 3 4 5 6 7 8 |
def log(text): def decorator(fun): def wrapper(*args, **kw): print('%s %s()' % (text, fun.__name__)) ret = fun(*args, **kw) return ret return wrapper return decorator |
这个3层嵌套的decorator用法如下:
1 2 3 |
>>> @log('execute') ... def now(): ... print('2017-3-25') |
执行结果如下:
1 2 3 |
>>> now() execute now() 2017-3-25 |
和两层嵌套的decorator相比,3层嵌套的效果是这样的:
1 2 3 4 5 |
>>> now = log('execute')(now) >>> now() execute wrapper() execute now() 2017-3-25 |
我们来剖析上面的语句,首先执行log(‘execute’),返回的是decorator函数,再调用返回的函数,参数是now函数,返回值最终是wrapper函数。其实这就是科里化过程,在函数式编程中非常有用的。
以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有 __name__等属性,但你去看经过decorator装饰之后的函数,它们的 __name__已经从原来的’now’变成了’wrapper’:
1 2 |
>>> now.__name__ 'wrapper' |
因为返回的那个wrapper()函数名字就是’wrapper’,所以,需要把原始函数的 __name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。
简单来说,我们在使用Decorator的过程中,被装饰后的函数其实已经是另外一个函数了(函数名等函数属性会发生改变),如: __name__, __doc__等属性,一个是获取函数名,一个是获取函数内部注释信息的(”’docstring”’)。
为了消除副作用,如保持函数签名,不需要编写 wrapper.__name__ = fun.__name__这样的代码,Python的functools包中提供了一个叫wraps的decorator来消除这样的副作用。写一个decorator的时候,最好在实现之前加上functools的wraps,它能保留原有函数的名称和docstring。functools提供了两个api,一个是update_wrapper,一个是wrap装饰器函数,但是wrap装饰器函数也是调用了update_wrapper。所以,一个完整的decorator的写法如下:
1 2 3 4 5 6 7 8 |
from functools import wraps def log(fun): @wraps(fun) def wrapper(*args, **kw): print('call %s():' % fun.__name__) return fun(*args, **kw) return wrapper |
或者针对带参数的装饰器:
1 2 3 4 5 6 7 8 9 10 |
from functools import wraps def log(text): def decorator(fun): @wraps(fun) def wrapper(*args, **kw): print('%s %s():' % (text, fun.__name__)) return fun(*args, **kw) return wrapper return decorator |
其中 from functools import wraps是导入解释器内置的wraps模块,模块的概念稍候讲解。现在,只需记住在定义wrapper()的前面加上 @wraps()即可。这个装饰器就是帮我们保持函数签名的。当我们在装饰器中加了这个 @wraps()装饰器之后,再执行 __name__时就恢复正常了。
1 2 3 4 5 6 7 8 9 |
# 装饰函数; >>> @log ... def now(): ... print('2017-10-01') ... # 查看函数签名; >>> now.__name__ 'now' |
三、实现wraps装饰器
上面说了,在decorator中使用wraps是用来帮我们消除副作用的。写一个decorator的时候,在实现之前加上functools的wraps,它能保留原有函数的名称和docstring,当还有一些其他信息。下面简单来看一下wraps实现方式:
首先我们定义一个简单的装饰器,如下:
1 2 3 4 |
def log(fn, *args, **kwargs): def wrap(*args, **kwargs): return fn(*args, **kwargs) return wrap |
然后装饰一下run函数,如下:
1 2 3 4 |
@log def run(): '''this is fun''' pass |
执行完成后,我们知道被装饰后的run函数产生了副作用。很多属性没有了。
1 2 |
>>> run.__name__ 'wrap' |
好,现在我们知道大概问题了。我们试着不使用wraps来解决,第一个解决方法如下:
1 2 3 4 5 6 |
def log(fn, *args, **kwargs): def wrap(*args, **kwargs): return fn(*args, **kwargs) wrap.__name__ = fn.__name__ wrap.__doc__ = fn.__doc__ return wrap |
我们可以在装饰器内部,把被装饰函数原有的属性给改变回去。
如果每个装饰器都要这么写,重复步骤多了,肯定会烦。所以可以在这个基础之上把重复要做的哪一部分给抽出来变成一个可执行函数即可。如下代码:
1 2 3 |
def wraps(src, dst): dst.__name__ = src.__name__ dst.__doc__ = src.__doc__ |
现在,这个装饰器变成这样的了:
1 2 3 4 5 |
def log(fn, *args, **kwargs): def wrap(*args, **kwargs): return fn(*args, **kwargs) wraps(fn, wrap) return wrap |
当我们再装饰run函数时,这个run函数对于我们已经定义的name和doc就不会有副作用了。
1 2 3 4 5 6 7 8 9 |
>>> @log ... def run(): ... '''this is fun''' ... pass ... >>> run.__name__ 'run' >>> run.__doc__ 'this is fun' |
虽然说此时基本解决问题了,但是还是需要传入几个函数。我们可以在这个基础之上再次改造,变为一个可带参数的装饰器,也就是类似functions.wraps了。
1 2 3 4 5 6 |
def wraps(src): def dec(dst): dst.__name__ = src.__name__ dst.__doc__ = src.__doc__ return dst return dec |
然后改变一下我们log装饰器:
1 2 3 4 5 |
def log(fn, *args, **kwargs): def wrap(*args, **kwargs): return fn(*args, **kwargs) wrap = wraps(fn)(wrap) return wrap |
然后可以去装饰一下log函数试试,同样会消除副作用的。对于这个wraps(fn)(wrap)函数,就很像我们的装饰器了,传入一个函数,返回一个函数。所以在使用上就可以像functions.wraps一样使用了,如下:
1 2 3 4 5 |
def log(fn, *args, **kwargs): @wraps(fn) def wrap(*args, **kwargs): return fn(*args, **kwargs) return wrap |
再来使用log装饰一下我们的run函数,执行一下看看效果:
1 2 3 4 5 6 7 8 9 |
>>> @log ... def run(): ... '''this is fun''' ... pass ... >>> run.__name__ 'run' >>> run.__doc__ 'this is fun' |
可以看到现在这种形式的使用方式就跟我们使用functions.wraps一样了。不同之处在于默认的wraps做的事情多于我们,可能更完善一些。
四、装饰器应用
4.1 缓存
写一个函数装饰器,用来缓存函数的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# 加载必要模块; import time from functools import wraps # 定义带参数的装饰器; def cache(instance): def dec(fun): @wraps(fun) def wrap(*args, **kwargs): ag = ','.join((str(x) for x in args)) kw = ','.join('{}={}'.format(k, v) for k, v in sorted(kwargs.items())) key = '{}::{}::{}'.format(fun.__name__, ag, kw) print(ag,kw,key) ret = instance.get(key) if ret is not None: return ret ret = fun(*args, **kwargs) instance.set(key, ret) return ret return wrap return dec # 定义类; class DictCache: def __init__(self): self.cache = dict() def get(self, key): return self.cache.get(key) def set(self, key, value): self.cache[key] = value def __str__(self): return str(self.cache) def __repr__(self): return repr(self.cache) # 初始化类; cache_instance = DictCache() # 调用装饰器,把类当参数传给装饰器; @cache(cache_instance) def long_time_fun(x, *args, **kwargs): time.sleep(x) return x # 执行被装饰的函数,看看缓存效果; long_time_fun(3) long_time_fun(3, k='v') |
Python 3内置functools提供了一个lru_cache装饰器,就是用来提供缓存功能的,只不过lru_cache更加高级,支持lru算法,可设置内存最大缓存条目,当达到上限后就触发lru算法,把最近最少使用的kv删除。
1 2 3 4 5 6 |
import time from functools import lru_cache @lru_cache(maxsize=1) def long_time_fun(x): time.sleep(x) return x |
如上我们设置最大缓存条目为1,然后装饰long_time_fun函数,就是让这个函数睡眠。如果我们设置睡眠时间为2秒,那么第一次执行应该会等待2秒钟,同时@lru_cache会把key缓存到内存中,所以第二次执行就非常快了,不需要调用long_time_fun函数了。
1 2 |
>>> long_time_fun(2) 2 |
第一次执行等待2秒,后面多次执行时间都会很快。另外,我们设置了最大缓存条目为1,所以你可以再调用long_time_fun函数,给3秒睡眠,然后去验证看看前一个2秒的key是否失效了。
4.2 监控
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# 加载必要模块; import time import logging from functools import wraps # 定义带参数的装饰器; def metric(prefix, instance): def timeit(fun): @wraps(fun) def wrap(*args, **kw): start = time.time() ret = fun(*args, **kw) key = '{}.{}.{}'.format(prefix, fun.__module__, fun.__name__) instance.send(key, time.time() - start) return ret return wrap return timeit # 定义一个打印日志的类,其中send方法传给装饰器instance.send方法; class LoggingMetric: def send(self, key, value): logging.warning('{} => {}'.format(key, value)) # 类实例化; LM = LoggingMetric() # 调用装饰器,把类传给装饰器; @metric(prefix='ywnds', instance=LM) def long_time_fun(x): time.sleep(x) return x # 调用被装饰的函数; >>> long_time_fun(3) WARNING:root:ywnds.__main__.long_time_fun => 3.0022189617156982 3 |
完结。。。