【Python学习手册(第四版)】学习笔记19-函数的高级话题
个人总结难免疏漏,请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。
本文主要介绍函数相关的高级概念:递归函数、函数注解、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函数,或者自己编写一个更加定制化的版本。这里是用递归编写的一个定制求和函数的示例:
def mysum(l):if not l:return 0else:return l[0] + mysum(l[1:])mysum([1,2,3,4])
10
在每一层,这个函数都递归地调用自己来计算列表剩余的值的和,这个和随后加到前面的一项中。当列表变为空的时候,递归循环结束并返回0。当像这样使用递归的时候,对函数调用的每一个打开的层级,在运行时调用堆栈上都有自己的一个函数本地作用域的副本,也就是说,这意味着L在每个层级都是不同的。
如果这很难理解(并且对于新程序员来说它常常是难以理解),尝试给函数添加一个L的打印并再次运行它,从而在每个调用层级记录下当前的列表:
def mysum(l):print(l)if not l:return 0else:return l[0] + mysum(l[1:])mysum([1,2,3,4])
[1, 2, 3, 4]
[2, 3, 4]
[3, 4]
[4]
[]
10
在每个递归层级上,要加和的列表变得越来越小,直到它变为空——递归循环结束。加和随着递归调用的展开而计算出来。这里实际就是把每次递归的第一个偏移给求和,因为每次都会返回L[0]。
替代方案
也可以使用Python的三元if/else表达式,也可以针对任何可加和的类型一般化(如果我们至少假设输入中的一项的话,这将会变得较容易些就像在第18笔记最小最大值的示例),并且使用扩展序列赋值来使得第一个/其他的解包更简单:

