Python基础教程6--函数装饰器Decorator

函数装饰器Decorator是Python语言一个很"神奇"的特性,是为其他函数添加额外功能的函数。 装饰器可以让我们以高可复用的简洁代码在保持原函数不变的前提下,为原函数在运行前后增加大量与函数功能本身无关的额外功能, 是权限校验、插入日志、性能测试等"切面"需求场景的绝佳解决方案。

函数特性

装饰器本质上是函数或类,装饰器的返回值也是一个函数或类对象,在讲装饰器之前,我们先看一下几个函数相关的特性,装饰器从根本上讲就是这些特性的应用。

  • 函数也是对象

    函数可以像字符串、列表、字典等对象一样使用,可以进行赋值、删除等操作,我们看一个示例

                            
                                def hello(greeting, name):
                                    print(greeting + ', ' + name)
    
                                hello('Hello','World')
                                #'Hello, World'
    
                                hi = hello              # 将hello函数赋值给hi变量,跟 hi = 'abc' 同样的操作模式
                                hi('Hello','World')     # 引用函数调用
                                #'Hello, World'
    
                                del hello               # 删除hello变量
                                hello('Hello','World')
                                #NameError: name 'hello' is not defined
    
                                hi('Hello','World')     # hi仍然存在
                                #'Hello, World'
                            
                        
  • 函数中定义函数

    在Python中可以在一个函数中定义一个或多个内部嵌套函数,比如

                            
                                def hi(name=None):
                                    def greet():                # 内部函数
                                        print('Hello ' + name)
    
                                    def welcome():
                                        print('Welcome')
    
                                    if name:
                                        return greet            # 返回的是函数,而不是函数的执行结果
                                    else:
                                        return welcome
    
                                result = hi()
                                print(result)                   # 返回结果为内部函数
                                #< function hi.< locals>.welcome at 0x0363ED20>
    
                                result()                        # 调用返回函数
                                #Welcome
    
                                welcome()                       # 外部无法访问内部函数
                                #NameError: name 'welcome' is not defined
                            
                        
  • 函数作为返回值

    可以将函数作为另外一个函数的返回值。

    参考上述示例,返回的是greet或welcome函数,而不是greet()/welcome(),带不带括号()在这里的主要区别就是,

    • 如果返回的是greet()/welcome(),那么返回的就是greet/welcome函数的运行结果,在当前函数中就是字符串。
    • 如果返回的是greet/welcome,那么返回的是可以被调用的函数。
  • 函数作为调用另外一个函数的参数

    函数和一般的字符串、列表、字典一样,都是对象,可以作为调用其他函数的参数。

                            
                                def hi():
                                    return 'Hello, World!!!'
    
                                def logging_fun(func):
                                    print('Do something before executing func.')
                                    print(func())
                                    print('Do something after executing func.')
    
                                logging_fun(hi)          # hi函数作为调用logging_fun的参数
                                #Do something before executing func.
                                #Hello, World!!!
                                #Do something after executing func.
                            
                        

以上就是函数的几个特性,接下来我们看下什么是装饰器,装饰器可以在一个函数的运行前后(横切面)去执行其它代码。

简单装饰器

其实上述logging_fun函数就已经初步具备装饰器的基本功能了,它封装一个已有函数,并且可以按照自己的方式修改它的行为。

我们再稍微封装一下。

                
                    def logging_fun(func):      # func为作为参数的函数
                        def wrapper():          # 内部函数
                            print('Do something before executing func.')
                            print(func())
                            print('Do something after executing func.')

                        return wrapper          # 返回一个函数,请注意没用()
                
            

我们在定义另外一个函数,并使用logging_fun函数

                
                    def hi():
                        return 'Hello, World!!!'

                    hi = logging_fun(hi)    # 返回一个附加了其它logging功能的函数,并将其赋值了hi变量
                    hi()                    # 运行hi函数,打印结果如下
                    #Do something before executing func.
                    #Hello, World!!!
                    #Do something after executing func.
                
            

以上代码描述了装饰器要做的事情,但是这并不是我们使用装饰器的方式,@语法糖才是使用装饰器的标准方式,其功能跟上述代码一样,不过更简单易用。

                
                    # @的功能跟logging_fun(welcome)代码类似,将welcome函数作为参数调用logging_fun,并将返回函数赋值给welcome
                    @logging_fun
                    def welcome():
                        """
                        Welcome function
                        :return:
                        """
                        return "Welcome"

                    welcome()           # 实际调用的是封装以后的函数
                    #Do something before executing func.
                    #Welcome
                    #Do something after executing func.
                
            

以上就是一个简单装饰器的完整示例。

保持函数原有属性

以上装饰器示例代码,有个小问题,我们打印一下封装以后的welcome函数的__name__和__doc__属性

                
                    print(welcome.__name__)     # 返回结果是经过封装的wrapper函数,而不是原来的welcome函数了
                    #wrapper
                    print(welcome.__doc__)      # welcome函数的文档也丢失了
                    #None
                
            

可以看到,因为welcome函数被装饰器内部的wrapper函数替代了,所以其__name__和__doc__属性都发生了变化,而这并不是我们想要的结果,我们希望装饰器不要影响函数原有的属性。

