【摘要】
通过上一章节闭包函数和简单装饰器的讲解,大家应该能够理解Python中的装饰器的运行原理是怎样的了,这一节就需要讲解一些进阶的知识,并将功能进行泛化,使其更具有通用性和严谨性。
【可变参数】
可以先思考一下,上述代码中的foo
函数只接受一个num
参数,那我们定义的timer
装饰器岂不是不能用来去装饰其他函数了?
这里就需要和之前的番外篇中提到的可变参数进行结合(可变参数的讲解可以跳转到【番外篇】可变参数中了解),代码如下:
import time
def timer(func):
def inner(*args, **kwargs):
"""doc of inner"""
start = time.time()
ret = func(*args, **kwargs)
end = time.time()
print("%s cost %f seconds" % (func.__name__, end - start))
return ret
return inner
上述代码就是修改之后可以用来装饰所有函数的一个装饰器
1.因为inner
函数接受的是可变的位置参数和关键字参数,所以理论上就可以接受任意被装饰函数的任意参数。
2.同时被装饰的函数可能本身会带有返回值,所以还需要定义一个变量接受它,并将其返回。
【函数一致性】
当对一个函数使用上述的装饰器进行装饰时,函数的内置变量会发生改变,诸如__name__
和__doc__
,示例如下:
@timer
def foo(num):
"""doc of foo"""
time.sleep(num)
print(foo.__name__)
print(foo__doc__)
# 输出如下:
inner
doc of inner
从代码运行结果可以看出,最终的输出与定义的foo
函数的内置变量并不相同,原因是经过装饰器的装饰后,foo.__name__
等价于timer(foo).__name__
,foo.__doc__
等价于timer(foo).__doc__
。
所以为了保证程序定义和输出的一致性,需要做出一定的修改,Python提供了内置的方法可以应对该现象,代码如下:
from functools import wraps
import time
def timer(func):
@wraps(func)
def inner(*args, **kwargs):
"""doc of inner"""
start = time.time()
ret = func(*args, **kwargs)
end = time.time()
print("%s cost %f seconds" % (func.__name__, end - start))
return ret
return inner
wraps
这里做出的改动是在内函数inner
上加一个Python内置的装饰器wraps
,该装饰器的功能就是将func
参数的内置属性修改到inner
上,使最终返回到inner
函数看起来更像func
,具体的warps
的实现可以从源码中看出。
通过command+单击
跳转到wraps
函数内部,可以看到warps
函数有三个参数,再调用partial
函数,然后直接返回。
wraps
函数接收三个参数,分别如下 :
1.wrapped
该参数就是timer
中的func
,也就是使用timer
要装饰的函数。
2.assigned
该参数等于内置的一个全局变量WRAPPER_ASSIGNMENTS
,值为('__module__', '__name__', '__qualname__', '__doc__','__annotations__')
,这些值就是被装饰函数需要修改的内置属性
3.updated
该参数等于内置的另一个全局变量WRAPPER_UPDATES
,值为('__dict__',)
表示要被更新的属性。
partial
partial
函数翻译过来是叫偏函数,通俗的讲,调用偏函数就是对一个函数做一些额外的操作,然后再返回该函数的调用。
这听起来有点儿像装饰器,但其实并不完全相同。
在官方文档的描述中,这个函数的声明如下:functools.partial(func, *args, **keywords)
。
它的作用就是返回一个partial
对象,当这个partial
对象被调用的时候,就像通过func(*args, **kwargs)
的形式来调用func
函数一样。如果有额外的 位置参数(*args)* 或者 关键字参数(*kwargs) 被传给了这个partial
对象,那它们也都会被传递给func
函数,如果一个参数被多次传入,那么后面的值会覆盖前面的值。
所以wraps
函数其实就是返回了一个partial
对象,该对象是对update_wrapper
的修饰,会将wraps
中的wrapped、assigned、updated
参数都传递到update_wrapped
中。
update_wrapper
最后只需要搞懂update_wrapper
函数就可以了,现在跳转进去看一下该函数的源码,如下图
通过源码可以看出,该函数接收一个wrapper
参数,然后通过getattr
获取wrapped
中的所有assigned
属性,然后通过setattr
一一设置给wrapper
,并且将wrapped
函数的__dict__
属性全部更新到wrapper
的__dict__
上(因为一个函数的__dict__
是字典类型,所以可以直接通过update
方法更新字典),最终返回wrapper
函数。
经过update_wrapper
函数之后,wrapped
函数(即foo
函数)的所有内置属性,都会被更新到wrapper
函数(即inner
函数)上去。
整体理解
1.对inner
函数加上@wraps(func)
的装饰,等价于wraps(func)(inner)
2.wraps(func)
等价于partial(update_wrapper, wrapped=func, assigned=assigned, updated=updated)
3.wraps(func)(inner)
等价于partial(update_wrapper, wrapped=func, assigned=assigned, updated=updated)(inner)
4.partial(update_wrapper, wrapped=func...)(inner)
等价于update_wrapper(inner, wrapped=func, , assigned=assigned, updated=updated)
所以对inner
函数使用@wraps(func)
的装饰后,最终timer
函数中的返回的inner
函数就会具备func
(即foo
)的所有属性;这样对foo
函数使用@timer
进行装饰才可以保证函数信息的一致性。
【带参数的装饰器】
通过wraps
的学习,大家可能已经发现,wraps
也是一个装饰器,但它却可以接受额外的参数,而自定义的timer
却只能接受被装饰的函数作为参数。
其实我们同样也可以对timer
进行修改,将其变成带参数的装饰器,方法如下:
from functools import wraps
import time
def timer(timeout=10):
def func_log(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""doc of inner"""
start = time.time()
ret = func(*args, **kwargs)
end = time.time()
print("%s cost %f seconds" % (func.__name__, end - start))
if end-start > timeout:
raise Exception("%s run timeout" % func.__name__)
return ret
return wrapper
return func_log
@timer(5)
def foo(num):
time.sleep(num)
if __name__ == "__main__":
foo(12)
上述代码经过修改后,timer
可以接受一个timeout
参数,这个参数默认值为10
,表示被装饰的函数如果执行超过10s,则判定为超时异常。
其实带参数的装饰器就是在原先的函数外面又包了一层函数,具体逻辑如下:
1.调用foo
函数foo()
等价于timer(5)(foo)(12)
2.timer(5)(foo)(12)
等价于func_log(foo)(12)
3.func_log(foo)(12)
等价于wraper(12)
【类装饰器】
上面的装饰器是由函数来完成,实际上由于Python的灵活性, 用类也可以实现一个装饰器。
类能实现装饰器的功能, 是由于当我们调用一个对象时,实际上调用的是它的 call 方法。
import time
class Cache:
__cache = {}
def __init__(self, func):
self.func = func
def __call__(self):
# 如果缓存字典中有这个方法的执行结果
# 直接返回缓存的值
if self.func.__name__ in Cache.__cache:
return Cache.__cache[self.func.__name__]
# 计算方法的执行结果
value = self.func()
# 将其添加到缓存
Cache.__cache[self.func.__name__] = value
# 返回计算结果
return value
@Cache
def long_time_func():
time.sleep(5)
return "ok"
start = time.time()
print(long_time_func())
end = time.time()
print("func cost %f seconds" % (end-start))
start = time.time()
print(long_time_func())
end = time.time()
print("func cost %f seconds" % (end-start))
# 输出内容如下
ok
func cost 5.004846 seconds
ok
func cost 0.000034 seconds
上述类装饰器实现的功能就是将函数的调用结果进行缓存。
由于类装饰器在平时的编程过程中并不多见,所以大家可以先简单理解上述示例代码了解原理即可。
【总结】
这可能目前番外篇中最硬核的一次讲解了,其中涉及到的源码都是大家并不常看到的部分,并且可能有的朋友发现,Python内置的源码,不管是从抽象角度,代码注释规范,参数命名,以及异常处理都十分的优雅,这其实也是阅读源码最大的好处,这会对我们今后的编程起到潜移默化的提升作用。
最后希望大家可以仔细阅读理解这一章节的内容,对Python装饰器能够有一个完整深入的理解。
欢迎大家添加我的个人公众号【Python玩转自动化运维】加入读者交流群,获取更多干货内容