Python 编程 - 装饰器

Python 3 装饰器是在不修改原函数代码的前提下,为函数动态添加额外功能的高级工具。其核心基于“闭包”和“函数式编程”思想。

下面我从基础原理高阶实战,为你构建一套完整的知识体系。

1. 核心原理:函数即“对象”

在 Python 中,函数是一等公民,这意味着它可以像变量一样被传递、赋值和返回。

def say_hello():
    print("Hello")

# 将函数赋值给变量
greet = say_hello
greet()  # 输出 Hello

# 将函数作为参数传入另一个函数
def run(func):
    func()
run(say_hello)  # 输出 Hello

装饰器正是利用了这一特性:它是一个接收函数作为参数、返回新函数的可调用对象。


2. 手写第一个装饰器(无参数)

最标准的装饰器结构是三层闭包(外层接收函数,内层包装逻辑,返回包装函数)。

import functools
import time

# 定义一个计时装饰器
def timer(func):
    @functools.wraps(func)  # 关键!保留原函数的元数据(如 __name__,文档字符串)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)  # 执行原函数
        end = time.perf_counter()
        print(f"{func.__name__} 执行耗时: {end - start:.4f} 秒")
        return result
    return wrapper

# 使用语法糖 @
@timer
def long_task(n):
    time.sleep(n)
    return n * 2

long_task(1)  # 输出: long_task 执行耗时: 1.0002 秒

3. 带参数的装饰器(三层嵌套)

如果需要让装饰器本身接收参数(如重复次数、日志级别),需要再包裹一层“装饰器工厂函数”。

def repeat(times):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def say_hi():
    print("Hi")

say_hi()  # 会连续打印 3 次 "Hi"

(调用顺序:repeat(3) 返回 decorator@decorator 再接收 say_hi


4. 类装饰器(使用 __call__

如果装饰逻辑复杂、需要维护状态,可以用类来实现(利用 __init__ 接收函数,__call__ 包装逻辑)。

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)  # 手动拷贝元数据
        self.func = func
        self.count = 0

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

@CountCalls
def test():
    pass

test()  # 调用第 1 次
test()  # 调用第 2 次

5. 关键陷阱与最佳实践(务必注意)

陷阱 解决方案
元数据丢失:装饰后的函数 __name__docstring 会变成 wrapper 必须使用 @functools.wraps(func) 修复。
作用域问题:内层 wrapper 中修改外层变量(如 count)会报错。 使用 nonlocal 声明(Python 3 专属)。
类装饰器与 self:装饰类方法时,wrapper 必须接收 self 参数,否则会报错。 wrapper(*args, **kwargs) 中透传,self 会自动传递。

6. 经典实战场景(拿来即用)

场景一:重试机制(处理网络抖动)

def retry(max_attempts=3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts:
                        raise
                    print(f"重试第 {attempt} 次...")
        return wrapper
    return decorator

场景二:参数校验(类型检查)

def type_check(**types):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # 绑定参数名和值
            bound = func.__annotations__  # 或使用 inspect.signature
            for name, value in {**kwargs}.items():
                if name in types and not isinstance(value, types[name]):
                    raise TypeError(f"{name} 应为 {types[name]}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@type_check(name=str, age=int)
def greet(name, age):
    print(f"{name} is {age}")

场景三:缓存(LRU,Python 3.2+ 内置)

from functools import lru_cache

@lru_cache(maxsize=128)
def expensive_fib(n):
    if n < 2: return n
    return expensive_fib(n-1) + expensive_fib(n-2)

7. 进阶:同时兼容带参和不带参的装饰器

为了让装饰器既可以 @deco 用,也可以 @deco(arg) 用,需要做一个“智能适配”,不过日常开发中分开写更清晰,不建议过度设计。


8. Python 3 特有增强

  • nonlocal:让嵌套函数修改外层变量成为可能(装饰器计数器必备)。
  • functools.wraps:修复元数据的标准方案(Python 3.2+ 完善)。
  • 异步装饰器:若装饰 async defwrapper 也必须定义为 async def,并用 await func() 调用。
def async_timer(func):
    @functools.wraps(func)
    async def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = await func(*args, **kwargs)
        print(f"耗时 {time.perf_counter() - start}")
        return result
    return wrapper

总结一句话

写装饰器 = 写“返回函数的函数”;用装饰器 = 用 @ 语法糖增强函数。
记住 @functools.wraps 是保命符,理解“闭包”是核心,掌握“带参装饰器”是分水岭。

posted @ 2026-06-21 21:24  箫笛  阅读(24)  评论(0)    收藏  举报