【Python学习手册(第四版)】学习条记19-函数的高级话题

[复制链接]
发表于 2026-2-9 17:10:04 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
个人总结难免疏漏,请多包涵。更多内容请检察原文。本文以及学习条记系列仅用于个人学习、研究互换。
本文紧张先容函数相干的高级概念:递归函数、函数注解、lambda表达式函数,常用函数工具如map、filter、reduce,以及通用的函数计划头脑。团体不是特殊难,如果你从之前每个条记看过来的话,部门观念是很容易继承的,大概稍难点的就是递归了,明确了难度实在不高。以及夸大了不要过多lambda表达式,重复嵌套,代码大概晦涩难懂了。

先容一系列更高级的与函数相干的话题:递归函数、函数属性和注解、lambda表达式、如map和filter如许的函数式编程工具。大概在一样寻常的工作中不会碰到它们。然而,由于它们在某些范畴中有效,有须要对它们有个根本的明确。比方,lambda在GUI中是很常用的。
函数计划概念

当你开始使用函数时,就开始面对怎样将组件聚合在一起的选择了。
比方,怎样将任务分解成为更有针对性的函数(导致了聚合性)、函数将怎样通讯(耦合性)等。须要深入思量函数的巨细等概念,由于它们直接影响到代码的可用性。此中的一些属于布局分析和计划的范畴,但是它们和其他概念一样也实用于Python代码。
在第17条记中先容过了关于函数和模块耦合性的观念,这里是一个对Python初学者的一些通用的引导方针的复习。

  • ·耦合性:对于输入使用参数而且对于输出使用return语句。一样寻常来讲,你须要力图让函数独立于它外部的东西。参数和return语句通常就是隔离对代码中少数夺目位置的外部的依赖关系的最好办法。
  • ·耦合性:只有在真正须要的环境下使用全局变量。全局变量(也就是说,在整个模块中的变量名)通常是一种蹩脚的函数间举行通讯的办法。它们引发了依赖关系和计时的题目,会导致步调调试和修改的困难。
  • ·耦合性:不要改变可变范例的参数,除非调用者盼望如许做。函数会改变传入的可变范例对象,但是就像全局变量一样,这会导致许多调用者和被调用者之间的耦合性,这种耦合性会导致一个函数过于特殊和不友好。
  • ·聚合性:每一个函数都应该有一个单一的、同一的目标。在计划完善的环境下,每一个函数中都应该做一件事:这件事可以用一个简朴阐明句来总结。如果这个句子很宽泛(比方,“这个函数实现了整个步调”),大概包罗了许多的排比(比方,“这个函数让员工产生并提交了一个比萨订单”),你大概就应该想想是不是要将它分解成多个更简朴的函数了。否则,是无法重用在一个函数中把全部步调都混淆在一起的代码。
  • ·巨细:每一个函数应该相对较小。从前面的目标延伸而来,这就比力天然,但是如果函数在表现器上须要翻几页才气看完,大概就到了应该把它分开的时间了。特殊是Python代码是以简朴明确而著称,一个过长大概有着深层嵌套的函数每每就成为计划缺陷的征兆。保持简朴,保持简短。
  • ·耦合:制止直接改变在另一个模块文件中的变量。在第17条记中先容过了这个概念,下面将会在下一部门学习模块时重新复习它。作为参考,记着在文件间改变变量会导致模块文件间的耦合性,就像全局变量产生了函数间的耦合一样:模块难于明确和重用。在大概的时间使用读取函数,而不是直接举行赋值语句。
图19-1总结了函数与外部天下通讯的方法。输入大概来自于左侧的元素,而结果能以右侧的恣意一种情势输出。许多函数计划者倾向于只使用参数作为输入,return语句作为输出。

函数实验环境。函数可以通过多种办法得到输入产生输出,只管使用参数作为输入,return语句并共同可变参数的改变作为输出时,函数每每更容易明确和维护。输出也大概接纳存在于一个封闭的函数作用域中的声明的nonlocal名称的情势。

