通过上一章节闭包函数和简单装饰器的讲解,大家应该能够理解Python中的装饰器的运行原理是怎样的了,这一节就需要讲解一些进阶的知识,并将功能进行泛化,使其更具有通用性和严谨性。
【可变参数】可以先思考一下,上述代码中的foo函数只接受一个num参数,那我们定义的timer装饰器岂不是不能用来去装饰其他函数了?
这里就需要和之前的番外篇中提到的可变参数进行结合(可变参数的讲解可以跳转到【番外篇】可变参数中了解),代码如下:
import timedef 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__,示例如下:
@timerdef foo(num): """doc of foo""" time.sleep(num) print(foo.__name__)print(foo__doc__)# 输出如下:innerdoc of inner
从代码运行结果可以看出,最终的输出与定义的foo函数的内置变量并不相同,原因是经过装饰器的装饰后,foo.__name__等价于timer(foo).__name__,foo.__doc__等价于timer(foo).__doc__。
所以为了保证程序定义和输出的一致性,需要做出一定的修改,Python提供了内置的方法可以应对该现象,代码如下:
from functools import wrapsimport timedef 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__',)表示要被更新的属性。
partialpartial函数翻译过来是叫偏函数,通俗的讲,调用偏函数就是对一个函数做一些额外的操作,然后再返回该函数的调用。
这听起来有点儿像装饰器,但其实并不完全相同。
在官方文档的描述中,这个函数的声明如下: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 wrapsimport timedef 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 timeclass 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 @Cachedef 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))# 输出内容如下okfunc cost 5.004846 secondsokfunc cost 0.000034 seconds
上述类装饰器实现的功能就是将函数的调用结果进行缓存。
由于类装饰器在平时的编程过程中并不多见,所以大家可以先简单理解上述示例代码了解原理即可。
【总结】 这可能目前番外篇中最硬核的一次讲解了,其中涉及到的源码都是大家并不常看到的部分,并且可能有的朋友发现,Python内置的源码,不管是从抽象角度,代码注释规范,参数命名,以及异常处理都十分的优雅,这其实也是阅读源码最大的好处,这会对我们今后的编程起到潜移默化的提升作用。
最后希望大家可以仔细阅读理解这一章节的内容,对Python装饰器能够有一个完整深入的理解。
欢迎大家添加我的个人公众号【Python玩转自动化运维】加入读者交流群,获取更多干货内容