《从问题到程序:用Python学编程和计算》——2.9 计算的抽象和函数

2.9 计算的抽象和函数

前面两节介绍了Python语言的所有控制结构。下面先对它们做一些概括和总结,而后介绍控制结构之上的另一类编程机制:函数定义。
2.9.1 计算的控制和抽象
前面介绍了Python语言的三种控制结构,再加上顺序执行,总共形成了三种基本的计算流程模式,分别是顺序、选择和重复。Python的一些语言结构分别对应于这三种模式。图2.2画出了相应计算流程的图示,这种图也称为流程图。
顺序计算模式就是做完一个操作之后做下一个操作,如图2.2a所示。图中矩形块表示操作,矩形块之间的箭头表示执行的流向。在Python里,位于同一层次的一系列语句描述这种计算模式。注意,由于一系列操作也可以抽象地看作一个组合操作(Python里有相应写法,见本章最后的“语言细节”一节),如果把图中从操作2到操作n看作一个操作(如图中虚线框所示),这里的一系列操作也可以看作两个操作的组合。
选择计算模式根据特定条件,从两个不同操作中选择一个执行,如图2.2b。

其中的菱形框表示条件检查,其两个出口分别标明真和假,表示条件判断后的两个可能流向。Python的if语句实现这种计算流程。应该注意,这样一个复合操作本身也可以抽象地看作一个操作(如虚线框所示),可以放在任何计算流程中作为组成部分。Python里的if结构也是语句,可以出现在程序里任何要求语句的位置,正反映了上面的认识。还应注意,图示中的操作并不限于一个语句,允许是任意复杂的组合操作(复杂的语句结构)。
重复计算模式要求根据某种控制方式重复执行操作,如图2.2c所示,具体控制方式视不同情况而定。Python的for和while语句实现重复计算,它们分别采用迭代器或者逻辑条件控制继续循环体的重复执行或者结束。由于Python有break语句,因此实际可以形成的执行流程比这个图示更复杂一些,但大体情况类似。同样,一个循环结构也可以作为单元出现在任何其他模式中。Python的for和while也是语句,可以作为其他结构的组成部分,形成复杂的嵌套结构。
上述讨论中反复说到的抽象和分解的观点,在1.3.3节里讨论过这个问题。在设计一个程序时,经常需要根据问题的情况,将其划分为顺序的一系列较小计算片段(顺序计算模式),每个片段看作一个抽象的操作。对每个较小片段,又可能需要按某种模式进一步分解,采用Python的if、for或者while结构进一步分解。这样分解下去,直至计算中的条件和操作都能用Python语言的基本功能描述。
上面这种想法非常重要,是开发复杂程序的基本技术。但这样做还是有缺点:随着一步步的分解,早前的抽象部分被一层层展开,程序也会变得越来越长,越来越复杂。各层次的结构搅在一起,程序的可读性和易修改性也会随之逐步恶化。对于开发者而言,直观的感觉就是这个程序越来越复杂、越来越把握不住、越来越难搞了。
今天的复杂程序可能包含多至百万、千万或者上亿行代码。把这么多代码写成一段连续的程序,任何人都无法把握其行为。要克服由程序的复杂性带来的问题,就需要维持程序中的抽象。就像人们在数学和其他科学研究中定义概念一样,编程也不能一直在语言的基本层面上进行,必须不断引入新的高级概念,包装起计算中的复杂性。Python中处理这一问题的最基本机制就是下一小节介绍的函数,其他机制将在后面介绍。
2.9.2 计算的抽象:函数
函数是Python语言的一种重要编程机制,用于将一段计算包装起来,然后就可以以非常方便的方式使用,可以使用一次,也可以使用任意多次。
值的抽象和计算的抽象
编程语言里的变量是最基本抽象机制,用于给对象命名。算出一个所需要的值,可能要做很多工作,如果不给它命名,用过就丢了,再次使用就必须重新计算,至少要花费一些代价,程序也会变得更长。把计算出的值(对象)赋给变量,就为其建立了一个抽象。后面再需要这个值就不必重新计算,只需要写出相应的变量名,就可以方便地使用了。这是建立值抽象的第一层意义:一次计算,可以任意多次使用。
实际上,为值建立抽象(赋给变量)还有另一层重要意义。举例说,程序里通过一段计算得到了值25.4,实际上是一个三角形的面积。把这个值赋给变量area之后,在写下面的程序时,写程序的人就可以基于“面积”这个概念思考问题,而不是基于那段很复杂的计算,或者基于形式上没有反映任何意义的25.4。这个例子也说明变量名的重要作用:合适的名字能成为很好的概念提示,减轻人们在编程中的思维负担。如果到处使用毫无意义的x1、a2或者0、1等,很快就会把自己搞糊涂。古人也知道“名不正言不顺”。给每个变量取一个好名字,是写出好程序的重要一环,绝不可轻视。
然而,需要建立抽象,希望给予适当命名,希望能方便地重复使用的不仅是计算出的值。如前所述,我们还可能希望为(实现一段计算的)一段代码建立抽象,通过命名方便地重复使用。应该注意,计算的抽象与值的抽象性质不同。举个例子,根据前面的编程经验,如果能把计算边长分别为5、7和11的三角形面积的代码封装起来,重复使用也就是重新计算这个三角形,意义比较有限。如果能建立一个计算抽象,它能对任给的三边长计算三角形面积,其作用就很大了。这样一个计算抽象能解决一个计算问题的所有实例。
Python函数定义的意义就是能建立起一般计算过程的抽象:写一次代码,任意多次重复使用,包括对不同的数据做同样计算。基于一段代码定义一个具有计算功能的实体并给予命名,就是定义函数抽象。定义好的函数可以通过名字以简单的方式使用。
前面我们已经看到了函数的意义,使用过许多内置函数和math程序包的函数。一个函数可以完成某种特定计算,其具体的计算过程可能复杂,但使用非常方便。例如,math库里的函数能完成各种数学函数计算。我们不知道其中采用了什么方法,实现有多复杂。但是,只需要调用适当的函数,正确提供参数,就能得到所需的结果。此外,在一个程序(或者一个表达式、一个语句)里,可以任意多次调用同一个数学函数,或调用多个不同的数学函数。由于Python系统提供了这些函数,我们就不需要自己费力去实现了。
易见,函数的特点是一次定义,任意使用。开发math函数库包(以及内置函数等)的人们做了一次,所有使用Python的人都可以用,从中获益。Python语言提供的标准库中有许多有用功能。还有许多人或组织开发了许多有用的第三方Python程序包,提供了许多有用功能。有关情况可查库文档,搜索Python主页或者互联网。
自定义函数的意义
但是,无论Python系统及其标准库提供了多少函数,都不可能满足所有人的所有需要,其他人已经开发出来的功能也未必合用。为了编程的需要,Python允许我们自己定义函数,这样定义的函数称为自定义函数。实际上,网上可以找到的很多公开资源,也就是其他人用Python语言开发的模块,其中一部分就是他们定义的各种函数。
自定义函数真的很有价值吗?作为例子,还是考虑前面基于三边求三角形面积的问题。前面的程序执行一次,能完成一个特定三角形的计算,输出的结果只能供人看,无法简单地继续使用。而如果有一个函数triangle(a, b, c)计算三角形面积,就可以用它写出很多有实际意义的有用代码,例如:

