美文网首页
python进阶——7. 装饰器

python进阶——7. 装饰器

作者: Lemon_Home | 来源:发表于2017-11-08 17:06 被阅读24次

    7.1 创建装饰器

    与java中的装饰器模式类似,其作用就是将一些多余的、能够重复使用的代码抽离出来,然后通过python的语法糖@作用在方法头部,能够起到对方法增进、装饰的作用。类似的像统计功能,log日志功能等,都可以作为装饰器的模式供其他方法使用。

    下面的实例是实现一个数列,类似1, 1, 2, 3, 5, 8,13... ...一个数等于前两个数之和,能够查询具体的第n个数。

    def fibonacci(n):
        if n <= 1:
            return 1
        return fibonacci(n - 1) + fibonacci(n - 2)
    
    print(fibonacci(5))
    

    实现逻辑很简单就是重复递归,最后一直调用到fibonacci(0), fibonacci(1)的值时才停止。但是仔细分析可知,在此过程中会造成一些不必要的计算,例如输出的n为5,下一步计算的是fibonacci(4) + fibonacci(3),再下一步(fibonacci(3) + fibonacci(2)) + (fibonacci(2) + fibonacci(1)),可以看出fibonacci(3)重复计算了。这种在数目特别大的情况下会导致运行速度很慢。

    如果需要求出第50项的数,就需要去掉一些重复的计算,可以通过添加缓存机制来解决。

    def fibonacci(n, cache=None):
        if cache is None:
            cache = {}
        if n in cache:
            return cache[n]
        if n <= 1:
            return 1
        cache[n] = fibonacci(n - 1, cache) + fibonacci(n - 2, cache)
        return cache[n]
    
    print(fibonacci(50))
    

    添加参数cache,当cache为空时,创建一个空的字典,将递归运行的结果存储在cache中,当之后递归遇到相同的结果时直接从缓存字典中取出即可,这样实现之后,大数字的运行速度会有明显提升。

    类似的很多其他算法也可能会遇到此类问题,需要建立一个缓存机制进行处理,所以有必要将缓存机制封装出来供其他方法调用。下面是创建的memo方法,此方法接收的是函数闭包对象,然后创建内函数并在外层函数返回此内函数对象。在内函数中接收传来的参数,进行对应处理操作。

    在调用memo方法时传入方法本身,并且生成一个新的方法对象,然后在这个新的方法对象中传入想要的参数值。

    def memo(func):
        cache = {}
        def wrap(*args):
            if args not in cache:
                cache[args] = func(*args)
            return cache[args]
        return wrap
    
    
    def fibonacci(n):
        if n <= 1:
            return 1
        return fibonacci(n - 1) + fibonacci(n - 2)
    
    fibonacci = memo(fibonacci)
    print(fibonacci(50))
    

    在python中对装饰器有个实用的语法糖,就是在使用装饰器的方法上添加@加上装饰器的方法名即可。

    def memo(func):
        cache = {}
        def wrap(*args):
            if args not in cache:
                cache[args] = func(*args)
            return cache[args]
        return wrap
    
    @memo
    def fibonacci(n):
        if n <= 1:
            return 1
        return fibonacci(n - 1) + fibonacci(n - 2)
    
    # fibonacci = memo(fibonacci)
    print(fibonacci(50))
    

    7.2 为装饰器的函数保留元数据

    在python中函数也属于对象,函数的元数据相当于对象的属性。下面看以下常用的函数属性。

    def f(a, b=10):
        ''' f test method
    
        :param a:
        :param b:
        :return:
        '''
        d = 10
        print("f")
    
    print(f.__doc__)
    print(f.__defaults__)
    print(f.__name__)
    print(f.__module__)
    
     f test method
    
        :param a:
        :param b:
        :return:
        
    (10,)
    f
    __main__
    

    doc函数的文档
    defaults函数的默认参数
    name函数名
    module函数所属的模块

    另外,对于函数内部的闭包来说,可以通过函数的属性访问闭包。

    def t():
        a = 2
        return lambda k: a ** k
    
    g = t()
    print(g.__closure__[0].cell_contents)
    
    2
    

    在t方法中有一个a字段,函数的返回值是一个lambda函数。当调用f方法时,a属性在闭包中,g变量可以通过closure访问函数闭包。

    当使用装饰器时,装饰器函数可能会影响到调用的函数属性

    def my_decortor(func):
        def wrapper(*args, **kwargs):
            """wrapper func """
            print("in wrapper")
            func(*args, **kwargs)
        return wrapper
    
    @my_decortor
    def example():
        '''example func'''
        print("example func")
    
    print(example.__doc__)
    print(example.__name__)
    
    wrapper func 
    wrapper
    

    因为在函数调用装饰器函数时,其函数对象已经发生了改变,变为了装饰器函数对象,所以调用example的函数元数据编成的是my_decortor的元数据了。

    如果想保留原来调用函数的元数据,可以使用标准库functools中的wraps装饰内部包裹函数,可以将原来的函数属性更新到包裹函数中。

    from functools import wraps, WRAPPER_ASSIGNMENTS, WRAPPER_UPDATES, update_wrapper
    
    def my_decortor(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            """wrapper func """
            print("in wrapper")
            # update_wrapper(wrapper, func)
            func(*args, **kwargs)
        return wrapper
    
    @my_decortor
    def example():
        '''example func'''
        print("example func")
    
    print(example.__doc__)
    print(example.__name__)
    
    example func
    example
    

    当给包裹函数加上装饰器@wraps,并传入原函数对象即可更新元数据到包裹函数上。其原理是调用update_wrapper(wrapper, func)方法,只不过使用装饰器调用更加简便。上面调用都是使用的默认参数,在装饰器@wraps的传参中可以传入其他的属性,默认参数具体为('module', 'name', 'qualname', 'doc', 'annotations')。

    7.3 定义有参数的装饰器

    带参数的装饰器,也就是根据参数定制化一个装饰器,可以看做生产装饰器的工厂。

    例如创建一个装饰器,其具有判断函数的参数类型是否为所需类型。具体的思路是在普通的装饰器 外层再套上一个工厂方法来接收给装饰器传入的参数,然后通过方法签名信息inspect库中的signature来获取参数的信息,然后判断参数类型是否为装饰器定义类型,如果不是抛出异常即可。

    from inspect import signature
    
    
    def type_assert(*ty_args, **ty_kargs):
        def decorator(func):
            sig = signature(func)
            btypes = sig.bind_partial(*ty_args, **ty_kargs).arguments
    
            def wrapper(*args, **kwargs):
                for name, obj in sig.bind(*args, **kwargs).arguments.items():
                    if name in btypes:
                        if not isinstance(obj, btypes[name]):
                            raise TypeError('"%s" must be "%s"' % (name, btypes[name]))
    
                return func(*args, **kwargs)
    
            return wrapper
    
        return decorator
    
    
    @type_assert(int, str, list)
    def f(a, b, c):
        print(a, b, c)
    
    f(1, 1, 1)
    
    

    signature主要是为了获取方法的相关信息,类似下面,能够获取出参数的名称、类型和默认值相关,并且通过bind方法绑定参数信息。

    
    def test(a, b, c=1): pass
    
    sig = signature(test)
    print(sig.parameters)
    print(sig.parameters['a'].name)
    print(sig.parameters['a'].kind)
    print(sig.parameters['c'].default)
    bargs = sig.bind(str, int, int)
    print(bargs.arguments['a'])
    

    7.4 属性可修改的函数装饰器

    在某些情况下,我们需要动态改函数装饰器的参数、属性值。

    下面看一个实例,首先实现一个能够计算函数运行时长的装饰器,并且传入timeout参数,如果函数执行的时长超过了timeout规定,就打印出相关信息。

    from functools import wraps
    import time
    import logging
    from random import randint
    
    
    def warn(timeout):
        def decorator(func):
            def wrapper(*args, **kwargs):
                start = time.time()
                res = func(*args, **kwargs)
                used = time.time() - start
                if used > timeout:
                    msg = '"%s": %s > %s ' % (func.__name__, used, timeout)
                    logging.warning(msg)
                return res
            return wrapper
        return decorator
    
    
    @warn(1.5)
    def test():
        print('In test')
        while randint(0, 1):
            time.sleep(0.5)
    
    
    for x in range(1, 31):
        test()
    

    在test方法中使用warn装饰器进行修饰,并且传入timeout为1.5的参数,之后随机地睡眠0.5秒,启动执行30次。可以看到随机打印出了log,WARNING:root:"test": 2.059203624725342 > 1.5

    接下来,上限在外层动态修改timeout的值。

    from functools import wraps
    import time
    import logging
    from random import randint
    
    
    def warn(timeout):
        def decorator(func):
            def wrapper(*args, **kwargs):
                start = time.time()
                res = func(*args, **kwargs)
                used = time.time() - start
                if used > timeout:
                    msg = '"%s": %s > %s ' % (func.__name__, used, timeout)
                    logging.warning(msg)
                return res
    
            def set_timeout(k):
                nonlocal timeout
                timeout = k
            wrapper.set_timeout = set_timeout
            return wrapper
    
        return decorator
    
    
    @warn(1.5)
    def test():
        print('In test')
        while randint(0, 1):
            time.sleep(0.5)
    
    
    # for x in range(1, 31):
    #     test()
    
    test.set_timeout(1)
    
    for x in range(1, 31):
        test()
    

    想要动态修改装饰器的参数,就需要对包裹函数增加属性,此属性可以是内函数,然后在内函数中对传入的装饰器参数进行进一步操作。需要注意的是,timeout是闭包内的数据,新建的内函数是不能直接访问到此参数,在python3中提供了nonlocal 修饰符可以访问到闭包内的数据。

    执行结果,随机打印出WARNING:root:"test": 1.0000569820404053 > 1

    相关文章

      网友评论

          本文标题:python进阶——7. 装饰器

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