返回文章列表

Python装饰器原理与实战

装饰器(Decorator)是Python中最优雅和强大的特性之一。它本质上是一个函数或类,用于在不修改原函数代码的前提下,为函数添加额外功能。本文将从函数作为一等公民和闭包概念讲起,逐步深入到各类装饰器的原理与实战应用。

一、函数是一等公民

在Python中,函数是一等公民(First-Class Citizen),意味着函数可以像普通对象一样被赋值给变量、作为参数传递、作为返回值返回,以及存储在数据结构中。

# 函数可以赋值给变量
def greet(name):
    return f"你好,{name}!"

say_hello = greet  # 将函数赋值给变量
print(say_hello("平平"))  # 你好,平平!

# 函数可以作为参数传递
def call_function(func, *args):
    """调用传入的函数"""
    return func(*args)

result = call_function(greet, "世界")
print(result)  # 你好,世界!

# 函数可以作为返回值
def get_multiplier(factor):
    """返回一个乘法函数"""
    def multiplier(x):
        return x * factor
    return multiplier

double = get_multiplier(2)
triple = get_multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15

# 函数可以存储在列表中
operations = [lambda x: x + 1, lambda x: x * 2, lambda x: x ** 2]
for op in operations:
    print(op(3))  # 4, 6, 9

二、闭包概念

闭包(Closure)是一个函数对象,它记住了定义时所在作用域中的变量,即使这个作用域已经结束。闭包是理解装饰器的关键基础。

def make_counter():
    """创建一个计数器闭包"""
    count = 0  # 自由变量

    def counter():
        nonlocal count  # 声明使用外层函数的变量
        count += 1
        return count

    return counter

# 每个调用创建独立的闭包
counter_a = make_counter()
counter_b = make_counter()

print(counter_a())  # 1
print(counter_a())  # 2
print(counter_a())  # 3
print(counter_b())  # 1(独立计数)
print(counter_b())  # 2

# 闭包的实际应用:带缓存的斐波那契
def fibonacci():
    cache = {0: 0, 1: 1}

    def fib(n):
        if n not in cache:
            cache[n] = fib(n - 1) + fib(n - 2)
        return cache[n]

    return fib

fib = fibonacci()
print(fib(10))   # 55
print(fib(50))   # 12586269025
print(fib(100))  # 354224848179261915075

三、装饰器基础语法

装饰器的核心思想是:接受一个函数作为参数,返回一个增强版的新函数。使用 @语法糖可以简洁地将装饰器应用到目标函数上。

# 装饰器的基本结构
def my_decorator(func):
    def wrapper(*args, **kwargs):
        # 在函数执行前添加功能
        print(f"[调用前] 正在执行 {func.__name__}")
        # 执行原函数
        result = func(*args, **kwargs)
        # 在函数执行后添加功能
        print(f"[调用后] {func.__name__} 执行完毕")
        return result
    return wrapper

# 使用装饰器(等价于 say_hi = my_decorator(say_hi))
@my_decorator
def say_hi(name):
    print(f"Hi, {name}!")
    return f"已问候 {name}"

print(say_hi("平平"))
# 输出:
# [调用前] 正在执行 say_hi
# Hi, 平平!
# [调用后] say_hi 执行完毕
# 已问候 平平

四、使用functools.wraps保留元信息

上面的装饰器有一个问题:被装饰后的函数丢失了原始函数的元信息(如 __name__、__doc__ 等)。functools.wraps 可以解决这个问题。

from functools import wraps

