Langchain系列文章目录

01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来

Python系列文章目录

PyTorch系列文章目录

机器学习系列文章目录

深度学习系列文章目录

Java系列文章目录

JavaScript系列文章目录

Python系列文章目录

01-【Python-Day 1】告别编程恐惧:轻松掌握 Python 安装与第一个程序的 6 个步骤
02-【Python-Day 2】掌握Python基石:变量、内存、标识符及int/float/bool数据类型
03-【Python-Day 3】玩转文本:字符串(String)基础操作详解 (上)
04-【Python-Day 4】玩转文本:Python 字符串常用方法深度解析 (下篇)
05-【Python-Day 5】Python 格式化输出实战:%、format()、f-string 对比与最佳实践
06- 【Python-Day 6】从零精通 Python 运算符(上):算术、赋值与比较运算全解析
07-【Python-Day 7】从零精通 Python 运算符(下):逻辑、成员、身份运算与优先级规则全解析
08-【Python-Day 8】从入门到精通:Python 条件判断 if-elif-else 语句全解析
09-【Python-Day 9】掌握循环利器:for 循环遍历序列与可迭代对象详解
10-【Python-Day 10】Python 循环控制流:while 循环详解与 for 循环对比
11-【Python-Day 11】列表入门:Python 中最灵活的数据容器 (创建、索引、切片)
12-【Python-Day 12】Python列表进阶:玩转添加、删除、排序与列表推导式
13-【Python-Day 13】Python 元组 (Tuple) 详解:从创建、操作到高级应用场景一网打尽
14-【Python-Day 14】玩转Python字典(上篇):从零开始学习创建、访问与操作
15-【Python-Day 15】深入探索 Python 字典 (下):常用方法、遍历、推导式与嵌套实战
16-【Python-Day 16】代码复用基石:详解 Python 函数的定义与调用
17-【Python-Day 17】玩转函数参数(上):轻松掌握位置、关键字和默认值
18-【Python-Day 18】玩转函数参数(下):*args 与 **kwargs 终极指南
19-【Python-Day 19】函数的回响:深入理解 return 语句与返回值
20-【Python-Day 20】揭秘Python变量作用域:LEGB规则与global/nonlocal关键字详解
21-【Python-Day 21】一行搞定!Python lambda 匿名函数的妙用与实战
22-【Python-Day 22】代码的基石:模块(Module)的导入与使用详解
23-【Python-Day 23】Python 模块化编程实战:创建、导入及 sys.path 深度解析
24-【Python-Day 24】告别杂乱代码!一文掌握 Python 包(Package)的创建与使用
25-【Python-Day 25】玩转数字:精通 math 与 random 模块,从数学运算到随机抽样
26-【Python-Day 26】解锁时间魔法:深入解析 time 与 datetime 模块
27-【Python-Day 27】轻松驾驭操作系统:精通 os 与 sys 模块核心功能
28-【Python-Day 28】从指令到蓝图:Python面向对象编程(OOP)入门指南
29-【Python-Day 29】万物皆对象:详解 Python 类的定义、实例化与 __init__ 方法
30-【Python-Day 30】从 self、cls 到 @staticmethod:Python 面向对象三大方法深度解析
31-【Python-Day 31】一文搞懂 Python 实例属性与类属性:从定义、区别到应用场景
32-【Python-Day 32】面向对象基石之封装:从 __private@property 的深度解析
33-【Python-Day 33】OOP核心之继承(Inheritance):代码复用与扩展的艺术
34-【Python-Day 34】深入解析Python继承:super()函数、MRO与菱形继承问题
35-【Python-Day 35】深入理解多态:代码更灵活的“鸭子类型”魔法
36-【Python-Day 36】解密文件IO:一文搞懂 Python 读写模式、编码与指针操作
37-【Python-Day 37】程序的守护者:一文彻底搞懂 Python 异常处理 (try-except-else-finally)
38-【Python-Day 38】告别通用错误!一文学会创建和使用 Python 自定义异常
39-【Python-Day 39】精通Python推导式:告别冗长for循环,提升代码效率与格调
40-【Python-Day 40】告别内存溢出!Python 生成器 (Generator) 的原理与实战
41-【Python-Day 41】揭秘函数的“魔法外衣”:一文彻底搞懂 Python 装饰器 (Decorator)