例子中的后两个由于空的列表而失败,但是考虑到支持+的任何对象类型的序列,而不只是数字:
mysum([1]) #第二个会失败 如果mysum([])
1mysum(['s','p','a','m'])
'spam'mysum(['spam','ham','eggs'])
'spamhameggs'
研究这3个变体,将会发现,后两者在一个单个字符串参数上也有效(例如,mysum('spam')),因为字符串是一字符的字符串的序列;第三种变体在任意可迭代对象上都有效,包括打开的输入文件,但是,其他的两种不会有效,因为它们索引;并且函数头部def mysum(first,*rest)尽管类似于第三种变体,但根本没法工作,因为它期待单个参数,而不是一个单独的可迭代对象。
归是可以是直接的,就像目前为止给出的例子一样;也可以是间接的,就像下面的例子一样(一个函数调用另一个函数,后者反过来调用其调用者)。直接的效果是相同的,尽管这在每个层级有两个函数调用:
def mysum(l):if not l: return 0return nonempty(l)def nonempty(l): return l[0] + mysum(l[1:])mysum([1.1,2.2,3.3,4.4])
11.0
循环语句VS递归
递归对于上一小节的求和的例子有效,但在那种环境中可能过于追求技巧。
实际上,递归在Python中并不像在Prolog或Lisp这样更加深奥的语言中那样常用,因为Python强调像循环这样的简单的过程式语句,循环语句通常更为自然。
例如,while常常使得事情更为具体一些,并且它不需要定义一个支持递归调用的函数:
l = [1,2,3,4]
sum = 0
while l:sum += l[0]l = l[1:]sum
10
以及可以使用for循环为我们自动迭代,使得递归在大多数情况下不必使用(并且,很可能,递归在内存空间和执行时间方面效率较低):
l = [1,2,3,4]
sum = 0
for x in l: sum +=xsum
10
有了循环语句,不需要在调用堆栈上针对每次迭代都有一个本地作用域的副本,并且避免了一般会与函数调用相关的速度成本。
处理任意结构
另一方面,递归(或者对等的显式的基于堆栈的算法)可以要求遍历任意形状的结构。
作为递归在这种环境中的应用的一个简单例子,考虑像下面的任务:计算一个嵌套的子列表结构中所有数字的总和:
l = [1,[2,[3,4],5],6,[7,8]]
简单的循环语句在这里不起作用,因为这不是一个线性迭代。嵌套的循环语句也不够用,因为子列表可能嵌套到任意的深度并且以任意的形式嵌套。
相反,下面的代码使用递归来对应这种一般性的嵌套,以便顺序访问子列表:
def sumtree(l):tot = 0for x in l:if not isinstance(x,list):tot += xelse:tot += sumtree(x) #子列复选return totl = [1,[2,[3,4],5],6,[7,8]]
print(sumtree(l))
36print(sumtree([1,[2,[3,[4,[5]]]]]))
15
print(sumtree([[[[[1],2],3],4],5]))
15
留意末尾的案例,看看递归是如何遍历其嵌套的列表的。尽管这个例子是人为编写的,它是一类更大的程序的代表,例如,继承树和模块导入链可以展示类似的通用结构。实际上,在后面更为实用的示例中再次使用递归的这一用法:
在第24笔记的reloadall.py中,用来遍历导入链。
·在第28笔记的classtree.py中,用来遍历类继承树。
·在第30笔记的lister.py中,再次用来遍历类继承树。
尽管出于简单性和高效率的目的,对于线性迭代通常应该更喜欢使用循环语句而不是递归,还是会发现像后面的示例一样的不可缺少递归的情况。
此外,有时候需要意识到程序中无意的递归的潜在性。类中的一些运算符重载方法,例如__setattr__和__getattribute__,如果使用不正确的话,都有潜在的可能会递归地循环。递归是一种强大的工具,但它会比预期的更好。
函数对象:属性和注解
Python函数比想象的更为灵活。Python中的函数比一个编译器的代码生成规范还要多——Python函数是俯拾皆是的对象,自身全部存储在内存块中。同样,它们可以跨程序自由地传递和间接调用。它们也支持与调用根本无关的操作——属性存储和注解。
间接函数调用
由于Python函数是对象,我们可以编写通用的处理它们的程序。函数对象可以赋值给其他的名字、传递给其他函数、嵌入到数据结构、从一个函数返回给另一个函数,等等,就好像它们是简单的数字或字符串。
函数对象还恰好支持一个特殊操作:它们可以由一个函数表达式后面的括号中的列表参数调用。然而,函数和其他对象一样,属于通用的领域。
已经在前面的示例中看到了函数的这些通用应用中的一些,这里进行一个快速概览,强调对象模型。
例如,对于用于一条def语句中的名称,没有什么特别的:它只是当前作用域中的一个变量赋值,就好像它出现在一个=符号的左边。在def运行之后,函数名直接是一个对象的引用——我们可以自由地把这个对象赋给其他的名称并且通过任何引用调用它:
def echo(msg): print(msg)echo('hello') #原始名称调用函数对象
hellox = echo #x也引用了函数
x('hihihi') #调用对象通过名称添加()
hihihi
由于参数通过赋值对象来传递,这就像是把函数作为参数传递给其他函数一样容易。随后,被调用者可能通过把参数添加到括号中来调用传入的函数:
def indirect(func,arg): func(arg) #通过传入对象()调用indirect(echo,'hihihi') #将函数传给另一个函数
hihihi
甚至可以把函数对象的内容填入到数据结构中,就好像它们是整数或字符串一样。
例如,下面的程序把函数两次嵌套到一个元组列表中,作为一种动作表。由于像这样的Python复合类型可以包含任意类型的对象:
schedule = [(echo,'hello'),(echo,'hi')]
for (func,arg) in schedule: func(arg) #容器中嵌入的调用函数hello
hi
这段代码只是遍历schedule列表,每次遍历的时候使用一个参数来调用echo函数。
在第17笔记示例中所见到的,函数也可以创建并返回以便之后使用:
def make(lab):def echo(msg):print(lab + ':' + msg)return echof = make('spam')
f('ham') #调用返回的函数
spam:ham
f('eggs')
spam:eggs
通过这些示例可以看出,Python的通用对象模式和无须类型声明使其有了非常高的灵活性。
函数内省
由于函数是对象,可以用常规的对象工具来处理函数。
实际上,函数比预料的更灵活。例如,一旦创建一个函数,可以像往常一样调用它:

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

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

