1.装饰器的使用
当你希望给一个函数前后永久性地增加新功能,又不想修改原代码时可以使用装饰器。[或者是希望对不同的函数前后添加一致的功能时]函数和新功能都可以变。
2.与闭包的区分
一个向外部函数传入函数引用(装饰器),一个传入变量值(闭包)。
3.装饰器工作原理
装饰器其实只是嵌套函数的一种应用。它们封装一个函数,并且用这样或者那样的方式来修改它的行为。内部函数体中,可以在调用指定函数前后添加需要执行的指令,最后在外部函数体的末尾返回内部函数名即可。
@:只是一个语法糖而已,只是生成一个被装饰的函数的一个简短方式。另外,嵌套函数内部定义的内部函数在嵌套函数体外是不能访问的:
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 。
4.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!
5.蓝本规范
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
6.几种常见形式
装饰带参数的函数传参: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() 产生同样的效果,但是在打日志的基础上,还会多发送一封邮件给管理员。
7.不要滥用装饰器
目前只是看到这样一篇公众号文章,暂时体会不深,留待后续。装饰器的 @ ,不要再乱用了
8.使用场景
-
日志
# 指定一个用于输出的日志文件 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 进阶》