我们可以通过再写一个装饰器,将这些属性赋值给新的函数,完整代码如下

                
                    def attr_wraps(w_func):             # 新的装饰器,主要作用是将原函数的属性,赋值给封装以后的函数,这是一个带参数的装饰器,这里还使用到了闭包的相关知识,后续会讲到
                        def tmp_wraps(func):
                            def decorator(*args, **kwargs):
                                decorator.__name__ = w_func.__name__
                                decorator.__doc__ = w_func.__doc__
                                decorator.__wrapped__ = w_func.__wrapped__

                            return decorator

                        return tmp_wraps


                    def logging_fun(func):
                        @attr_wraps(func)               # 使用新的装饰器,这里其实返回的也不是wrapper函数,而是经过attr_wraps封装的新函数
                        def wrapper():
                            print('Do something before executing func.')
                            print(func())
                            print('Do something after executing func.')

                        return wrapper

                    @logging_fun
                    def welcome():
                        """
                        Welcome function
                        :return:
                        """
                        return "Welcome"


                    print(welcome.__name__)
                    #Welcome
                    print(welcome.__doc__)
                    #    Welcome function
                    #    :return:
                
            

幸运的是Python给我们提供了一个简单的函数来解决这个问题,而不用我们自己实现,那就是functools.wraps函数, wraps函数接受一个函数进行装饰,并加入了复制函数名称、注释文档、参数列表等功能,将原函数的信息拷贝到装饰器里面的封装函数中,这样我们就可以访问装饰之前函数相关的信息。

我们修改一下上述示例代码

                
                    from functools import wraps
                    def logging_fun(func):
                        @wraps(func)
                        def wrapper():
                            print('Do something before executing func.')
                            print(func())
                            print('Do something after executing func.')

                        return wrapper

                    @logging_fun
                    def welcome():
                        """
                        Welcome function
                        :return:
                        """
                        return "Welcome"


                    print(welcome.__name__)         # 打印结果跟上边示例一样
                    #Welcome
                    print(welcome.__doc__)
                    #    Welcome function
                    #    :return:

                    print(welcome.__wrapped__)      # 指向原函数
                    #< function welcome at 0x037A1AE0>
                
            

请注意,虽然封装函数拥有原函数相同的信息,但是终究不是原函数, welcome的指向已经发生了变化,指向了封装以后的函数,如果想要访问原函数的话,可以通过__wrapped__属性访问。

保持函数原有参数传递

讲到现在,我们上述示例用到的welcome函数是没有参数的,那如果有参数怎么办,比如以下函数

                
                    def hello(greeting, name):
                        return greeting + ', ' + name
                
            

首先可能会想到为logging_fun装饰器的wrapper函数,添加对应的参数,但是装饰器可能会引用到很多函数上,这些函数的参数都是不一样的,所以无法为wrapper添加具体的参数。

这里就可以使用函数收集参数的特性,将封装函数接收到的所有位置参数和关键字参数以*arg和**kwargs的方式传递到原函数中。

我们修改一下上述代码示例

                
                    from functools import wraps
                    def logging_fun(func):
                        @wraps(func)
                        def wrapper(*args, **kwargs):
                            print('Do something before executing func.')
                            print(func(*args, **kwargs))    # 封装函数接收到的所有参数都传递到了原函数中
                            print('Do something after executing func.')

                        return wrapper


                    @logging_fun
                    def hello(greeting, name):
                        return greeting + ', ' + name


                    hello('Hello', name="World")
                    #Do something before executing func.
                    #Hello, World
                    #Do something after executing func.
                
            

带参数的装饰器

有时们希望能给logging_fun装饰器添加一个日志级别(info/warn),即需要一个可以传参的装饰器。

其实上述wraps就是一个带参数的装饰器,它接收一个函数作为参数,并在封装函数中使用,我们参考wraps改写一下logging_fun装饰器。

                
                    def logging_fun(level):
                        def decorator(func):
                            @wraps(func)
                            def wrapper(*args, **kwargs):
                                print(level)
                                print('Do something before executing func.')
                                print(func(*args, **kwargs))
                                print('Do something after executing func.')

                            return wrapper

                        return decorator

                    @logging_fun('info')    # 请注意跟上边示例代码的区别,这里用到的是@logging_fun('info'),而上面代码并没有带()括号
                    def hello(greeting, name):
                        return greeting + ', ' + name

                    hello('Hello', name="World")
                
            

这里用到了闭包的知识点。

请注意带参数的的装饰器语法是@logging_fun('info'),是logging_fun装饰器函数运行以后返回的结果函数作为实际的装饰器(== @decorator),这样就好理解了。

多个装饰器叠加

多个函数装饰器可以叠加使用,语法如下

                
                    # flask 路由代码示例
                    @app.route('/secret/', methods=['PUT', 'PATCH'])
                    @login_required
                    def secret():
                        pass
                
            

多个装饰器作用于一个函数时,各装饰器只对随后的装饰器和目标函数起作用。 在这个示例中secret视图函数受login_required装饰器的保护(禁止未授权用户访问),被保护后返回的函数又注册为一个路由,如果调换顺序,逻辑就不对了。

所以多个装饰器叠加使用,一定要注意使用顺序

应用实例

这里看一个函数装饰器的具体应用示例

web请求中,我们会封装很多的路由及其处理函数,我们希望对所有的请求响应都有权限控制功能,只有拥有相应的权限才能访问。

                
                    def permission_required(module, op_permission=None):
                        def decorate(func):
                            @wraps(func)
                            def wrapper(*args, **kwargs):
                                if _check_login() is False:                             # 检查用户是否登录,否则返回401
                                    return abort(401, response='forbidden')
                                if _check_permission(module, op_permission) is False:   # 检查用户是否有对应模块的操作权限,否则返回403
                                    return abort(403, response='unauthorized')
                                return func(*args, **kwargs)                            # 如果都满足,则执行对应的响应函数

                            return wrapper

                        return decorate
                
            

在具体的路由响应中,就可以通过permission_required进行权限控制了。

                
                    # flask blueprint代码示例
                    @sys_bp.route('/role/', methods=['PUT', 'PATCH'])   #装饰器叠加
                    @permission_required('role', 'update')
                    def sys_role_update():
                        pass
                
            

类装饰器

高级应用