Python面试:装饰器原理与应用

2026-08-14 06:00:44 536阅读 0评论

Python面试:装饰器原理与实战,面试官到底想听什么?

每次Python面试,装饰器绝对是绕不开的“照妖镜”。很多候选人背得出“在不修改原函数的前提下增加功能”,但一让手写个带参数的装饰器,或者问问 wraps 的具体作用,立马拉胯。今天咱们不背干瘪的八股文,从底层逻辑到实战避坑,把装饰器彻底盘明白。

撕开语法糖,看透装饰器本质

把装饰器想象成给手机贴钢化膜。手机(原函数)还是那个手机,没拆机没改内部零件,但贴了膜(装饰器)后,就多了防刮的功能。在Python里,这层“膜”本质上是个接收函数作为参数,并返回一个新函数的高阶函数

我们来看一个最基础的计时装饰器:

import time

def timer_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        # 执行原函数,并接收返回值
        result = func(*args, **kwargs) 
        end_time = time.time()
        print(f"函数 {func.__name__} 耗时: {end_time - start_time:.4f}秒")
        return result
    return wrapper

@timer_decorator
def slow_function():
    time.sleep(1)

这里有个核心细节:@timer_decorator 只是语法糖,它在底层等价于 slow_function = timer_decorator(slow_function)。当你调用 slow_function() 时,实际执行的是 wrapper()

面试官的连环追问:元信息丢失与万能参数

写出上面的代码,面试官通常会抛出两个问题。

第一个问题:“原函数的文档字符串和函数名还在吗?” 答案是不在了。因为 slow_function 现在指向的是 wrapper,它的 __name__ 变成了 wrapper。这在依赖函数名进行路由或日志记录的场景中是致命的。解决办法是引入 functools.wraps,它能把原函数的元信息拷贝到包装函数上:

from functools import wraps

def timer_decorator(func):
    @wraps(func) # 关键一步,保留原函数元信息
    def wrapper(*args, **kwargs):
        # ... 逻辑同上

第二个问题:“如果原函数参数不固定怎么办?” 这就是为什么在 wrapper 中必须使用 *`args, kwargs` 来接收参数,并在调用 func 时原样解包传递。这是保证装饰器通用性的标配写法,千万别写死参数。

进阶实战:装饰器自己也需要传参

搞懂了基础版,面试官往往会升级难度:“如果装饰器本身需要配置参数,比如指定重试次数,怎么写?”

这时候就需要用到三层嵌套函数,俗称“俄罗斯套娃”。最外层接收装饰器参数,中间层接收原函数,最内层执行具体逻辑:

def retry(max_attempts):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f"第 {attempt + 1} 次尝试失败: {e}")
            raise Exception("达到最大重试次数")
        return wrapper
    return decorator

@retry(max_attempts=3)
def unstable_api_call():
    pass

理解这个结构的关键在于明白执行顺序:@retry(max_attempts=3) 会先执行 retry(3),返回真正的装饰器 decorator,然后再用 decorator 去包装 unstable_api_call

拓展视野:类装饰器与叠加顺序

除了函数,其实类也能做装饰器。当你需要维护复杂的内部状态,或者闭包层级太深导致代码可读性下降时,利用类的 __call__ 魔术方法来实现装饰器会优雅得多:

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"{self.func.__name__} 被调用了 {self.count} 次")
        return self.func(*args, **kwargs)

实战中还有一个极易踩坑的点:多个装饰器叠加时的执行顺序。 假设有 @decorator_A@decorator_B 同时作用于一个函数。在装饰阶段,Python是从下往上执行的,等价于 func = A(B(func));但在实际调用阶段,是从上往下执行,即先执行 A 的包装逻辑,再进入 B 的包装逻辑,最后才触达原函数。理清这个顺序,排查复杂日志或权限校验bug时能少走很多弯路。

总结

装饰器绝不仅仅是面试场上的敲门砖,它背后体现的是软件设计中极其重要的开放封闭原则——对扩展开放,对修改封闭。在日常写代码时,遇到日志记录、权限校验、缓存注入等横切关注点,多尝试用装饰器去解耦。当你不再把它当成一种语法炫技,而是作为架构设计的利器时,面试官自然能感受到你真正的代码功底。

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
验证码
评论列表 (暂无评论,536人围观)

还没有评论,来说两句吧...

目录[+]