这里用上面的make函数举例:
make.__name__
'make'dir(make)
['__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__']make.__code__
<code object make at 0x0000019CC375EB10, file "<pyshell#79>", line 1>dir(make.__code__)
['__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']make.__code__.co_varnames
('lab', 'echo')make.__code__.co_argcount
1
工具编写者可以利用这些信息来管理函数。
函数属性
函数对象不仅限于前面中列出的系统定义的属性,也能向函数附加任意的用户定义的属性:
make
<function make at 0x0000019CC3CDA660>make.count = 0
make.count += 1
make.count
1make.handles = 'hihi'
make.handles
'hihi'dir(make)
['__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个参数并且返回一个结果:
def func(a,b,c): return a+b+cfunc(1,2,3)
6
从语法上讲,函数注解编写在def头部行,就像与参数和返回值相关的任意表达式一样。对于参数,它们出现在紧随参数名之后的冒号之后;对于返回值,它们编写于紧跟在参数列表之后的一个->之后。
例如,这段代码,注解了前面函数的3个参数及其返回值:
def func(a:'spam',b:(1,10),c:float)->int:return a + b + cfunc(1,2,3)
6
调用一个注解过的函数,像以前一样,不过当注解出现的时候,Python将它们收集到字典中并且将它们附加给函数对象自身。参数名变成键,如果编写了返回值注解的话,它存储在键"return"下,而注解键的值则赋给了注解表达式的结果:
func.__annotations__
{'a': 'spam', 'b': (1, 10), 'c': <class 'float'>, 'return': <class 'int'>}
由于注解只是附加到一个Python对象的Python对象,注解可以直接处理。下面的例子只是注解了3个参数中的两个,并且通用地遍历附加的注解:
def func(a:'spam',b,c:99):return a + b + cfunc(1,2,3)
6func.__annotations__
{'a': 'spam', 'c': 99}for arg in func.__annotations__:print(arg, '=>',func.__annotations__[arg])a => spam
c => 99
值得注意有2点。首先,如果编写了注解,仍然可以对参数使用默认值——注解(及其:字符)出现在默认值(及其=字符)之前。
例如,下面的a:'spam'=4意味着参数a的默认值是4,并且用字符串'spam'注解它:
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)
16func.__annotations__
{'a': 'spam', 'b': (1, 10), 'c': <class 'float'>, 'return': <class 'int'>}
还要注意前面例子中的空格都是可选的——可以在函数头部的各部分之间使用空格,也可以不用,但省略它们对某些读者来说可能会提高代码的可读性:
def func(a:'spam'=4, b:(1,10)=5, c:float=6)->int:return a + b + cfunc(1,2,3)
6func.__annotations__
{'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中使用一样。
x = lambda a='a1',b='b2',c='c3': a+b+c
x()
'a1b2c3'
x('ee')
'eeb2c3'
在lambda主体中的代码想在def内的代码一样都遵循相同的作用域查找法则。
lambda表达式引入的一个本地作用域更像一个嵌套的def语句,将会自动从上层函数中、模块中以及内置作用域中(通过LEGB法则)查找变量名。

为什么使用lambda
通常来说,lambda起到了一种函数速写的作用,允许在使用的代码内嵌入一个函数的定义。它们完全是可选的(你总是能够使用def来替代它们),但是在你仅需要嵌入小段可执行代码的情况下它们会带来一个更简洁的代码结构。
例如,在稍后会看到回调处理器,它常常在一个注册调用(registration call)的参数列表中编写成单行的lambda表达式,而不是使用在文件其他地方的一个def来定义,之后引用那个变量名。
lambda通常用来编写跳转表(jump table),也就是行为的列表或字典,能够按照需要执行相应的动作。如下段代码所示。
l = [lambda x: x**2, lambda x: x**3, lambda x: x**4]for f in l: print(f(2))4
8
16print(l[0](3))
9
当需要把小段的可执行代码编写进def语句从语法上不能编写进的地方时,lambda表达式作为def的一种速写来说是最为有用的。
例如,这种代码片段可以通过在列表常量中嵌入lambda表达式创建一个含有三个函数的列表。一个def是不会在列表常量中工作的,因为它是一个语句,而不是一个表达式。对等的def代码可能需要在想要使用的环境之外有临时性函数名称和函数定义。下面是对等def语句:

实际上,可以用Python中的字典或者其他的数据结构来构建更多种类的行为表,从而做同样的事情。下面是另一个例子:
key = 'get'{'a1': lambda:2+2, 'get': lambda:2*4, 'z9': lambda:2**6}[key]()
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函数中来实现选择逻辑。
lower = lambda x,y:x if x<y else ylower('bb2','a')
'a'
lower('a','bb2')
'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还有比这里介绍的更高级的使用方法。例如,提供了多个序列作为参数,它能够并行返回分别以每个序列中的元素作为函数对应参数得到的结果的列表。
pow(3,4) #3**4
81list(map(pow,[1,2,3],[2,3,4])) #1**2 2**3 3**4
[1, 8, 81]
对于多个序列,map期待一个N参数的函数用于N序列。这里,pow函数在每次调用中都使用了两个参数:每个传入map的序列中都取一个。尽管我们大概也能够来模拟这样做,但是当有速度优势的内置函数已经提供了这样的功能,再去模拟,意义不是很大。
注意:map调用学过的列表解析很相似,但是map对每一个元素都应用了函数调用而不是任意的表达式。因为这点限制,从某种意义上来说,它成为了不太通用的工具。尽管如此,在某些情况下,目前map比列表解析运行起来更快(也就是说,当映射一个内置函数时),并且它所编写的代码也较少。
函数式编程工具:filter和reduce
在Python内置函数中,map函数是用来进行函数式编程的这类工具中最简单的内置函数代表:函数式编程的意思就是对序列应用一些函数的工具。
例如,基于某一测试函数过滤出一些元素(filter),以及对每对元素都应用函数并运行到最后结果(reduce)。由于range和filter都返回可迭代对象,它们需要list调用来显示其所有结果。
例如,下面这个filter的调用实现了从一个序列中挑选出大于0的元素。
list(range(-5,5))
[-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]list(filter((lambda x :x>0),range(-5,5)))[1, 2, 3, 4]
序列中的元素若其返回值为真的话,将会被键入到结果的列表中。就像map,这个函数也能够概括地用一个for循环来等效,但是它也是内置的,运行比较快。

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

编写自己的reduce版本实际上相当直接。如下的函数模拟内置函数的大多数行为,并且帮助说明其一般性的运作:

这个内置的reduce还允许一个可选的第三个参数放置于序列的各项之前,从而当序列为空时充当一个默认的结果。
再看看内置的operator模块,其中提供了内置表达式对应的函数,并且对于函数式工具来说,它使用起来是很方便的(要了解关于这一模块的更多内容,请参阅Python的库手册)。
import operator,functools
functools.reduce(operator.add,[2,4,6])
12
functools.reduce((lambda x,y: x+y),[2,4,6])
12
与map一样,filter和reduce支持了强大的函数式编程的技术。一些人也将lambda、列表解析扩展进了Python中函数式工具集中。
相关文章:
【Python学习手册(第四版)】学习笔记19-函数的高级话题
个人总结难免疏漏,请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。 本文主要介绍函数相关的高级概念:递归函数、函数注解、lambda表达式函数,常用函数工具如map、filter、reduce,以及通用的函数设…...
Selenium + Python 自动化测试11(unittest组织用例)
我们的目标是:按照这一套资料学习下来,大家可以独立完成自动化测试的任务。 上一篇我们讨论了unittest基本使用方法。 本篇文章我们接着讲。一些概念和一些常用的构造测试集的方法。 1、基本概念 1)Test Case 一个Test Case的实例就是一个测…...
【唐氏题目 nt题】与众不同
# 与众不同 ## 题目描述 A是某公司的CEO,每个月都会有员工把公司的盈利数据送给A,A是个与众不同的怪人,A不注重盈利还是亏本,而是喜欢研究「完美序列」:一段连续的序列满足序列中的数互不相同。 A想知道区间[L,R]之…...
2000块的活嫌低?这个 6 位数的项目,你可不能错过哟!
2000块钱嫌低?这个6位数的项目,你可不能错过,关注有好礼。 最近写了一篇“接了一个2000块钱的活,大家看看值不值”的文章,发现流量和大家互动的热情出奇的高,可能是跟有钱有关的缘故,大家不是奔…...
【Postman工具】
一.接口扫盲 1.什么是接口? 接口是系统之间数据交互的通道。拿小红到沙县点餐为例:小红想吃鸭腿饭。她要用什么语言来表达?跟谁表达?通过什么表达?按照生活习惯应该是:小红根据菜单对服务员用中文表达她想要…...
全网超详细攻略-从入门到精通haproxy七层代理
目录 一.haproxy概述 1.1 haproxy简介 1.2 haproxy的主要特性 1.3 haproxy的优缺点 二.负载均衡介绍 2.1 什么是负载均衡 2.2 为什么用负载均衡 2.3 负载均衡类型 2.3.1 四层负载均衡 2.3.2 七层负载均衡 2.3.3 四层和七层的区别 三.haproxy的安装及服务 3.1 实验环…...
AI编程辅助工具:CodeGeeX 插件使用
CodeGeeX 插件使用 前言1.支持的平台2.安装步骤3.启用插件4.代码生成5.代码优化 前言 CodeGeeX 是一款基于 AI 技术的编程助手插件,旨在帮助开发者提高编程效率和代码质量。它能够智能生成代码、优化现有代码、自动生成文档以及回答编程相关的问题。无论您是初学者…...
sql注入实战——thinkPHP
sql注入实战——thinkPHP sql注入实战——thinkPHPthinkPHP前期环境搭建创建数据库开始寻找漏洞点输入SQL注入语句漏洞分析 实验错误 sql注入实战——thinkPHP thinkPHP前期环境搭建 下载thinkPHP文件 解压,将framework关键文件放到think-5.0.15中,改…...
MySQL 迁移 OceanBase 的 Oracle模式中,实现自增主键的方法
本文作者:赵黎明,爱可生 MySQL DBA 团队成员,熟练掌握Oracle、MySQL等数据库系统,擅长对数据库性能问题的诊断,以及事务与锁机制的分析等。负责解决客户在MySQL及爱可生自主研发的DMP平台日常运维中所遇到的各种问题&a…...
【C++ 面试 - 基础题】每日 3 题(十一)
✍个人博客:Pandaconda-CSDN博客 📣专栏地址:http://t.csdnimg.cn/fYaBd 📚专栏简介:在这个专栏中,我将会分享 C 面试中常见的面试题给大家~ ❤️如果有收获的话,欢迎点赞👍收藏&…...
ESP8266在线升级OTA固件
OTA的基本实现方式: ESP8266 的 OTA 实现有几种方式,常用的方式包括: 1、Arduino OTA:使用Arduino IDE提供的OTA功能,可以直接通过Arduino IDE上传固件到ESP8266。 2、Web OTA:ESP8266运行一个简易的Web服…...
精通C++ STL(六):list的模拟实现
目录 类及其成员函数接口总览 结点类的模拟实现 构造函数 迭代器类的模拟实现 迭代器类存在的意义 迭代器类的模板参数说明 构造函数 运算符的重载 --运算符的重载 运算符的重载 !运算符的重载 *运算符的重载 ->运算符的重载 list的模拟实现 默认成员函数 构造函数 拷贝…...
《雅思口语真经总纲1.0》话题实战训练笔记part1——6. Music
《雅思口语真经总纲1.0》笔记——第四章:口语素材大全(part1、part2、part3回答准则及练习方法,不包括范例答案)★★★★★ 文章目录 MusicWhen do you listen to music?20240804答评价注意事项1、在说到“no music”时ÿ…...
Python之赋值语句(多重赋值和交换赋值)
这是《Python入门经典以解决计算问题为导向的Python编程实践》73-74页关于赋值的内容。讲了Python中几种赋值方式。 赋值语句 1、最简单的赋值:ab2、多重赋值:a,b,c1,2,33、交换:a,bb,a 1、最简单的赋值:ab b可以是数字、字符串…...
网络协议七 应用层 HTTP 协议
应用层常见的协议 HTTP协议 1. 如何查看我们的http 协议全部的内容有哪些呢? 一种合理的方法是 通过 wireshark 软件,找到想要查看的HTTP --->追踪流--->HTTP流 来查看 结果如下:红色部分 为 发送给服务器的,蓝色部分为服务…...
uniapp vue 在适配百度小程序平台动态:style
uniapp vue 在适配百度小程序平台动态:style踩坑报错Unexpected string concatenation of literals 抖快平台动态style写法基本是 <view :style"{width: 686rpx, height: (setHeight 96) rpx}"> </view>这种写法在百度上会又解析报错: Une…...
【最小生成树】(二) Kruskal 算法
题目: 寻宝 题目描述 在世界的某个区域,有一些分散的神秘岛屿,每个岛屿上都有一种珍稀的资源或者宝藏。国王打算在这些岛屿上建公路,方便运输。 不同岛屿之间,路途距离不同,国王希望你可以规划建公路的方案…...
haproxy最强攻略
1、负载均衡 负载均衡(Load Balance,简称 LB)是高并发、高可用系统必不可少的关键组件,目标是 尽力将网络流量平均分发到多个服务器上,以提高系统整体的响应速度和可用性。 负载均衡的主要作用如下: 高并发…...
XetHub 加入 Hugging Face!
我们非常激动地正式宣布,Hugging Face 已收购 XetHub 🔥 XetHub 是一家位于西雅图的公司,由 Yucheng Low、Ajit Banerjee 和 Rajat Arya 创立,他们之前在 Apple 工作,构建和扩展了 Apple 的内部机器学习基础设施。XetH…...
在编程学习的海洋中,如何打造高效的知识宝库
目录 在编程学习的海洋中,如何打造高效的知识宝库一、笔记记录的重要性:为知识设立灯塔二、快速记录的策略:抓住知识的核心三、系统化的整理:构建个人知识体系四、实用工具推荐:为知识管理添砖加瓦五、保持条理性的秘诀…...
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...
srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
VM虚拟机网络配置(ubuntu24桥接模式):配置静态IP
编辑-虚拟网络编辑器-更改设置 选择桥接模式,然后找到相应的网卡(可以查看自己本机的网络连接) windows连接的网络点击查看属性 编辑虚拟机设置更改网络配置,选择刚才配置的桥接模式 静态ip设置: 我用的ubuntu24桌…...
return this;返回的是谁
一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请,不同级别的经理有不同的审批权限: // 抽象处理者:审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...
LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...
RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...
Python Einops库:深度学习中的张量操作革命
Einops(爱因斯坦操作库)就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库,用类似自然语言的表达式替代了晦涩的API调用,彻底改变了深度学习工程…...
Git常用命令完全指南:从入门到精通
Git常用命令完全指南:从入门到精通 一、基础配置命令 1. 用户信息配置 # 设置全局用户名 git config --global user.name "你的名字"# 设置全局邮箱 git config --global user.email "你的邮箱example.com"# 查看所有配置 git config --list…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...
绕过 Xcode?使用 Appuploader和主流工具实现 iOS 上架自动化
iOS 应用的发布流程一直是开发链路中最“苹果味”的环节:强依赖 Xcode、必须使用 macOS、各种证书和描述文件配置……对很多跨平台开发者来说,这一套流程并不友好。 特别是当你的项目主要在 Windows 或 Linux 下开发(例如 Flutter、React Na…...