前面计划的法则有许多特例,包罗一些与Python的OOP支持相干的内容。背面的OPP内容将会讲到,Python的类依赖于修改传入的可变对象:类的函数会主动设置传入参数self的属性,从而修改每个对象的状态信息(比方,self.name='bob')。别的,如果没有使用类,全局变量通常是模块中函数保存调用中状态的最佳方式。如果都在预料之中,副作用就没什么伤害。
通常来讲,我们应该勉力使函数和其他编程组件中的外部依赖性最小化。函数的自包罗性越好,它越容易被明确、复用和修改。

递归函数

第17条记讨论作用域规则的时间,简短地提及Python支持递归函数——即直接或间接地调用自身以举行循环的函数。递归是颇为高级的话题,而且它在Python中相对少见。
然而,它是一项应该相识的有效的技能,由于它允许步调遍历拥有恣意的、不可预知的形状的布局。递归以致是简朴循环和迭代的更换,只管它不愿定是最简朴的或最高效的一种。
递归求和

要对一个数字列表(大概其他序列)求和,我们可以使用内置的sum函数,大概自己编写一个更加定制化的版本。这里是用递归编写的一个定制求和函数的示例:
  1. def mysum(l):
  2.     if not l:
  3.         return 0
  4.     else:
  5.         return l[0] + mysum(l[1:])
  6.    
  7. mysum([1,2,3,4])
  8. 10
复制代码
在每一层,这个函数都递归地调用自己来盘算列表剩余的值的和,这个和随后加到前面的一项中。当列表变为空的时间,递归循环竣事并返回0。当像如许使用递归的时间,对函数调用的每一个打开的层级,在运行时调用堆栈上都有自己的一个函数本地作用域的副本,也就是说,这意味着L在每个层级都是差别的。
如果这很难懂白(而且对于新步调员来说它常常是难以明确),实验给函数添加一个L的打印并再次运行它,从而在每个调用层级记载下当前的列表:
  1. def mysum(l):
  2.     print(l)
  3.     if not l:
  4.         return 0
  5.     else:
  6.         return l[0] + mysum(l[1:])
  7.    
  8. mysum([1,2,3,4])
  9. [1, 2, 3, 4]
  10. [2, 3, 4]
  11. [3, 4]
  12. [4]
  13. []
  14. 10
复制代码
在每个递归层级上,要加和的列表变得越来越小,直到它变为空——递归循环竣事。加和随着递归调用的睁开而盘算出来。这里实际就是把每次递归的第一个偏移给求和,由于每次都会返回L[0]。
替换方案

也可以使用Python的三元if/else表达式,也可以针对任何可加和的范例一样寻常化(如果我们至少假设输入中的一项的话,这将会变得较容易些就像在第18条记最小最大值的示例),而且使用扩展序列赋值来使得第一个/其他的解包更简朴:

例子中的后两个由于空的列表而失败,但是思量到支持+的任何对象范例的序列,而不但是数字:
  1. mysum([1])    #第二个会失败 如果mysum([])
  2. 1
  3. mysum(['s','p','a','m'])
  4. 'spam'
  5. mysum(['spam','ham','eggs'])
  6. 'spamhameggs'
复制代码
研究这3个变体,将会发现,后两者在一个单个字符串参数上也有效(比方,mysum('spam')),由于字符串是一字符的字符串的序列;第三种变体在恣意可迭代对象上都有效,包罗打开的输入文件,但是,其他的两种不会有效,由于它们索引;而且函数头部def mysum(first,*rest)只管雷同于第三种变体,但根本没法工作,由于它等待单个参数,而不是一个单独的可迭代对象。
归是可以是直接的,就像现在为止给出的例子一样;也可以是间接的,就像下面的例子一样(一个函数调用另一个函数,后者反过来调用其调用者)。直接的结果是雷同的,只管这在每个层级有两个函数调用:
  1. def mysum(l):
  2.     if not l: return 0
  3.     return nonempty(l)
  4. def nonempty(l): return l[0] + mysum(l[1:])
  5. mysum([1.1,2.2,3.3,4.4])
  6. 11.0
复制代码
循环语句VS递归

