在之前的课程中,我们讲到过关于函数的知识,我们还讲到过Python中常用的数据类型,这些类型的变量都可以作为函数的参数或返回值。如果我们把这些知识汇总一下,我们的函数就可以做更多的事情。
高阶函数的用法
Python中任意一个函数都有数据类型,这种数据类型是function
,被称为函数类型。函数类型的数据和其他类型的数据是一样的,任意类型的数据都可以作为函数返回值使用,还可以作为函数参数使用。这就意味着函数本身也可以作为函数的参数或返回值,这就是所谓的高阶函数。
如果我们希望上面的calc
函数不仅仅可以做多个参数求和,还可以做多个参数求乘积甚至更多的二元运算,我们就可以使用高阶函数的方式来改写上面的代码,将加法运算从函数中移除掉,具体的做法如下所示。
1 | def calc(*args, init_value, op, **kwargs): |
注意,上面的函数增加了两个参数,其中init_value
代表运算的初始值,op
代表二元运算函数。经过改造的calc
函数不仅仅可以实现多个参数的累加求和,也可以实现多个参数的累乘运算,代码如下所示。
1 | def add(x, y): |
通过对高阶函数的运用,calc
函数不再和加法运算耦合,所以灵活性和通用性会变强,这是编程中一种常用的技巧,但是最初学者来说可能会稍微有点难以理解。需要注意的是,将函数作为参数和调用函数是有显著的区别的,调用函数需要在函数名后面跟上圆括号,而把函数作为参数时只需要函数名即可。上面的代码也可以不用定义add
和mul
函数,因为Python标准库中的operator
模块提供了代表加法运算的add
和代表乘法运算的mul
函数,我们直接使用即可,代码如下所示。
1 | import operator |
Python内置函数中有不少高阶函数,我们前面提到过的filter
和map
函数就是高阶函数,前者可以实现对序列中元素的过滤,后者可以实现对序列中元素的映射,例如我们要去掉一个整数列表中的奇数,并对所有的偶数求平方得到一个新的列表,就可以直接使用这两个函数来做到,具体的做法是如下所示。
1 | def is_even(num): |
当然,要完成上面代码的功能,也可以使用列表生成式,列表生成式的做法更为简单优雅。
1 | numbers1 = [35, 12, 8, 99, 60, 52] |
过滤函数filter()
Python中定义了一些用于数据处理的函数,如filter()
和map()
等。我们先介绍filter
函数。
filter()
函数用于对容器中的元素进行过滤处理。filter()
函数的语法如下:
1 | filter(function or None, iterable) --> filter object |
- 参数
function
是一个提供过滤条件的函数,返回布尔值,可以为None。 - 参数
iterable
是容器类型的数据。
在调用filter()
函数时,iterable
会被遍历,他的元素会被逐一传入function()函数中。function()函数若返回True,则元素被保留;返回False,则元素被过滤。最后遍历完成,已保留的元素被放到一个新的容器数据中,容器类型为迭代器(后面会讲)。
1 | # 提供过滤条件的函数 |
注意:filter()
函数的返回值并不是一个列表,如果需要返回列表类型的数据,还需要通过list()
函数进行转换。
补充:我们可以用列表生成式或列表生成器实现filter()
函数同样的功能。
映射函数map()
map()
函数用于对容器中的元素进行映射(或变换)。例如:我想讲列表中所有元素都乘以2,返回新的列表。
map()
函数的语法如下:
1 | map(function, iterable) |
- 参数
function
是一个提供变换规则的函数,返回变换之后的元素。 - 参数
iterable
是容器类型的数据。
在调用map()
函数时,iterable
会被遍历,他的元素会被逐一传入function()函数中。function()函数对元素进行变换,最后遍历完成,变换后的元素被放到一个新的容器数据中,容器类型为迭代器(后面会讲)。
1 | # 提供变换规则的函数 |
补充:我们可以用列表生成式或列表生成器实现filter()
函数同样的功能。
reduce函数
对一个序列进行压缩运算,得到一个值。但是reduce在python2的时候是内置函数,到了python3移到了functools模块,所以使用之前需要 from functools import reduce
。
reduce()
函数的语法如下:
1 | reduce(function,iterable) |
- 参数
function
是一个对于两个元素进行运算的函数。 - 参数
iterable
是容器类型的数据。
数将一个数据集合(链表,元组等)中的所有数据进行下列操作:
- function(有两个参数)先对容器中的第 1、2 个元素进行操作,得到的结果再与第三个数进行操作。
- 按照此规则用 function 函数运算,最后得到一个结果。
1 | def kfactorial(k): |
Lambda函数
在使用高阶函数的时候,如果作为参数或者返回值的函数本身非常简单,一行代码就能够完成,那么我们可以使用Lambda函数来表示。Python中的Lambda函数是没有的名字函数,所以很多人也把它叫做匿名函数,匿名函数只能有一行代码,代码中的表达式产生的运算结果就是这个匿名函数的返回值。上面代码中的is_even
和square
函数都只有一行代码,我们可以用Lambda函数来替换掉它们,代码如下所示。
1 | numbers1 = [35, 12, 8, 99, 60, 52] |
通过上面的代码可以看出,定义Lambda函数的关键字是lambda
,后面跟函数的参数,如果有多个参数用逗号进行分隔;冒号后面的部分就是函数的执行体,通常是一个表达式,表达式的运算结果就是Lambda函数的返回值,不需要写return
关键字。
如果需要使用加减乘除这种简单的二元函数,也可以用Lambda函数来书写,例如调用上面的calc
函数时,可以通过传入Lambda函数来作为op
参数的参数值。当然,op
参数也可以有默认值,例如我们可以用一个代表加法运算的Lambda函数来作为op
参数的默认值。
1 | def calc(*args, init_value=0, op=lambda x, y: x + y, **kwargs): |
提示:注意上面的代码中的
calc
函数,它同时使用了可变参数、关键字参数、命名关键字参数,其中命名关键字参数要放在可变参数和关键字参数之间,传参时先传入可变参数,关键字参数和命名关键字参数的先后顺序并不重要。
有很多函数在Python中用一行代码就能实现,我们可以用Lambda函数来定义这些函数,调用Lambda函数就跟调用普通函数一样,代码如下所示。
1 | import operator, functools |
提示1:上面使用的
reduce
函数是Python标准库functools
模块中的函数,它可以实现对数据的归约操作,通常情况下,过滤(filter)、映射(map)和归约(reduce)是处理数据中非常关键的三个步骤,而Python的标准库也提供了对这三个操作的支持。提示2:上面使用的
all
函数是Python内置函数,如果传入的序列中所有布尔值都是True
,all
函数就返回True
,否则all
函数就返回False
。
偏函数
函数在执行时,要带上所有必要的参数进行调用。但是,有时参数可以在函数被调用之前提前获知。这种情况下,一个函数有一个或多个参数预先就能用上,以便函数能用更少的参数进行调用。偏函数是将所要承载的函数作为partial()函数的第一个参数,原函数的各个参数依次作为partial()函数后续的参数,除非使用关键字参数。通过语言描述可能无法理解偏函数是怎么使用的,那么就举一个常见的例子来说明。在这个例子里,我们实现了一个取余函数,对于整数 100,取得对于不同数 m 的 100%m 的余数。
1 | from functools import partial |
由于之前看到的例子一般选择加法或乘法来讲解,无法体会偏函数参数的位置问题,容易给人造成 partial 的第二个参数也是原函数的第二个参数的假象,所以我在这里选择 mod 来讲解。而对于有关键字参数的情况下,就可以不按照原函数的参数位置和个数了。下面再看一个例子,讲的是如何进行不同的进制转换。
1 | from functools import partial |
偏函数的这些应用看似简单,用途却很大,可以现在心里放个位置,等哪天用到的时候就会发现它的强大。
递归调用
Python中允许函数嵌套定义,也允许函数之间相互调用,而且一个函数还可以直接或间接的调用自身。函数自己调用自己称为递归调用,那么递归调用有什么用处呢?现实中,有很多问题的定义本身就是一个递归定义,例如我们之前讲到的阶乘,非负整数N
的阶乘是N
乘以N-1
的阶乘,即N! = N * (N-1)!
,定义的左边和右边都出现了阶乘的概念,所以这是一个递归定义。既然如此,我们可以使用递归调用的方式来写一个求阶乘的函数,代码如下所示。
1 | def fac(num): |
上面的代码中,fac
函数中又调用了fac
函数,这就是所谓的递归调用。代码第2行的if
条件叫做递归的收敛条件,简单的说就是什么时候要结束函数的递归调用,在计算阶乘时,如果计算到0
或1
的阶乘,就停止递归调用,直接返回1
;代码第4行的num * fac(num - 1)
是递归公式,也就是阶乘的递归定义。下面,我们简单的分析下,如果用fac(5)
计算5
的阶乘,整个过程会是怎样的。
1 | # 递归调用函数入栈 |
注意,函数调用会通过内存中称为“栈”(stack)的数据结构来保存当前代码的执行现场,函数调用结束后会通过这个栈结构恢复之前的执行现场。栈是一种先进后出的数据结构,这也就意味着最早入栈的函数最后才会返回,而最后入栈的函数会最先返回。例如调用一个名为a
的函数,函数a
的执行体中又调用了函数b
,函数b
的执行体中又调用了函数c
,那么最先入栈的函数是a
,最先出栈的函数是c
。每进入一个函数调用,栈就会增加一层栈帧(stack frame),栈帧就是我们刚才提到的保存当前代码执行现场的结构;每当函数调用结束后,栈就会减少一层栈帧。通常,内存中的栈空间很小,因此递归调用的次数如果太多,会导致栈溢出(stack overflow),所以递归调用一定要确保能够快速收敛。我们可以尝试执行fac(5000)
,看看是不是会提示RecursionError
错误,错误消息为:maximum recursion depth exceeded in comparison
(超出最大递归深度),其实就是发生了栈溢出。
我们使用的Python官方解释器,默认将函数调用的栈结构最大深度设置为1000
层。如果超出这个深度,就会发生上面说的RecursionError
。当然,我们可以使用sys
模块的setrecursionlimit
函数来改变递归调用的最大深度,例如:sys.setrecursionlimit(10000)
,这样就可以让上面的fac(5000)
顺利执行出结果,但是我们不建议这样做,因为让递归快速收敛才是我们应该做的事情,否则就应该考虑使用循环递推而不是递归。
再举一个之前讲过的生成斐波那契数列的例子,因为斐波那契数列前两个数都是1
,从第3个数开始,每个数是前两个数相加的和,可以记为f(n) = f(n - 1) + f(n - 2)
,很显然这又是一个递归的定义,所以我们可以用下面的递归调用函数来计算第n
个斐波那契数。
1 | def fib(n): |
需要提醒大家,上面计算斐波那契数的代码虽然看起来非常简单明了,但执行性能是比较糟糕的,原因大家可以自己思考一下,更好的做法还是之前讲过的使用循环递推的方式,代码如下所示。
1 | def fib(n): |
简单的总结
Python中的函数也是对象,所以函数可以作为函数的参数和返回值,也就是说,在Python中我们可以使用高阶函数。如果我们要定义的函数非常简单,只有一行代码且不需要名字,可以将函数写成Lambda函数(匿名函数)的形式。一些复杂的问题用函数递归调用的方式写起来真的很简单,但是函数的递归调用一定要注意收敛条件和递归公式,找到递归公式才有机会使用递归调用,而收敛条件确定了递归什么时候停下来。函数调用通过内存中的栈空间来保存现场和恢复现场,栈空间通常都很小,所以递归如果不能迅速收敛,很可能会引发栈溢出错误,从而导致程序的崩溃。