python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python装饰器decorator实际应用

Python装饰器decorator实际应用与高级使用详解

作者:Tipriest_

本文系统讲解Python装饰器,强调其作为函数包装工具的核心作用,通过不修改原函数代码实现功能扩展,适用于日志、计时、认证等场景,利用函数一等公民特性提升代码复用性与可维护性

在这篇文章中,我们来详细、系统地讲解一下Python的装饰器(Decorator)。我会从基本概念讲起,循序渐进,直到实际应用和高级用法。

1. 什么是装饰器?(核心思想)

装饰器本质上是一个Python函数,它可以让其他函数在不改变其源代码的情况下增加额外的功能。

核心思想包装 (Wrapping)

你可以把装饰器想象成一个包装礼物的过程:

礼物(函数)的核心功能没有变,但经过包装(装饰器)后,它看起来更漂亮、更完整了(增加了新功能)。

在代码层面,装饰器是一个接收函数作为参数,并返回一个新函数的函数。

2. 为什么需要装饰器?(动机)

假设我们有很多函数,现在想给每个函数都加上一个“计算运行时间”的功能。

不好的方法:修改每个函数

import time

def func_A():
    start_time = time.time()
    print("函数A开始执行...")
    time.sleep(1)
    print("函数A执行完毕。")
    end_time = time.time()
    print(f"函数A耗时: {end_time - start_time:.2f}秒")

def func_B():
    start_time = time.time()
    print("函数B开始执行...")
    time.sleep(2)
    print("函数B执行完毕。")
    end_time = time.time()
    print(f"函数B耗时: {end_time - start_time:.2f}秒")

# ... 还有 func_C, func_D ...

问题:

装饰器就是为了解决这类问题而生的,它能将这些通用的、与核心业务无关的功能(如计时、日志、认证)抽离出来。

3. Python基础:理解装饰器的前提

要理解装饰器,必须先理解Python中 “函数是一等公民” (Functions are First-Class Objects) 的概念。这意味着函数可以:

被赋值给一个变量

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

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

作为参数传递给另一个函数

def do_something(func):
    print("准备做某事...")
    func()  # 调用传入的函数
    print("做完了。")

def work():
    print("正在努力工作...")

do_something(work)
# 输出:
# 准备做某事...
# 正在努力工作...
# 做完了。

在函数内部定义,并作为返回值返回

def get_greeter():
    def greet():
        print("你好,世界!")
    return greet  # 返回内部定义的greet函数

greeter_func = get_greeter()
greeter_func()  # 输出: 你好,世界!

装饰器正是利用了这几点特性,尤其是第2点和第3点。

4. 一步步构建你的第一个装饰器

最简单的装饰器(手动方式)

我们来写一个简单的装饰器,它在函数执行前后打印信息。

# 1. 定义一个装饰器函数
def my_decorator(func):
    # 2. 定义一个内部包装函数,它将“包装”原始函数
    def wrapper():
        print("在被装饰的函数执行之前...")
        func()  # 3. 调用原始函数
        print("在被装饰的函数执行之后...")
    
    # 4. 返回这个包装好的函数
    return wrapper

# 定义一个需要被装饰的函数
def say_whee():
    print("Whee!")

# 手动使用装饰器
say_whee = my_decorator(say_whee)

# 现在调用say_whee,实际上是在调用wrapper函数
say_whee()

输出:

在被装饰的函数执行之前...
Whee!
在被装饰的函数执行之后...

say_whee = my_decorator(say_whee) 这行代码是理解装饰器的关键。

它用 my_decorator 返回的 wrapper 函数覆盖了原来的 say_whee 函数。

使用@语法糖

Python提供了一个更优雅、更Pythonic的方式来使用装饰器,那就是 @ 语法糖。

def my_decorator(func):
    def wrapper():
        print("在被装饰的函数执行之前...")
        func()
        print("在被装饰的函数执行之后...")
    return wrapper

@my_decorator  # 这行等价于 say_whee = my_decorator(say_whee)
def say_whee():
    print("Whee!")

