当你希望给一个函数前后永久性地增加新功能,又不想修改原代码时可以使用装饰器。[或者是希望对不同的函数前后添加一致的功能时]函数和新功能都可以变。
一个向外部函数传入函数引用(装饰器),一个传入变量值(闭包)。
装饰器其实只是嵌套函数的一种应用。它们封装一个函数,并且用这样或者那样的方式来修改它的行为。内部函数体中,可以在调用指定函数前后添加需要执行的指令,最后在外部函数体的末尾返回内部函数名即可。
@:只是一个语法糖而已,只是生成一个被装饰的函数的一个简短方式。另外,嵌套函数内部定义的内部函数在嵌套函数体外是不能访问的:
def a_new_decorator(a_func): def wrap_The_Function(): """Hey you! I'm wrap_The_Function.""" print("I am doing some boring work before executing a_func()") a_func() print("I am doing some boring work after executing a_func()") return wrap_The_Function def a_function_requiring_decoration(): """Hey you! Decorate me!""" print("I am the function which needs some decoration to remove") a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration) # 装饰 a_function_requiring_decoration 函数 a_function_requiring_decoration() # outputs:I am doing some boring work before executing a_func() # I am the function which needs some decoration to remove my foul smell # I am doing some boring work after executing a_func() -------------------------------------------------------------------------------------------------------------------------------- @a_new_decorator def a_function_requiring_decoration(): """Hey you! Decorate me!""" print("I am the function which needs some decoration to remove my foul smell") a_function_requiring_decoration() #outputs: I am doing some boring work before executing a_func() # I am the function which needs some decoration to remove my foul smell # I am doing some boring work after executing a_func()
# @a_new_decorator 其实就只是下面这条语句的简短说明: a_function_requiring_decoration = a_new_decorator(a_function_requiring_decoration) # 不能从外部访问嵌套函数内部定义的函数 wrap_The_Function() # NameError: name 'wrap_The_Function' is not defined
从上面的内容,我们可以看到,虽然最后调用的时候函数虽然还是同一个函数名字 a_function_requiring_decoration,但是打印它的 __name__ 和 __doc__看看:
print(a_function_requiring_decoration.__name__, '|', a_function_requiring_decoration.__doc__) # outputs: wrap_The_Function | Hey you! I'm wrap_The_Function.
会发现这里的函数被 wrap_The_Function替代了,它重写了函数的名字和注释文档。不过,Python 提供了一个简单的函数来解决它,那就是 functools.wraps 。
functools.wraps 可以将原函数对象的指定属性复制给包装函数对象,默认有module、name、doc,或者通过参数选择。
@wraps 接受一个函数来进行装饰,并加入了复制函数名称、注释文档、参数列表等等的功能。这可以让我们在装饰器里面访问在装饰之前的函数的属性。
from functools import wraps def a_new_decorator(a_func): @wraps(a_func) def wrap_The_Function(): """Hey you! I'm wrap_The_Function.""" print("I am doing some boring work before executing a_func()") a_func() print("I am doing some boring work after executing a_func()") return wrap_The_Function @a_new_decorator def a_function_requiring_decoration(): """Hey you! Decorate me!""" print("I am the function which needs some decoration to remove my foul smell") print(a_function_requiring_decoration.__name__, '|', a_function_requiring_decoration.__doc__) # Output: a_function_requiring_decoration | Hey you! Decorate me!
from functools import wraps def decorator_name(f): @wraps(f) def decorated(*args, **kwargs): if not can_run: return "Function will not run" return f(*args, **kwargs) return decorated @decorator_name def func(): return("Function is running") can_run = True print(func()) # Output: Function is running can_run = False print(func()) # Output: Function will not run
装饰带参数的函数传参:1.装饰器的内部函数 wrapper 需要传参; 2.内部函数中调用的被装饰函数也需要传参。
# 打印被装饰函数调用的结果,并在调用前后给出提示信息 def tips(func): def wrapper(a,b): # 传参 print('start') func(a,b) # 调用时也需要传参 print('stop') return wrapper @tips def add(a,b): # 参数 a,b print(a + b) @tips def sub(a,b): # 参数 a,b print(a - b) add(3,2) # start | 5 | stop; 格式所限,三个输出之间没有换行, | 隔开 sub(5,3) # start | 2 | stop add(4,2) # start | 6 | stop sub(6,3) # start | 3 | stop
带参数的装饰器:如有必要,可以通过 if-else 针对不同的函数进行不同的装饰。
def new_tips(argv): # 外面再加一层接收装饰器的参数 print('传递装饰器参数: ', argv) def tips(func): # 这层还是接收被装饰函数的函数名 print('传递函数名:', func.__name__) # 注意一下 func.__name__ 的值 def wrapper(a,b): print('start %s, %s' % (argv, func.__name__)) # 注意一下 func.__name__ 的值 func(a,b) print('stop %s, %s' % (argv, func.__name__)) return wrapper return tips @new_tips('add_module') def add(a,b): # 参数 a,b print(a + b) # ~ @new_tips('sub_module') # ~ def sub(a,b): # 参数 a,b # ~ print(a - b) # 1.在 @ 的基础上直接调用函数 add(5,4) # outputs: 传递装饰器参数: add_module # 传递函数名: add # start add_module, add # 9 # stop add_module, add # 2.注释掉 add 函数前的 @, 直接这么调用: fun = new_tips('add_module')(add) fun(5,4) # outputs: 传递装饰器参数: add_module # 传递函数名: add # start add_module, add # 9 # stop add_module, add # 3.未注释掉函数前面的 @,直接这么调用 fun = new_tips('add_module')(add) fun(5,4) # outputs: 传递装饰器参数: add_module # 传递函数名: add # 传递装饰器参数: add_module # 传递函数名: wrapper # start add_module, wrapper # start add_module, add # 9 # stop add_module, add # stop add_module, wrapper # 4.在 3 的基础上,再将 sub 函数部分的注释取消掉,分别执行以上两种调用方式的话: add(5,4) # outputs: 传递装饰器参数: add_module # 传递函数名: add # 传递装饰器参数: sub_module # 传递函数名: sub # start add_module, add # 9 # stop add_module, add fun = new_tips('add_module')(add) fun(5,4) # outputs: 传递装饰器参数: add_module # 传递函数名: add # 传递装饰器参数: sub_module # 传递函数名: sub # 传递装饰器参数: add_module # 传递函数名: wrapper # start add_module, wrapper # start add_module, add # 9 # stop add_module, add # stop add_module, wrapper
从上面可以看到:一到 @new_tips 的时候,就会去一步步执行函数 new_tips,直到返回函数名索引 wrapper,最后可直接调用被装饰函数 add 获得装饰后的输出。
多个装饰器叠加:以两个装饰器叠加为例,其顺序如下:显然,前面函数定义的部分仅仅是定义;到两个 @decorator 处时,会由里到外的一步步执行 decorator 函数的准备部分(暂且叫这个吧。。就是下面的 wrapper 函数外面的部分)。因为准备部分返回函数名索引,所以它接下来就会去执行下一个装饰器函数的准备部分。再然后经过 test 函数定义部分,准备完毕,开始调用。调用的时候,也许是因为之前返回的函数名索引是外层的在后面,现在开始调用的时候,外层的就会先调用。调用的大致顺序类似于这个模型:(out2前(out1前 test )out2后 )out2后,左括号代表被装饰函数前面的装饰,右括号代表被装饰函数后面的装饰。
def decorator1(func): print('--out11--') def wrapper1(*args, **kwargs): print("--in11--") ret = func(*args, **kwargs) print("--in12--") return ret print("--out12--") return wrapper1 def decorator2(func): print('--out21--') def wrapper2(*args, **kwargs): print("--in21--") ret = func(*args, **kwargs) print("--in22--") return ret print("--out22--") return wrapper2 @decorator2 @decorator1 def test(): print("--test run--") return 1 * 2 print("准备完毕,下面开始调用:") test() # outputs: --out11-- # --out12-- # --out21-- # --out12-- # 准备完毕,下面开始调用: # --in21-- # --in11-- # --test run-- # --in12-- # --in22-- # Out[11]: 2 # 这是 IPython 中才会显示出运算结果的, py 文件中是不会打印出来的, 因为它没有打印啊!
类装饰器:当应用的某些部分还比较脆弱时,异常也许是需要更紧急关注的事情。比方说有时你只想打日志到一个文件。而有时你想把引起你注意的问题发送到一个email,同时也保留日志,留个记录。这是一个使用继承
的场景,但目前为止我们只看到过用来构建装饰器的函数。不过,类也可以用来构建装饰器。
# 以下文的日志场景为例 from functools import wraps class Logit(object): # __init__函数内接受装饰器参数, __call__函数内实现具体装饰器结构即可 def __init__(self, logfile='out.log'): self.logfile = logfile def __call__(self, func): @wraps(func) def wrapped_function(*args, **kwargs): log_string = func.__name__ + 'was called' with open(self.logfile, 'a') as opened_file: # save to the named file opened_file.write(log_string + '\n') self.notify() # send a message return func(*args, **kwargs) return wrapped_function def notify(self): # Logit 只打日志, 不做别的 pass @Logit() # Logit() 生成一个实例; 类中实现 __call__ 方法, 可以使实例如普通函数一样调用 def my_func1(): pass class EmailLogit(Logit): # 继承 Logit def __init__(self, email='admin@myproject.com', *args, **kwargs): self.email = email super(EmailLogit, self).__init__(*args, **kwargs) def notify(self): # send a email to self.eamil # 这里没有写了 pass
从现在开始,@email_logit() 将会和 @logit() 产生同样的效果,但是在打日志的基础上,还会多发送一封邮件给管理员。
目前只是看到这样一篇公众号文章,暂时体会不深,留待后续。装饰器的 @ ,不要再乱用了
日志
# 指定一个用于输出的日志文件 from functools import wraps def logit(logfile='out.log'): def logging_decorator(func): @wraps(func) def wrapped_function(*args, **kwargs): log_string = func.__name__ + " was called" with open(logfile, 'a') as opened_file: # 打开 logfile, 并写入内容 opened_file.write(log_string + '\n') # 现在将日志打到指定的logfile return func(*args, **kwargs) return wrapped_function return logging_decorator @logit() def myfunc1(): pass myfunc1() # Output: myfunc1 was called # 现在一个叫做 out.log 的文件出现了, 里面的内容就是上面的字符串 @logit(logfile='func2.log') def myfunc2(): pass myfunc2() # Output: myfunc2 was called # 现在一个叫做 func2.log 的文件出现了,里面的内容就是上面的字符串
权限校验等场景
# 检查某个人是否被授权去使用一个web应用的端点(endpoint) from functools import wraps def requires_auth(f): @wraps(f) def decorated(*args, **kwargs): auth = request.authorization if not auth or not check_auth(auth.username, auth.password): authenticate() return f(*args, **kwargs) return decorated
函数缓存
函数缓存允许我们将一个函数对于给定参数的返回值缓存起来。当一个 I/O 密集的函数被频繁使用相同的参数调用的时候,函数缓存可以节约时间。其实是将函数的输入和返回值缓存,下次再调用此函数时,若有相同的输入,那么返回值就可直接从缓存中提取,函数体本身无需再执行,用一点内存,加快了速度。
在 Python 3.2 版本以前只能自定义实现。在 Python 3.2 以后版本,有个 lru_cache
的装饰器,允许我们将一个函数的返回值快速地缓存或取消缓存。
# 实现一个佩波纳契计算器 from functools import lru_cache @lru_cache(maxsize=32, typed=False) def fibs(n): if n < 2: return n return fibs(n-1) + fibs(n-2) print([fibs(n) for n in range(10)]) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
maxsize: 最多缓存的次数,超过这个值之后,旧的结果就会被释放,然后将新的计算结果进行缓存,其值应设为 2 的幂,默认是128;若为None, 则无限制;
typed 默认为 False;为 True 时:则不同参数类型的调用将分别缓存, 如 f(3), f(3.0).
被 lru_cache 装饰的函数会有 cache_clear 和 cache_info 两个方法,分别用于清除缓存和查看缓存信息, 我们可以轻松地对返回值清空缓存:
fibs.cache_info() # CacheInfo(hits=16, misses=10, maxsize=32, currsize=10) fibs.cache_clear() fibs.cache_info() # CacheInfo(hits=0, misses=0, maxsize=32, currsize=0) 参数尚未解识
执行函数后清理功能
执行函数前预备处理
函数执行时间统计
参考资料:《Python 进阶》