递归对于上一末节的求和的例子有效,但在那种环境中大概过于寻求本领。
实际上,递归在Python中并不像在Prolog或Lisp如许更加深奥的语言中那样常用,由于Python夸大像循环如许的简朴的过程式语句,循环语句通常更为天然。
比方,while常常使得事变更为详细一些,而且它不须要界说一个支持递归调用的函数:
  1. l = [1,2,3,4]
  2. sum = 0
  3. while l:
  4.     sum += l[0]
  5.     l = l[1:]
  6.    
  7. sum
  8. 10
复制代码
以及可以使用for循环为我们主动迭代,使得递归在大多数环境下不必使用(而且,很大概,递归在内存空间和实验时间方面服从较低):
  1. l = [1,2,3,4]
  2. sum = 0
  3. for x in l: sum +=x
  4. sum
  5. 10
复制代码
有了循环语句,不须要在调用堆栈上针对每次迭代都有一个本地作用域的副本,而且制止了一样寻常会与函数调用相干的速率资源。
处理惩罚恣意布局

另一方面,递归(大概对等的显式的基于堆栈的算法)可以要求遍历恣不测形的布局。
作为递归在这种环境中的应用的一个简朴例子,思量像下面的任务:盘算一个嵌套的子列表布局中全部数字的总和:
  1. l = [1,[2,[3,4],5],6,[7,8]]
复制代码
简朴的循环语句在这里不起作用,由于这不是一个线性迭代。嵌套的循环语句也不敷用,由于子列表大概嵌套到恣意的深度而且以恣意的情势嵌套。
相反,下面的代码使用递归来对应这种一样寻常性的嵌套,以便次序访问子列表:
  1. def sumtree(l):    tot = 0    for x in l:        if not isinstance(x,list):            tot += x        else:            tot += sumtree(x)    #子列复选    return tot
  2. l = [1,[2,[3,4],5],6,[7,8]]print(sumtree(l))36print(sumtree([1,[2,[3,[4,[5]]]]]))15print(sumtree([[[[[1],2],3],4],5]))15
复制代码
注意末了的案例,看看递归是怎样遍历其嵌套的列表的。只管这个例子是人为编写的,它是一类更大的步调的代表,比方,继续树和模块导入链可以展示雷同的通用布局。实际上,在背面更为实用的示例中再次使用递归的这一用法:
在第24条记的reloadall.py中,用来遍历导入链。
·在第28条记的classtree.py中,用来遍历类继续树。
·在第30条记的lister.py中,再次用来遍历类继续树。
只管出于简朴性和高服从的目标,对于线性迭代通常应该更喜好使用循环语句而不是递归,还是会发现像背面的示例一样的不可缺少递归的环境。
别的,偶然间须要意识到步调中偶然的递归的潜伏性。类中的一些运算符重载方法,比方__setattr__和__getattribute__,如果使用不精确的话,都有潜伏的大概会递归地循环。递归是一种强大的工具,但它会比预期的更好。

函数对象:属性和注解

Python函数比想象的更为机动。Python中的函数比一个编译器的代码天生规范还要多——Python函数是俯拾皆是的对象,自身全部存储在内存块中。同样,它们可以跨步调自由地转达和间接调用。它们也支持与调用根本无关的使用——属性存储和注解。
间接函数调用

由于Python函数是对象,我们可以编写通用的处理惩罚它们的步调。函数对象可以赋值给其他的名字、转达给其他函数、嵌入到数据布局、从一个函数返回给另一个函数,等等,就似乎它们是简朴的数字或字符串。
函数对象还恰恰支持一个特殊使用:它们可以由一个函数表达式背面的括号中的列表参数调用。然而,函数和其他对象一样,属于通用的范畴。
已经在前面的示例中看到了函数的这些通用应用中的一些,这里举行一个快速概览,夸大对象模子。
比方,对于用于一条def语句中的名称,没有什么特殊的:它只是当前作用域中的一个变量赋值,就似乎它出如今一个=符号的左边。在def运行之后,函数名直接是一个对象的引用——我们可以自由地把这个对象赋给其他的名称而且通过任何引用调用它:
  1. def echo(msg): print(msg)
  2. echo('hello')    #原始名称调用函数对象
  3. hello
  4. x = echo    #x也引用了函数
  5. x('hihihi')    #调用对象通过名称添加()
  6. hihihi