x = triangle(6, 8, 11                        # 记录三角形面积,后面使用
y = triangle(3, 5, 6) * 3                    # 一个三角柱体的体积
z = triangle(13, 15, 16) - triangle(6, 8, 11)    # 一个空心三角形的面积
... ...                                        # 等等

这样的例子无穷无尽,而这只是一个自定义函数。
函数定义
函数定义也是Python的一种复合语句,其执行效果就是定义好一个函数。一个函数定义(语句)包装起一段代码,建立起一个函数抽象,并为其命名。
函数定义的语法是:

def 函数名(参数列表):
    语句组

其中函数名是作为被定义函数的名字的标识符,参数列表是列出的0个或多个名字,多个名字之间用逗号分隔(这是最简单的情况,更复杂的情况在第5章介绍)。这里列出的名字称为所定义函数的形式参数(简称形参),整个圆括号及其包起的部分称为函数的形式参数表。函数定义的最后是一个语句组,称为函数体。函数体之前的部分称为函数头部。
Python语言把函数定义也看作一种复合语句,其语义(其执行)就是根据形式参数表和函数体建立一个函数对象,并将其约束到给定的函数名。此后就可以通过函数名使用所定义的函数了。自定义函数同样通过函数调用的方式使用,其形式前面已介绍:

函数名(实际参数, 实际参数,...)

这里的实际参数是表达式,需要与函数定义的形参匹配,最基本的情况就是一个实参对应一个形参(更复杂的情况留待后面章节讨论)。
函数体是个语句组,函数被调用时,就会执行这个语句组。体中的语句执行完毕,一次函数调用就结束了。但这里还有一个问题:许多函数需要有返回值,编程语言必须提供描述函数返回值的机制。Python语言为此提供了一种语句:函数体里的return语句有特殊作用,用于描述函数的返回值。在进一步介绍之前先看一个例子:
【例】三角形面积的函数及其使用
根据上面讨论可以写出下面代码:

from math import sqrt

def triangle(a, b, c):
    s = (a + b + c) / 2
    area = sqrt(s * (s - a) * (s - b) * (s - c))
    return area

x = triangle(6, 8, 11)
y = triangle(12, 17, 19) - triangle(4, 7, 9)
z = triangle(3, 9, 7) * 4
print("Area of a triangle:", x)
print("Area of a triangle with a hole:", y)
print("Volume of a triangle prism:", z)

这里的def语句定义了一个名字为triangle的函数,函数体照搬前面的代码,其中从形参出发计算出三角形的面积,最后增加了一个return语句。
随后的三个语句里都调用刚刚定义的函数,它们分别计算出一些值。调用自定义函数的过程就是把实参的值送给形参,然后执行函数体包装的语句。执行中遇到return语句时函数结束,求出return所带表达式的值作为函数的结果返回。
现在详细解释return语句。这种语句只能出现在函数里,有两种形式:

return 表达式
return

无论何时执行到一个return语句,当前函数总是立即结束。如果语句包含表达式部分,在函数结束前先算出这个表达式的值,函数结束后以这个值作为函数的返回值。如果语句不包含表达式部分,函数结束返回特殊值None,看作不返回值。
None是Python语言的一个特殊对象,用于表示不存在有意义的值的情况。Python解释器在很多时候也用到这个值。例如赋值语句不像普通表达式,在交互方式下执行不会显示出任何结果。Python就让它的值取None,这个值不显示。再如:

>>> None
>>>

执行一个函数调用时的详细情况如下:
1)从左到右逐个求值实际参数(表达式),得到的值赋给函数定义中对应的形参;
2)执行函数体里的语句;
3)执行中遇return语句时函数的执行立即结束。如果这个return有表达式部分,就求值其表达式作为返回值,没有表达式时返回值为None;
4)所有语句都执行完时函数也结束,返回值为None。
2.9.3 函数定义和使用实例
本小节给出几个函数定义实例。
求三角形面积的函数(修改)
在前面函数定义里,用一个赋值语句把求出的三角形面积赋给变量area,紧随其后的return语句返回area的值。可以看到,这个变量并没有其他作用,只是临时记录一下函数的返回值。对于这种情况,完全可以不引进新变量,直接把计算返回值的表达式写在return语句里,这里允许写任意复杂的表达式:

