--- title: Nested Functions in Python localeTitle: Python中的嵌套函数 --- ### 命名空间 函数的参数,以及函数体中绑定的任何变量(通过赋值或其他绑定语句,如def)构成函数的本地名称空间,也称为本地作用域。这些变量中的每一个都被称为函数的局部变量。 非本地变量称为全局变量(在没有嵌套函数定义的情况下,我们将在稍后讨论)。全局变量是模块对象的属性,如第140页的“模块对象的属性”中所述。每当函数的局部变量与全局变量具有相同的名称时,该函数体内的名称引用局部变量,不是全球性的。我们通过说局部变量在整个函数体中隐藏同名的全局变量来表达这一点。 ### 全球声明 默认情况下,函数体中绑定的任何变量都是函数的局部变量。如果一个函数需要重新绑定一些全局变量,那么第一个 功能声明必须是: 全局标识符 其中identifier是由逗号(,)分隔的一个或多个标识符。全局语句中列出的标识符指的是函数需要重新绑定的全局变量(即模块对象的属性)。例如,我们在第73页的“函数对象的其他属性”中看到的函数计数器可以使用全局变量和全局变量来实现,而不是函数对象的属性: \_count = 0 def counter(): 全局\_count \_count + = 1 return \_count 如果没有全局语句,计数器函数将引发UnboundLocal-Error异常,因为\_count将是未初始化(未绑定)的局部变量。虽然全局声明支持这种编程,但这种风格通常不够优雅且不可取。正如我之前提到的,当你想将一些状态和某些行为组合在一起时,第5章中介绍的面向对象机制通常是最好的。 如果函数体只使用全局变量,则不要使用全局变量(如果对象是可变的,则包括变量绑定到该变量的对象)。仅当函数体重新绑定全局变量时(通常通过分配变量的名称),才使用全局语句。作为一种风格问题,不要使用全局,除非它是绝对必要的,因为它的存在将使你的程序的读者认为该声明是出于某些有用的目的。特别是,除了作为函数体中的第一个语句之外,永远不要使用全局。 {mospagebreak title =嵌套函数和嵌套作用域} 函数体内的def语句定义嵌套函数,其体包含def的函数称为嵌套函数的外部函数。嵌套函数体中的代码可以访问(但不重新绑定)外部函数的局部变量,也称为嵌套函数的自由变量。 让嵌套函数访问值的最简单方法通常不依赖于嵌套作用域,而是显式地将该值作为函数参数之一传递。如果需要,可以在使用值作为可选参数的缺省值定义嵌套函数时绑定参数的值。例如: def percent1(a,b,c): def pc(x,total = a + b + c):return(x \* 100.0)/ total print“百分比是:”,pc(a),pc(b),pc(c) 以下是使用嵌套作用域的相同功能: def percent2(a,b,c): def pc(x):return(x \* 100.0)/(a + b + c) print“百分比是:”,pc(a),pc(b),pc(c) 在这种特定情况下,percent1有一个很小的优势:a + b + c的计算只发生一次,而percent2的内部函数pc重复计算三次。但是,如果外部函数在对嵌套函数的调用之间重新绑定其局部变量,则可能需要重复计算。因此,建议您了解这两种方法,并根据具体情况选择最合适的方法。 从外部局部变量访问值的嵌套函数也称为闭包。以下示例显示了如何构建闭包: def make\_adder(augend): def add(addend): 返回addend + augend 返回添加 闭包是一般规则的一个例外,第5章中介绍的面向对象机制是将数据和代码捆绑在一起的最佳方法。当你需要专门构造可调用对象时,在对象构造时固定一些参数,闭包可以比类更简单,更有效。例如,make\_adder(7)的结果是一个接受单个参数并向该参数添加7的函数。返回闭包的外部函数是由一些参数区分的函数族成员的“工厂”,例如前一个示例中参数augend的值,并且通常可以帮助您避免代码重复。 ### lambda表达式 如果函数体是单个返回表达式语句,则可以选择使用特殊的lambda表达式替换该函数: lambda参数:表达式 lambda表达式是正常函数的匿名等价物,其正文是单个return语句。请注意,lambda语法不使用return关键字。您可以在任何可以使用函数引用的地方使用lambda表达式。当你想使用一个简单的函数作为参数或返回值时,lambda有时会很方便。这是一个使用lambda表达式作为内置过滤器函数的参数的示例(在过滤器中介绍,第161页): aList = \[1,2,3,4,5,6,7,8,9\] 低= 3 高= 7 过滤器(lambda x,l =低, h =高:h> x> l,aList)#返回:\[4,5,6\] 作为替代方法,您始终可以使用本地def语句为函数对象提供名称。然后,您可以使用此名称作为参数或返回值。这是使用本地def语句的相同过滤器示例: aList = \[1,2,3,4,5,6,7,8,9\] 低= 3 高= 7 def在_界限内(值,l =低,h =高): 返回h> value> l filter(在_ bounds,aList中)#return:\[4,5,6\] 虽然lambda偶尔会有用,但许多Python用户更喜欢def,这更通用,如果为函数选择合理的名称,可能会使代码更具可读性。 {mospagebreak title = Generators} 当函数体包含一个或多个关键字yield时,该函数称为生成器。当您调用生成器时,函数体不会执行。相反,调用生成器返回一个特殊的迭代器对象,它包装函数体,其局部变量(包括其参数)和当前执行点,最初是函数的开始。 当调用此迭代器对象的下一个方法时,函数体将执行下一个yield语句,该语句采用以下形式: 屈服表达 当yield语句执行时,函数执行被“冻结”,当前执行点和局部变量保持不变,并且作为下一个方法的结果返回yield之后的表达式。当再次调用next时,函数体的执行将从中断处继续执行,再次执行下一个yield语句。如果函数体结束或执行return语句,迭代器会引发StopIteration异常以指示迭代已完成。生成器中的return语句不能包含表达式。 生成器是构建迭代器的一种非常方便的方法。因为使用迭代器的最常见方法是使用for语句循环它,所以通常调用这样的生成器: 对于somegenerator(参数)中的可变: 例如,假设您想要一系列数字从1到N再次计数,然后再次减少到1。发电机可以帮助: def updown(N): for x in xrange(1,N):yield x for x in xrange(N,0,-1):yield x 对于我在updown(3): print i#prints:1 2 3 2 1 这是一个有点像内置xrange函数的生成器,但返回一系列浮点值而不是整数序列: def frange(start,stop,step = 1.0): 而开始<停止: 产量开始 开始+ =步骤 这个frange示例有点像xrange,因为为简单起见,它使参数开始和停止是强制性的,并且默认地假设步骤是正的。 生成器比返回列表的函数更灵活。生成器可以构建无界迭代器,意味着返回无限结果流(仅在通过其他方式终止的循环中使用,例如,通过break语句)。此外,生成器构建的迭代器执行延迟评估:迭代器仅在需要时及时地计算每个连续项,而等效函数提前完成所有计算并且可能需要大量内存来保存结果列表。因此,如果您只需要迭代计算序列,则通常最好在生成器中而不是在返回列表的函数中计算序列。如果调用者需要一些有界生成器G(参数)生成的所有项的列表,则调用者可以简单地使用以下代码: resul\_list = list(G(arguments)) ### 生成器表达式 Python 2.4引入了一种更简单的方法来编写特别简单的生成器:生成器表达式,通常称为genexps。 genexp的语法就像列表推导的语法(如第67页的“列表推导”中所述),除了genexp括在括号(())而不是括号(\[\]); genexp的语义与相应列表理解的语义相同,只是genexp产生迭代器一次产生一个项目,而列表理解产生内存中所有结果的列表(因此,使用genexp,当适当,节省记忆)。例如,要对所有单位数整数的平方求和,在任何现代Python中,您都可以进行编码 sum(\[x _x for x in xrange(10)\]);在Python 2.4中,您可以更好地表达此功能,将其编码为sum(x_ x表示xrange(10)中的x)(只是相同,但省略括号),并获得完全相同的结果,同时消耗更少的内存。请注意,表示函数调用的括号也“执行双重任务”并包含genexp(不需要额外的括号)。 {mospagebreak title = Python 2.5中的生成器} 在Python 2.5中,生成器得到进一步增强,可以在每个yield执行时从调用者接收值(或异常)。这些高级功能允许2.5中的生成器实现完整的协同例程,如http://www.python.org/peps/pep-0342.html中所述。主要的变化是,在2.5中,yield不是一个语句,而是一个表达式,因此它有一个值。通过下一步调用其方法恢复生成器时,相应的yield的值为None。要将值x传递给某个生成器g(以便g接收x作为其挂起的yield的值),而不是调用g.next(),调用者调用g.send(x)(调用g.send) (无)就像调用g.next())。此外,在Python 2.5中,没有参数的裸产率变得合法,并且相当于产生None。 生成器的其他Python 2.5增强功能与异常有关,并在第126页的“生成器增强功能”中介绍。 ### 递归 Python支持递归(即Python函数可以调用自身),但递归的深度有限。默认情况下,Python会在检测到递归调用堆栈的深度超过1,000时,中断递归并引发RecursionLimitExceeded异常(在“标准异常类”(第130页)中介绍)。您可以使用模块sys的函数setrecursionlimit更改递归限制,请参阅第171页的setrecursionlimit。 但是,更改递归限制并不能为您提供无限递归;绝对最大限制取决于运行程序的平台,特别是底层操作系统和C运行时库,但通常只有几千个级别。如果递归调用太深,程序崩溃。在调用超出平台功能的setrecursionlimit之后,这种失控的递归是Python程序崩溃的极少数方式之一 - 真正崩溃,很难,没有通常的Python异常机制的安全网。因此,要小心尝试使用setrecursionlimit通过将递归限制提高得太高来修复正在获取RecursionLimitExceeded异常的程序。通常,建议您更好地寻找删除递归的方法,或者更具体地说,限制程序所需的递归深度。 熟悉Lisp,Scheme或函数式编程语言的读者必须特别注意Python不实现“尾部调用消除”的优化,这在这些语言中非常重要。在Python中,任何调用(递归与否)在时间和内存空间方面都有相同的成本,仅取决于参数的数量:成本不会改变,调用是否是“尾调用”(意思是该调用是调用者执行的最后一个操作)或任何其他非尾调用。