复制代码
由于参数通过赋值对象来转达,这就像是把函数作为参数转达给其他函数一样容易。随后,被调用者大概通过把参数添加到括号中来调用传入的函数:
  1. def indirect(func,arg): func(arg)    #通过传入对象()调用
  2. indirect(echo,'hihihi')    #将函数传给另一个函数
  3. hihihi
复制代码
以致可以把函数对象的内容填入到数据布局中,就似乎它们是整数或字符串一样。
比方,下面的步调把函数两次嵌套到一个元组列表中,作为一种动作表。由于像如许的Python复合范例可以包罗恣意范例的对象:
  1. schedule = [(echo,'hello'),(echo,'hi')]
  2. for (func,arg) in schedule: func(arg)    #容器中嵌入的调用函数
  3. hello
  4. hi
复制代码
这段代码只是遍历schedule列表,每次遍历的时间使用一个参数来调用echo函数。
在第17条记示例中所见到的,函数也可以创建并返回以便之后使用:
  1. def make(lab):
  2.     def echo(msg):
  3.         print(lab + ':' + msg)
  4.     return echo
  5. f = make('spam')
  6. f('ham')    #调用返回的函数
  7. spam:ham
  8. f('eggs')
  9. spam:eggs
复制代码
通过这些示例可以看出,Python的通用对象模式和无须范例声明使其有了非常高的机动性。
函数内省

由于函数是对象,可以用通例的对象工具来处理惩罚函数。
实际上,函数比预料的更机动。比方,一旦创建一个函数,可以像往常一样调用它:

但是,调用表达式只是界说来在函数对象上工作的一个使用。也可以通用地查抄它们的属性:

内省工具允许我们探索实现细节——比方,函数已经附加了代码对象,代码对象提供了函数的本地变量和参数等方面的细节:

这里用上面的make函数举例:
  1. make.__name__
  2. 'make'
  3. dir(make)
  4. ['__annotations__', '__builtins__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__getstate__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__type_params__']
  5. make.__code__
  6. <code object make at 0x0000019CC375EB10, file "<pyshell#79>", line 1>
  7. dir(make.__code__)
  8. ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_co_code_adaptive', '_varname_from_oparg', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_exceptiontable', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lines', 'co_linetable', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_positions', 'co_posonlyargcount', 'co_qualname', 'co_stacksize', 'co_varnames', 'replace']
  9. make.__code__.co_varnames
  10. ('lab', 'echo')
  11. make.__code__.co_argcount
  12. 1
复制代码
工具编写者可以使用这些信息来管理函数。
函数属性

函数对象不但限于前面中列出的体系界说的属性,也能向函数附加恣意的用户界说的属性:
  1. make
  2. <function make at 0x0000019CC3CDA660>
  3. make.count = 0
  4. make.count += 1
  5. make.count
  6. 1
  7. make.handles = 'hihi'
  8. make.handles
  9. 'hihi'
  10. dir(make)
  11. ['__annotations__', '__builtins__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__getstate__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__type_params__', 'count', 'handles']
复制代码
如许的属性可以用来直接把状态信息附加到函数对象,而不必使用全局、非本地和类等其他技能。
和非本地差别,如许的属性可以在函数自身的任何地方访问。从某种意义上讲,这也是模仿其他语言中的“静态本地变量”的一种方式——这种变量的名称对于一个函数来说是本地的,但是,其值在函数退出后仍然保存。属性与对象相干而不是与作用域相干,但直接结果是雷同的。
函数注解

可以给函数对象附加注解信息——与函数的参数和结果相干的恣意的用户界说的数据。
可以明确成用于提供函数的范例提示和文档阐明。‌这种范例提示可以资助开辟者更好地明确函数的使用方式,‌同时也可以使用这些信息举行一些静态范例查抄,‌从而进步代码的可读性和可维护性。‌
Python为声明注解提供了特殊的语法,但是自身不做任何事变;注解美满是可选的,而且,出现的时间只是直接附加到函数对象的__annotations__属性以供其他用户使用。
前面先容了 keyword-only参数,注解则进一步使函数头部语法通用化。思量如下的不带注解的函数,它编写为带有3个参数而且返回一个结果:
  1. def func(a,b,c): return a+b+c
  2. func(1,2,3)
  3. 6