摘要

欢迎来到 Python 学习之旅的第 41 天!今天,我们将深入探讨一个 Python 中极其强大且富有魅力的特性——装饰器 (Decorator)。装饰器是 Python 函数式编程思想的杰出体现,它允许我们在不修改原函数代码的前提下,为其增加额外的功能。本文将从装饰器的基本概念、实现原理、语法糖 @,到 functools.wraps 的作用,再到带参数的装饰器和类装饰器等高级用法,为你构建一个完整而清晰的知识体系。通过本文,你将能彻底理解装饰器的工作机制,并学会在实际项目中灵活运用它来编写更优雅、更模块化的代码。

一、什么是装饰器?

从本质上讲,装饰器是一个函数,它接收一个函数作为参数,并返回一个新的函数。这个新的函数通常会“包装”或“装饰”原始函数,从而在调用原始函数之前或之后执行一些额外的代码。

1.1 核心思想:开放封闭原则

装饰器的设计完美契合了软件工程中的一个重要原则——开放封闭原则 (Open-Closed Principle)。该原则指出:软件实体(类、模块、函数等)应该对扩展开放,对修改封闭

这意味着,当我们想为一个已有的函数增加新功能时(例如,记录日志、计算运行时间、进行权限检查),我们不应该去修改这个函数内部的代码,而是通过装饰器这种“外挂”的方式来扩展它。这样做的好处是:

  • 保持原函数纯净:原函数的核心逻辑不变,易于维护和测试。
  • 功能复用:同一个装饰器可以被应用到多个不同的函数上。
  • 代码解耦:将核心业务逻辑与附加功能(如日志、缓存)分离开来。

1.2 生活中的类比:给礼物包装

想象一下,你有一个礼物(原始函数),你想把它送给朋友。

  • 原始函数:礼物本身,比如一个玩具熊。
  • 装饰器:包装纸、彩带和贺卡。
  • 新函数:经过包装后的整个礼物。

包装的过程并没有改变玩具熊本身,但它为礼物增添了美感(附加功能),并附带了祝福信息(附加功能)。朋友收到的是一个包装好的礼物,他打开包装最终还是能拿到那个玩具熊。

二、装饰器的基石:函数是一等公民

在 Python 中,函数被视为“一等公民”(First-Class Citizen),这意味着函数与其他数据类型(如整数、字符串、列表)的地位是相同的。理解这一点是掌握装饰器的前提。

2.1 函数可以作为变量

一个函数可以被赋值给一个变量,通过这个变量可以调用该函数。

def say_hello():
    print("Hello, Python!")

# 将函数 say_hello 赋值给变量 greet
greet = say_hello

# 通过变量 greet 调用函数
greet()  # 输出: Hello, Python!

2.2 函数可以作为参数传递

一个函数可以作为另一个函数的参数。

def greet(func):
    # 在 greet 函数内部调用传入的 func 函数
    func()

def say_hello():
    print("Hello from say_hello!")

# 将 say_hello 函数作为参数传递给 greet 函数
greet(say_hello)  # 输出: Hello from say_hello!

2.3 函数可以作为返回值

一个函数可以返回另一个函数。这种在函数内部定义并返回另一个函数的结构,通常被称为闭包 (Closure)

def get_greeter():
    def say_hello():
        print("Hello from inner function!")
    # 返回内部定义的函数
    return say_hello

# 调用 get_greeter 函数,其返回值是一个函数
greeter_func = get_greeter()

