像计算机科学家一样思考Python

978-7-115-32092-6
作者: 【美】Allen B. Downey
译者: 赵普明
编辑: 杨海玲
分类: Python

图书目录:

详情

这是一本实用的Python语言学习指南。本书从最基本的编程概念开始讲起,包括语言语法和语义,而且每个编程概念都有清晰的定义,引领读者循序渐进地学习值、变量、语句、函数和数据结构。书中还会探讨如何处理文件和数据库,如何理解对象、方法,以及面向对象编程,如何使用调试技巧来修正语法、运行时和语义错误,此外还会通过案例分析探讨接口设计、数据结构和基于GUI的程序。

图书摘要

版权信息

书名:像计算机科学家一样思考Python

ISBN:978-7-115-32092-6

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] Allen B. Downey

  译    赵普明

  责任编辑 杨海玲

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Copyright ©2012 by O’Reilly Media, Inc.

Simplified Chinese Edition, jointly published by O’Reilly Media, Inc. and Posts & Telecom Press, 2013. Authorized translation of the English edition, 2013 O’Reilly Media, Inc., the owner of all rights to publish and sell the same.

All rights reserved including the rights of reproduction in whole or in part in any form.

本书中文简体版由O'Reilly Media,Inc.授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式复制或抄袭。

版权所有,侵权必究。


本书按照培养读者像计算机科学家一样的思维方式的思路来教授Python语言编程。全书贯穿的主体是如何思考、设计、开发的方法,而具体的编程语言,只是提供一个具体场景方便介绍的媒介。它并不是一本介绍语言的书,而是一本介绍编程思想的书。和其他编程设计语言书籍不同,它不拘泥于语言细节,而是尝试从初学者的角度出发,用生动的示例和丰富的练习来引导读者渐入佳境。

作者从最基本的编程概念开始讲起,包括语言的语法和语义,而且每个编程概念都有清晰的定义,引领读者循序渐进地学习变量、表达式、语句、函数和数据结构。此外,书中还探讨了如何处理文件和数据库,如何理解对象、方法和面向对象编程,如何使用调试技巧来修正语法、运行时和语义错误。每一章都配有术语表和练习题,方便读者巩固所学的知识和技巧。此外,每一章都抽出一节来讲解如何调试程序。作者针对每章中所专注的语言特性,或者相关的开发问题,总结了调试的方方面面。可以说这是一种非常有益的创新,让初学编程的读者少走很多弯路。

全书共19章和3个附录,详细介绍了Python语言编程的方方面面。这是一本实用的学习指南,适合没有Python编程经验的程序员阅读,也适合高中或大学的学生、Python爱好者及需要了解编程基础的人阅读。对于第一次接触程序设计的人来说,是一本不可多得的佳作。


1999年,我正在为一门Java的编程入门课程备课。这门课我已经教过3个学期,感到有些灰心。课程的不及格率太高,即使是那些及格的学生,也只获得了很低的成就。

我发现问题之一是教材。它们太厚,有太多冗余的细节,而针对编程技巧的高阶的指导却很不足。并且学生们都受着“陷阱效应”的苦恼:开头时很容易,也能循序渐进,但接着在第5章左右,整个地板就突然陷落了。新资讯来得太多、来得太快,以至于我必须花费一学期剩下的全部时间来帮助他们拾回丢失的片段。

开课前两周,我决定自己来编写教材。我的目标有以下几个。

我需要一个标题,所以心血来潮选择了How to Think Like a Computer Scientist

第一版教材很粗糙,但确实有效。学生们读完课本,懂得了足够的基础知识,以至我甚至可以利用课堂时间和他们一起讨论更难、更有趣的话题,并且(最重要的是)可以让学生们有足够的时间在课堂上做练习。

我将这本书按照GNU自由文档许可协议(GNU Free Documentation License)发布,让用户可以复制、修改和分发本书。

接下来发生了最酷的事情。Jeff Elkner,弗吉尼亚州的一位高中老师,使用了我的书,并且将其翻译成Python语言的版本。他寄给我他的翻译副本,于是我有了一次很奇特的经历——通过读我自己的书来学习Python。通过绿茶出版社(Green Tea Press),在2001年我出版了第一个Python版本。

2003年,我开始在欧林学院(Olin College)教学,并第一次需要教授Python语言。和Java的对比非常惊人。学生们困扰更少,学会得更多,从事更有意思的项目,总的来说得到了更多的乐趣。

结果就产生了本书,并使用了不那么宏伟堂皇的书名:Think Python。部分改动如下所述。

我希望你喜欢这本书,并希望它至少能提供一点帮助,助你学会像计算机科学家那样编程和思考。

—— Allen B. Downey

Needham,MA


Allen Downey是欧林工程学院的计算机科学教授。他曾在韦尔斯利学院、科尔比学院和加州大学伯克利分校教授计算机科学课程。他从加州大学伯克利分校获得计算机科学博士学位,并拥有MIT的硕士和学士学位。