复制代码
从语法上讲,函数注解编写在def头部行,就像与参数和返回值相干的恣意表达式一样。对于参数,它们出如今紧随参数名之后的冒号之后;对于返回值,它们编写于紧跟在参数列表之后的一个->之后。
比方,这段代码,注解了前面函数的3个参数及其返回值:
  1. def func(a:'spam',b:(1,10),c:float)->int:
  2.     return a + b + c
  3. func(1,2,3)
  4. 6
复制代码
调用一个注解过的函数,像从前一样,不外当注解出现的时间,Python将它们网络到字典中而且将它们附加给函数对象自身。参数名变成键,如果编写了返回值注解的话,它存储在键"return"下,而注解键的值则赋给了注解表达式的结果:
  1. func.__annotations__
  2. {'a': 'spam', 'b': (1, 10), 'c': <class 'float'>, 'return': <class 'int'>}
复制代码
由于注解只是附加到一个Python对象的Python对象,注解可以直接处理惩罚。下面的例子只是注解了3个参数中的两个,而且通用地遍历附加的注解:
  1. def func(a:'spam',b,c:99):
  2.     return a + b + c
  3. func(1,2,3)
  4. 6
  5. func.__annotations__
  6. {'a': 'spam', 'c': 99}
  7. for arg in func.__annotations__:
  8.     print(arg, '=>',func.__annotations__[arg])
  9.    
  10. a => spam
  11. c => 99
复制代码
值得注意有2点。起首,如果编写了注解,仍然可以对参数使用默认值——注解(及其:字符)出如今默认值(及其=字符)之前。
比方,下面的a:'spam'=4意味着参数a的默认值是4,而且用字符串'spam'注解它:
  1. def func(a: 'spam' = 4,b: (1,10) = 5, c: float = 6)->int:    return a + b + cfunc(1,2,3)6func()15func(1,c=10)16
  2. func.__annotations__
  3. {'a': 'spam', 'b': (1, 10), 'c': <class 'float'>, 'return': <class 'int'>}
复制代码
还要注意前面例子中的空格都是可选的——可以在函数头部的各部门之间使用空格,也可以不消,但省略它们对某些读者来说大概会进步代码的可读性:
  1. def func(a:'spam'=4, b:(1,10)=5, c:float=6)->int:    return a + b + cfunc(1,2,3)6
  2. func.__annotations__
  3. {'a': 'spam', 'b': (1, 10), 'c': <class 'float'>, 'return': <class 'int'>}
复制代码
解释可以用作参数范例或值的特定限定,而且较大的API大概使用这一功能作为注册函数接口信息的方式。
实际上,背面将会在第38条记中看到一个潜伏的应用,那里将看到注解作为函数装饰器参数(这是一个更为通用的概念,此中,信息编写于函数头部之外,而且由此不但限于一种用途)的一种替换方法。和Python自身一样,注解是一种功能随着你的想象来变革的工具。
末了,注意注解只在def语句中有效,在lambda表达式中无效,由于lambda的语法已经限定了它所界说的函数工具。这把我们带入到下一个主题。

匿名函数:lambda

除了def语句之外,Python还提供了一种天生函数对象的表达式情势。由于它与LISP语言中的一个工具很相似,以是称为lambda。在Python中,这实在只是一个关键词,作为引入表达式的语法而已。除了继续了数学的暗昧性,lambda比想象的要容易使用。
lambda表达式

lambda的一样寻常情势是关键字lambda,之后是一个或多个参数(与一个def头部内用括号括起来的参数列表极其相似),紧跟的是一个冒号,之后是一个表达式:

由lambda表达式所返回的函数对象与由def创建并赋值后的函数对象工作起来是完全一样的,但是lambda有一些差别之处让其在饰演特定的脚色时很有效。

  • ·lambda是一个表达式,而不是一个语句。由于这一点,lambda可以或许出如今Python语法不允许def出现的地方——比方,在一个列表常量中大概函数调用的参数中。
  • 作为一个表达式,lambda返回了一个值(一个新的函数),可以选择性地赋值给一个变量名。相反,def语句总是得在头部将一个新的函数赋值给一个变量名,而不是将这个函数作为结果返回。
  • ·lambda的主体是一个单个的表达式,而不是一个代码块。这个lambda的主体简朴得就似乎放在def主体的return语句中的代码一样。简朴地将结果写成一个顺畅的表达式,而不是明确的返回。由于它仅限于表达式,lambda通常要比def功能要小:你仅可以或许在lambda主体中封装有限的逻辑进去,连if如许的语句都不可以或许使用。这是故意计划的——它限定了步调的嵌套:lambda是一个为编写简朴的函数而计划的,而def用来处理惩罚更大的任务。
