python中等难度面试题(3)装饰器

部署运行你感兴趣的模型镜像

好的!装饰器(Decorator)是Python的核心特性之一,对OOP设计和函数增强非常关键。让我们通过几个典型问题深入理解:


问题1:基础装饰器执行顺序

def decorator1(func):
    print('装饰器1安装')
    def wrapper():
        print('装饰器1执行前')
        func()
        print('装饰器1执行后')
    return wrapper

def decorator2(func):
    print('装饰器2安装')
    def wrapper():
        print('装饰器2执行前')
        func()
        print('装饰器2执行后')
    return wrapper

@decorator1
@decorator2
def target():
    print('目标函数')

print('--- 调用阶段 ---')
target()

请回答:

  1. 装饰器安装阶段的打印顺序?
  2. 函数调用时的执行顺序?
  3. 最终target指向的对象是什么?

答:

知识点
(1)什么是装饰器

装饰器的定义

装饰器是一个接受函数(或可调用对象)并返回一个新函数或可调用对象的 callable。常用来在不改原函数代码的情况下,给函数“套”上额外功能(比如日志、计时、缓存、权限检查等)。语法糖就是 @decorator 放在函数定义上。

更正式:装饰器是高阶函数(higher-order function)或实现了 __call__ 的类,它接收一个可调用对象并返回一个新的可调用对象。

#装饰器的基本原理

  1. 一等函数:Python 的函数是对象,可以作为参数传递、作为返回值、赋值等。
  2. 闭包:装饰器通常用闭包来创建“包装器”函数,包装器在调用时增加额外行为,然后调用原函数。
  3. 语法糖:写 @dec 等同于:foo = dec(foo) —— 装饰发生在函数定义(模块加载)时。

示例

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("调用前,做点事")
        result = func(*args, **kwargs)
        print("调用后,做点事")
        return result
    return wrapper

@my_decorator
def greet(name):
    print("Hello", name)

# 等价于:
# greet = my_decorator(greet)
greet("Alice")

输出:

调用前,做点事
Hello Alice
调用后,做点事

#装饰器的几种形式(常见分类)

  1. 普通函数装饰器(上面示例)

  2. 带参数的装饰器(装饰器工厂):外层函数先接收参数,返回真正的装饰器。

    def repeat(n):
        def decorator(func):
            def wrapper(*args, **kwargs):
                for _ in range(n):
                    func(*args, **kwargs)
            return wrapper
        return decorator
    
    @repeat(3)
    def hi(): print("hi")
    hi()  # 会打印三次
    
  3. 类装饰器:通过实现 __call__ 的类来做装饰器(有状态时常用)。

    class CountCalls:
        def __init__(self, func):
            self.func = func
            self.count = 0
        def __call__(self, *args, **kwargs):
            self.count += 1
            print("call #", self.count)
            return self.func(*args, **kwargs)
    
    @CountCalls
    def f():
        print("run")
    
    f(); f()
    
  4. 装饰方法(instance method):装饰器对方法工作正常,但要注意 self 的传递 —— 通常不需要额外处理。

  5. 装饰异步函数:如果被装饰的是 async def,包装函数也应 async defawait 原函数。

  6. 内置/特殊装饰器@staticmethod, @classmethod, @property —— 它们实际上是描述器(descriptor)/特殊可调用对象,用法类似但语义不同。

(2)Python代码执行的两个阶段
你提到的装饰器执行流程理解是完全正确的!但关于「安装阶段」和「执行阶段」的分界确实需要更本质的解释。让我们从 Python 解释器的工作机制来彻底理解:


🔧 最底层的原理:Python代码执行的两个阶段

所有.py文件的执行都分为两大阶段:

阶段1️⃣:编译/加载阶段(安装阶段)
  • 触发时机:当Python解释器第一次遇到你的代码时(比如import模块或直接运行脚本)
  • 核心操作
    • 逐行解析代码
    • 遇到函数/类定义时:创建对应的函数对象/类对象
    • 遇到装饰器时立即执行装饰器函数,替换被装饰对象
  • 你的例子详解
    @decorator1       # 这行代码本身就是可执行语句!
    @decorator2       # 这行也是!
    def target():     # 定义函数的动作本身也会产生函数对象
        pass
    
    实际执行顺序
    1. 解释器看到def target():创建一个函数对象target
    2. 看到@decorator2立刻执行target = decorator2(target)
      → 打印"装饰器2安装"
      → 现在target指向decorator2.wrapper
    3. 看到@decorator1立刻执行target = decorator1(target)
      → 打印"装饰器1安装"
      → 现在target指向decorator1.wrapper
