【自动化运维番外篇】Python装饰器-进阶

【摘要】

通过上一章节闭包函数和简单装饰器的讲解,大家应该能够理解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玩转自动化运维】加入读者交流群,获取更多干货内容
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值