如何在闭包内使用外围作用域中的变量?在正式开始前,我们先来看下面一个简单的例子:
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
异常。
变量赋值
给变量赋值时,规则有所不同。如果当前作用域已经定义了这个变量,那么该变量就会具备新值;若是当前作用域未定义这个变量,那么赋值操作将变为对该变量的定义。定义的这个新变量,其作用域就是包含赋值操作的函数。因此,helper
中 priority = 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
的状态。
上级作用域中的变量是字典、集合或某个类的实例时,该技巧也同样适用。