Python & AI Tutorials Logo
Python 编程

38. 装饰器:为函数添加行为

装饰器(decorator)是 Python 中用于编写整洁、可复用代码的最强大特性之一。它们允许你在不更改函数实际代码的情况下,修改或增强函数的行为。在本章中,我们将在第 23 章对一等函数(first-class function)与闭包(closure)的理解基础上,进一步探索装饰器的工作原理以及如何有效使用它们。

38.1) 装饰器是什么,以及为什么它们很有用

装饰器(decorator) 是一个函数,它以另一个函数作为输入,并返回该函数的一个修改版本。这之所以可行,是因为正如我们在第 23 章学到的,Python 中的函数是“一等对象(first-class object)”——它们可以作为参数传递,并从其他函数中返回。装饰器让你能够在现有函数周围“包裹(wrap)”额外的行为,从而轻松添加诸如日志记录(logging)、计时(timing)、校验(validation)或访问控制(access control)等通用功能,而不会让核心逻辑变得杂乱。

为什么装饰器很重要

想象一下,你的程序里有多个函数,并且你想记录每个函数何时被调用。如果不使用装饰器,你可能会写出这样的代码:

python
# 不使用装饰器 - 重复的日志记录代码
def calculate_total(prices):
    print("Calling calculate_total")
    result = sum(prices)
    print(f"calculate_total returned: {result}")
    return result
 
def find_average(numbers):
    print("Calling find_average")
    result = sum(numbers) / len(numbers)
    print(f"find_average returned: {result}")
    return result
 
def process_order(order_id):
    print("Calling process_order")
    result = f"Order {order_id} processed"
    print(f"process_order returned: {result}")
    return result
 
# 使用这些函数
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60

这种做法有几个问题:

  1. 代码重复:每个函数里都重复了日志记录的代码行
  2. 关注点混杂:日志记录代码与业务逻辑混在一起
  3. 难以维护:如果你想更改日志格式,就必须更新每个函数
  4. 容易遗漏:新函数可能不会包含日志记录

装饰器通过让你把日志行为与核心函数分离来解决这些问题:

python
# 使用装饰器 - 干净且易维护
# (我们会在本章学习如何创建 @log_calls)
 
@log_calls
def calculate_total(prices):
    return sum(prices)
 
@log_calls
def find_average(numbers):
    return sum(numbers) / len(numbers)
 
@log_calls
def process_order(order_id):
    return f"Order {order_id} processed"
 
# 使用这些函数会产生相同的输出
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60

区别在哪里? 日志行为只在 @log_calls 装饰器中定义一次,然后在所有地方复用。你的核心函数保持干净,并专注于其主要目的。

装饰器的常见使用场景

装饰器尤其适用于:

  • 日志记录:记录函数何时被调用以及它们返回什么
  • 计时:测量函数执行所需的时间
  • 校验:检查函数参数是否满足特定要求
  • 缓存:存储代价高昂的函数调用结果以便复用
  • 访问控制:在允许函数执行之前检查权限
  • 重试逻辑:自动重试失败的操作
  • 类型检查:校验参数与返回值类型

关键优势在于:你只需编写一次装饰器,就能用一行代码把它应用到许多函数上。

38.2) 函数作为对象:装饰器的基础

在我们理解装饰器之前,需要回顾并扩展“函数在 Python 中是一等对象(first-class object)”这一概念。正如我们在第 23 章学到的,这意味着函数可以赋值给变量、作为参数传递,以及从其他函数中返回。

函数可以赋值给变量

当你定义一个函数时,Python 会创建一个函数对象,并将其绑定到一个名称上:

python
def greet(name):
    return f"Hello, {name}!"
 
# 函数对象可以赋值给另一个变量
say_hello = greet
 
# 两个名字都指向同一个函数对象
print(greet("Alice"))      # Output: Hello, Alice!
print(say_hello("Bob"))    # Output: Hello, Bob!

名称 greetsay_hello 都指向同一个函数对象。这是装饰器工作方式的基础。

函数可以作为参数传递

你可以像传递其他值一样,把函数传给其他函数:

python
def apply_twice(func, value):
    """对一个值应用某个函数两次。"""
    result = func(value)
    result = func(result)
    return result
 