say_whee()

输出和上面完全一样,但代码是不是简洁多了?

5. 处理带参数的函数

如果我们的原始函数带参数怎么办?比如 greet(name)。上面的 wrapper 函数不接受任何参数,会报错。

我们需要让 wrapper 函数能接受任意参数,并把它们传递给原始函数。这里就要用到 *args**kwargs

def decorator_with_args(func):
    # wrapper现在可以接受任意数量的位置参数和关键字参数
    def wrapper(*args, **kwargs):
        print("装饰器:函数开始执行")
        result = func(*args, **kwargs)  # 将参数传递给原始函数,并保存返回值
        print("装饰器:函数执行完毕")
        return result  # 返回原始函数的执行结果
    return wrapper

@decorator_with_args
def greet(name, message="你好"):
    print(f"{message}, {name}!")
    return f"问候了 {name}"

returned_value = greet("Alice", message="早上好")
print(f"函数返回值: {returned_value}")

输出:

装饰器:函数开始执行
早上好, Alice!
装饰器:函数执行完毕
函数返回值: 问候了 Alice

6.functools.wraps的重要性

使用装饰器有一个小问题:它会丢失原始函数的一些元信息(metadata),比如函数名 (__name__)、文档字符串 (__doc__)等。

@decorator_with_args
def greet(name):
    """这是一个打招呼的函数"""
    print(f"你好, {name}!")

print(greet.__name__)  # 输出: wrapper (而不是 greet)
print(greet.__doc__)   # 输出: None (而不是 "这是一个打招呼的函数")

这对于调试和自省工具来说非常不便。为了解决这个问题,Python的 functools 模块提供了一个专门的装饰器:@wraps

import functools

def decorator_for_wraps(func):
    @functools.wraps(func)  # 关键!将func的元信息拷贝到wrapper上
    def wrapper(*args, **kwargs):
        # ... 装饰器逻辑 ...
        print("装饰器逻辑...")
        return func(*args, **kwargs)
    return wrapper

@decorator_for_wraps
def greet(name):
    """这是一个打招呼的函数"""
    print(f"你好, {name}!")

print(greet.__name__)  # 输出: greet
print(greet.__doc__)   # 输出: 这是一个打招呼的函数

最佳实践: 编写任何装饰器时,都应该在你的 wrapper 函数上使用 @functools.wraps

7. 带参数的装饰器(进阶)

如果我们想让装饰器本身也接收参数呢?比如 @repeat(num=3),让函数重复执行3次。

这就需要再加一层函数嵌套。

结构:

  1. 最外层函数 (repeat):接收装饰器的参数(如 num=3),并返回一个真正的装饰器
  2. 中间层函数 (decorator_repeat):这就是我们之前写的标准装饰器,它接收一个函数作为参数。
  3. 最内层函数 (wrapper):执行增强后的逻辑。
import functools

def repeat(num=2):  # 1. 最外层函数,接收装饰器参数
    def decorator_repeat(func):  # 2. 中间层,接收被装饰的函数
        @functools.wraps(func)
        def wrapper(*args, **kwargs):  # 3. 最内层,执行逻辑
            print(f"函数将重复执行 {num} 次。")
            for _ in range(num):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator_repeat

@repeat(num=4)
def say_hello(name):
    print(f"Hello, {name}!")

say_hello("World")

执行过程:

  1. @repeat(num=4) 首先被调用,它返回 decorator_repeat 函数。
  2. Python接着执行 @decorator_repeat,这等价于 say_hello = decorator_repeat(say_hello)
  3. 最终 say_hello 变量指向了 wrapper 函数。

8. 类装饰器(进阶)

除了用函数,我们也可以用类来创建装饰器。这在需要维护状态时特别有用。一个类要成为装饰器,需要实现 __init____call__ 方法。

class Counter:
    def __init__(self, func):
        functools.update_wrapper(self, func) # 类似 @wraps
        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)

