Python 是一种通用编程语言,可用于各种类型的项目,尤其是机器学习、后端开发和数据分析。在提高性能的同时,编写简洁易懂的代码也至关重要,尤其是当使用 Python 这样的动态解释型编程语言时。
为了提高性能,我们可以使用一些 Python 内置模块。在本文中,你将学习到编写更少、更高效代码的 functools 技巧。
Functools 是一个内置的 Python 模块,用于高阶函数和可调用对象上的操作。
cache 装饰器
如果您想提高一个函数的性能,而该函数返回一个值,您需要多次调用相同的参数,那么使用缓存装饰器就非常实用了。
基本上,缓存装饰器会记住参数和返回值。如果使用缓存装饰器多次调用具有相同参数的函数,则无需运行函数本身,因为我们已经知道了基于参数的输出值。
from functools import cache
import time
@cache
def add(x, y):
"""Adds two numbers together."""
print("running")
time.sleep(2)
return x + y
# First two calls
print("NOT CACHED", add(2, 5))
print("CACHED", add(2, 5), "\n")
# Last two calls
print("NOT CACHED", add(6, 5))
print("NOT CACHED", add(3, 5))
在前两次调用中,add 函数运行了一次而不是两次,因为在第一次运行时,cache 恢复了参数和返回值对,而且因为 add 函数的第二次调用与第一次调用的参数相同,所以第二次调用没有运行函数,直接返回值。
但在最后两次调用中,add 函数运行了两次,因为这两次调用的参数与前两次调用的参数不同。不过需要注意,如果需要在函数中使用随机值,请不要使用缓存装饰器。
cached_property
将一个类方法转换为特征属性,一次性计算该特征属性的值,然后将其缓存为实例生命周期内的普通属性。 类似于 property()
但增加了缓存功能。
cached_property
装饰器仅在执行查找且不存在同名属性时才会运行。 当运行时,cached_property
会写入同名的属性。 后续的属性读取和写入操作会优先于 cached_property
方法,其行为就像普通的属性一样。
对于在其他情况下实际不可变的高计算资源消耗的实例特征属性来说该函数非常有用。可以理解为它是缓存和属性装饰器的组合。如果你熟悉 C# 和 Java 等其他编程语言,那么属性就类似于 getters
。
class Numbers:
def __init__(self, *numbers):
self.numbers = numbers
@cached_property
def total(self):
print("Calculating total...")
return sum(self.numbers)
@cached_property
def average(self):
print("Calculating avarage...")
return self.total/len(self.numbers)
m = Numbers(100, 90, 95)
print(m.average)
print(m.average)
print(m.average)
我们在 total
和 average
方法中使用 print
函数来检查方法本身是否正在运行,正如你所看到的,它们只运行一次,因为我们已经知道了返回值。
LRU 缓存
一个为函数提供缓存功能的装饰器,缓存 maxsize 组传入参数,在下次以相同参数调用时直接返回上一次的结果。用以节约高开销或 I/O 函数的调用时间。该缓存是线程安全的,因此被包装的函数可在多线程中使用。 这意味着下层的数据结构将在并发更新期间保持一致性。
简单地说,我们提供了一个数字,这个数字就是你想要调用的最近调用次数的最大值。当我们使用递归时,lru_cache 可能会根据问题的不同而有所帮助。
@lru_cache(maxsize=2)
def fib(n):
if n < 2:
return n
print(f"calculating fib {n}")
return fib(n-1) + fib(n-2)
s = time.time()
[fib(x) for x in range(30)]
print(time.time()-s)
print(fib.cache_info())
请尝试使用和不使用 lru_cache 装饰器的代码示例。使用 lru_cache 装饰器时,运行时间约为 9 秒,但在我的机器上,使用 lru_cache 装饰器时,运行时间约为 0.07 秒。
一般来说,LRU 缓存只应在你需要重复使用先前计算的值时使用。 因此,缓存有附带影响的函数、每次调用都需要创建不同的可变对象的函数(如生成器和异步函数)或不纯的函数如 time()
或 random()
等是没有意义的。