def add_five(x):
    return x + 5
 
result = apply_twice(add_five, 10)
print(result)  # Output: 20 (10 + 5 = 15, then 15 + 5 = 20)

这里,apply_twice 接收 add_five 函数作为参数,并调用它两次。

函数可以返回其他函数

一个函数可以创建并返回一个新函数:

python
def make_multiplier(factor):
    """创建一个按指定因子进行乘法的函数。"""
    def multiply(x):
        return x * factor
    return multiply
 
times_three = make_multiplier(3)
times_five = make_multiplier(5)
 
print(times_three(10))  # Output: 30
print(times_five(10))   # Output: 50

make_multiplier 函数返回一个新函数,该函数通过闭包(closure)“记住”了 factor 的值(正如我们在第 23 章学到的)。

包裹函数:核心装饰器模式

装饰器模式结合了这些概念:一个函数接收另一个函数作为输入,创建一个增加行为的包装函数(wrapper function),并返回这个包装函数:

python
def simple_wrapper(original_func):
    """为一个函数包裹额外的行为。"""
    def wrapper():
        print("Before calling the function")
        result = original_func()
        print("After calling the function")
        return result
    return wrapper
 
def say_hello():
    print("Hello!")
    return "greeting"
 
# 手动包裹函数
wrapped_hello = simple_wrapper(say_hello)
return_value = wrapped_hello()
# Output:
# Before calling the function
# Hello!
# After calling the function
 
print(f"Returned: {return_value}")
# Output: Returned: greeting

让我们跟踪一下发生了什么:

  1. simple_wrapper 接收 say_hello 作为 original_func
  2. 它创建一个新函数 wrapper,该函数会:
    • 打印 "Before calling the function"
    • 调用 original_func()(也就是 say_hello
    • 打印 "After calling the function"
    • 返回结果
  3. simple_wrapper 返回 wrapper 函数
  4. 当我们调用 wrapped_hello() 时,实际上是在调用 wrapper,而它会在内部调用原始的 say_hello

这就是所有装饰器背后的核心模式。

处理带参数的函数

上面的包装器只适用于不带参数的函数。要让它适用于任何函数,我们需要 *args**kwargs

python
def flexible_wrapper(original_func):
    """包裹一个可以接收任意参数的函数。"""
    def wrapper(*args, **kwargs):
        # *args 捕获位置参数
        # **kwargs 捕获关键字参数
        print("Before calling the function")
        result = original_func(*args, **kwargs)
        print("After calling the function")
        return result
    return wrapper
 
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"
 
# 手动包裹函数
greet = flexible_wrapper(greet)
 
result = greet("Alice")
# Output:
# Before calling the function
# After calling the function
 
print(result)
# Output: Hello, Alice!
 
result = greet("Bob", greeting="Hi")
# Output:
# Before calling the function
# After calling the function
 
print(result)
# Output: Hi, Bob!

*args**kwargs 是如何工作的:

正如我们在第 20 章学到的,*args**kwargs 允许函数接收可变数量的参数:

  • *args 将所有位置参数收集到一个元组(tuple)中
  • **kwargs 将所有关键字参数收集到一个字典(dictionary)中
  • 当我们调用 original_func(*args, **kwargs) 时,会把它们再解包成原函数的参数

这种模式让我们的包装器能够适用于任何函数,而不管它需要多少参数。

迁移到更简洁的语法

这种模式就是装饰器的基础。我们接下来要学习的装饰器语法,只是应用该模式的一种更简洁的方式。我们不再写:

python
greet = flexible_wrapper(greet)

而是使用 @ 语法:

python
@flexible_wrapper
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

两者做的事情完全相同——@ 语法只是语法糖(syntactic sugar),让代码更简洁、更易读。

38.3) @decorator 语法:更简洁的应用方式

function_name = decorator(function_name) 虽然可行,但很啰嗦也容易忘记。Python 提供了 @decorator 语法,用于更干净地应用装饰器。

使用 @ 符号

与手动包裹函数不同,你可以把 @decorator_name 放在函数定义的上一行:

python
def log_call(func):
    """记录函数调用的装饰器。"""
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper
 
