《编写高质量Python代码的59个有效方法》——第15条:了解如何在闭包里使用外围作用域中的变量

第15条:了解如何在闭包里使用外围作用域中的变量
假如有一份列表,其中的元素都是数字,现在要对其排序,但排序时,要把出现在某个群组内的数字,放在群组外的那些数字之前。这种用法在绘制用户界面时候可能会遇到,我们可以用这个办法把重要的消息或意外的事件优先显示在其他内容前面。
实现该功能的一种常见做法,是在调用列表的sort方法时,把辅助函数传给key参数。这个辅助函数的返回值,将会用来确定列表中各元素的顺序。辅助函数可以判断受测元素是否处在重要群组中,并据此返回相应的排序关键字(sort key)。

这个函数能够应对比较简单的输入值。

这个函数之所以能够正常运作,是基于下列三个原因:
Python支持闭包(closure):闭包是一种定义在某个作用域中的函数,这种函数引用了那个作用域里面的变量。helper函数之所以能够访问sort_priority的group参数,原因就在于它是闭包。
Python的函数是一级对象(f?irst-class object),也就是说,我们可以直接引用函数、把函数赋给变量、把函数当成参数传给其他函数,并通过表达式及if语句对其进行比较和判断,等等。于是,我们可以把helper这个闭包函数,传给sort方法的key参数。
Python使用特殊的规则来比较两个元组。它首先比较各元组中下标为0的对应元素,如果相等,再比较下标为1的对应元素,如果还是相等,那就继续比较下标为2的对应元素,依次类推。
这个sort_priority函数如果能够改进一下,就更好了,它应该返回一个值,用来表示用户界面里是否出现了优先级较高的元件,使得该函数的调用者,可以根据这个返回值做出相应的处理。添加这样的功能,看似非常简单。既然该函数里的闭包函数,能够判断受测数字是否处在群组内,那么不妨在发现优先级较高的元件时,从闭包函数中翻转某个标志变量,然后令sort_priority函数把经过闭包修改的那个标志变量,返回给调用者。
我们先试试下面这种简单的写法:

用刚才那些输入数据,来运行这个函数:

排序结果是对的,但是found值不对。numbers里面的某些数字确实包含在group中,可是函数却返回了False。这是为什么呢?
在表达式中引用变量时,Python解释器将按如下顺序遍历各作用域,以解析该引用:
1)当前函数的作用域。
2)任何外围作用域(例如,包含当前函数的其他函数)。
3)包含当前代码的那个模块的作用域(也叫全局作用域,global scope)。
4)内置作用域(也就是包含len及str等函数的那个作用域)。
如果上面这些地方都没有定义过名称相符的变量,那就抛出NameError异常。
给变量赋值时,规则有所不同。如果当前作用域内已经定义了这个变量,那么该变量就会具备新值。若是当前作用域内没有这个变量,Python则会把这次赋值视为对该变量的定义。而新定义的这个变量,其作用域就是包含赋值操作的这个函数。
上面所说的这种赋值行为,可以解释sort_priority2函数的返回值错误的原因。将found变量赋值为True,是在helper闭包里进行的。于是,闭包中的这次赋值操作,就相当于在helper内定义了名为found的新变量,而不是给sort_priority2中的那个found赋值。

这种问题有时称为作用域bug(scoping bug),它可能会使Python新手感到困惑。其实,Python语言是故意要这么设计的。这样做可以防止函数中的局部变量污染函数外面的那个模块。假如不这么做,那么函数里的每个赋值操作,都会影响外围模块的全局作用域。那样不仅显得混乱,而且由于全局变量还会与其他代码产生交互作用,所以可能引发难以探查的bug。
1.?获取闭包内的数据
Python 3中有一种特殊的写法,能够获取闭包内的数据。我们可以用nonlocal语句来表明这样的意图,也就是:给相关变量赋值的时候,应该在上层作用域中查找该变量。nonlocal的唯一限制在于,它不能延伸到模块级别,这是为了防止它污染全局作用域。
下面用nonlocal来实现这个函数:

