在参数保持不变的情况下,尽量减少昂贵的函数调用次数(python)
在参数保持不变的情况下,尽量减少昂贵的函数调用次数(python)
假设有一个函数costly_function_a(x)
,满足以下条件:
- 在执行时间上非常耗时;
- 在给定相同的
x
时返回相同的输出; - 除了返回一个输出之外,它不执行“额外的任务”。
在这种情况下,我们可以将函数连续两次调用相同的x
的结果存储在一个临时变量中,然后使用该变量进行计算。
现在假设有一些函数(在下面的示例中为f(x)
,g(x)
和h(x)
),这些函数调用costly_function_a(x)
,其中一些函数可能彼此调用(在下面的示例中,g(x)
和h(x)
都调用f(x)
)。在这种情况下,使用上述简单方法仍会重复调用具有相同x
的costly_function_a(x)
(参见下面的OkayVersion
)。我找到了一种减少调用次数的方法,但它很“丑陋”(参见下面的FastVersion
)。有没有更好的方法来做到这一点?
#代表非常慢的代码的虚拟函数。 #目标是尽可能少地调用这些耗时函数。 def costly_function_a(x): print("costly_function_a已被调用。") return x #虚拟操作。 def costly_function_b(x): print("costly_function_b已被调用。") return 5.*x #虚拟操作。 #最简单(但最慢)的实现。 class SlowVersion: def __init__(self,a,b): self.a = a self.b = b def f(self,x): #虚拟操作。 return self.a(x) + 2.*self.a(x)**2 def g(self,x): #虚拟操作。 return self.f(x) + 0.7*self.a(x) + .1*x def h(self,x): #虚拟操作。 return self.f(x) + 0.5*self.a(x) + self.b(x) + 3.*self.b(x)**2 #与SlowVersion等效,但较少调用耗时函数。 class OkayVersion: def __init__(self,a,b): self.a = a self.b = b def f(self,x): #与SlowVersion.f(x)相同的结果 a_at_x = self.a(x) return a_at_x + 2.*a_at_x**2 def g(self,x): #与SlowVersion.g(x)相同的结果 return self.f(x) + 0.7*self.a(x) + .1*x def h(self,x): #与SlowVersion.h(x)相同的结果 a_at_x = self.a(x) b_at_x = self.b(x) return self.f(x) + 0.5*a_at_x + b_at_x + 3.*b_at_x**2 #与SlowVersion等效,但更少调用耗时函数。 #这是最简单的方法吗?我知道这段代码非常冗余。可以通过定义一些工厂函数来简化它... class FastVersion: def __init__(self,a,b): self.a = a self.b = b def f(self, x, _at_x=None): #与SlowVersion.f(x)相同的结果 if _at_x is None: _at_x = dict() if 'a' not in _at_x: _at_x['a'] = self.a(x) return _at_x['a'] + 2.*_at_x['a']**2 def g(self, x, _at_x=None): #与SlowVersion.g(x)相同的结果 if _at_x is None: _at_x = dict() if 'a' not in _at_x: _at_x['a'] = self.a(x) return self.f(x,_at_x) + 0.7*_at_x['a'] + .1*x def h(self,x,_at_x=None): #与SlowVersion.h(x)相同的结果 if _at_x is None: _at_x = dict() if 'a' not in _at_x: _at_x['a'] = self.a(x) if 'b' not in _at_x: _at_x['b'] = self.b(x) return self.f(x,_at_x) + 0.5*_at_x['a'] + _at_x['b'] + 3.*_at_x['b']**2 if __name__ == '__main__': slow = SlowVersion(costly_function_a,costly_function_b) print("使用慢速版本。") print("f(2.) = " + str(slow.f(2.))) print("g(2.) = " + str(slow.g(2.))) print("h(2.) = " + str(slow.h(2.)) + "\n") okay = OkayVersion(costly_function_a,costly_function_b) print("使用正常版本。") print("f(2.) = " + str(okay.f(2.))) print("g(2.) = " + str(okay.g(2.))) print("h(2.) = " + str(okay.h(2.)) + "\n") fast = FastVersion(costly_function_a,costly_function_b) print("随便使用快速版本。") print("f(2.) = " + str(fast.f(2.))) print("g(2.) = " + str(fast.g(2.))) print("h(2.) = " + str(fast.h(2.)) + "\n") print("最佳使用快速版本。") _at_x = dict() print("f(2.) = " + str(fast.f(2.,_at_x))) print("g(2.) = " + str(fast.g(2.,_at_x))) print("h(2.) = " + str(fast.h(2.,_at_x))) #当然,在使用不同的x之前,必须“清理”_at_x...
此代码的输出为:
使用慢速版本。 costly_function_a已被调用。 costly_function_a已被调用。 f(2.) = 10.0 costly_function_a已被调用。 costly_function_a已被调用。 costly_function_a已被调用。 g(2.) = 11.6 costly_function_a已被调用。 costly_function_a已被调用。 costly_function_a已被调用。 costly_function_b已被调用。 costly_function_b已被调用。 h(2.) = 321.0 使用正常版本。 costly_function_a已被调用。 f(2.) = 10.0 costly_function_a已被调用。 costly_function_a已被调用。 g(2.) = 11.6 costly_function_a已被调用。 costly_function_b已被调用。 costly_function_a已被调用。 h(2.) = 321.0 随便使用快速版本。 costly_function_a已被调用。 f(2.) = 10.0 costly_function_a已被调用。 g(2.) = 11.6 costly_function_a已被调用。 costly_function_b已被调用。 h(2.) = 321.0 最佳使用快速版本。 costly_function_a已被调用。 f(2.) = 10.0 g(2.) = 11.6 costly_function_b已被调用。 h(2.) = 321.0
请注意,我不想“存储”过去使用的所有x
的结果(因为这需要太多的内存)。此外,我不想有一个返回形如(f,g,h)
的元组的函数,因为有些情况下我只需要f
(所以无需评估costly_function_b
)。
在上述内容中,问题的出现原因是作者需要一个缓存功能,以便在函数参数相同时,尽可能减少昂贵的函数调用。然而,作者认为使用现有的装饰器_cache(1)
有些过度,因此决定自己编写一个装饰器来解决这个问题。
为了解决这个问题,作者编写了名为last_cache
的装饰器函数。这个装饰器函数将被应用在需要缓存最后一个返回值的函数上。装饰器函数内部定义了一些变量用于缓存最后的参数和返回值。当被装饰的函数被调用时,装饰器函数会检查是否是第一次调用或者参数是否与上一次调用相同。如果是第一次调用或者参数不同,则执行原函数并更新缓存的参数和返回值;如果参数相同,则直接返回缓存的返回值。
这样,通过使用last_cache
装饰器,可以在函数参数相同时避免重复调用昂贵的函数,从而提高执行效率。
需要注意的是,作者提到自己是Python的新手,所以这段代码可能并不完美。
在Python中,当参数保持不变时,减少昂贵函数调用的次数是一个常见问题。为了平衡调用成本和内存需求,可以使用LRU缓存,只缓存最近使用的项。对于每个唯一的x值,缓存多个返回值,当缓存已满时,最近不使用的缓存结果将被丢弃。
Python 3.2引入了functools库中的.lru_cache()装饰器实现LRU缓存。可以使用以下代码导入并使用该装饰器:
from functools import lru_cache @lru_cache(16) # 缓存16个不同的x返回值 def costly_function_a(x): print("costly_function_a has been called.") return x #Dummy operation. @lru_cache(32) # 缓存32个不同的x返回值 def costly_function_b(x): print("costly_function_b has been called.") return 5.*x #Dummy operation.
如果使用的Python版本较早,可以使用早期版本的LRU缓存的后移版本,或者选择其他可在PyPI上找到的可处理LRU缓存的库。
如果只需要缓存最近的一个项,可以创建自己的装饰器。以下是一个示例:
from functools import wraps def cache_most_recent(func): cache = [None, None] @wraps(func) def wrapper(*args, **kw): if (args, kw) == cache[0]: return cache[1] cache[0] = args, kw cache[1] = func(*args, **kw) return cache[1] return wrapper @cache_most_recent def costly_function_a(x): print("costly_function_a has been called.") return x #Dummy operation. @cache_most_recent def costly_function_b(x): print("costly_function_b has been called.") return 5.*x #Dummy operation.
这个简单的装饰器比功能更丰富的functools.lru_cache()的开销更小。
需要注意的是,缓存也会带来性能损失。例如,在将缓存应用于re模块时,性能实际上有所下降,因为之前使用的简单模式缓存更快。因此,Python开发者撤销了这个更改。
总之,使用LRU缓存可以减少昂贵函数调用的次数,提高程序的性能。通过缓存最近使用的项,可以在保持参数不变的情况下限制内存使用,平衡调用成本和内存需求。