一、举例
题目:
假如有一份列表,其中的额元素都是数字,现在要对其排序,但排序时候,要把出现在某个群组内的数字,放在群组外的那些数字之前。
用途
- 将某些重要的消息或意外事件优先显示在其他内容前面。
实现
在调动列表的sort
方法时,把辅助函数传给key
参数。这个辅助参数的返回值,将会确定列表中各个元素的顺序。
def sort_priority(values, group):
def helper(x):
if x in group:
return (0, x)
return (1, x)
values.sort(key=helper)
numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}
sort_priority(numbers, group)
print(numbers)
# 输出
[2, 3, 5, 7, 1, 4, 6, 8]
实现的原因:
- 1. Python支持闭包
闭包是一种定义在某个作用域的函数,这种函数引用了那个作用域里面的变量。
helper
函数之所以能够访问sort_priority
的group
参数,原因在于它就是闭包
- 2. Python的函数是一级对象
可以直接引用函数,把函数赋给变量、把函数当成参数传递给其他函数,并通过表达式及
if
语句对其进行比较和判断,等等。
- 3. Python使用特殊的规则来比较两个元组
它首先比较元组中哥下表为0的对应元素,如果相等,再比较下标为1的对应元素。如果还是相等,那么就继续比较下标为2的对应元素,以此类推。
二、改进需求
需求
sort_priority
函数改进一下,应该返回一个值,用来表示用户界面里是否出现了优先级较高的元件,使得该函数的调用者,可以根据这个返回值做出相应的处理。
实现(wrong)*
def sort_priority2(numbers, group):
found = False
def helper(x):
if x in group:
found = True
return (0, x)
return (1, x)
numbers.sort(key=helper)
return found
numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}
found = sort_priority2(numbers, group)
print("Found:", found)
输出如下:
Found: False
排序结果是对的,但是found
值不对。
问题:
numbers
里面的某些数字确实包含在group
中,可是函数却返回了False
,这是为什么呢?
在表达式引用变量时,Python解释器将按如下顺序遍历各作用域,以解析该引用:
- 当前函数作用域
- 任何外围作用域(例如,包含当前函数的其他函数)
- 包含当前代码的那个模块的作用域(全局作用域)
- 内置作用域(包含
len
及str
等函数的那个作用域)
如果上面这些地方都没有定义过名称相符的变量,那就抛出NameError
异常
给变量赋值的规则:
- 如果当前作用域已经定义了这个变量,那么该变量就会具备新值
- 如果当前作用域没有这个变量,则会把这次赋值视为对该变量的定义,而新定义的这个变量,其作用域就是包含赋值操作的这个函数。
解释
上面所说的这种赋值行为,可以解释sort_priority2
函数的返回值错误的原因:
将
found
变量定义为True
,是在helper闭包里进行的。于是,比保重的这次赋值操作,就相当于在helper内定义了名为found
的新变量,而不是给sort_priority2
中的那个found
赋值。
def sort_priority2(numbers, group):
found = False # Scope: 'sort_priority2'
def helper(x):
if x in group:
found = True # Scope: 'helper'
return (0, x)
return (1, x)
numbers.sort(key=helper)
return found
这种问题,有时称为作用域bug。
其实,Python语言是故意要这么设计的。这样做可以防止函数中的局部变量污染函数外面的那个模块。
三、获取闭包内的数据
Python3中有一种特殊的写法,能够获取闭包内的数据。我们可以用nonlocal
语句来表明这样的意图。
-
nonlocal
作用:即给相关变量赋值的时候,应该再上层作用域中查找该变量。 -
nonlocal
的唯一限制:不能延伸到模块级别,这是为了防止它污染全局作用域。
def sort_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
numbers = [8, 3, 1, 2, 5, 4, 7, 6]
group = {2, 3, 5, 7}
found = sort_priority3(numbers, group)
print("Found:", found)
输出结果如下:
Found: True
nonlocal
语句:如果在闭包内给该变量赋值,那么修改的其实是闭包外那个作用域中的变量。
global
语句:对该变量的赋值操作,将会直接修改模块作用域里的那个变量
四、建议
-
nonlocal
也会像全局变量那样,遭到滥用,所以,建议大家只在极其简单的函数里使用这种机制。 -
nonlocal
的副作用很难追踪,尤其在比较长的函数中,修饰某变量的nonlocal
语句可能和修改该变量的赋值操作离得比较远,从而导致代码更加难以理解。
如果使用
nonlocal
的代码,已经写得越来越复杂,那就应该将相关的状态封装成辅助类。
下面定义的这个类,与nonlocal
所达成的功能相同。它虽然有点长,但是理解起来相当容易:
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.found == True
五、Python2中的值
不幸的是,Python2中不支持nonlocal
关键字。
为此,我们需要利用Python的作用域规则来解决。虽然这种做法不太优雅,但是已经成了一种编码习惯
# Python2
def sort_priority(numbers, group):
found = [False]
def helper(x):
nonlocal found
if x in group:
found[0] = True
return (0, x)
return (1, x)
numbers.sort(key=helper)
return found
运行上面这段代码时,Python要解析found
变量的当前值,于是,它会按照刚才所讲的变量搜寻选择,在上级作用域中查找这个变量。
上级作用域中的found
变量是个列表,由于列表本身是可供修改的,所以获取到这个found
的列表后,我们就可以在比闭包通过found[0]=True
语句,来修改found
的状态。
六、总结
- 对于定义在某作用域内的闭包来说,它可以引用这些作用域中的变量
- 使用默认方式对闭包内的变量赋值,不会影响外围作用域中的同名变量
- 在Python3中,程序可以在比包内用
nonlocal
语句来修饰整个名称,使该闭包能够修改外围作用域中的同名变量 - 在Python2中,程序可以使用可变值(例如列表、字典)来实现与
nonlocal
语句相仿的机制 - 除了比较简单的函数,尽量不要使用
nonlocal
语句
网友评论