原文标题:KNOW HOW CLOSURES INTERACT WITH VARIABLE SCOPE
比如说你现在想要对一组数字进行排序,同时希望提高一组数字的优先级使这组数字优先显示。这种模式在展示用户接口时非常有用,在展示用户接口时经常需要优先展示一些重要信息以及异常事件。
解决这类问题的一个常用方法是在调用排序方法时传递一个帮助方法作为关键字参数。帮助方法的返回值用于对列表中的每一个元素进行排序。帮助方法能够检查列表中的值是否属于优先显示数组。帮助方法的实例代码如下:
下面是一个简单应用:
这个帮助方法能够正常工作有3个原因:
- Python支持闭包:方法可以引用包含它们的作用域内的变量(functions that refer to variables from the scope in which they were defined)。这就是为什么helper方法可以访问group参数。
- 在Python中方法是第一类对象(first-class object), 这意味着你可以直接引用方法、将方法赋值给变量、作为参数将方法传递给另一个方法、在if语句或者表达式用比较方法。这就是为什么sort方法可以接收另一个方法作为参数。
- Python针对元组有一套特殊的比较规则。Python中元组的比较是按照元组中每个元素进行比较的,首先比较第0个元素,然后比较第一个,然后第二个,一直比较下去。这就是为什么在helper方法中返回两个不同的元组。
如果帮助方法在返回以上元组之外还能返回元素是否出现在了优先数组中将会对调用者更有帮助。看起来在前面代码的基础上实现这个行为非常容易,只需要在将代码做如下调整即可:
执行代码:
排序正确但是found结果错误。这是为什么呢?
当你在一个表达式中引用变量时,Python解释器会按照以下顺序遍历所有作用域来找到参数引用:
- 当前方法的作用域
- 任何闭合作用域(例如其它外围方法)
- 包含当前代码的模块的作用域(也叫 global作用域)
- 内嵌作用域(例如len和str)
如果遍历完上面全部作用域还没有找到需要引用的名字,就会抛出一个NameError。
但是给变量赋值的顺序就不一样了。如果在当前作用域中已经定义了变量,那么这个变量就会直接获得新值。如果当前作用域中没有这个变量,那么Python会把赋值语句当作变量定义。这个新变量的作用域就是包含赋值语句的方法。
赋值语句的行为就解释了为什么sort_priority2 的返回值错误了。found变量在helper中被赋予了true,闭包中的赋值语句被认为是一个新变量,这不影响外面方法中的变量。
两个found的作用域:
这就是闭包bug。但是这又是希望的结果。这种行为可以有效的防止方法中的变量污染外部模块。否则方法内部的任意赋值语句都会影响全局变量。这样会产生很多噪音,还会引起奇怪的bug。
使数据可以被访问
Python 3:在Python 3中有一种特殊语法可以使闭包内的数据被提取出来。使用nonlocal语句可以指定一个变量的遍历方式是从上级开始遍历。唯一的限制就是nonlocal不能遍历到模块级别(防止污染全局变量)。
下面使用nonlocal语句重写上面的方法:
在此,nonlocal语句可以很清晰的将一个作用域的变量以入到另一个作用域。它是对global语句的一个补充,global指定全局变量。
然而,就像全局变量的反面模式一样(much like the anti-pattern of global variables),除了简单的函数,我不建议大家使用nonlocal。nonlocal的副作用很难被追踪。尤其是在一个非常长的方法中,nonlocal语句、赋值语句与相关的变量离的非常远的情况。
一旦nonlocal是的代码变复杂,你就需要考虑是否可以将代码打包成一个帮助方法。下面我修改前面的代码,不使用nonlocal以达到同样目的,代码有点长,但是更易于阅读:
Python 2:不幸的是,Python 2中根本就没有nonlocal关键字。为了达到同样目的,你需要利用Python的高级用法来实现一个变通方法。下面的代码实现并不友好,但却是Python中的常用方法。
就像前面解释的一样,Python会向上遍历found被引用的域以确定found当前的值。这里引起欺诈性就是found是一个列表,它的值非常易变。这意味着,一旦found被检索到,闭包方法就可以修改found的状态进而把值从内部作用域中传递出来(在这里使用 found[0] = True)。
除了使用列表,通过使用字典、集合、类的实例一样可以在域间传值。
注意事项:
- 闭包方法可以在它们定义的作用域内引用任何变量;
- 默认情况下,闭包方法不能给外部变量赋值;
- 在Python 3中可以使用nonlocal语句指定修改闭包以外的变量;
- 在Python 2中使用可变值变量来代替nonlocal语句;
- 除了非常简单的方法外,避免使用nonlocal语句。