系列文章

Python 从入门到精通

第 17 / 36 篇

从环境安装和基础语法出发,逐步学习工程实践、自动化、数据处理与 Web 开发。

  1. 01
    Python 怎么装、怎么用
  2. 02
    变量、数字和字符串
  3. 03
    列表:把一组数据放在一起
  4. 04
    元组、集合和字典
  5. 05
    条件判断:让程序做选择
  6. 06
    循环:重复的事交给程序
  7. 07
    函数:把代码整理成可复用的块
  8. 08
    模块与包:拆分你的程序
  9. 09
    输入、输出与字符串格式化
  10. 10
    文件读写:保存程序的数据
  11. 11
    异常处理:程序出错时怎么办
  12. 12
    基础阶段练习:命令行记账本
  13. 13
    类和对象:面向对象入门
  14. 14
    继承、组合与特殊方法
  15. 15
    迭代器与生成器
  16. 16
    列表推导式与生成器表达式
  17. 17
    装饰器:给函数增加能力正在阅读
  18. 18
    上下文管理器与 with
  19. 19
    类型标注与 dataclass
  20. 20
    正则表达式:从文本中找规律
  21. 21
    日期、时间与时区
  22. 22
    日志与调试
  23. 23
    虚拟环境与依赖管理
  24. 24
    测试:让修改不再提心吊胆
  25. 25
    网络请求:用 Python 调用 API
  26. 26
    网页解析与合规采集
  27. 27
    操作 Excel、CSV 与批量文件
  28. 28
    SQLite:给程序加一个数据库
  29. 29
    数据分析入门:NumPy 与 Pandas
  30. 30
    画图:把数据变得直观
  31. 31
    Flask 入门:做一个小网站
  32. 32
    异步编程:同时处理多项任务
  33. 33
    线程、进程与并发选择
  34. 34
    性能分析与优化
  35. 35
    项目结构、配置与发布
  36. 36
    综合项目:从需求到上线

查看整个系列 →

这一篇讲 Python 里最”魔法”的语法之一:装饰器(decorator)。你可能已经见过 @staticmethod@property 这种带 @ 的写法。装饰器的本质是:在不改函数代码的前提下,给函数加功能。日志、计时、权限检查、缓存——都是它的经典应用。

先理解:函数是一等公民

Python 里函数也是对象,可以传来传去、可以嵌套定义:

def greet(name):
    print(f"你好,{name}")

f = greet          # 把函数赋给变量(不加括号!)
f("南山")           # 你好,南山

def make_adder(x):
    def adder(y):      # 嵌套函数
        return x + y
    return adder       # 返回函数

add5 = make_adder(5)
print(add5(3))         # 8(add5 记住了 x=5,这就是闭包)

嵌套函数”记住”了外层变量(x=5),这种结构叫闭包(closure)。装饰器就是建立在闭包之上的。

手写第一个装饰器

需求:给函数加计时功能,但不改函数本身。

import time

def timed(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)   # 调用原函数
        elapsed = time.time() - start
        print(f"{func.__name__} 耗时 {elapsed:.4f} 秒")
        return result
    return wrapper

def slow_add(a, b):
    time.sleep(0.5)
    return a + b

# 手动用法
slow_add = timed(slow_add)
print(slow_add(1, 2))   # slow_add 耗时 0.5001 秒 / 3

timed 接收一个函数,返回一个新函数 wrapper。wrapper 里:先计时 → 调原函数 → 打印耗时 → 返回结果。原函数一行没改,功能加上了

@ 语法糖

上面的手动写法太啰嗦,Python 提供了 @ 语法:

@timed
def slow_add(a, b):
    time.sleep(0.5)
    return a + b
# 等价于 slow_add = timed(slow_add)

@timed 写在函数定义上一行,就是”把下面这个函数交给 timed 处理”。代码干净多了。

带参数的装饰器

