美文网首页
Python装饰器

Python装饰器

作者: 莫忘初心_倒霉熊 | 来源:发表于2020-02-18 17:18 被阅读0次

    装饰器

    装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
    概括的讲,装饰器的作用就是为已经存在的函数或对象添加额外的功能

    最简单的装饰器

    def debug(func):
        def wrapper():
            print("这是一个装饰器")
            return func()
        return wrapper
    
    # 等价于say = debug(say)
    @debug
    def say():
        print("hello")
    
    say()
    # 输出如下:
    # 这是一个装饰器
    # hello
    

    这是最简单的装饰器,但是有一个问题,如果被装饰的函数需要传入参数,那么这个装饰器就坏了。因为返回的函数并不能接受参数,你可以指定装饰器函数wrapper接受和原函数一样的参数,比如:

    带有参数的装饰器

    def debug(func):
        # 指定一毛一样的参数
        def wrapper(something):
            print("这是一个装饰器")
            return func(something)
        # 返回包装过函数
        return wrapper
    
    @debug
    def say(something):
        print("hello %s!" % something)
    
    say("laowang")
    
    # 输出如下:
    # 这是一个装饰器
    # hello laowang!
    

    这样你就解决了一个问题,但又多了N个问题。因为函数有千千万,你只管你自己的函数,别人的函数参数是什么样子,鬼知道?还好Python提供了可变参数*args和关键字参数**kwargs,有了这两个参数,装饰器就可以用于任意目标函数了。

    任意参数的装饰器

    def debug(func):
        # 指定任意参数的装饰器
        def wrapper(*args, **kwargs):
            print("这是一个装饰器")
            return func(*args, **kwargs)
        return wrapper
    
    @debug
    def say1(arg1):
        print("hello %s" % arg1)
    
    @debug
    def say2(arg1,arg2):
        print("hello %s and %s" % (arg1,arg2))
    
    @debug
    def say3(arg1):
        print("hello %s and %s" % (arg1["zhangsan"],arg1["lisi"]))
    
    say1("zhangsan")
    say2(*["zhangsan","lisi"])
    say3({"zhangsan":"张三","lisi":"李四"})
    
    # 输出如下:
    # 这是一个装饰器
    # hello zhangsan
    # 这是一个装饰器
    # hello zhangsan and lisi
    # 这是一个装饰器
    # hello 张三 and 李四
    

    注意:装饰器在没有调用函数之前就已经装饰了

    def debug(func):
        print("开始装饰")
        def wrapper():
            print("这是一个装饰器")
            return func()
        return wrapper
    
    # 等价于say = debug(say)
    @debug
    def say():
        print("hello")
    
    # 输出如下:
    # 开始装饰
    

    ⚠️上面代码的say()函数并没有被调用,但是print("开始装饰")已经被打印了,这是因为@debug等价于say = debug(say),代码print("开始装饰")执行,但wrapper()函数是在say调用时才执行,所以print("这是一个装饰器")并没有打印

    多个装饰器对同一个函数装饰

    如果是多个装饰器对同一个函数装饰,那又会怎么执行呢???如下:

    def debug1(func):
        print("开始装饰器1")
        def wrapper(*args, **kwargs):
            print("这是一个装饰器1")
            return func(*args, **kwargs)
        return wrapper
    
    def debug2(func):
        print("开始装饰器2")
        def wrapper(*args, **kwargs):
            print("这是一个装饰器2")
            return func(*args, **kwargs)
        return wrapper
    
    @debug2
    @debug1
    def say(*args,**kwargs):
        print("hello,say")
    
    say()
    
    # 输出如下:
    # 开始装饰器1
    # 开始装饰器2
    # 这是一个装饰器2
    # 这是一个装饰器1
    # hello,say
    

    结论:如果是多个装饰器对同一个函数装饰,装饰时,从下到上进行装饰,即先装饰debug1(),在装饰debug2(),执行时,先执行debug2(),在执行debug1(),最后执行say()

    带有参数的装饰器

    假设我们前文的装饰器需要完成的功能不仅仅是能在进入某个函数后打出log信息,而且还需指定log的级别,那么装饰器就会是这样的。

    def logging(level):
        def wrapper(func):
            def inner_wrapper(*args, **kwargs):
                if level == 1:
                    print("----权限级别1----")
                elif level == 2:
                    print("----权限级别2----")
                return func(*args, **kwargs)
            return inner_wrapper
        return wrapper
    
    # 如果没有使用@语法,等同于
    # say1 = logging(1)(say)
    @logging(1)
    def say1():
        print ("say1")
    
    @logging(2)
    def say2():
        print ("say2")
    
    
    if __name__ == '__main__':
        say1()
        say2()
    
    # 输出如下:
    # ----权限级别1----
    # say1
    # ----权限级别2----
    # say2
    

    执行流程:
    1)调用logging()并且将1当作实参传递
    2)用上一步调用的返回值当作装饰器对say1函数进行装饰

    类装饰器

    装饰器函数其实是这样一个接口约束,它必须接受一个callable对象作为参数,然后返回一个callable对象。在Python中一般callable对象都是函数,但也有例外。只要某个对象重载了__call__()方法,那么这个对象就是callable的。

    class Test():
        def __call__(self):
            print 'call me!'
    
    t = Test()
    t()  # call me
    

    像__call__这样前后都带下划线的方法在Python中被称为内置方法,有时候也被称为魔法方法。重载这些魔法方法一般会改变对象的内部行为。上面这个例子就让一个类对象拥有了被调用的行为。

    回到装饰器上的概念上来,装饰器要求接受一个callable对象,并返回一个callable对象。那么用类来实现也是也可以的。我们可以让类的构造函数__init__()接受一个函数,然后重载__call__()并返回一个函数,也可以达到装饰器函数的效果。

    class Logging(object):
        def __init__(self, func):
            self.func = func
    
        def __call__(self, *args, **kwargs):
            print ("我是类装饰器")
            return self.func(*args, **kwargs)
    
    # 相当于say = Logging(say)
    @ Logging
    def say(*args, **kwargs):
        print ("Say")
    
    say()
    
    # 输出如下:
    # 我是类装饰器
    # Say
    

    带参数的类装饰器

    如果需要通过类形式实现带参数的装饰器,那么会比前面的例子稍微复杂一点。那么在构造函数里接受的就不是一个函数,而是传入的参数。通过类把这些参数保存起来。然后在重载__call__方法是就需要接受一个函数并返回一个函数。

    class Logging(object):
        def __init__(self, level):
            self.level = level
    
        # 接受函数
        def __call__(self, func):  
            def wrapper(*args, **kwargs):
                print("我是个类装饰器,level:%s" % self.level)
                func(*args, **kwargs)
            # 返回函数
            return wrapper 
    
    # 相当于say = Logging(1)(say)
    @Logging(1)
    def say():
        print("Say")
    
    say()
    
    # 输出如下:
    # 我是个类装饰器,level:1
    # Say
    
    

    执行流程:
    1)调用 Logging(1),实例化一个Logging对象,调用Logging的魔法方法__call__,并且将1当作实参传递
    2)用上一步调用的返回值当作装饰器对say1函数进行装饰

    装饰器里的那些坑

    1. 位置错误的代码
      在装饰器中我在各个可能的位置都加上了print语句,用于记录被调用的情况。你知道他们最后打印出来的顺序吗?如果你心里没底,那么最好不要在装饰器函数之外添加逻辑功能,否则这个装饰器就不受你控制了。以下是输出结果:
    def html_tags(tag_name):
        print ('begin outer function.')
        def wrapper_(func):
            print ("begin of inner wrapper function.")
            def wrapper(*args, **kwargs):
                content = func(*args, **kwargs)
                print ("<{tag}>{content}</{tag}>".format(tag=tag_name, content=content))
            print ('end of inner wrapper function.')
            return wrapper
        print ('end of outer function')
        return wrapper_
    
    @html_tags('b')
    def hello(name='Toby'):
        return 'Hello {}!'.format(name)
    
    hello()
    hello()
    hello()
    hello()
    
    # 输出如下:
    # begin outer function.
    # end of outer function
    # begin of inner wrapper function.
    # end of inner wrapper function.
    # <b>Hello Toby!</b>
    # <b>Hello Toby!</b>
    # <b>Hello Toby!</b>
    # <b>Hello Toby!</b>
    

    可以看到多次调用hello(),装饰器函数wrapper()会被调用多次,但是装饰器函数之外的print()打印只会调用一次。

    1. 错误的函数签名和文档
      装饰器装饰过的函数看上去名字没变,其实已经变了。
    def logging(func):
        def wrapper(*args, **kwargs):
            """print log before a function."""
            print("wrapper:",func.__name__)
            return func(*args, **kwargs)
        return wrapper
    
    @logging
    def say(something):
        """say something"""
        print ("say {}!".format(something))
    
    print(say.__name__)             # wrapper
    print(say.__doc__)              # print log before a function.
    

    为什么会这样呢?只要你想想装饰器的语法糖@代替的东西就明白了。@等同于这样的写法

    say = logging(say)
    

    logging其实返回的函数名字刚好是wrapper,那么上面的这个语句刚好就是把这个结果赋值给say,say的__name__自然也就是wrapper了,不仅仅是name,其他属性也都是来自wrapper,比如doc,source等等。

    使用标准库里的functools.wraps,可以基本解决这个问题。

    from functools import wraps
    
    def logging(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            """print log before a function."""
            print ("wrapper:",func.__name__)
            return func(*args, **kwargs)
        return wrapper
    
    @logging
    def say(something):
        """say something"""
        print("say {}!".format(something))
    
    print(say.__name__)     # say
    print(say.__doc__)      # say something
    
    1. 不能装饰@staticmethod 或者 @classmethod
    def logging(func):
        def wrapper(*args, **kwargs):
            print ("wrapper:")
            return func(*args, **kwargs)
        return wrapper
    
    class Car(object):
        def __init__(self):
            pass
    
        @logging  # 装饰实例方法,OK
        def run(self):
            pass
    
        # @logging  # 装饰静态方法,Failed
        # @staticmethod
        # def static_method():
        #     pass
    
        @logging  # 装饰类方法,Failed
        @classmethod
        def class_method(cls):
            pass
    
    car = Car()
    # car.static_method()
    car.class_method()
    
    """
    Traceback (most recent call last):
    wrapper:
      File "/Users/xxxxxx/PycharmProjects/untitled/yuanlei.py", line 28, in <module>
        car.static_method()
      File "/Users/xxxxxx/PycharmProjects/untitled/yuanlei.py", line 4, in wrapper
        return func(*args, **kwargs)
    TypeError: 'staticmethod' object is not callable
    """
    """
    wrapper:
    Traceback (most recent call last):
      File "/Users/xxxxxx/PycharmProjects/untitled/yuanlei.py", line 29, in <module>
        car.class_method()
      File "/Users/xxxxxx/PycharmProjects/untitled/yuanlei.py", line 4, in wrapper
        return func(*args, **kwargs)
    TypeError: 'classmethod' object is not callable
    """
    

    @staticmethod/@classmethod这个装饰器,其实它返回的并不是一个callable对象,而是一个staticmethod对象,那么它是不符合装饰器要求的,你自然不能在它之上再加别的装饰器。要解决这个问题很简单,只要把你的装饰器放在@staticmethod/@classmethod之前就好了,因为你的装饰器返回的还是一个正常的函数,然后再加上一个@staticmethod/@classmethod是不会出问题的。

    def logging(func):
        def wrapper(*args, **kwargs):
            print ("wrapper:")
            return func(*args, **kwargs)
        return wrapper
    
    class Car(object):
        def __init__(self):
            pass
    
        @logging  # 装饰实例方法,OK
        def run(self):
            pass
    
        @staticmethod
        @logging  # 在@staticmethod之前装饰,OK
        def static_method():
            pass
    
        @classmethod
        @logging  # 在@ classmethod之前装饰,OK
        def class_method(cls):
            pass
    
    car = Car()
    car.static_method()
    car.class_method()
    
    # 输入如下:
    # wrapper:
    # wrapper:
    

    小结

    Python的装饰器和Java的注解(Annotation)并不是同一回事,和C#中的特性(Attribute)也不一样,完全是两个概念。

    装饰器的理念是对原函数、对象的加强,相当于重新封装,所以一般装饰器函数都被命名为wrapper(),意义在于包装。函数只有在被调用时才会发挥其作用。比如@logging装饰器可以在函数执行时额外输出日志,@cache装饰过的函数可以缓存计算结果等等。

    而注解和特性则是对目标函数或对象添加一些属性,相当于将其分类。这些属性可以通过反射拿到,在程序运行时对不同的特性函数或对象加以干预。比如带有Setup的函数就当成准备步骤执行,或者找到所有带有TestMethod的函数依次执行等等。

    相关文章

      网友评论

          本文标题:Python装饰器

          本文链接:https://www.haomeiwen.com/subject/kyggfhtx.html