def triangle(a, b, c):
    s = (a + b + c) / 2
    return sqrt(s * (s - a) * (s - b) * (s - c))

这个定义比前一个略显紧凑,计算效率略高。这种做法值得效仿。
前面说过,并不是任意数都是某个三角形的三条边。在定义计算三角形面积的函数时,也应该考虑这个问题。现在遇到了一个麻烦:计算三角形面积的函数需要返回一个值,如果参数不符合函数要求,返回值怎么办?数学函数库的方法是报错,自定义函数也可以报错,有关情况后面讨论。现在考虑一个简单的方法,让函数返回一个特殊值。
浮点数计算得不到结果,说明计算中遇到了问题,无法得到可以用浮点数表示的值。在Python里,这种值可以用float("nan")表示,其中的nan是Not A Number的缩写(的小写)形式,表示得到的不是一个float能表示的数值。
利用上述机制定义的函数如下:

def triangle(a, b, c):
    if a > 0 and b > 0 and c > 0 and \
       a+b > c and a+c > b and b+c > a:
        s = (a + b + c) / 2
        return sqrt(s * (s - a) * (s - b) * (s - c))
    else:
        return float("nan")

注意,由于描述if条件的表达式很长,这里同样使用续行。
调用这种函数时有一个麻烦:它可能返回正常结果,也可能返回非正常的特殊值。如果能保证送给函数的参数都合适,例如在调用函数之前检查实参,就不需要担心第二种情况。计算中出错是经常需要考虑的情况,Python有专门机制处理这方面问题。
计算并输出三角形面积的函数
有时我们定义函数不是为了计算出一个值,而是希望实现一些操作效果。例如,内置函数print并没完成什么有意义的计算,但也很有用。有时我们也需要定义特殊的输出函数,例如完成某些计算然后输出结果的函数。
现在考虑定义一个计算三角形面积并且输出这个面积的函数。根据已有知识,这个函数可以如下定义:

