当前位置:首页 > Python > 正文内容

Python 装饰器进阶:从理解到实战

admin12小时前Python6

装饰器是 Python 中一个非常强大的特性,它允许你在不修改原函数代码的情况下,为函数添加额外的功能。很多开发者虽然用过装饰器,但对其底层原理和高级用法理解不深。本文将从基础出发,深入讲解装饰器的工作原理,并通过多个实用的原创示例,带你掌握装饰器的进阶用法,包括带参数的装饰器、类装饰器、装饰器链以及实际开发中的常见应用场景。

什么是装饰器

装饰器本质上是一个接受函数作为输入,并返回一个新函数的函数。它利用了 Python 的闭包特性,允许我们在函数执行前后注入自定义逻辑。

先看一个最简单的装饰器示例:

def simple_decorator(func):
    def wrapper(*args, **kwargs):
        print("函数执行前")
        result = func(*args, **kwargs)
        print("函数执行后")
        return result
    return wrapper

@simple_decorator
def say_hello(name):
    print(f"Hello, {name}!")

say_hello("World")

输出结果:

函数执行前
Hello, World!
函数执行后

这里 @simple_decorator 等价于 say_hello = simple_decorator(say_hello),语法糖让代码更简洁。

为什么需要装饰器

装饰器的核心价值在于遵循开闭原则:对扩展开放,对修改关闭。当你需要为多个函数添加相同的逻辑(比如日志记录、性能统计、权限校验等),装饰器可以帮助你避免代码重复,保持代码整洁。

常见使用场景:

  • 日志记录
  • 性能测试
  • 权限校验
  • 缓存
  • 输入验证
  • 事务处理

保留原函数元信息

使用装饰器后,原函数的 __name____doc__ 等元信息会被装饰器返回的 wrapper 函数覆盖,这对调试和文档生成不友好。解决方法是使用 functools.wraps

import functools

def preserved_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 这里添加装饰逻辑
        return func(*args, **kwargs)
    return wrapper

@preserved_decorator
def test_func():
    """这是测试函数的文档字符串"""
    pass

print(test_func.__name__)  # 输出: test_func
print(test_func.__doc__)   # 输出: 这是测试函数的文档字符串

如果不使用 functools.wraps,输出会变成 wrapperNone,所以养成习惯,永远给你的装饰器加上 functools.wraps

带参数的装饰器

有时候我们希望装饰器本身可以接受参数,来配置它的行为。这时候需要再封装一层:

import functools

def repeat(times):
    """重复执行函数的装饰器,接受次数参数"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    print(f"Hello {name}!")

greet("Alice")

输出:

Hello Alice!
Hello Alice!
Hello Alice!

这里 @repeat(times=3) 的执行过程是:

  1. 调用 repeat(times=3) 返回 decorator 函数
  2. 将原函数传递给 decorator,返回 wrapper
  3. 原函数被替换为 wrapper

类装饰器

除了函数,类也可以作为装饰器。类装饰器主要依靠 __call__ 方法,当你使用 @ 语法装饰函数时,会调用类的构造函数,然后把原函数作为参数传递。

一个简单的示例,统计函数被调用的次数:

import functools

class CountCalls:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.num_calls = 0

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

@CountCalls
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

print(fib(5))

输出中会看到每个递归调用都会计数,非常直观。类装饰器适合需要维护状态,或者多个方法协作的场景。

多个装饰器链式调用

一个函数可以被多个装饰器装饰,执行顺序是从下到上应用,从上到下执行。看这个例子:

import functools

def decorator1(func):
    @functools.wraps(func)
    def wrapper():
        print("装饰器1 执行前")
        func()
        print("装饰器1 执行后")
    return wrapper

def decorator2(func):
    @functools.wraps(func)
    def wrapper():
        print("装饰器2 执行前")
        func()
        print("装饰器2 执行后")
    return wrapper

@decorator1
@decorator2
def say_hi():
    print("Hi!")

say_hi()

输出:

装饰器1 执行前
装饰器2 执行前
Hi!
装饰器2 执行后
装饰器1 执行后

等价于 say_hi = decorator1(decorator2(say_hi)),所以应用顺序是先 decorator2,再 decorator1,执行的时候外层先执行,所以 decorator1 的前处理先执行,然后进入 decorator2

实用装饰器示例

下面是几个实际开发中经常用到的原创装饰器示例。

1. 计时装饰器

统计函数执行时间:

import time
import functools

def timer(func):
    """计时装饰器,打印函数执行耗时"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        result = func(*args, **kwargs)
        end_time = time.perf_counter()
        elapsed = end_time - start_time
        print(f"{func.__name__} 执行耗时: {elapsed:.4f} 秒")
        return result
    return wrapper

@timer
def slow_calculation():
    time.sleep(1)
    return sum(i * i for i in range(10000))

slow_calculation()

2. 缓存装饰器

对函数结果进行缓存,避免重复计算:

import functools

def cache(func):
    """简单的基于字典的缓存装饰器"""
    cached_results = {}

    @functools.wraps(func)
    def wrapper(*args):
        # 只有位置参数可以作为键,关键字参数需要额外处理
        if args not in cached_results:
            cached_results[args] = func(*args)
        return cached_results[args]
    return wrapper