本书封面的动物是卡罗来纳鹦鹉,也叫卡罗来纳长尾鹦鹉(学名Conuropsis carolinensis)。这种鹦鹉分布于美国东南部,最北到达纽约和大湖区,但主要分布在佛罗里达州到卡罗来纳州一带。

卡罗来纳鹦鹉主色是绿色,头部黄色,成熟时前额和两颊会出现一些橙红色的条纹。 它的平均尺寸是31~33cm。它叫声狂暴而巨大,并且在捕食过程中会喋喋不休。

它居住在沼泽与河畔的树洞中。卡罗来纳鹦鹉是喜欢群居的生物,平时以小群体形式生活,在捕食时可以达到几百只。

不幸的是,这些捕食过程往往在农田的庄稼地里进行,农夫会射击它们,以免破坏庄稼。它们的群体特性让它们会集体救助受伤的鹦鹉,结果让农夫可以一次杀光整群鹦鹉。不但如此,它们的羽毛被用做妇女的帽饰,也有一些鹦鹉被作为宠物。这些因素组合起来,导致在19世纪晚期,卡罗来纳鹦鹉变得非常稀少,并且禽类疾病也加剧了它们的减少。到1920年左右,这个物种灭绝了。

今天,全世界的博物馆中保存了700多只卡罗来纳鹦鹉的标本。

封面图片来自《约翰逊的自然历史》(Johnson’s Natural History)。


《像计算机科学家一样思考》这一系列书,早有耳闻,它可谓开创了程序设计入门书的一个新思路。授人以鱼,不若授人以渔;教人编程,不如引导人思考;教人语言细节,不若指明语言精要。而结合Python语言之后,得到的《像计算机科学家一样思考Python》这本书,则是在这个思路上走到了一个极致的佳作。

我是工作之后才开始接触Python的。那时候已经接触过C/C++、Java、C#等传统风格的语言,再看到Python,不免耳目一新。为何以前觉得难以理解的程序设计理念,在Python中的表达却这么简洁而易懂?为何以往需要好多行代码绞尽脑汁才能编写出来的功能,在Python里却只需要几个简单调用即可?为何繁复的集合操作,在Python中却只需要一行for循环语句就完成了?为何Python的文档那么容易找,还可以使用交互模式轻松尝试?每次使用Python编写程序之后,总会感慨,当初初学程序设计语言的时候,如果教的是Python该多好。相信所有学过C/C++之后再接触Python、Ruby、Haskell、Lisp等类似语言的人,都会有相同的感受吧。

那么是什么原因让C/C++几乎垄断了程序设计语言的教材呢?历史惯性。在计算机科学教育开始普及的20世纪70、80年代,C语言正在其鼎盛时期,几乎所有的人都在用C开发程序,操作系统、软件、游戏几乎都是用C甚至汇编开发的。硬件的限制,让那些更抽象、更高阶的语言,无法普及开来。因此教学自然也使用它。久而久之形成了惯性,到了新世纪,程序设计的教学已经赶不上语言发展的潮流了。我们的程序越来越复杂,越来越像人脑,而教学的语言仍然在使用最贴近机器语言的C。而C++、Java、C#不过是在这一惯性上多走了五十步而已。

本书正是扭转这种矛盾局面的一个有益的尝试。《像计算机科学家一样思考》是对程序设计教学模式的真谛的领悟,而使用Python这种简洁强大的高阶语言,也正是这种新思路最贴切的贯彻。授人以渔,自然应当用最好的渔具;引导人思考,当然也应使用更贴近人的思路而不是机器思路的语言。Python在高阶语言中,是一个从理念和实际综合考量后非常合适的候选。

在翻译过程中我发现,本书不但思路很贴切其教学主旨,从行文和用例来看也非常浅显易懂。全书讲了非常多的程序设计理念,在读过之后却会觉得那些理念都很自然,大概也是因为作者苦心安排,前后穿插,让读者能循序渐进地明白每个程序设计理念是因为什么出现的原因吧。这种风格,再配合上精心编辑的示例,用于介绍任何程序设计语言,都是非常合适的。

如果将来我的孩子愿意学习程序设计,我愿意用这本书教他。

尽管我已尽最大努力用心使译文准确、完善,但仍然难免有疏漏之处,如发现问题,欢迎批评指正。电子邮箱 zhaopuming@gmail.com。


赵普明 清华大学计算机科学技术专业毕业,长期从事Web应用、高性能服务器以及计算平台的开发。从2.3版本开始接触Python,至今已逾5年。工作中使用Python编写脚本程序,用于快速原型构建以及日志计算等日常作业;业余时,作为一个编程语言爱好者,对D、Kotlin、Clojure、Scala、Ruby等语言均有了解,但至今仍为Python独特的风格、简洁的设计而惊叹。


非常感谢Jeff Elkner,他将我的Java书翻译成Python,致使我开始这个项目,并向我介绍了Python语言,结果成为我最爱的编程语言。

另外感谢Chris Meyers,他在How to Think Like a Computer Scientist一书中贡献了好几章。

感谢自由软件基金会(Free Software Foundation)开发了GNU自由文档协议,让我和Jeff以及Chris的合作成为可能。感谢创用CC(Creative Commons)开发了我们现在使用的协议。

