你是否希望通过在Python中使用缓存来加速脚本、Web应用程序,甚至是机器学习系统的执行?那么现在就来学习缓存的基础知识以及如何在Python中实现缓存,以提升性能并减少任务执行时间。
当你的应用程序中存在某个执行缓慢的瓶颈时,而其返回值大多数情况下都是相同的,并且经常被访问,你会怎么做?对于一些人来说,寻找加速的方法是解决之道。然而,这并不是唯一的解决方法,有时甚至可能过于复杂或不可行。
在编程中,有一种解决方案称为缓存。通过缓存,你可以存储经常访问的数据或函数的结果,以便后续访问时在时间和CPU负载方面的成本更低。
Python确实支持缓存,并且在Web应用程序、网络爬虫、机器学习以及其他一些CPU密集型任务中非常有用。在本指南中,我将讨论Python中的缓存以及如何有效地实现它。在此之前,让我们先了解缓存的概念以及何时适合实现缓存。
编程中缓存和缓存的概念
缓存的引入是为了解决由于数据访问或CPU密集型任务而引入的瓶颈,这些瓶颈导致等待时间并影响用户体验。缓存是将数据存储在其源之外的临时存储区域中,并在需要时从该区域访问它的过程。
数据临时存储的区域称为缓存,它有助于加快数据访问时间、减少系统负载并改善用户体验。一个完美的例子是前端使用的静态文件,如流行的Bootstrap。
全球超过 24% 的网站使用 Bootstrap,用户每次发送请求以访问已加载 Bootstrap 的页面时,都必须检索 Bootstrap。借助缓存,浏览器不会一直请求 Bootstrap。相反,Bootstrap 的副本会保存在浏览器中,因此可以节省下载其他内容所需的带宽和时间。这对用户体验有很大帮助。
通常,必须满足 3 个条件,缓存才能发挥作用,实现缓存才有意义。如果不能满足任何一项要求,实现缓存就毫无意义。
- 经常访问但不经常更改的数据
这是设置缓存系统的首要要求。你不会想花费资源来缓存下一分钟会发生变化的数据,因为下一分钟检索这些数据时,返回的数据将是不完整或不正确的。
举例来说,在投注赔率每秒都会变化的投注套利系统中,你的套利查找工具无法缓存奇数值 – 否则,基于此做出的任何决定都是错误的。
同样,存储不经常检索的数据也是没有意义的。就访问频率而言,特定数据点的访问频率越高,就越适合缓存。以我给出的 Bootstrap 示例为例,每次用户访问使用 Bootstrap 的网站上的页面时,都会请求该页面。
这个频率太高了,你不可能一直从原始来源检索它。将其缓存在浏览器中,访问速度不仅更快,而且网站负载也更小。
- 从缓存中检索数据的速度较低
是的,速度是决定是否应该缓存数据点的因素之一,因为它与性能直接相关。如果你有一个 CPU 密集型函数,需要 0.5 秒来计算并返回一个值,但基于特定参数的返回值不会经常更改,并且很多用户在短时间内访问此函数,那么缓存它很有用。
如果返回值被缓存,你无需花费 50 分钟来服务 100 位用户,而是可以累计花费 2 分钟来服务所有 100 位用户。
- 从缓存中检索数据的成本更低
上面提到的第二个要求是缓存系统必须降低时间成本。还有其他成本,例如每次请求的检索成本或整个系统的负载。通常,将数据保存在缓存中应该比从原始源检索数据花费更少。
在大多数情况下,如果正确实施,缓存应该可以帮助你节省资金。这是因为与每个请求都会导致执行 CPU 密集型任务或访问数据库相比,对 CPU 资源和数据库访问的需求会更少。
如何在 Python 中实现缓存
Python 编程语言支持缓存。就缓存而言,有多种选择。在本指南中,我将重点介绍 Python 中的两种缓存方法。第一种方法是使用 Python 字典数据结构,第二种方法是使用 Python 标准库中的 lru_cache 装饰器。让我们从字典方法开始。
使用 Python 字典进行缓存
这里的方法很简单。创建一个 Python 字典,用于存储函数返回的值。下次需要计算该值时,检查该参数是否作为键存在于字典中,如果存在,则表示该参数在过去已被计算过并返回该值。字典数据结构是 O(n) 时间复杂度结构,这意味着无论字典的大小如何,检索项目所需的时间都是相同的。
下面的示例中有一个函数,它将用户代理字符串解析为其各自的详细信息。这是通过使用 API 完成的。我将使用它来演示缓存。
import time
import requests
def user_agent_parser_normal(user_agent_string):
url = 'https://api.apicagent.com'
response = requests.post(url=url, data=user_agent_string, headers={'content-type': 'application/json'})
return response.json()
if __name__ == "__main__":
ua_string = '{"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36"}'
start_time = time.time()
print(user_agent_parser_normal(ua_string))
end_time = time.time()
time_taken = end_time - start_time
print(f'time taken: {time_taken} seconds')
如上所示,Python 中的 request 模块用于访问执行解析的 API 端点。如果运行以下代码,你将得到类似以下结果。

