装饰器本质与手写

一、本质:装饰器就是闭包

装饰器(Decorator):接收一个函数,返回一个新函数,在不改原函数代码的前提下给它加功能。换句话说——装饰器就是一个返回函数的闭包

生活类比:你有一部手机(func),你不想拆手机就能给它加壳、贴膜、装防摔套——这些”外套”就是装饰器。手机还是那部手机,但功能(防摔、美观)增强了。

二、手写一个最朴素的装饰器

 
def logger(func):            # 外层:接收被装饰的函数
 
    def wrapper(*args, **kwargs):   # 内层:替代原函数的"壳"
 
        print(f"调用了 {func.__name__},参数 {args}")
 
        result = func(*args, **kwargs)   # 真正干活
 
        print(f"{func.__name__} 返回 {result}")
 
        return result
 
    return wrapper           # 返回壳
 
def add(a, b):
 
    return a + b
 
add = logger(add)            # 手动把 add 包一层
 
print(add(3, 5))            # 调用前打印、调用后打印,结果 8
 

logger 就是装饰器:它没有碰 add 的内部代码,只是把它”裹”进 wrapper,顺手加了日志。

三、语法糖 @ —— 只是语法简化

 
@logger                     # 等价于 add = logger(add)
 
def add(a, b):
 
    return a + b
 

@logger 写在函数上面,就是告诉 Python:“定义完 add 后,立刻执行 add = logger(add)”。语法糖而已,本质不变。


graph LR

    F[原函数 add] --> D[装饰器 logger]

    D --> W[wrapper 壳]

    W -->|包裹| F

    note["@logger 等价于 add = logger(add)<br/>调用 add 实际调用 wrapper"]

四、常见实用装饰器

 
import time
 
def timer(func):
 
    def wrapper(*args, **kwargs):
 
        start = time.perf_counter()
 
        result = func(*args, **kwargs)
 
        print(f"{func.__name__} 耗时 {time.perf_counter()-start:.4f}s")
 
        return result
 
    return wrapper
 
@timer
 
def slow():
 
    sum(i*i for i in range(10_000_000))
 
slow()
 