@cache
def fibonacci(n):
    print(f"计算 fibonacci({n})")
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(10))
print("
第二次调用:")
print(fibonacci(10))

第一次计算会递归计算所有子问题,第二次直接从缓存读取,速度提升非常明显。Python 标准库实际上已经提供了 functools.lru_cache,这个示例主要帮助理解缓存装饰器的原理。

3. 输入验证装饰器

验证函数参数类型:

import functools

def validate_types(*expected_types):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args):
            if len(args) != len(expected_types):
                raise ValueError("参数数量不匹配")
            for arg, expected_type in zip(args, expected_types):
                if not isinstance(arg, expected_type):
                    raise TypeError(
                        f"参数 {arg} 类型应为 {expected_type.__name__}, 实际为 {type(arg).__name__}"
                    )
            return func(*args)
        return wrapper
    return decorator

@validate_types(int, int)
def add(a, b):
    return a + b

print(add(2, 3))       # 正常输出 5
# print(add("2", 3))   # 抛出 TypeError

这个装饰器可以快速对函数参数做类型检查,在开发阶段能提前发现很多错误。

4. 异步任务重试装饰器

对于不稳定的网络请求,添加自动重试逻辑:

import asyncio
import functools

def async_retry(max_retries=3, delay=1):
    def decorator(func):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(max_retries):
                try:
                    return await func(*args, **kwargs)
                except Exception as e:
                    last_exception = e
                    print(f"尝试 {attempt + 1} 失败: {e}, {delay} 秒后重试...")
                    await asyncio.sleep(delay)
            # 所有重试都失败后抛出异常
            raise last_exception
        return wrapper
    return decorator

# 使用示例
import aiohttp

@async_retry(max_retries=3, delay=2)
async def fetch_url(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url, timeout=10) as response:
            return await response.text()

# 如果要运行:
# asyncio.run(fetch_url("https://example.com"))

装饰器的常见坑

  1. 多个装饰器顺序错误
    很多新手容易搞反装饰器顺序,记住:离函数近的先应用,离函数远的后应用

  2. 不保留元信息
    忘记使用 functools.wraps,导致调试困难,文档错误。

  3. 装饰器不支持关键字参数
    wrapper 时只用了 *args 没加 **kwargs,导致带关键字参数的函数无法使用装饰器。正确做法永远是 def wrapper(*args, **kwargs)

  4. 带参数装饰器忘记加括号
    当使用带参数装饰器时,@repeat@repeat() 是不同的:

    • @repeat 会把函数直接传给 repeat,返回的是 decorator,调用时会出错
    • @repeat(times=3) 才是正确用法
    所以如果你的装饰器设计成需要参数,哪怕是可选参数,也要提醒用户加上括号。

总结

装饰器是 Python 中非常优雅的一个特性,掌握它能让你的代码更简洁、更符合设计原则。本文从基础原理出发,讲解了:

  1. 装饰器的本质:接受函数,返回函数的闭包
  2. 如何保留原函数元信息:使用 functools.wraps
  3. 带参数装饰器的实现方法:再嵌套一层
  4. 类装饰器的实现:利用 __call__ 方法
  5. 多个装饰器的执行顺序
  6. 四个实用的原创装饰器示例:计时、缓存、验证、异步重试
  7. 常见的坑和避免方法

装饰器的学习曲线比较平缓,从简单的无参数装饰器开始,逐步理解带参数和类装饰器,多写几个示例就能掌握。在实际开发中合理使用装饰器,可以大幅提升代码的可维护性和复用性。

相关文章

[Python 教程] OpenCV-Python 入门:图像处理基础详解

OpenCV-Python 入门:图像处理基础详解OpenCV 是一个跨平台计算机视觉库,轻量级且高效,支持 Python 接口。本文将系统介绍 OpenCV 的核心概念和基础操作。一、OpenCV...

[Python 教程] OpenCV 实战:图像与视频文件处理

OpenCV 实战:图像与视频文件处理本文详细介绍如何使用 OpenCV 处理图像和视频文件,包括读取、显示、保存等操作。一、图像文件操作1.1 读取图像import cv2 #&nb...

[Python 教程] NumPy 数组操作详解

NumPy 数组操作详解 NumPy 是 Python 科学计算的基础库,提供高性能的多维数组对象。本文详细介绍 NumPy 数组的核心操作。 一、创建数组 import numpy as np...

[Python 教程] Pandas 数据分析实战

Pandas 数据分析实战 Pandas 是 Python 数据分析的核心库,提供 DataFrame 和 Series 数据结构。本文介绍 Pandas 的实用技巧。 一、创建 DataFrame...

[Python 教程] Matplotlib 数据可视化教程

Matplotlib 数据可视化教程 Matplotlib 是 Python 最常用的绘图库。本文介绍常用图表的绘制方法。 一、基础设置 import matplotlib.pyplot as pl...

[Python 教程] Python 多线程编程指南

Python 多线程编程指南 Python 的 threading 模块提供多线程支持。本文介绍多线程编程的基础和实用技巧。 一、创建线程 import threading import time...

发表评论

访客

看不清,换一张

◎欢迎参与讨论,请在这里发表您的看法和观点。