编程Python后端开发工程师

Python中的函数装饰器是什么,它们的历史是什么,以及它们在现代编程中为什么被使用?

用 Hintsage AI 助手通过面试

回答

问题的历史

装饰器作为语法糖在Python 2.4版本中出现,以简化与高阶函数的工作——那些接受或返回其他函数的函数。扩展函数功能的方法的演变导致了通过@decorator进行简洁且生动的注释格式。

问题

在大型项目中,通常需要修改或包装某些函数以添加额外的逻辑:日志记录、访问检查、缓存、运行时间测量。没有装饰器时,必须手动调用包装函数,这会使代码冗长。

解决方案

装饰器允许将重复的方面抽出到单独的包装中,从而提高代码的可读性和可重用性。使用@decorator可以优雅地将功能添加到函数和方法中:

示例代码:

import time def timing_decorator(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) print(f'Elapsed: {time.time() - start:.3f}s') return result return wrapper @timing_decorator def slow_function(): time.sleep(0.5) slow_function() # 将输出执行所花费的时间

关键特性:

  • 允许代码重用(DRY原则);
  • 可以用于函数、方法和类;
  • 允许集中引入跨功能任务(日志记录、缓存、访问、性能分析);

误导性问题。

装饰器可以改变被包装函数的签名吗?

人们常常错误地认为装饰器不会改变函数的签名。实际上,如果不使用模块functools.wraps,元信息会消失,这可能导致自动文档生成或反射中的意外错误。

示例代码:

from functools import wraps def decorator(func): @wraps(func) def wrapper(*args): return func(*args) return wrapper

装饰器可以参数化吗?

人们常常回答说不可以。实际上,通过额外的嵌套层——返回装饰器的函数,可以创建参数化装饰器。

示例代码:

def repeat(n): def decorator(func): def wrapper(*args, **kwargs): result = None for _ in range(n): result = func(*args, **kwargs) return result return wrapper return decorator @repeat(3) def hello(): print("Hello!")

可以对一个函数应用多个装饰器吗?执行顺序是什么?

人们常常错误地认为顺序不重要或者与代码中装饰器的排列顺序相同。实际上,最底层的装饰器首先应用,然后是下一个,依此类推。

示例代码:

def dec1(f): def wrapper(*a, **k): print("dec1") return f(*a, **k) return wrapper def dec2(f): def wrapper(*a, **k): print("dec2") return f(*a, **k) return wrapper @dec1 @dec2 def f(): print("core") f() # dec1, dec2, core

常见错误和反模式

  • 忽略functools.wraps会丢失原始函数的元信息;
  • 装饰器的逻辑没有考虑异常,无法捕获和处理内部错误;
  • 几个装饰器的叠加不明显

实际例子

负面案例

手动将运行时间的日志记录添加到10个函数中,通过复制和编码。

优点:

  • 逻辑清晰,代码在身边,容易找到错误。

缺点:

  • 难以维护——需要更改数十个部分,如果需要更改行为。
  • 代码重复,违反DRY原则。

正面案例

所有计时逻辑被提取到装饰器中,所有需要此度量的函数都被 @timing_decorator 装饰。

优点:

  • 变更集中进行;
  • 代码更简洁且易读。

缺点:

  • 如果不小心,可能会丢失无functools.wraps的信息;
  • 初学者可能很难立即理解装饰器的工作机制。