nonlocal语句清楚地表明:如果在闭包内给该变量赋值,那么修改的其实是闭包外那个作用域中的变量。这与global语句互为补充,global用来表示对该变量的赋值操作,将会直接修改模块作用域里的那个变量。
然而,nonlocal也会像全局变量那样,遭到滥用,所以,建议大家只在极其简单的函数里使用这种机制。nonlocal的副作用很难追踪,尤其是在比较长的函数中,修饰某变量的nonlocal语句可能和修改该变量的赋值操作离得比较远,从而导致代码更加难以理解。
如果使用nonlocal的那些代码,已经写得越来越复杂,那就应该将相关的状态封装成辅助类(helper class)。下面定义的这个类,与nonlocal所达成的功能相同。它虽然有点长,但是理解起来相当容易(其中有个名叫__call__的特殊方法,详情参见本书第23条)。

2.?Python 2中的值
不幸的是,Python 2不支持nonlocal关键字。为了实现类似的功能,我们需要利用Python的作用域规则来解决。这个做法虽然不太优雅,但已经成了一种Python编程习惯。

运行上面这段代码时,Python要解析found变量的当前值,于是,它会按照刚才所讲的变量搜寻规则,在上级作用域中查找这个变量。上级作用域中的found变量是个列表,由于列表本身是可供修改的(mutable,可变的),所以获取到这个found列表后,我们就可以在闭包里面通过found[0] = True语句,来修改found的状态。这就是该技巧的原理。
上级作用域中的变量是字典(dictionary)、集(set)或某个类的实例时,这个技巧也同样适用。
要点
对于定义在某作用域内的闭包来说,它可以引用这些作用域中的变量。
使用默认方式对闭包内的变量赋值,不会影响外围作用域中的同名变量。
在Python 3中,程序可以在闭包内用nonlocal语句来修饰某个名称,使该闭包能够修改外围作用域中的同名变量。
在Python 2中,程序可以使用可变值(例如,包含单个元素的列表)来实现与nonlocal语句相仿的机制。
除了那种比较简单的函数,尽量不要用nonlocal语句。

时间: 2024-11-02 21:04:27

《编写高质量Python代码的59个有效方法》——第15条:了解如何在闭包里使用外围作用域中的变量的相关文章

《编写高质量Python代码的59个有效方法》——导读

目 录 前 言 致 谢第1章 用Pythonic方式来思考 第1条:确认自己所用的Python版本 第2条:遵循PEP 8风格指南 第3条:了解bytes.str与unicode的区别第4条:用辅助函数来取代复杂的表达式第5条:了解切割序列的办法第6条:在单次切片操作内,不要同时指定start.end和stride第7条:用列表推导来取代map和f?ilter 第8条:不要使用含有两个以上表达式的列表推导第9条:用生成器表达式来改写数据量较大的列表推导第10条:尽量用enumerate取代ran

《编写高质量Python代码的59个有效方法》——第3条:了解bytes、str与unicode的区别

第3条:了解bytes.str与unicode的区别 Python 3有两种表示字符序列的类型:bytes和str.前者的实例包含原始的8位值:后者的实例包含Unicode字符. Python 2也有两种表示字符序列的类型,分别叫做str和unicode.与Python 3不同的是,str的实例包含原始的8位值:而unicode的实例,则包含Unicode字符. 把Unicode字符表示为二进制数据(也就是原始8位值)有许多种办法.最常见的编码方式就是UTF-8.但是大家要记住,Python 3

《编写高质量Python代码的59个有效方法》——第2条:遵循PEP 8风格指南

第2条:遵循PEP 8风格指南 <Python Enhancement Proposal #8>(8号Python增强提案)又叫PEP 8,它是针对Python代码格式而编订的风格指南.尽管可以在保证语法正确的前提下随意编写Python代码,但是,采用一致的风格来书写可以令代码更加易懂.更加易读.采用和其他Python程序员相同的风格来写代码,也可以使项目更利于多人协作.即便代码只会由你自己阅读,遵循这套风格也依然可以令后续的修改变得容易一些. PEP 8列出了许多细节,以描述如何撰写清晰的P

