• Python装饰器


    引入装饰器

    如果想在一个函数执行前后执行一些别的代码,比如打印一点日志用来输出这个函数的调用情况那应该怎么做呢?

    #!/usr/bin/env python
    # coding=utf-8
    
    def logger(fn):									# 函数作为参数即fn可以为任何参数
        def wrap(*args, **kwargs):					# 可变参数args和kwargs
            print('call {}'.format(fn.__name__))	
            ret = fn(*args, **kwargs)				# 函数调用时的参数解构
            print('{} called'.format(fn.__name__))
            return ret								# 返回函数的返回值
        return wrap
    
    def add(x, y):
        return x + y
    
    logger_add = logger(add)
    print(logger_add.__name__)
    print(logger_add)
    ret = logger_add(3, 5)
    print(ret)
    
    #输出结果:
    wrap
    <function logger.<locals>.wrap at 0x7fba35f4fe18>
    call add
    add called
    8
    

    也可以用以下方式来实现这种效果

    @logger                                                                                  
    def add(x, y):                                                                            
    	return x + y                                                                         ret = add(3, 5)                                                                      
    print(ret) 
    
    # 输出结果:
    call add
    add called
    8
    

    这就是Python装饰器的一个简单使用

    什么是装饰器?

    装饰器是用于软件设计模式的名称。 装饰器可以动态地改变函数,方法或类的功能,而不必直接使用子类或改变被装饰的函数的源代码。Python装饰器是对Python语法的一种特殊改变,它允许我们更方便地修改函数,方法以及类。

    当我们按照以下方式编写代码时:

    @logger
    def add(x, y):
    	...
    

    和单独执行下面的步骤是一样的:

    def add(x, y):
    	...
    logger_add = logger(add)
    

    装饰器内部的代码一般会创建一个新的函数,利用*args**kwargs来接受任意的参数,上述代码中的wrap()函数就是这样的。在这个函数内部,我们需要调用原来的输入函数(即被包装的函数,它是装饰器的输入参数)并返回它的结果。但是也可以添加任何想要添加的代码,比如在上述代码中输出函数的调用情况,也可以添加计时处理等等。这个新创建的wrap函数会作为装饰器的结果返回,取代了原来的函数。

    所以在Python中,装饰器的参数是一个函数, 返回值是一个函数的函数

    装饰器的示例:计时处理

    写一个装饰器,用来计算一个函数的执行时间

    import time
    
    def timethis(fn):
        def wrap(*args, **kwargs):
            start = time.time()
            ret = fn(*args, **kwargs)
            end = time.time()
            print(fn.__name__, end - start)
            return ret
        return wrap
    

    如果要对add函数计时:

    @timethis
    def add(x, y):
        return x + y
    
    ret = add(3, 5)
    print(ret)
    
    # 输出结果
    add 1.9073486328125e-06
    8
    

    如果要对sleep函数计时:

    @timethis
    def sleep(x):
        time.sleep(x)
    
    sleep(3)
    
    # 输出结果
    sleep 3.003262519836426
    

    保存被装饰函数的元信息

    什么是函数的元信息

    比如装饰器的名称,装饰器的doc等等。我们可以使用dir函数列出函数的所有元信息:dir(sleep),输出结果如下

    ['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
    

    可以看到有很多的元信息,我们比较常用的是__name____doc__这两个属性

    而且__doc__属性也就是函数的文档信息,可以通过help函数查看得到

    为什么要保存被装饰函数的元信息

    改写装饰器的应用1:计时处理中的sleep函数如下:

    @timeit
    def sleep(x):
        '''This function is sleep.'''
        time.sleep(x)
    
    sleep(3)
    print(sleep.__name__)
    print(sleep.__doc__)
    

    以上代码输出结果如下:

    3.0032713413238525
    wrap
    None
    

    可以发现sleep函数的__name__是wrap,而不是sleep,而__doc__属性为空,而不是sleep函数的docstring。也就是说经过装饰器装饰过后的函数的元信息发生了改变,这时候如果程序需要函数的元信息,那么就有问题了。

    如何保存被装饰函数的元信息

    方案1:手动给被装饰函数的元信息赋值

    __name____doc__这两个属性为例

    import time
    
    def timeit(fn):
        def wrap(*args, **kwargs):
            start = time.time()
            ret = fn(*args, **kwargs)
            end = time.time()
            print(end - start)
            return ret
        wrap.__doc__ = fn.__doc__	# 手动赋值__doc__信息
        wrap.__name__ = fn.__name__	# 手动赋值__name__信息
        return wrap
    
    @timeit
    def sleep(x):
        '''This function is sleep.'''
        time.sleep(x)
    
    if __name__ == "__main__":
        sleep(3)
        # print(dir(sleep))
        print(sleep.__name__)
        print(sleep.__doc__)
    

    输出结果如下

    3.004547119140625
    sleep
    This function is sleep.
    

    可以发现,__name____doc__这两个属性确实赋值成功了。

    我们可以将元信息赋值的过程改写为函数,如下

    import time
    
    
    def copy_properties(src, dst):	# 将元信息赋值的过程改成函数copy_properties
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
    
    def timeit(fn):
        def wrap(*args, **kwargs):
            start = time.time()
            ret = fn(*args, **kwargs)
            end = time.time()
            print(end - start)
            return ret
        copy_properties(fn, wrap)	# 调用copy_properties函数修改元信息
        return wrap
    
    @timeit
    def sleep(x):
        '''This function is sleep.'''
        time.sleep(x)
    
    if __name__ == "__main__":
        sleep(3)
        # print(dir(sleep))
        print(sleep.__name__)
        print(sleep.__doc__)
    

    这样修改后,同样可以解决问题。

    继续修改copy_properties函数,使得copy_properties可以返回一个函数

    def copy_properties(src):
        def _copy(dst):	# 内置一个_copy函数便于返回
            dst.__name__ = src.__name__
            dst.__doc__ = src.__doc__
        return _copy
    
    def timeit(fn):
        def wrap(*args, **kwargs):
            start = time.time()
            ret = fn(*args, **kwargs)
            end = time.time()
            print(end - start)
            return ret
        copy_properties(fn)(wrap)	# 调用copy_properties函数
        return wrap
    

    同样可以问题。

    如果继续修改copy_properties函数,使得_copy函数是一个装饰器,传入dst,返回dst,修改如下:

    def copy_properties(src):	# 先固定dst,传入src
        def _copy(dst):	# 传入dst
            dst.__name__ = src.__name__
            dst.__doc__ = src.__doc__
            return dst	# 返回dst
        return _copy	# 返回一个装饰器
    
    def timeit(fn):
        @copy_properties(fn)	# 带参数装饰器的使用方法
        def wrap(*args, **kwargs):
            start = time.time()
            ret = fn(*args, **kwargs)
            end = time.time()
            print(end - start)
            return ret
        return wrap
    

    copy_properties在此处返回一个带参数的装饰器,因此可以直接按照装饰器的使用方法来装饰wrap函数,这个修改copy_properties函数的过程称为函数的柯里化。

    方案2:使用functools库的@wraps装饰器

    functools库的@wraps装饰器本质上就是copy_properties函数的高级版本:包含更多的函数元信息。首先查看wrap装饰器的帮助信息:

    import functools
    help(functools.wraps)
    

    wrap装饰器函数的原型是:

    wraps(wrapped, assigned=('module', 'name', 'qualname', 'doc', 'annotations'), updated=('dict',))
    

    所以这个装饰器会复制module等元信息,但是也不是所有的元信息,并且会更新dict。

    使用示例如下:

    import time
    import functools
    
    def timeit(fn):
        @functools.wraps(fn)	# wraps装饰器的使用
        def wrap(*args, **kwargs):
            start = time.time()
            ret = fn(*args, **kwargs)
            end = time.time()
            print(end - start)
            return ret
        return wrap
    
    def sleep(x):
        time.sleep(x)
    
    print(sleep.__name__)
    print(sleep.__doc__)
    

    编写一个带参数的装饰器

    如果上述的timeit装饰器,我们需要输出执行时间超过若干秒(比如一秒)的函数的名称和执行时间,那么就需要给装饰器传入一个参数s,表示传入的时间间隔,默认为1s。

    我们可以给写好的装饰器外面包一个函数timeitS,时间间隔s作为这个函数的参数传入,并且对内层的函数可见,然后这个函数返回写好的装饰器。

    import time
    import functools
    
    
    def timeitS(s):
        def timeit(fn):
            @functools.wraps(fn)
            def wrap(*args, **kwargs):
                start = time.time()
                ret = fn(*args, **kwargs)
                end = time.time()
                if end - start > s:
                    print('call {} takes {}s'.format(fn.__name__, end - start))
                else:
                    print('call {} takes {}s less than {}'.format(fn.__name__, end - start, s))
                return ret
            return wrap
        return timeit
    
    @timeitS(2)
    def sleep(x):
        time.sleep(x)
    
    sleep(3)
    sleep(1)
    
    

    输出结果如下:

    call sleep takes 3.001342535018921s
    call sleep takes 1.000471830368042s less than 2
    

    所以,我们可以将带参数的装饰器理解为:

    • 带参数的装饰器就是一个函数, 这个函数返回一个不带参数的装饰器

    记得帮我点赞哦!

    精心整理了计算机各个方向的从入门、进阶、实战的视频课程和电子书,按照目录合理分类,总能找到你需要的学习资料,还在等什么?快去关注下载吧!!!

    resource-introduce

    念念不忘,必有回响,小伙伴们帮我点个赞吧,非常感谢。

    我是职场亮哥,YY高级软件工程师、四年工作经验,拒绝咸鱼争当龙头的斜杠程序员。

    听我说,进步多,程序人生一把梭

    如果有幸能帮到你,请帮我点个【赞】,给个关注,如果能顺带评论给个鼓励,将不胜感激。

    职场亮哥文章列表:更多文章

    wechat-platform-guide-attention

    本人所有文章、回答都与版权保护平台有合作,著作权归职场亮哥所有,未经授权,转载必究!

  • 相关阅读:
    mysql定时删除数据
    【video__播放】视频播放插件video7.4.1使用
    【ueditor__使用】百度ueditor富文本编辑器
    【 CSS__样式收集】常用样式案例收集整理
    【Layui__监听button】在form中监听按钮事件
    【Layui__分页和模板】分页和模板的整合使用
    【Layui__模板引擎】layui.laytpl
    DataTable转list
    反射方法创建委托
    EPPlusHelper
  • 原文地址:https://www.cnblogs.com/CHLL55/p/13756063.html
Copyright © 2020-2023  润新知