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

Python装饰器完全指南:从基础到高级应用

admin15小时前Python6
装饰器是 Python 中最强大也最容易被误解的特性之一。很多初学者听说过装饰器,但总是感觉云里雾里,不敢在实际项目中使用。本文从最基础的概念讲起,逐步深入到高级应用场景,通过大量原创示例代码帮助你彻底掌握装饰器的用法。无论你是 Python 初学者还是有一定经验的开发者,相信都能从中收获新知识。 装饰器本质上就是一个接受函数作为参数并返回新函数的可调用对象。它允许你在不修改原函数代码的情况下,给函数增加额外的功能。这种特性在面向切面编程(AOP)中非常有用,比如日志记录、性能统计、权限校验、缓存处理等场景都非常适合使用装饰器。 理解装饰器的关键在于理解:Python 中一切皆对象,函数也是第一类公民,可以作为参数传递,可以被赋值给变量,可以在函数内部定义函数。

让我们从最简单的例子开始,看看装饰器最基本的结构是什么样的。

python
def simple_decorator(func):
    def wrapper():
        print("函数调用前执行")
        func()
        print("函数调用后执行")
    return wrapper

@simple_decorator
def hello():
    print("Hello, World!")


hello()


输出结果:

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


上面这个例子非常直观,我们通过装饰器给 `hello` 函数增加了在调用前后打印日志的功能,而完全不需要修改 `hello` 函数本身的代码。这就是装饰器的核心优势——开闭原则:对扩展开放,对修改关闭。

上面的示例中被装饰的函数没有参数,如果原函数有参数怎么办呢?其实很简单,我们可以在 `wrapper` 函数中使用 `*args` 和 `**kwargs` 来接收任意数量的位置参数和关键字参数,然后透传给原函数。
python
def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"调用函数: {func.__name__}, 参数: {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"函数 {func.__name__} 执行完毕")
        return result
    return wrapper

@log_decorator
def add(a, b):
    return a + b

@log_decorator
def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")

print(add(2, 3))
greet("Alice", greeting="Good morning")


输出结果:

调用函数: add, 参数: (2, 3), {}
函数 add 执行完毕
5
调用函数: greet, 参数: ('Alice',), {'greeting': 'Good morning'}
Good morning, Alice!
函数 greet 执行完毕


现在我们的装饰器已经可以处理任意参数的函数了。`*args` 接收所有位置参数,`**kwargs` 接收所有关键字参数,然后透传给原函数 `func`,最后还把原函数的返回值正确返回出去了,完美!

不知道你有没有发现一个小问题:被装饰之后,函数的元信息变了。比如我们打印 `add.__name__`,会得到 `wrapper` 而不是原来的 `add`。这是因为我们返回的是新函数 `wrapper`,原函数的元信息都丢失了。这对于调试和一些反射操作来说不太友好。

怎么解决这个问题呢?Python 标准库提供了 `functools.wraps` 装饰器专门用来处理这个问题,它会把原函数的元信息复制到包装函数中。

python
import functools

def log_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"调用函数: {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_decorator
def add(a, b):
    return a + b

print(add.__name__)  # 输出: add,而不是 wrapper


所以记住,写装饰器的时候一定要加上 `@functools.wraps(func)`,这是一个很好的习惯,可以避免很多奇怪的问题。

有时候我们希望装饰器本身也可以接收参数,来配置装饰器的行为。比如,我们想要一个日志装饰器,可以指定日志级别。怎么实现呢?其实很简单,只需要再包一层,外层函数接收参数,然后返回真正的装饰器。
python
import functools
import logging

def log(level="INFO"):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if level == "INFO":
                logging.info(f"调用函数 {func.__name__}")
            elif level == "DEBUG":
                logging.debug(f"调用函数 {func.__name__}, 参数 {args} {kwargs}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@log(level="DEBUG")
def multiply(a, b):
    return a * b

@log()
def subtract(a, b):
    return a - b



如果你希望装饰器既可以带参数使用,也可以不带参数直接使用,需要做一点特殊处理,让它自动识别:

python
import functools

def smart_decorator(func_or_arg=None, *, option=False):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if option:
                print("Option is enabled")
            return func(*args, **kwargs)
        return wrapper
    
    if func_or_arg is None:
        # 带参数调用: @smart_decorator(option=True)
        return decorator
    else:
        # 不带参数调用: @smart_decorator
        return decorator(func_or_arg)

@smart_decorator(option=True)
def test1():
    pass

@smart_decorator
def test2():
    pass


这样就完美支持两种用法了。


现在我们来看几个装饰器在实际开发中的常见用法,这些都是可以直接用到项目中的代码示例。


对于一些计算密集型的函数,如果输入参数相同,我们希望把结果缓存起来,避免重复计算,这就是缓存装饰器的作用。

python
import functools

def cache(func):
    cache_dict = {}
    
    @functools.wraps(func)
    def wrapper(*args):
        # 注意:这里只支持不可变的位置参数作为缓存键
        if args not in cache_dict:
            cache_dict[args] = func(*args)
        return cache_dict[args]
    return wrapper

@cache
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(50))  # 计算非常快,因为已经缓存了中间结果


Python 标准库其实已经提供了成熟的缓存实现 `functools.lru_cache`,比我们自己写的更完善,支持最大缓存数量限制,支持清除缓存等功能。但是理解了原理之后,你就可以根据自己的需求定制缓存逻辑了。

用来统计函数执行时间,性能优化的时候非常有用。
python
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()
        print(f"{func.__name__} 执行时间: {end_time - start_time:.6f} 秒")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    return "Done"

