Python中的闭包函数

8

我一直在尝试学习Python,虽然我对在Python中使用闭包很感兴趣,但是我一直无法让一些代码正常工作:

def memoize(fn):
    def get(key):
        return (False,)

    def vset(key, value):
        global get
        oldget = get
        def newget(ky):
            if key==ky: return (True, value)
            return oldget(ky)
        get = newget

    def mfun(*args):
        cache = get(args)
        if (cache[0]): return cache[1]

        val = apply(fn, args)
        vset(args, val)
        return val

    return mfun

def fib(x):
    if x<2: return x
    return fib(x-1)+fib(x-2)

def fibm(x):
    if x<2: return x
    return fibm(x-1)+fibm(x-2)

fibm = memoize(fibm)

基本上,这个代码使用闭包来维护函数的记忆化状态。我知道可能有很多更快、更易读、更符合Python风格的实现方式;然而,我的目标是要完全理解Python中闭包的工作原理以及它们与Lisp的区别,所以我不对替代方案感兴趣,只关心为什么我的代码无法运行并且我能否做些什么来修复它。
我遇到的问题是当我尝试使用fibm时,Python坚持认为get未定义:
Python 2.6.1 (r261:67515, Feb  1 2009, 11:39:55) 
[GCC 4.0.1 (Apple Inc. build 5488)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import memoize
>>> memoize.fibm(35)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "memoize.py", line 14, in mfun
    cache = get(args)
NameError: global name 'get' is not defined
>>> 

由于我刚接触Python,我不知道是否做错了什么,或者这只是语言的限制。我希望是前者。 :-)

6个回答

8
问题在于你的作用域,而不是你的闭包。如果你愿意阅读一些重量级的内容,那么你可以尝试 http://www.python.org/dev/peps/pep-3104/
如果不是这种情况,这里有一个简单的解释:
问题出在语句global get上。 global指的是最外层的作用域,由于没有任何全局函数get,它就会抛出异常。
你需要的是一个访问封闭作用域变量的访问限定符,而不是全局作用域。
在Python 3.0中,正如我测试过的那样,nonlocal关键字正是你所需要的,取代了global
nonlocal get
...

在Python 2.x中,我只需删除global getoldget的引用,就可以正常工作。

1
有一个解决方法,而且相当简单,除非它与您的纯洁概念相冲突——只需将get()作为全局函数即可。话虽如此,我必须说,您的记忆化方法对我来说似乎太复杂了 :) - sykora
它如何正常工作?只需删除“global get”和“oldget = get”行,它就不会使用保存的值。 - Roger Pate
将newget中的"oldget"更改为"get"。 - Roger Pate
我认为他所说的“正常工作”是指它返回了正确的答案(尽管没有记忆化)。 - Kyle Cronin
我花了一段时间才弄清楚它没有被记忆化,但是没错,那就是我的意思。我正在尝试使用记忆化使其正常工作,但是我不理解代码足够多,无法找出它在做什么。 - sykora
显示剩余3条评论

8
def memoize(fn):
  get = [lambda key: (False, None)]

  def vset(args):
    value = fn(*args)
    oldget = get[0]
    def newget(key):
      if args == key:
        return (True, value)
      return oldget(key)
    get[0] = newget
    return value

  def mfun(*args):
    found, value = get[0](args)
    if found:
      return value
    return vset(args)

  return mfun

CALLS = 0

def fib(x):
  global CALLS
  CALLS += 1
  if x<2: return x
  return fib(x-1)+fib(x-2)

@memoize
def fibm(x):
  global CALLS
  CALLS += 1
  if x<2: return x
  return fibm(x-1)+fibm(x-2)

CALLS = 0
print "fib(35) is", fib(35), "and took", CALLS, "calls"
CALLS = 0
print "fibm(35) is", fibm(35), "and took", CALLS, "calls"

输出结果为:

fib(35) is 9227465 and took 29860703 calls
fibm(35) is 9227465 and took 36 calls

与其他答案类似,但这个答案是有效的。:)
问题代码的重要改变是分配给一个非全局非本地的(get)对象; 然而,在试图保持你*cough*broken*cough*闭包使用的同时,我也做了一些改进。通常,缓存是一个字典,而不是一个链表的闭包。

虽然不是理想的解决方案,但这个变通方法应该足够好用了。感谢您对代码进行了一些优化。 - Kyle Cronin
在我看来,使用这段代码除了学习Python如何处理闭包之外,其他任何用途都是注定要失败的。其他更改只是为了把你引向正确的方向。 - Roger Pate
那是主要目标。如果我用Python编码,我会使用字典,但我很欣赏在一个语句中设置多个值和使用fn(*args)而不是apply等小细节。 - Kyle Cronin

1

你想要在每个函数的开头(除了get本身)放置global get

def get是对名称get的赋值,因此在此之前希望声明get为全局变量。

在mfun和vset中放置global get可以使它们正常工作。我无法指出这样做的作用域规则,但它起作用了;-)

您的conses也非常lispy…:)


嗯...在我的机器上,在get和vset中放置全局get并没有起作用。将其添加到memoize本身的开头可以解决问题,但这会导致每次调用memoize时都使用相同的get函数(这是不可取的)。 - Kyle Cronin
啊,好的。提醒自己:先喝咖啡,清醒过来,然后再回答;-) - Jonas Kölker

1

Get 不是全局的,而是局部的,所以 global 声明会失败。

如果你移除了 global,它仍然会失败,因为你不能给捕获的变量名赋值。为了解决这个问题,你可以使用一个对象作为闭包捕获的变量,然后只需更改该对象的属性:

class Memo(object):
    pass

def memoize(fn):
    def defaultget(key):
        return (False,)

    memo = Memo()
    memo.get = defaultget

    def vset(key, value):
        oldget = memo.get
        def newget(ky):
            if key==ky: return (True, value)
            return oldget(ky)
        memo.get = newget

    def mfun(*args):
        cache = memo.get(args)
        if cache[0]: return cache[1]

        val = apply(fn, args)
        vset(args, val)
        return val

    return mfun

这样你就不需要为捕获的变量名称赋值,但仍然可以得到你想要的结果。


0

我认为最好的方法是:

class Memoized(object):
    def __init__(self,func):
        self.cache = {}
        self.func = func
    def __call__(self,*args):
        if args in self.cache: return cache[args]
        else:
            self.cache[args] = self.func(*args)
            return self.cache[args]

0

可能是因为你想要获取全局变量,但它并不是全局的? 顺便说一下,apply已经被弃用了,请使用fn(*args)代替。

def memoize(fn):
    def get(key):
        return (False,)

    def vset(key, value):
        def newget(ky):
            if key==ky: return (True, value)
            return get(ky)
        get = newget

    def mfun(*args):
        cache = get(args)
        if (cache[0]): return cache[1]

        val = fn(*args)
        vset(args, val)
        return val

    return mfun

def fib(x):
    if x<2: return x
    return fib(x-1)+fib(x-2)

def fibm(x):
    if x<2: return x
    return fibm(x-1)+fibm(x-2)

fibm = memoize(fibm)

谢谢你提供关于apply的提示。然而,运行上述修改后的代码可以得到正确的答案,但它根本没有使用记忆化。(您可以通过运行类似于fibm(35)的东西来验证这一点,如果进行了记忆化,则应该几乎瞬间完成。) - Kyle Cronin
x = y 将本地变量 x 赋值为 y,除非该函数中有 global 语句。 - Markus Jarderot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接