如何使用函数装饰器
常用的斐波那契的写法
def fibonacci(n):
if n<=1:
return 1
return fibonacci(n-1)+fibonacci(n-2)
这种方法也是C语言常用的递归算法。会进行大量的重复计算。如计算(10)时需要计算(8)和(9),计算(9)时需要计算(7)和(8)
改写传统的斐波那契
def fibonacci(n,cache=None):
#cache(n) #计算开始时先去缓存里找是否计算过n,如存在直接返回,否则计算,并把计算结果放在缓存中。
if cache is None: #创建一个空的字典
cache = {}
if n in cache:
return cache[n]
if n<=1:
return 1
cache[n] = fibonacci(n-1,cache) + fibonacci(n-2,cache)
return cache[n]
if __name__ == '__main__':
print(fibonacci(50))
可以发现瞬间计算出数值
使用装饰器
def memo(func):
cache = {}
def wrap(n):
if n not in cache:
cache[n] = func(n)
return cache[n]
return wrap
@memo
def fibonacci(n):
if n <= 1:
return 1
return fibonacci(n - 1) + fibonacci(n - 2)
if __name__ == '__main__':
print(fibonacci(50))
如何为被装饰的函数保存元数据
在函数对象中保存着一些函数的元数据, 例如:
f.__name__: 函数的名字
f.__doc__: 函数文档字符串
f.__module__: 函数所属模块名
f.__dict__: 属性字典
f.__defaults__: 默认参数元组
我们在使用装饰器后, 再访问上面这些属性访问时,
看到的是内部包裹函数的元数组, 原来函数的元数据便丢失掉了, 应该如何解决?
解决方案
使用update_wrapper
使用wraps
from functools import wraps
def mydecorator(func): #装饰器,本例没增加功能,只做为演示
@wraps(func)
def wrapper(*args,**kargs):
"""wrapper function"""
print ('In wrapper')
func(*args,**kargs)
# update_wrapper(wrap, func)
return wrapper
@mydecorator
def example():
"""example function"""
print ('In example')
if __name__ == '__main__':
example()
print(example.__name__)
如何定义带参数的装饰器
实际案例
实现一个装饰器, 它用来检查被装饰函数的参数类型。
装饰器可以态可以通过参数指明函数参数的类型, 调用时如果检测出类型不匹配则抛出异常。
@type_assert(str, int, int)
def f(a, b, c):
...
@type_assert(y=list)
def g(x, y):
.解决方案
提取函数签名: inspect.signature()
带参数的装饰器, 也就是根据参数定制化一个装饰器, 可以看成生产装饰器的工厂。 每次调用type_assert, 返回一个特定的装饰器,然后用它去修饰其他函数
import inspect
def type_assert(*ty_args, **ty_kwargs): # 带参数的装饰器函数, 要增加一层包裹 参数是 装饰器的参数
def decorator(func):
# inspect.signature(func) 函数观察对象, 方便后面获取 参数-类型 字典与参数-值字典
func_sig = inspect.signature(func) # (a,b,c)
# 将装饰器参数 组成参数-类型 字典 如 {a:int, b:str}
bind_type = func_sig.bind_partial(*ty_args, **ty_kwargs).arguments # OrderedDict([('c', )])
# func_sig.bind_partial 绑定部分参数可以得到 参数类型字典,
# 比如 参数是a=1, b='bbbb', c=2 装饰器参数是 a=int, b=str ,则得到{'a':int, 'b':str}
# 如果使用 func_sig.bind 则装饰器参数中 不能缺少 c 的类型
def wrap(*args, **kwargs): # 参数是func的 参数
for name, obj in func_sig.bind(*args, **kwargs).arguments.items(): # 得到 参数-值 字典
type_ = bind_type.get(name) # 从 参数-类型 字典中 得到 参数 应该属于的 类型
if type_:
if not isinstance(obj, type_):
raise TypeError('%s must be %s' % (name, type_))
return func(*args, **kwargs)
return wrap
return decorator
@type_assert(c=str)
def f(a, b, c):
pass
if __name__ == '__main__':
f(5, 10, 's') # 校验通过
f(5, 10, 1) # 检验失败 1 不是字符串类型
如何实现属性可修改的函数装饰器
实际案例
在某项目中, 程序运行效率差, 为分析程序内哪些函数执行时间开销大, 我们实现一个带timeout参数的函数装饰器。 装饰功能如下:
1.统计被装饰函数单次调用运行时间
2.时间大于参数 timeout的, 将此次函数调用记录到log 日志中
3.运行时可修改 timeout 的值
@warn_timeout(1.5)
def func(a, b):
...
解决方案
为包裹函数添加一个函数, 用来修改闭包中使用的自由变量。 在python3中:使用nonlocal 来访问潜逃作用域中的变量引用
import time
import logging
def warn_timeout(timeout):
def decorator(func):
# _timeout = [timeout]
def wrap(*args, **kwargs):
# timeout = _timeout[0]
t0 = time.time()
res = func(*args, **kwargs)
used = time.time() - t0
if used > timeout:
logging.warning('%s: %s > %s', func.__name__, used, timeout) # logging.warning 打印 输出到控制台
return res
def set_timeout(new_timeout):
nonlocal timeout # timeout 是闭包 变量
timeout = new_timeout
# _timeout[0] = new_timeout
wrap.set_timeout = set_timeout # 使timeout 可修改
return wrap
return decorator
import random
@warn_timeout(1.5)
def f(i):
print('in f [%s]' % i)
while random.randint(0, 1):
time.sleep(0.6)
for i in range(3):
f(i)
f.set_timeout(1) # 修改timeout 参数 从1.5 变为1
for i in range(3):
f(i)
输出结果如下
如何在类中定义装饰器
实际案例
实现一个能将函数调用信息记录到日志的装饰器:
把每次函数的调用时间, 执行时间, 调用次数写入日志
可以对被装饰函数分组, 调用信息记录到不同日志
动态修改参数, 比如日志格式
动态打开关闭日志输出功能
解决方案
为了让装饰器在使用上更加灵活, 可以把类的实例方法作为装饰器,此时在包裹函数中就可以持有实例对象, 便于修改属性和拓展功能
import time
import logging
DEFAULT_FORMAT = '%(func_name)s -> %(call_time)s\t%(used_time)s\t%(call_n)s'
class CallInfo:
def __init__(self, log_path, format_=DEFAULT_FORMAT, on_off=True):
self.log = logging.getLogger(log_path)
self.log.addHandler(logging.FileHandler(log_path))
# 这样可以通过log 往 log_path 输出信息
self.log.setLevel(logging.INFO) # 设置log级别
self.format = format_
self.is_on = on_off
# 装饰器方法
def info(self, func):
_call_n = 0 # 被调用次数
def wrap(*args, **kwargs):
func_name = func.__name__
call_time = time.strftime('%x %X', time.localtime())
# localtime 格式化时间戳为本地的时间 strftime 则得到时间字符串
# % x
# 本地相应的日期表示
# % X
# 本地相应的时间表示
t0 = time.time()
res = func(*args, **kwargs)
used_time = time.time() - t0
nonlocal _call_n
_call_n += 1
call_n = _call_n
if self.is_on:
self.log.info(self.format % locals()) # locals 即wrap函数中的变量 对应的字典
return res
return wrap
def set_format(self, format_):
self.format = format_
def turn_on_off(self, on_off):
self.is_on = on_off
# 测试代码
import random
ci1 = CallInfo('mylog1.log')
ci2 = CallInfo('mylog2.log')
@ci1.info
def f():
sleep_time = random.randint(0, 6) * 0.1
time.sleep(sleep_time)
@ci1.info
def g():
sleep_time = random.randint(0, 8) * 0.1
time.sleep(sleep_time)
@ci2.info
def h():
sleep_time = random.randint(0, 7) * 0.1
time.sleep(sleep_time)
for _ in range(3):
random.choice([f, g, h])()
ci1.set_format('%(func_name)s -> %(call_time)s\t%(call_n)s') # 去掉使用时间
for _ in range(3):
random.choice([f, g])()