像计算机科学家一样思考Python —— 函数篇
1 函数调用
在程序设计中,函数是指用于进行某种计算的一系列语句的有名称的组合。定义一个函数时,需要指定函数的名称并写下一系列程序语句。之后,就可以使用名称来“调用”这个函数。前面我们已经见过函数调用的例子:
这个函数的名称是 type
,括号中的表达式我们称之为函数的参数。这个函数调用的结果是参数的类型。
我们通常说函数“接收”参数,并“返回”结果。这个结果称为返回值(return value)。
2 类型转换函数
Python提供了一些可将某个值从一种类型转换为另一种类型的内置函数。int
函数可以把任何可以转换为整型的值转换为整型;如果转换失败,则会报错:
int
可以将浮点数转换为整数,但不会做四舍五入操作,而是直接舍弃小数部分。
float
函数将整数和字符串转换为浮点数:
最后,str
函数将参数转换为字符串:
3 数学函数
Python有一个数学计算模块,提供了大多数常用的数学函数。模块是指包含一组相关的函数的文件。
要想使用一个模块,需要先将它导入(import)运行环境:
这个语句将会建立一个名为math的模块对象(module object)。如果打印这个对象,可以看到它的一些信息:
模块对象包含了这个模块中定义的函数和变量。若要访问其中的一个函数,需要同时指定模块名称和函数名称,用一个句点(.
)分隔。这个格式称为句点表示法(dot notation)。
上面第一个例子使用了log10
来计算以分贝为单位的信号/噪声比(假设signal_power
和noise_power
都已经事先定义好了)。math
模块也提供了log
函数,用来计算底为e的自然对数。
第二个例子计算radians
的正弦值。这个变量名已经暗示了,sin
以及cos
、tan
等三角函数接受的参数是以弧度(radians
)为单位的。若要将角度转换为弧度,可以除以360再乘以2π:
表达式math.pi
从math
模块中获得变量pi
。这个变量的值是π的近似值,大约精确到15位数字。
如果你了解三角函数,可以把上面的结果和2的平方根的一半进行比较:
4 组合
到现在为止,我们已经分别了解了程序的基本元素——变量、表达式和语句,但还没有接触如何将它们有机地组合起来。
程序设计语言最有用的特性之一就是可以将各种小的构建块(building block)组合起来。比如,函数的参数可以是任何类型的表达式,包括算术符号:
甚至还包括函数调用:
基本上,在任何可以使用值的地方,都可以使用任意表达式,只有一个例外:赋值表达式的左边必须是变量名称,在左边放置任何其他的表达式都是语法错误(后面我们还会看到这条规则的例外情况)。
5 添加新函数
至此,我们都只是在使用Python提供的函数,其实我们也可以自己添加新的函数。函数定义指定新函数的名称,并提供一系列程序语句。当函数被调用时,这些语句会顺序执行。
下面是一个例子:
def
是关键字,表示接下来是一个函数定义。这个函数的名称是print_lyrics
。函数名称的书写规则和变量名称一样:字母、数字和某些标点是合法的,但第一个字符不能是数字。关键字不能作为函数名,而且我们应尽量避免函数和变量同名。
函数名后的空括号表示它不接收任何参数。
函数定义的第一行称为函数头(header),其他部分称为函数体(body)。函数头应该以冒号结束,函数体整体缩进一级。依照惯例,缩进总是使用4个空格,参看3.14节。函数体的代码语句行数不限。
本例中print语句里的字符串使用双引号括起来。单引号和双引号的作用相同。大部分情况下,人们都使用单引号,只在本例中这样的特殊情况下才使用双引号。本例中的字符串里本身就存在单引号(单引号也作为省略符号用,如I'm
)。
如果在交互模式里输入函数定义,则解释器会输出省略号(...
)提示你当前的定义还没有结束:
想要结束这个函数的定义,需要输入一个空行(在脚本文件中则不需要如此)。
定义一个函数后,会创建一个同名的变量。
变量print_lyrics
的值是一个函数对象,其类型是'function'
。
调用新创建的函数的方式,与调用内置函数是一样的:
定义好一个函数之后,就可以在其他函数中调用它。比如,若想重复上面的歌词,我们可以写一个repeat_lyrics
函数:
然后可以调用repeat_lyrics
:
当然,这首歌其实并不是这么唱的。
6 定义和使用
将前面一节零散的代码整合起来,整个程序就像下面这个样子:
这个程序包含两个函数定义:print_lyrics
和repeat_lyrics
。(在解释器执行程序代码时)函数定义的执行方式和其他语句一样,不同的是执行后会创建函数对象。函数体里面的语句并不会立即执行,而是等到函数被调用时才执行。函数定义不会产生任何输出。
你可能已经猜到,必须先创建一个函数,才能执行它。换言之,函数定义必须在函数的第一次调用之前先执行。
练习1
将程序的最后一行移动到首行,于是函数调用会先于函数定义执行。运行程序并查看会有什么样的错误信息。
练习2
将函数调用那一行放回到末尾,并将函数print_lyrics
的定义放到函数repeat_lyrics
定义之后。这时候运行程序会发生什么?
7 执行流程
为了保证函数的定义先于其首次调用执行,你需要知道程序中语句执行的顺序,即执行流程。
执行总是从程序的第一行开始。从上到下,按顺序,每次执行一条语句。
函数定义并不会改变程序的执行流程,但应注意函数体中的语句并不立即执行,而是等到函数被调用时执行。
函数调用可以看作程序执行流程中的一个迂回路径。遇到函数调用时,并不会直接继续执行下一条语句,而是跳到函数体的第一行,继续执行完函数体的所有语句,再跳回到原来离开的地方。
这样看似简单,但马上你会发现,函数体中可以调用其他函数。当程序流程运行到一个函数之中时,可能需要执行其他函数中的语句。但当执行那个函数中的语句时,又可能再需要调用执行另一个函数的语句!
幸好Python对于它运行到哪里有很好的记录,所以每个函数执行结束后,程序都能跳回到它离开的地方。直到执行到整个程序的结尾,才会结束程序。
前面这段枯燥的描述,寓意何在?当你阅读代码时,并不总是应该一行行按照书写顺序阅读。有时候,按照执行的流程来阅读代码,可能理解效果更好。
8 形参和实参①
我们已经看到,有些内置函数需要传入参数。比如,当调用math.sin
时,需要传入一个数字作为实参。有的函数需要多个实参:math.pow
需要两个,分别是基数(base)和指数(exponent)。
在函数内部,实参会被赋值给形参。下面的例子是一个用户自定义的函数,接收一个实参:
这个函数在调用时会把实参的值赋到形参bruce
上,并将其打印两次。
这个函数对任何可以打印的值都可用。
内置函数的组合规则,在用户自定义函数上也同样可用,所以我们可以对print_twice
使用任何表达式作为实参:
作为实参的表达式会在函数调用之前先执行。所以在这个例子中,表达式'Spam'*4
和math.cos(math.pi)
都只执行一次。
你也可以使用变量作为实参:
作为实参传入到函数的变量的名称(michael
)和函数定义里形参的名称(bruce
)没有关系。函数内部只关心形参的值,而不用关心它在调用前叫什么名字;在print_twice
函数内部,大家都叫bruce
。
9 变量和形参是局部的
当你在函数体内新建一个变量时,它是局部的(local),即它只存在于这个函数之内。比如:
这个函数接收两个实参,将它们拼接起来,并将结果打印两遍。下面是一个使用这一函数的例子:
当cat_twice
结束时,变量cat
会被销毁。这时再尝试打印它的话,会得到一个异常:
形参也是局部的。比如,在print_twice
bruce这个变量。
10 栈图
要跟踪哪些变量在哪些地方使用,有时候画一个栈图(stack diagram)会很方便。和状态图一样,栈图可以展示每个变量的值,不同的是它会展示每个变量所属的函数。
每个函数使用一个帧包含,帧在栈图中就是一个带着函数名称的盒子,里面有函数的参数和变量。前面的函数示例的栈图如图1所示。
图1 栈图
图中各个帧从上到下安排成一个栈,能够展示出哪个函数被哪个函数调用了。在这个例子里,printtwice
被cattwice
调用,而cattwice
被main
调用。_main
是用于表示整个栈图的图框的特别名称。当你在所有函数之外新建变量时,它就是属于__main
的。
每个形参都指向与其对应的实参相同的值,所以,part1
和line1
的值相同,part2
和line2
的值相同,而bruce
和cat
的值相同。
如果调用函数的过程中发生了错误,Python会打印出函数名,以及调用它的函数的名称,以及调用这个调用者的函数名,依此类推,一直到main
。
比如,如果你在print_twice
中访问cat
变量,则会得到一个NameError
:
上面这个函数列表被称为回溯(traceback)。它告诉你错误出现在哪个程序文件,哪一行,以及哪些函数正在运行。它也会显示导致错误的那一行代码。
回溯中函数的顺序和栈图中图框的顺序一致。当前正在执行的函数列在最底部。
11 有返回值函数和无返回值函数
我们使用过的函数中,有一部分函数,如数学函数,会产生结果。因为没有想到更好的名字,我称这类函数为有返回值函数(fruitful function)。另一些函数,如print_twice
,会执行一个动作,但不返回任何值。我们称这类函数为无返回值函数(void function)。
当你调用一个有返回值的函数时,大部分情况下都想要对结果做某种操作。比如,你可能会想把它赋值给一个变量,或者用在一个表达式中:
在交互模式中调用函数时,Python会直接显示结果:
但是在脚本中,如果只是直接调用这类函数,那么它的返回值就会永远丢失掉!
这个脚本计算5的平方根,但由于并没有把计算结果存储到某个变量中,或显示出来,所以其实没什么实际作用。
无返回值函数可能在屏幕上显示某些东西,或者有其他的效果,但是它们没有返回值。如果你试着把它们的结果赋值给某个变量,则会得到一个特殊的值None
。
值None和字符串'None'
并不一样。它是一个特殊的值,有自己独特的类型:
到目前为止,我们自定义的函数都是无返回值函数。
12 为什么要有函数
为什么要花功夫将程序拆分成函数呢?也许刚开始编程的时候这其中的原因并不明晰。下面这些解释都可作为参考。
新建一个函数,可以让你有机会给一组语句命名,这样可以让代码更易读和更易调试。
函数可以通过减少重复代码使程序更短小。后面如果你需要修改代码,也只要修改一个地方即可。
将一长段程序拆分成几个函数后,可以对每一个函数单独进行调试,再将它们组装起来成为完整的产品。
一个设计良好的函数,可以在很多程序中使用。书写一次,调试一次,复用无穷。
13 使用from导入模块
Python提供了两种导入模块的方式;我们已经见过其中一种:
如果你导入math
,则会得到名为math
的模块对象。模块对象包含了pi
这样的常量以及诸如sin
和exp
这样的函数。
但是如果直接访问pi
,则会发生错误。
这时候,你可以像下面这样来导入模块中的某个对象:
现在就可以直接访问pi
,而不需要使用句点表示法math.pi
了。
或者,也可以使用星号来导入一个模块的所有成员:
用这种方式导入模块内所有的成员,好处是可以使你的代码更简洁,但缺点是不同模块的同名成员之间,或者和自定义的变量之间,可能发生名字冲突。
14 调试
如果你使用文本编辑器来编写脚本,则可能会遇到缩进时空格和制表符混淆的问题。避免这种问题的最好办法是只使用空格(不用制表符)。大部分识别Python的文本编辑器都默认这么处理,不过也有一些不支持。
制表符和空格都是不可见的,因而会很难调试,所以应尝试找一个能帮你处理缩进的编辑器。
另外,不要忘了在运行程序前保存它。有的开发环境会自动保存,但也有不自动保存的。如果不保存,则你写好的代码和运行的代码并不一样。
如果运行的报错的代码和你写的不一样,调试时会浪费很多时间!
所以一定要确保你眼前所看的代码和所运行的代码是一致的。如果不确定,可以在程序开头写一句print 'hello'
并再运行一次。如果没有看到hello
输出,则你运行的不是正确的程序!
15 术语表
函数(function):一个有名称的语句序列,可以进行某种有用的操作。函数可以接收或者不接收参数,可以返回或不返回结果。
函数定义(function definition):一个用来创建新函数的语句,指定函数的名称、参数以及它执行的语句序列。
函数对象(function object):函数定义所创建的值。函数名可以用作变量来引用一个函数对象。
函数头(header):函数定义的第一行。
函数体(body):函数定义内的语句序列。
形参(parameter):函数内使用的用来引用作为实参传入的值的名称。
函数调用(function call):执行一个函数的语句。它由函数名称和参数列表组成。
实参(argument):当函数调用时,提供给它的值。这个值会被赋值给对应的形参。
局部变量(local variable):函数内定义的变量。局部变量只能在函数体内使用。
返回值(return value):函数的结果。如果函数被当做表达式调用,返回值就是表达式的值。
有返回值函数(fruitful function):返回一个值的函数。
无返回值函数(void function):没有返回值的函数。
模块(module):一个包含相关函数以及其他定义的集合的文件。
import语句(import statement):读入一个模块文件,并创建一个模块对象的语句。
模块对象(module object):使用import
语句时创建的对象,提供对模块中定义的值的访问。
句点表示法(dot notation):调用另一个模块中的函数的语法,使用模块名加上一个句点符号,再加上函数名。
组合(composition):使用一个表达式作为更大的表达式的一部分,或者使用语句作为更大的语句的一部分。
执行流程(flow of execution):程序运行中语句执行的顺序。
栈图(stack diagram):函数栈的图形表达形式,也展示它们的变量,以及这些变量引用的值。
图框(frame):栈图中的一个图框,表达一个函数调用。它包含了局部变量以及函数的参数。
回溯(traceback):当异常发生时,打印出正在执行的函数栈。
16 练习
练习3 Python提供了一个内置函数len
,返回一个字符串的长度。所以len('allen')
的值是5。
编写一个函数right_justify
,接收一个字符串形参s,并打印出足够的前导空白,以达到最后一个字符显示在第70列上。
练习4 函数对象是一个值,你可以将它赋值给变量,或者作为实参传递。例如do_twice
是一个函数,接收一个函数对象作为实参,并调用它两次:
下面是一个使用do_twice
来调用一个print_spam
函数两次的示例:
1.将这个示例存入脚本中并测试它。
2.修改do_twice,让它接收两个实参,一个是函数对象,另一个是一个值,它会调用函数对象两次,并传入那个值作为实参。
3.编写一个更通用的print_spam
,叫做print_twice
,接收一个字符串形参,并打印它两次。
4.使用修改版的do_twice
来调用print_twice
两次,并传入实参'spam'
。
5.定义一个新的函数do_four
,接收一个函数对象与一个值,使用这个值作为实参调用函数4次。这个函数的函数体应该只有两个语句,而不是四个。
练习5 这个练习可以只用语句和我们已经学过的其他语言特性实现。
1.编写一个函数,绘制如下的表格:
提示
:
要在同一行打印多个值,你可以使用逗号分隔不同的值:
print '+','-'
如果值序列的结尾有一个逗号,Python不会换行,所以后面的打印语句会出现在同一行。
print '+',print '-'
这两个语句的输出是'+ -'
。 2.编写一个函数绘制类似的表格,但有4行4列。
解答:http://thinkpython.com/code/grid.py。
鸣谢:这个练习基于Oualline的《实践C编程》第3版(O'Reilly Media,1997)中的一个示例。
① 这一段中讲的参数有两种:函数定义里的形参(parameter),以及调用函数时传入的实参(argument),这里两种是有区分的。——译者注
本文节选自《像计算机科学家一样思考Python》
内容简介
本书按照培养读者像计算机科学家一样的思维方式的思路来教授Python语言编程。全书贯穿的主体是如何思考、设计、开发的方法,而具体的编程语言,只是提供一个具体场景方便介绍的媒介。它并不是一本介绍语言的书,而是一本介绍编程思想的书。和其他编程设计语言书籍不同,它不拘泥于语言细节,而是尝试从初学者的角度出发,用生动的示例和丰富的练习来引导读者渐入佳境。
作者从最基本的编程概念开始讲起,包括语言的语法和语义,而且每个编程概念都有清晰的定义,引领读者循序渐进地学习变量、表达式、语句、函数和数据结构。此外,书中还探讨了如何处理文件和数据库,如何理解对象、方法和面向对象编程,如何使用调试技巧来修正语法、运行时和语义错误。每一章都配有术语表和练习题,方便读者巩固所学的知识和技巧。此外,每一章都抽出一节来讲解如何调试程序。作者针对每章中所专注的语言特性,或者相关的开发问题,总结了调试的方方面面。可以说这是一种非常有益的创新,让初学编程的读者少走很多弯路。
全书共19章和3个附录,详细介绍了Python语言编程的方方面面。这是一本实用的学习指南,适合没有Python编程经验的程序员阅读,也适合高中或大学的学生、Python爱好者及需要了解编程基础的人阅读。对于第一次接触程序设计的人来说,是一本不可多得的佳作。
本文转载自异步社区
原文链接:https://www.epubit.com/articleDetails?id=NC7E3EF91548000018FAE13C312F953F0
- 点赞
- 收藏
- 关注作者
评论(0)