8.装饰器的使用及问题解决技巧

一. 如何使用函数装饰器

实际案例

某些时候我们想为多个函数, 统一添加某种功能, 比如计时统计, 记录日志, 缓存运算结果等等。

我们不想在每个函数内 一一 添加完全相同的代码, 有什么好的解决方案?

解决方案

# 使用缓存, 存储计算过的结果, 以减少递归次数,  避免重复计算问题
def memo(func):
    cache = {}
    def wrap(*args):
        res = cache.get(args)
        if not res:
            res = cache[args] = func(*args)
        return res

    return wrap



# [题目1] 斐波那契数列(Fibonacci sequence):
# F(0)=1,F(1)=1, F(n)=F(n-1)+F(n-2)(n>=2)
# 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
# 求数列第n项的值?
@memo
def fibonacci(n):
    if n <= 1:
        return 1
    return fibonacci(n-1) + fibonacci(n-2)

# fibonacci = memo(fibonacci)
print(fibonacci(50))

# [题目2] 走楼梯问题
# 有100阶楼梯, 一个人每次可以迈1~3阶. 一共有多少走法? 
@memo
def climb(n, steps):
    count = 0
    if n == 0:
        count = 1
    elif n > 0:
        for step in steps:
            count += climb(n-step, steps)
    return count

print(climb(100, (1,2,3)))


二. 如何为被装饰的函数保存元数据?

实际案例

在函数对象中保存着一些函数的元数据, 例如:

f.__name__: 函数的名字
f.__doc__: 函数文档字符串
f.__module__: 函数所属模块名
f.__dict__: 属性字典
f.__defaults__: 默认参数元组
...


我们在使用装饰器后, 再访问上面这些属性访问时,
看到的是内部包裹函数的元数组, 原来函数的元数据便丢失掉了, 应该如何解决?

解决方法

  • 使用update_wrapper
  • 使用wraps
from functools import update_wrapper,  wraps

def my_decorator(func):
    @wraps(func)
    def wrap(*args, **kwargs):
        '''某功能包裹函数'''

        # 此处实现某种功能
        # ...

        return func(*args, **kwargs)
    # update_wrapper(wrap, func)
    return wrap


@my_decorator
def xxx_func(a: int, b: int) -> int:  # python3  中:int   以及->int   可以起到提示作用, 传入参数是int   返回的是int
    '''
    xxx_func函数文档:
    ...
    '''
    pass


print(xxx_func.__name__)  #xxx_func

print(xxx_func.__doc__)
'''
xxx_func函数文档:
    ...
'''





# python中闭包的使用
def nnn(a):
    i = 0

    def f():
        nonlocal i   # 修改闭包的数据 要用 nonlocal  或者列表等可变数据结构
        i += a+i
        return i

    return f

a = nnn(1)
print(a())
print(a())
print(a())
print(a())
print(a())

'''
1
3
7
15
31
'''

三. 如何定义带参数的装饰器?

实际案例

实现一个装饰器, 它用来检查被装饰函数的参数类型。
装饰器可以态可以通过参数指明函数参数的类型, 调用时如果检测出类型不匹配则抛出异常。


@type_assert(str, int, int)
def f(a, b, c):
    ...
    
@type_assert(y=list)
def g(x, y):
    ...

解决方案

  • 提取函数签名: inspect.signature()
  • 带参数的装饰器, 也就是根据参数定制化一个装饰器, 可以看成生产装饰器的工厂。 每次调用type_assert, 返回一个特定的装饰器,然后用它去修饰其他函数
import inspect


def type_assert(*ty_args, **ty_kwargs):  # 带参数的装饰器函数, 要增加一层包裹    参数是 装饰器的参数
    def decorator(func):
        # inspect.signature(func)  函数观察对象, 方便后面获取 参数-类型 字典   与   参数-值字典
        func_sig = inspect.signature(func)

        # 将装饰器参数  组成参数-类型 字典  如  {a:int, b:str}
        bind_type = func_sig.bind_partial(*ty_args, **ty_kwargs).arguments   

        # func_sig.bind_partial      绑定部分参数可以得到 参数类型字典,  
        # 比如 参数是a=1, b='bbbb', c=2    装饰器参数是 a=int, b=str   ,则得到{'a':int, 'b':str}
        # 如果使用 func_sig.bind    则装饰器参数中   不能缺少  c 的类型

        def wrap(*args, **kwargs):  # 参数是func的 参数
            for name, obj in func_sig.bind(*args, **kwargs).arguments.items():  # 得到 参数-值 字典
                type_ = bind_type.get(name)  # 从 参数-类型  字典中  得到 参数 应该属于的 类型
                if type_:
                    if not isinstance(obj, type_):
                        raise TypeError('%s must be %s' % (name, type_))
            return func(*args, **kwargs)

        return wrap

    return decorator


@type_assert(c=str)
def f(a, b, c):
    pass


f(5, 10, 5.3)

# TypeError: c must be <class 'str'>

四. 如何实现属性可修改的装饰器?

实际案例

在某项目中, 程序运行效率差, 为分析程序内哪些函数执行时间开销大, 我们实现一个带timeout参数的函数装饰器。 装饰功能如下:

@warn_timeout(1.5)
def func(a, b):
    ...


