了解闭包中是怎样使用外围作用域变量


作用域问题

有没有遇到这样的一种情况,你想对一个列表中而定数字进行排序,但是有一组要优先进行。这个模式在实际的运用中也是很普遍的,比如你想渲染一个用户接口,或者在其他要处理的事情之前展示重要的消息或者异常事件,优先排序的重要性就体现出来了。

一个通用的方式是把一个工具函数当做一个关键参数传递给列表排序方法,此工具函数的返回值将作为每一个元素排序的权重依据,从而实现优先排序。这个工具函数也可以检测给定的元素是否在那个重要的组别内或者改变排序元素的顺序。


In [1]: values = [1,5,3,9,7,4,2,8,6]

In [2]: group = [7,9]

In [3]: def sort_priority(values, group):
   ...:     def helper(x):
   ...:         if x in group:
   ...:             return (0, x)
   ...:         return (1, x)
   ...:     values.sort(key=helper)
   ...:

In [4]: sort_priority(values,group)

In [5]: print values
[7, 9, 1, 2, 3, 4, 5, 6, 8]

关于此函数可以返回预期结果有如下三个原因:

  • Python支持闭包,即可以从它们被限定的范围引用变量的函数(通俗点讲就是函数可以引用声明域比自己大的变量)。这也是为什么helper函数可以访问set_priority函数中的group变量。
  • Python中函数是头等对象,意味着你可以直接引用他们,把它们赋值给变量,作为参数传递给其他的函数,在表达式或者if条件语句中直接比较等等。这就是为什么排序算法可以接受一个闭包函数作为其参数。
  • 在比较元组方面,Python有一个特殊的规则。那就是先比较元组中下标为0的元素,然后依次递增。这也是为什么从闭包函数返回的值能够在两个不同的列表中的排序起到作用的原因。

如果拥有高优先级的元素无论被包含与否,用户接口处的代码,函数都可以正常工作,那将会是极好的。看起来添加这么一个行为似乎很是直截了当。然而这里仍然是后一个闭包函数来决定当前元素隶属于哪一个组别。那为什么不在高优先级元素被发现的时候使用闭包来翻转一下呢?届时函数就可以返回一个标志,来代表着其被闭包函数修改过了。这里,我尝试着模拟了一下。

def sort_priority2(values, group):
    found = False
    def helper(x):
        if x in group:
            found = True
            return (0, x)
        return (1, x)
    values.sort(key=helper)
    return found
# begin to call
found = sort_priority2(values, group)
print("Found:",found)
print(values)
>>>
Found: False
[7, 9, 1, 2, 3, 4, 5, 6, 8]

排序的结果是正确的,但是很明显分组的那个标志是不正确的了。group中的元素无疑可以在values里面找到,但是函数却返回了False,为什么会发生这样的状况呢?

我们不妨这样来理解,当你在表达式中引用一个变量的时候,Python的解释器将会横向的在作用域按如下顺序查找该值

  • 当前函数的作用域。
  • 任何其他的封闭域(比如其他的包含着的函数)。
  • 包含该段代码的模块域(也称之为全局域)。
  • 内置域(包含了像len,str等函数的域)。

如果在上面四种情况中没有一个域是包含一个定义过了的变量的话,NameError异常就会被触发。

给不同的变量赋值的原理也是不同的。如果一个变量在当前作用域内已经被定义过了,仅仅赋予其新值即可;如果当前作用域内不存在该值,Python就会将其当做变量先进行定义。此新定义的变量的作用域就是包含了这个赋值语句的函数块了。

def sort_priority2(numbers, group):
    found = False    # 作用域:sort_priority2
    def helper(x):
        if x in group:
            found = True    # 作用域: helper
            return (0, x)
        return (1, x)      # 一旦执行了return语句,found在helper的作用域就会由helper转至sort_priority2函数。相应的其值也会发生变化。
    numbers.sort(key=helper)
    return found

产生这个问题的原因就是有时函数调用,新值被定义而引起的作用域漏洞。但是这也是预期的结果。这可以很好的避免函数中的局部变量污染模块中的同名变量的值。另外函数内部的每一次赋值都会把垃圾放入到全局的模块域。当然,这不仅会扰乱视线,更加会由于全局变量的相互作用而引发一些不起眼的错误。

把数据放到外边

Python3中,对于闭包而言有一个把数据放到外边的特殊的语法。nonlocal语句习惯于用来表示一个特定变量名称的域的遍历发生在赋值之前。 唯一的限制就是nonlocal不会向上遍历到模块域级别(这也是为了防止污染全局变量空间)。这里,我定义了一个使用了nonlocal关键字的函数。

def srt_priority3(numbers, group):
    found = False
    def helper(x):
        nonlocal found 
        if x in group:
            found = True
            return (0, x)
        return (1, x)
    numbers.sort(key=helper)
    return found

当数据在闭包外将被赋值到另一个域时,nonlocal 语句使得这个过程变得很清晰。它也是对global语句的一个补充,可以明确的表明变量的赋值应该被直接放置到模块域中。

然而,像这样的反模式的全局便令,我发对使用在那些简单函数之外的其他的任何地方。nonlocal引起的副作用是难以追踪的,而在那些包含着nonlocal语句和赋值语句交叉联系的大段代码的函数的内部则尤为明显。

当你感觉自己的nonlocal语句开始变的复杂的时候,我非常建议你重构一下代码,写成一个工具类。这里,我定义了一个实现了与上面的那个函数功能相一致的工具类。虽然有点长,但是代码却变得更加的清晰了(详见第23项:对于简单接口使用函数而不是类里面的__call__方法)。

class Sorter(object):
    def __init__(self, group):
        self.group = group
        self.found = False

    def __call__(self, x):
        if x in self.group:
            self.found = True
            return (0, x)
        return (1, x)

sorter = Sorter(group)
numbers.sort(key=sorter)
assert sorter is True

Python2中的作用域

不幸的是,Python2是不支持nonlocal关键字的。为了实现相似的功能,你需要广泛的借助于Python的作用与域规则。虽然这个方法并不是完美的,但是这是Python中比较常用的一种做法。

# Python2
def sort_priority(numbers, group):
    found = [False]
    def helper(x):
        if x in group:
            found[0] = True
            return (0, x)
        return (1, x)
    numbers.sort(sort=helper)
    return found[0]

就像上面解释的那样,Python 将会横向查找该变量所在的域来分析其当前值。技巧就是发现的值是一个易变的列表。这意味着一旦检索,闭包就可以修改found的状态值,并且把内部数据的改变发送到外部,这也就打破了闭包引发的局部变量作用域无法被改变的难题。其根本还是在于列表本身元素值可以被改变,这才是此函数可以正常工作的关键。

found为一个dictionary类型的时候,也是可以正常工作的,原理与上文所言一致。此外,found还可以是一个集合,一个你自定义的类等等。


备忘录

  • 闭包函数可以从变量被定义的作用域内引用变量。
  • 默认地,闭包不能通过赋值来影响其检索域。
  • Python3中,可以使用nonlocal关键字来突破闭包的限制,进而在其检索域内改变其值。
  • Python2中没有nonlocal关键字,替代方案就是使用一个单元素(如列表,字典,集合等等)来实现与nonlocal一致的功能。
  • 除了简单的函数,在其他任何地方都应该尽力的避免使用nonlocal关键字。

results matching ""

    No results matching ""