感谢Lulu,负责How to Think Like a Computer Scientist的编辑。

感谢所有参与了本书早期版本编写的学生,以及所有(下面列出的)贡献者提供的修订和建议。


在最近几年中,超过100名眼光犀利、思维敏捷的读者给我寄来了建议和修订。他们对这个项目的贡献和热情,对我是极大的帮助。如果你有建议或者修订意见,请发邮件到feedback@thinkpython.com。如果我根据你的回馈做出了修改,会将你加入到贡献者列表中(除非你要求被隐藏)。

如果你给出错误出现的位置的部分语句,会让我更容易搜索。页码或者章节号码也可以,但并不那么容易处理。谢谢!


O’Reilly Media通过图书、杂志、在线服务、调查研究和会议等方式传播创新知识。自1978年开始,O’Reilly一直都是前沿发展的见证者和推动者。超级极客们正在开创着未来,而我们关注真正重要的技术趋势——通过放大那些“细微的信号”来刺激社会对新科技的应用。作为技术社区中活跃的参与者,O’Reilly的发展充满了对创新的倡导、创造和发扬光大。

O’Reilly为软件开发人员带来革命性的“动物书”;创建第一个商业网站(GNN);组织了影响深远的开放源代码峰会,以至于开源软件运动以此命名;创立了Make杂志,从而成为DIY革命的主要先锋;公司一如既往地通过多种形式缔结信息与人的纽带。O’Reilly的会议和峰会集聚了众多超级极客和高瞻远瞩的商业领袖,共同描绘出开创新产业的革命性思想。作为技术人士获取信息的选择,O’Reilly现在还将先锋专家的知识传递给普通的计算机用户。无论是通过书籍出版,在线服务或者面授课程,每一项O’Reilly的产品都反映了公司不可动摇的理念——信息是激发创新的力量。

业界评论

“O’Reilly Radar博客有口皆碑。”

——Wired

“O’Reilly凭借一系列(真希望当初我也想到了)非凡想法建立了数百万美元的业务。”

——Business 2.0

“O’Reilly Conference是聚集关键思想领袖的绝对典范。”

——CRN

“一本O’Reilly的书就代表一个有用、有前途、需要学习的主题。”

——Irish Times

“Tim是位特立独行的商人,他不光放眼于最长远、最广阔的视野并且切实地按照Yogi Berra的建议去做了:‘如果你在路上遇到岔路口,走小路(岔路)。’回顾过去Tim似乎每一次都选择了小路,而且有几次都是一闪即逝的机会,尽管大路也不错。”

——Linux Journal


在程序设计中,函数是指用于进行某种计算的一系列语句的有名称的组合。定义一个函数时,需要指定函数的名称并写下一系列程序语句。之后,就可以使用名称来“调用”这个函数。前面我们已经见过函数调用的例子:

>>> type(32)
<type 'int'>

这个函数的名称是type,括号中的表达式我们称之为函数的参数。这个函数调用的结果是参数的类型。

我们通常说函数“接收”参数,并“返回”结果。这个结果称为返回值(return value)。

Python提供了一些可将某个值从一种类型转换为另一种类型的内置函数。int函数可以把任何可以转换为整型的值转换为整型;如果转换失败,则会报错:

>>> int('32')
32
>>> int('Hello')
ValueError: invalid literal for int(): Hello

int可以将浮点数转换为整数,但不会做四舍五入操作,而是直接舍弃小数部分。

>>> int(3.99999)
3
>>> int(-2.3)
-2

float函数将整数和字符串转换为浮点数:

>>> float(32)
32.0
>>> float('3.14159')
3.14159

最后,str函数将参数转换为字符串:

>>> str(32)
'32'
>>> str(3.14159)
'3.14159'

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_powernoise_power都已经事先定义好了)。math模块也提供了log函数,用来计算底为e的自然对数。

第二个例子计算radians的正弦值。这个变量名已经暗示了,sin以及costan等三角函数接受的参数是以弧度(radians)为单位的。若要将角度转换为弧度,可以除以360再乘以2π:

>>> degrees = 45
>>> radians = degrees / 360.0 * 2 * math.pi
>>> math.sin(radians)
0.707106781187

表达式math.pimath模块中获得变量pi。这个变量的值是π的近似值,大约精确到15位数字。

如果你了解三角函数,可以把上面的结果和2的平方根的一半进行比较:

>>> math.sqrt(2) / 2.0
0.707106781187

到现在为止,我们已经分别了解了程序的基本元素——变量、表达式和语句,但还没有接触如何将它们有机地组合起来。