获取此数据大约需要 1.16 秒。如果引入循环以便获取 10 次,则所用时间会翻倍。让我们在实践中看看。
if __name__ == "__main__":
ua_string = '{"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36"}'
start_time = time.time()
for i in range(10):
print(user_agent_parser_normal(ua_string))
end_time = time.time()
time_taken = end_time - start_time
print(f'time taken: {time_taken} seconds')
如下所示,上面这个所花费的时间为 12.15。
![]()
速度会因你所在地区的互联网速度而异。但即使是 0.4 秒的值,乘以请求数也是很长的。如果你按请求付费,财务成本也是令人担心的事情。
我们可以创建一个缓存系统,用于存储用户代理的结果并检索它们,如果使用相同的用户代理,则不必发送另一个 API 请求。下面是如何将上面的代码转换为使用字典数据结构的缓存。
import time
import requests
def memoize_with_dict(func):
cache = {}
# Inner wrapper function to store the data in the cache
def wrapper(*args):
if args in cache:
return cache[args]
else:
result = func(*args)
cache[args] = result
return result
return wrapper
@memoize_with_dict
def user_agent_parser_normal(user_agent_string):
url = 'https://api.apicagent.com'
response = requests.post(url=url, data=user_agent_string, headers={'content-type': 'application/json'})
return response.json()
if __name__ == "__main__":
ua_string = '{"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36"}'
start_time = time.time()
for i in range(10):
print(user_agent_parser_normal(ua_string))
end_time = time.time()
time_taken = end_time - start_time
print(f'time taken: {time_taken} seconds')
在上面的代码中,创建了一个装饰器函数来跟踪 Python 字典中的结果。当你发送请求时,它会检查缓存字典中是否存在该值。如果是,则返回该值。否则,它会继续调用该函数,检索值,并将它们存储在缓存字典中以供以后访问。如果你运行上面的代码,即使经过 10 次迭代,结果如下。
![]()
如你所见,所花费的时间与发送单个请求时相同。这是因为它不会发送其他请求,因为用户代理字符串结果也已被获取和缓存。虽然这在某种程度上有效,但它不仅简单,而且还有需要改进的复杂用法。
在 Python 中使用 LRU 缓存装饰器
最近最少使用 (LRU) 缓存方法会保留缓存数据列表,如果达到缓存的最大大小,则会删除缓存中最近最少使用的项目。你可以使用 functools 中的 lru_cache 装饰器在 Python 中实现此功能。
它以 maxsize 和 typed 作为参数。max size 用于定义要缓存的数据条目的最大大小,超过该大小后,最近最少使用的项目将从缓存中删除。让我们看看如何实现上述相同的代码。
from functools import lru_cache
import time
import requests
@lru_cache(maxsize=100)
def user_agent_parser_normal(user_agent_string):
url = 'https://api.apicagent.com'
response = requests.post(url=url, data=user_agent_string, headers={'content-type': 'application/json'})
return response.json()
if __name__ == "__main__":
ua_string = '{"ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36"}'
start_time = time.time()
for i in range(10):
print(user_agent_parser_normal(ua_string))
end_time = time.time()
time_taken = end_time - start_time
print(f'time taken: {time_taken} seconds')
如上所示,我们传递了 100 作为最大大小。如果不设置,缓存可能会变得太大,并出现意外行为。上面的结果与我们自己的实现几乎相同,而且似乎更快。在这里,我无法判断性能略有提升是由于网络速度波动还是 lru_cache 表现更好——但我怀疑 lru_cache 表现更好。
结 论
作为总结,你需要知道,上述的缓存方法是基础的,仅仅向你解释了缓存的概念,可以在小型应用程序和脚本中实现。对于Web应用程序和其他大型应用程序,你需要使用专门的缓存系统,这些系统具备专门的缓存内存,用于高效存储和检索缓存数据。
然而,基本理念仍然相同——存储经常访问但不经常变化的数据,以提高性能并降低成本。

