记得在第一章说过,程序语言的由来就是对底层微程序的封装。可最为程序语言,我们仍然会遇到“使用重复代码”的情况,这说明“程序语言”这种语法的封装程度仍然不够。所以对于程序语言那些经常使用的代码块,我们仍然需要对其封装,恰巧所有的程序语言都提供了封装程序语言的方法,Python也不例外。之前讲过,Python是一个集成多种程序语言风格的语言,包括面向过程、面向对象等,在本章我们主要讲解在面向过程基础上对程序封装的方法,也就是马上要讲的函数。
1 函数及其定义
函数就是在面向过程的程序语言中,对程序封装的办法。在Python我们使用def
关键字定义函数。def后跟函数名(自己命名,尽量通俗易懂,一般命名都采用小写字母,用下划线隔开)和参数,参数用小括号括起,在最后打上冒号即可,冒号的下面可以写函数具体内容,但都需要一个缩进。我们可以在函数内容里添加return
关键字返回想要的值,注意,执行一次return
后函数会立刻退出执行。
def function_name(para1, para2,...,paran):
first content of function
return answer1
second content of function # 这行不会执行,在执行上一行return后会退出整个函数
我们可以把函数想象成一个黑盒,并类比数学上的函数试试。比如说这里的参数,就好比自变量,return的结果好比因变量,定义的内容好比函数里的数学式子。只要给它指定的参数,中间的过程不用操心,它会自动给出结果。第二章的勾股定理其实就是一个数学公式,也就是函数,现在我们把它转换成Python的函数试试:
def gou_gu_ding_li(bian1, bian2):
bian3_2 = bian1**2 + bian2**2
return bian3_2
可以看到,我们取函数名为勾股定理,参数是两条边长度,然后函数内容是计算第三条边的平方和,并把值返回。函数写好了,那该怎么用呢,我们会在之后深入学习参数技巧,以及函数的调用。
因为Python的函数太多了,我们常常忘记某个函数时干什么的,所以Python提供帮助文档查看函数的作用。在帮助文当中,我们可以详细的看到函数的作用,每个参数的意义,以及返回值等。在Python中,我们可以直接打开帮助文档,利用Ctrl+F查找关键字来寻找帮助,也可以直接在对话窗口查看。帮助文档就不赘述了,在对话窗口查询的方式是使用关键字help
。例如之前的set
函数,为了得到帮助文档,我们可以这么写:
help(set)
按回车会我们会立刻看到帮助,见下图。至于英文看不懂,找百度翻译即可。
除此之外,我们自己定义的函数也可以加上帮助文档,这样通过help
也可以找到。自定义函数帮助文档,仅需在函数内容开头,利用'''
三单引号括起即可,但注意,一定要有缩进,看上去是函数的内容。我们在勾股定理的函数加上帮助文档:
def gou_gu_ding_li(bian1, bian2):
'''
函数计算并返回第三条边长度的平方,两个参数是三角形两边长
'''
bian3_2 = bian1**2 + bian2**2
return bian3_2
我们可以通过以下方式查看:
help(gou_gu_ding_li)
2 函数的调用
函数有两种调用方法,一种是在一个文件内调用,另一种是跨文件调用。在一个文件内容易很多,比如我想使用勾股定理的函数,可以直接调用,返回值可以直接给一个变量。
c2 = gou_gu_ding_li(3,4)
print(c2)
如果是跨文件调用,比如我在另一个Python文件调用sump.py
(这个文件所在文件夹和sump.py
相同)中的勾股定理函数,那么需要先导入文件,并引用该函数。导入文件用关键字from
,应用函数使用关键字import
,可以这么写:
from sump import gou_gu_ding_li
c2 = gou_gu_ding_li(3,4)
print(c2)
如果sump.py
文件里有许多函数想引用,可以用逗号隔开,如果想引用全部的函数,可以这么写:
from sump import *
c2 = gou_gu_ding_li(3,4)
print(c2)
其实Python有很多官方库和第三方库,都是通过这种方法调用的。其实还有一种调用方法,比如我想调用官方的math库,可以这么写:
import math
但是这么写,引用函数的写法要改变。比如我想调用pow(x,y)
函数(计算x的y次方):
import math
ans = math.pow(3, 4)
print(ans)
3 作用域和参数的生命周期
从字面理解,作用域就是一段代码在整个程序运行过程有效的区域,而生命周期就是在这个作用域内执行一次的过程,一旦出了生命周期,这段代码在程序中就相当于没了,如果这时候调用这段代码,程序就会出错。
函数是有作用域的,参数是有生命周期的,其中参数又分为形参和实参。形参是在定义函数时括号里的参数,而实参就是调用时的参数。我们先讲讲形参和实参,这涉及到了函数的调用机理。记得之前我们定义了勾股定理函数,有两个形参bian1
和bian2
,但在调用函数时,括号里却变成实实在在的数字,也就是3和4,事实上,两者是一一对应的。我们定义的形参在函数的内容里使用过,并计算出一个结果返回,那么实参里的3和4在函数调用后会分别写入bian1
和bian2
的值里,并进行相同的计算并返回值。看看下图可以更清晰的理解。
从这个过程,我们可以清楚的看到一个问题:只有在调用函数时,bian1
和bian2
这两个变量才有值,在return bian3_2
的值给c
后,这三个形参都将不复存在。于是我们可以知道这几个变量的生命周期就是在函数调用这个过程,如果在调用函数外部使用这些形参程序就会报错。
那么除了使用实参,我们真的没有其他方法沟通函数了吗?其实Python远比我们想象的强大,在函数内没有这个变量时,它可以在函数外寻找与函数内变量相名相同的变量使用。我们举个例子:
def var():
print(var)
var = 1
var()
这个程序最终结果会打印1,因为函数从外部找到与内部同名的var,于是就调用了。有时因为这个会出现奇怪的错误,写代码时要格外注意自己变量的调用过程。
4 参数调用技巧与函数多值调用
除了像上文的调用方法,Python还提供了多种调用方法。可以看到我们上文的gou_gu_ ding_li
的参数,在调用该函数时,两个参数是一一对应的关系(可以从上图看到),这要求形参和实参顺序不能颠倒,我们现在可以这么写:
c = gou_gu_ding_li(bian2 = 4, bian1 = 3)
这样写的参数调用顺序和之前的是一模一样的。可以看到,只要分别对指定的形参赋值,传递的顺序就不会改变。我们甚至可以混着用:
c = gou_gu_ding_li(bian1 = 3, 4)
但请注意,如果混用,使用第一种方法的赋值必须和形参的位置对应,若第二种方法的赋值占用了这个位置,程序会报错。比如这么写:
c = gou_gu_ding_li(bian2 = 4, 3)
明显bian2
把bian1
的位置占用了,实际上调用参数时没有调用bian1
,所以程序会报错。
定义函数时,也有几个调用形参的方法,首先我们可以设置形参的默认值,建议参数有默认值的写在最后。
def gou_gu_ding_li(bian1, bian2 = 4):
现在我们可以这样调用函数:
c = gou_gu_ding_li(3)
因为我们没有给1传值,所以它会默认使用4这个值,和之前的调用是一样的。
有时,我们不确定需要传递多少参数,所以我们可以传入一个元组作为一个参数,而元组内可以装下任意数量的参数。参数中如果要使用元组,在该参数前加*
即可,但要求这个参数必须是最后一个,所以现在我们可以这么定义gou_gu_ding_li
函数:
def gou_gu_ding_li(*bian):
bian3_2 = bian[0]**2 + bian[1]**2
return bian3_2
那么我们现在就可以这么调用该函数:
c = gou_gu_ding_li(3,4)
程序会把“3,4”
识别成元组(3,4)
,因此我们可以像使用元组一样使用参数。其实返回的结果也可以是元组,如果我们想返回多个值,可以在return后把返回的值用逗号隔开,那么默认返回的是元组。现在我们这么定义函数gou_gu_ding_li
:
def gou_gu_ding_li(*bian):
bian3_2 = bian[0]**2 + bian[1]**2
return bian[0], bian[1], bian3_2
当调用该函数时
c = gou_gu_ding_li(3,4)
c
就是一个元组,c[2]
就是bian3_2
的值。
此外,我们还可以把字典所谓参数,只需要在该变量前加上两个*即可。我们可以这么定义:
def gou_gu_ding_li(**key):
bian3_2 = key['bian1']**2+key['bian2']**2
return bian3_2
然后我们这么调用函数:
c = gou_gu_ding_li(bian1 = 3, bian2 = 4)
参数的规则是,等号左边是键,右边是值。最终的结果和之前是一样的。其实返回值也可以是字典,我们可以这么定义:
def gou_gu_ding_li(**key):
bian3_2 = key['bian1']**2+key['bian2']**2
return {'bian1': key['bian1'], 'bian2':key['bian2'], 'bian3_2':bian3_2}
这样函数将返回一个字典,我们现在调用函数:
c = gou_gu_ding_li(bian1 = 3, bian2 = 4)
print(c['bian3_2'])
这个结果和之前是一样的。
所以总的来说,参数调用和函数调用非常灵活,我们可以在函数内调用函数外变量,可以以不同的顺序调用参数,可以以不同的数据结构作为参数以及函数值的返回。所以我们在使用Python函数时,要充分利用这些特性解决问题。
5 循环迭代,函数递归
迭代和递归是程序语言中最常用,最基本的算法思想,下面将举例说明。首先是迭代,以高斯求和为例,现在要求1+...+100的值,我们可以利用循环解决:
sum = 0
for i in range(1,101):
sum = sum+i
print(sum) # 打印5050
可以看到循环内的计算有个特点:每一次赋值都要用到之前的值。有这样特点的计算都是迭代,所以说如果以后需要用到循环来计算,第一个想到的方式应该是迭代。
下面说说递归,这算是一个难点,一般我们很难想到递归的数据流状态如何。递归其实就是在函数中继续调用函数,这个调用可以在函数内容中使用,也可以在返回值return中使用。现在可以想象一下,数据进到一个函数里,然后又进到相同的函数里,然后又进进进......似乎脑袋要炸了,这么看来它会无限循环下去,所以迭代是需要有停止条件的。现在我们把高斯求和改成递归的方式感受一下:
def guass_sum(n): # n代表加到n,即1+...+n
if n==1:
return 1
else :
reuturn n+guass_sum(n-1)
如果我调用:
ans = guass_sum(100)
会怎样?我们来分析一下过程。首先n = 100
,那么执行100+guass_sum(99)
。guass_sum(99)
是多少呢,这需要调用这个函数才知道,于是在这个函数,n = 99
, 返回99+guass_sum(98)
,guass_sum(98)
又是多少?于是我们又要调用guass_sum(98)
就这样拼命的调用自己,直到n = 1
,于是可以知道guass_sum(1) return 1
,所以guass_sum(2) return 2+guass_sum(1)
,所以是return 2 + 1
以此方法从调用的个自身返回,就可以知道guass_sum(100) return 100 + guass_sum(99)
,也就是,和上面的循环一模一样,只是不那么好理解。
我们往往产生疑惑,为什么有这么简单的循环不用,用这么麻烦的递归呢?其实写起来简单的程序,用起来就不简单了,而写的不简单的反而好用一些。我们解决问题时常常要自己动手写程序,其实这个过程又叫做算法设计,我们通过设计一个计算方法,让计算机进行计算,刚刚的高斯求和就是一种算法,一般算法设计好后都会封装以便重复使用。一个好算法设计起来并不容易,于是有人就发明了一套算法的设计方法,就是分治法。分治法给我们提供一种思想设计算法,最终设计结果就是函数的递归形式。感兴趣的话可以上网查查。
有人又会说,那最终又要设计成递归,有没有更好的方式呢?其实递归等价于一种名为栈的数据结构,我们又可以利用栈实现递归函数的循环实现,于是递归和循环有了相互转换的桥梁。由于这涉及到算法的分析与设计,是新的一块内容,感兴趣可以上网查查,这里不多讲了。
6 异常处理
异常处理不算是函数的部分,但是可以在函数里使用。可以想象我们写程序不会一帆风顺,时不时的计算机就会送给我们一个bug(错误),如果检查错误的方式(debug)不好,会带来麻烦,浪费时间和精力。究竟是什么阻挡我们解决错误的脚步呢?
我的天,刚说完就来了个bug,这鲜红的大字,还有该死的英文,看的就烦。瞧瞧,这些就是阻碍你解决问题的绊脚石,要是你的错误可以用自己的方式表达出来就好了。Python提供异常处理机制,可以将异常以自己的方式选择输出。它的语法是这样的:
try:
# 程序内容
except [错误名称(可以不写)] :
# 错误时执行的代码
except :
# 以此类推......
else:
# 如果没有异常则执行的代码内容
finally:
# 无论是否有异常都会执行的代码内容
错误名称由程序定义,比如上图的错误NameError
就是名字错误,那么如果遇到这种错误我们想自定义输出方式,可以这么写:
try:
print(hahahahaha)
except NameError:
print('没有这个变量呀?!')
那么程序不会输出鲜红的大字,而是输出你想要的内容。有时这个错误里面可以写一些代码错误的补救方法。
一般一个程序有很多种类的错误,要写很多except
,照顾不过来,那么索性except后不跟错误名称,无论程序遇到什么错都会跳转这里:
try:
print(hahahahaha)
except:
print('我不管什么错,反正你就是错了~')
不过如果你足够个性,喜欢红红的英文大字错误,也有办法。Python里可以使用raise关键字主动抛出错误,比如无法除以0的异常可以这么写:
raise ZeroDivisionError()
如果这么写,Python在执行该语句时会自动停止运行程序,并把错误给出。
实例代码可以看我的码云:第三章样例代码