【问题标题】:Closures in PythonPython 中的闭包
【发布时间】:2010-10-05 01:39:19
【问题描述】:

我一直在努力学习 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)

基本上,这应该做的是使用闭包来维持函数的记忆状态。我意识到可能有许多更快、更容易阅读并且通常更“Pythonic”的方法来实现它;但是,我的目标是准确了解闭包在 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 新手,我不知道是我做错了什么,还是这只是语言的限制。我希望是前者。 :-)

【问题讨论】:

    标签: python closures lexical-scope


    【解决方案1】:

    问题在于您的范围,而不是您的闭包。如果你想阅读一些繁重的东西,那么你可以试试http://www.python.org/dev/peps/pep-3104/

    如果不是这样,下面是简单的解释:

    问题出在声明 global get 中。 global 指的是最外层的作用域,由于没有任何全局函数get,所以它会抛出。

    您需要的是封闭范围内变量的访问说明符,而不是全局范围。

    在 python 3.0 中,正如我所测试的,nonlocal 关键字正是您所需要的,它代替了 global

    nonlocal get
    ...
    

    在 python 2.x 中,我刚刚删除了 global getoldget 引用,它可以正常工作。

    【讨论】:

    • 所以这是语言的缺陷。我希望你不要这么说。 :-( 一种可能的解决方法是将值存储在一个可变结构(如列表)中并对其进行修改,但这太难了。我想要一个 2.X 版本以实现兼容性,但我可能不得不跳到 3000。
    • 有一个解决方法,一个相当简单的解决方法,除非它与您的纯度概念混淆——只需将 get() 设为全局函数。也就是说,我必须说你的记忆方法对我来说似乎太复杂了:)
    • 它是如何正常工作的?只删除“global get”和“oldget = get”行,它从不使用保存的值。
    • 并在newget中将“oldget”改为“get”。
    • 我假设“正常工作”,他的意思是它返回了正确的答案(尽管没有记忆)。
    【解决方案2】:
    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* 闭包使用时,我也做了一些改进。通常缓存是一个字典而不是闭包的链表。

    【讨论】:

    • 这并不理想,但我想该解决方法效果很好。感谢您也对代码进行了一些润色。
    • 恕我直言,将这段代码用于学习 Python 如何处理闭包之外的任何事情都注定要失败。其他变化更能推动您朝着正确的方向前进。
    • 这是主要目标。如果我用 Python 编写代码,我会改用字典,但我确实很欣赏一些小改动,比如在一个语句中设置多个值并使用 fn(*args) 而不是 apply。
    【解决方案3】:

    您希望将global get 放在每个 函数的开头(get 本身除外)。

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

    global get 放入 mfun 和 vset 使它们工作。我无法指出使这成为必要的范围规则,但它有效;-)

    你的 conses 也很笨拙... :)

    【讨论】:

    • 嗯.. 在我的机器上,将全局 get 放入 get 和 vset 不起作用。将它添加到 memoize 本身的开头是有效的,但这会导致 memoize 的每次调用都使用相同的 get 函数(不可取)。
    • 啊,是的。自我提醒:喝咖啡,醒来,然后回答;-)
    【解决方案4】:

    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
    

    这样你不需要分配给捕获的变量名,但仍然可以得到你想要的。

    【讨论】:

      【解决方案5】:

      可能是因为您希望 global 在它不是全局的时候得到它? 顺便说一句, 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)
      

      【讨论】:

      • 感谢您提供有关应用的提示。然而,运行上面修改过的代码在它给出正确答案的意义上是有效的,但它根本不使用记忆。 (你可以通过运行 fibm(35) 之类的东西来验证这一点,如果记住,它应该是接近瞬时的。
      • x = y 分配给(或创建)局部变量 x,除非该函数中某处有全局语句。
      【解决方案6】:

      我认为最好的方法是:

      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]
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-12-03
        • 2018-02-04
        • 2015-08-04
        • 2019-10-28
        • 2011-05-20
        • 1970-01-01
        相关资源
        最近更新 更多