Python实战进阶:如何用装饰器实现函数级缓存,让重复计算快到飞起?

Python实战进阶:如何用装饰器实现函数级缓存,让重复计算快到飞起?

在 Python 编程中,有些技术看起来只是语法糖,真正理解之后却会发现,它们其实是工程效率的放大器。装饰器就是其中之一。

很多初学者第一次看到 @timer@login_required@lru_cache 时,会觉得它像魔法:为什么只是在函数上面加了一行代码,函数就突然拥有了计时、鉴权、缓存等能力?而当你开始写 Web 接口、数据分析脚本、自动化任务、机器学习特征处理流程时,你会越来越频繁地遇到一个问题:同样的函数、同样的参数,被反复调用,结果也完全一样,为什么还要一次次重新计算?

这就是函数级缓存发挥价值的地方。

本文将以“如何用装饰器实现函数级缓存”为主线,从 Python 装饰器基础讲起,逐步实现一个可用、可扩展、可控制过期时间的缓存装饰器。无论你正在学习 Python教程,还是已经在做 Python实战 项目,都可以把这篇文章当作一次从基础语法到工程思维的完整练习。


一、为什么函数级缓存值得掌握?

Python 自诞生以来,就以简洁、优雅、可读性强著称。它从最初的脚本语言,逐步成长为 Web 开发、自动化运维、数据科学、人工智能、物联网、测试平台等领域的重要工具。今天,Python 不只是“能快速写脚本”的语言,更是连接系统、数据、模型和业务的“胶水语言”。

在实际开发中,我们经常会遇到这些场景:

  • 查询某个用户信息,多次请求结果相同;
  • 计算复杂数学结果,输入相同则输出相同;
  • 读取配置文件,内容不会频繁变化;
  • 调用第三方接口,短时间内无需重复请求;
  • 数据清洗流程中,同一批参数会反复触发相同逻辑。

如果每次都重新执行函数,不仅浪费 CPU、内存和网络资源,还会拖慢用户体验。函数级缓存的核心思想很简单:

当函数使用相同参数调用时,直接返回上一次计算结果,而不是重新执行函数体。

这听起来朴素,却是很多高性能系统的基础优化手段。


二、先从一个慢函数开始

假设我们有一个模拟耗时查询的函数:

import time

def get_user_profile(user_id):
    print(f"正在查询用户 {user_id} 的信息...")
    time.sleep(2)
    return {
        "user_id": user_id,
        "name": "Alex",
        "level": "vip"
    }

print(get_user_profile(1001))
print(get_user_profile(1001))
print(get_user_profile(1001))

每次调用都会等待 2 秒。即使参数都是 1001,函数仍然重复执行。

这就是缓存可以优化的地方。我们希望第一次调用时正常执行,之后如果参数相同,就直接返回缓存结果。


三、装饰器基础:给函数“套一层外壳”

在 Python 中,函数是一等公民。函数可以被赋值、传参,也可以作为另一个函数的返回值。

装饰器的本质是:

函数 = 装饰器(函数)

例如:

def simple_decorator(func):
    def wrapper(*args, **kwargs):
        print("函数执行前")
        result = func(*args, **kwargs)
        print("函数执行后")
        return result
    return wrapper

@simple_decorator
def say_hello(name):
    print(f"你好,{name}")

say_hello("Python")

这段代码等价于:

say_hello = simple_decorator(say_hello)

所以,装饰器并不神秘。它只是把原函数包起来,在执行前后增加额外逻辑。函数级缓存正适合用装饰器实现,因为缓存逻辑通常不属于业务逻辑本身,却又需要包裹函数调用过程。


四、实现第一个函数级缓存装饰器

我们先实现一个最简单版本,只支持位置参数,并假设参数都是可哈希对象。

from functools import wraps

def cache(func):
    cached_data = {}

    @wraps(func)
    def wrapper(*args):
        if args in cached_data:
            print("命中缓存")
            return cached_data[args]

        print("未命中缓存,开始计算")
        result = func(*args)
        cached_data[args] = result
        return result

    return wrapper

使用它:

import time

@cache
def slow_add(a, b):
    time.sleep(1)
    return a + b

print(slow_add(3, 5))
print(slow_add(3, 5))
print(slow_add(4, 6))
print(slow_add(3, 5))

输出大致如下:

未命中缓存,开始计算
8
命中缓存
8
未命中缓存,开始计算
10
命中缓存
8

这个版本已经具备函数级缓存的基本能力。

它的工作流程可以表示为:

调用函数
   ↓