@log_call
def calculate_total(prices):
    return sum(prices)
 
@log_call
def find_average(numbers):
    return sum(numbers) / len(numbers)
 
# 使用被装饰的函数
total = calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60
 
print(f"Total: {total}")
# Output: Total: 60
 
average = find_average([10, 20, 30])
# Output:
# Calling find_average
# find_average returned: 20.0
 
print(f"Average: {average}")
# Output: Average: 20.0

@log_call 语法与下面写法完全等价:

python
def calculate_total(prices):
    return sum(prices)
 
calculate_total = log_call(calculate_total)

@ 语法更简洁,并且能让人立刻看出该函数被装饰了。

堆叠多个装饰器

你可以通过堆叠(stacking)把多个装饰器应用到同一个函数上:

python
import time
 
def log_call(func):
    """记录函数调用的装饰器。"""
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper
 
def timer(func):
    """为函数执行计时的装饰器。"""
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start_time
        print(f"{func.__name__} took {elapsed:.4f} seconds")
        return result
    return wrapper
 
@timer
@log_call
def process_data(items):
    total = sum(items)
    return total * 2
 
result = process_data([1, 2, 3, 4, 5])
# Output:
# Calling process_data
# process_data returned: 30
# process_data took 0.0001 seconds
 
print(f"Final result: {result}")
# Output: Final result: 30

当装饰器堆叠时,它们会按照 从下到上(离函数更近的先应用)的顺序被应用:

python
@timer          # 第二个应用(最外层)
@log_call       # 第一个应用(最靠近函数)
def process_data(items):
    pass

这等价于:

python
process_data = timer(log_call(process_data))

应用顺序(从下到上):

  1. @log_call 先包裹原始函数
  2. @timer 再包裹结果(包裹已经被包裹过的函数)

执行顺序(从上到下,从最外层到最内层):

  1. timer 的 wrapper 开始(最外层,先执行)
  2. log_call 的 wrapper 开始(内层 wrapper)
  3. 原始函数执行
  4. log_call 的 wrapper 结束
  5. timer 的 wrapper 结束(最外层,最后结束)

把装饰器想成一层层的包装纸:你是从里往外包的,但在拆开(执行)时,是从外往里拆。

装饰器应用:

原始函数
process_data

步骤 1:@log_call(底部装饰器)

log_call 包裹原始函数

步骤 2:@timer(顶部装饰器)

timer 包裹 log_call wrapper

最终:timer 包裹 log_call 包裹原始函数

执行流程:

调用 process_data

1. timer wrapper 开始
2. log_call wrapper 开始
3. 原始函数执行
4. log_call wrapper 结束
5. timer wrapper 结束

返回结果

38.4) 实用的装饰器示例(日志、计时、校验)

现在让我们探索几个你在真实程序中可能会用到的实用装饰器。这些示例展示了常见模式,并说明装饰器如何解决现实世界的问题。

示例 1:增强的日志装饰器

一个更复杂的日志装饰器,包含时间戳并处理异常:

python
import time
 
def log_with_timestamp(func):
    """记录带时间戳的函数调用的装饰器。"""
    def wrapper(*args, **kwargs):
        timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{timestamp}] Calling {func.__name__}")
        
        try:
            result = func(*args, **kwargs)
            print(f"[{timestamp}] {func.__name__} completed successfully")
            return result
        except Exception as e:
            print(f"[{timestamp}] {func.__name__} raised {type(e).__name__}: {e}")
            raise
    
    return wrapper
 
@log_with_timestamp
def divide(a, b):
    return a / b
 
@log_with_timestamp
def process_user(user_id):
    # 模拟处理过程
    if user_id < 0:
        raise ValueError("User ID must be positive")
    return f"Processed user {user_id}"
 
# 测试成功执行
result = divide(10, 2)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide completed successfully
 
print(f"Result: {result}")
# Output: Result: 5.0
 
# 测试带校验的成功执行
user = process_user(42)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user completed successfully
 
print(user)
# Output: Processed user 42
 
# 测试异常处理
try:
    divide(10, 0)
    # Output:
    # [2025-12-31 10:30:45] Calling divide
    # [2025-12-31 10:30:45] divide raised ZeroDivisionError: division by zero
