递归函数
Python支持递归函数——即直接或者间接地调用自身以进行循环的函数。递归是Python中比较的高级的话题,并且它在Python中比较少见。然后,它是一项非常有用的技术,因为它允许程序遍历拥有任意的,不可预知的形状的结构。
用递归求和
我们来看一个例子。假如要对一个数字列表求和,我们可以使用内置的sum函数,或者是自己编写一个更加定制化的版本。示例1是用递归编写的一个定制求和函数:
#示例1
>>> def mysum(N):
... if not N:
... return 0
... else:
... return N[0] + mysum(N[1:])
...
>>> mysum(range(10)) #[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
45
在每一层。这个函数都递归地调用自己来计算列表剩余的值的和,这个和随后加到前面的一项中。当列表为空的时候,递归循环结束并返回0.就像这样使用递归的时候,对函数调用的每一个打开的层级,在运行时调用堆栈上都有自己的一个函数本地作用域的副本,也就是说,这意味着N在每个层级都是不同的。
这可能对新手比较难以理解,我们可以这样来看示例2。尝试给函数添加一个N的打印并再次运行它,从而在每个调用层级记录下当前列表:
示例2
>>> def mysum(N):
... print(N)
... if not N:
... return 0
... else:
... return N[0] + mysum(N[1:])
...
>>> mysum(list(range(10)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
[2, 3, 4, 5, 6, 7, 8, 9]
[3, 4, 5, 6, 7, 8, 9]
[4, 5, 6, 7, 8, 9]
[5, 6, 7, 8, 9]
[6, 7, 8, 9]
[7, 8, 9]
[8, 9]
[9]
[]
45
正如你所看见的,在每个递归的层级上,要加和的列表变得越来越小,直到它变为空——递归循环结束。加和随着递归调用的展开而计算开来。
编码替代方案
这有一个很有意思的是,我们可以使用Python的三元if/else表达式在这里保存某些代码。我们也可以针对任何可加和的类型一般化,
以下是一些例子:
示例3-1
>>> def mysum(N):
... return 0 if not N else N[0] + mysum(N[1:])
...
>>> mysum(list(range(10)))
45
示例3-2
>>> def mysum(N):
... return N[0] if len(N) == 1 else N[0] + mysum(N[1:])
...
>>> mysum(list(range(10)))
45
示例3-3
>>> def mysum(N):
... first, *rest = N
... return first if not rest else first + mysum(rest)
...
>>> mysum(list(range(10)))
45
#示例3-2和3-3会由于空的列表而失败。
示例3-2和3-3会由于空的列表而失败,但考虑到支持+任何对象类型的序列,而不单单是数字:
>>> mysum([1])
1
>>> mysum(list(range(10)))
45
>>> >>> mysum(('m', 'y', 'l', 'o', 'v', 'e', 'r'))
'mylover'
>>> mysum(['my', 'sweet', 'heart'])
'mysweetheart'
如果你比较好奇去研究这3个变体,将会发现,后2个在一个单个字符串参数上也有效(例如:mysum('love')),因为字符串是一字符的字符串的序列; 第三个变体中任意可迭代对象上都有效(包括打开的文件)。
前面我们说过递归可以是直接的,就像目前所给出的例子一样,也可以是间接的,就像下面将要给出的例子一样(一个函数调用另一个函数,后者反过来调用其调用者)。直接的效果是相同的,尽管这在每个层级有2个函数调用:
#示例4
>>> def mysum(N):
... if not N:
... return 0
... return nonempty(N)
...
>>> def nonempty(N):
... return N[0] + mysum(N[1:])
...
>>> mysum(list(range(10)))
45
循环语句OR递归
尽管递归对于之前求和的例子都有效,但是在那种环境中,它可能有点过于追求技巧了。实际上,递归在Python中并没有像Lisp那些语言中那样常用,因为Python强调像循环这样的简单的过程式语句,循环语句通常更为自然。例如,while常常使得事情变得更为具体一些,并且它不需要定义一个支持递归调用的函数:
#示例5
>>> L = list(range(10))
>>> sum = 0
>>> while L:
... sum += L[0]
... L = L[1:]
...
>>> sum
45
更好的情况是for循环为我们自动迭代,使得递归在大多数情况下不必使用(有可能,递归在内存空间和执行时间方面效率比较低):
>>> L = list(range(10))
>>> sum = 0
>>> for x in L:
... sum += x
...
>>> sum
45
处理任意结构
另一方面,递归可以要求遍历任意形状的结构(我们前面说过)。作为递归在这种环境中的应用的一个简单的例子,考虑像下面这样的一个任务:
#示例6
#计算一个嵌套的子列表结构中的所有数字的总和
>>> def sumtree(N):
... tot = 0
... for x in N:
... if not isinstance(x, list):
... tot += x
... else:
... tot += sumtree(x)
... return tot
...
>>> N = [2, [2, 5, [6, 3, [9]] , 6], 6, [5, 1]]
>>> print(sumtree(N))
45
简单的循环语句在这里不起作用了,因为这不是一个线性迭代。嵌套的循环语句也不够用,因为子列表可能嵌套到任意的深度并且以任意的形式嵌套。相反,示例6的代码却能够使用递归来对应这种一般性的嵌套,以便顺序访问子列表。