0
点赞
收藏
分享

微信扫一扫

Python 函数 - 在闭包内使用外围作用域的变量

如何在闭包内使用外围作用域中的变量?在正式开始前,我们先来看下面一个简单的例子:

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

我们先来说此函数的功能,它能把 values 中位于 group 的元素在排序时,优先放在前面。比如:

>> numbers = list(range(10))
>> numbers
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>> group = list(range(0, 10, 2))
>> group
[0, 2, 4, 6, 8]
>> sort_priority(numbers, group)
>> numbers
[0, 2, 4, 6, 8, 1, 3, 5, 7, 9]

函数运行结果完全符合我们的预期,这主要基于以下几点:

Python 支持闭包

闭包是指定义在某个作用域中的函数,该函数引用了那个作用域里面的变量。函数 helper 之所以能够引用 group 变量,原因就在于它是闭包。

Python 的函数为一等公民

在 Python 中,我们可以直接引用函数,把函数赋值给变量,把函数作为参数传递给其它函数,通过表达式及 if 语句对其进行比较和判断,等等。因此,我们可以把闭包函数 helper 作为参数传递给 sort 方法的 key 参数。

元组的比较方法

Python 在比较两个元组时,首先比较各元组下标为 0 的元素,如果相等,再比较下标为 1 的元素,如果还是相等,就继续比较下标为 2 的元素,以此类推。

下面我们对 sort_priority 提出一个更高的要求,希望它可以返回某个标志,来告诉调用者在排序时是否有元素使用了优先权,即是否发现了位于 group 的元素:

def sort_priority(values, group):
    priority = False
    def helper(x):
        if x in group:
            priority = True
            return (0, x)
        return (1, x)
    values.sort(key=helper)
    return priority

还是刚才的验证步骤,我们来重点看 sort_priority 函数的返回值:

>> numbers = list(range(10))
>> group = list(range(0, 10, 2))
>> sort_priority(numbers, group)
False
>> numbers
[0, 2, 4, 6, 8, 1, 3, 5, 7, 9]

奇怪的事情发生了,排序结果是对的,返回值是错的。这主要涉及到 Python 在表达式中 引用变量给变量赋值 时的规则不同。我们分别来看:

引用变量

Python 在表达式中引用变量时,将按如下顺序遍历各作用域:

  • 当前函数的作用域;
  • 任何外围作用域,例如包含当前函数的其它函数;
  • 包含当前代码的模块作用域,即全局作用域;
  • Python 内置作用域,即包含 len str 等内置对象的作用域。

如果以上位置均为找到,则抛出 NameError 异常。

变量赋值

给变量赋值时,规则有所不同。如果当前作用域已经定义了这个变量,那么该变量就会具备新值;若是当前作用域未定义这个变量,那么赋值操作将变为对该变量的定义。定义的这个新变量,其作用域就是包含赋值操作的函数。因此,helperpriority = True 语句,实则是在 helper 作用域中定义了一个新的变量,也叫 priority,闭包外围的 priority 并未被修改。

Python 之所以这样设计,是为了防止函数中的局部变量污染函数外面的那个模块。如果不这样做,那么函数里的每个赋值操作,都会影响外围模块的全局作用域。那样不仅看上去混乱,而且全局变量会和其他代码产生交互,将引发难以探查的 bug。

尝试一

为了在闭包内修改外围作用域中的变量 priority,Python 提供了 nonlocal 关键字,来讲闭包内的变量声明为外部作用域的变量:

def sort_priority(values, group):
    priority = False
    def helper(x):
        nonlocal priority
        if x in group:
            priority = True
            return (0, x)
        return (1, x)
    values.sort(key=helper)
    return priority

运行结果:

>> numbers = list(range(10))
>> group = list(range(0, 10, 2))
>> sort_priority(numbers, group)
True

nonlocal 语句清晰地指出如果在闭包内给该变量赋值,那么修改的其实是闭包外那个作用域的变量。与 global 语句互为补充,global 用来表明对该变量的赋值操作,将会直接修改模块作用域里的那个变量。

值得注意的是,nonlocal 应该尽量只在简短的函数中使用,对于比较长的函数,修饰变量的 nonlocal 语句可能和修改变量的语句相隔得非常远,不易于后期的维护。

尝试二

如果使用 nonlocal 的那些代码,已经写得越来越复杂,那么我们可以考虑将相关的状态封装为辅助类。比如,我们可以将 helper 闭包函数封装为 Sorter 辅助类:

class Sorter:
    def __init__(self, group):
        self.group = group
        self.priority = False
        
    def __call__(self, x):
        if x in self.group:
            self.priority = True
            return (0, x)
        return (1, x)

运行结果:

>> sorter = Sorter(group=group)
>> numbers.sort(key=sorter)
>> numbers
[0, 2, 4, 6, 8, 1, 3, 5, 7, 9]
>> assert sorter.priority is True
>> 

实现 __call__ 特殊方法的类,其类的实例可以变得和函数一样可调用。当类的实例被调用时,将会执行 __call__ 特殊方法。

补充说明

Python2 不支持 nonlocal 关键字,因此在 Python2 中我们习惯使用可变值(如包含单个元素的列表)来实现与 nonlocal 语句相仿的机制:

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

运行结果:

>> numbers = list(range(10))
>> group = list(range(0, 10, 2))
>> sort_priority(numbers, group)
True

上述代码运行时,helper 函数要解析 priority 变量的当前值,即按照上文所介绍的变量搜寻规则,在上级作用域中查找 priority 。上级作用域中的 priority 变量是个列表,由于列表本身是可供修改的,所以获取到 priority 列表后,我们可以在闭包中通过 priority[0] = True 语句,来修改 priority 的状态。

上级作用域中的变量是字典、集合或某个类的实例时,该技巧也同样适用。

举报

相关推荐

0 条评论