except ZeroDivisionError:
    print("Handled division by zero")
    # Output: Handled division by zero
 
try:
    process_user(-5)
    # Output:
    # [2025-12-31 10:30:45] Calling process_user
    # [2025-12-31 10:30:45] process_user raised ValueError: User ID must be positive
except ValueError:
    print("Handled invalid user ID")
    # Output: Handled invalid user ID

这个装饰器:

  • 为所有日志消息添加时间戳
  • 记录成功完成与异常两种情况
  • 在记录日志后重新抛出异常(使用不带参数的 raise
  • 使用 try/except 块捕获并记录任何异常

示例 2:性能计时装饰器

一个测量并报告函数执行时间的装饰器:

python
import time
 
def measure_time(func):
    """测量并报告执行时间的装饰器。"""
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        
        # 以合适的方式格式化时间
        if elapsed < 0.001:
            time_str = f"{elapsed * 1000000:.2f} microseconds"
        elif elapsed < 1:
            time_str = f"{elapsed * 1000:.2f} milliseconds"
        else:
            time_str = f"{elapsed:.2f} seconds"
        
        print(f"{func.__name__} executed in {time_str}")
        return result
    
    return wrapper
 
@measure_time
def find_primes(limit):
    """找出所有不超过 limit 的素数。"""
    primes = []
    for num in range(2, limit):
        is_prime = True
        for divisor in range(2, int(num ** 0.5) + 1):
            if num % divisor == 0:
                is_prime = False
                break
        if is_prime:
            primes.append(num)
    return primes
 
@measure_time
def calculate_factorial(n):
    """计算 n 的阶乘。"""
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result
 
# 测试被装饰的函数
primes = find_primes(1000)
# Output: find_primes executed in 15.23 milliseconds
 
print(f"Found {len(primes)} primes")
# Output: Found 168 primes
 
factorial = calculate_factorial(100)
# Output: calculate_factorial executed in 45.67 microseconds
 
print(f"Factorial has {len(str(factorial))} digits")
# Output: Factorial has 158 digits

这个装饰器会根据持续时间自动选择合适的时间单位(微秒、毫秒或秒)来格式化计时结果。

示例 3:输入校验装饰器

一个在执行前校验函数参数的装饰器:

python
def validate_positive(func):
    """确保所有数值参数为正数的装饰器。"""
    def wrapper(*args, **kwargs):
        # 检查位置参数
        for i, arg in enumerate(args):
            if isinstance(arg, (int, float)) and arg <= 0:
                raise ValueError(
                    f"Argument {i} to {func.__name__} must be positive, got {arg}"
                )
        
        # 检查关键字参数
        for key, value in kwargs.items():
            if isinstance(value, (int, float)) and value <= 0:
                raise ValueError(
                    f"Argument '{key}' to {func.__name__} must be positive, got {value}"
                )
        
        return func(*args, **kwargs)
    
    return wrapper
 
@validate_positive
def calculate_area(width, height):
    """计算矩形面积。"""
    return width * height
 
@validate_positive
def calculate_discount(price, discount_percent):
    """计算折扣后的价格。"""
    discount = price * (discount_percent / 100)
    return price - discount
 
# 测试有效输入
area = calculate_area(10, 5)
print(f"Area: {area}")
# Output: Area: 50
 
discounted = calculate_discount(100, 20)
print(f"Discounted price: ${discounted:.2f}")
# Output: Discounted price: $80.00
 
# 测试无效输入
try:
    calculate_area(-5, 10)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: Argument 0 to calculate_area must be positive, got -5
 
try:
    calculate_discount(100, discount_percent=-10)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: Argument 'discount_percent' to calculate_discount must be positive, got -10

这个装饰器:

  • 检查所有数值参数(位置参数与关键字参数)
  • 如果发现任何非正数参数,就抛出描述清晰的错误
  • 提供明确的错误信息,指出哪个参数未通过校验

38.5)(可选)带参数的装饰器

到目前为止,我们的所有装饰器都是简单的函数:接收一个函数作为输入。但如果你想配置装饰器的行为怎么办?例如,你可能想要一个重试装饰器,可以指定尝试次数;或者一个日志装饰器,可以指定日志级别。

