像计算机科学家一样思考python-第3章 函数
在程序设计中,函数是指用于进行某种计算的一系列语句的有名称的组合。定义一个函数时,需要指定函数的名称并写下一系列程序语句。之后,就可以使用名称来“调用”这个函数
3.1函数调用
一个函数调用的例子
>>> type(42)
<class 'int'>
这个函数的名称是type,括号中的表达式我们称之为函数的参数。这个函数调用的结果是求得参数的类型。
我们通常说函数“接收”参数,并“返回”结果。这个结果也称为返回值
3.2数学函数
Python有一个数学计算模块,提供了大多数常用的数学函数。模块是指包含一组相关的函数的文件。
要想使用模块中的函数,需要先使用import语句将它导入运行环境
>>> import math
这个语句将会创建一个名为math的模块对象。如果显示这个对象,可以看到它的一些信息:
>>> math
<module 'math' (built-in)>
模块对象包含了该模块中定义的函数和变量。若要访问其中一个函数,需要同时指定模块名称和函数名称,用一个句点(.)分隔。这个格式称为句点表示法(dot notation)
>>> radians=0.7
>>> height=math.sin(radians)
>>> height
0.644217687237691
3.3组合
到现在为止,我们已经分别了解了程序的基本元素——变量、表达式和语句,但还没有接触如何将它们有机地组合起来。
程序设计语言最有用的特性之一就是可以将各种小的构建块(building block)组合起来。例如,函数的参数可以是任何类型的表达式,包括算术操作符:
>>> x = math.sin(degrees / 360.0 * 2 * math.pi)
甚至还包括函数调用:
>>> math.exp(math.log(x+1))
基本上在任何可以使用值的地方,都可以使用任意表达式,只有一个例外:赋值表达式的左边必须是变量名称,在左边放置任何其它的表达式都语法错误(后面还会看到这条规则的例外情况)
3.4添加新函数
我们可以自己添加新的函数。函数定义指定新函数的名称,并提供一系列程序语句,当函数被调用时,这些语句会顺序运行。
下面是一个例子:
>>> def print_lyrics():
... print("I'm a lumberjack,and I'm okay")
... print("I sleep all night and I Work all day.")
def是一个关键字,表示接下来是一个函数定义。这个函数的名称是print_lyrics。函数名称的书写规则与变量名称一:字母、数字、下划线是合法的,但第一个字符不能是数字。关键字不能作为函数名,而且我们应用尽量避免函数和变量同名。函数名后的空括号表示它不接收任何参数。
有所思:如果函数名与变量名一样会发生什么情况呢?
假如有一个变量a=1,后面又定一个函数也叫a,执行1个print语句,这时候变量名a指向的是函数对象呢,还是int类型的1呢?我猜肯定指向的是函数a,因为python是按顺序执行的语句,如果同一个变量名被重复定义,则以最后一次定义的内容为准。
>>> a =1
>>> type(a)
<class 'int'>
>>> def a():
... print("函数a")
...
>>> type(a)
<class 'function'>
函数定义的第一行称为函数头(header),其它部分称为函数体(body)。函数头应该以冒号结束,函数体则应当整体缩进一级。依照惯例,缩进总是使用4个空格,函数体的代码语句行数不限。
本例中的print语句里的字符串使用双引号括起来。单引号和双引号的作用相同。大部分情况下,人们都使用单引号,只在本例中这样的特殊情况下才使用双引号。本例中的字符串里本身就存在单引号(这里的单引号作为缩略符号用)。
代码中所有的引号(包括双引号和单引号)都必须是”直引号”,通常在键盘上Enter键附近。而”斜引号”,在Python中是非法的。
如果在交互模式里输入函数定义,则解释器会输出省略号(...)提示用户当前的定义还没有结束:
>>> def print_lyrics():
... print("I'm a lumberjack,and I'm okay")
... print("I sleep all night and I Work all day.")
...
想要结束这个函数的定义,需要输入一个空行。
定义一个函数会创建一个函数对象,其类型是”function”。
>>> print(print_lyrics)
<function print_lyrics at 0x7f25f96a8e18>
>>> type(print_lyrics)
<class 'function'>
调用新创建的函数的方式,与调用内置函数是一样的:
>>> print_lyrics()
I'm a lumberjack,and I'm okay
I sleep all night and I Work all day.
定义好一个函数之后,就可以在其它函数中调用它。例如,若想重复上面的歌词,我们可以写一个repeat_lyrics函数:
>>> def repeat_lyrics():
... print_lyrics()
... print_lyrics()
然后调用 repeat_lyrics:
>>> repeat_lyrics()
I'm a lumberjack,and I'm okay
I sleep all night and I Work all day.
I'm a lumberjack,and I'm okay
I sleep all night and I Work all day.
3.5 定义和使用
将前面一节的代码片段整合起来,整个流程就像下面这个样子:
def print_lyrics():
print("I'm a lumberjack,and I'm okay")
print("I sleep all night and I Work all day.")
def repeat_lyrics():
print_lyrics()
print_lyrics()
repeat_lyrics()
这个程序包含两个函数定义: print_lyrics和repeat_lyrics。函数定义的执行方式和其他语句一样,不同的是执行后会创建函数对象。函数体里面的语句并不会立即运行,而是等到函数被调用时才执行。函数定义不会产生任何输出。
你可能已经猜到,必须先创建一个函数,才能运行它。换言之,函数定义必须在函数被调用之前先运行。
练习1:
作为练习,将程序的最后一行移动到行首,于是函数调用会先于函数定义执行。运行程序并查看会有什么样的错误信息。
repeat_lyrics()
def print_lyrics():
print("I'm a lumberjack,and I'm okay")
print("I sleep all night and I Work all day.")
def repeat_lyrics():
print_lyrics()
print_lyrics()
运行程序报错如下:
NameError: name 'repeat_lyrics' is not defined
练习2:
现在将函数调用那一行放回到末尾,并将函数print_lyrics 的定义移到函数 repeat_lyrics 定义之后。这时候运行程序会发生什么?
def repeat_lyrics():
print_lyrics()
print_lyrics()
def print_lyrics():
print("I'm a lumberjack,and I'm okay")
print("I sleep all night and I Work all day.")
repeat_lyrics()
我以为会报错,提示 print_lyrics 函数没有定义,为什么没有报错呢?
打开程序的debug模式调试程序,发现程序的执行顺序是这样的
def repeat_lyrics(): #1
print_lyrics()#4
print_lyrics()#7
def print_lyrics():#2
print("I'm a lumberjack,and I'm okay")#5 #8
print("I sleep all night and I Work all day.")#6 #9
repeat_lyrics()#3
为什么练习1会报错,而练习2却能正常运行呢?
原因就是:函数定义必须在函数被调用之前先运行。 函数体里面的语句并不会立即运行,而是等到函数被调用时才执行
在练习2中,在repeat_lyrics函数的函数体中,调用 print_lyrics函数,而在执行repeat_lyrics函数前(#3), print_lyrics已经被定义过了(#2),所以程序不会出错。
而在练习1中,程序自上而下运行,首先执行调用repeat_lyrics函数的语句,而在此条语句之前又没有执行定义repeat_lyrics函数的语句所以会出错。
举一反三,下面这样执行程序也会报错,因为 print_lyrics函数还没有被定义,就已经被调用。
def repeat_lyrics():
print_lyrics()
print_lyrics()
repeat_lyrics()
def print_lyrics():
print("I'm a lumberjack,and I'm okay")
print("I sleep all night and I Work all day.")
NameError: name 'print_lyrics' is not defined
3.6 执行流程
为了保证函数的定义先于其首次调用执行,需要知道程序中语句运行的顺序,即执行流程。
执行总是从程序的第一行开始。语句按照从下到下的顺序逐一运行。
函数定义并不会改变程序的执行流程,但应注意函数体中的语句并不立即运行,而是等到函数被调用时运行。
函数调用可以看作程序运行流程中的一个迂回路径。遇到函数调用时,并不会直接继续运行下一条语句,而是跳到被调用函数的函数体的第一行,继续运行完函数体的所有语句,再跳回到原来离开的地方。
这样看似简单,但马上你就会发现,函数体中可以调用其他函数。当程序流程运行到一个函数之中时,可能需要运行其他函数中的语句。而后,当运行那个函数中的语句时,又可能再需要调用运行另外一个函数语句!
幸好Python对于它运行到哪里有很好的记录,所以每个函数执行结束后,程序都能跳回到它离开的地方。直到执行到整个程序的结尾,才会结束程序。
总之,在阅读代码时,并不总应该按照代码书写顺序一行行阅读;有时候,按照程序执行的流程来阅读代码,理解的效果可能会更好。
3.7 形参和实参
在函数内部,实参会赋值给称为形参(parameter)的变量。下面的例子是一个函数的定义,接收一个实参
def print_twice(bruce):
print(bruce)
print(bruce)
这个函数在调用时会把实参的值赋到形参bruce上,并将其打印两次。这个函数对任何可以打印的值都可用。
print_twice('span'*4)
span span span span
span span span span
print_twice(42)
42
42
print_twice(math.pi)
3.141592653589793
3.141592653589793
内置函数的组合规则,在用户自定义函数上同样可用,所以我们可以对print_twice 使用任何表达式作为实参:
>>> print_twice('span '*4)
span span span span
span span span span
>>> print_twice(math.cos(math.pi))
-1.0
-1.0
作为实参的表达式会在函数调用之前先执行。所以在这个例子中,表达式'span '*4和math.cos(math.pi)都只执行一次。
也可以使用变量作为实参:
>>> michael = 'Eric,the half a bee'
>>> print_twice(michael)
Eric,the half a bee
Eric,the half a bee
作为实参传入到函数的变量的名称(michael)和函数定义里形参的名称(bruce)没有关系。函数内部只关心形参的值,而不用关心它在调用前叫什么名字;在 print_twice函数内部,大家都叫bruce。
3.8 变量和形参是局部的
在函数体内新建一个变量时,这个变量是局部的(local),即它只存在于这个函数之内。例如:
def print_twice(bruce):
print(bruce)
print(bruce)
def cat_twice(part1,part2):
cat = part1+part2
print_twice(cat)
这个函数接收两个实参,将它们拼接起来,并将结果打印两遍。下面是一个使用这一函数的例子:
line1 = 'Bing tiddle '
line2 = 'tiddle bang. '
>>> cat_twice(line1,line2)
Bing tiddle tiddle bang.
Bing tiddle tiddle bang.
当 cat_twice 结束时,变量cat会被销毁。这时再尝试打印它的话,会得到一个异常:
>>> print(cat)
NameError: name 'cat' is not defined
形参也是局部的。例如,在 print_twice函数之外,不存在 bruce这个变量。
3.9 栈图
要跟踪哪些变量在哪些地方使用,有时候画一个栈图(stack diagram)会很方便。和状态图一样,栈图可以展示每个变量的值,不同的是它会展示每个变量所属的函数。
每个函数使用一个帧包含,帧在栈图中就是一个带着函数名称的盒子,里面有函数的参数和变量。前面的函数示例的栈图如图3-1所示。
图中各个帧从上到下安排成一个栈,能够展示出哪个函数被哪个函数调用了。在这个例子里,print_twice被cat_twice调用,而cat_twice被__main__调用。__main__是用于表示整个栈图的图框的特别名称。在所有函数之外新建变量时,它就是属于__main__的。
每个形参都指向与其对应的实参相同的值,所以part1和line1的值相同,part2和line2的值相同,而bruce和cat的值相同。
如果调用函数的过程中发生了错误,Python会打出函数名、调用它的函数的名称,以及调用这个调用者的函数名,依此类推,一直到__main__。
例如,如果在print_twice中访问cat变量,则会得到一个NameError:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in cat_twice
File "<stdin>", line 4, in print_twice
NameError: name 'cat' is not defined
上面这个函数列表被称为回溯(traceback)。它告诉你错误出现在哪个程序文件,哪一行,以及哪些函数正在运行。它也会显示导致错误的那一行代码。
回溯中函数的顺序和栈图中图框的顺序一致。当前正在执行的函数在最底部。
这里插个题外话,说到形参与实参的值相同,突然想到一个点,如果a与b的值相同,a 与 b可以视作是同个对象吗?神奇的是如果a与b都指同一个整数(指向同一个对象地址),python会认为它们是同一个对象,而如果a与b是浮点型,则python不认为它们是同一个对象(指向不同的对象地址)
>>> a=2
>>> b=2
>>> a is b
True
>>> id(a)
10943040
>>> id(b)
10943040
>>> a=2.3
>>> b=2.3
>>> a is b
False
>>> id(a)
139801075219456
>>> id(b)
139801075219648
3.10 有返回值函数和无返回值函数
在我们使用过的函数中,有一部分函数,如数学函数,会返回结果。因为没有想到更好的名字,我称这类函数为有返回值函数(fruitful function)。 另一些函数,如print_twice,会执行一个动作,但不返回任何值。我们称这类函数为无返回值函数(void function)。
当调用一个有返回值的函数时,大部分情况下你都想要对结果做某种操作。例如,你可能会想把它赋值给一个变量,或者在一个表达式中:
x = math.cos(radians)
golden = (math.sqrt(5)+1)/2
在交互模式中调用函数时,Python会直接显示结果:
>>> math.sqrt(5)
2.23606797749979
但是在脚本中,如果只是直接调用这类函数,那么它的返回值就会永远选择掉!
math.sqrt(5)
这个脚本计算5的平方根,但由于并没有把计算结果存储到某个变量中,或显示出来,所以其实没什么实际作用。
无返回值函数可能在屏幕上显示某些东西,或者有其他的效果,但是它们没有返回值。如果把该结果赋值给某个变量,则会得到一个特殊的值None。
>>> result = print_twice('Bing')
Bing
Bing
>>> print(result)
None
值None和字符串’None’并不一样。它是一个特殊的值,有自己独特的类型:
>>> type(None)
<class 'NoneType'>
到目前为止,我们自定义的函数都是无返回值函数。再过几章我们就会开始写有返回值的函数了。
3.11 为什么要有函数
为什么要花功夫将程序拆分成函数呢?也许刚开始编程的时候这其中的原因并不明晰。下面这些解释都可作为参考。
新建一个函数,可以让你有机会给一组语句命名,这样可以让代码更易读和更易调试。
函数可以通过减少重复代码使程序更短小。后面如果需要修改代码,也只要修改一个地方即可。
将一长段程序拆分成几个函数后,可以对每一个函数单独进行调试,再将它们组装起来成为完整的产品。
一个设计良好的函数,可以在很多程序中使用。书写一次,调试一次,复用无穷。
3.12调试
你将会掌握的一个最重要的技能就是调试。虽然调试可能时有烦恼,但它的确是编程活动中最耗脑力、最有挑战、最有趣的部分。
在某种程度上,调试和刑侦工作很像。你会面对一些线索,而且必须推导出事情发生的过程,以及导致现场结果的事件。
调试也像是一种实验科学。一旦猜出错误的可能原因,就可以修改程序,再运行一次。如果猜对了,那么程序的运行结果会符合预测,这样就离正确的程序更近了一步。如果猜错了,则需要重新思考。正如夏洛克.福尔摩丝所说的:“当你排除掉所有的可能性,那么剩下的,不管多么不可能,必定是真相。”
对某些人来说,编程和调试是同一件事。也就是说,编程正是不断调试修改直到程序达到设计目的的过程。这种想法的要旨是,应该从一个能做某些事的程序开始,然后做一点点修改,并调试改正,如此迭代,以确保总是有一个可以运行的程序。
例如,Linux是包含了数百万行代码的操作系统,但最开始只是Linus Torvalds编写的用来研究Intel 80386芯片的简单程序。据Larry Greenfield所说:“Linus最早的一个程序是交替打印AAAA 和 BBBB。后来这些程序演化成了Linux”
3.13术语表
函数(function):一个有名称的语句序列,可以进行某种有用的操作。函数可以接收或者不接收参数,可以返回或不返回结果。
函数定义(function definition):一个用来创建新函数的语句,指定函数的名称、参数以及它包含的语句序列。
函数对象(function object):函数定义所创建的值。函数名可以用作变量来引用一个函数对象。
函数头(header):函数定义的第一行。
函数体(body):函数定义内的语句序列。
形参(parameter):函数内使用的用来引用作为实参传入的值的名称。
函数调用(function call ):运行一个函数的语句。它由函数名称和括号中的参数列表组成。
实参(argument):当函数调用时,提供给它的值。这个值会被赋值给对应的形参。
局部变量(local variable):函数内定义的变量。局部变量只能在函数体内使用。
返回值(return value):函数的结果。如果函数被当作表达式调用,返回值就是表达式的值。
有返回值函数(fruitful function):返回一个值的函数
无返回值函数(void function):总是返回None的函数
None:由无返回值函数返回的一个特殊值。
模块(module):一个包含相关函数以及其他定义的集合的文件
import语句(import statement):读入一个模块文件,并创建一个模块对象的语句。
模块对象(module object):使用import语句时创建的对象,提供对模块中定义的值的访问。
句点表示法(dot notation):调用另一个模块中的函数的语法,使用模块名加上一个句点符号,再加上函数名。
组合(composition):使用一个表达式作为一个更大的表达式的一部分,或者使用语句作为更大的语句的一部分。
栈图(stack diagram):函数栈的图形表达形式,也展示它们的变量,以及这些变量引用的值。
图框(frame):栈图中的一个图框,表达一个函数调用。它包含了局部变量以及函数的参数。
回溯(traceback):当异常发生时,打印出正在执行的函数栈。