# 调用返回的函数
greeter_func() # 输出: Hello from inner function!

正是因为函数具备这些特性,我们才能够创造出接收函数、返回新函数的“装饰器”。

三、从零开始,手写第一个装饰器

让我们通过一个实际需求来一步步构建装饰器。

3.1 需求场景:为函数计时

假设我们有两个业务函数,我们想知道每个函数执行所花费的时间,但又不想在每个函数内部都写一遍计时逻辑。

import time

def slow_function_A():
    time.sleep(2)  # 模拟耗时操作
    print("Function A has finished.")

def slow_function_B():
    time.sleep(1)  # 模拟耗时操作
    print("Function B has finished.")

3.2 最原始的实现

最直接但最笨拙的方法是修改每个函数:

def slow_function_A():
    start_time = time.time()
    time.sleep(2)
    print("Function A has finished.")
    end_time = time.time()
    print(f"Function A took {end_time - start_time:.2f} seconds.")

# 这种方式违反了“对修改封闭”的原则,且代码重复。

3.3 进化:使用闭包实现装饰器

我们可以利用“函数作为参数”和“闭包”的思想来创建一个计时器函数,这就是装饰器的原型。

import time

def timer_decorator(func):
    """一个简单的计时器装饰器"""
    # 定义一个包装函数,它将接收原函数的所有参数
    def wrapper(*args, **kwargs):
        start_time = time.time()
        # 调用原始函数,并传递其参数
        result = func(*args, **kwargs)  
        end_time = time.time()
        print(f"Function '{func.__name__}' took {end_time - start_time:.2f} seconds to run.")
        # 返回原始函数的执行结果
        return result
    # 返回包装函数
    return wrapper

# ---- 手动应用装饰器 ----
def slow_function_A():
    time.sleep(2)
    print("Function A has finished.")

def slow_function_B(name):
    time.sleep(1)
    print(f"Function B for {name} has finished.")
    return "OK"

# 装饰 slow_function_A
decorated_A = timer_decorator(slow_function_A)
decorated_A()

print("-" * 20)

# 装饰 slow_function_B
decorated_B = timer_decorator(slow_function_B)
status = decorated_B("Alice")
print(f"Status from B: {status}")

代码解析:

  1. timer_decorator 接收一个函数 func (如 slow_function_A) 作为输入。
  2. 它内部定义了一个新的函数 wrapper。这个 wrapper 函数就是未来用来替代 func 的新函数。
  3. wrapper 使用 *args**kwargs 来接收任意数量的位置参数和关键字参数,确保它可以包装任何函数。
  4. wrapper 内部,我们先记录开始时间,然后调用原始函数 func(*args, **kwargs),再记录结束时间,最后打印耗时。
  5. timer_decorator 返回这个 wrapper 函数。
  6. decorated_A = timer_decorator(slow_function_A) 这行代码的本质是,decorated_A 现在指向了 timer_decorator 返回的 wrapper 函数。当我们调用 decorated_A() 时,实际上是在执行 wrapper()

四、语法糖 @:让装饰器更优雅

Python 提供了一个非常方便的语法糖 @,来简化装饰器的应用。

4.1 @ 符号的本质

@decorator_name 放在一个函数定义之前,就等同于在函数定义之后执行 function_name = decorator_name(function_name)

# 这段代码
@timer_decorator
def my_func():
    pass

# 完全等同于下面这段代码
def my_func():
    pass
my_func = timer_decorator(my_func)

4.2 使用 @ 简化代码

现在,我们可以用 @ 来重写上面的计时器例子,代码变得更加简洁和直观。

import time

