为什么会有函数呢?
函数:把代码的一部分视作有机整体,然后切分出来并为之命名的程序设计机制。(这种机制在不同时期和不同语言中,有事务、程序、子程序等不同的叫法,但大多数人都习惯称它为“函数”。)
函数为什么必不可少呢?
有没有因为没有函数而不能编写的程序呢?答案是否定的。
尽管没有函数也可以编写程序,但使用函数编写程序将变得更轻松简便:因为它便于理解和重复使用。
便于理解——如同一个组织
把代码切分为多个函数,如同将一个大的组织按部门划分开。在一个小的程序中,函数的优越性体现不出来,但当源代码的行数多起来后要把握整体就变得越来越困难。因此,把一定行数的代码视作一个整体并为之取名,这就是函数。
便于再利用——如同零部件
构建函数类似于将小的零部件组合起来制造大的零部件。
程序中再利用的特征
编写程序和制造物理实体有一个很大的区别,那就是重复使用零部件时所产生的成本的类型。
举个例子,假如要为整栋公寓楼所有房间的水龙头安装净水器,公寓楼有100个房间的话就需要配备100个净水器,相应地,有200个房间的话就要200个净水器。房间数量越多,所耗费的资金和空间资源也就越多。
在程序中,假如要用某个函数来处理列表中所有的数据,有100个数据的话就要调用此函数100次,有200个的话就要调用200次,数据量越多,所需的执行时间也就越长。但是,函数的实现只需一个就足够了,调用200次并不需要将此函数实现200次,通过把需要反复执行的操作封装成函数,进而多次调用,可以确保源代码紧凑且清晰。
把相同的操作封装在一起的好处不仅仅在于使程序更简短,也在于能使阅读程序的人无需反复读取相同内容的源代码,从冗长的程序中切分出反复使用的代码将其封装成一个整体,程序就更容易理解了。
返回命令
从第四章了解到,If、while、for语句全部都可以借助goto语句实现,但是从源代码再利用的角度来看,仅仅依靠goto语句是不够的。
goto语句无法将程序返回原来的位置,我们期望的运行是,执行跳转语句时记住这一位置,之后碰到返回语句时又能跳转回到该位置后面的语句。
有了返回原来的位置这样的命令,代码的再利用成为可能,一个程序中有几处执行相同的操作时,就可以把这些操作封装在一个地方了。
函数的诞生
把反复使用的命令封装在一起再利用,这种需求在很早以前就有了。
当时,程序的命令和数据完全都存储在内存中,修改程序就如同把数值代入变量中一样简单。通过修改程序中跳转命令的跳转目的地,就能使函数调用后返回原来的位置。
l 1:将110处跳转命令的跳转目的地改写为3处
l 2:调用函数(跳转至100处))
l 3:下一个命令
l ……
l 51:将110处跳转命令的跳转目的地改写为53处
l 52:调用函数(跳转至100处)
l 53:下一个命令
l ……
l 100:函数操作
l ……
l 110:返回(跳转至X处)
就这样,函数诞生了。
记录跳转目的地的专用内存
在函数调用前修改返回命令的跳转目的地时,函数调用者必须同时知道跳转目的地在哪里和返回命令所在地在哪里。这是很难办到的,假如在函数中增加几行代码,返回命令的位置就会相应地往后挪一些。这样一来,就不得不修改调用这一函数的全部代码。
后来出现了稍微改良过的方法,即创建用来事先记录返回目的地的内存空间,并设计能跳转到该内存空间里记录的地址的命令。这样,即使函数调用前不知道返回命令所在地也没关系了。
l 1:将3写入返回目的地内存
l 2:调用函数(跳转至100处))
l 3:下一个命令
l ……
l 100:函数操作
l ……
l 110:返回至返回目的地内存所记录的地址
然而,这种方法也有一个问题。当调用函数X期间又调用了函数Y时,返回目的地内存被写覆盖,函数X执行之后应该返回的目的地地址就找不到了,这时该如何处理呢?
栈
栈终于登场了,栈是一种存储有多个值的数据结构,实现最后被存入的值最先被读取。
那么栈具体是怎么实现的呢?
首先,决定记录栈顶位置(即最后被存入的数据的地址)的内存地址。图中这一地址就是42,之后每当存入新数据时将按步骤执行,42处数值加1后把数据存入该数值指向的地址。
最初的状态如下:
l 42:栈顶在哪(当前值:100)
l 100:将函数X的返回目的地写入该栈后的状态如下:
l 42:栈顶在哪(当前值:101)
l 100:
l 101:函数X的返回目的地
然后把函数Y的返回目的地写入栈。42处的值加1后变成102,数据被写入102处
l 42:栈顶在哪(当前值:102)
l 100:
l 101:函数X的返回目的地
l 102:函数Y的返回目的地
接下来,我们来看一下数据的读取过程。数据读取时按照步骤,先读42处数值指向的地址上存储的数据,再将42处数值减1.
l 42:栈顶在哪(当前值:101)
l 100:
l 101:函数X的返回目的地
l 102:函数Y的返回目的地
这样一来,即使在调用函数X期间又调用了函数Y,也不至于把函数X的返回目的地写覆盖,程序可以顺利地返回。
递归调用
所谓递归调用,是指函数内部再次调用当前函数的过程。
嵌套结构体的处理方法
作为处理嵌套结构的一个例子,我们来看一下为嵌套列表的全部元素求和的问题。
比如,【1,2,【3,4,】,5】这样一个列表,可以看作是将【3,4】这个列表嵌套放入【1,2,?,5】这个列表产生的,要为这样一个嵌套结构的列表里所有元素求和,该如何实现呢?
下面的Python代码使用for语句把列表中的元素逐个取出,如果为整数则做相加运算,执行情况如下:
def total(xs)
result = 0
for x in xs: #逐个取出列表xs中的元素放进x
if is_integer(x):#如果x为整数则做加法
result += x
else:#如果x不为整数该如何处理?
return result
最早出来的是1和2,因为它们都是整数,所以与result做加法,最后结果是3,到此为止都很顺利,但接下来出现的【3,4】不是整数了,这个该如何处理呢?
无法用for语句实现
也许有人说,因为这是一个列表,在for语句结构中对其元素做特别加法不就行了,对于这个例子中的输入数据,这种实现方法恰巧是可行的,对于最多仅有两重嵌套的输入数据,只要用二重for语句就可以处理,但是,输入为三重嵌套结构的列表又会怎样呢?此时第二个for语句的处理中又碰到一个列表,对这个列表该怎么处理呢?
def total(xs)
result = 0
for x in xs: #逐个取出列表xs中的元素放进x
if is_integer(x):#如果x为整数则做加法
result += x
else:#x为列表,所以用for语句处理
for y in x
if is_integer(y):
result += y
else:
#再来一个列表时该如何处理呢?
return result
此处即使再追加一个for语句,这段程序也只是针对三重嵌套结构的处理管用,如果数据有四重、五重嵌套就无法处理了。
这种多重嵌套的数据结构并不罕见,比如html语言中的标签就有几十层嵌套,处理这样的数据结构,需要的是不管多少层嵌套都能做处理的机制,多次嵌套for语句是无法做到的。
使用递归调用
于是就有了递归调用,这种方法在实现对嵌套列表元素求和的函数total中,又调用该函数自身,如同该函数已经实现完成了一样。
def total(xs)
result = 0
for x in xs: #逐个取出列表xs中的元素放进x
if is_integer(x):#如果x为整数则做加法
result += x
else:#x为嵌套列表,所以用total求里面的元素的总和
result += total(x)
return result
这样就完成了,不管对函数total传递几重嵌套结构体的列表,都能把它里面的元素的和求出来。
递归调用执行时的程序流
给函数total传递参数【1,【2,3】,4】并调用会发生什么呢?我们顺着递归调用的执行过程一起来看一下。
首先,函数total带有参数【1,【2,3】,4】被调用时,xs为【1,【2,3】,4】,result为0.
然后开始执行for语句循环,先把xs的第一个元素取出,为整数的1,故执行result加1,result由0变成1。
然后循环至下一个元素。把xs的第二个元素取出,发现是非整数的【2,3】,为了求解这个数组的和,把【2,3】作为参数调用函数total,在第二个调用中,xs为【2,3】,result为0.
第二个调用中for语句开始执行,把xs的第一个元素取出,为整数的2,故执行result加2,result由0变成2.
第二个调用中的for语句继续执行,把xs的第二个元素取出,为整数的3,故执行result加3,result由2变成5.
这样第二个调用中的for语句循环结束,循环体外有return result,把此时result的值5返回函数调用的地方,在函数调用处,返回值与result相加,result由1变成6.
把xs的第三个元素取出,为整数的4,故执行result加4,result由6变成10.
total的第一次调用中的for语句执行完毕,result值为10,故将10返回至函数调用的地方。
这样就成功地返回了正确的结果。在这个执行过程中,针对每次函数调用都有单独的地方用来存储xs和result的值,并且在第二个total执行结束时第一个total能紧接着执行,这两点值得注意。
随着程序变得越来越庞大,把握全局逐渐地变得困难起来。同时,有可能需要多次用到非常相似的操作。
函数就是为解决这个问题产生的。通过在语义上把一整块代码切分出来并为之命名,理解这段代码变得更加容易。此外,通过在其他地方调用这个函数,实现了代码的再利用。
伴随着函数的使用产生了递归调用这一编程技巧,它非常适合处理嵌套形式的数据。