【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函数,或者自己编写一个更加定制化的版本。这里是用递归编写的一个定制求和函数的示例:

python 复制代码
def mysum(l):
    if not l:
        return 0
    else:
        return l[0] + mysum(l[1:])

    
mysum([1,2,3,4])
10

在每一层,这个函数都递归地调用自己来计算列表剩余的值的和,这个和随后加到前面的一项中。当列表变为空的时候,递归循环结束并返回0。当像这样使用递归的时候,对函数调用的每一个打开的层级,在运行时调用堆栈上都有自己的一个函数本地作用域的副本,也就是说,这意味着L在每个层级都是不同的。

如果这很难理解(并且对于新程序员来说它常常是难以理解),尝试给函数添加一个L的打印并再次运行它,从而在每个调用层级记录下当前的列表:

python 复制代码
def mysum(l):
    print(l)
    if not l:
        return 0
    else:
        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笔记最小最大值的示例),并且使用扩展序列赋值来使得第一个/其他的解包更简单:

例子中的后两个由于空的列表而失败,但是考虑到支持+的任何对象类型的序列,而不只是数字:

python 复制代码
mysum([1])    #第二个会失败 如果mysum([]) 
1

mysum(['s','p','a','m'])
'spam'

mysum(['spam','ham','eggs'])
'spamhameggs'

研究这3个变体,将会发现,后两者在一个单个字符串参数上也有效(例如,mysum('spam')),因为字符串是一字符的字符串的序列;第三种变体在任意可迭代对象上都有效,包括打开的输入文件,但是,其他的两种不会有效,因为它们索引;并且函数头部def mysum(first,*rest)尽管类似于第三种变体,但根本没法工作,因为它期待单个参数,而不是一个单独的可迭代对象。

归是可以是直接的,就像目前为止给出的例子一样;也可以是间接的,就像下面的例子一样(一个函数调用另一个函数,后者反过来调用其调用者)。直接的效果是相同的,尽管这在每个层级有两个函数调用:

python 复制代码
def mysum(l):
    if not l: return 0
    return 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常常使得事情更为具体一些,并且它不需要定义一个支持递归调用的函数:

python 复制代码
l = [1,2,3,4]
sum = 0
while l:
    sum += l[0]
    l = l[1:]

    
sum
10

以及可以使用for循环为我们自动迭代,使得递归在大多数情况下不必使用(并且,很可能,递归在内存空间和执行时间方面效率较低):

python 复制代码
l = [1,2,3,4]
sum = 0
for x in l: sum +=x

sum
10

有了循环语句,不需要在调用堆栈上针对每次迭代都有一个本地作用域的副本,并且避免了一般会与函数调用相关的速度成本。

处理任意结构

另一方面,递归(或者对等的显式的基于堆栈的算法)可以要求遍历任意形状的结构。

作为递归在这种环境中的应用的一个简单例子,考虑像下面的任务:计算一个嵌套的子列表结构中所有数字的总和:

python 复制代码
l = [1,[2,[3,4],5],6,[7,8]]

简单的循环语句在这里不起作用,因为这不是一个线性迭代 。嵌套的循环语句也不够用,因为子列表可能嵌套到任意的深度并且以任意的形式嵌套

相反,下面的代码使用递归来对应这种一般性的嵌套,以便顺序访问子列表:

python 复制代码
def sumtree(l):
    tot = 0
    for x in l:
        if not isinstance(x,list):
            tot += x
        else:
            tot += sumtree(x)    #子列复选
    return tot

l = [1,[2,[3,4],5],6,[7,8]]
print(sumtree(l))
36

