python进阶——7. 装饰器

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. 装饰器)