根据参数生成缓存 key
   ↓
检查 key 是否在缓存字典中
   ↓
存在:直接返回缓存结果
   ↓
不存在:执行原函数
   ↓
保存结果到缓存
   ↓
返回结果

五、为什么要使用 functools.wraps

在装饰器中,@wraps(func) 是非常重要的最佳实践。

如果不使用 wraps,被装饰函数的名称、文档字符串等元信息会丢失:

def bad_cache(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_cache
def hello():
    """这是 hello 函数"""
    return "hello"

print(hello.__name__)
print(hello.__doc__)

输出可能是:

wrapper
None

在调试、日志、单元测试、接口文档生成时,这会带来麻烦。因此,生产代码中写装饰器时,建议默认加上:

from functools import wraps

六、支持关键字参数的缓存装饰器

真实项目中,函数不仅有位置参数,也可能有关键字参数:

def search(keyword, page=1, size=10):
    ...

如果只用 args 作为缓存 key,就无法正确处理 kwargs。我们可以把 args 和排序后的 kwargs 一起作为 key。

from functools import wraps

def cache(func):
    cached_data = {}

    @wraps(func)
    def wrapper(*args, **kwargs):
        key = (args, tuple(sorted(kwargs.items())))

        if key in cached_data:
            print("命中缓存")
            return cached_data[key]

        print("未命中缓存,开始计算")
        result = func(*args, **kwargs)
        cached_data[key] = result
        return result

    return wrapper

测试一下:

@cache
def build_url(host, path="/", https=True):
    protocol = "https" if https else "http"
    return f"{protocol}://{host}{path}"

print(build_url("example.com", path="/users", https=True))
print(build_url("example.com", https=True, path="/users"))

因为我们对 kwargs.items() 做了排序,所以即使关键字参数顺序不同,也能生成相同的缓存 key。


七、缓存 key 的难点:可变对象怎么办?

Python 中,字典的 key 必须是可哈希对象。整数、字符串、元组通常可以作为 key,但列表、字典、集合不可以。

例如:

@cache
def total(numbers):
    return sum(numbers)

print(total([1, 2, 3]))

这会报错,因为列表不可哈希。

为了提高通用性,我们可以写一个函数,把常见可变对象转换成可哈希结构。

def make_hashable(value):
    if isinstance(value, dict):
        return tuple(sorted((k, make_hashable(v)) for k, v in value.items()))
    if isinstance(value, list):
        return tuple(make_hashable(item) for item in value)
    if isinstance(value, set):
        return tuple(sorted(make_hashable(item) for item in value))
    return value

然后改造缓存装饰器:

from functools import wraps

def cache(func):
    cached_data = {}

    @wraps(func)
    def wrapper(*args, **kwargs):
        hashable_args = make_hashable(args)
        hashable_kwargs = make_hashable(kwargs)
        key = (hashable_args, hashable_kwargs)

        if key in cached_data:
            print("命中缓存")
            return cached_data[key]

        print("未命中缓存,开始计算")
        result = func(*args, **kwargs)
        cached_data[key] = result
        return result

    return wrapper

这样就可以支持列表参数了:

@cache
def total(numbers):
    print("正在计算总和...")
    return sum(numbers)

print(total([1, 2, 3]))
print(total([1, 2, 3]))

不过要注意,如果参数中包含自定义对象、文件句柄、数据库连接等复杂对象,缓存 key 的设计就需要更谨慎。


八、带过期时间的缓存:TTL Cache

普通缓存有一个问题:结果可能会过期。

例如用户信息、商品价格、天气数据、接口返回值,都可能在一段时间后发生变化。此时我们需要 TTL,也就是 Time To Live,缓存存活时间。

下面实现一个支持过期时间的缓存装饰器:

import time
from functools import wraps

def ttl_cache(seconds=60):
    def decorator(func):
        cached_data = {}

        @wraps(func)
        def wrapper(*args, **kwargs):
            key = (
                make_hashable(args),
                make_hashable(kwargs)
            )

            now = time.time()

            if key in cached_data:
                cached_time, cached_value = cached_data[key]
                if now - cached_time < seconds:
                    print("命中缓存")
                    return cached_value

                print("缓存已过期")

            print("未命中缓存,开始计算")
            result = func(*args, **kwargs)
            cached_data[key] = (now, result)
            return result

        return wrapper
    return decorator

使用方式:

@ttl_cache(seconds=3)
def get_config(name):
    print(f"正在读取配置:{name}")
    return {
        "name": name,
        "value": time.time()
    }

print(get_config("database"))
print(get_config("database"))

time.sleep(4)

print(get_config("database"))

第一次调用会执行函数,第二次调用命中缓存。等待 4 秒后再次调用,缓存过期,函数重新执行。

这类 TTL 缓存非常适合短时间内稳定、但长期可能变化的数据。


九、增加缓存清理能力

在工程项目中,缓存不能只进不出。否则随着调用参数越来越多,缓存字典可能无限增长。

我们可以给包装函数挂载一个 cache_clear 方法。

from functools import wraps

def cache_with_clear(func):
    cached_data = {}

    @wraps(func)
    def wrapper(*args, **kwargs):
        key = (
            make_hashable(args),
            make_hashable(kwargs)
        )

        if key in cached_data:
            print("命中缓存")
            return cached_data[key]

        result = func(*args, **kwargs)
        cached_data[key] = result
        return result

    def cache_clear():
        cached_data.clear()
        print("缓存已清空")

    wrapper.cache_clear = cache_clear
    return wrapper

使用:

@cache_with_clear
def multiply(a, b):
    print("正在计算...")
    return a * b

print(multiply(2, 5))
print(multiply(2, 5))

multiply.cache_clear()

print(multiply(2, 5))

这个技巧在实际项目中很有用。例如后台配置变更后,可以主动清除缓存,让系统读取最新结果。


十、限制缓存容量:避免内存失控

如果函数参数组合很多,缓存会不断膨胀。我们可以实现一个简单的最大容量控制。当缓存超过指定数量时,删除最早进入的缓存项。

Python 3.7 之后,普通字典保持插入顺序,因此可以用 next(iter(cache)) 找到最早的 key。

from functools import wraps

def limited_cache(maxsize=128):
    def decorator(func):
        cached_data = {}

        @wraps(func)
        def wrapper(*args, **kwargs):
            key = (
                make_hashable(args),
                make_hashable(kwargs)
            )

            if key in cached_data:
                print("命中缓存")
                return cached_data[key]

            result = func(*args, **kwargs)

            if len(cached_data) >= maxsize:
                oldest_key = next(iter(cached_data))
                cached_data.pop(oldest_key)

            cached_data[key] = result
            return result

        return wrapper
    return decorator

使用:

@limited_cache(maxsize=2)
def square(n):
    print(f"计算 {n} 的平方")
    return n * n

print(square(2))
print(square(3))
print(square(2))
print(square(4))
print(square(3))

这个版本是简单的 FIFO,也就是先进先出。生产场景中,更常见的是 LRU,也就是最近最少使用淘汰策略。


十一、标准库方案:functools.lru_cache

虽然自己实现缓存装饰器很适合学习原理,但在生产项目中,如果需求符合标准库能力,优先使用 Python 自带的 functools.lru_cache

from functools import lru_cache

@lru_cache(maxsize=128)
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

print(fibonacci(40))
print(fibonacci.cache_info())

如果不加缓存,递归计算斐波那契数列会产生大量重复计算。加上 lru_cache 后,相同参数的结果只会计算一次。

cache_info() 可以查看缓存状态:

CacheInfo(hits=..., misses=..., maxsize=128, currsize=...)

其中:

  • hits 表示命中次数;
  • misses 表示未命中次数;
  • maxsize 表示最大缓存容量;
  • currsize 表示当前缓存数量。

也可以清理缓存:

fibonacci.cache_clear()

这就是 Python最佳实践 中很重要的一条:先理解原理,再优先使用成熟工具。


十二、实践案例:缓存第三方 API 查询结果

假设我们正在开发一个简单的天气查询服务。真实项目中可能会调用第三方接口,这里用 time.sleep 模拟网络请求。

需求如下:

  1. 同一个城市 10 秒内重复查询,直接返回缓存;
  2. 超过 10 秒后重新请求;
  3. 支持手动清理缓存;
  4. 保留函数元信息,便于调试和测试。

代码如下:

import time
from functools import wraps

def make_hashable(value):
    if isinstance(value, dict):
        return tuple(sorted((k, make_hashable(v)) for k, v in value.items()))
    if isinstance(value, list):
        return tuple(make_hashable(item) for item in value)
    if isinstance(value, set):
        return tuple(sorted(make_hashable(item) for item in value))
    if isinstance(value, tuple):
        return tuple(make_hashable(item) for item in value)
    return value

def ttl_cache(seconds=60):
    def decorator(func):
        cached_data = {}

        @wraps(func)
        def wrapper(*args, **kwargs):
            key = (
                make_hashable(args),
                make_hashable(kwargs)
            )

            now = time.time()

            if key in cached_data:
                cached_time, cached_value = cached_data[key]
                if now - cached_time < seconds:
                    print(f"[CACHE HIT] {func.__name__}")
                    return cached_value

            print(f"[CACHE MISS] {func.__name__}")
            result = func(*args, **kwargs)
            cached_data[key] = (now, result)
            return result

        def cache_clear():
            cached_data.clear()

        def cache_size():
            return len(cached_data)

        wrapper.cache_clear = cache_clear
        wrapper.cache_size = cache_size

        return wrapper
    return decorator

@ttl_cache(seconds=10)
def get_weather(city):
    print(f"正在请求天气接口:{city}")
    time.sleep(2)
    return {
        "city": city,
        "temperature": "26°C",
        "weather": "晴"
    }

print(get_weather("上海"))
print(get_weather("上海"))
print("当前缓存数量:", get_weather.cache_size())

get_weather.cache_clear()
print(get_weather("上海"))

这个案例虽然简单,却已经具备很多工程思维:

业务函数
   ↓
缓存装饰器
   ↓
参数生成 key
   ↓
检查缓存和过期时间
   ↓
命中则直接返回
   ↓
未命中则执行请求
   ↓
保存结果

在 Web 应用中,这类缓存可以用于商品详情、用户权限、系统配置、热门文章列表等场景。在数据分析中,它可以用于缓存中间计算结果。在 AI 项目中,它也可以用于缓存特征提取、模型推理或数据预处理结果。


十三、缓存不是银弹:什么时候不该缓存?

缓存可以提升性能,但并不是所有函数都适合缓存。

适合缓存的函数通常有几个特点:

  1. 输入相同,输出稳定;
  2. 函数执行成本较高;
  3. 数据允许短时间不更新;
  4. 函数没有明显副作用。

不适合缓存的函数包括:

def get_current_time():
    return time.time()

这个函数每次都应该返回当前时间,不适合缓存。

再比如:

def create_order(user_id, product_id):
    # 写入数据库
    # 扣库存
    # 生成订单
    ...

这类函数有明显副作用,绝不能简单缓存。否则可能导致订单没有真正创建,却直接返回了旧结果。

判断一个函数能不能缓存,可以问自己一句话:

如果我跳过函数执行,直接返回上一次结果,会不会破坏业务正确性?

如果答案是“可能会”,那就不要轻易缓存。


十四、并发环境下的注意事项

在单线程脚本里,字典缓存通常没问题。但在多线程 Web 服务中,同时读写同一个缓存字典可能出现竞争问题。

简单做法是加锁:

import threading
from functools import wraps

def thread_safe_cache(func):
    cached_data = {}
    lock = threading.Lock()

    @wraps(func)
    def wrapper(*args, **kwargs):
        key = (
            make_hashable(args),
            make_hashable(kwargs)
        )

        with lock:
            if key in cached_data:
                return cached_data[key]

        result = func(*args, **kwargs)

        with lock:
            cached_data[key] = result

        return result

    return wrapper

不过,这个版本仍然可能出现多个线程同时计算同一个 key 的情况。对于高并发生产系统,可以进一步做细粒度锁,或者使用 Redis、Memcached 这类外部缓存系统。

函数级本地缓存适合轻量场景;分布式系统中的共享缓存,则应交给专业缓存中间件。


十五、性能对比:缓存前后差异有多大?

我们用一个简单实验感受缓存效果。

import time
from functools import lru_cache

def slow_fib(n):
    if n <= 1:
        return n
    return slow_fib(n - 1) + slow_fib(n - 2)

@lru_cache(maxsize=None)
def fast_fib(n):
    if n <= 1:
        return n
    return fast_fib(n - 1) + fast_fib(n - 2)

start = time.time()
print(fast_fib(35))
print("缓存版本耗时:", time.time() - start)

递归斐波那契是缓存教学中的经典案例,因为它存在大量重复子问题。未加缓存时,函数会重复计算同一个 fib(n);加缓存后,每个 n 基本只计算一次。

对比如下:

无缓存:
fib(35) 会触发大量重复递归调用

有缓存:
fib(0) 到 fib(35) 每个结果只需计算一次

这就是缓存的威力:它不是让单次计算变快,而是避免重复计算。


十六、和 Web 框架结合的思路

在 Flask、Django、FastAPI 等 Web 框架中,缓存装饰器非常常见。

例如伪代码:

@app.get("/products/{product_id}")
@ttl_cache(seconds=30)
def get_product(product_id: int):
    return query_product_from_database(product_id)

不过要注意,真实 Web 服务中,如果函数返回的是用户相关数据,缓存 key 必须包含用户身份、权限、语言、地区等上下文信息。否则可能出现严重的数据串用问题。

例如,下面这种缓存是危险的:

@ttl_cache(seconds=60)
def get_current_user_profile():
    ...

因为函数没有显式参数,所有用户都可能命中同一个缓存结果。

更安全的方式是:

@ttl_cache(seconds=60)
def get_user_profile(user_id):
    ...

缓存优化的前提永远是业务正确性。


十七、最佳实践总结

在 Python实战 项目中使用函数级缓存时,可以遵循这些建议:

1. 优先缓存纯函数

纯函数指输入相同、输出相同、没有副作用的函数。数学计算、格式转换、配置读取、静态数据查询都比较适合。

2. 缓存 key 要设计清楚

不要只看函数名,要看参数是否完整表达了结果依赖。用户身份、语言环境、权限范围、分页参数、过滤条件都可能是 key 的一部分。

3. 设置容量或过期时间

无限缓存很危险。缓存会占用内存,也可能返回旧数据。生产系统中通常要有 maxsizettl 或清理机制。

4. 使用 wraps

保留函数元信息,方便调试、测试和文档生成。

5. 避免缓存有副作用的函数

写数据库、发消息、扣库存、创建订单、发送邮件,这些操作通常不适合用函数级缓存跳过执行。

6. 标准库能解决时,优先用标准库

functools.lru_cache 简洁、稳定、性能好。自己实现装饰器更适合学习、定制特殊逻辑,或处理 TTL、外部缓存等扩展需求。


十八、进阶方向:从本地缓存到分布式缓存

本文实现的是函数级本地缓存,缓存数据保存在当前 Python 进程内。它的优点是简单、快速、无额外依赖;缺点也很明显:

进程重启后缓存消失
多进程之间无法共享缓存
缓存容量受限于本机内存
不适合大规模分布式系统

如果你的应用部署在多个实例上,比如多个 Gunicorn worker、多个容器、多个服务器,那么本地缓存就无法保证全局一致。此时可以考虑:

  • Redis:适合高性能键值缓存;
  • Memcached:适合简单高速缓存;
  • 数据库缓存表:适合需要持久化的缓存;
  • CDN:适合静态资源和接口边缘缓存。

不过,无论缓存放在哪里,思想都是一样的:

先查缓存
缓存命中直接返回
缓存未命中执行计算或查询
把结果写入缓存
返回结果

掌握函数级缓存,是理解更复杂缓存架构的第一步。


十九、附录:推荐学习资料

如果你希望进一步系统学习,可以关注这些方向:

  • Python 官方文档中的 functools 模块;
  • PEP8 代码风格规范;
  • 《Python编程:从入门到实践》;
  • 《流畅的Python》;
  • 《Effective Python》;
  • Flask、Django、FastAPI 中的装饰器实践;
  • Redis 缓存设计与缓存失效策略;
  • Pytest 单元测试与性能测试工具。

这些内容能帮助你从“会写 Python”逐渐走向“写出可靠、清晰、可维护的 Python”。


二十、总结:缓存是一种克制的性能优化

装饰器让函数级缓存变得优雅。我们不需要把缓存逻辑散落在每个业务函数里,只需要用一个 @cache@ttl_cache,就能把重复计算、接口查询、配置读取等场景优化起来。

本文从最简单的缓存字典开始,逐步实现了:

  • 基础缓存装饰器;
  • 支持 argskwargs 的缓存;
  • 支持可变参数转换的缓存 key;
  • 支持 TTL 过期时间的缓存;
  • 支持手动清理的缓存;
  • 限制容量的缓存;
  • 标准库 lru_cache 的使用;
  • Web、API、数据处理中的实践建议。

但也请记住:缓存不是炫技,而是一种克制的性能优化。它的目标不是让代码看起来高级,而是让系统在正确的前提下更快、更稳、更节省资源。

真正优秀的 Python 开发者,不只是知道怎么写 @cache,更知道什么时候该缓存,什么时候不该缓存,缓存多久,缓存什么,以及缓存失效后如何保证业务正确。

最后留两个问题给你:

你在日常 Python 开发中,遇到过哪些重复计算或重复查询的问题?

如果让你给当前项目加一个函数级缓存,你最想优化哪个函数?

欢迎在评论区分享你的案例。也许你的一个真实问题,正是另一个开发者正在寻找的答案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铭渊老黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值