带参数的装饰器 需要额外一层函数嵌套。装饰器不再是“接收函数的函数”,而变成“接收参数并返回一个装饰器的函数”。

模式:装饰器工厂

带参数的装饰器实际上是一个 装饰器工厂(decorator factory)——一个创建并返回装饰器的函数。理解这一点的关键是知道 Python 对 @ 符号做了什么。

核心原则:Python 会先求值 @

Python 总是先对 @ 后面的内容进行求值,然后使用其结果来装饰你的函数。

让我们对比一下:

A) 基础装饰器:

基于这个示例:

python
def log_call(func):
    """记录函数调用的装饰器。"""
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper
 
@log_call
def greet(name):
    return f"Hello, {name}!"

Python 所做的事情:

  1. @log_call 求值 → 结果:log_call 本身(函数对象)
  2. 应用到 greetgreet = log_call(greet)

B) 装饰器工厂:

基于这个示例:

python
def repeat(times):
    """第 1 层:工厂 - 接收配置"""
    def decorator(func):
        """第 2 层:装饰器 - 接收要装饰的函数"""
        def wrapper(*args, **kwargs):
            """第 3 层:包装器 - 在被装饰函数被调用时执行"""
            for i in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator
 
@repeat(3)
def greet(name):
    print(f"Hello, {name}!")
 
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

Python 所做的事情:

  1. @repeat(3) 求值 → 结果:调用 repeat(3),并返回一个装饰器函数
  2. 把该装饰器应用到 greetgreet = decorator(greet)

区别在于:@log_call 给你的是函数本身,但 @repeat(3)调用一个函数(repeat),该函数返回一个装饰器。

理解三层结构

装饰器工厂有三个嵌套函数,每一层都有特定角色:

python
def repeat(times):                      # 第 1 层:工厂
    def decorator(func):                # 第 2 层:装饰器  
        def wrapper(*args, **kwargs):   # 第 3 层:包装器
            for i in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

