美文网首页
装饰器基础

装饰器基础

作者: 西西里加西 | 来源:发表于2020-04-25 17:53 被阅读0次

    一、装饰器概述

    装饰器(无参):

    • 它是一个函数
    • 函数作为它的形参
    • 返回值也是一个函数
    • 可以使用@function_name方式,简化调用

    此处定义不准确,只是方便理解


    装饰器和高阶函数:装饰器是高阶函数,但装饰器是对传入函数的功能的装饰(功能增强)


    带参装饰器:

    • 它是一个函数
    • 函数作为它的形参
    • 返回值是一个不带参的装饰器函数
    • 使用@function_name(参数列表)方式调用
    • 可以看做在装饰器外层又加了一层函数

    二、为什么需要装饰器

    2.1 在不是用装饰器的情况下,给某个函数添加功能

    来看一个需求:一个加法函数,想增强它的功能,能够输出被调用过以及调用的参数信息。

    原函数:

    def add(x, y):
        return x + y
    

    增加信息输出功能:

    def add(x, y):
      print("call add, x + y") # 日志输出到控制台
      return x + y
    

    上面的加法函数是完成了需求,但是有以下的缺点:

    • 打印语句的耦合太高,换句话说,我们不推荐去修改初始的add函数原始代码
    • 加法函数属于业务功能,而输出信息的功能,属于非业务功能代码,不该放在业务函数加法中

    2.2 使用高阶函数给某个函数添加功能

    def add(x,y):
        return x + y
    
    def logger(func):
        print('begin') # 增强的输出
        f = func(4,5)
        print('end') # 增强的功能
        return f
    
    print(logger(add))
    

    上面的代码做到了业务代码与功能代码分离,但是func函数的传参是个问题

    为了解决传参的问题,进一步改变代码:

    def add(x,y):
        return x + y
    
    def logger(func,*args,**kwargs):
        print('begin') # 增强的输出
        f = func(*args,**kwargs)
        print('end') # 增强的功能
        return f
    
    print(logger(add,5,y=60))
    

    2.3 柯里化实现add函数功能增强

    def add(x,y):
        return x + y
    
    def logger(fn):
        def wrapper(*args,**kwargs):
            print('begin')
            x = fn(*args,**kwargs)
            print('end')
            return x
        return wrapper
    
    # print(logger(add)(5,y=50))        #这行代码等价于下面两行代码,只是换了一种写法而已
    add = logger(add)
    print(add(x=5, y=10))
    

    2.4 装饰器语法糖

    def logger(fn):
        def wrapper(*args,**kwargs):
            print('begin')
            x = fn(*args,**kwargs)
            print('end')
            return x
        return wrapper
    
    @logger # 等价于add = logger(add),这就是装饰器语法
    def add(x,y):
        return x + y
    
    print(add(45,40))
    

    @logger 就是装饰器语法,本质是柯里化实现函数功能增强。

    三、文档字符串

    • 查看Python的帮助文档:help(function)

    • 文档字符串Documentation Strings:帮助文档中的一部分内容

      • 在函数语句块的第一行,且习惯是多行的文本,所以多使用三引号
      • 惯例是首字母大写,第一行写概述,空一行,第三行写详细描述
      • 可以使用特殊属性__doc__访问这个文档字符串
    image

    3.1 自定义文档字符串

    def add(x,y):
        """This is a function of addition"""
        a = x+y
        return x + y
    
    print("name = {}\ndoc = {}".format(add.__name__, add.__doc__))
    
    print(help(add))
    
    
    
    #以上代码执行结果如下:
    name = add
    doc = This is a function of addition
    Help on function add in module __main__:
    
    add(x, y)
        This is a function of addition
    
    None
    

    3.2 装饰器的副作用

    def logger(fn):
        def wrapper(*args,**kwargs):
            'I am wrapper'
            print('begin')
            x = fn(*args,**kwargs)
            print('end')
            return x
        return wrapper
    
    @logger #add = logger(add)
    def add(x,y):
        '''This is a function for add'''
        return x + y
    
    print("name = {}\ndoc= {}".format(add.__name__, add.__doc__))      #使用装饰器,原函数对象的属性都被替换了,我们的需求是查看被封装函数的属性,如何解决?
    

    3.3 解决装饰器的副作用

    提供一个函数,copy 被装饰函数属性到装饰器函数中去。

    def copy_properties(src): # 柯里化
        def _copy_properties(dst):
            dst.__name__ = src.__name__
            dst.__doc__ = src.__doc__
            return dst
        return _copy_properties
    
    def logger(fn):
        @copy_properties(fn) # wrapper = copy_properties(fn)(wrapper)
        def wrapper(*args,**kwargs): 
            'I am wrapper'
            print('begin')
            x = fn(*args,**kwargs)
            print('end')
            return x
        return wrapper
    
    @logger #add = logger(add)
    def add(x,y):
        '''This is a function for add'''
        return x + y
    
    print("name = {}\ndoc = {}".format(add.__name__, add.__doc__))
    
    
    #以上代码执行结果如下:
    name = add
    doc = This is a function for add
    

    通过 copy_properties 函数将被包装函数的属性覆盖掉包装函数的属性,凡是被装饰的函数都需复制这些属性,这个函数很通用。

    而在Python中,为了解决此问题,提供了wraps修改被装饰的doc信息。

    from functools import wraps
    
    def logger(fn):
        @wraps(fn) #其实查看wraps源码是利用update_wrapper()实现的(需要有偏函数知识),但是实际开发中我们推荐使用wraps装饰去。
        def wrapper(*args,**kwargs):
            'I am wrapper'
            print('begin')
            x = fn(*args,**kwargs)
            print('end')
            return x
        return wrapper
    
    @logger #add = logger(add)
    def add(x,y):
        '''This is a function for add'''
        return x + y
    
    print("name = {}\ndoc = {}".format(add.__name__, add.__doc__))
    
    
    #以上代码执行结果如下:
    name = add
    doc = This is a function for add
    

    四、装饰器分类

    4.1 无参装饰器

    需求:设计装饰器来获取函数执行时长。

    import datetime, time
    
    def logger(fn):
        def wrap(*args, **kwargs):
            # before 功能增强
            print('args={}, kwargs={}'.format(args, kwargs))
            start = datetime.datetime.now()
            ret = fn(*args, **kwargs)
            # after 功能增强
            duration = datetime.datetime.now() - start
            print("function {} took {}s".format(fn.__name__, duration.total_seconds()))
            return ret
        return wrap
    
    @logger # 相当于add = logger(add),调用装饰器
    def add(x, y):
        print("===call add===========")
        time.sleep(2)
        return x + y
    
    print(add(1,2))
    
    
    #以上代码输出结果如下:
    args=(1, 2), kwargs={}
    ===call add===========
    function add took 2.000522s
    3
    

    4.2 带参装饰器

    需求:设计装饰器来获取函数执行时长,并对时长超过阈值的函数记录一下。

    import datetime, time
    from functools import wraps
    
    def logger(duration):
        def _logger(fn):
            @wraps(fn)
            def wrapper(*args, **kwargs):
                start = datetime.datetime.now()
                ret = fn(*args, **kwargs)
                delta = (datetime.datetime.now() - start).total_seconds()
                print('it\'s so slow') if delta > duration else print('it\'s so fast')
                print('function {} took {}s'.format(fn.__name__, delta))
                return ret
            return wrapper
        return _logger
    
    @logger(2)
    def add(x, y):
        print("===call add===========")
        time.sleep(2)
        return x + y
    
    print(add(1,2))
    
    
    
    #以上代码执行结果如下:
    ===call add===========
    it's so slow
    function add took 2.001955s
    3
    

    为了传多一个参数进去装饰器函数中,多加了一层嵌套。这样就会先执行 logger(2),返回_logger。这样又回到 @_logger 无参装饰器的情况,把被装饰函数传进 @_logger,函数功能将得到增强。

    改进:将记录的功能提取出来,这样就可以通过外部提供的函数来灵活的控制输出。

    import datetime, time
    from functools import wraps
    
    # 通过 func 参数,可自定义一个输出函数来控制输出格式
    def logger(duration, func=lambda name, duration: print('function {} took {}s'.format(name, duration))):
        def _logger(fn):
            @wraps(fn)
            def wrapper(*args, **kwargs):
                start = datetime.datetime.now()
                ret = fn(*args, **kwargs)
                delta = (datetime.datetime.now() - start).total_seconds()
                print('it\'s so slow') if delta > duration else print('it\'s so fast')
                func(fn.__name__, delta) # func 参数是一个函数
                return ret
            return wrapper
        return _logger
    
    @logger(2)
    def add(x, y):
        print("===call add===========")
        time.sleep(2)
        return x + y
    
    print(add(1,2))
    
    
    
    #以上代码执行结果如下:
    ===call add===========
    it's so slow
    function add took 2.000923s
    3
    

    五、functools.update_wrapper

    5.1 概述

    语法:

    functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS,updated=WRAPPER_UPDATES)
    
    # 
    wrapper:包装函数、被更新者
    
    wrapped:被包装函数、数据源
    
    assigned:是元组,用于指定将原始函数的哪些属性直接分配给包装函数上的匹配属性。
    默认是模块级常量WRAPPER_ASSIGNMENTS,其中是要被覆盖的属性'__module__', '__name__', '__qualname__', '__doc__', '__annotations__',即模块名、名称、限定名、文档、参数注解
    
    updated:使用原函数的相应属性更新包装函数的哪些属性。
    默认是模块级常量WRAPPER_UPDATES,其中是要被更新的属性(更新wrapper的__dict__,即实例字典)
    
    wrapper增加了一个__wrapped__属性,保留着wrapped函数
    

    功能:类似copy_properties功能,用于保护更新包装函数,使其看起来像被包装函数(属性和被包装函数保持一致)

    5.2 实例

    import datetime, time, functools
    
    def logger(duration, func=lambda name, duration: print('{} took {}s'.format(name, duration))):
        def _logger(fn):
            @functools.wraps(fn)
            def wrapper(*args,**kwargs):
                start = datetime.datetime.now()
                ret = fn(*args,**kwargs)
                delta = (datetime.datetime.now() - start).total_seconds()
                if delta > duration:
                    func(fn.__name__, duration)
                return ret
            return wrapper
        return _logger
    
    @logger(5) # add = logger(5)(add)
    def add(x,y):
        time.sleep(1)
        return x + y
    
    print(add(5, 6), add.__name__, add.__wrapped__, add.__dict__, sep='\n')
    
    
    
    
    #以上代码执行结果如下:
    11 # 打印的是add(5, 6)结果
    add # add.__name__ 被装饰后的函数名
    <function add at 0x0000000002A0F378> # add.__wrapped__ 保留着被装饰函数
    {'__wrapped__': <function add at 0x0000000002A0F378>} # add.__dict__ 属性字典
    

    5.3 装饰器的调用过程

    import datetime, time, functools
    
    def logger(fn):
        @functools.wraps(fn)
        def wrapper(*args, **kwargs):
            start = datetime.datetime.now()
            ret = fn(*args, **kwargs)
            delta = (datetime.datetime.now() - start).total_seconds()
            if delta > 3:
                print('so slow')
            return ret
    
        return wrapper
    
    
    @logger
    def add(x,y):
        pass
    
    @logger
    def sub(x,y):
        pass
    
    print(add.__name__, sub.__name__)
    

    查看上面的代码,思考:

    • logger什么时候执行
      • 解释器读到 16、20行就被调用
    • logger执行几次
      • 2次,16、20行
    • wraps装饰器执行几次
      • 2次,因为 wraps 装饰器在 logger 才会被调用,logger执行2次,wraps 装饰器执行两次
    • wrapper的 __name__ 被覆盖过几次
      • 各1次
    • print(add.__name__, sub.__name__) 打印了什么
      • add sub

    由上可知:

    • 装饰器函数,在语法糖一被读取时,就调用装饰器函数了,而不是等到被装饰函数被调用的时候

    六、装饰器的用途和应用场景

    用途:

    装饰器是AOP面向切面编程 Aspect Oriented Programming 的思想的体现。
    
        面向对象往往需要通过继承或者组合依赖等方式调用一些功能,这些功能的代码往往可能再多个类中出现,例如logger功能代码。这样造成代码的重复,增加了耦合。loggger的改变影响所有其它的类或方法。
    
        而AOP再许哟啊的类或者方法上切下,前后的切入点可以加入增强的功能。让调用者和被调用者解耦,这是一种不修改原来的业务代码,给程序员动态添加功能的技术。例如logger函数就是对业务函数增加日志的功能,而业务函数中应该把业务无关的日志功能剥离干净。
    

    使用场景:

    日志,监控,权限,审计,参数检查,路由等处理。
    
    这些功能与业务功能无关,是很多都需要的公有的功能,所有适合独立出来,需要的时候,对目标对象进行增强。
    
    简单讲:缺什么,补什么。
    

    相关文章

      网友评论

          本文标题:装饰器基础

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