阶段2️⃣:运行时阶段(执行阶段)
  • 触发时机显式调用函数时(如target()
  • 核心操作
    • 执行被装饰器包装后的新函数
    • 按装饰器嵌套顺序执行逻辑

🌰 用「做蛋糕」类比理解

# 原料准备阶段(安装阶段)
@加奶油    # ← 现在立刻把蛋糕胚拿去裹奶油
@加草莓    # ← 现在立刻把蛋糕胚放上草莓
def 蛋糕胚():  # ← 先准备基础蛋糕
    return "基础蛋糕"

# 吃蛋糕阶段(执行阶段)
吃蛋糕 = 蛋糕胚()  # ← 实际吃的是已经处理过的蛋糕
  1. 安装阶段:厨师在厨房准备蛋糕(解释器加载代码时)

    • 先做基础蛋糕(定义函数)
    • 立刻加草莓 → 变成「草莓蛋糕」
    • 立刻加奶油 → 变成「奶油草莓蛋糕」
  2. 执行阶段:客人吃蛋糕时(调用函数时)

    • 吃到的是已经处理好的「奶油草莓蛋糕」
    • 感受到的味道顺序:奶油 → 草莓 → 基础蛋糕

🚨 关键总结

  1. 安装阶段

    • 发生在「代码第一次被解释器读取时」
    • 装饰器@xxx就是立即执行的函数调用
    • 会永久修改被装饰的函数对象
  2. 执行阶段

    • 发生在「显式调用函数时」
    • 执行的是被装饰器改造后的新函数
    • 装饰器的wrapper逻辑这时才运行
  3. 为什么叫「安装」

    • 就像给软件安装插件,程序启动时就把装饰器「安装」到函数上
    • 不同于运行时临时添加功能

(3)多层装饰器嵌套
多层装饰器嵌套时,装饰器的安装顺序是从近到远,也就是说离被装饰函数最近的装饰器最先被应用,然后逐层向外。这通常被称为“装饰器堆叠”或“装饰器链”。

假设我们有三个装饰器 decorator1decorator2decorator3,它们依次装饰同一个函数 func,如下所示:

@decorator3
@decorator2
@decorator1
def func():
    pass

这个装饰器链的安装顺序如下:

  1. 最内层的装饰器 decorator1 首先被应用到 func 上。此时,funcdecorator1 替换。
  2. 接下来,decorator2 被应用到已经被 decorator1 装饰过的 func 上。此时,funcdecorator2 替换。
  3. 最后,decorator3 被应用到已经被 decorator1decorator2 装饰过的 func 上。此时,funcdecorator3 替换。

最终,当你调用 func() 时,实际上是调用了 decorator3 返回的函数,这个函数内部依次调用 decorator2 返回的函数,然后是 decorator1 返回的函数,最后才是原始的 func

结果及分析:

(1)执行结果:

--- 安装阶段 ---
装饰器2安装
装饰器1安装
--- 调用阶段 ---
装饰器1执行前
装饰器2执行前
目标函数
装饰器2执行后
装饰器1执行后

— 最终指向 —

target = decorator1(decorator2(target))  # 最终指向decorator1的wrapper

(2)结果分析:

🏗️ 安装阶段(装饰阶段)

🔄 步骤1:遇到@decorator2
@decorator1
@decorator2  # 解释器首先处理这一行
def target(): ...

实际操作:
target = decorator2(target)
此时发生:

  1. 执行decorator2(target)
    • 打印装饰器2安装
    • 将原始target函数作为func参数传入
    • 返回decorator2wrapper函数
      (此时原始target被保存在decorator2.wrapper的闭包中)

✅ 此刻内存中的target
→ decorator2.wrapper
(内含原始target的引用)


🔄 步骤2:遇到@decorator1
@decorator1  # 处理完内层装饰器后处理这一行
def target(): ...  # 注意此时的target已是decorator2的wrapper

实际操作:
target = decorator1(target)
此时发生:

  1. 执行decorator1(target)
    • 此时的targetdecorator2.wrapper
    • 打印装饰器1安装
    • 返回decorator1wrapper函数
      (此时decorator2.wrapper被保存在decorator1.wrapper的闭包中)

✅ 最终内存中的target
→ decorator1.wrapper
  → 包含decorator2.wrapper
    → 包含原始target


🎬 执行阶段(调用阶段)

当调用target()时发生的完整堆栈展开:

1. 执行 decorator1.wrapper():print('装饰器1执行前')
   │
   ├──2. 调用闭包中的 func() → 即 decorator2.wrapper():
   │   │ print('装饰器2执行前')
   │   │
   │   ├──3. 调用闭包中的 func() → 即 原始target():
   │   │      print('目标函数')
   │   │
   │   └── print('装饰器2执行后')
   │
   └── print('装饰器1执行后')

📜 输出结果与执行路径

# 安装阶段输出(模块加载时立即打印):
装饰器2安装  # decorator2(target)执行
装饰器1安装  # decorator1(装饰后的target)执行

# 调用阶段输出:
--- 调用阶段 ---
装饰器1执行前  # decorator1.wrapper开始执行
   装饰器2执行前  # decorator2.wrapper开始执行
      目标函数    # 原始target执行
   装饰器2执行后  # decorator2.wrapper收尾
装饰器1执行后  # decorator1.wrapper收尾

🌐 内存结构可视化

target 
│
├── decorator1.wrapper  # 最外层包装器
│   │
│   ├── print('装饰器1执行前/后')  # 装饰器1的逻辑
│   │
│   └── func → decorator2.wrapper  # 闭包捕获
│       │
│       ├── print('装饰器2执行前/后')  # 装饰器2的逻辑
│       │
│       └── func → 原始target  # 最内层闭包捕获
│           │
│           └── print('目标函数')  # 核心逻辑

🔍 关键原理总结

  1. 安装顺序:从下往上(@decorator2先于@decorator1应用)

    • 如同穿衣服:先内衣(@decorator2) → 再外套(@decorator1)
  2. 执行顺序:从上往下(@decorator1的逻辑先于@decorator2执行)

    • 如同拆快递:先拆外包装(@decorator1) → 再拆内包装(@decorator2)
  3. 闭包链:每个装饰器的wrapper都牢牢捕获了下一层的函数引用


问题2:带参数的装饰器

需要实现一个装饰器,可以定义最大重试次数:

@retry(max_attempts=3)
def fetch_data():
    print("尝试获取数据...")
    raise ValueError("网络错误")

当函数抛出异常时自动重试,直到成功或达到最大次数。


问题3:类装饰器 vs 函数装饰器

实现一个能统计函数调用耗时的装饰器,分别用:

  1. 函数装饰器实现
  2. 类装饰器实现

您可能感兴趣的与本文相关的镜像

Python3.9

Python3.9

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值