除了这些差别,def和lambda都可以或许做同样种类的工作。比方,怎样使用def语句创建函数。

但是,可以或许使用lambda表达式到达雷同的结果,通过明确地将结果赋值给一个变量名,之后就可以或许通过这个变量名调用这个函数。

这里的f被赋值给一个lambda表达式创建的函数对象。这也就是def所完成的任务,只不外def的赋值是主动举行的。
默认参数也可以或许在lambda参数中使用,就像在def中使用一样。
  1. x = lambda a='a1',b='b2',c='c3': a+b+c
  2. x()
  3. 'a1b2c3'
  4. x('ee')
  5. 'eeb2c3'
复制代码
在lambda主体中的代码想在def内的代码一样都遵照雷同的作用域查找法则。
lambda表达式引入的一个本地作用域更像一个嵌套的def语句,将会主动从上层函数中、模块中以及内置作用域中(通过LEGB法则)查找变量名。

为什么使用lambda

通常来说,lambda起到了一种函数速写的作用,允许在使用的代码内嵌入一个函数的界说。它们美满是可选的(你总是可以或许使用def来替换它们),但是在你仅须要嵌入小段可实验代码的环境下它们会带来一个更轻巧的代码布局。
比方,在稍后会看到回调处理惩罚器,它常常在一个注册调用(registration call)的参数列表中编写成单行的lambda表达式,而不是使用在文件其他地方的一个def来界说,之后引用谁人变量名。
lambda通常用来编写跳转表(jump table),也就是运动的列表或字典,可以或许按照须要实验相应的动作。如下段代码所示。
  1. l = [lambda x: x**2,
  2.     lambda x: x**3,
  3.     lambda x: x**4]
  4. for f in l: print(f(2))
  5. 4
  6. 8
  7. 16
  8. print(l[0](3))
  9. 9
复制代码
当须要把小段的可实验代码编写进def语句从语法上不能编写进的地方时,lambda表达式作为def的一种速写来说是最为有效的。
比方,这种代码片断可以通过在列表常量中嵌入lambda表达式创建一个含有三个函数的列表。一个def是不会在列表常量中工作的,由于它是一个语句,而不是一个表达式。对等的def代码大概须要在想要使用的环境之外有临时性函数名称和函数界说。下面是对等def语句:


实际上,可以用Python中的字典大概其他的数据布局来构建更多种类的运动表,从而做同样的事变。下面是另一个例子:
  1. key = 'get'
  2. {'a1': lambda:2+2, 'get': lambda:2*4, 'z9': lambda:2**6}[key]()
  3. 8
复制代码
在这里当Python创建这个字典的时间,每个嵌套的lambda都天生并留下了一个在之后可以或许调用的函数。通过键索引来取回此中一个函数,而括号使取出的函数被调用。与之前的if语句的扩展用法相比,如许编写代码可以使字典成为更加通用的多路分支工具。
如果不是用lambda做这种工作,须要使用三个文件中其他地方出现过的def语句来替换,也就是在这些函数将会使用的谁人字典外的某处须要界说这些函数。

同样会实现雷同的功能,但是def大概会出如今文件中的恣意位置,纵然它们只有很少的代码。雷同刚才lambda的代码,提供了一种特殊有效的可以在单个环境出现的函数:如果这里的三个函数不会在其他的地方使用到,那么将它们的界说作为lambda嵌入在字典中就是很公道的了。不但云云,def格式要求为这些小函数创建变量名,这些变量名大概会与这个文件中的其他变量名发生辩说(也大概不会,但总是有大概)。
lambda在函数调用参数里作为行内临时函数的界说,而且该函数在步调中不在其他地方使用时是很方便的。
怎样(不要)让Python代码变得晦涩难懂

