为啥 Python 的装饰器语法比普通的包装器语法提供更快的记忆代码?
Posted
技术标签:
【中文标题】为啥 Python 的装饰器语法比普通的包装器语法提供更快的记忆代码?【英文标题】:Why does Python's decorator syntax give faster memoization code than plain wrapper syntax?为什么 Python 的装饰器语法比普通的包装器语法提供更快的记忆代码? 【发布时间】:2014-06-30 05:10:18 【问题描述】:我一直在尝试理解 Real World OCaml (RWO) 第 8 章中关于记忆的部分。我没有得到它,所以我决定将 OCaml 代码翻译成 Python。这个练习非常有用,因为 (1) 我终于明白了 RWO 在说什么,并且 (2) 我写了一些更快的 Python 代码,似乎可以工作。然而,在编写 Python 代码时,我尝试了两种不同的方式来执行记忆:一次使用对包装函数的普通调用,一次使用 Python 的装饰器语法。
我以三种不同的方式记忆斐波那契函数,并测量了在我的 2.9 GHz 英特尔酷睿 i7 MacBook Pro、8 GB RAM 和 OS 10.9.2、运行 Python 2.7 上两次计算第 32 个斐波那契数所需的时间。这给了我一个令人惊讶的结果:
-
完全没有记忆:2 秒
常规记忆:1 秒
使用纯语法的 RWO 式相互递归记忆:2 秒
使用装饰器语法的 RWO 式相互递归记忆:0.0001 秒
我读过的所有内容都说装饰器语法实际上只是语法糖:
memoFib = memoize(Fib)
那么为什么 #4 比 #3 快这么多?
from time import time
def unfib(n):
'''Unmemoized Fibonacci'''
if n <= 1: return 1
else: return unfib(n-1) + unfib(n-2)
def memoize(f):
'''A simple memoization function'''
hash =
def memo_f(x):
if not hash.has_key(x):
res = f(x)
hash[x] = res
return hash[x]
return memo_f
# Simple approach to memorizing Fibonacci (#2 from the list above)
memoFib1 = memoize(unfib)
# Simple approach to timing functions
def timeit(its,f,arg):
zero = time()
for i in range(its):
res = f(arg)
one = time()
print res, one - zero
# A non-recursive form of Fibonacci
# that expects the remainder of the
# function to be passed as an argument.
# Together, they make a pair of mutually
# recursive functions. Real World Ocaml
# refers to this as 'tying the recursive
# knot' (Ch. 8, Imperative programming).
def fib_norec(fib,x):
if x <= 1: return 1
else: return fib(x-1) + fib(x-2)
def memo_rec(f_norec):
'''A memoizing version of make_rec,
but using the plain wrapper
syntax of the memoize function'''
def f(x):
return f_norec(f,x)
return memoize(f)
# #3 from list above: memoized using plain call to wrapping function
memoFib2 = memo_rec(fib_norec)
def memo_rec2(f_norec):
'''A second memoizing version of
make_rec (from RWO), but using
the decorator syntax'''
@memoize
def f(x):
return f_norec(f,x)
return f
# #4 from list above, memoized using decorator syntax
memoFib3 = memo_rec2(fib_norec)
print 'no memo\t\t\t\t\t',
timeit(2,unfib,32)
print 'basic memo\t\t\t\t',
timeit(2,memoFib1,32)
print 'mutually recursive memo, plain wrapper syntax',
timeit(2,memoFib2,32)
print 'mutually recursive memo, decorator syntax',
timeit(2,memoFib3,32)
【问题讨论】:
【参考方案1】:def f(x):
return f_norec(f,x)
return memoize(f)
这里返回的函数是memoized
产生的函数,但是本地名称 f
仍然是指上面sn-p中定义的非记忆函数,因此没有递归调用受益于记忆。调用图是这样的:
<memoized f>
f
f_noref
f
f_norec
...
另一方面,
@memoize
def f(x):
return f_norec(f,x)
return f
本地名称f
指的是记忆化的函数,所以你会得到一个这样的调用图:
<memoized f>
f
f_norec
<memoized f>
f
f_norec
<memoized f>
...
(看起来像更多的调用,确实如此。我只显示了每个级别的两个递归调用中的第一个,所以你看不到记忆如何缩短第二个。)
如果您手动编写装饰器语法实际上去糖 (f = memoize(f); return f
),您会看到相同的行为和性能。
【讨论】:
以上是关于为啥 Python 的装饰器语法比普通的包装器语法提供更快的记忆代码?的主要内容,如果未能解决你的问题,请参考以下文章