程序设计语言最有用的特性之一就是可以将各种小的构建块(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 operator

至此,我们都只是在使用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.

当然,这首歌其实并不是这么唱的。

将前面一节零散的代码整合起来,整个程序就像下面这个样子:

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_lyricsrepeat_lyrics。(在解释器执行程序代码时)函数定义的执行方式和其他语句一样,不同的是执行后会创建函数对象。函数体里面的语句并不会立即执行,而是等到函数被调用时才执行。函数定义不会产生任何输出。

你可能已经猜到,必须先创建一个函数,才能执行它。换言之,函数定义必须在函数的第一次调用之前先执行。

练习3-1

将程序的最后一行移动到首行,于是函数调用会先于函数定义执行。运行程序并查看会有什么样的错误信息。

练习3-2

将函数调用那一行放回到末尾,并将函数print_lyrics的定义放到函数repeat_lyrics定义之后。这时候运行程序会发生什么?

为了保证函数的定义先于其首次调用执行,你需要知道程序中语句执行的顺序,即执行流程

执行总是从程序的第一行开始。从上到下,按顺序,每次执行一条语句。

函数定义并不会改变程序的执行流程,但应注意函数体中的语句并不立即执行,而是等到函数被调用时执行。

函数调用可以看作程序执行流程中的一个迂回路径。遇到函数调用时,并不会直接继续执行下一条语句,而是跳到函数体的第一行,继续执行完函数体的所有语句,再跳回到原来离开的地方。

这样看似简单,但马上你会发现,函数体中可以调用其他函数。当程序流程运行到一个函数之中时,可能需要执行其他函数中的语句。但当执行那个函数中的语句时,又可能再需要调用执行另一个函数的语句!

幸好Python对于它运行到哪里有很好的记录,所以每个函数执行结束后,程序都能跳回到它离开的地方。直到执行到整个程序的结尾,才会结束程序。

前面这段枯燥的描述,寓意何在?当你阅读代码时,并不总是应该一行行按照书写顺序阅读。有时候,按照执行的流程来阅读代码,可能理解效果更好。

我们已经看到,有些内置函数需要传入参数。比如,当调用math.sin时,需要传入一个数字作为实参。有的函数需要多个实参:math.pow需要两个,分别是基数(base)和指数(exponent)。

在函数内部,实参会被赋值给形参。下面的例子是一个用户自定义的函数,接收一个实参:

def print_twice(bruce):
   print bruce
   print bruce

这个函数在调用时会把实参的值赋到形参bruce上,并将其打印两次。

这个函数对任何可以打印的值都可用。

>>> print_twice('Spam')
Spam
Spam
>>> print_twice(17)
17
17
>>> print_twice(math.pi)
3.14159265359
3.14159265359

内置函数的组合规则,在用户自定义函数上也同样可用,所以我们可以对print_twice使用任何表达式作为实参:

>>> print_twice('Spam '*4)
Spam Spam Spam Spam
Spam Spam Spam Spam
>>> print_twice(math.cos(math.pi))
-1.0
-1.0

作为实参的表达式会在函数调用之前先执行。所以在这个例子中,表达式'Spam'*4math.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

当你在函数体内新建一个变量时,它是局部的(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 cat
NameError: name 'cat' is not defined

形参也是局部的。比如,在print_twice函数之外,不存在bruce这个变量。

要跟踪哪些变量在哪些地方使用,有时候画一个栈图(stack diagram)会很方便。和状态图一样,栈图可以展示每个变量的值,不同的是它会展示每个变量所属的函数。

每个函数使用一个包含,帧在栈图中就是一个带着函数名称的盒子,里面有函数的参数和变量。前面的函数示例的栈图如图3-1所示。

图3-1 栈图

图中各个帧从上到下安排成一个栈,能够展示出哪个函数被哪个函数调用了。在这个例子里,print_twicecat_twice调用,而cat_twice__main__调用。__main__是用于表示整个栈图的图框的特别名称。当你在所有函数之外新建变量时,它就是属于__main__的。

每个形参都指向与其对应的实参相同的值,所以,part1line1的值相同,part2line2的值相同,而brucecat的值相同。

如果调用函数的过程中发生了错误,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 cat
NameError: name 'cat' is not defined

上面这个函数列表被称为回溯(traceback)。它告诉你错误出现在哪个程序文件,哪一行,以及哪些函数正在运行。它也会显示导致错误的那一行代码。

回溯中函数的顺序和栈图中图框的顺序一致。当前正在执行的函数列在最底部。

我们使用过的函数中,有一部分函数,如数学函数,会产生结果。因为没有想到更好的名字,我称这类函数为有返回值函数(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')
Bing
Bing
>>> print result
None

None和字符串'None'并不一样。它是一个特殊的值,有自己独特的类型:

>>> print type(None)
<type 'NoneType'>

到目前为止,我们自定义的函数都是无返回值函数。再过几章我们就会开始写有返回值的函数了。

为什么要花功夫将程序拆分成函数呢?也许刚开始编程的时候这其中的原因并不明晰。下面这些解释都可作为参考。

Python提供了两种导入模块的方式;我们已经见过其中一种:

>>> import math
>>> print math
<module 'math' (built-in)>
>>> print math.pi
3.14159265359

如果你导入math,则会得到名为math的模块对象。模块对象包含了pi这样的常量以及诸如sinexp这样的函数。

但是如果直接访问pi,则会发生错误。

>>> print pi
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'pi' is not defined

这时候,你可以像下面这样来导入模块中的某个对象:

>>> from math import pi

现在就可以直接访问pi,而不需要使用句点表示法math.pi了。

>>> print pi
3.14159265359

或者,也可以使用星号来导入一个模块的所有成员:

>>> from math import *
>>> cos(pi)
-1.0

用这种方式导入模块内所有的成员,好处是可以使你的代码更简洁,但缺点是不同模块的同名成员之间,或者和自定义的变量之间,可能发生名字冲突。

如果你使用文本编辑器来编写脚本,则可能会遇到缩进时空格和制表符混淆的问题。避免这种问题的最好办法是只使用空格(不用制表符)。大部分识别Python的文本编辑器都默认这么处理,不过也有一些不支持。

制表符和空格都是不可见的,因而会很难调试,所以应尝试找一个能帮你处理缩进的编辑器。

另外,不要忘了在运行程序前保存它。有的开发环境会自动保存,但也有不自动保存的。如果不保存,则你写好的代码和运行的代码并不一样。

如果运行的报错的代码和你写的不一样,调试时会浪费很多时间!

所以一定要确保你眼前所看的代码和所运行的代码是一致的。如果不确定,可以在程序开头写一句print 'hello'并再运行一次。如果没有看到hello输出,则你运行的不是正确的程序!

函数(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不会换行,所以后面的打印语句会出现在同一行。

这两个语句的输出是'+ -'

练习3-3

Python提供了一个内置函数len,返回一个字符串的长度。所以len('allen')的值是5。

print '+','-'

编写一个函数right_justify,接收一个字符串形参s,并打印出足够的前导空白,以达到最后一个字符显示在第70列上。

>>> right_justify('allen')
                                   allen
print '+',
print '-'
练习3-4

函数对象是一个值,你可以将它赋值给变量,或者作为实参传递。例如do_twice是一个函数,接收一个函数对象作为实参,并调用它两次:

def do_twice(f):
   f()
   f()

下面是一个使用do_twice来调用一个print_spam函数两次的示例:

def print_spam():
   print 'spam'

do_twice(print_spam)

1.将这个示例存入脚本中并测试它。

2.修改do_twice,让它接收两个实参,一个是函数对象,另一个是一个值,它会调用函数对象两次,并传入那个值作为实参。

3.编写一个更通用的print_spam,叫做print_twice,接收一个字符串形参,并打印它两次。

4.使用修改版的do_twice来调用print_twice两次,并传入实参'spam'

5.定义一个新的函数do_four,接收一个函数对象与一个值,使用这个值作为实参调用函数4次。这个函数的函数体应该只有两个语句,而不是四个。

练习3-5

这个练习可以只用语句和我们已经学过的其他语言特性实现。

1.编写一个函数,绘制如下的表格:

提示:

2.编写一个函数绘制类似的表格,但有4行4列。

解答:http://thinkpython.com/code/grid.py

鸣谢:这个练习基于Oualline的《实践C编程》第3版(O'Reilly Media,1997)中的一个示例。

 这一段中讲的参数有两种:函数定义里的形参(parameter),以及调用函数时传入的实参(argument),这里两种是有区分的。——译者注


本章的代码示例可以从http://thinkpython.com/code/polygon.py下载。

为了配合本书,我写了一个程序包,称为Swampy。你可以从http://thinkpython.com/swampy下载Swampy;参考网站上的指导,将Swampy安装到你的系统中。

程序包(package)是多个模块的组合;Swampy中有一个模块“乌龟世界”(TurtleWorld),它提供各种函数,可以引导一只乌龟在屏幕上爬行,并画出其踪迹。

系统中安装好了Swampy之后,就可以像下面这样导入TurtleWorld模块:

from swampy.TurtleWorld import *

如果你下载了Swampy但并没有安装,则可以在其代码目录中使用,或者将其目录加入到Python的搜索路径中。接下来就可以这样导入TurtleWorld:

from TurtleWorld import *

安装过程的细节和如何设置Python的搜索路径,依赖于你所使用的系统。所以我在这里不细加讨论,而在http://thinkpython.com/swampy里维护几种系统的相关安装信息。

创建一个文件mypolygon.py,并输入如下代码:

from swampy.TurtleWorld import *

world = TurtleWorld()
bob = Turtle()
print bob

wait_for_user()

第一行从swampy程序包的TurtleWorld模块里导入全部成员。

接下来几行建立一个TurtleWorld对象,赋值给变量world;建立一个Turtle对象,赋值给bob

打印bob对象会得到如下信息:

<TurtleWorld.Turtle instance at 0xb7bfbf4c>

这意味着bob变量引用着TurtleWorld模块中定义的Turtle类的一个实例(instance)。在这个语境中,“实例”是指集合中的一员;bob这个Turtle对象是可能存在的所有Turtle的集合的一员。

wait_for_user告诉TurtleWorld等待用户进行某些操作,虽然现在除了关闭窗口之外,并没有提供给用户多少有用的操作。

TurtleWorld提供了几个用来指挥乌龟的函数:fdbk用于前进和后退,ltrt用于左转和右转。另外,每只乌龟都拿着一只笔,可以朝上或者朝下;若笔朝下,则会绘制出走过的路迹。函数pupd分别表示“笔朝上”(pen up)和“笔朝下”(pen down)。

若要画一个朝右的角,在程序中(建立bob实例之后,调用wait_for_user之前)添加如下代码:

fd(bob, 100)
lt(bob)
fd(bob, 100)

第一行告诉bob前进100步。第二行告诉它左拐。

当你运行这个程序时,将会看到bob先向东走,再向北走,身后留下两条线段。

现在试着修改程序,画出一个正方形来。在成功之前请不要继续!

你可能会写下如下代码(除去新建TurtleWorld和等待用户的操作外):

fd(bob, 100)
lt(bob)

fd(bob, 100)
lt(bob)

fd(bob, 100)
lt(bob)

fd(bob, 100)

使用for语句,可以更紧凑地实现同样功能。把下面的例子加到mypolygon.py中,并再运行一次:

for i in range(4):
   print 'Hello!'

你可能会看到如下输出:

Hello!
Hello!
Hello!
Hello!

这是for语句的最简单用法,后面我们会看到更多的用法。但这样已经足够重写刚才的画正方形的程序了。请重写后再接着阅读。

下面是使用for语句绘制正方形的程序:

for i in range(4):
    fd(bob, 100)
    lt(bob)

for语句的语法和函数定义类似。它也有一个以冒号结束的语句头,并有一个缩进的语句体。语句体可以包含任意数量的语句。

for语句有时被称为循环(loop),因为执行流程会遍历语句体,之后从语句体的最开头重新循环执行。在这个例子里,语句体执行了4次。

这个版本的代码和之前的绘制正方形的代码其实还稍有不同,因为在最后一次循环后它多做了一次左转。多余的左转稍微多消耗了点时间,但因为每次循环做的事情都一样,也让代码更简练。这个版本的代码还有一个效果,程序执行完之后,乌龟会回归到初始的位置,并朝向初始相同的方向。

下面是一系列使用TurtleWorld的练习。它们力求有趣,但也包含着某些寓意。当你做这些练习时,可以猜想一下其寓意。

在接下来的章节中有这些练习的解答,所以在完成(或着至少尝试过)之前,请先别继续阅读。

1.写一个函数square,接受一个形参t,用来表示一只乌龟。利用乌龟来画一个正方形。写一个函数调用传入bob作为实参来调用square函数,并再运行一遍程序。

2.给square函数再添加一个形参length。修改函数内容,保证正方形的长度是length,并修改函数调用以提供这第二个实参。再运行一遍程序。使用不同的length值测试你的程序。

3.函数ltrt默认会进行90度的拐弯。但也可以提供第二个形参指定转弯的角度。比如,lt(bob, 45)会让bob左拐45度。

复制square函数,并命名为polygon。再添加一个形参n并修改函数体以绘制一个正n边形。提示:正n边形的拐角是360/n度。

4.写一个函数circle接受代表乌龟的形参t,以及表示半径的形参r,并使用合适的长度和边数调用polygon画一个近似的圆。使用不同的r值来测试你的函数。

提示:


思考圆的周长(circumference),并保证length * n =circumference

另一个提示:如果你感觉bob太慢了,可以修改bob.delay来加速。bob.delay代表每次行动之间的停顿,单位是秒。bob.delay = 0.01应该能让它跑得足够快。

5.给circle函数写一个更通用的版本,称为arc。增加一个形参angle,用来表示画的圆弧的大小。这里angle的单位是度数,所以当arc=360时,则会画一个整圆。

第一个练习要求你把画正方形的代码放到一个函数定义中,并将乌龟bob作为实参传入,调用该函数。下面是一个解答:

def square(t):
    for i in range(4):
     fd(t, 100)
     lt(t)

square(bob)

最内侧的语句,fdlt都缩进了两层,表示它们是在for语句的语句体内部,而for语句在函数定义的函数体内部。最后一行,square(bob),又重新从左侧开始而没有缩进,所以这里for语句和square函数的定义都已经结束了。

在函数体中,t引用的乌龟和bob引用的相同,所以lt(t)和直接调用lt(bob)是一样的效果。那么为什么不直接把形参写为bob呢?原因是t可以是任何乌龟,而不仅仅是bob,所以你可以再新建一只乌龟,并将它作为参数传入到square函数:

ray = Turtle()
square(ray)

把一段代码用函数包裹起来,称为封装(encapsulation)。封装的一个好处是,它给这段代码一个有意义的名称,增加了可读性。另一个好处是,当你重复使用这段代码时,调用一次函数比复制粘贴代码要简易得多!

下一步是给square函数添加一个length参数。这里是一个解决方案:

def square(t, length):
    for i in range(4):
     fd(t, length)
     lt(t)

square(bob, 100)

给函数添加参数的过程称为泛化(generalization),因为它会让函数变得更通用:在之前的版本中,正方形总是一个大小,而新的版本中,可以是任意大小。

下一步也是一次泛化。我们不再只绘制正方形,而是可以绘制任意边数的多边形。这里是一个方案:

def polygon(t, n, length):
    angle = 360.0 / n
    for i in range(n):
      fd(t, length)
     lt(t, angle)

polygon(bob, 7, 70)

这个例子绘制一个7边形,边长是70。如果函数的形参比较多,很容易忘掉每一个具体是什么,或者忘掉它们的顺序。所以在Python中,调用函数时可以加上形参名称,这样是合法的,并且有时候会有帮助:

polygon(bob, n=7, length=70)

这些参数被称为关键词参数(keyword argument),因为它们使用“关键词”的形式带上了形参的名称调用 (请别和whiledef之类的Python关键字混淆)。

这个语法使得程序更加可读。它也同样提示了我们实参和形参的工作方式:当调用函数时,实参传入并赋值给形参。

下一步是写画圆的circle函数,接受形参r,表示圆的半径。下面是一个简单的例子,通过调用polygon函数画50边的多边形:

def circle(t, r):
    circumference = 2 * math.pi * r
    n = 50
    length = circumference / n
    polygon(t, n, length)

第一行计算半径为r的圆的周长,使用公式2πr。因为我们使用的是math.pi,所以需要先导入math模块。依照惯例,import语句一般都放在脚本开头。

n是我们用于近似画圆的多边形的边数,所以length是每个边的长度。因此,polygon画出一个50边形,近似于一个半径为r的圆。

这个解决方案的缺点之一是n是一个常量,因此对于很大的圆,多边形的边线太长,而对于小圆,我们又浪费时间去画过短的边线。解决办法之一是泛化这个函数,加上形参n。这样可以给用户(调用circle函数的人)更多的控制选择,但接口就不那么清晰简洁了。

函数的接口是如何使用它的概要说明:它有哪些参数?这个函数做什么?它的返回值是什么?我们说一个接口“简洁”(clean),是希望它“尽可能简单,但不能过度。(爱因斯坦)”。

在这个例子里,r属于函数的接口,因为它指定了所画的圆的基本属性。相对地,n则不那么适合,因为它说明的是如何画圆的细节信息。

所以与其弄乱接口,不如在代码内部根据周长来选择合适的n值:

def circle(t, r):
    circumference = 2 * math.pi * r
    n = int(circumference / 3) + 1
    length = circumference / n
    polygon(t, n, length)

现在多边形的边数(近似)是circumference/3,所以每个边长(近似)是3,已经小到足够画出好看的圆形,但又足够大到不影响画线效率,并且适合于任何尺寸的圆。

当我写circle函数时,我可以复用polygon,因为边数很多的正多边形是圆的很好的近似。但是arc则并不那么容易对付;我们不能使用polygon或者circle来画圆弧。

一个选择是先复制一个polygon函数,再通过修改得到arc函数。结果可能类似下面的示例:

def arc(t, r, angle):
    arc_length = 2 * math.pi * r * angle / 360
    n = int(arc_length / 3) + 1
    step_length = arc_length / n
    step_angle = float(angle) / n

    for i in range(n):
      fd(t, step_length)
      lt(t, step_angle)

这个函数的第二部分很像polygon的实现,但如果不修改polygon的接口,无法直接复用。我们也可以泛化polygon函数以接受第三个参数表示圆弧的角度,但那样的话polygon(多边形)就不是合适的名称了! 所以,我们将这个更泛化的函数称为polyline(多边线):

def polyline(t, n, length, angle):
    for i in range(n):
     fd(t, length)
     lt(t, angle)

现在我们可以重写polygonarc,让它们调用polyline

def polygon(t, n, length):
    angle = 360.0 / n
    polyline(t, n, length, angle)

def arc(t, r, angle):
    arc_length = 2 * math.pi * r * angle / 360
    n = int(arc_length / 3) + 1
    step_length = arc_length / n
    step_angle = float(angle) / n
    polyline(t, n, step_length, step_angle)

最后,我们可以重写circle,改为调用arc

def circle(t, r):
    arc(t, r, 360)

这个过程——重新组织程序,以改善函数的接口,提高代码复用——被称为重构(refactoring)。在这个例子里,我们注意到arcpolygon中有类似的代码,因此我们把它们的共同之处“重构出来”抽取到polyline函数中。

如果我们早早计划,可能会直接先写下polyline,也就避免了重构,但实际上在工程开始时我们往往并没有足够的信息去完美设计所有的接口。开始编码之后,你会更了解面对的问题。有时候,重构正意味着你在编程中掌握了一些新的东西。

开发计划(development plan)是写程序的过程。本章的案例分析中,我们使用的过程是“封装和泛化”。这个过程的具体步骤是:

1.最开始写一些小程序,而不需要函数定义。

2.一旦程序成功运行,将它封装到一个函数中,并加以命名。

3.泛化这个函数,添加合适的形参。

4.重复步骤1到步骤3,直到你得到一组可行的函数。复制粘贴代码,以避免重复输入(以及重复调试)。

5.寻找可以使用重构来改善程序的机会。例如,如果发现程序中几处地方有相似的代码,可以考虑将它们抽取出来做一个合适的通用函数。

这个过程也有一些缺点——我们会在后面看到其他方式——但如果你在开始编程时不清楚如何将程序分成适合的函数,这样做会带来帮助。这个方法能让你一边开发一边设计。

文档字符串(docstring)是在函数开头用来解释其接口的字符串(doc是“文档”documentation的缩写)。下面是一个示例:

def polyline(t, n, length, angle):
    """Draws n line segments with the given length and
    angle (in degrees) between them. t is a turtle.
    """
    for i in range(n):
     fd(t, length)
     lt(t, angle)

这里的文档字符串是一个使用三引号括起来的字符串。三引号字符串又称为多行字符串,因为三引号允许字符串跨行表示。

文档字符串很简洁,但已经包含了其他人需要知道的关于函数的基本信息。它精确地解释了函数是做什么的(而不会涉及它是如何实现的细节)。它解释了每个形参对函数行为的影响效果以及每个形参应有的类型(如果其类型并不显而易见)。

编写这类文档是接口设计的重要部分。一个设计良好的接口,也应当很简单就能解释清楚;如果你发现解释一个函数很困难,很可能表示它的接口设计有改进的空间。

函数的接口,作用就像是函数和调用者之间签订的一个合同。调用者同意提供某些参数,而函数则同意使用这些参数做某种工作。

例如,polyline需要4个参数:t必须是一个Turtle;n是边线的个数,所以必须是整数;length应当是个正数;而angle则必须是一个数字,并且按照度数来理解。

这些需求被称为前置条件,因为它们应当在函数开始执行之前就保证为真。相对地,函数结束的时候需要满足的条件称为后置条件。后置条件包含了函数预期的效果(比如画出线段)以及任何副作用(比如移动乌龟或者引起World中的其他改变)。

满足前置条件是调用者的职责。如果调用者违反了一个(文档说明清晰的!)前置条件,因而导致函数没有正确运行,则bug是在调用者,而不在函数本身。

实例(instance):一个集合中的一员。本章中的TurtleWorld是TurtleWorlds集合中的一员。

循环(loop):程序中的一个片段,可以重复执行。

封装(encapsulation):将一组语句转换为函数定义的过程。

泛化(generalization):将一些不必要的具体值(如一个数字)替换为合适的通用参数或变量的过程。

关键词参数(keyword argument):调用函数时,附带了参数名称(作为一个“关键词”来使用)的参数。

接口(interface):描述函数如何使用的说明。包括函数的名称,以及形参与返回值的说明。

重构(refactoring):修改代码并改善函数的接口以及代码质量的过程。

开发计划(development plan):写程序的过程。

文档字符串(docstring):在函数定义开始处出现的用于说明函数接口的字符串。

前置条件(precondition):在函数调用开始前应当满足的条件。

后置条件(postcondition):在函数调用结束后应当满足的条件。

练习4-1

http://thinkpython.com/code/polygon.py下载本章的代码。

1.给函数polygonarccircle编写合适的文档字符串。

2.画一个栈图来显示函数circle(bob,radius)运行时的程序状态。你可以手动计算,或者在代码中添加一些print语句。

3.在4.7节中的arc函数并不准确,因为使用多边形模拟近似圆,总是会在真实的圆之外。因此,乌龟画完线之后会停在偏离正确的目标几个单位的地方。我的解决方案里展示了一种方法可以减少这种错误的效果。阅读代码并考虑是否合理。如果你自己画图,可能会发现它是如何生效的。

练习4-2

写一组合适的通用函数,用来画出图4-1所示的花朵图案。

解答:http://thinkpython.com/code/flower.py,另外也需要http://thinkpython.com/code/ polygon.py

图4-1 花朵图案

练习4-3

写一组合适的通用函数,用来画出图4-2所示的图形。

解答:http://thinkpython.com/code/pie.py

图4-2 饼图

练习4-4

字母表中的字母可以使用一些基本元素来构成。如横线、竖线以及一些曲线。设计一个字体,可以使用最少的基本元素画出来,并编写函数来画出字母表中所有的字母。

你应当给每个字母单独写一个函数,名称为draw_adraw_b等,并把这些函数放到letters.py文件中。可以从http://thinkpython.com/code/typewriter.py 下载一个“乌龟打字机”程序来帮助测试你的代码。

解答:http://thinkpython.com/code/letters.py,另外也需要http://thinkpython.com/code/ polygon.py

练习4-5

http://en.wikipedia.org/wiki/Spiral阅读关于螺旋线(spiral)的信息;接着编写一段程序来画出阿基米德螺旋(或者其他的某种螺旋线)。

解答:http://thinkpython.com/code/spiral.py


相关图书

深度学习的数学——使用Python语言
深度学习的数学——使用Python语言
动手学自然语言处理
动手学自然语言处理
Web应用安全
Web应用安全
Python高性能编程(第2版)
Python高性能编程(第2版)
图像处理与计算机视觉实践——基于OpenCV和Python
图像处理与计算机视觉实践——基于OpenCV和Python
Python数据科学实战
Python数据科学实战

相关文章

相关课程