第 1 层 - 工厂(repeat):

  • 接收:配置(times
  • 返回:一个装饰器函数
  • 调用时机:Python 对 @repeat(3) 求值时

第 2 层 - 装饰器(decorator):

  • 接收:要装饰的函数(func
  • 返回:一个包装函数
  • 调用时机:紧接第 1 层之后,作为 @ 语法的一部分

第 3 层 - 包装器(wrapper):

  • 接收:函数被调用时传入的参数(*args, **kwargs
  • 返回:结果
  • 调用时机:每次调用被装饰函数时

逐步执行过程

让我们跟踪 @repeat(3) 发生了什么:

python
# 你写的是:
@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

第 1 步: Python 对 repeat(3) 求值

python
decorator = repeat(3)  # 工厂返回一个装饰器(捕获 times=3)

第 2 步: Python 把装饰器应用到 greet

python
def greet(name):
    print(f"Hello, {name}!")
 
greet = decorator(greet)  # 装饰器返回一个 wrapper(捕获 func=greet)

注意: 此时,greet 现在指向 wrapper 函数。原始的 greet 被捕获在 func 中。

第 3 步: 当你调用 greet("Alice") 时,wrapper 执行

python
greet("Alice")  # 实际是在调用 wrapper("Alice")
# wrapper 使用捕获的 'times' 和 'func'

为什么需要三层?

每一层通过闭包(closure)捕获不同的信息:

python
def repeat(times):                      # 捕获:times
    def decorator(func):                # 捕获:func(并记住 times)
        def wrapper(*args, **kwargs):   # 捕获:times、func,并接收 args
            for i in range(times):      # 使用捕获的 'times'
                result = func(*args, **kwargs)  # 使用捕获的 'func' 与 'args'
            return result
        return wrapper
    return decorator
  • 第 1 层 捕获配置(times
  • 第 2 层 捕获要装饰的函数(func
  • 第 3 层 接收调用时的参数(argskwargs

如果没有这三层,我们就无法拥有一个可配置的装饰器,它既能记住自身设置,也能记住它所装饰的函数。

示例 1:可配置的日志装饰器

下面是一个接受配置的日志装饰器的实用示例:

python
def log_with_prefix(prefix="LOG"):
    """装饰器工厂:创建带自定义前缀的日志装饰器。"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{prefix}] Calling {func.__name__}")
            result = func(*args, **kwargs)
            print(f"[{prefix}] {func.__name__} returned: {result}")
            return result
        return wrapper
    return decorator
 
@log_with_prefix(prefix="INFO")
def calculate_total(prices):
    return sum(prices)
 
@log_with_prefix()  # 使用默认前缀
def get_average(numbers):
    return sum(numbers) / len(numbers)
 
# 测试被装饰的函数
total = calculate_total([10, 20, 30])
# Output:
# [INFO] Calling calculate_total
# [INFO] calculate_total returned: 60
 
print(f"Total: {total}")
# Output: Total: 60
 
average = get_average([10, 20, 30])
# Output:
# [LOG] Calling get_average
# [LOG] get_average returned: 20.0
 
print(f"Average: {average}")
# Output: Average: 20.0

注意:

  • @log_with_prefix(prefix="INFO") 使用自定义前缀
  • @log_with_prefix() 使用默认前缀 "LOG"
  • 即便使用默认值,你也必须写上括号

示例 2:带多个参数的装饰器

下面是一个校验数值范围的装饰器:

python
def validate_range(min_value=None, max_value=None):
    """
    装饰器工厂:校验数值参数是否在某个范围内。
    
    Args:
        min_value: 允许的最小值(包含)
        max_value: 允许的最大值(包含)
    """
    def decorator(func):
        def wrapper(*args, **kwargs):
            # 检查所有数值参数
            all_args = list(args) + list(kwargs.values())
            
            for arg in all_args:
                if isinstance(arg, (int, float)):
                    if min_value is not None and arg < min_value:
                        raise ValueError(
                            f"{func.__name__} received {arg}, "
                            f"which is below minimum {min_value}"
                        )
                    if max_value is not None and arg > max_value:
                        raise ValueError(
                            f"{func.__name__} received {arg}, "
                            f"which is above maximum {max_value}"
                        )
            
            return func(*args, **kwargs)
        return wrapper
    return decorator
 
@validate_range(min_value=0, max_value=100)
def calculate_percentage(value, total):
    """计算百分比。"""
    return (value / total) * 100
 
@validate_range(min_value=0)
def calculate_age(birth_year, current_year):
    """根据出生年份计算年龄。"""
    return current_year - birth_year
 
# 测试有效输入
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%")
# Output: Percentage: 25.0%
 
age = calculate_age(1990, 2025)
print(f"Age: {age}")
# Output: Age: 35
 
# 测试无效输入
try:
    calculate_percentage(150, 100)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: calculate_percentage received 150, which is above maximum 100
 
try:
    calculate_age(-5, 2025)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: calculate_age received -5, which is below minimum 0

何时使用带参数的装饰器

在以下情况使用带参数的装饰器:

  • 你需要配置装饰器的行为
  • 同一个装饰器在不同上下文中应该表现不同
  • 你希望让装饰器更可复用、更灵活

常见示例包括:

  • 可配置尝试次数与延迟的重试装饰器
  • 可配置日志级别或格式的日志装饰器
  • 可配置规则的校验装饰器
  • 可配置缓存大小或过期时间的缓存装饰器
  • 可配置限制值的限流装饰器

关于复杂度的说明

带参数的装饰器会增加一层复杂度。编写它们时:

  • 使用清晰、描述性的参数名
  • 提供合理的默认值
  • 包含解释参数的文档字符串(docstring)
  • 考虑额外的灵活性是否值得增加的复杂度

对于简单场景,不带参数的装饰器通常更清晰,也更容易理解。


装饰器是编写整洁、可维护 Python 代码的强大工具。它们让你能够把横切关注点(cross-cutting concern)(例如日志记录、计时与校验)从核心业务逻辑中分离出来,使代码更易阅读、测试与修改。随着你继续使用 Python 编程,你会发现装饰器在框架与库中被大量使用,并且你也会发现很多机会,去编写自己的装饰器,以优雅的方式解决常见问题。

© 2025. Primesoft Co., Ltd.
support@primesoft.ai