@Counter
def some_function():
    print("执行 some_function")

some_function()
some_function()
some_function()

输出:

'some_function' 已被调用 1 次。
执行 some_function
'some_function' 已被调用 2 次。
执行 some_function
'some_function' 已被调用 3 次。
执行 some_function

Counter 类的实例 some_function 能够记住自己被调用的次数。

9. 装饰器的实际应用场景(重点)

日志记录 (Logging)

import functools
import logging

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

def log_function_call(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"Calling function '{func.__name__}' with args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        logging.info(f"Function '{func.__name__}' returned {result}")
        return result
    return wrapper

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

add(5, 3)

性能计时 (Timing)

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()
        run_time = end_time - start_time
        print(f"Function '{func.__name__}' took {run_time:.4f} seconds to complete.")
        return result
    return wrapper

@timer
def process_data():
    time.sleep(1)
    print("数据处理完成!")

process_data()

用户认证 (Authentication)

在Web框架(如Flask, Django)中非常常见。

# 这是一个简化的概念性例子
import functools

def login_required(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # 伪代码:假设有一个函数 check_user_logged_in()
        if check_user_logged_in():
            return func(*args, **kwargs)
        else:
            raise PermissionError("用户未登录,禁止访问!")
    return wrapper

# 模拟一个全局登录状态
_user_is_logged_in = False
def check_user_logged_in(): return _user_is_logged_in

@login_required
def view_profile():
    print("这是用户的个人资料页面。")

# 尝试调用
try:
    view_profile()
except PermissionError as e:
    print(e)  # 输出: 用户未登录,禁止访问!

# 模拟用户登录
_user_is_logged_in = True
print("\n用户登录后:")
view_profile() # 输出: 这是用户的个人资料页面。

缓存 (Caching/Memoization)

对于计算成本高的函数,可以将结果缓存起来。Python 3.9+ 自带了 functools.cache。我们也可以自己实现一个简单的版本。

import functools
import time

def memoize(func):
    cache = {}
    @functools.wraps(func)
    def wrapper(*args):
        if args in cache:
            return cache[args]
        result = func(*args)
        cache[args] = result
        return result
    return wrapper

# 使用Python自带的会更高效
# from functools import lru_cache, cache

@memoize # 或者 @functools.cache
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

start = time.time()
print(fibonacci(35))
end = time.time()
print(f"第一次计算耗时: {end - start:.4f}s")

start = time.time()
print(fibonacci(35))
end = time.time()
print(f"第二次(从缓存)计算耗时: {end - start:.4f}s")

输入验证 (Validation)

在函数执行前检查其参数是否符合要求。

import functools

def validate_types(*type_args):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for i, (arg, expected_type) in enumerate(zip(args, type_args)):
                if not isinstance(arg, expected_type):
                    raise TypeError(f"Argument {i+1} must be of type {expected_type.__name__}, not {type(arg).__name__}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@validate_types(int, int)
def multiply(a, b):
    return a * b

print(multiply(5, 10))  # 正常工作
try:
    multiply(5, "10") # 会抛出 TypeError
except TypeError as e:
    print(e)

10. 装饰器栈(多个装饰器)

一个函数可以被多个装饰器修饰。执行顺序是从下到上(最靠近函数的先被应用),然后执行时是从上到下

@timer
@log_function_call
def complex_calculation(x, y):
    time.sleep(0.5)
    return x * y + 2

这等价于:

complex_calculation = timer(log_function_call(complex_calculation))

执行 complex_calculation(3, 4) 时:

  1. timerwrapper 开始执行,记录开始时间。
  2. timer 调用 log_function_callwrapper
  3. log_function_callwrapper 开始执行,打印日志 “Calling function…”。
  4. log_function_call 调用原始的 complex_calculation 函数。
  5. complex_calculation 执行,返回结果 14
  6. log_function_callwrapper 拿到结果,打印日志 “…returned 14”。
  7. timerwrapper 拿到结果,记录结束时间,并打印耗时。

11. 总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文