1.统计被装饰函数单次调用运行时间 
2.时间大于参数 timeout的, 将此次函数调用记录到log 日志中
3.运行时可修改 timeout 的值

解决方法

  • 为包裹函数添加一个函数, 用来修改闭包中使用的自由变量。 在python3中:使用nonlocal 来访问潜逃作用域中的变量引用
import time
import logging


def warn_timeout(timeout):
    def decorator(func):
        # _timeout = [timeout]
        def wrap(*args, **kwargs):
            # timeout = _timeout[0]
            t0 = time.time()
            res = func(*args, **kwargs)
            used = time.time() - t0
            if used > timeout:
                logging.warning('%s: %s > %s', func.__name__, used, timeout)  # logging.warning 打印 输出到控制台
            return res

        def set_timeout(new_timeout):
            nonlocal timeout    # timeout 是闭包 变量
            timeout = new_timeout
            # _timeout[0] = new_timeout

        wrap.set_timeout = set_timeout  # 使timeout 可修改
        return wrap

    return decorator


import random


@warn_timeout(1.5)
def f(i):
    print('in f [%s]' % i)
    while random.randint(0, 1):
        time.sleep(0.6)


for i in range(30):
    f(i)

f.set_timeout(1)    # 修改timeout  参数    从1.5 变为1
for i in range(30):
    f(i)

五. 如何在类中定义装饰器?

实际案例

实现一个能将函数调用信息记录到日志的装饰器:
1. 把每次函数的调用时间, 执行时间, 调用次数写入日志
2. 可以对被装饰函数分组, 调用信息记录到不同日志
3. 动态修改参数, 比如日志格式
4. 动态打开关闭日志输出功能


@call_info(arg1, arg2, arg3...)
def func(a, b):
    ...

解决方案

  • 为了让装饰器在使用上更加灵活, 可以把类的实例方法作为装饰器,此时在包裹函数中就可以持有实例对象, 便于修改属性和拓展功能
import time
import logging

DEFAULT_FORMAT = '%(func_name)s -> %(call_time)s\t%(used_time)s\t%(call_n)s'


class CallInfo:
    def __init__(self, log_path, format_=DEFAULT_FORMAT, on_off=True):
        self.log = logging.getLogger(log_path)
        self.log.addHandler(logging.FileHandler(log_path))  
        # 这样可以通过log 往  log_path 输出信息
        self.log.setLevel(logging.INFO)  # 设置log级别
        self.format = format_
        self.is_on = on_off

    # 装饰器方法
    def info(self, func):
        _call_n = 0   # 被调用次数

        def wrap(*args, **kwargs):
            func_name = func.__name__
            call_time = time.strftime('%x %X', time.localtime())  
            # localtime 格式化时间戳为本地的时间    strftime 则得到时间字符串
            # % x
            # 本地相应的日期表示
            # % X
            # 本地相应的时间表示
            t0 = time.time()
            res = func(*args, **kwargs)
            used_time = time.time() - t0
            nonlocal _call_n
            _call_n += 1
            call_n = _call_n
            if self.is_on:
                self.log.info(self.format % locals())   # locals  即wrap函数中的变量 对应的字典
            return res

        return wrap

    def set_format(self, format_):
        self.format = format_

    def turn_on_off(self, on_off):
        self.is_on = on_off


# 测试代码
import random

ci1 = CallInfo('mylog1.log')
ci2 = CallInfo('mylog2.log')


@ci1.info
def f():
    sleep_time = random.randint(0, 6) * 0.1
    time.sleep(sleep_time)


@ci1.info
def g():
    sleep_time = random.randint(0, 8) * 0.1
    time.sleep(sleep_time)


@ci2.info
def h():
    sleep_time = random.randint(0, 7) * 0.1
    time.sleep(sleep_time)


for _ in range(30):
    random.choice([f, g, h])()

ci1.set_format('%(func_name)s -> %(call_time)s\t%(call_n)s')  # 去掉使用时间
for _ in range(30):
    random.choice([f, g])()

mylog1.log

f -> 11/04/18 17:06:35	0.6018779277801514	1
f -> 11/04/18 17:06:35	2.7894973754882812e-05	2
g -> 11/04/18 17:06:35	0.60042405128479	1
g -> 11/04/18 17:06:36	0.30515503883361816	2
....
f -> 11/04/18 17:06:47	8
f -> 11/04/18 17:06:47	9
f -> 11/04/18 17:06:48	10
g -> 11/04/18 17:06:48	14
f -> 11/04/18 17:06:48	11
f -> 11/04/18 17:06:48	12
f -> 11/04/18 17:06:49	13
g -> 11/04/18 17:06:49	15
...

mylog2.log

h -> 11/04/18 17:06:36	0.30077385902404785	1
h -> 11/04/18 17:06:37	1.71661376953125e-05	2
h -> 11/04/18 17:06:38	0.4031031131744385	3
h -> 11/04/18 17:06:38	0.2054128646850586	4
h -> 11/04/18 17:06:39	0.704901933670044	5
h -> 11/04/18 17:06:41	0.5018999576568604	6
h -> 11/04/18 17:06:41	0.10228610038757324	7
h -> 11/04/18 17:06:42	0.5047738552093506	8
h -> 11/04/18 17:06:44	0.4032928943634033	9
h -> 11/04/18 17:06:45	0.6031460762023926	10
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值