由于lambda的主体必须是单个表达式(而不是一些语句),由此可见仅能将有限的逻辑封装到一个lambda中。如果你知道在做什么,那么你就能在Python中作为基于表达式等效的写法编写充足多的语句。
比方,如果你盼望在lambda函数中举行print,直接编写sys.stdout.write(str(x)+'\n')这个表达式,而不是使用print(x)如许的语句。
雷同地,要在一个lambda中嵌套逻辑,可以使用曾经先容过的if/else三元表达式,大概对等的但须要些本领的and/or组合。正如我们前面所相识到的,如下语句:

可以或许由以下的概括等效的表达式来模仿:

由于如许雷同的表达式可以或许放在lambda中,以是它们可以或许在lambda函数中来实现选择逻辑。
  1. lower = lambda x,y:x if x<y else y
  2. lower('bb2','a')
  3. 'a'
  4. lower('a','bb2')
  5. 'a'
复制代码
别的,如果须要在lamdba函数中实验循环,可以或许嵌入map调用或列表剖析表达式来实现。

这些本领必须在万不得已的环境下才使用。一不鉴戒,它们就会导致不可读(也称为晦涩难懂)的Python代码。
一样寻常来说,轻巧优于复杂,明确优于晦涩,而且一个完备的语句要比秘密的表达式要好。
这就是为什么lambda仅限于表达式。如果你有更复杂的代码要编写,可使用def,lambda针对较小的一段内联代码。从另一个方面来说,你也会发现适度的使用这些技能是很有效处的。
嵌套lambda和作用域

lambda是嵌套函数作用域查找的最大受益者。
比方,在下面的例子中,lambda出如今def中(很范例的环境),而且在上层函数调用的时间,嵌套的lambda可以或许获取到在上层函数作用域中的变量名x的值。

18条记中关于嵌套函数作用域的讨论没有表明的就是lambda也可以或许获取恣意上层lambda中的变量名。这种环境有些隐晦,但是想象一下,如果把上一个例子中的def换成一个lambda。

这里嵌套的lambda布局让函数在调用时创建了一个函数。无论以上哪种环境,嵌套的lambda代码都可以或许获取在上层lambda函数中的变量x。这可以工作,但是这种代码让人相称费解。出于对可读性的要求,通常来说,最好制止使用嵌套的lambda。
为什么要在意:回调

lambda的另一个常见的应用就是为Python的tkinter GUI API界说行内的回调函数。
比方,如下的代码创建了一个按钮,这个按钮在按下的时间会打印一行信息,假设tkinter在你的盘算机上可用(它在Windows和其他使用体系上是默认打开的)。

这里,回调处理惩罚器是通过转达一个用lambda所生产的函数作为command的关键字参数。与def相比lambda的优点就是处理惩罚按钮动作的代码都在这里,嵌入了按钮创建的调用中。
实际上,lambda直到事故发生时才会调用处理惩罚器实验。在按钮按下时,编写的调用才发生,而不是在按钮创建时发生。
由于嵌套的函数作用域法则对lambda也有效,它们也使回调处理惩罚器变得更简朴易用,自Python2.2之后,它们主动查找编写时地点的函数中的变量名,而且在绝大多数环境下,都不再须要传入参数默认参数。这对于获取特定的self实例参数是很方便的,这些参数是在上层的类方法函数中的本地变量(关于类的更多内容在第六部门先容)。

这里self必须要作为默认参数来传入到lambda中,详细视版本。

在序列中映射函数:map

步调对列表和其他序列常常要做的一件事就是对每一个元素举行一个使用并把其结果聚集起来。比方,在一个列表counter中更新全部的数字,可以简朴地通过一个for循环来实现。

由于这是一个云云常见的使用,Python实际上提供了一个内置的工具,为你做了大部门的工作。map函数会对一个序列对象中的每一个元素应用被传入的函数,而且返回一个包罗了全部函数调用结果的一个列表。如下所示。

