• python装饰器的使用方法


    0. 前言

    装饰器在 python 中使用的频率非常高,它可以在不改动原有函数的基础上对其进行增强功能。

    下面主要是介绍装饰器的各种用法,并理解其运行过程。

    1. 使用

    1.1 在函数上添加装饰器

    decro 是一个装饰器函数,其实现是将内部的函数 wrapper 作为返回值返回出去。

    在函数 test 上添加 @decro 进行使用,可以将本函数作为一个参数传入到 decro 函数中,然后,然后得到的是装饰器函数内部返回的函数 wrapper, 我们在调用 test 方法时,其实调用的是装饰器返回的 wrapper 函数,该函数中会调用被装饰的函数 test

    def decro(func):  
        def wrapper(*args, **kwargs):  
            print("wrapper")  
            return func()  
      
        return wrapper
    
    
    @decro  
    def test():  
        print("test")  
    
    
    test()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    输出:

    wrapper
    test
    
    • 1
    • 2

    1.2 装饰器的运行过程

    装饰器时在被装饰的函数定义之后立即运行的,当执行到@decro 装饰 test 函数时,会马上执行函数 decro,然后将 wrapper 给返回出去。

    def decro(func):  
        print("decro")  
      
        def wrapper(*args, **kwargs):  
            print("wrapper")  
            return func()  
      
        return wrapper  
      
    print("start")  
      
    @decro  
    def test():  
        print("test")  
      
    print("end")
    
    test()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    输出如下,可以看到先执行了 start,然后马上执行了装饰器 decro,然后再执行 end,当我们调用 test 函数时,执行了装饰器内部函数 wrapper,然后再调用被装饰的函数 test

    start
    decro
    end
    wrapper
    test
    
    • 1
    • 2
    • 3
    • 4
    • 5

    1.3 保存原函数信息

    在使用装饰器时,调用的原方法已经被替换为装饰器返回的新方法了,所以方法的元信息已经被替换了, 通过 name、doc 得到的元数据已经被替换成了新方法的。

    def decro(func):  
        def wrapper(*args, **kwargs):  
            """ wrapper doc """  
            print("wrapper")  
            return func(*args, **kwargs)  
      
        return wrapper  
      
      
    @decro  
    def test():  
        """ test doc """  
        print("test")  
      
      
    print("test's __name__ = {}".format(test.__name__))  
    print("test's __doc__ = {}".format(test.__doc__))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    输出如下,会发现函数 test 的函数信息 __name____doc__ 变成 wrapper 的信息。

    test's __name__ = wrapper
    test's __doc__ =  wrapper doc
    
    • 1
    • 2

    但是我们不想要改变原方法的元信息,这个时候需要使用 functools.wraps 解决。

    from functools import wraps  
      
    def decro(func):  
      
        @wraps(func)  
        def wrapper(*args, **kwargs):  
            """ wrapper doc """  
            print("wrapper")  
            return func(*args, **kwargs)  
      
        return wrapper  
      
      
    @decro  
    def test():  
        """ test doc """  
        print("test")  
      
      
    print("test's __name__ = {}".format(test.__name__))  
    print("test's __doc__ = {}".format(test.__doc__))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    输出如下,会发现,test 函数信息没有被替换掉,保证了函数的原汁原味。

    test's __name__ = test
    test's __doc__ =  test doc 
    
    • 1
    • 2

    1.4 调用原函数

    装饰器可以增强函数的功能,但是在某些场景我就想要使用原函数,而不想使用装饰之后的函数,可以通过调用__wrapped__来调用原函数。

    from functools import wraps  
      
    
    def decro(func):  
        @wraps(func)  
        def wrapper(*args, **kwargs):  
            """ wrapper doc """  
            print("wrapper")  
            return func(*args, **kwargs)  
      
        return wrapper  
      
      
    @decro  
    def test():  
        """ test doc """  
        print("test")  
      
      
    test.__wrapped__()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    输出如下,输出 text,而没有输出 wrapper,说明调用的是原函数。

    test
    
    • 1

    1.5 带参数的装饰器

    还有这么一种场景,我们想要在装饰器中添加参数。

    想要通过参数决定日志级别,这里的 logged 接收 level 参数并将它作用在内部函数中,返回值是将 decro 函数返回,然后再将函数 test 作为参数从传入到 decro 函数中,再将 wrapper 返回,最终 test 还是替换成了 wrapper,在该方法中使用了传入的 ERROR 的日志级别。

    from functools import wraps  
    import logging  
      
      
    def logged(level):  
        def decro(func):  
            @wraps(func)  
            def wrapper(*args, **kwargs):  
                """ wrapper doc """  
                logging.log(level, func.__name__)  
                return func(*args, **kwargs)  
      
            return wrapper  
      
        return decro  
      
      
    @logged(level=logging.ERROR)  
    def test():  
        """ test doc """  
        print("test")  
      
      
    test()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    输出了 ERROR 日志级别的日志:

    ERROR:root:test
    test
    
    • 1
    • 2

    1.6 带可选参数的装饰器

    上面实现的装饰器是必须要带上参数的,但是有的时候,我们不需要带参数,那么该如何实现?

    装饰器的 func 默认值为 None,当传入 level 参数时,则返回偏函数 partial ,该函数会基于 logged 创建一个仅包含 level 的新的函数,这个新的函数作为新的装饰器来装饰 add 函数。

    当没有传入 level 参数时,就和普通的装饰器一样使用。

    from functools import wraps, partial  
    import logging  
      
      
    def logged(func=None, level=logging.INFO):  
        if func is None:  
            return partial(logged, level=level)  
      
        @wraps(func)  
        def wrapper(*args, **kwargs):  
            logging.log(level, func.__name__)  
            return func(*args, **kwargs)  
      
        return wrapper  
      
      
    @logged(level=logging.ERROR)  
    def add(a, b):  
        print("add")  
        return a + b  
      
      
    @logged  
    def add2(a, b):  
        print("add2")  
        return a + b  
      
      
    print(add(1, 2))  
    print("-" * 10)  
    print(add2(1, 2))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    输出如下,add 函数的装饰器传入了日志级别为 ERROR 的参数,输出了 ERROR 的日志,而add2 没有。

    ERROR:root:add
    add
    3
    ----------
    add2
    3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    1.7 在类上添加装饰器

    上面都是使用装饰器来增强函数的功能,但它还可以增强类的功能,对类进行装饰。

    下面的例子中,decro 将被装饰的类 Demo 传入进来,主要是将其类中 __getattribute__ 方法替换成了 new_getattribute 方法。

    def decro(cls):  
        orig_getattribute = cls.__getattribute__  
      
        def new_getattribute(self, name):  
            print("get name = {}".format(name))  
            return orig_getattribute(self, name)  
      
        cls.__getattribute__ = new_getattribute  
        return cls  
      
      
    @decro  
    class Demo:  
      
        def __init__(self, num):  
            self.num = num  
      
      
    d = Demo(1)  
    print(d.num)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    输出如下,获取 num 的值时,调用了装饰器替换的 new_getattribute 方法。

    get name = num
    1
    
    • 1
    • 2

    1.8 类装饰器

    之前都是使用函数方法来定义装饰器,但其实也可以通过类来定义装饰器。

    在类装饰器中定义__init__方法,被它装饰的函数会被传入到 func 参数中,这个时候该类装饰器已经被实例化了,也就是将该实例对象替换了被装饰的函数 say。

    当我们调用 say 函数时,其实调用的是类装饰器的对象,这个时候会调用__call__方法,该方法中可以对原函数进行增强,并进行调用原方法。

    class logger(object):  
        def __init__(self, func):  
            self.func = func  
      
        def __call__(self, *args, **kwargs):  
            print("the function {func}() is running...".format(func=self.func.__name__))  
            return self.func(*args, **kwargs)  
      
      
    @logger  
    def say(something):  
        print("say {}!".format(something))  
      
      
    say("hello")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    输出如下:

    the function say() is running...
    say hello!
    
    • 1
    • 2

    1.9 暴露被装饰的元信息

    这个时候会出现和函数装饰器一样的问题,那就是被装饰的函数的元信息已经被替换掉了,这个时候我们还是想保留原有的原信息。

    还是使用 wraps 函数来解决该问题。

    from functools import wraps  
      
      
    class logger(object):  
        """ logger doc """  
      
        def __init__(self, func):  
            wraps(func)(self)  
      
        def __call__(self, *args, **kwargs):  
            print("the function {func}() is running...".format(func=self.__wrapped__.__name__))  
            return self.__wrapped__(*args, **kwargs)  
      
      
    @logger  
    def say(something):  
        """ say doc """  
        print("say {}!".format(something))  
      
      
    say("hello2")  
    print(say.__doc__)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    输出的是 say 方法的 doc:

    the function say() is running...
    say hello2!
     say doc 
    
    • 1
    • 2
    • 3

    1.10 带参数的类装饰器

    那么带参数的类装饰器该如何实现呢?

    __init__ 方法中接收装饰器传入的参数,保存起来,然后再通过 __call__ 函数将内部函数 wrapper 给返回出去,这个时候被装饰的函数已经被 wrapper 给替换了。

    class logger(object):  
      
        def __init__(self, level="INFO"):  
            self.level = level  
      
        def __call__(self, func):  
            def wrapper(*args, **kwargs): 
    	        print("[{level}]: the function {func}() is running...".format(level=self.level, func=func.__name__)) 
                func(*args, **kwargs)  
      
            return wrapper  
      
      
    @logger(level="ERROR")  
    def say(something):  
        print("say {}!".format(something))  
      
      
    say("hello2")
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    输出如下,调用 say 函数也就是调用 wrapper 函数:

    [ERROR]: the function say() is running...
    say hello2!
    
    • 1
    • 2

    2. 总结

    装饰器的用法很多,封装成库,给其他人使用也非常的方便,我们需要理解它的运行过程,才能更好的使用它。

    3. 参考资料

    • https://python3-cookbook.readthedocs.io/zh_CN/latest/c09/p01_put_wrapper_around_function.html

    欢迎关注,互相学习,共同进步~

    我的个人博客
    公众号:编程黑洞

  • 相关阅读:
    TypeScript中的Symbol,确实唯一。。。
    (附源码)php二手服装网站 毕业设计 201711
    第一篇【传奇开心果系列】Python深度学习库技术点案例示例:深度解读深度学习在自动驾驶领域的应用
    Scala基础【入门及安装】
    VCS工具学习笔记(6)
    交互与前端14 Tabulator 表格实践2
    【阿旭机器学习实战】【26】逻辑斯蒂回归----糖尿病预测实战
    设计模式之构造者(Builder)模式
    视频编解码从H.264到H.266:浅析GB28181安防视频汇聚EasyCVR视频压缩技术
    Python tkinter用iconphoto方法修改窗口标题的图片
  • 原文地址:https://blog.csdn.net/qq_22918243/article/details/127484172