def log_decorator(func):
    """带日志功能的装饰器"""
    @wraps(func)  # 保留原函数的元信息
    def wrapper(*args, **kwargs):
        print(f"[LOG] 调用函数:{func.__name__}")
        print(f"[LOG] 参数:args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"[LOG] 返回值:{result}")
        return result
    return wrapper

@log_decorator
def add(a, b):
    """计算两个数的和"""
    return a + b

@log_decorator
def multiply(a, b):
    """计算两个数的乘积"""
    return a * b

print(add.__name__)   # add(保留了原函数名)
print(add.__doc__)    # 计算两个数的和(保留了文档字符串)
print("---")
result = add(3, 5)
print(f"结果:{result}")
print("---")
result2 = multiply(4, 6)
print(f"结果:{result2}")

五、带参数的装饰器

有时装饰器自身也需要参数。这需要三层嵌套:外层函数接收装饰器参数,中间层接收被装饰函数,最内层是实际的包装函数。

from functools import wraps
import time

def retry(max_attempts=3, delay=1, exceptions=(Exception,)):
    """重试装饰器:在遇到异常时自动重试"""

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts:
                        print(f"[重试] {func.__name__} 失败 {max_attempts} 次,放弃")
                        raise
                    print(f"[重试] 第{attempt}次失败:{e},{delay}秒后重试...")
                    time.sleep(delay)
        return wrapper
    return decorator

# 使用带参数的装饰器
@retry(max_attempts=3, delay=0.5, exceptions=(ValueError, TypeError))
def parse_int(text):
    """将文本解析为整数"""
    return int(text)

# 模拟不稳定的网络请求
import random

@retry(max_attempts=5, delay=1)
def fetch_data(url):
    """模拟网络请求(随机失败)"""
    if random.random() < 0.7:
        raise ConnectionError("网络连接失败")
    return {"status": "ok", "data": "Hello from server"}

print(parse_int("42"))      # 42
print(parse_int("abc"))     # 会重试3次后报错
# print(fetch_data("http://example.com"))  # 可能会重试多次

六、类装饰器

除了函数装饰器,还可以使用类来实现装饰器。类装饰器通常需要实现 __init__ 和 __call__ 方法。

import time
from functools import wraps

class CountCalls:
    """统计函数调用次数的类装饰器"""

    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"[统计] {self.func.__name__} 第 {self.count} 次调用")
        return self.func(*args, **kwargs)

    def reset(self):
        """重置计数"""
        self.count = 0

@CountCalls
def calculate_pi(terms):
    """用莱布尼茨公式计算圆周率"""
    pi = 0
    for i in range(terms):
        pi += (-1) ** i / (2 * i + 1)
    return pi * 4

print(f"Pi(100) = {calculate_pi(100):.6f}")
print(f"Pi(1000) = {calculate_pi(1000):.10f}")
print(f"Pi(10000) = {calculate_pi(10000):.12f}")