slow_function()


输出:

slow_function 执行时间: 1.001234 秒



当网络请求失败的时候,我们希望自动重试几次,而不是直接报错,这用装饰器实现非常优雅:

python
import functools
import time

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

import requests

@retry(max_retries=3, delay=2)
def fetch_url(url):
    response = requests.get(url, timeout=5)
    response.raise_for_status()
    return response.text



Python 是动态类型语言,但有时候我们希望强制检查函数参数类型,可以用装饰器实现:

python
import functools

def type_check(*types):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if len(args) != len(types):
                raise ValueError(f"参数数量不匹配,期望 {len(types)} 个,实际 {len(args)} 个")
            
            for arg, expected_type in zip(args, types):
                if not isinstance(arg, expected_type):
                    raise TypeError(f"参数类型错误,期望 {expected_type.__name__},实际是 {type(arg).__name__}")
            
            return func(*args, **kwargs)
        return wrapper
    return decorator

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

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



你可以给一个函数同时应用多个装饰器,它们会从内向外执行,或者说从下向上应用。让我们看个例子:

python
def decorator1(func):
    def wrapper():
        print("装饰器 1 开始")
        func()
        print("装饰器 1 结束")
    return wrapper

def decorator2(func):
    def wrapper():
        print("装饰器 2 开始")
        func()
        print("装饰器 2 结束")
    return wrapper

@decorator1
@decorator2
def hello():
    print("Hello")

hello()


输出结果:

装饰器 1 开始
装饰器 2 开始
Hello
装饰器 2 结束
装饰器 1 结束


堆叠顺序非常重要,记住:离函数最近的装饰器先应用,然后外层的装饰器再包装内层包装后的结果。上面的例子等价于 `hello = decorator1(decorator2(hello))`。

除了函数装饰器,我们还可以用类来实现装饰器,只需要让类实现 `__call__` 方法即可。类装饰器适合需要维护内部状态的场景,比如统计调用次数:
python
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 say_hello():
    print("Hello")

say_hello()
say_hello()
say_hello()
print(say_hello.num_calls)  # 输出: 3


这里我们用 `functools.update_wrapper(self, func)` 来更新类实例的元信息,作用和 `@functools.wraps` 类似。类装饰器非常强大,因为它可以很方便地用实例属性来保存状态,而函数装饰器要保存状态就需要用到闭包或者 nonlocal 关键字。

当你想用装饰器修饰类的实例方法时,需要注意什么吗?其实不需要特殊处理,因为 `self` 也只是一个普通的参数而已,`*args` 会自动把 `self` 作为第一个参数接收进去,然后透传给原方法。让我们看个例子:
python
import functools

def instance_method_decorator(func):
    @functools.wraps(func)
    def wrapper(self, *args, **kwargs):
        print(f"调用方法 {self.__class__.__name__}.{func.__name__}")
        return func(self, *args, **kwargs)
    return wrapper

class MyClass:
    @instance_method_decorator
    def say_hello(self, name):
        print(f"Hello {name}")

obj = MyClass()
obj.say_hello("World")


即使你不明确写 `self`,直接用 `*args` 也没问题,因为 `args` 会把 `self` 作为第一个元素包含进去,透传的时候不会出问题。明确写出 `self` 只是让代码可读性更好而已。

1. **不要忘记返回结果**:很多新手写包装函数的时候忘记 `return func(*args, **kwargs)`,结果被装饰的函数变成了返回 `None`,调试半天才能发现问题。 2. **一定要保留原函数元信息**:使用 `@functools.wraps`,否则函数名、文档字符串都会丢失,调试的时候非常痛苦。 3. **慎用装饰器修改原函数参数**:装饰器最好透传参数,不要轻易修改参数,这样会让函数行为变得不直观,增加理解成本。 4. **不要滥用装饰器**:不是所有场景都需要用装饰器,如果一个简单的函数调用就能解决问题,就不需要非要用装饰器。过度使用装饰器会让代码变得难以理解和调试。 5. **类装饰器和函数装饰器怎么选?**:如果需要维护状态或者需要更复杂的逻辑,用类装饰器更清晰;简单场景用函数装饰器写起来更简洁。 装饰器是 Python 中一个非常优雅的特性,它可以帮助我们写出更简洁、更易维护的代码,实现横切关注点的分离。本文从最基础的概念开始,一步步讲解了装饰器的各种用法,包括处理参数、保存元信息、带参数的装饰器、实际应用场景、装饰器堆叠、类装饰器等等,并且给出了大量可以直接运行的原创代码示例。

读完这篇文章之后,你应该对装饰器有了完整的理解,可以开始在你的项目中使用装饰器了。记住,实践是最好的老师,看完教程之后,不妨自己动手写几个装饰器,体验一下它的强大之处。

- Python 官方文档: https://docs.python.org/3/glossary.html#term-decorator - 《Python Cookbook》第三版,第 9 章元编程

相关文章

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

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

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

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

[Python 教程] OpenCV 绘图教程:图形与文本标注

OpenCV 绘图教程:图形与文本标注本文介绍如何在 OpenCV 中绘制各种图形和添加文本,用于图像标注和可视化。一、绘制基本图形1.1 创建画布import cv2 import&nb...

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

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

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

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

[Python 教程] Python 网络请求与爬虫基础

Python 网络请求与爬虫基础 requests 是 Python 最常用的 HTTP 库。本文介绍网络请求和爬虫的基础知识。 一、基础请求 import requests # GET 请求 r...

发表评论

访客

看不清,换一张

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