《编写高质量Python代码的59个有效方法》——第9条:用生成器表达式来改写数据量较大的列表推导

第9条:用生成器表达式来改写数据量较大的列表推导 列表推导(参见本书第7条)的缺点是:在推导过程中,对于输入序列中的每个值来说,可能都要创建仅含一项元素的全新列表.当输入的数据比较少时,不会出问题,但如果输入的数据非常多,那么可能会消耗大量内存,并导致程序崩溃. 例如,要读取一份文件并返回每行的字符数.若采用列表推导来做,则需把文件每一行的长度都保存在内存中.如果这个文件特别大,或是通过无休止的network socket(网络套接字)来读取,那么这种列表推导就会出问题.下面的这段列表推导代码,

《编写高质量Python代码的59个有效方法》——第8条:不要使用含有两个以上表达式的列表推导

第8条:不要使用含有两个以上表达式的列表推导除了基本的用法(参见本书第7条)之外,列表推导也支持多重循环.例如,要把矩阵(也就是包含列表的列表,即二维列表)简化成一维列表,使原来的每个单元格都成为新列表中的普通元素.这个功能采用包含两个for表达式的列表推导即可实现,这些for表达式会按照从左至右的顺序来评估. 上面这个例子简单易懂,这就是多重循环的合理用法.还有一种包含多重循环的合理用法,那就是根据输入列表来创建有两层深度的新列表.例如,我们要对二维矩阵中的每个单元格取平方,然后用这些平方值构

《编写高质量Python代码的59个有效方法》——第16条:考虑用生成器来改写直接返回列表的函数

第16条:考虑用生成器来改写直接返回列表的函数 如果函数要产生一系列结果,那么最简单的做法就是把这些结果都放在一份列表里,并将其返回给调用者.例如,我们要查出字符串中每个词的首字母,在整个字符串里的位置.下面这段代码,用append方法将这些词的首字母索引添加到result列表中,并在函数结束时将其返回给调用者. 输入一些范例值,以验证该函数能够正常运作: index_words函数有两个问题. 第一个问题是,这段代码写得有点拥挤.每次找到新的结果,都要调用append方法.但我们真正应该强调的

《编写高质量Python代码的59个有效方法》——第12条:不要在for和while循环后面写else块

第12条:不要在for和while循环后面写else块 Python提供了一种很多编程语言都不支持的功能,那就是可以在循环内部的语句块后面直接编写else块. 奇怪的是,这种else块会在整个循环执行完之后立刻运行.既然如此,那它为什么叫做else呢?为什么不叫and?在if/else语句中,else的意思是:如果不执行前面那个if块,那就执行else块.在try/except语句中,except的定义也类似:如果前面那个try块没有成功执行,那就执行except块. 同理,try/except

《编写高质量Python代码的59个有效方法》——第13条:合理利用try/except/else/f?inally结构中的每个代码块

第13条:合理利用try/except/else/f?inally结构中的每个代码块Python程序的异常处理可能要考虑四种不同的时机.这些时机可以用try.except.else和f?inally块来表述.复合语句中的每个块都有特定的用途,它们可以构成很多种有用的组合方式(参见本书第51条).1.?f?inally块如果既要将异常向上传播,又要在异常发生时执行清理工作,那就可以使用try/f?inally结构.这种结构有一项常见的用途,就是确保程序能够可靠地关闭文件句柄(还有另外一种写法,参见

《编写高质量Python代码的59个有效方法》——第17条:在参数上面迭代时,要多加小心

第17条:在参数上面迭代时,要多加小心如果函数接受的参数是个对象列表,那么很有可能要在这个列表上面多次迭代.例如,要分析来美国Texas旅游的人数.假设数据集是由每个城市的游客数量构成的(单位是每年百万人).现在要统计来每个城市旅游的人数,占总游客数的百分比.为此,需要编写标准化函数(normalization function).它会把所有的输入值加总,以求出每年的游客总数.然后,用每个城市的游客数除以总数,以求出该城市所占的比例. 把各城市的游客数量放在一份列表里,传给该函数,可以得到正确结