print(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运行之后,函数名直接是一个对象的引用 ------我们可以自由地把这个对象赋给其他的名称并且通过任何引用调用它

python 复制代码
def echo(msg): print(msg)

echo('hello')    #原始名称调用函数对象
hello

x = echo    #x也引用了函数
x('hihihi')    #调用对象通过名称添加()
hihihi

由于参数通过赋值对象来传递, 这就像是把函数作为参数传递给其他函数 一样容易。随后,被调用者可能通过把参数添加到括号中来调用传入的函数

python 复制代码
def indirect(func,arg): func(arg)    #通过传入对象()调用

indirect(echo,'hihihi')    #将函数传给另一个函数
hihihi

甚至可以把函数对象的内容填入到数据结构中,就好像它们是整数或字符串一样。

例如,下面的程序把函数两次嵌套到一个元组列表中,作为一种动作表。由于像这样的Python复合类型可以包含任意类型的对象:

python 复制代码
schedule = [(echo,'hello'),(echo,'hi')]
for (func,arg) in schedule: func(arg)    #容器中嵌入的调用函数

hello
hi

这段代码只是遍历schedule列表,每次遍历的时候使用一个参数来调用echo函数。

在第17笔记示例中所见到的,函数也可以创建并返回以便之后使用:

python 复制代码
def make(lab):
    def echo(msg):
        print(lab + ':' + msg)
    return echo

f = make('spam')
f('ham')    #调用返回的函数
spam:ham
f('eggs')
spam:eggs

通过这些示例可以看出,Python的通用对象模式和无须类型声明使其有了非常高的灵活性。

函数内省

由于函数是对象,可以用常规的对象工具来处理函数。

实际上,函数比预料的更灵活。例如,一旦创建一个函数,可以像往常一样调用它:

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

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

这里用上面的make函数举例:

python 复制代码
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

工具编写者可以利用这些信息来管理函数。

函数属性

函数对象不仅限于前面中列出的系统定义的属性,也能向函数附加任意的用户定义的属性

python 复制代码
make
<function make at 0x0000019CC3CDA660>

make.count = 0
make.count += 1
make.count
1

make.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个参数并且返回一个结果:

python 复制代码
def func(a,b,c): return a+b+c

func(1,2,3)
6

从语法上讲,函数注解编写在def头部行,就像与参数和返回值相关的任意表达式一样。对于参数,它们出现在紧随参数名之后的冒号之后;对于返回值,它们编写于紧跟在参数列表之后的一个->之后。

例如,这段代码,注解了前面函数的3个参数及其返回值:

python 复制代码
def func(a:'spam',b:(1,10),c:float)->int:
    return a + b + c

func(1,2,3)
6

调用一个注解过的函数,像以前一样,不过当注解出现的时候,Python将它们收集到字典中并且将它们附加给函数对象自身。 参数名变成键,如果编写了返回值注解的话,它存储在键"return"下,而注解键的值则赋给了注解表达式的结果

python 复制代码
func.__annotations__
{'a': 'spam', 'b': (1, 10), 'c': <class 'float'>, 'return': <class 'int'>}

由于注解只是附加到一个Python对象的Python对象,注解可以直接处理。下面的例子只是注解了3个参数中的两个,并且通用地遍历附加的注解:

python 复制代码
def func(a:'spam',b,c:99):
    return a + b + c

func(1,2,3)
6

func.__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'注解它:

python 复制代码
def func(a: 'spam' = 4,b: (1,10) = 5, c: float = 6)->int:
    return a + b + c

func(1,2,3)
6

func()
15

func(1,c=10)
16

func.__annotations__
{'a': 'spam', 'b': (1, 10), 'c': <class 'float'>, 'return': <class 'int'>}

还要注意前面例子中的空格都是可选的------可以在函数头部的各部分之间使用空格,也可以不用,但省略它们对某些读者来说可能会提高代码的可读性:

python 复制代码
def func(a:'spam'=4, b:(1,10)=5, c:float=6)->int:
    return a + b + c

func(1,2,3)
6

func.__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中使用一样。

python 复制代码
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),也就是行为的列表或字典,能够按照需要执行相应的动作。**如下段代码所示。

python 复制代码
l = [lambda x: x**2, 
    lambda x: x**3, 
    lambda x: x**4]

for f in l: print(f(2))

4
8
16

print(l[0](3))
9

当需要把小段的可执行代码编写进def语句从语法上不能编写进的地方时,lambda表达式作为def的一种速写来说是最为有用的。

例如,这种代码片段可以通过在列表常量中嵌入lambda表达式创建一个含有三个函数的列表。一个def是不会在列表常量中工作的,因为它是一个语句,而不是一个表达式。对等的def代码可能需要在想要使用的环境之外有临时性函数名称和函数定义。下面是对等def语句:

实际上,可以用Python中的字典或者其他的数据结构来构建更多种类的行为表,从而做同样的事情。下面是另一个例子:

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函数中来实现选择逻辑。

python 复制代码
lower = lambda x,y:x if x<y else y

lower('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还有比这里介绍的更高级的使用方法。例如,提供了多个序列作为参数,它能够并行返回分别以每个序列中的元素作为函数对应参数得到的结果的列表

python 复制代码
pow(3,4)    #3**4
81

list(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的元素。

python 复制代码
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调用,计算了在一个列表中所有元素加起来的和以及乘起来的乘积。

python 复制代码
from functools import reduce
     
reduce((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的库手册)。

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中函数式工具集中。

相关推荐
数据智能老司机42 分钟前
精通 Python 设计模式——分布式系统模式
python·设计模式·架构
数据智能老司机2 小时前
精通 Python 设计模式——并发与异步模式
python·设计模式·编程语言
数据智能老司机2 小时前
精通 Python 设计模式——测试模式
python·设计模式·架构
数据智能老司机2 小时前
精通 Python 设计模式——性能模式
python·设计模式·架构
c8i2 小时前
drf初步梳理
python·django
每日AI新事件2 小时前
python的异步函数
python
使一颗心免于哀伤2 小时前
《设计模式之禅》笔记摘录 - 21.状态模式
笔记·设计模式
这里有鱼汤3 小时前
miniQMT下载历史行情数据太慢怎么办?一招提速10倍!
前端·python
databook12 小时前
Manim实现脉冲闪烁特效
后端·python·动效
程序设计实验室13 小时前
2025年了,在 Django 之外,Python Web 框架还能怎么选?
python