1 函数调用在程序设计中,函数是指用于进行某种计算的一系列语句的有名称的组合。定义一个函数时,需要指定函数的名称并写下一系列程序语句。之后,就可以使用名称来“调用”这个函数。前面我们已经见过函数调用的例子:
>>> type(32)<type 'int'>这个函数的名称是 type,括号中的表达式我们称之为函数的参数。这个函数调用的结果是参数的类型。
我们通常说函数“接收”参数,并“返回”结果。这个结果称为返回值(return value)。
2 类型转换函数Python提供了一些可将某个值从一种类型转换为另一种类型的内置函数。int函数可以把任何可以转换为整型的值转换为整型;如果转换失败,则会报错:
>>> int('32')32>>> int('Hello')ValueError: invalid literal for int(): Helloint可以将浮点数转换为整数,但不会做四舍五入操作,而是直接舍弃小数部分。
>>> int(3.99999)3>>> int(-2.3)-2float函数将整数和字符串转换为浮点数:
>>> float(32)32.0>>> float('3.14159')3.14159最后,str函数将参数转换为字符串:
>>> str(32)'32'>>> str(3.14159)'3.14159'3 数学函数Python有一个数学计算模块,提供了大多数常用的数学函数。模块是指包含一组相关的函数的文件。
要想使用一个模块,需要先将它导入(import)运行环境:
>>> import math这个语句将会建立一个名为math的模块对象(module object)。如果打印这个对象,可以看到它的一些信息:
>>> print math<module 'math' (built-in)>模块对象包含了这个模块中定义的函数和变量。若要访问其中的一个函数,需要同时指定模块名称和函数名称,用一个句点(.)分隔。这个格式称为句点表示法(dot notation)。
>>> ratio = signal_power / noise_power>>> decibels = 10 * math.log10(ratio)>>> radians = 0.7>>> height = math.sin(radians)上面第一个例子使用了log10来计算以分贝为单位的信号/噪声比(假设signal_power和noise_power都已经事先定义好了)。math模块也提供了log函数,用来计算底为e的自然对数。
第二个例子计算radians的正弦值。这个变量名已经暗示了,sin以及cos、tan等三角函数接受的参数是以弧度(radians)为单位的。若要将角度转换为弧度,可以除以360再乘以2π:
>>> degrees = 45>>> radians = degrees / 360.0 * 2 * math.pi>>> math.sin(radians)0.707106781187表达式math.pi从math模块中获得变量pi。这个变量的值是π的近似值,大约精确到15位数字。
如果你了解三角函数,可以把上面的结果和2的平方根的一半进行比较:
>>> math.sqrt(2) / 2.00.7071067811874 组合到现在为止,我们已经分别了解了程序的基本元素——变量、表达式和语句,但还没有接触如何将它们有机地组合起来。
程序设计语言最有用的特性之一就是可以将各种小的构建块(building block)组合起来。比如,函数的参数可以是任何类型的表达式,包括算术符号:
x = math.sin(degrees / 360.0 * 2 * math.pi)甚至还包括函数调用:
x = math.exp(math.log(x+1))基本上,在任何可以使用值的地方,都可以使用任意表达式,只有一个例外:赋值表达式的左边必须是变量名称,在左边放置任何其他的表达式都是语法错误(后面我们还会看到这条规则的例外情况)。
>>> minutes = hours * 60 # 正确>>> hours * 60 = minutes # 错误!SyntaxError: can't assign to operator5 添加新函数至此,我们都只是在使用Python提供的函数,其实我们也可以自己添加新的函数。函数定义指定新函数的名称,并提供一系列程序语句。当函数被调用时,这些语句会顺序执行。
下面是一个例子:
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。函数名称的书写规则和变量名称一样:字母、数字和某些标点是合法的,但第一个字符不能是数字。关键字不能作为函数名,而且我们应尽量避免函数和变量同名。
函数名后的空括号表示它不接收任何参数。
函数定义的第一行称为函数头(header),其他部分称为函数体(body)。函数头应该以冒号结束,函数体整体缩进一级。依照惯例,缩进总是使用4个空格,参看3.14节。函数体的代码语句行数不限。
本例中print语句里的字符串使用双引号括起来。单引号和双引号的作用相同。大部分情况下,人们都使用单引号,只在本例中这样的特殊情况下才使用双引号。本例中的字符串里本身就存在单引号(单引号也作为省略符号用,如I'm)。
如果在交互模式里输入函数定义,则解释器会输出省略号(...)提示你当前的定义还没有结束:
>>> def print_lyrics(): ... print "I'm a lumberjack, and I'm okay." ... print "I sleep all night and I work all day." ...想要结束这个函数的定义,需要输入一个空行(在脚本文件中则不需要如此)。
定义一个函数后,会创建一个同名的变量。
>>> print print_lyrics <function print_lyrics at 0xb7e99e9c> >>> type(print_lyrics) <type 'function'>变量print_lyrics的值是一个函数对象,其类型是'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.当然,这首歌其实并不是这么唱的。
6 定义和使用将前面一节零散的代码整合起来,整个程序就像下面这个样子:
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将程序的最后一行移动到首行,于是函数调用会先于函数定义执行。运行程序并查看会有什么样的错误信息。
练习2将函数调用那一行放回到末尾,并将函数print_lyrics的定义放到函数repeat_lyrics定义之后。这时候运行程序会发生什么?
7 执行流程为了保证函数的定义先于其首次调用执行,你需要知道程序中语句执行的顺序,即执行流程。
执行总是从程序的第一行开始。从上到下,按顺序,每次执行一条语句。
函数定义并不会改变程序的执行流程,但应注意函数体中的语句并不立即执行,而是等到函数被调用时执行。
函数调用可以看作程序执行流程中的一个迂回路径。遇到函数调用时,并不会直接继续执行下一条语句,而是跳到函数体的第一行,继续执行完函数体的所有语句,再跳回到原来离开的地方。
这样看似简单,但马上你会发现,函数体中可以调用其他函数。当程序流程运行到一个函数之中时,可能需要执行其他函数中的语句。但当执行那个函数中的语句时,又可能再需要调用执行另一个函数的语句!
幸好Python对于它运行到哪里有很好的记录,所以每个函数执行结束后,程序都能跳回到它离开的地方。直到执行到整个程序的结尾,才会结束程序。
前面这段枯燥的描述,寓意何在?当你阅读代码时,并不总是应该一行行按照书写顺序阅读。有时候,按照执行的流程来阅读代码,可能理解效果更好。
8 形参和实参①我们已经看到,有些内置函数需要传入参数。比如,当调用math.sin时,需要传入一个数字作为实参。有的函数需要多个实参:math.pow需要两个,分别是基数(base)和指数(exponent)。
在函数内部,实参会被赋值给形参。下面的例子是一个用户自定义的函数,接收一个实参:
def print_twice(bruce): print bruce print bruce这个函数在调用时会把实参的值赋到形参bruce上,并将其打印两次。
这个函数对任何可以打印的值都可用。
>>> print_twice('Spam')SpamSpam>>> print_twice(17)1717>>> print_twice(math.pi)3.141592653593.14159265359内置函数的组合规则,在用户自定义函数上也同样可用,所以我们可以对print_twice使用任何表达式作为实参:
>>> print_twice('Spam '*4)Spam Spam Spam SpamSpam Spam Spam Spam>>> print_twice(math.cos(math.pi))-1.0-1.0作为实参的表达式会在函数调用之前先执行。所以在这个例子中,表达式'Spam'*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。
9 变量和形参是局部的当你在函数体内新建一个变量时,它是局部的(local),即它只存在于这个函数之内。比如:
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 catNameError: name 'cat' is not defined形参也是局部的。比如,在print_twicebruce这个变量。
10 栈图要跟踪哪些变量在哪些地方使用,有时候画一个栈图(stack diagram)会很方便。和状态图一样,栈图可以展示每个变量的值,不同的是它会展示每个变量所属的函数。
每个函数使用一个帧包含,帧在栈图中就是一个带着函数名称的盒子,里面有函数的参数和变量。前面的函数示例的栈图如图1所示。
图1 栈图
图中各个帧从上到下安排成一个栈,能够展示出哪个函数被哪个函数调用了。在这个例子里,printtwice被cattwice调用,而cattwice被main调用。_main是用于表示整个栈图的图框的特别名称。当你在所有函数之外新建变量时,它就是属于__main的。
每个形参都指向与其对应的实参相同的值,所以,part1和line1的值相同,part2和line2的值相同,而bruce和cat的值相同。
如果调用函数的过程中发生了错误,Python会打印出函数名,以及调用它的函数的名称,以及调用这个调用者的函数名,依此类推,一直到main。
比如,如果你在print_twice中访问cat变量,则会得到一个NameError:
Traceback (innermost last): File "test.py", line 13, in __main__ cat_twice(line1, line2) File "test.py", line 5, in cat_twice print_twice(cat) File "test.py", line 9, in print_twice print catNameError: name 'cat' is not defined上面这个函数列表被称为回溯(traceback)。它告诉你错误出现在哪个程序文件,哪一行,以及哪些函数正在运行。它也会显示导致错误的那一行代码。
回溯中函数的顺序和栈图中图框的顺序一致。当前正在执行的函数列在最底部。
11 有返回值函数和无返回值函数我们使用过的函数中,有一部分函数,如数学函数,会产生结果。因为没有想到更好的名字,我称这类函数为有返回值函数(fruitful function)。另一些函数,如print_twice,会执行一个动作,但不返回任何值。我们称这类函数为无返回值函数(void function)。
当你调用一个有返回值的函数时,大部分情况下都想要对结果做某种操作。比如,你可能会想把它赋值给一个变量,或者用在一个表达式中:
x = math.cos(radians)golden = (math.sqrt(5) + 1) / 2在交互模式中调用函数时,Python会直接显示结果:
>>> math.sqrt(5)2.2360679774997898但是在脚本中,如果只是直接调用这类函数,那么它的返回值就会永远丢失掉!
math.sqrt(5)这个脚本计算5的平方根,但由于并没有把计算结果存储到某个变量中,或显示出来,所以其实没什么实际作用。
无返回值函数可能在屏幕上显示某些东西,或者有其他的效果,但是它们没有返回值。如果你试着把它们的结果赋值给某个变量,则会得到一个特殊的值None。
>>> result = print_twice('Bing')BingBing>>> print resultNone值None和字符串'None'并不一样。它是一个特殊的值,有自己独特的类型:
>>> print type(None)<type 'NoneType'>到目前为止,我们自定义的函数都是无返回值函数。
12 为什么要有函数为什么要花功夫将程序拆分成函数呢?也许刚开始编程的时候这其中的原因并不明晰。下面这些解释都可作为参考。
新建一个函数,可以让你有机会给一组语句命名,这样可以让代码更易读和更易调试。函数可以通过减少重复代码使程序更短小。后面如果你需要修改代码,也只要修改一个地方即可。将一长段程序拆分成几个函数后,可以对每一个函数单独进行调试,再将它们组装起来成为完整的产品。一个设计良好的函数,可以在很多程序中使用。书写一次,调试一次,复用无穷。13 使用from导入模块Python提供了两种导入模块的方式;我们已经见过其中一种:
>>> import math>>> print math<module 'math' (built-in)>>>> print math.pi3.14159265359如果你导入math,则会得到名为math的模块对象。模块对象包含了pi这样的常量以及诸如sin和exp这样的函数。
但是如果直接访问pi,则会发生错误。
>>> print piTraceback (most recent call last): File "<stdin>", line 1, in <module>NameError: name 'pi' is not defined这时候,你可以像下面这样来导入模块中的某个对象:
>>> from math import pi现在就可以直接访问pi,而不需要使用句点表示法math.pi了。
>>> print pi3.14159265359或者,也可以使用星号来导入一个模块的所有成员:
>>> from math import *>>> cos(pi)-1.0用这种方式导入模块内所有的成员,好处是可以使你的代码更简洁,但缺点是不同模块的同名成员之间,或者和自定义的变量之间,可能发生名字冲突。
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):当异常发生时,打印出正在执行的函数栈。
本文节选自《像计算机科学家一样思考Python》
内容简介
本书按照培养读者像计算机科学家一样的思维方式的思路来教授Python语言编程。全书贯穿的主体是如何思考、设计、开发的方法,而具体的编程语言,只是提供一个具体场景方便介绍的媒介。它并不是一本介绍语言的书,而是一本介绍编程思想的书。和其他编程设计语言书籍不同,它不拘泥于语言细节,而是尝试从初学者的角度出发,用生动的示例和丰富的练习来引导读者渐入佳境。
作者从最基本的编程概念开始讲起,包括语言的语法和语义,而且每个编程概念都有清晰的定义,引领读者循序渐进地学习变量、表达式、语句、函数和数据结构。此外,书中还探讨了如何处理文件和数据库,如何理解对象、方法和面向对象编程,如何使用调试技巧来修正语法、运行时和语义错误。每一章都配有术语表和练习题,方便读者巩固所学的知识和技巧。此外,每一章都抽出一节来讲解如何调试程序。作者针对每章中所专注的语言特性,或者相关的开发问题,总结了调试的方方面面。可以说这是一种非常有益的创新,让初学编程的读者少走很多弯路。
全书共19章和3个附录,详细介绍了Python语言编程的方方面面。这是一本实用的学习指南,适合没有Python编程经验的程序员阅读,也适合高中或大学的学生、Python爱好者及需要了解编程基础的人阅读。对于第一次接触程序设计的人来说,是一本不可多得的佳作。