有时候装饰器自己也要参数,比如”日志级别”。做法:再包一层:

def log_with(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{level}] 调用 {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log_with("INFO")
def hello():
    print("你好")

hello()   # [INFO] 调用 hello / 你好

结构:log_with("INFO") 先执行,返回 decorator;decorator 再接收函数。三层嵌套,理解”每一层返回下一层”即可。

functools.wraps:别弄丢函数信息

装饰后有个副作用:函数的名字、文档字符串变成了 wrapper 的:

def timed(func):
    def wrapper(*args, **kwargs):
        ...
    return wrapper

@timed
def slow_add(a, b):
    """两个数相加"""
    ...

print(slow_add.__name__)   # wrapper(应该是 slow_add!)
print(slow_add.__doc__)    # None(应该是 两个数相加!)

解决办法:装饰器内部用 functools.wraps

from functools import wraps

def timed(func):
    @wraps(func)                 # 把原函数的名字、文档等复制过来
    def wrapper(*args, **kwargs):
        ...
    return wrapper

写装饰器时永远记得加 @wraps,这是规范。

实战:三个常用装饰器

from functools import wraps
import time

# 1. 重试:失败自动重试 N 次
def retry(times=3):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            last_error = None
            for i in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    last_error = e          # 记住最后一次异常
                    print(f"第 {i+1} 次失败:{e}")
            raise last_error               # 重试耗尽,把最后一次异常抛出去
        return wrapper
    return decorator

@retry(times=3)
def unstable_api():
    import random
    if random.random() < 0.6:
        raise ConnectionError("网络抖动")
    return "成功"

print(unstable_api())   # 失败重试,最终成功

# 2. 缓存:相同参数不重复计算(记忆化)
def memoize(func):
    cache = {}
    @wraps(func)
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(30))   # 832040,秒出(不缓存的话要算半天)

# 3. 权限检查
def require_login(func):
    @wraps(func)
    def wrapper(user, *args, **kwargs):
        if not getattr(user, "logged_in", False):
            raise PermissionError("请先登录")
        return func(user, *args, **kwargs)
    return wrapper

class User:
    def __init__(self, logged_in):
        self.logged_in = logged_in

@require_login
def view_profile(user):
    return "个人资料页"

print(view_profile(User(True)))   # 个人资料页
# view_profile(User(False))       # PermissionError: 请先登录

多个装饰器叠加

一个函数可以叠多个装饰器,从上往下装饰,从下往上执行

@timed
@log_with("INFO")
def work():
    time.sleep(0.1)
    print("干活中")

work()

执行顺序:work = timed(log_with(“INFO”)(work))。调用时先经过 timed 的 wrapper,再进 log 的 wrapper,最后才到原函数。离函数最近的装饰器先执行

新手坑

坑 1:忘加 @wraps。函数名、文档字符串全丢,调试时一脸懵。

坑 2:装饰器忘 return。装饰器函数必须返回一个函数,返回 None 的话原函数就变成 None 了。

坑 3:带参数装饰器少包一层。@log_with(“INFO”) 这种如果 decorator 只写两层,会报”参数类型错误”——数清楚层数。

小结

  • 函数是一等公民,可赋值、可嵌套、可返回(闭包)
  • 装饰器 = 接收函数、返回新函数的函数;@装饰器 是语法糖
  • 带参数装饰器三层嵌套;多个装饰器从下往上执行
  • 写装饰器永远加 @wraps(func)
  • 经典应用:计时、日志、重试、缓存、权限检查

练习

  1. 写一个 @logged 装饰器:调用前后各打印一行日志(函数名 + 参数)
  2. 写一个 @slow_down 装饰器:调用前 sleep 1 秒(提示:wrapper 里 time.sleep(1))
  3. 思考:为什么装饰器返回的 wrapper 要用 *args, **kwargs 接参数?如果原函数有默认参数或关键字参数,漏掉会怎样?