freeCodeCamp/guide/chinese/python/nested-functions/index.md

11 KiB
Raw Blame History

title localeTitle
Nested Functions in Python Python中的嵌套函数

命名空间

函数的参数以及函数体中绑定的任何变量通过赋值或其他绑定语句如def构成函数的本地名称空间也称为本地作用域。这些变量中的每一个都被称为函数的局部变量。

非本地变量称为全局变量在没有嵌套函数定义的情况下我们将在稍后讨论。全局变量是模块对象的属性如第140页的“模块对象的属性”中所述。每当函数的局部变量与全局变量具有相同的名称时该函数体内的名称引用局部变量不是全球性的。我们通过说局部变量在整个函数体中隐藏同名的全局变量来表达这一点。

全球声明

默认情况下,函数体中绑定的任何变量都是函数的局部变量。如果一个函数需要重新绑定一些全局变量,那么第一个 功能声明必须是:

全局标识符

其中identifier是由逗号分隔的一个或多个标识符。全局语句中列出的标识符指的是函数需要重新绑定的全局变量即模块对象的属性。例如我们在第73页的“函数对象的其他属性”中看到的函数计数器可以使用全局变量和全局变量来实现而不是函数对象的属性

_count = 0 def counter 全局_count _count + = 1 return _count

如果没有全局语句计数器函数将引发UnboundLocal-Error异常因为_count将是未初始化未绑定的局部变量。虽然全局声明支持这种编程但这种风格通常不够优雅且不可取。正如我之前提到的当你想将一些状态和某些行为组合在一起时第5章中介绍的面向对象机制通常是最好的。

如果函数体只使用全局变量,则不要使用全局变量(如果对象是可变的,则包括变量绑定到该变量的对象)。仅当函数体重新绑定全局变量时(通常通过分配变量的名称),才使用全局语句。作为一种风格问题,不要使用全局,除非它是绝对必要的,因为它的存在将使你的程序的读者认为该声明是出于某些有用的目的。特别是,除了作为函数体中的第一个语句之外,永远不要使用全局。

{mospagebreak title =嵌套函数和嵌套作用域}

函数体内的def语句定义嵌套函数其体包含def的函数称为嵌套函数的外部函数。嵌套函数体中的代码可以访问但不重新绑定外部函数的局部变量也称为嵌套函数的自由变量。

让嵌套函数访问值的最简单方法通常不依赖于嵌套作用域,而是显式地将该值作为函数参数之一传递。如果需要,可以在使用值作为可选参数的缺省值定义嵌套函数时绑定参数的值。例如:

def percent1abc def pcxtotal = a + b + creturnx * 100.0/ total print“百分比是pcapcbpcc

以下是使用嵌套作用域的相同功能:

def percent2abc def pcxreturnx * 100.0/a + b + c print“百分比是pcapcbpcc

在这种特定情况下percent1有一个很小的优势a + b + c的计算只发生一次而percent2的内部函数pc重复计算三次。但是如果外部函数在对嵌套函数的调用之间重新绑定其局部变量则可能需要重复计算。因此建议您了解这两种方法并根据具体情况选择最合适的方法。

从外部局部变量访问值的嵌套函数也称为闭包。以下示例显示了如何构建闭包:

def make_adderaugend def addaddend 返回addend + augend 返回添加

闭包是一般规则的一个例外第5章中介绍的面向对象机制是将数据和代码捆绑在一起的最佳方法。当你需要专门构造可调用对象时在对象构造时固定一些参数闭包可以比类更简单更有效。例如make_adder7的结果是一个接受单个参数并向该参数添加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 xl =低, h =高h> x> laList返回[4,5,6]

作为替代方法您始终可以使用本地def语句为函数对象提供名称。然后您可以使用此名称作为参数或返回值。这是使用本地def语句的相同过滤器示例

aList = [1,2,3,4,5,6,7,8,9] 低= 3 高= 7 def在_界限内l =低h =高): 返回h> value> l filter在_ boundsaList中#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 updownN for x in xrange1Nyield x for x in xrangeN0-1yield x 对于我在updown3 print iprints1 2 3 2 1

这是一个有点像内置xrange函数的生成器但返回一系列浮点值而不是整数序列

def frangestartstopstep = 1.0 而开始<停止: 产量开始 开始+ =步骤

这个frange示例有点像xrange因为为简单起见它使参数开始和停止是强制性的并且默认地假设步骤是正的。

生成器比返回列表的函数更灵活。生成器可以构建无界迭代器意味着返回无限结果流仅在通过其他方式终止的循环中使用例如通过break语句。此外生成器构建的迭代器执行延迟评估迭代器仅在需要时及时地计算每个连续项而等效函数提前完成所有计算并且可能需要大量内存来保存结果列表。因此如果您只需要迭代计算序列则通常最好在生成器中而不是在返回列表的函数中计算序列。如果调用者需要一些有界生成器G参数生成的所有项的列表则调用者可以简单地使用以下代码

resul_list = listGarguments

生成器表达式

Python 2.4引入了一种更简单的方法来编写特别简单的生成器生成器表达式通常称为genexps。 genexp的语法就像列表推导的语法如第67页的“列表推导”中所述除了genexp括在括号而不是括号[]; genexp的语义与相应列表理解的语义相同只是genexp产生迭代器一次产生一个项目而列表理解产生内存中所有结果的列表因此使用genexp当适当节省记忆。例如要对所有单位数整数的平方求和在任何现代Python中您都可以进行编码 sum[x x for x in xrange10];在Python 2.4中您可以更好地表达此功能将其编码为sumx x表示xrange10中的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.sendx调用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异常的程序。通常建议您更好地寻找删除递归的方法或者更具体地说限制程序所需的递归深度。

熟悉LispScheme或函数式编程语言的读者必须特别注意Python不实现“尾部调用消除”的优化这在这些语言中非常重要。在Python中任何调用递归与否在时间和内存空间方面都有相同的成本仅取决于参数的数量成本不会改变调用是否是“尾调用”意思是该调用是调用者执行的最后一个操作或任何其他非尾调用。