from math import sqrt

def print_triangle(a, b, c):
    if a > 0 and b > 0 and c > 0 and \
       a+b > c and a+c > b and b+c > a:
        s = (a + b + c) / 2
        area = sqrt(s * (s - a) * (s - b) * (s - c))
        print("Area of triangle:", area)
    else:
        print(a, b, c, "do not form a triangle.")

print_triangle(6, 8, 11)
print_triangle(4, 7, 9)
print_triangle(12, 17, 19)

这里还包含了几个使用函数的语句,执行这个脚本时就能看到输出。
自定义输出函数是一类常见的不返回值的函数,后面还会看到其他情况。
计算阶乘的函数
把计算中的关键部分提取出来定义为函数,可以使程序的结构更清晰,意义也更容易理解。例如,下面是另一个阶乘计算器脚本,这里定义了一个函数完成阶乘计算,随后的代码实现循环控制,其中调用前面定义的函数:

def fact(n):
    prod = 1
    for k in range(2, n+1):
        prod = prod * k
    return prod

while True:
    n = int(input("Next int:"))
    if n < 0:
        break
    print("Factorial of", n, "is", fact(n))

print("Bye!")

与前面实现同样功能的程序相比,这个程序多了几行。把阶乘计算的部分独立出来,使之脱离复杂的全局控制结构,这个新写法使程序清晰得多。可见,虽然有时定义的函数在程序里只使用一次,但做出这个函数定义也很值得。
函数可以包装任意复杂的计算,在函数体的实现中可以使用任何语句,有关语句可以任意地嵌套。此外,return语句不一定出现在函数的最后,可以写在函数里的任何地方,包括出现在循环或者其他结构中。

时间: 2024-12-04 08:01:26

《从问题到程序:用Python学编程和计算》——2.9 计算的抽象和函数的相关文章

《从问题到程序:用Python学编程和计算》——2.12 练习

2.12 练习 概念和理解 复习下面概念:表达式,语句,赋值,控制结构,函数,提示符,值,求值,语法错误,单词,整数,浮点数,运算符,一元运算符,二元运算符,优先级,结合顺序,字面量,语法错误,续行,续行符,尾数,指数,精度,溢出,数值计算,误差,近似计算,对象,类型,整型,浮点型,类型名,数值类型,内置类型(标准类型),类型转换,强制类型转换,内置函数(标准函数),程序包,函数调用,实际参数(实参),返回值,程序包,导入,字符串,下标,下标越界,字符串长度,拼接,切片,变量,标识符,关键字,赋

《从问题到程序:用Python学编程和计算》——第2章 计算和编程初步 2.1 数值表达式和算术

第2章 计算和编程初步 写程序是为了实现所需要的计算,计算中总需要处理数据,因此写程序时必然要涉及数据的描述,以及从数据出发的计算过程描述. 人们在学数学时已经写过许多数学表达式,其中的一些表达式描述的就是从一些数值出发的计算过程,例如下面这个数学表达式: 这一表达式描述的是从一些数值出发,通过三角函数和各种算术运算,要求算出一个结果.复杂的计算需要用计算机完成,因此在用Python(或其他语言)写程序时,也经常需要写这种计算描述.上面数学表达式对应的Python语言描述是: 5.17 + 1.

《从问题到程序:用Python学编程和计算》——第1章 程序设计和Python 1.1 计算机和程序

第1章 程序设计和Python 我们已经生活在信息时代,环顾四周,信息技术的影响无处不在.由于信息科学技术的发展和应用,我们的世界的方方面面都与20年前大不相同了,例如: 个人生活:看看人们在每天生活中做的各种事情,有多少是在与屏幕键盘(可能是触摸屏)交互,这些都是20年前没有的事情. 人际交流:20年前的人际交流方式很简单.除面对面交流外,只能通过纸笔写信或长途电话(要找专门的电话或者到电话局).今天人手一部手机,可以通过电话.短信.各种网络即时消息相互交流.电子邮件也是私人之间的交流媒介,而

《从问题到程序:用Python学编程和计算》——导读

前 言 计算机诞生至今不过六七十年,但它已经改变了世界,改变了每个人的生活.人们每天都在与计算机交流(如智能手机),各领域专业人员的大量日常工作都需要使用计算机,从事与计算机相关工作的人们已经发展为社会上最大的专业技术社团.计算机的研究和应用.互联网和其他相关领域,还在不断呼唤大量熟悉计算机的专业开发人才.计算机科学技术的开发和应用能力已被广泛认为是国家竞争力的重要组成部分.因此,学习计算机科学技术知识,不仅是社会发展的需要,而且已成为个人的重要职业竞争力.然而,要深入理解计算和计算机,使其成为

