python 实用编程技巧 —— 装饰器使用问题与技巧
如何使用函数装饰器
常用的斐波那契的写法
1 2 3 4 | def fibonacci(n): if n<=1: return 1 return fibonacci(n-1)+fibonacci(n-2) |
这种方法也是C语言常用的递归算法。会进行大量的重复计算。如计算(10)时需要计算(8)和(9),计算(9)时需要计算(7)和(8)
改写传统的斐波那契
1 2 3 4 5 6 7 8 9 10 11 12 | def fibonacci(n,cache=None): #cache(n) #计算开始时先去缓存里找是否计算过n,如存在直接返回,否则计算,并把计算结果放在缓存中。 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] if __name__ == '__main__' : print(fibonacci(50)) |
可以发现瞬间计算出数值
使用装饰器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | def memo(func): cache = {} def wrap(n): if n not in cache: cache[n] = func(n) return cache[n] return wrap @memo def fibonacci(n): if n < = 1 : return 1 return fibonacci(n - 1 ) + fibonacci(n - 2 ) if __name__ = = '__main__' : print (fibonacci( 50 )) |
如何为被装饰的函数保存元数据
在函数对象中保存着一些函数的元数据, 例如:
- f.__name__: 函数的名字
- f.__doc__: 函数文档字符串
- f.__module__: 函数所属模块名
- f.__dict__: 属性字典
- f.__defaults__: 默认参数元组
我们在使用装饰器后, 再访问上面这些属性访问时,
看到的是内部包裹函数的元数组, 原来函数的元数据便丢失掉了, 应该如何解决?
解决方案
- 使用update_wrapper
- 使用wraps
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | from functools import wraps def mydecorator( func ): #装饰器,本例没增加功能,只做为演示 @wraps( func ) def wrapper(*args,**kargs): "" "wrapper function" "" print ( 'In wrapper' ) func (*args,**kargs) # update_wrapper(wrap, func ) return wrapper @mydecorator def example(): "" "example function" "" print ( 'In example' ) if __name__ == '__main__' : example() print(example.__name__) |
如何定义带参数的装饰器
实际案例
- 实现一个装饰器, 它用来检查被装饰函数的参数类型。
- 装饰器可以态可以通过参数指明函数参数的类型, 调用时如果检测出类型不匹配则抛出异常。
@type_assert(str, int, int)
def f(a, b, c):
...
@type_assert(y=list)
def g(x, y):
.解决方案
- 提取函数签名: inspect.signature()
- 带参数的装饰器, 也就是根据参数定制化一个装饰器, 可以看成生产装饰器的工厂。 每次调用type_assert, 返回一个特定的装饰器,然后用它去修饰其他函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | import inspect def type_assert(*ty_args, **ty_kwargs): # 带参数的装饰器函数, 要增加一层包裹 参数是 装饰器的参数 def decorator( func ): # inspect.signature(func) 函数观察对象, 方便后面获取 参数-类型 字典与参数-值字典 func_sig = inspect.signature( func ) # (a,b,c) # 将装饰器参数 组成参数-类型 字典 如 {a:int, b:str} bind_type = func_sig.bind_partial(*ty_args, **ty_kwargs).arguments # OrderedDict([( 'c' , <class 'str' >)]) # 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 if __name__ == '__main__' : f(5, 10, 's' ) # 校验通过 f(5, 10, 1) # 检验失败 1 不是字符串类型 |
如何实现属性可修改的函数装饰器
实际案例
在某项目中, 程序运行效率差, 为分析程序内哪些函数执行时间开销大, 我们实现一个带timeout参数的函数装饰器。 装饰功能如下:
- 1.统计被装饰函数单次调用运行时间
- 2.时间大于参数 timeout的, 将此次函数调用记录到log 日志中
- 3.运行时可修改 timeout 的值
@warn_timeout(1.5)
def func(a, b):
...
解决方案
- 为包裹函数添加一个函数, 用来修改闭包中使用的自由变量。 在python3中:使用nonlocal 来访问潜逃作用域中的变量引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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 (3): f(i) f.set_timeout(1) # 修改timeout 参数 从1.5 变为1 for i in range (3): f(i) |
输出结果如下
如何在类中定义装饰器
实际案例
- 实现一个能将函数调用信息记录到日志的装饰器:
- 把每次函数的调用时间, 执行时间, 调用次数写入日志
- 可以对被装饰函数分组, 调用信息记录到不同日志
- 动态修改参数, 比如日志格式
- 动态打开关闭日志输出功能
解决方案
- 为了让装饰器在使用上更加灵活, 可以把类的实例方法作为装饰器,此时在包裹函数中就可以持有实例对象, 便于修改属性和拓展功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | 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 (3): random.choice([f, g, h])() ci1.set_format( '%(func_name)s -> %(call_time)s\t%(call_n)s' ) # 去掉使用时间 for _ in range (3): random.choice([f, g])() |
分类:
python 实用编程技巧
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理