之前简短地先容过map,它对一个可迭代对象中的项应用一个内置函数。
这里将会传入一个用户界说的函数来对它举行充实的使用,从而可以对列表中的每一个元素应用这个函数:map对每个列表中的元素都调用了inc函数,并将全部的返回值网络到一个新的列表中。别忘了,map在是一个可迭代对象。
由于map等待传入一个函数,它恰恰是lambda通常出现的地方之一:

这里,函数将会为counters列表中的每一个元素加3。由于这个函数不会在其他的地方用到,以是将它写成了一行的lambda。由于如许使用map与for循环是等效的,在多编写一些的代码后,你就可以或许自己编写一个一样寻常的映射工具了。

假设函数inc仍然像前面出现时那样,可以用内置函数或我们自己的对等情势将其映射到一个序列:

由于map是内置函数,它总是可用的,并总是以同样的方式工作,尚有一些性能方面的上风(简而言之,它要比自己编写的for循环更快)。
别的,map尚有比这里先容的更高级的使用方法。比方,提供了多个序列作为参数,它可以或许并行返回分别以每个序列中的元素作为函数对应参数得到的结果的列表。
  1. pow(3,4)    #3**4
  2. 81
  3. list(map(pow,[1,2,3],[2,3,4]))    #1**2 2**3 3**4
  4. [1, 8, 81]
复制代码
对于多个序列,map等待一个N参数的函数用于N序列。这里,pow函数在每次调用中都使用了两个参数:每个传入map的序列中都取一个。只管我们大概也可以或许来模仿如许做,但是当有速率上风的内置函数已经提供了如许的功能,再去模仿,意义不是很大。
注意:map调用学过的列表剖析很相似,但是map对每一个元素都应用了函数调用而不是恣意的表达式。由于这点限定,从某种意义上来说,它成为了不太通用的工具。只管云云,在某些环境下,现在map比列表剖析运行起来更快(也就是说,当映射一个内置函数时),而且它所编写的代码也较少。
函数式编程工具:filter和reduce

在Python内置函数中,map函数是用来举行函数式编程的这类工具中最简朴的内置函数代表:函数式编程的意思就是对序列应用一些函数的工具。
比方,基于某一测试函数过滤出一些元素(filter),以及对每对元素都应用函数并运行到末告终果(reduce)。由于range和filter都返回可迭代对象,它们须要list调用来表现其全部结果。
比方,下面这个filter的调用实现了从一个序列中挑选出大于0的元素。
  1. list(range(-5,5))
  2. [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]
  3. list(filter((lambda x :x>0),range(-5,5)))
  4.      
  5. [1, 2, 3, 4]
复制代码
序列中的元素若其返回值为真的话,将会被键入到结果的列表中。就像map,这个函数也可以或许概括地用一个for循环来等效,但是它也是内置的,运行比力快。

reduce在Python中则位于functools模块中,要更复杂一些。它继承一个迭代器来处理惩罚,但是,它自身不是一个迭代器,它返回一个单个的结果。这里是两个reduce调用,盘算了在一个列表中全部元素加起来的和以及乘起来的乘积。
  1. from functools import reduce
  2.      
  3. reduce((lambda x,y: x+y),[1,2,3,4])
  4.      
  5. 10
  6. reduce((lambda x,y: x*y),[1,2,3,4])
  7.      
  8. 24
复制代码
每一步,reduce转达了当前的和或乘积以及列表中下一个的元素,传给列出的lambda函数。默认,序列中的第一个元素初始化了起始值。这里是一个对第一个调用的for循环的等效,在循环中使用了额外的代码。

编写自己的reduce版本实际上相称直接。如下的函数模仿内置函数的大多数运动,而且资助阐明其一样寻常性的运作:

这个内置的reduce还允许一个可选的第三个参数放置于序列的各项之前,从而当序列为空时充当一个默认的结果。
再看看内置的operator模块,此中提供了内置表达式对应的函数,而且对于函数式工具来说,它使用起来是很方便的(要相识关于这一模块的更多内容,请参阅Python的库手册)。
  1. import operator,functools
  2. functools.reduce(operator.add,[2,4,6])
  3. 12
  4. functools.reduce((lambda x,y: x+y),[2,4,6])
  5. 12
复制代码
与map一样,filter和reduce支持了强大的函数式编程的技能。一些人也将lambda、列表剖析扩展进了Python中函数式工具会集。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表