在内部函数内部分配外部变量的python闭包

Posted

技术标签:

【中文标题】在内部函数内部分配外部变量的python闭包【英文标题】:python closure with assigning outer variable inside inner function 【发布时间】:2012-08-18 23:44:30 【问题描述】:

我有这段代码:

#!/usr/bin/env python

def get_match():
  cache=[]
  def match(v):
    if cache:
      return cache
    cache=[v]
    return cache
  return match
m = get_match()
m(1)

如果我运行它,它会说:

UnboundLocalError: local variable 'cache' referenced before assignment

但如果我这样做:

#!/usr/bin/env python

def get():
  y = 1
  def m(v):
    return y + v
  return m

a=get()
a(1)

它运行。

列表有什么东西吗?还是我的代码组织错了?

【问题讨论】:

您的第二个示例没有类似于cache=[v] 的行。这使得 Python 将变量视为局部变量,使函数内的早期引用非法。 你的条件不应该是if v in cache:吗?否则,这是一个“缓存”,仅包含第一次调用的参数 我忍不住把你的代码和记忆联系起来,你打算这样做吗?在这种情况下,请查看***.com/a/3377272/321973 这能回答你的问题吗? UnboundLocalError with nested function scopes 【参考方案1】:

问题是变量cache不在函数匹配的范围内。如果您只想像在第二个示例中那样读取它,这不是问题,但是如果您正在分配它,python 会将其解释为局部变量。如果您使用的是 python 3,则可以使用 nonlocal 关键字来解决此问题 - 不幸的是,对于 python 2,没有简单的解决方法。

def f():
    v = 0

    def x():
        return v    #works because v is read from the outer scope

    def y():
        if v == 0:  #fails because the variable v is assigned to below
            v = 1

    #for python3:
    def z():
        nonlocal v  #tell python to search for v in the surrounding scope(s)
        if v == 0:
            v = 1   #works because you declared the variable as nonlocal

问题与全局变量有些相同 - 每次分配给全局变量时都需要使用global,而不是读取它。

简要解释其背后的原因: python 解释器将所有函数编译成function 类型的特殊对象。在此编译期间,它检查函数创建的所有局部变量(用于垃圾收集等)。这些变量名保存在函数对象中。由于“隐藏”外部范围变量(创建具有相同名称的变量)是完全合法的,因此假定分配给且未显式声明为 global(或 python3 中的 nonlocal)的任何变量是一个局部变量。

当函数执行时,解释器必须查找它遇到的每个变量引用。如果在编译过程中发现该变量是本地变量,则在函数 f_locals 字典中进行搜索。如果尚未分配,则会引发您遇到的异常。如果变量未在函数范围内分配,因此不是其局部变量的一部分,则在周围范围内查找它 - 如果在周围找不到它,则会引发类似的异常。

【讨论】:

没问题 :) 我已经扩展了一点来描述解释器中发生的事情。 @Brandon 这是一个简单但有效的性能优化 - 局部变量将作为指向 PyObject 的指针存储在 PyFrameObject 的数组中;获取或设置局部变量就像索引数组一样简单。外部作用域的变量必须通过更复杂的过程来查找;有关详细信息,请参阅the source(本地人为 LOAD_FAST,否则为 LOAD_NAME/GLOBAL/DEREF)。而且我认为当你想要覆盖一个全局变量时必须声明它是很好的:就像 Zen 状态一样,explicit > implicit 哇,很好的解释。 看来python可以在未定义变量时自动查找非本地范围。【参考方案2】:

访问变量与分配变量不同。

全局变量也有类似的情况。您可以在任何函数中访问它们,但如果您尝试在没有 global 语句的情况下对其进行分配,它将在本地上下文中重新声明它。

不幸的是,对于本地函数,没有等效的 global 语句,但您可以通过替换来绕过重新声明

cache=[v]

与:

cache[:] = [v]

【讨论】:

在 Python 3 中有 nonlocal 关键字。 我没有得到你的替换 - cache[:] 创建列表的副本(这显然有效,因为你现在不再分配给缓存)并分配给这个副本 - 但是副本立即被丢弃,因此您实际上并未分配给变量。因此,虽然您的替换修复了错误消息,但它实际上并没有做任何有用的事情...... @l4mpi:不正确。 cache[:] 在赋值的右边时创建一个副本,但是当它在左边时,它表示赋值给一个切片,这是一个变异手术。要查看实际情况,请将其粘贴到解释器中:a=[]; a_id = id(a); a[:] = [1,2,3]; id(a) == a_id。它将返回True @StevenRumbalski,感谢您提供的信息,我不知道切片操作的这种用法 - 赞成答案。好的,因为它在列表上调用__setslice__,所以这行得通,但仍然有一些限制:显然它只适用于列表而不是任意变量类型;如果变量已用None 而不是[] 初始化,它将失败;右侧只能包含可迭代对象,这意味着您不能将变量设置为 None 或将其引用更改为另一种类型甚至另一个列表(如 a[:]=b 其中 b 是可迭代的仅复制 b,因此修改 a 不会影响 b )。【参考方案3】:

由于 Python 看到 cache=[v] - 分配给 cache,它会将其视为局部变量。所以这个错误是相当合理的——在if语句中使用之前没有定义局部变量cache

你可能想写成:

def get_match():
  cache=[]
  def match(v):
    if cache:
      return cache
    cache.append(v)
    return cache
  return match
m = get_match()
m(1)

强烈推荐阅读:Execution Model - Naming and binding 和 PEP 227 - Statically Nested Scopes

【讨论】:

【参考方案4】:

替换

cache=[]
def match(v):

def match(v,cache=[])

解释:您的代码将cache 声明为get_match 的变量,返回的match(v) 对此一无所知(由于以下赋值)。但是,您确实希望 cache 成为 match 命名空间的一部分。

我知道“恶意”用户可以通过这种方式重新定义缓存,但这是他们自己的错误。 如果这一个问题,替代方案是:

def match(v):
     try:
         if cache:
             return cache
     except NameError:
         cache = []
     ...

(见here)

【讨论】:

捕获 NameError 对 OP 不起作用,因为它总是会被提升。见this contrived example。 @StevenRumbalski 谢谢,我还不明白为什么,但我会尝试......无论如何,the other way 有效 here 是包含缓存的代码,我理解缓存应该工作的方式...(if v in cachecache.append(v) 而不是 if cachecache = [v]

以上是关于在内部函数内部分配外部变量的python闭包的主要内容,如果未能解决你的问题,请参考以下文章

python 函数闭包()

JS封闭函数闭包内置对象

python闭包

Python—闭包

Python函数编程——闭包和装饰器

Python 学习日记第七篇 -- 函数相关