装饰器作用
@property方法当属性用
@staticmethod / @classmethod14-类方法实例方法静态方法
@functools.lru_cache记忆化缓存(见 09-functools-wraps的作用
自定义 @timer / @retry计时、重试等横切逻辑

五、手写考点

常让你手写一个:计时、重试、权限校验、单例。核心套路永远是——**三层函数:外层收 func,内层 wrapper 包 *args/kwargs,返回 wrapper。记住这招,万变不离其宗。

六、一句话讲清

“装饰器本质是一个闭包,接收函数返回新函数,在不改原函数代码的情况下附加功能。@ 只是 f = deco(f) 的语法糖。手写模板是外层收 func、内层 wrapper 包参数、返回 wrapper。“

1. 函数嵌套调用

函数可以调用另一个函数,形成调用链:

 
def is_even(n):
 
    """判断是否为偶数"""
 
    return n % 2 == 0
 
def filter_evens(numbers):
 
    """从列表中筛选偶数(调用 is_even)"""
 
    result = []
 
    for n in numbers:
 
        if is_even(n):      # 函数嵌套调用
 
            result.append(n)
 
    return result
 
print(filter_evens([1, 2, 3, 4, 5, 6]))  # [2, 4, 6]
 

2. 作用域

2.1 LEGB 规则

Python 查找变量时按 LEGB 顺序:Local → Enclosing → Global → Built-in


graph TB

    L[Local<br/>函数内部] --> E[Enclosing<br/>外层函数]

    E --> G[Global<br/>模块级别]

    G --> B[Built-in<br/>print/len/range]

2.2 四种作用域演示

 
# Global 作用域
 
x = "全局变量"
 
def outer():
 
    # Enclosing 作用域
 
    x = "外层函数的变量"
 
    def inner():
 
        # Local 作用域
 
        x = "内层函数的变量"
 
        print(f"inner: {x}")    # 内层函数的变量
 
    inner()
 
    print(f"outer: {x}")        # 外层函数的变量
 
outer()
 
print(f"global: {x}")           # 全局变量
 

2.3 全局变量与局部变量

 
# 函数内可以读取全局变量,但不能直接修改
 
total = 0
 
def add_one():
 
    # print(total)    # ❌ 下一行有 total= 声明,所以 total 被视为局部变量
 
    total = total + 1  # ❌ UnboundLocalError:局部变量 total 还没赋值就被引用了
 
# 正确做法:用 global 声明
 
def add_one_correct():
 
    global total
 
    total = total + 1
 

2.4 global 和 nonlocal

 
# global:在函数内修改全局变量
 
count = 0
 
def increment():
 
    global count
 
    count += 1
 
increment()
 
print(count)  # 1
 
# nonlocal:在内层函数中修改外层(非全局)变量
 
def outer():
 
    count = 0
 
    def inner():
 
        nonlocal count
 
        count += 1        # 修改的是 outer 的 count,不是全局的
 
        return count
 
    return inner
 
counter = outer()
 
print(counter())  # 1
 
print(counter())  # 2
 
print(counter())  # 3
 

3. 闭包 ⭐

闭包 = 函数 + 它”记住”的外部变量

 
def make_power(exponent):
 
    """创建一个计算 n 次幂的函数"""
 
    def power(base):
 
        return base ** exponent  # exponent 来自外层函数,被"记住"了
 
    return power                # 返回内层函数(闭包)
 
square = make_power(2)   # exponent=2 被记住
 
cube = make_power(3)     # exponent=3 被记住
 
print(square(5))  # 25  (5²)
 
print(square(10)) # 100 (10²)
 
print(cube(5))    # 125 (5³)
 

3.1 闭包三要素

  1. 函数嵌套——内层函数定义在外层函数内部

  2. 引用外部变量——内层函数使用了外层函数的变量

  3. 返回内层函数——外层函数返回内层函数

3.2 闭包的作用

 
# 数据隐藏:外部无法直接访问 count
 
def make_counter():
 
    count = 0              # 私有变量
 
    def counter():
 
        nonlocal count
 
        count += 1
 
        return count
 
    return counter
 
c = make_counter()
 
print(c())  # 1
 
print(c())  # 2
 
# 无法从外部直接访问或修改 count
 

4. 递归

递归 = 函数调用自己

4.1 递归三要素

要素说明
终止条件必须有一个不再递归的条件,否则无限循环
问题递减每次递归,问题规模必须缩小
同类子问题大问题能分解为结构相同的子问题

4.2 阶乘

 
def factorial(n):
 
    """计算 n! = n × (n-1) × ... × 1"""
 
    if n <= 1:            # 终止条件
 
        return 1
 
    return n * factorial(n - 1)  # 问题递减
 
print(factorial(5))  # 120
 
#             = 5 × 4 × 3 × 2 × 1 = 120
 

4.3 斐波那契数列(递归 vs 迭代)

 
# 递归版本(简洁但效率低)
 
def fib_recursive(n):
 
    if n <= 1:
 
        return n
 
    return fib_recursive(n-1) + fib_recursive(n-2)
 
# 迭代版本(推荐)
 
def fib_iterative(n):
 
    a, b = 0, 1
 
    for _ in range(n):
 
        a, b = b, a + b
 
    return a
 

递归的陷阱

Python 默认递归深度限制为 1000。递归太深会导致 RecursionError

大多数递归问题可以用循环更好地解决。

5. lambda 匿名函数

 
# 普通函数
 
def add(x, y):
 
    return x + y
 
# lambda 等价写法
 
add_lambda = lambda x, y: x + y
 
# lambda 常用于排序、过滤、映射
 
students = [("张三", 85), ("李四", 92), ("王五", 78)]
 
# 按成绩排序
 
students.sort(key=lambda s: s[1])
 
print(students)  # [('王五', 78), ('张三', 85), ('李四', 92)]
 
# filter:筛选偶数
 
nums = [1, 2, 3, 4, 5, 6]
 
evens = list(filter(lambda x: x % 2 == 0, nums))
 
print(evens)  # [2, 4, 6]
 
# map:全部平方
 
squared = list(map(lambda x: x**2, nums))
 
print(squared)  # [1, 4, 9, 16, 25, 36]
 
# lambda x: if x > 0: return x  ← 这种复杂逻辑还是用 def
 

6. 函数注释(Type Hints 入门)

 
def calculate(weight: float, height: float) -> float:
 
    """
 
    计算 BMI
 
    :param weight: 体重(千克)
 
    :param height: 身高(米)
 
    :return: BMI 值
 
    """
 
    return weight / (height ** 2)
 
# calculate("abc", 1.75)  # IDE 会警告,但运行时不会报错
 

7. 文件操作

7.1 文件打开模式

模式说明文件不存在
r只读(默认)报错
w写入(覆盖)创建新文件
a追加(在末尾添加)创建新文件
r+读写报错
b二进制模式配合以上使用(如 rb, wb

7.2 读文件

 
# 方式一:read() - 一次读取全部
 
with open("data.txt", "r", encoding="utf-8") as f:
 
    content = f.read()
 
    print(content)
 
# 方式二:readline() - 逐行读取
 
with open("data.txt", "r", encoding="utf-8") as f:
 
    while True:
 
        line = f.readline()
 
        if not line:        # 读到文件末尾返回空字符串
 
            break
 
        print(line.strip())  # strip() 去掉行尾换行符
 
# 方式三:readlines() - 读取所有行到列表
 
with open("data.txt", "r", encoding="utf-8") as f:
 
    lines = f.readlines()
 
    for line in lines:
 
        print(line.strip())
 
# 方式四:直接遍历文件对象(推荐!内存友好)
 
with open("data.txt", "r", encoding="utf-8") as f:
 
    for line in f:             # 文件对象本身是可迭代的
 
        print(line.strip())
 

7.3 写文件

 
# write() - 写入字符串
 
with open("output.txt", "w", encoding="utf-8") as f:
 
    f.write("第一行\n")
 
    f.write("第二行\n")
 
# writelines() - 写入列表
 
lines = ["第一行\n", "第二行\n", "第三行\n"]
 
with open("output.txt", "w", encoding="utf-8") as f:
 
    f.writelines(lines)
 

7.4 with 语句详解

 
# ❌ 传统方式:需要手动关闭
 
f = open("data.txt", "r", encoding="utf-8")
 
try:
 
    content = f.read()
 
finally:
 
    f.close()        # 必须手动关闭,否则资源泄漏
 
# ✅ with 方式:自动关闭
 
with open("data.txt", "r", encoding="utf-8") as f:
 
    content = f.read()
 
# 离开 with 块后,文件自动关闭(即使发生异常)
 

📝 综合练习:日记本程序

 
"""
 
简易日记本
 
"""
 
import os
 
from datetime import datetime
 
DIARY_FILE = "diary.txt"
 
def add_entry():
 
    """添加一篇日记"""
 
    content = input("今天想写点什么?\n> ")
 
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
 
    with open(DIARY_FILE, "a", encoding="utf-8") as f:
 
        f.write(f"\n{'='*40}\n")
 
        f.write(f"[{timestamp}]\n")
 
        f.write(f"{content}\n")
 
def view_entries():
 
    """查看所有日记"""
 
    if not os.path.exists(DIARY_FILE):
 
        print("还没有写过日记")
 
        return
 
    with open(DIARY_FILE, "r", encoding="utf-8") as f:
 
        for line in f:
 
            print(line, end="")
 
def search_entries(keyword):
 
    """搜索包含关键字的日记"""
 
    if not os.path.exists(DIARY_FILE):
 
        print("还没有写过日记")
 
        return
 
    with open(DIARY_FILE, "r", encoding="utf-8") as f:
 
        content = f.read()
 
    entries = content.split("=" * 40)
 
    found = [e for e in entries if keyword in e]
 
    if found:
 
        for entry in found:
 
            print(entry)
 
    else:
 
        print(f"未找到包含 '{keyword}' 的日记")
 
# 主菜单(lambda 做命令分发)
 
commands = {
 
    "1": lambda: add_entry(),
 
    "2": lambda: view_entries(),
 
    "3": lambda: search_entries(input("搜索关键字:")),
 
    "4": lambda: exit("再见!")
 
}
 
while True:
 
    print("\n=== 日记本 ===")
 
    print("1. 写日记  2. 查看全部  3. 搜索  4. 退出")
 
    choice = input("> ")
 
    action = commands.get(choice)
 
    if action:
 
        action()
 
    else:
 
        print("无效选择")
 

速记卡(面试闪卡)

Q1:一句话讲清「装饰器(Decorator)—— 给函数”穿衣服”」到底是什么?

A:装饰器本质是一个返回函数的闭包,不改原码就能给函数加功能。

Q2:一、装饰器像什么、为什么需要 —— 怎么理解?

A:你有一部手机(func),不想拆机就能加壳贴膜装防摔套——这些外套就是装饰器。手机还是那部,但防摔美观增强了。痛点:给一批函数统一加日志/计时/权限,逐个改源码要改 N 处;装饰器把横切逻辑抽出来包裹原函数,源码一行不动。英文:decorator(装饰器)、AOP(面向切面)、closure(闭包)。

Q3:二、手写最朴素装饰器(三层结构) —— 怎么理解?

A:def logger(func): 外层收被装饰函数;def wrapper(*args,**kwargs): 内层是替代原函数的壳,先干额外活、再 result=func(…) 真正干活、后处理、return result;return wrapper 返回壳。手动 add=logger(add) 就包了一层,@logger 写在 def 上等价于这行。英文:wrapper(包装函数)、*args/**kwargs。

Q4:三、常见实用装饰器有哪些 —— 怎么理解?

A:计时 @timer(前后掐 perf_counter)、重试 @retry、权限校验、单例 @singleton;内置还有 @property(方法当属性)、@staticmethod/@classmethod、@functools.lru_cache(记忆化缓存)。手写考点常让你写计时/重试/权限/单例——套路永远是”外层收 func、内层 wrapper 包参数、返回 wrapper”。英文:lru_cache、property。

Q5:四、为什么装饰器里要加 @wraps —— 怎么理解?

A:add 被替换成 wrapper 后,add.name 变成 “wrapper”、doc 丢失,调试/日志/Flask 路由名会乱。解法:在 wrapper 上加 @wraps(func)(来自 functools),把原函数的 name/doc/module 复制回来。铁律:写装饰器就在 wrapper 上加 @wraps。英文:functools.wraps、metadata(元信息)。

Q6:核心速记主线有哪些?

  • 装饰器=返回函数的闭包,AOP 横切思想

  • @ 是 f=deco(f) 语法糖

  • 手写三层模板:收 func / wrapper 包参数 / 返回 wrapper

  • 常见装饰器:timer/retry/property/lru_cache

  • wrapper 上必须 @wraps 保元信息

口诀

A:装饰器是闭包筐,不改原码加功能;

@ 只是语法糖,等于 f=deco(f) 套上。

三层模板记心房,外层收 func 内层包参数;

wrapper 加 @wraps,元信息保住才像样。

相关链接