class Timer:
    """计时装饰器(类实现)"""

    def __init__(self, precision=4):
        self.precision = precision

    def __call__(self, func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            start = time.perf_counter()
            result = func(*args, **kwargs)
            elapsed = time.perf_counter() - start
            print(f"[计时] {func.__name__} 耗时 {elapsed:.{self.precision}f}秒")
            return result
        return wrapper

@Timer(precision=6)
def slow_sum(n):
    """慢速求和(故意使用循环)"""
    total = 0
    for i in range(n):
        total += i
    return total

print(f"结果:{slow_sum(1000000)}")

七、常用内置装饰器

Python自带了几个非常实用的装饰器:@property、@staticmethod、@classmethod、@functools.lru_cache 等。

# ========== @property:将方法变为属性访问 ==========
class Rectangle:
    def __init__(self, width, height):
        self._width = width
        self._height = height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        if value <= 0:
            raise ValueError("宽度必须大于0")
        self._width = value

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value <= 0:
            raise ValueError("高度必须大于0")
        self._height = value

    @property
    def area(self):
        """只读属性:面积"""
        return self._width * self._height

    @property
    def perimeter(self):
        """只读属性:周长"""
        return 2 * (self._width + self._height)

rect = Rectangle(10, 5)
print(f"面积:{rect.area}")
print(f"周长:{rect.perimeter}")
rect.width = 20
print(f"新面积:{rect.area}")

# ========== @functools.lru_cache:缓存装饰器 ==========
from functools import lru_cache

@lru_cache(maxsize=128)
def expensive_computation(x):
    """模拟耗时计算"""
    print(f"  正在计算 {x}...")
    import time
    time.sleep(0.1)
    return x ** 2 + x + 1

print("第一次调用 expensive_computation(5):")
print(expensive_computation(5))   # 会实际计算

print("第二次调用 expensive_computation(5):")
print(expensive_computation(5))   # 从缓存返回,不计算

print("缓存信息:", expensive_computation.cache_info())

# ========== @functools.singledispatch:单分派泛型函数 ==========
from functools import singledispatch

@singledispatch
def process_data(data):
    """默认处理"""
    print(f"默认处理:{data}")

@process_data.register
def _(data: int):
    print(f"整数处理:{data} 的平方是 {data ** 2}")

@process_data.register
def _(data: str):
    print(f"字符串处理:'{data}' 长度为 {len(data)}")

@process_data.register(list)
def _(data):
    print(f"列表处理:包含 {len(data)} 个元素")

process_data(42)
process_data("hello")
process_data([1, 2, 3])
process_data(3.14)  # 走默认

八、实战案例:计时器装饰器

import time
import functools

def timer(func):
    """精确计时装饰器"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            result = func(*args, **kwargs)
            return result
        finally:
            elapsed = time.perf_counter() - start
            if elapsed < 1:
                unit = "毫秒"
                elapsed *= 1000
            else:
                unit = "秒"
            print(f"[{func.__name__}] 执行耗时: {elapsed:.2f}{unit}")
    return wrapper

@timer
def bubble_sort(arr):
    """冒泡排序"""
    n = len(arr)
    for i in range(n):
        for j in range(0, n - i - 1):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

import random
data = random.sample(range(10000), 1000)
bubble_sort(data.copy())

九、实战案例:日志装饰器

import functools
import logging
from datetime import datetime

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s"
)

def log_execution(level=logging.INFO):
    """日志装饰器,可指定日志级别"""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            logger = logging.getLogger(func.__module__)
            func_name = func.__name__

            logger.log(level, f">>> 开始执行 {func_name}")
            logger.log(level, f"    参数: args={args}, kwargs={kwargs}")

            try:
                result = func(*args, **kwargs)
                logger.log(level, f"<<< {func_name} 执行成功")
                logger.log(level, f"    返回值类型: {type(result).__name__}")
                return result
            except Exception as e:
                logger.error(f"!!! {func_name} 执行异常: {e}")
                raise
        return wrapper
    return decorator

@log_execution(level=logging.INFO)
def divide(a, b):
    """除法运算"""
    return a / b

print(divide(10, 3))
print("---")
try:
    divide(10, 0)
except ZeroDivisionError:
    print("捕获到除零错误")

十、实战案例:缓存装饰器

import functools
import hashlib
import pickle
import time

def timed_cache(max_age_seconds=60):
    """带过期时间的缓存装饰器"""

    def decorator(func):
        cache = {}

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # 生成缓存键
            key_parts = [func.__name__, str(args), str(sorted(kwargs.items()))]
            key = hashlib.md5("|".join(key_parts).encode()).hexdigest()

            # 检查缓存
            if key in cache:
                result, timestamp = cache[key]
                if time.time() - timestamp < max_age_seconds:
                    print(f"[缓存命中] {func.__name__} 缓存有效")
                    return result
                else:
                    print(f"[缓存过期] {func.__name__} 重新计算")
                    del cache[key]

            # 执行函数并缓存
            result = func(*args, **kwargs)
            cache[key] = (result, time.time())
            print(f"[缓存写入] {func.__name__} 结果已缓存")
            return result

        def clear_cache():
            """清除缓存"""
            cache.clear()
            print(f"[缓存清除] {func.__name__} 缓存已清空")

        wrapper.clear_cache = clear_cache
        return wrapper

    return decorator

@timed_cache(max_age_seconds=10)
def get_weather(city):
    """模拟获取天气数据(带缓存)"""
    print(f"  [模拟请求] 正在获取 {city} 的天气...")
    time.sleep(0.5)  # 模拟网络延迟
    weather_data = {
        "北京": {"temp": 28, "weather": "晴"},
        "上海": {"temp": 32, "weather": "多云"},
        "广州": {"temp": 35, "weather": "雷阵雨"},
    }
    return weather_data.get(city, {"temp": 25, "weather": "未知"})

print(get_weather("北京"))
print(get_weather("北京"))  # 缓存命中
time.sleep(11)
print(get_weather("北京"))  # 缓存过期,重新请求
get_weather.clear_cache()

十一、装饰器叠加与执行顺序

多个装饰器可以叠加使用,执行顺序是从下往上(靠近函数的先执行)。

import functools

def bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return f"{func(*args, **kwargs)}"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return f"{func(*args, **kwargs)}"
    return wrapper

def underline(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return f"{func(*args, **kwargs)}"
    return wrapper

# 装饰器从下往上执行
# 等价于: text = bold(italic(underline(text)))
@bold
@italic
@underline
def text():
    return "Hello, Decorator!"

print(text())
# 输出: Hello, Decorator!

装饰器是Python中非常强大的工具,理解其原理后,你可以在日志记录、性能监控、权限验证、缓存、重试机制等众多场景中灵活运用。建议在实际项目中多加练习,逐步掌握各种装饰器的使用技巧。

动手挑战

学到这里,不妨动手试一试以下练习,巩固你的理解:

  1. 基础练习:回顾本文核心概念,用自己的话总结关键知识点。
  2. 进阶实践:将文中的示例代码运行一遍,尝试修改参数观察变化。
  3. 拓展思考:想一想这个技术/方法还能应用在哪些场景中?

小贴士:遇到问题时,先独立思考,再查阅资料,最后请教他人——这是成长最快的学习方式。

赞赏支持

本文更新于 2026-08-22,环境 Python 3.12