《从问题到程序:用Python学编程和计算》——1.2 Python语言简介

1.2 Python语言简介 本节将首先简单介绍Python语言的一些基本情况,包括其发展和使用的情况.而后介绍Python语言系统的安装和使用方面的基本常识.1.2.1 Python语言的发展和应用 Python语言是CWI(荷兰国家数学和计算机研究中心)的程序员Guido van Rossum在1989年开始开发的一种高级编程语言,当时的主要设计目标是希望能用于方便地管理CWI的Amoeba操作系统.后来,由于其各方面的优点而逐渐流行起来. Python语言现在由Python软件基金会(Py

《从问题到程序:用Python学编程和计算》——2.8 重复计算和循环

2.8 重复计算和循环 在前面几节,我们首先看到如何通过语句的顺序组合构造最简单的程序,这种程序是直线型程序,就是简单的一系列语句.这样的程序中只有一条执行路径(一种可能执行方式):Python解释器顺序执行程序里的语句,每个语句执行一次,当语句序列中最后一条语句的执行结束时,整个程序的执行就结束了. 增加了if复合语句,能写出的程序更多,程序的形式也更丰富,其中出现了选择和分支.这样得到的程序可称为分支程序.在分支程序里,每条基本语句最多执行一次,如果实际条件导致的执行没进入某个分支,该分支里

《从问题到程序:用Python学编程和计算》——3.4 定义函数

3.4 定义函数 在最简单的程序中,可能只用到表达式.语句和几种控制结构.但是,仅限于这些基本机制,很难写出很长的解决复杂问题的程序.随着遇到的问题更复杂,我们必须组织好程序的结构,在语句层面之上的基本结构就是函数.一个函数包装起一段代码并给予命名,引进参数将其通用化.定义好的函数可以通过调用表达式使用,非常方便.学习编程的重要一步就是学习定义函数:理解为什么需要定义函数,学会识别编程中定义函数的需求,掌握正确定义函数的技术.本小节和下一章将集中讨论这个问题.3.4.1 为什么定义函数 实际中需

《从问题到程序:用Python学编程和计算》——第3章 基本编程技术 3.1 循环程序设计

第3章 基本编程技术 第2章讨论了简单的计算和编程,展示了一些实例.通过对有关内容的学习,读者应该已经做了一些简单程序,对写程序和做计算有了些实际体会.虽然编程中细节较多,但也是很有趣的工作.为了完成一个程序,首先要分析问题.寻找解决方案,这些需要发挥人的聪明才智和想象力,也可能涉及一些相关领域的知识.要把设计变成可以运行的程序,既需要智力,也需要有条理的工作,一个小错误就可能使程序不能正确执行.当然,高度精确性也是现代社会对人的基本要求,写程序的过程能给我们许多有益的经验. 学习编程要经历一个

《从问题到程序:用Python学编程和计算》——1.3 程序开发

1.3 程序开发 在用Python学习编程时,自然需要了解Python语言,但更重要的是学习.理解和运用人们长期程序设计工作总结出的经验,包括正确的思考问题方法.正确的程序开发方法以及一些有益的常规做法,还要养成良好的编程习惯.随着学习的深入,需要解决的问题也会变得越来越复杂(当然,实际中的问题和解决它们的程序更复杂得多).比较复杂的东西不是随随便便就能做好的,需要认真工作,也需要正确的工作方法.本书中许多地方提出了这些方面的建议,希望引起读者的重视. 本节简单讨论程序的开发过程,包括程序的设计

《从问题到程序:用Python学编程和计算》——2.11 补充材料

2.11 补充材料 本书各章的主要内容将围绕着怎样通过编程解决计算问题展开,正文中对Python语言的机制只做必要的说明,有些细节情况没有涉及.另外,用Python编程也有许多有趣而且有用的技术.如果在各章的主要部分详细罗列,也可能冲淡讨论的主线.但是上述两方面的一些情况也值得介绍.本书采用的方法是在一些章的最后增加称为"补充材料"一节,补充一些细节,供读者参考,也供用本书教授课程的教师选用. 除了讨论语言细节和编程技术的两个小节外,有时还总结了一些常用的编程模式.练习中的第1题总是对