Python func_dict用于备忘录;还有其他有用技巧吗?
一个Python函数对象有一个叫做 func_dict
的属性字典,这个字典可以在函数外部看到,并且是可以修改的,但在调用函数时并不会被改变。(我从我昨天问的一个问题的回答中学到这个的,感谢大家!)我在阅读一段代码时(在http://pythonprogramming.jottit.com/functional_programming上),这段代码是用来缓存计算斐波那契数的,我想,“为什么不利用 func_dict
属性来做缓存呢?”结果真的成功了(见下面;输出结果在代码的最后)。这有点像是有一个类的属性可以用,但初始化的代码是在对象外部(在这个例子中,不是类而是一个函数)。
我想知道使用这个属性还可以做哪些类似(或不同)的技巧呢?
def fib(n):
if n in fib.cache:
print "found fib.cache[%d] = %d: " %(n, fib.cache[n])
return fib.cache[n]
else:
print "fib.cache[%d] = fib(%d) + fib(%d)" % (n, n-1, n-2)
fib.cache[n] = fib(n-1) + fib(n-2)
print "modified fib.cache: ", fib.cache
return fib.cache[n]
fib.cache = {0:0, 1:1}
for x in range(7):
print "==================>", x
print fib( x)
"""
==================> 0
found fib.cache[0] = 0:
0
==================> 1
found fib.cache[1] = 1:
1
==================> 2
fib.cache[2] = fib(1) + fib(0)
found fib.cache[1] = 1:
found fib.cache[0] = 0:
modified fib.cache: {0: 0, 1: 1, 2: 1}
1
==================> 3
fib.cache[3] = fib(2) + fib(1)
found fib.cache[2] = 1:
found fib.cache[1] = 1:
modified fib.cache: {0: 0, 1: 1, 2: 1, 3: 2}
2
==================> 4
fib.cache[4] = fib(3) + fib(2)
found fib.cache[3] = 2:
found fib.cache[2] = 1:
modified fib.cache: {0: 0, 1: 1, 2: 1, 3: 2, 4: 3}
3
==================> 5
fib.cache[5] = fib(4) + fib(3)
found fib.cache[4] = 3:
found fib.cache[3] = 2:
modified fib.cache: {0: 0, 1: 1, 2: 1, 3: 2, 4: 3, 5: 5}
5
==================> 6
fib.cache[6] = fib(5) + fib(4)
found fib.cache[5] = 5:
found fib.cache[4] = 3:
modified fib.cache: {0: 0, 1: 1, 2: 1, 3: 2, 4: 3, 5: 5, 6: 8}
8
"""
2 个回答
我一直在用这种方法来做记忆化处理。这个方法利用了一个特点,就是你可以“调用”一个不是函数的对象,只要这个对象定义了它的__call__
方法。对于behindthefall的方法和Alex Martelli的方法,我之前都没有想到,不知道为什么。我猜它的性能大概和behindthefall的方法差不多,因为它们的工作原理差不多。可能会稍微慢一点。不过正如链接页面所说,“fib()的定义现在是‘显而易见’的,没有缓存代码让算法变得复杂”,这点还挺不错的。
要小心哦:这个 fib.cache
的技巧只有在 fib
确实是当前执行环境中相关函数的名字时才有效(比如,当你像现在这样给它加装饰器时,你必须把缓存的初始值赋给装饰器的包装函数,而不是给被装饰的函数。如果之后再给它加装饰器,那就会出问题)。
跟标准的记忆化方法相比,这个方法有点脆弱:
def fib(n, _memo={0:1, 1:1}):
if n in _memo:
return _memo[n]
else:
_memo[n] = fib(n-1) + fib(n-2)
return _memo[n]
或者说装饰器的版本。标准方法的速度也更快(虽然差别不大)——把这两个方法放在 mem.py 文件里,分别命名为 fib1(使用 .cache 技巧的那个,没有打印和装饰)和 fib2(我的版本),我们可以看到...:
$ python -mtimeit -s'import mem' 'mem.fib1(20)'
1000000 loops, best of 3: 0.754 usec per loop
$ python -mtimeit -s'import mem' 'mem.fib2(20)'
1000000 loops, best of 3: 0.507 usec per loop
所以标准方法的版本节省了大约 33% 的时间,但这是在几乎所有调用都能命中缓存的情况下(缓存是在第一次调用那一百万次循环后填充的)——而 fib2 在缓存未命中的情况下速度优势就小了,因为它的速度来自于访问 _memo
(一个局部变量)比访问 fib.cache
(一个全局名字 fib 及其属性 cache)要快,而在缓存命中的情况下,缓存访问占主导地位(没有其他东西;-)),但是在缓存未命中的情况下,两个函数的额外工作量是一样的。
总之,我不是想打击你的热情,但当你发现一些新的酷点子时,一定要和“老办法”进行比较,看看在“稳健性”和性能方面哪个更好(如果你在做缓存,想必你是很在意性能的;-)。