def timer_decorator(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Function '{func.__name__}' took {end_time - start_time:.2f} seconds.")
        return result
    return wrapper

@timer_decorator
def slow_function_A():
    """这是一个演示函数A"""
    time.sleep(2)
    print("Function A has finished.")

@timer_decorator
def slow_function_B(name):
    """这是一个演示函数B"""
    time.sleep(1)
    print(f"Function B for {name} has finished.")
    return "OK"

# 现在可以直接调用原始函数名,它已经被装饰了
slow_function_A()
print("-" * 20)
status = slow_function_B("Bob")
print(f"Status from B: {status}")

五、完美的伪装:functools.wraps 的妙用

5.1 装饰器带来的“副作用”

虽然我们的装饰器工作得很好,但它有一个隐藏的问题:它改变了原函数的元信息(metadata)。

让我们检查一下被装饰后的 slow_function_A 的信息:

# 在上一个代码块之后运行
print(f"Function name: {slow_function_A.__name__}")
print(f"Docstring: {slow_function_A.__doc__}")

输出结果:

Function name: wrapper
Docstring: None

你会发现,函数名变成了 wrapper,文档字符串 __doc__ 也丢失了。这是因为我们调用的 slow_function_A 实际上是 wrapper 函数。这在调试和使用 help() 函数时会造成困扰。

5.2 使用 functools.wraps 修正元信息

为了解决这个问题,Python 的 functools 模块提供了一个专门的装饰器 wrapswraps 本身也是一个装饰器,用在我们的包装函数 wrapper 上。

import time
import functools # 导入 functools 模块

def timer_decorator(func):
    @functools.wraps(func) # 关键步骤:使用 wraps 装饰 wrapper
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Function '{func.__name__}' took {end_time - start_time:.2f} seconds.")
        return result
    return wrapper

@timer_decorator
def slow_function_C():
    """这是一个被正确装饰的函数C"""
    time.sleep(1)
    print("Function C has finished.")

# 再次检查元信息
print(f"Function name: {slow_function_C.__name__}")
print(f"Docstring: {slow_function_C.__doc__}")

slow_function_C()

输出结果:

Function name: slow_function_C
Docstring: 这是一个被正确装饰的函数C
Function 'slow_function_C' took 1.00 seconds.
Function C has finished.

可以看到,@functools.wraps(func) 巧妙地将原始函数 func__name____doc__ 等元信息复制到了 wrapper 函数上,使得装饰后的函数看起来和原始函数一模一样。在编写任何装饰器时,都强烈建议使用 @functools.wraps

六、装饰器进阶玩法

6.1 带参数的装饰器

有时,我们希望装饰器本身能够接收参数,以便更灵活地配置其行为。例如,我们想自定义一个重复执行N次的装饰器。

6.1.1 实现原理

带参数的装饰器本质上是一个三层嵌套的函数。最外层函数接收装饰器的参数(如重复次数 n),并返回一个真正的装饰器。

6.1.2 代码实现

import functools

def repeat(n):
    """一个可以传入参数的装饰器,用于重复执行函数 n 次"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"Repeating '{func.__name__}' {n} times.")
            for i in range(n):
                print(f"--- Repetition {i+1} ---")
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

# 使用带参数的装饰器
@repeat(n=3)
def greet(name):
    """打招呼的函数"""
    print(f"Hello, {name}!")
    return "Greeting sent"

# 调用被装饰的函数
final_result = greet("World")
print(f"\nFinal result from greet: {final_result}")

6.2 类装饰器

除了使用函数,我们还可以使用类来实现装饰器。这在需要维护状态时特别有用。

6.2.1 使用类实现装饰器

要让一个类成为装饰器,它需要实现 __init____call__ 方法。

  • __init__(self, func): 在装饰时被调用,接收被装饰的函数 func 作为参数。
  • __call__(self, *args, **kwargs): 在每次调用被装饰的函数时被调用。
import functools

class CountCalls:
    """一个记录函数调用次数的类装饰器"""
    def __init__(self, func):
        functools.update_wrapper(self, func) # 类似 @functools.wraps
        self.func = func
        self.num_calls = 0

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"Call {self.num_calls} of '{self.func.__name__}'")
        return self.func(*args, **kwargs)

@CountCalls
def say_whee():
    print("Whee!")

say_whee() # 第一次调用
say_whee() # 第二次调用
say_whee() # 第三次调用

# 我们可以访问装饰器实例的属性
print(f"Total calls: {say_whee.num_calls}")

输出结果:

Call 1 of 'say_whee'
Whee!
Call 2 of 'say_whee'
Whee!
Call 3 of 'say_whee'
Whee!
Total calls: 3

6.2.2 类装饰器的优势

  • 状态管理:类实例可以方便地存储状态(如 num_calls),而函数闭包需要通过 nonlocal 关键字,有时会更复杂。
  • 代码组织:对于逻辑复杂的装饰器,使用类可以更好地组织代码,使其更符合面向对象的原则。

七、装饰器的常见应用场景

装饰器在 Python 标准库和第三方库中被广泛使用,以下是一些经典的应用场景:

7.1 日志记录 (Logging)

在函数执行前后记录日志信息,便于调试和追踪。

7.2 权限校验 (Authentication)

在 Web 框架(如 Flask、Django)中,常使用装饰器来检查用户是否登录或拥有特定权限,只有通过校验才能执行视图函数。

# Flask 框架中的示例
@app.route('/secret')
@login_required
def secret_page():
    return "This is a secret page."

7.3 性能分析 (Profiling)

精确测量函数执行时间、内存消耗等性能指标,如我们前面实现的计时器。

7.4 缓存 (Caching)

对于计算成本高但结果相对稳定的函数,可以用装饰器将结果缓存起来。当以相同的参数再次调用时,直接返回缓存的结果,避免重复计算。Python 3.9+ 内置了 @functools.cache

import functools
import time

@functools.cache
def fibonacci(n):
    """计算斐波那契数(带缓存)"""
    if n < 2:
        return n
    time.sleep(0.1) # 模拟计算耗时
    return fibonacci(n-1) + fibonacci(n-2)

print("Calculating fib(10)...")
start = time.time()
result = fibonacci(10)
end = time.time()
print(f"Result: {result}, Time: {end - start:.4f}s") # 首次计算,较慢

print("\nCalculating fib(10) again...")
start = time.time()
result = fibonacci(10)
end = time.time()
print(f"Result: {result}, Time: {end - start:.4f}s") # 再次计算,几乎瞬时完成,因为命中了缓存

八、总结

今天,我们系统地学习了 Python 装饰器,它是提升代码质量和开发效率的利器。让我们回顾一下核心要点:

  1. 核心概念:装饰器是一个接收函数并返回新函数的函数,遵循“开放封闭原则”,用于在不修改原函数的基础上增加功能。
  2. 实现基础:Python 中“函数是一等公民”的特性(可赋值、可作参数、可作返回值)是实现装饰器的基石。
  3. 基本实现:通过函数闭包结构,可以手动实现一个装饰器,其内部的 wrapper 函数负责执行额外逻辑和调用原函数。
  4. 语法糖 @@decoratorfunc = decorator(func) 的简洁写法,让代码更具可读性。
  5. functools.wraps:这是编写专业装饰器的必备工具,它能将被装饰函数的元信息(如名称、文档)复制到包装函数上,避免信息丢失。
  6. 高级用法
    • 带参数的装饰器:通过三层函数嵌套实现,最外层函数接收参数,并返回一个标准的装饰器。
    • 类装饰器:通过实现 __init____call__ 方法,将类的实例变成一个可调用的对象,特别适合需要维护状态的场景。
  7. 应用广泛:装饰器在日志、权限、性能、缓存等领域有大量成熟应用,是 Pythonic 编程风格的重要组成部分。

掌握装饰器,意味着你向 Python 高级编程又迈出了坚实的一步。希望你能多加练习,尝试编写自己的装饰器来解决实际问题。


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