├── .gitignore ├── README.md ├── SUMMARY.md ├── book.json ├── chapter1.md ├── chapter10.md ├── chapter11.md ├── chapter12.md ├── chapter13.md ├── chapter14.md ├── chapter15.md ├── chapter16.md ├── chapter17.md ├── chapter18.md ├── chapter19.md ├── chapter2.md ├── chapter3.md ├── chapter4.md ├── chapter5.md ├── chapter6.md ├── chapter7.md ├── chapter8.md ├── chapter9.md ├── cover.jpg └── images ├── figure10.1.jpg ├── figure10.2.jpg ├── figure10.3.jpg ├── figure10.4.jpg ├── figure10.5.jpg ├── figure11.1.jpg ├── figure11.2.jpg ├── figure12.1.jpg ├── figure12.2.jpg ├── figure15.1.jpg ├── figure15.2.jpg ├── figure15.3.jpg ├── figure16.1.jpg ├── figure18.1.jpg ├── figure18.2.jpg ├── figure2.1.jpg ├── figure3.1.jpg ├── figure4.1-4.2.jpg ├── figure5.1.jpg ├── figure5.2.jpg ├── figure6.1.jpg ├── figure7.1.jpg ├── figure8.1.jpg └── figure8.2.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | # Node rules: 2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins# storing-task-files) 3 | .grunt 4 | 5 | ## Dependency directory 6 | ## Commenting this out is preferred by some people, see 7 | ## https://docs.npmjs.com/misc/faq# should-i-check-my-node_modules-folder-into-git 8 | node_modules 9 | 10 | # Book build output 11 | _book 12 | 13 | # eBook build output 14 | *.epub 15 | *.mobi 16 | *.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Think Python 2 | 3 | > 第二版,基于Python3 4 | 5 | 6 | > 原作者 Allen B. Downey 7 | 8 | 9 | > 翻译 [CycleUser](http://blog.cycleuser.org) 10 | 11 | [在线阅读地址](https://cycleuser.gitbooks.io/think-python/content/) 12 | 13 | 14 | 15 | ======= 16 | ## 译者的话 17 | 这是一本很经典的Python入门教材,也是一本很适合初学者的编程入门书籍。网上有过一些翻译,不过我觉得都还是自己动手来尝试一下,这样更有利于深入了解和体验,所以就再造轮子了。 18 | 19 | ## 作者的话 20 | 这是Think Python这本书的第二版,本次使用的是Python3,与Python2有很多不同,这些不同之处会有标注。如果你用Python2的话,还是建议你去阅读[上一个版本](http://www.greenteapress.com/thinkpython/index.html)。 21 | 22 | 读者可以到[亚马逊](http://amzn.to/Owtmjy)购买本书;或者下载 Think Python 2e [PDF格式的电子版.](http://www.greenteapress.com/thinkpython2/thinkpython2.pdf);也可以在线阅读 Think Python 2e [HTML网页版本](http://www.greenteapress.com/thinkpython2/html/index.html)(推荐这个,都是文字格式,更方便). 23 | 24 | 25 | 样例代码以及其他问题的解决可以到[这里](http://www.greenteapress.com/thinkpython2/code)找(具体样例的链接在书中就有)。 26 | 27 | ## 简要介绍 28 | Think Python 这本书是面向初学者介绍Python编程。 29 | 30 | 首先介绍的是一些编程的基本内容,给出概念和解释,然后循序渐进地深入讲解每个概念。 31 | 32 | 复杂的部分,比如递归以及面向对象编程,这些都分成一个个小块,以多个章节的方式来逐步介绍。 33 | 34 | ## 第二版的更新 35 | 36 | * 开始用Python3:书里面所有样例都用Python3来实现,参考代码也都做了升级,用Python2或者3都能运行。 37 | 38 | * 去掉了一些比较难的内容:基于读者反馈,我们认识到大家存在某些困难,所以就调整或者去掉了一些难点。 39 | 40 | * 浏览器内能Python编程了:初学者遇到的第一个困难就是安装Python。另外有的读者可能不想去直接就安装Python,我们就提供了一个用浏览器来运行Python的简介:使用PythonAnywhere,一个免费的在线Python编程环境。(译者注:中国用户以考虑试试fenby.com,也有类似的实现,还有视频的介绍。) 41 | 42 | * 引入了更多的Python特性:单独加了一章来介绍一些第一版中没有提及的Python功能,比如列表解析和附加的数据结构。 43 | 44 | 45 | 这本书是一本自由的书,遵循[创作共用署名-非商业性使用-第三版协议](http://creativecommons.org/licenses/by-nc/3.0/),这意味着你可以自由地复制、分发和修改他,只要你有所贡献,并且不用于商业目的,就可以。 46 | 47 | 如果你有一些评论、修正或者建议,可以发邮件给feedback@thinkpython.com。 48 | 49 | 其他由 Allen Downey 编写的自-和谐-由书籍都可以在[Green Tea Press](http://greenteapress.com/)找到. 50 | 51 | ## 英文原版下载 52 | 53 | * 编译好的PDF版本在这里下载:[PDF](http://www.greenteapress.com/thinkpython2/thinkpython2.pdf)。 54 | 55 | * LaTeX代码在GitHub这里可以下载:[this GitHub repository](https://github.com/AllenDowney/ThinkPython2). 56 | 57 | 58 | ## 过往历史 59 | 60 | 第一版在[这里](http://www.greenteapress.com/thinkpython),是由剑桥大学出版社出版的,标题是 Python for Software Design. 可以到亚马逊去买到。 61 | 本书的原始版本由Green Tea Press 出版,标题为 How to Think Like a Computer Scientist: Learning with Python. 这个版本可以从[Lulu.com](http://lulu.com)这个网站找到。其他由 Allen Downey 编写的自由书籍都可以在Green Tea Press找到. 62 | 63 | # 前言 64 | 65 | ## 本书的奇幻历史 66 | 67 | 68 | 在1999年1月的时候呢,我正准备教一门Java的入门编程课。我当时已经教过三次了,受挫感很强。班上挂科率特别高,而且即使那些没挂科的学生编程的整体水平也特别低。 69 | 70 | 当时有很多问题,首先我就发现教材不太好用。那些教材都特别大部头,有很多关于Java的细节,特别琐碎又并不重要,而且也没有足够的关于如何编程的高层次指导(译者注:就是缺乏战略性指导,没有告诉学生编程的心法)。这些教材总有一些『陷阱门效应』:开头他们都却是挺简单,然后逐步提升,接着突然在某个地方,比如第五章,出现很坑很复杂的陷阱。学生们要突然一下子应对太多新东西,甚至措手不及,而我作为教师就得花费整个后半个学期来一点点给学生们补上。 71 | 72 | 开课的两周之前,我最终决定要写个自己的教科书。目标如下: 73 | 74 | * 简短。让学生读10页就够比让他们读50页效果好得多。 75 | 76 | * 降低词汇难度。我尽量把术语用量降到最低,并且在首次使用的时候对每一个都进行定义。 77 | 78 | * 循序渐进。为了避免『陷阱门效应』,我专门把这些最为复杂的部分抽离成一个个专题,并且都切分成小规模的部分,一步步来进行。 79 | 80 | * 专注于编程,而不是编程语言。我只保留了关于Java的最小规模内容,没有涉及更多的细节。 81 | 82 | 83 | 我还需要个标题,就突发奇想,选了个标题叫做『如何像计算机科学家一样思考』。 84 | 85 | 我的第一版教材很粗糙,不过用起来效果还不错。学生真能看得进去,并且理解了我在课上所讲的那些难点和有趣的专题,最重要的是,他们能够根据这本教材来实践。 86 | 87 | 之后我就以GNU自由文档协议来发布了这本书,这一协议允许所有人去复制、修改以及分发这本书。 88 | 89 | 接下来的事情很有趣了。Jeff Elkner,维吉尼亚的一位高中教师,他很欣赏我这本书,把这本书从Java翻译成了Python的版本。他发给我一份『译稿』,然后我开启了阅读『自己的书』来学习Python的奇妙经历。于是在2001年,我通过Green Tea Press出版了本书的第一个Python版本。 90 | 91 | 在2003年,我开始在奥林商学院教学,并且第一次开始教Python了。这和Java的对比很鲜明。学生们省力多了,学得也更多了,在有趣的项目上也更努力,整体上都觉得这一学习过程很有乐趣。从那以后,我就继续维护这本书,修正错误,改进样例、附加资料以及练习题。 92 | 93 | 结果就产生了现在这本书,现在标题简化了很多——Think Python。 94 | 95 | 主要的改变如下: 96 | 97 | * 在每一章的末尾,我加了关于debug的部分。这些内容提供了关于debug的一些整体策略,比如如何找到和避免bug,还有就是关于Python一些陷阱进行了提醒。 98 | 99 | 100 | * 我加了更多的练习,从简单的理解方面的测试,到一些比较充足的项目。大多数练习都有解决方案的样本链接。 101 | 102 | 103 | * 我还添加了一些案例研究,包含练习、解决方案和讨论的更大规模的样例。 104 | 105 | 106 | * 此外我还扩展了关于程序开发规划和基本设计模式的讨论。 107 | 108 | 109 | * 关于debug和算法分析,还额外加了一些附录。 110 | 111 | 112 | 113 | 这本Think Python 的第二版有如下的新内容: 114 | 115 | 116 | * 本书内的所有参考代码都升级到Python3了。 117 | 118 | 119 | * 我增加了一部分内容,以及一些关于web方面的细节,这是为了帮助初学者能够在浏览器中开始尝试Python,这样即便你不想安装Python也没问题了。 120 | 121 | 122 | * 在第四章的第一节,我把我自己的一个原来叫做Swampy的小乌龟图形包转换撑了一个更标准的Python模块,名字叫做turtle,更好安装,功能也比之前强大了。 123 | 124 | 125 | * 我还添加了新的一章,叫做『彩蛋』,介绍了一些Python的额外功能,严格来说,这些功能并不算必备的,但有时候蛮好用的。 126 | 127 | 128 | 我希望大家能享受学习这本书的过程,也希望这本书能帮助大家学习编程,并且让大家学会像计算机科学家一样思考,哪怕有一点点也好。 129 | 130 | 本书英文版原作者:Allen B. Downey(艾伦 唐尼) 131 | 132 | Olin College 奥林商学院 133 | 134 | ## 致谢 135 | 136 | 非常感谢Jeff Elkner,是他把我的Java教材翻译成了Python,才引起了这一项目的开始,并且也把Python语言介绍给我,它已经是我最喜欢的编程语言了。 137 | 也要感谢Chris Meyers,他对『如何像计算机科学家一样思考』的一些章节有贡献。 138 | 感谢自由软件基金会,是他们提出了GNU自由文档协议,在这一协议的帮助下,我和Jeff以及Chris的合作成为了可能,当然也要感谢我现在使用的知识共享协议。 139 | 感谢Lulu的编辑们,他们出版了『如何像计算机科学家一样思考』。 140 | 感谢O’Reilly公司的编辑们,他们出版了这本『Think Python』。 141 | 142 | 最后还要感谢所有曾对本书早期版本做出过贡献的同学们,以及其他参与改错和提出建议的朋友们(列表如下)。 143 | 144 | ## # 贡献列表 145 | 146 | 有几百名读者,他们目光敏锐又思维迅捷,在过去的这些年里提供了各种建议,发现了各种错误。他们贡献和热情都是对本项目的巨大帮助。 147 | 148 | 如果大家有任何意见建议,请发邮件到feedback@thinkpython2.com联系我们。如果基于反馈做出了修改,我会将你添加到贡献列表(当然你不想被添加也可以的)。 149 | 150 | 希望你能至少把出错句子的一部分提供出来,这都让我更容易去搜索。页码和章节编号也可以,但不太容易找。多谢了! 151 | 152 | (译者注:以下贡献列表省略不在此处提供,有兴趣的朋友可以去看英文原版。) 153 | 154 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [简介](README.md) 4 | * [第一章 编程之路](chapter1.md) 5 | * [第二章 变量,表达式,语句](chapter2.md) 6 | * [第三章 函数](chapter3.md) 7 | * [第四章 案例学习:交互设计](chapter4.md) 8 | * [第五章 条件循环](chapter5.md) 9 | * [第六章 有返回值的函数](chapter6.md) 10 | * [第七章 迭代](chapter7.md) 11 | * [第八章 字符串](chapter8.md) 12 | * [第九章 案例学习:单词游戏](chapter9.md) 13 | * [第十章 列表](chapter10.md) 14 | * [第十一章 字典](chapter11.md) 15 | * [第十二章 元组](chapter12.md) 16 | * [第十三章 案例学习:数据结构的选择](chapter13.md) 17 | * [第十四章 文件](chapter14.md) 18 | * [第十五章 类和对象](chapter15.md) 19 | * [第十六章 类和函数](chapter16.md) 20 | * [第十七章 类和方法](chapter17.md) 21 | * [第十八章 继承](chapter18.md) 22 | * [第十九章 更多功能](chapter19.md) 23 | 24 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /chapter1.md: -------------------------------------------------------------------------------- 1 | # 第一章 编程之路 2 | 3 | 本书的目的是教你学会像计算机科学家一样来思考。这种思考方式汇聚了数学、工程和自然科学的精华。计算机科学家像数学家一样,使用规范的语言来阐述思想(尤其是一些计算);像工程师一样设计、组装系统,并且在多重选择中寻找最优解;像自然科学家一样观察复杂系统的行为模式,建立猜想,测试预估的结果。 4 | 5 | 6 | 计算机科学家唯一最重要的技能就是『解决问题』。解决问题意味着要有能力把问题进行方程化,创造性地考虑解决思路,并且清晰又精确地表达出解决方案。而学习编程的过程,正是一个培养这种解决问题能力的绝佳机会。本章的标题是『编程之路』,原因就在此。 7 | 8 | 在一定层面上,大家将通过编程本身来学习编程这一重要的技巧。在另外一些层面上,大家也将把编程作为实现一种目的的途径。这一目的会随着我们逐渐学习而越发清楚。 9 | 10 | ## 1.1 程序是什么? 11 | 12 | 程序是一个指令的序列,来告诉机器如何进行一组运算。这种运算也许是数学上的,比如求解一组等式或者求多项式的根;当然也可以是符号运算,比如在文档中搜索和替换文字,或者一些图形化过程,比如处理图像或者播放一段视频。 13 | 14 | 不同编程语言的具体细节看着很不一样,但几乎所有编程语言都会有一些基础指令: 15 | 16 | 17 | * 输入系统:从键盘、文件、网络或者其他设备上获得数据。 18 | 19 | * 输出系统:将数据在屏幕中显示,或者存到文件中、通过网络发送等等。 20 | 21 | * 数学运算:进行基本的数学操作,比如加法或者乘法。 22 | 23 | * 条件判断:检查特定条件是否满足来运行相应的代码。 24 | 25 | * 重复判断:重复进行一些操作,通常会有些变化。 26 | 27 | 大家刚开始接触编程的话,可能还有点难以置信,核心内容仅仅上述这些而已。你用过的所有程序,无论多么复杂,都是由一些这样的指令组合而成的。因此大家可以把编程的过程理解成一个把庞大复杂任务进行拆分来解决的过程,分解到适合使用上述的基本指令来解决为止。 28 | 29 | ## 1.2 运行Python 30 | 31 | 新手在刚接触Python的时候遇到的困难之一就是必须在电脑上安装Python和相关的一些软件。如果你熟悉操作系统,并且还很习惯用命令行接口,那安装Python对你来说就没啥问题了。但对初学者来说,要求他们既要了解系统管理又要学习编程,就可能有些困难了。 32 | 33 | 为了避免这种问题,我推荐大家可以在开始的时候用浏览器来体验Python。熟悉了之后,再安装Python到计算机上。 34 | 35 | 有很多站点提供在线运行Python的功能。如果你已经用过并且有一定经验了,可以选择你喜欢的。我推荐大家可以试试PythonAnywhere,对此的使用介绍可以在[这个链接](http://tinyurl.com/thinkpython2e)中找到。 36 | 37 | Python现在有两个主要的分之,即Python2和Python3。如果你学过其中的一个,你会发现他们还挺相似的,而且转换起来也不算难。实际上对于初学者来说,他们只有很细微的差别而已。这本书是用Python3写的,但也会对Python2进行注解。 38 | 39 | Python的解释器是一个读取并执行Python代码的程序。根据你的系统环境,你可以点击图标或者在命令行中输入python来运行解释器。它运行起来,你会看到类似这样的输出: 40 | 41 | ```python 42 | Python 3.4.0 (default, Jun 19 2015, 14:20:21) 43 | [GCC 4.8.2] on linux 44 | Type "help", "copyright", "credits" or "license" for more information. 45 | >>> 46 | >>> 47 | ``` 48 | 49 | 开头的三行包含了关于解释器和所在操作系统的信息,所以大家各自的情况可能有所不同。不过当你检查版本的时候,比如例子中的是3.4.0,使用3开头的,那就告诉你了,他运行的是Python3。你肯定也能猜到,如果开头的是2那就是Python2咯。 50 | 51 | 最后一行那个是提示符,告诉你解释器已经就绪了,你可以输入代码了。如果你输入一行代码然后回车键,解释器就会显示结果了,如下所示: 52 | 53 | ```python 54 | >>> 1 + 1 55 | >>> 1 + 1 56 | 2 57 | ``` 58 | 现在你已经做好开始学习Python的准备了。现在我估计你应该已经知道怎么来启动Python解释器和运行Python代码了。 59 | 60 | ## 1.3 第一个程序 61 | 62 | 传统意义上,大家学一门新编程语言要写的第一个程序都被叫做『Hello,World!』,因为这第一个程序就用来显示这个词组『Hello,World!』。在Python中,是这样实现的: 63 | 64 | ```python 65 | >>> print('Hello, World!') 66 | >>> Hello, World! 67 | ``` 68 | 这是一个打印语句的例子,虽然并没有往纸张上面进行实际的『打印』。这个程序把结果显示在屏幕上。结果就是输出了这个词组『Hello,World!』 69 | 70 | 括号表明了print是一个函数。关于函数我们到第三章再讨论。 71 | 72 | 在Python2中,打印的语句有一点点不一样:print不是一个函数,所以就不用有括号了。 73 | 74 | ```python 75 | >>> print 'Hello, World!' 76 | >>> Hello, World! 77 | ``` 78 | 79 | 这个区别以后会理解更深入,现在说这点就够了。 80 | 81 | ## 1.4 运算符 82 | 83 | 在『Hello,World!』之后,下一步就是运算了。Python提供了运算符,就是一些用来表示例如加法、乘法等运算的符号了。 84 | 85 | 运算符+,-和*表示加法、减法和乘法,如下所示: 86 | 87 | ```python 88 | >>> 40 + 2 89 | >>> 40 + 2 90 | 42 91 | >>> 43 - 1 92 | >>> 43 - 1 93 | 42 94 | >>> 6 * 7 95 | >>> 6 * 7 96 | 42 97 | ``` 98 | 99 | 运算符右斜杠/意味着除法: 100 | 101 | ```python 102 | >>> 84 / 2 103 | >>> 84 / 2 104 | 42.0 105 | ``` 106 | 107 | 你估计在纳闷为啥结果是42.0而不是42,这个下一章节我再解释。 108 | 109 | 110 | 最后,再说个运算符**,它表示乘方,就是前一个数为底数,后一个数为指数的次幂运算: 111 | 112 | ```python 113 | >>> 6**2 + 6 114 | >>> 6**2 + 6 115 | 42 116 | ``` 117 | 118 | 在其他的一些编程语言中,^这个符号是乘方的意思,但在Python中这是一个位运算操作符叫做『异或』。要是你不熟悉位运算操作符,结果一定让你很惊讶: 119 | 120 | ```python 121 | >>> 6 ^ 2 122 | >>> 6 ^ 2 123 | 4 124 | ``` 125 | 126 | 127 | 我在本书中不会涉及到位运算,但你可以在下面这个链接里面读一下来了解:[Wiki](http://wiki.python.org/moin/BitwiseOperators)。 128 | 129 | ## 1.5 值和类型 130 | 131 | 值就是一个程序操作的基本对象之一,比如一个字母啊,或者数字。刚刚我们看到了一些值的例子了,比如2,42.0,还有那个字符串『Hello,World!』 132 | 133 | 这些值属于不同的类型:2是一个整形值,42.0是浮点数,『Hello,World!』是字符串咯。之所以叫字符串就是因为有一串字符。(译者注:这本书的作者真心掰开揉碎地讲解每一个点啊,高中生甚至初中生都应该理解起来没有什么问题,所以大家用这本书来学编程绝对是最佳选择了。) 134 | 135 | 如果你不确定一个值是什么类型呢,你可以让解释器来告诉你: 136 | 137 | ```python 138 | >>> type(2) 139 | >>> type(2) 140 | 141 | >>> type(42.0) 142 | >>> type(42.0) 143 | 144 | >>> type('Hello, World!') 145 | >>> type('Hello, World!') 146 | 147 | ``` 148 | 149 | 在这些例子中,『class』这个字样表明这是一类,一种类型就是对值的一种划分。 150 | 151 | 152 | 很自然了,整形的就是int了,字符串就是str了,浮点数就是float了。 153 | 154 | 155 | 那'2' 和 '42.0'这种是啥呢?他们看着像是数字,但带了单引号了。 156 | 157 | ```python 158 | >>> type('2') 159 | >>> type('2') 160 | 161 | >>> type('42.0') 162 | >>> type('42.0') 163 | 164 | ``` 165 | 166 | 真相就是字符串了。 167 | 168 | 169 | 咱们现在输入一个大的整数,在中间用逗号分隔试试看,比如1,000,000,并不是Python中合乎语法的整形,但也被接受了: 170 | 171 | ```python 172 | >>> 1,000,000 173 | >>> 1,000,000 174 | (1, 0, 0) 175 | ``` 176 | 177 | 出乎意料吧,Python把逗号当做了分隔三个整形数字的分隔符了。我们以后再对这种序列进行讨论。 178 | 179 | ## 1.6 公式语言和自然语言 180 | 181 | 自然语言就是人说的语言,比如英语、西班牙语、法语,当然包括中文了。他们往往都不是人主动去设计出来的(当然,人会试图去分析语言的规律),自然而然地发生演进。 182 | 183 | 184 | 公式语言是人们为了特定用途设计出来的。比如数学的符号就是一种公式语言,特别适合表达数字和符号只见的关系。化学家也用元素符号和化学方程式来表示分子的化学结构。要注意的是: 185 | 186 | 编程语言是一种用来表达运算的公式语言。 187 | 公式语言有严格的语法规则和对语句结构的要求。比如数学式3+3=6是正确的,而3+=3¥6就不是了。化学上H2O 是正确的化学式,而2Zz 就不是。 188 | 189 | 语法规则体现在两个方面,代号和结构。 代号是语言的基础元素,比如单词、数字以及化学元素。3 += 3 $ 6这个式子数学上无意义的一个原因就是因为 $ 并不是数学上的符号 (至少我所学的数学是没有这个符号的)。类似地, 2Zz 也不对,因为没有一种化学元素的缩写是 Zz. 190 | 191 | 第二个语法规则是代号必须有严格的组合结构。3 += 3这个式子数学上错误就因为虽然这些符号都是数学符号,但不能把加号等号放一起。类似地,化学方程式中要先写元素名字后写个数,而不是反着。 192 | 193 | This is @ well-structured Engli$h sentence with invalid t*kens in it. This sentence all valid tokens has, but invalid structure with. 194 | 195 | 这句英语的单词和结构都有错误,大家还是能看懂的哈。(译者注,作者故意这样写,来表明人类的自然语言容错率高。) 196 | 197 | 你读一句英语或者公式语言中的语句时候,你必须搞清楚结构(虽然在自然语言中大家潜意识就能搞定了)。这就叫做解译。 198 | 199 | 虽然公式语言和自然语言有很多共同特征,比如代号、结构、语法这些元素,但差别还是显著的,比如: 200 | 201 | 202 | * 二义性 ambiguity: 203 | 204 | 自然语言充满二义性,也就是歧义了,人们有时候用上下文线索或者其他信息来帮助处理这种情况。公式语言被设计为尽量不具有二义性,这就意味着一个语句往往只有唯一的一种含义,与上下文无关。 205 | 206 | * 冗余性 redundancy: 207 | 208 | 为了弥补歧义,减少误解,自然语言有很多冗余,结果就是经常有废话。公式语言要精简的多。 209 | 210 | * 文字修辞 literalness: 211 | 212 | 自然语言充满习语和隐喻等。比如我说 “The penny dropped”, 可能并不是字面意思说硬币掉了(这个俚语意思是过了一会终于弄明白了)。公式语言的意思严格精准。 213 | 214 | 咱们大家都是说着自然语言长大的,要调节到公式语言有时候挺难的。这两者之间的差别有点像诗词和散文,但差别更大: 215 | 216 | 217 | >* 诗词 Poetry: 218 | 219 | 单词的运用要兼顾词义和押韵,诗的整体要有一定的意境或者情感上的共鸣。双关很常见,并且多是故意的。 220 | 221 | >* 散文 Prose: 222 | 223 | 文字意思更重要,结构也有重要作用。相比诗词更好理解,但也有一定的双关语歧义。 224 | 225 | >* 程序 Programs: 226 | 227 | 计算机程序的意义必须是无歧义和文采修饰的,能完全用代号和结构的方式进行解析。 228 | 229 | 公式语言比自然语言要更加密集,读起来也需要更长时间。公式语言的结构也非常重要,所以从头到尾或者从左到右未必就是最佳方式。大家应该学着动脑来解译程序,分辨代号,解析结构。最后要注意的就是在公式语言中,细节特别特别重要。拼写和符号的小错误对于自然语言来说没什么,但对公式语言来说就能带来大问题。 230 | 231 | ## 1.7 调试 232 | 233 | 程序员也会犯错的。由于很奇妙的原因,程序的错误被叫做bug,调试的过程就叫debug了。(译者注:一个传言是最早的计算机中经常有虫子进去导致短路之类的,清理虫子就成了常规调试的操作,流传至今。。。) 234 | 235 | 编程,尤其是调试的过程,有时候会给人带来强烈的挫败感。面对特别复杂的状况,你可能就感到愤怒、压抑,或者特别难受。 236 | 237 | 别担心,这些都是正常人对计算机的正常反应。计算机工作正常了,我们会觉得他们像是队友一样;一旦工作出错了,对我们很粗暴,我们对他们的反应就像是对待粗暴可恨的人一样(参考Reeves和Nass,The Media Equation: How People Treat Computers, Television, and New Media Like Real People and Places)。 238 | 239 | 为这些反应做好心理准备,这样你在遇到类似情况就更好应对了。我们也可以把计算机当做一个有一定优点但也有特定缺陷的员工,比如速度快精度高,但缺乏共鸣和应对大场面的能力。 240 | 241 | 你的工作就是做个好的经理人:尽量充分利用员工优势并降低他们缺陷的作用。然后想办法把你的情绪用在解决问题上,而不要让过激的反应干扰工作效率。 242 | 243 | 调试的过程挺烦人的,但这个本领很有价值,而且在编程之外的其他领域都有用武之地。在每一章的末尾,都会有这样的一段,我会给出一些关于调试方面的建议。希望能帮到大家! 244 | 245 | ## 1.8 Glossary 术语列表 246 | problem solving: 247 | The process of formulating a problem, finding a solution, and expressing it. 248 | 249 | >问题解决:将问题方程化,找到解决方案,并表达出来的过程。 250 | 251 | high-level language: 252 | A programming language like Python that is designed to be easy for humans to read and write. 253 | 254 | >高级语言:例如Python这样的编程语言,设计初衷为易于被人阅读和书写。 255 | 256 | l 257 | ow-level language: 258 | A programming language that is designed to be easy for a computer to run; also called “machine language” or “assembly language”. 259 | 260 | >低级语言:设计初衷为易于被计算机运行的语言,比如机器语言和汇编语言。 261 | 262 | portability: 263 | A property of a program that can run on more than one kind of computer. 264 | 265 | >可移植性:程序能运行于多种平台的特性。 266 | 267 | interpreter: 268 | A program that reads another program and executes it 269 | 270 | >解释器:一边读取一边执行代码的程序。 271 | 272 | prompt: 273 | Characters displayed by the interpreter to indicate that it is ready to take input from the user. 274 | 275 | >提示符:解释器显示的,提醒用户准备就绪,随时可以输入。 276 | 277 | program: 278 | A set of instructions that specifies a computation. 279 | 280 | >程序:进行一种特定运算的一系列指令。 281 | 282 | print statement: 283 | An instruction that causes the Python interpreter to display a value on the screen. 284 | 285 | >打印语句:让Python解释器输出值到屏幕的指令。 286 | 287 | operator: 288 | A special symbol that represents a simple computation like addition, multiplication, or string concatenation. 289 | 290 | >运算符(操作符):一系列特殊的符号,表示一些简单的运算,比如加减乘除或者字符串操作。 291 | 292 | value: 293 | One of the basic units of data, like a number or string, that a program manipulates. 294 | 295 | >值:数据的基本组成单元,比如数字或者字符串,是程序处理的对象。 296 | 297 | type: 298 | A category of values. The types we have seen so far are integers (typeint), floating-point numbers (type float), and strings (type str). 299 | 300 | >类型:对值的分类,大家刚刚接触到的有整形int,浮点数float,以及字符串str。 301 | 302 | integer: 303 | A type that represents whole numbers. 304 | 整形:就是整数咯。 305 | floating-point: 306 | A type that represents numbers with fractional parts. 307 | 308 | >浮点数:简单说,就是有小数点的数了。 309 | 310 | string: 311 | A type that represents sequences of characters. 312 | 313 | >字符串:一串有序的字符了。 314 | 315 | natural language: 316 | Any one of the languages that people speak that evolved naturally. 317 | 318 | >自然语言:人们说的语言,自然地演化。 319 | 320 | formal language: 321 | Any one of the languages that people have designed for specific purposes, such as representing mathematical ideas or computer programs; all programming languages are formal languages. 322 | 323 | >公式语言:人为设计的用于特定用途的语言,比如数学用途或者计算机编程用的;所有编程语言都是公式语言。 324 | 325 | token: 326 | One of the basic elements of the syntactic structure of a program, analogous to a word in a natural language. 327 | 328 | >代号:程序结构中的一种基本元素,相当于自然语言中的单词。 329 | 330 | syntax: 331 | The rules that govern the structure of a program. 332 | 333 | >语法:程序语言结构的规则。 334 | 335 | parse: 336 | To examine a program and analyze the syntactic structure. 337 | 338 | >解译:理解程序并分析语法结构的过程。 339 | 340 | bug: 341 | An error in a program. 342 | 343 | >Bug:程序的错误。 344 | 345 | debugging: 346 | The process of finding and correcting bugs. 347 | 348 | >调试(debug):搜索和改正程序错误的过程。 349 | 350 | ## 1.9 练习 351 | ## # 练习1 352 | 353 | 你读这本书的同时最好手边有台电脑,这样你就能把样例在电脑上随时运行来看看效果了。 354 | 355 | 无论你学任何一种新功能的时候,都可以试着犯点错误。比如就在这个『Hello,World!』程序,你可以试试去掉一个引号会怎么样,都去掉会怎么样,print这个单词拼错了会怎么样等等。 356 | 357 | 这种尝试能让你对读到的内容有更深刻的记忆;也有助于你编程,因为你在编程的时候也得知道调试信息的意思。所以最好现在就故意犯些错误来看看,比以后毫无准备地遇到要好多了。 358 | 359 | 1. 在print语句后面的括号去掉一个或者两个,看看会怎么样? 360 | 361 | 2. Print字符串的时候如果你丢掉一个引号或者两个引号试试看会如何? 362 | 363 | 3. 输入一个负数试试,比如-2。然后再试试在数字前面添加加号会怎么样?比如2++2。 364 | 365 | 4. 数学上计数用零开头是可以得,比如02,在Python下面试试会怎样? 366 | 367 | 5. 两个值中间没有运算符会怎么样? 368 | -------------------------------------------------------------------------------- /chapter11.md: -------------------------------------------------------------------------------- 1 | # 第十一章 字典 2 | 3 | 本章要讲的内容是另外一种内置的类型,叫字典。字典是 Python 最有特色的功能之一;使用字典能构建出很多高效率又很优雅的算法。 4 | 5 | ## 11.1 字典是一种映射 6 | 7 | 字典就像是一个列表一样,但更加泛化了,是列表概念的推广。在列表里面,索引必须是整数;而在字典里面,你可以用几乎任何类型来做索引了。 8 | 9 | (译者注:从字符串 string,到列表 list,再到字典 dictionary,Python 这个变量类型就是一种泛化的过程,内容在逐步推广,适用范围更大了,这里大家一定要对泛化好好理解一下,以后自己写类的时候很有用。) 10 | 11 | 字典包括一系列的索引,不过就已经不叫索引了,而是叫键,然后还对应着一个个值,就叫键值。每个键对应着各自的一个单独的键值。这种键和键值的对应关系也叫键值对,有时候也叫项。 12 | 13 | 14 | (译者注:计算机科学上很多内容都是对数学的应用,大家真应该加油学数学啊。) 15 | 16 | 用数学语言来说,一个字典就代表了从键到键值的一种映射关系,所以你也可以说每个键映射到一个键值。举例来说,我们可以建立一个从英语单词映射到西班牙语单词的字典,这样键和简直就都是字符串了。 17 | 18 | dict 这个函数创建一个没有项目的空字典。因为 dict 似乎内置函数的名字了,所以你应该避免用来做变量名。 19 | 20 | ```Python 21 | >>> eng2sp = dict() 22 | >>> eng2sp 23 | {} 24 | ``` 25 | 26 | 大括号,也叫花括号,就是{},代表了一个空字典。要在字典里面加项,可以使用方括号: 27 | 28 | ```Python 29 | >>> eng2sp['one'] = 'uno' 30 | ``` 31 | 这一行代码建立了一个项,这个项映射了键 'one' 到键值 'uno'。如果我们再来打印输出一下这个字典,就会看到里面有这样一个键值对了,键值对中间用冒号隔开了: 32 | 33 | ```Python 34 | >>> eng2sp 35 | {'one': 'uno'} 36 | ``` 37 | 这种输出的格式也可以用来输入。比如你可以这样建立一个有三个项的字典: 38 | 39 | ```Python 40 | >>> eng2sp = {'one': 'uno', 'two': 'dos', 'three': 'tres'} 41 | ``` 42 | 再来输出一下,你就能看到字典建好了,但顺序不一样: 43 | 44 | ```Python 45 | >>> eng2sp 46 | {'one': 'uno', 'three': 'tres', 'two': 'dos'} 47 | ``` 48 | 49 | 这些键值对的顺序不一样了。如果你在你电脑上测试上面这段代码,你得到的结果也可能不一样,实际上,字典中的项的顺序是不确定的。 50 | 51 | 52 | 但者其实也不要紧,因为字典里面的元素并不是用整数索引来排列的。所以你就可以直接用键来查找对应的键值: 53 | 54 | ```Python 55 | >>> eng2sp['two'] 56 | 'dos' 57 | ``` 58 | 键'two'总会映射到键值'dos',所以项的排列顺序并不要紧。 59 | 60 | 61 | 如果你字典中没有你指定的键,你就得到如下提示: 62 | 63 | ```Python 64 | >>> eng2sp['four'] 65 | KeyError: 'four' 66 | ``` 67 | len 函数也可以用在字典上;它会返回键值对的数目: 68 | 69 | ```Python 70 | >>> len(eng2sp) 71 | 3 72 | ``` 73 | in 运算符也适用于字典;你可以用它来判断某个键是不是存在于字典中(是判断键,不能判断键值)。 74 | 75 | ```Python 76 | >>> 'one' in eng2sp 77 | True 78 | >>> 'uno' in eng2sp 79 | False 80 | ``` 81 | 要判断键值是否在字典中,你就要用到 values 方法,这个方法会把键值返回,然后用 in 判断就可以了: 82 | 83 | ```Python 84 | >>> vals = eng2sp.values() 85 | >>> 'uno' in vals 86 | True 87 | ``` 88 | 89 | in 运算符在字典中和列表中有不同的算法了。对列表来说,它就按照顺序搜索列表中的每一个元素,如8.6所示。随着列表越来越长了,这种搜索就消耗更多的时间,才能找到正确的位置。 90 | 91 | 92 | 而对字典来说,Python 使用了一种叫做哈希表的算法,这就有一种很厉害的特性:in 运算符在对字典来使用的时候无论字典规模多大,无论里面的项有多少个,花费的时间都是基本一样的。我在13.4会解释一下其实现原理,不过你要多学几章之后才能理解对此的解释。 93 | 94 | ## 11.2 用字典作为计数器 95 | 96 | 假设你得到一个字符串,然后你想要查一下每个字母出现了多少次。你可以通过一下方法来实现: 97 | 98 | 1. 你可以建立26个变量,每一个代表一个字母。然后你遍历整个字符串,每个字母的个数都累加到对应的计数器里面,可能会用到分支条件判断。 99 | 100 | 2. 你可以建立一个有26个元素的列表。然后你把每个字母转换成一个数字(用内置的 ord 函数),用这些数字作为这个列表的索引,然后累加相应的计数器。 101 | 102 | 3. 你可以建立一个字典,用字母作为键,用该字母出现的次数作为对应的键值。第一次遇到一个字母,就在字典里面加一个项。此后再遇到这个字母,就每次在已有的项上进行累加即可。 103 | 104 | 上面这些方法进行的都是一样的运算,但它们各自计算的实现方法是不同的。 105 | 106 | 实现是一种运算进行的方式;有的实现要比其他的更好一些。比如用字典来实现的优势就是我们不需要实现知道字符串中有哪些字母,只需要为其中存在的字母来提供存储空间。 107 | 108 | 109 | 下面是代码样例: 110 | 111 | ```Python 112 | def histogram(s): 113 | d = dict() 114 | for c in s: 115 | if c not in d: 116 | d[c] = 1 117 | else: 118 | d[c] += 1 119 | return d 120 | ``` 121 | 函数的名字为 histogram,这是一个统计学术语,意思是计数(或者频次)的集合。 122 | 123 | 函数的第一行创建了一个空字典。for 循环遍历了整个字符串、每次经过循环的时候,如果字符 c 没有在字典中,就在字典中创建一个新的项,键为 c,初始值为1(因为这就算遇到一次了)。如果 c 已经存在于字典中了,就对 d[c]进行一下累加。 124 | 125 | 下面是使用的样例: 126 | 127 | ```Python 128 | >>> h = histogram('brontosaurus') 129 | >>> h 130 | {'a': 1, 'b': 1, 'o': 2, 'n': 1, 's': 2, 'r': 2, 'u': 2, 't': 1} 131 | ``` 132 | 133 | histogram的结果表明字母a 和 b 出现了一次,o 出现了两次,等等。 134 | 135 | 136 | 字典有一个方法,叫做 get,接收一个键和一个默认值。如果这个键在字典中存在,get 就会返回对应的键值;如果不存在,它就会返回这个默认值。比如: 137 | 138 | ```Python 139 | >>> h = histogram('a') 140 | >>> h 141 | {'a': 1} 142 | >>> h.get('a', 0) 143 | 1 144 | >>> h.get('b', 0) 145 | 0 146 | ``` 147 | 148 | 做个练习,用 get 这个方法,来缩写一下 histogram 这个函数,让它更简洁些。可以去掉那些 if 语句。 149 | 150 | ## 11.3 循环与字典 151 | 152 | 如果你在 for 语句里面用字典,程序会遍历字典中的所有键。例如下面这个 print_hist 函数就输出其中的每一个键与对应的键值: 153 | 154 | ```Python 155 | def print_hist(h): 156 | for c in h: 157 | print(c, h[c]) 158 | ``` 159 | 160 | 输出如下所示: 161 | 162 | ```Python 163 | >>> h = histogram('parrot') 164 | >>> print_hist(h) 165 | a 1 p 1 r 2 t 1 o 1 166 | ``` 167 | 168 | 明显这些键的输出并没有特定顺序。字典有一个内置的叫做 keys 的方法,返回字典中的所有键成一个列表,以不确定的顺序。做个练习,修改一下上面这个 print_hist 函数,让它按照字母表的顺序输出键和键值。 169 | 170 | ## 11.4 逆向查找 171 | 172 | 给定一个字典 d,以及一个键 k,很容易找到对应的键值 v=d[k]。这个操作就叫查找。 173 | 174 | 但如果你有键值 v 而要找键 k 呢?你有两个问题了:首先,可能有不止一个键的键值为 v。根据应用的不同,你也许可以从中选一个,或者就可以把所有对应的键做成一个列表。其次,没有一种简单的语法能实现这样一种逆向查找;你必须搜索一下。 175 | 176 | ```Python 177 | def reverse_lookup(d, v): 178 | for k in d: 179 | if d[k] == v: 180 | return k 181 | raise LookupError() 182 | ``` 183 | 184 | 这个函数是搜索模式的另一个例子,用到了一个新的功能:raise。raise语句会导致一个异常;在这种情况下是 LookupError,这是一个内置异常,表示查找操作失败。 185 | 186 | 187 | 如果我们运行了整个循环,就意味着 v 在字典中没有作为键值出现果,所以就 raise 一个异常回去。 188 | 189 | 190 | 下面是一个成功进行逆向查找的样例: 191 | 192 | ```Python 193 | >>> h = histogram('parrot') 194 | >>> k = reverse_lookup(h, 2) 195 | >>> k 196 | 'r' 197 | ``` 198 | 199 | 下面这个是一个不成功的: 200 | 201 | ```Python 202 | >>> k = reverse_lookup(h, 3) 203 | Traceback (most recent call last): File "", line 1, in File "", line 5, in reverse_lookup ValueError 204 | ``` 205 | 206 | 你自己 raise 一个异常的效果就和 Python 抛出的异常是一样的:程序会输出一个追溯以及一个错误信息。 207 | 208 | 209 | raise 语句可以给出详细的错误信息作为可选的参数。如下所示: 210 | 211 | ```Python 212 | >>> raise ValueError('value does not appear in the dictionary') 213 | Traceback (most recent call last): File "", line 1, in ? 214 | ValueError: value does not appear in the dictionary 215 | ``` 216 | 217 | 逆向查找要比正常查找慢很多很多;如果要经常用到的话,或者字典变得很大了,程序的性能就会大打折扣。 218 | 219 | ## 11.5 字典和列表 220 | 221 | 列表可以视作字典中的值。比如给你一个字典,映射了字符与对应的频率,你可能需要逆转一下;也就是建立一个从频率映射到字母的字典。因为可能有几个字母有同样的频率,在这个逆转字典中的每个值就应该是一个字母的列表。 222 | 223 | 224 | 下面就是一个逆转字典的函数: 225 | 226 | ```Python 227 | def invert_dict(d): 228 | inverse = dict() 229 | for key in d: 230 | val = d[key] 231 | if val not in inverse: 232 | inverse[val] = [key] 233 | else: 234 | inverse[val].append(key) 235 | return inverse 236 | ``` 237 | 238 | 每次循环的时候,key这个变量都得到 d 中的一个键,val 获取对应的键值。如果 val 不在 inverse 这个字典里面,就意味着这是首次遇到它,所以就建立一个新项,然后用一个单元素集来初始化。否则就说明这个键值已经存在了,这样我们就在对应的键的列表中添加上新的这一个键就可以了。 239 | 240 | 下面是一个样例: 241 | 242 | ```Python 243 | >>> hist = histogram('parrot') 244 | >>> hist 245 | {'a': 1, 'p': 1, 'r': 2, 't': 1, 'o': 1} 246 | >>> inverse = invert_dict(hist) 247 | >>> inverse 248 | {1: ['a', 'p', 't', 'o'], 2: ['r']} 249 | ``` 250 | ________________________________________ 251 | ![Figure 11.1: State diagram](./images/figure11.1.jpg) 252 | Figure 11.1: State diagram. 253 | ________________________________________ 254 | 255 | 图11.1为hist 和 inverse 两个字典的状态图。字典用方框表示,上方标示了类型 dict,方框内为键值对。如果键值为整数、浮点数或者字符串,就把它们放到一个方框内,不过通常我习惯把它们放到方框外面,这样图表看着简单干净。 256 | 257 | 258 | 如图所示,用字典中的键值组成列表,而不能用键。如果你要用键的话,就会遇到如下所示的错误: 259 | 260 | ```Python 261 | >>> t = [1, 2, 3] 262 | >>> d = dict() 263 | >>> d[t] = 'oops' 264 | Traceback (most recent call last): File "", line 1, in ? TypeError: list objects are unhashable 265 | ``` 266 | 267 | 我之前说过,字典是用哈希表(散列表)来实现的,这就意味着所有键都必须是散列的。 268 | 269 | hash 是一个函数,接收任意一种值,然后返回一个整数。字典用这些整数来存储和查找键值对,这些整数也叫做哈希值。 270 | 271 | 如果键不可修改,系统工作正常。但如果键可以修改,比如是列表,就悲剧了。例如,你创建一个键值对的时候,Python 计算键的哈希值,然后存在相应的位置。如果你修改了键,然后在计算哈希值,就不会指向同一个位置了。这时候一个键就可以有两个指向了,或者你就可能找不到某个键了。总之字典都不能正常工作了。 272 | 273 | 这就是为什么这些键必须是散列的,而像列表这样的可变类型就不行。解决这个问题的最简单方式就是使用元组,这个我们会在下一章来学习。 274 | 275 | 276 | 因为字典是可以修改的,所以不能用来做键,只能用来做键值。 277 | 278 | (译者注:哈希表是一种散列表,相关内容译者知道的太少,所以这段翻译的质量大打折扣,实在抱歉。) 279 | 280 | ## 11.6 Memos 备忘 281 | 282 | 如果你试过了6.7中提到的斐波那契数列,你估计会发现随着参数增大,函数运行的时间也变长了。另外,运行时间的增长很显著。 283 | 284 | 要理解这是怎么回事,就要参考一下图11.2,图中展示了当 n=4的时候函数调用的情况。 285 | 286 | ________________________________________ 287 | ![Figure 11.2: Call graph](./images/figure11.2.jpg) 288 | Figure 11.2: Call graph. 289 | ________________________________________ 290 | 291 | 调用图展示了一系列的函数图框,图框直接的连线表示了函数只见的调用关系。顶层位置函数的参数 n =4,调用了 n=3和 n=2两种情况的函数。相应的 n=3的时候要调用 n=2和 n=1两种情况。依此类推。 292 | 293 | 294 | 算算fibonacci(0)和fibonacci(1)要被调用多少次吧。这样的解决方案是低效率的,随着参数增大,效率就越来越低了。 295 | 296 | 297 | 另外一种思路就是保存一下已经被计算过的值,然后保存在一个字典中。之前计算过的值存储起来,这样后续的运算中能够使用,这就叫备忘。下面是一个用这种思路来实现的斐波那契函数: 298 | 299 | ```Python 300 | known = {0:0, 1:1} 301 | def fibonacci(n): 302 | if n in known: 303 | return known[n] 304 | res = fibonacci(n-1) + fibonacci(n-2) 305 | known[n] = res 306 | return res 307 | ``` 308 | 309 | known 是一个用来保存已经计算斐波那契函数值的字典。开始项目有两个,0对应0,1对应1,各自分别是各自的斐波那契函数值。 310 | 311 | 312 | 这样只要斐波那契函数被调用了,就会检查 known 这个字典,如果里面有计算过的可用结果,就立即返回。不然的话就计算出新的值,并且存到字典里面,然后返回这个新计算的值。 313 | 314 | 315 | 如果你运行这一个版本的斐波那契函数,你会发现比原来那个版本要快得多。 316 | 317 | ## 11.7 全局变量 318 | 319 | 在上面的例子中,known 这个字典是在函数外创建的,所以它属于主函数内,这是一个特殊的层。在主函数中的变量也叫全局变量,因为所有函数都可以访问这些变量。局部变量在所属的函数结束后就消失了,而主函数在其他函数调用结束后依然还存在。 320 | 321 | 322 | 一般常用全局变量作为 flag,也就是标识;比如用来判断一个条件是否成立的布尔变量之类的。比如有的程序用名字为 verbose 的标识变量,来控制输出内容的详细程度: 323 | 324 | ```Python 325 | verbose = True 326 | def example1(): 327 | if verbose: 328 | print('Running example1') 329 | ``` 330 | 331 | 如果你想给全局变量重新赋值,结果会很意外。下面的例子中,本来是想要追踪确定函数是否被调用了: 332 | 333 | ```Python 334 | been_called = False 335 | def example2(): 336 | been_called = True # WRONG 337 | ``` 338 | 339 | 你可以运行一下,并不报错,只是 been_called 的值并不会变化。这个情况的原因是 example2这个函数创建了一个新的名为 been_called 的局部变量。函数结束之后,局部变量就释放了,并不会影响全局变量。 340 | 341 | 342 | 要在函数内部来给全局变量重新赋值,必须要在使用之前声明这个全局变量: 343 | 344 | ```Python 345 | been_called = False 346 | def example2(): 347 | global been_called 348 | been_called = True 349 | ``` 350 | 351 | global 那句代码的效果是告诉解释器:『在这个函数内,been_called 使之全局变量;不要创建一个同名的局部变量。』 352 | 353 | 354 | 下面的例子中,试图对全局变量进行更新: 355 | 356 | ```Python 357 | count = 0 358 | def example3(): 359 | count = count + 1 # WRONG 360 | ``` 361 | 362 | 运行的话,你会得到如下提示: 363 | 364 | ```Python 365 | UnboundLocalError: local variable 'count' referenced before assignment 366 | ``` 367 | 368 | 369 | (译者注:错误提示的意思是未绑定局部错误:局部变量 count 未经赋值就被引用。) 370 | 371 | 372 | Python 会假设这个 count 是局部的,然后基于这样的假设,你就是在写出该变量之前就试图读取。这样问题的解决方法依然就是声称count 为全局变量。 373 | 374 | ```Python 375 | def example3(): 376 | global count 377 | count += 1 378 | ``` 379 | 380 | 如果全局变量指向的是一个可修改的值,你可以无需声明该变量就直接修改: 381 | 382 | ```Python 383 | known = {0:0, 1:1} 384 | def example4(): 385 | known[2] = 1 386 | ``` 387 | 388 | 所以你可以在全局的列表或者字典里面添加、删除或者替换元素,但如果你要重新给这个全局变量赋值,就必须要声明了: 389 | 390 | ```Python 391 | def example5(): 392 | global known 393 | known = dict() 394 | ``` 395 | 396 | 全局变量很有用,但不能滥用,要是总修改全局变量的值,就让程序很难调试了。 397 | 398 | ## 11.8 调试 399 | 400 | 现在数据结构逐渐复杂了,再用打印输出和手动检验的方法来调试就很费劲了。下面是一些对这种复杂数据结构下的建议: 401 | 402 | 缩减输入:尽可能缩小数据的规模。如果程序要读取一个文本文档,而只读前面的十行,或者用你能找到的最小规模的样例。你可以编辑一下文件本身,或者直接修改程序来仅读取前面的 n 行,这样更好。如果存在错误了,你可以减小一下 n,一直到错误存在的最小的 n 值,然后再逐渐增加 n,这样就能找到错误并改正了。 403 | 404 | 检查概要和类型:这回咱就不再打印检查整个数据表,而是打印输出数据的概要:比如字典中的项的个数,或者一个列表中的数目总和。导致运行错误的一种常见原因就是类型错误。对这类错误进行调试,输出一下值的类型就可以了。 405 | 406 | 写自检代码:有时你也可以写自动检查错误的代码。举例来说,假如你计算一个列表中数字的平均值,你可以检查一下结果是不是比列表中的最大值还大或者比最小值还小。这也叫『心智检查』,因为是来检查结果是否『疯了』(译者注:也就是错得很荒诞的意思。)另外一种检查方法是用两种不同运算,然后对比结果,看看他们是否一致。后面这种叫『一致性检查』。 407 | 408 | 格式化输出:格式化的调试输出,更容易找到错误。在6.9的时候我们见过一个例子了。pprint 模块内置了一个 pprint 函数,该函数能够把内置的类型用人读起来更容易的格式来显示出来(pprint 就是『pretty print』的缩写)。 409 | 410 | 再次强调一下,搭建脚手架代码的时间越长,用来调试的时间就会相应地缩短。 411 | 412 | ## 11.9 Glossary 术语列表 413 | mapping: 414 | A relationship in which each element of one set corresponds to an element of another set. 415 | 416 | >映射:一组数据中元素与另一组数据中元素的一一对应的关系。 417 | 418 | dictionary: 419 | A mapping from keys to their corresponding values. 420 | 421 | >字典:从键到对应键值的映射。 422 | 423 | key-value pair: 424 | The representation of the mapping from a key to a value. 425 | 426 | >键值对:有映射关系的一对键和对应的键值。 427 | 428 | item: 429 | In a dictionary, another name for a key-value pair. 430 | 431 | >项:字典中键值对也叫项。 432 | 433 | key: 434 | An object that appears in a dictionary as the first part of a key-value pair. 435 | 436 | >键:字典中的一个对象,键值对中的第一部分。 437 | 438 | value: 439 | An object that appears in a dictionary as the second part of a key-value pair. This is more specific than our previous use of the word “value”. 440 | 441 | >键值:字典中的一个对象,键值对的第二部分。这个和之前提到的值不同,在字典使用过程中指代的是键值,而不是数值。 442 | 443 | implementation: 444 | A way of performing a computation. 445 | 446 | >实现:进行计算的一种方式。 447 | 448 | hashtable: 449 | The algorithm used to implement Python dictionaries. 450 | 451 | >哈希表:Python 实现字典的一种算法。 452 | 453 | hash function: 454 | A function used by a hashtable to compute the location for a key. 455 | 456 | >哈希函数:哈希表使用的一种函数,能计算出一个键的位置。 457 | 458 | hashable: 459 | A type that has a hash function. Immutable types like integers, floats and strings are hashable; mutable types like lists and dictionaries are not. 460 | 461 | >散列的:一种类型,有哈希函数。不可变类型比如整形、浮点数和字符串都是散列的;可变类型比如列表和字典则不是。 462 | 463 | >(译者注:这段我翻译的很狗,因为术语不是很熟悉,等有空我再查查去。) 464 | 465 | lookup: 466 | A dictionary operation that takes a key and finds the corresponding value. 467 | 468 | >查找:字典操作的一种,根据已有的键查找对应的键值。 469 | 470 | reverse lookup: 471 | A dictionary operation that takes a value and finds one or more keys that map to it. 472 | 473 | >逆向查找:字典操作的一种,根据一个键值找对应的一个或者多个键。 474 | 475 | raise statement: 476 | A statement that (deliberately) raises an exception. 477 | 478 | >raise 语句:特地要求抛出异常的一个语句。 479 | 480 | singleton: 481 | A list (or other sequence) with a single element. 482 | 483 | >单元素集:只含有一个单独元素的列表或者其他序列。 484 | 485 | call graph: 486 | A diagram that shows every frame created during the execution of a program, with an arrow from each caller to each callee. 487 | 488 | >调用图:一种图解,解释程序运行过程中每一个步骤,用箭头来来连接调用者和被调用者之间。 489 | 490 | memo: 491 | A computed value stored to avoid unnecessary future computation. 492 | 493 | >备忘:将计算得到的值存储起来,避免后续的额外计算。 494 | 495 | global variable: 496 | A variable defined outside a function. Global variables can be accessed from any function. 497 | 498 | >全局变量:函数外定义的变量。全局变量能被所有函数来读取使用。 499 | 500 | global statement: 501 | A statement that declares a variable name global. 502 | 503 | >global 语句:声明一个变量为全局的语句。 504 | 505 | flag: 506 | A boolean variable used to indicate whether a condition is true. 507 | 508 | >标识:一个布尔变量,用来指示一个条件是否为真。 509 | 510 | declaration: 511 | A statement like global that tells the interpreter something about a variable. 512 | 513 | >声明:比如 global 这样的语句,用来告诉解释器变量的特征。 514 | 515 | ## 11.10 练习 516 | ## # 练习1 517 | 518 | 写一个函数来读取 words.txt 文件中的单词,然后作为键存到一个字典中。键值是什么不要紧。然后用 in 运算符来快速检查一个字符串是否在字典中。 519 | 520 | 如果你做过第十章的练习,你可以对比一下这种实现和列表中的 in 运算符以及对折搜索的速度。 521 | 522 | ## # 练习2 523 | 524 | 读一下字典中 setdefault 方法的相关文档,然后用这个方法来写一个更精简版本的 invert_dict 函数。 [样例代码](http://thinkpython2.com/code/invert_dict.py])。 525 | 526 | ## # 练习3 527 | 528 | 用备忘的方法来改进一下第二章练习中的Ackermann函数,看看是不是能让让函数处理更大的参数。提示:不行。[样例代码](http://thinkpython2.com/code/ackermann_memo.py)。 529 | 530 | ## # 练习4 531 | 532 | 如果你做过了第七章的练习,应该已经写过一个名叫 has_duplicates 的函数了,这个函数用列表做参数,如果里面有元素出现了重复,就返回真。 533 | 534 | 535 | 用字典来写一个更快速更简单的版本。[样例代码](http://thinkpython2.com/code/has_duplicates.py)。 536 | 537 | ## # 练习5 538 | 539 | 一个词如果翻转顺序成为另外一个词,这两个词就为『翻转词对』(参见第五章练习的 rotate_word,译者注:作者这个练习我没找到。。。)。 540 | 541 | 542 | 写一个函数读取一个单词表,然后找到所有这样的单词对。[样例代码](http://thinkpython2.com/code/rotate_pairs.py)。 543 | 544 | ## # 练习6 545 | 546 | 下面是一个来自[Car Talk](http://www.cartalk.com/content/puzzlers)的谜语: 547 | 548 | 549 | >这条谜语来自一个名叫 Dan O'Leary的朋友。他最近发现一个单词,这个单词有一个音节,五个字母,然后有以下所述的特定性质。 550 | >去掉第一个字母,得到的是与原词同音异形异义词,发音与原词一模一样。替换一下首字母,也就是把第一个字母放回去,然后把第二个字母去掉,得到的是另外一个这样的同音异形异义词。那么问题来了,这是个什么词呢? 551 | 552 | 553 | >现在我给你提供一个错误的例子。咱们先看一下五个字母的单词,「wrack」。去掉第一个字母,得到的四个字母单词是「R-A-C-K」。但去掉第二个字母得到的是「W-A-C-K」,这就不是前两个词的同音异形异义词。(译者注:词义的细节就略去了,没有太大必要。) 554 | 555 | 556 | 557 | >但这个词至少有一个,Dan 和咱们都知道的,分别删除前两个字母会产生两个同音异形异义的四个字母的单词。问题就是,这是哪个词? 558 | 559 | 你可以用本章练习1的字典来检查一个字符串是否在一个字典之中。检查两个单词是不是同音异形异义词,可以用 CMU 发音字典。可以从[这里](http://www.speech.cs.cmu.edu/cgi-bin/cmudict)或者[这里](http://thinkpython2.com/code/c06d)或者[这里](http://thinkpython2.com/code/pronounce.py)来下载, 该字典提供了一个名为read_dictionary的函数,该函数会读取发音词典,然后返回一个 Python 词典,返回的这个词典会映射每一个单词到描述单词读音的字符串。 560 | 561 | 写一个函数来找到所有满足谜语要求的单词。[样例代码](http://thinkpython2.com/code/homophone.py)。 562 | -------------------------------------------------------------------------------- /chapter12.md: -------------------------------------------------------------------------------- 1 | # 第十二章 元组 2 | 本章我们要说的是另外一种内置类型,元组,以及列表、字典和元组如何协同工作。此外还有一个非常有用的功能:可变长度的列表,聚集和分散运算符。 3 | 4 | 5 | 一点提示:元组的英文单词 tuple 怎么读还有争议。有人认为是发[tʌpəl] 的音,就跟『supple』里面的一样读音。但编程语境下,大家普遍读[tu:pəl],跟『quadruple』里一样。 6 | 7 | ## 12.1 元组不可修改 8 | 9 | 元组是一系列的值。这些值可以是任意类型的,并且用整数序号作为索引,所以可以发现元组和列表非常相似。二者间重要的区别就是元组是不可修改的。 10 | 11 | 12 | 元组的语法是一系列用逗号分隔的值: 13 | 14 | ```Python 15 | >>> t = 'a', 'b', 'c', 'd', 'e' 16 | ``` 17 | 18 | 通常都用一对圆括号把元组的元素包括起来,当然不这样也没事。 19 | 20 | ```Python 21 | >>> t = ('a', 'b', 'c', 'd', 'e') 22 | ``` 23 | 24 | 要建立一个单个元素构成的元组,必须要在结尾加上逗号: 25 | 26 | ```Python 27 | >>> t1 = 'a', 28 | >>> type(t1) 29 | 30 | ``` 31 | 32 | 只用括号放一个值则并不是元组: 33 | 34 | ```Python 35 | >>> t2 = ('a') 36 | >>> type(t2) 37 | 38 | ``` 39 | 另一中建立元组的方法是使用内置函数 tuple。不提供参数的情况下,默认就建立一个空的元组。 40 | 41 | ```Python 42 | >>> t = tuple() 43 | >>> t 44 | () 45 | ``` 46 | 如果参数为一个序列(比如字符串、列表或者元组),结果就会得到一个以该序列元素组成的元组。 47 | 48 | ```Python 49 | >>> t = tuple('lupins') 50 | >>> t 51 | ('l', 'u', 'p', 'i', 'n', 's') 52 | ``` 53 | 54 | tuple 是内置函数命了,所以你就不能用来作为变量名了。 55 | 56 | 列表的各种运算符也基本适用于元组。方括号可以用来索引元素: 57 | 58 | ```Python 59 | >>> t = ('a', 'b', 'c', 'd', 'e') 60 | >>> t[0] 61 | 'a' 62 | ``` 63 | 64 | 切片运算符也可以用于选取某一区间的元素。 65 | 66 | ```Python 67 | >>> t[1:3] 68 | ('b', 'c') 69 | ``` 70 | 71 | 但如果你想修改元组中的某个元素,就会得到错误了: 72 | 73 | ```Python 74 | >>> t[0] = 'A' 75 | TypeError: object doesn't support item assignment 76 | ``` 77 | 因为元组是不能修改的,你不能修改其中的元素。但是可以用另一个元组来替换已有的元组。 78 | 79 | ```Python 80 | >>> t = ('A',) + t[1:] 81 | >>> t 82 | ('A', 'b', 'c', 'd', 'e') 83 | ``` 84 | 上面这个语句建立了一个新的元组,然后让 t 指向了这个新的元组。 85 | 86 | 关系运算符也适用于元组和其他序列;Python 从每个元素的首个元素开始对比。如果相等,就对比下一个元素,依此类推,之道找到不同元素为止。 87 | 88 | 有了不同元素之后,后面的其他元素就被忽略掉了(即便很大也没用)。 89 | 90 | ```Python 91 | >>> (0, 1, 2) < (0, 3, 4) 92 | True 93 | >>> (0, 1, 2000000) < (0, 3, 4) 94 | True 95 | ``` 96 | ## 12.2 元组赋值 97 | 98 | 对两个变量的值进行交换是一种常用操作。用常见语句来实现的话,就必须有一个临时变量。比如下面这个例子中是交换 a 和 b: 99 | 100 | ```Python 101 | >>> temp = a 102 | >>> a = b 103 | >>> b = temp 104 | ``` 105 | 106 | 这样解决还是挺麻烦的;用元组赋值就更简洁了: 107 | 108 | ```Python 109 | >>> a, b = b, a 110 | ``` 111 | 等号左边的是变量组成的一个元组;右边的是表达式的元组。每个值都被赋给了对应的变量。等号右边的表达式的值保留了赋值之前的初始值。 112 | 113 | 114 | 等号左右两侧的变量和值的数目都必须是一样的。 115 | 116 | ```Python 117 | >>> a, b = 1, 2, 3 118 | ValueError: too many values to unpack 119 | ``` 120 | 121 | 122 | 更普适的情况下,等号右边以是任意一种序列(字符串、列表或者元组)。比如,要把一个电子邮件地址转换成一个用户名和一个域名,可以用如下代码实现: 123 | 124 | ```Python 125 | >>> addr = 'monty@python.org' 126 | >>> uname, domain = addr.split('@') 127 | ``` 128 | 129 | split 的返回值是一个有两个元素的列表;第一个元素赋值给了 uname 这个变量,第二个赋值给了 domain 这个变量。 130 | 131 | ```Python 132 | >>> uname 133 | 'monty' 134 | >>> domain 135 | 'python.org' 136 | ``` 137 | ## 12.3 用元组做返回值 138 | 139 | 严格来说,一个函数只能返回一个值,但如果这个值是一个元组,效果就和返回多个值一样了。例如,如果你想要将两个整数相除,计算商和余数,如果要分开计算 x/y 以及 x%y 就很麻烦了。更好的办法是同时计算这两个值。 140 | 141 | 142 | 内置函数 divmod 就会接收两个参数,然后返回一个有两个值的元组,这两个值分别为商和余数。 143 | 144 | 可以把结果存储为一个元组: 145 | 146 | ```Python 147 | >>> t = divmod(7, 3) 148 | >>> t 149 | (2, 1) 150 | ``` 151 | 152 | 或者可以用元组赋值来分别存储这两个值: 153 | 154 | ```Python 155 | >>> quot, rem = divmod(7, 3) 156 | >>> quot 157 | 2 158 | >>> rem 159 | 1 160 | ``` 161 | 162 | 下面的例子中,函数返回一个元组作为返回值: 163 | 164 | ```Python 165 | def min_max(t): 166 | return min(t), max(t) 167 | ``` 168 | 169 | max 和 min 都是内置函数,会找到序列中的最大值或者最小值,min_max 这个函数会同时求得最大值和最小值,然后把这两个值作为元组来返回。 170 | 171 | ## 12.4 参数长度可变的元组 172 | 173 | 函数的参数可以有任意多个。用星号*开头来作为形式参数名,可以将所有实际参数收录到一个元组中。例如 printall 就可以获取任意多个数的参数,然后把它们都打印输出: 174 | 175 | ```Python 176 | def printall(*args): 177 | print(args) 178 | ``` 179 | 180 | 你可以随意命名收集来的这些参数,但 args 这个是约定俗成的惯例。下面展示一下这个函数如何使用: 181 | 182 | ```Python 183 | >>> printall(1, 2.0, '3') 184 | (1, 2.0, '3') 185 | ``` 186 | 187 | 与聚集相对的就是分散了。如果有一系列的值,然后想把它们作为多个参数传递给一个函数,就可以用星号*运算符。比如 divmod 要求必须是两个参数;如果给它一个元组,是不能进行运算的: 188 | 189 | ```Python 190 | >>> t = (7, 3) 191 | >>> divmod(t) 192 | TypeError: divmod expected 2 arguments, got 1 193 | ``` 194 | 但如果拆分这个元组,就可以了: 195 | 196 | ```Python 197 | >>> divmod(*t) 198 | (2, 1) 199 | ``` 200 | 很多内置函数都用到了参数长度可变的元组。比如 max 和 min 就可以接收任意数量的参数: 201 | 202 | ```Python 203 | >>> max(1, 2, 3) 204 | 3 205 | ``` 206 | 207 | 但求和函数 sum 就不行了。 208 | 209 | ```Python 210 | >>> sum(1, 2, 3) 211 | TypeError: sum expected at most 2 arguments, got 3 212 | ``` 213 | 214 | 做个练习,写一个名为 sumall 的函数,让它可以接收任意数量的参数,返回总和。 215 | 216 | ## 12.5 列表和元组 217 | 218 | zip 是一个内置函数,接收两个或更多的序列作为参数,然后返回返回一个元组列表,该列表中每个元组都包含了从各个序列中的一个元素。这个函数名的意思就是拉锁,就是把不相关的两排拉锁齿连接到一起。 219 | 220 | 221 | 下面这个例子中,一个字符串和一个列表通过 zip 这个函数连接到了一起: 222 | 223 | ```Python 224 | >>> s = 'abc' 225 | >>> t = [0, 1, 2] 226 | >>> zip(s, t) 227 | 228 | ``` 229 | 230 | 该函数的返回值是一个 zip 对象,该对象可以用来迭代所有的数值对。zip 函数经常被用到 for 循环中: 231 | 232 | ```Python 233 | >>> for pair in zip(s, t): ... 234 | print(pair) ... 235 | ('a', 0) ('b', 1) ('c', 2) 236 | ``` 237 | 238 | zip 对象是一种迭代器,也就是某种可以迭代整个序列的对象。迭代器和列表有些相似,但不同于列表的是,你无法通过索引来选择迭代器中的指定元素。 239 | 240 | 241 | 如果想用列表的运算符和方法,可以用 zip 对象来构成一个列表: 242 | 243 | ```Python 244 | >>> list(zip(s, t)) 245 | [('a', 0), ('b', 1), ('c', 2)] 246 | ``` 247 | 248 | 返回值是一个由元组构成的列表;在这个例子中,每个元组都包含了字符串中的一个字母,以及列表中对应位置的元素。 249 | 250 | 在长度不同的序列中,返回的结果长度取决于最短的一个。 251 | 252 | ```Python 253 | >>> list(zip('Anne', 'Elk')) 254 | [('A', 'E'), ('n', 'l'), ('n', 'k')] 255 | ``` 256 | 257 | 用 for 循环来遍历一个元组列表的时候,可以用元组赋值语句: 258 | 259 | ```Python 260 | t = [('a', 0), ('b', 1), ('c', 2)] 261 | for letter, number in t: 262 | print(number, letter) 263 | ``` 264 | 265 | 每次经历循环的时候,Python 都选中列表中的下一个元组,然后把元素赋值给字母和数字。该循环的输出如下: 266 | 267 | ```Python 268 | 0 a 1 b 2 c 269 | ``` 270 | 271 | 如果结合使用 zip、for 循环以及元组赋值,就能得到一种能同时遍历两个以上序列的代码组合。比如下面例子中的 has_match 这个函数,接收两个序列t1和 t2作为参数,然后如果存在一个索引位置 i 使得 t1[i] == t2[i]就返回真: 272 | 273 | ```Python 274 | def has_match(t1, t2): 275 | for x, y in zip(t1, t2): 276 | if x == y: 277 | return True 278 | return False 279 | ``` 280 | 281 | 如果你要遍历一个序列中的所有元素以及它们的索引,可以用内置的函数 enumerate: 282 | 283 | ```Python 284 | for index, element in enumerate('abc'): 285 | print(index, element) 286 | ``` 287 | 288 | 289 | enumerate 函数的返回值是一个枚举对象,它会遍历整个成对序列;每一对都包括一个索引(从0开始)以及给定序列的一个元素。在本节的例子中,输出依然如下: 290 | 291 | 292 | ```Python 293 | 0 a 1 b 2 c 294 | ``` 295 | 296 | ## 12.6 词典与元组 297 | 298 | 字典有一个名为 items 的方法,会返回一个由元组组成的序列,每一个元组都是字典中的一个键值对。 299 | 300 | ```Python 301 | >>> d = {'a':0, 'b':1, 'c':2} 302 | >>> t = d.items() 303 | >>> t 304 | dict_items([('c', 2), ('a', 0), ('b', 1)]) 305 | ``` 306 | 307 | 结果是一个 dict_items 对象,这是一个迭代器,迭代所有的键值对。可以在 for 循环里面用这个对象,如下所示: 308 | 309 | ```Python 310 | >>> for key, value in d.items(): 311 | ... print(key, value) 312 | ... c 2 a 0 b 1 313 | ``` 314 | 315 | 你也应该预料到了,字典里面的项是没有固定顺序的。 316 | 317 | 反过来使用的话,你就也可以用一个元组的列表来初始化一个新的字典: 318 | 319 | ```Python 320 | >>> t = [('a', 0), ('c', 2), ('b', 1)] 321 | >>> d = dict(t) 322 | >>> d 323 | {'a': 0, 'c': 2, 'b': 1} 324 | ``` 325 | 326 | 结合使用 dict 和 zip ,会得到一种建立字典的简便方法: 327 | 328 | ```Python 329 | >>> d = dict(zip('abc', range(3))) 330 | >>> d 331 | {'a': 0, 'c': 2, 'b': 1} 332 | ``` 333 | 334 | 字典的 update 方法也接收一个元组列表,然后把它们作为键值对添加到一个已存在的字典中。 335 | 336 | 把元组用作字典中的键是很常见的做法(主要也是因为这种情况不能用列表)。比如,一个电话字典可能就映射了姓氏、名字的数据对到不同的电话号码。假如我们定义了 last,first 和 number 这三个变量,可以用如下方法来实现: 337 | 338 | ```Python 339 | directory[last, first] = number 340 | ``` 341 | 方括号内的表达式是一个元组。我们可以用元组赋值语句来遍历这个字典。 342 | 343 | ```Python 344 | for last, first in directory: 345 | print(first, last, directory[last,first]) 346 | ``` 347 | 上面这个循环会遍历字典中的键,这些键都是元组。程序会把每个元组的元素分别赋值给 last 和 first,然后输出名字以及对应的电话号。 348 | 349 | 在状态图中表示元组的方法有两种。更详尽的版本会展示索引和元素,就如同在列表中一样。例如图12.1中展示了元组('Cleese', 'John') 。 350 | 351 | ________________________________________ 352 | ![Figure 12.1: State diagram](./images/figure12.1.jpg) 353 | Figure 12.1: State diagram. 354 | ________________________________________ 355 | 356 | 但随着图解规模变大,你也许需要省略掉一些细节。比如电话字典的图解可能会像图12.2所示。 357 | 358 | ________________________________________ 359 | ![Figure 12.2: State diagram](./images/figure12.2.jpg) 360 | Figure 12.2: State diagram. 361 | ________________________________________ 362 | 363 | 图中的元组用 Python 的语法来简单表示。其中的电话号码是 BBC 的投诉热线,所以不要给人家打电话哈。 364 | 365 | ## 12.7 由序列组成的序列 366 | 367 | 之前我一直在讲由元组组成的列表,但本章几乎所有的例子也适用于由列表组成的列表、元组组成的元组以及列表组成的元组。为了避免枚举所有的组合,咱们直接讨论序列组成的序列就更方便一些。 368 | 369 | 很多情况下,不同种类的序列(字符串、列表和元组)是可以交换使用的。那么该如何选择用哪种序列呢? 370 | 371 | 先从最简单的开始,字符串比起其他序列,功能更加有限,因为字符串中的元素必须是字符。而且还不能修改。如果你要修改字符串里面的字符(而不是要建立一个新字符串),你最好还是用字符列表吧。 372 | 373 | 374 | 列表用的要比元组更广泛,主要因为列表可以修改。但以下这些情况下,你还是用元组更好: 375 | 376 | 377 | 在某些情况下,比如返回语句中,用元组来实现语法上要比列表简单很多。 378 | 379 | 380 | 如果你要用一个序列作为字典的键,必须用元组或者字符串这样不可修改的类型才行。 381 | 382 | 383 | 如果你要把一个序列作为参数传给一个函数,用元组能够降低由于别名使用导致未知情况而带来的风险。 384 | 385 | 386 | 由于元组是不可修改的,所以不提供 sort 和 reverse 这样的方法,这些方法都只能修改已经存在的列表。但 Python 提供了内置函数 sorted,该函数接收任意序列,然后返回一个把该序列中元素重新排序过的列表,另外还有个内置函数 reversed,接收一个序列然后返回一个以逆序迭代整个列表的迭代器。 387 | 388 | ## 12.8 调试 389 | 390 | 列表、字典以及元组,都是数据结构的一些样例;在本章我们开始见识这些复合的数据结构,比如由元组组成的列表,或者包含元组作为键而列表作为键值的字典等等。符合数据结构非常有用,但容易导致一些错误,我把这种错误叫做结构错误;这种错误往往是由于一个数据结构中出现了错误的类型、大小或者结构而引起的。比如,如果你想要一个由一个整形构成的列表,而我给你一个单纯的整形变量(不是放进列表的),就会出错了。 391 | 392 | 393 | 要想有助于解决这类错误,我写了一个叫做structshape 的模块,该模块提供了一个同名函数,接收任何一种数据结构作为参数,然后返回一个字符串来总结该数据结构的形态。可以从 [这里](http://thinkpython2.com/code/structshape.py)下载。 394 | 395 | 396 | 下面是一个简单列表的示范: 397 | 398 | ```Python 399 | >>> from structshape import structshape 400 | >>> t = [1, 2, 3] 401 | >>> structshape(t) 402 | 'list of 3 int' 403 | ``` 404 | 405 | 更带劲点的程序可能还应该写“list of 3 ints”,但不理会单复数变化有利于简化问题。下面是一个列表的列表: 406 | 407 | ```Python 408 | >>> t2 = [[1,2], [3,4], [5,6]] 409 | >>> structshape(t2) 410 | 'list of 3 list of 2 int' 411 | ``` 412 | 413 | 如果列表元素是不同类型,structshape 会按照顺序,把每种类型都列出: 414 | 415 | ```Python 416 | >>> t3 = [1, 2, 3, 4.0, '5', '6', [7], [8], 9] 417 | >>> structshape(t3) 418 | 'list of (3 int, float, 2 str, 2 list of int, int)' 419 | ``` 420 | 421 | 下面是一个元组的列表: 422 | 423 | ```Python 424 | >>> s = 'abc' 425 | >>> lt = list(zip(t, s)) 426 | >>> structshape(lt) 427 | 'list of 3 tuple of (int, str)' 428 | ``` 429 | 430 | 下面是一个有三个项的字典,该字典映射了从整形数到字符串。 431 | 432 | ```Python 433 | >>> d = dict(lt) 434 | >>> structshape(d) 435 | 'dict of 3 int->str' 436 | ``` 437 | 438 | 如果你追踪自己的数据结构有困难,structshape这个模块能有所帮助。 439 | 440 | ## 12.9 Glossary 术语列表 441 | tuple: 442 | An immutable sequence of elements. 443 | 444 | >元组:一列元素组成的不可修改的序列。 445 | 446 | tuple assignment: 447 | An assignment with a sequence on the right side and a tuple of variables on the left. The right side is evaluated and then its elements are assigned to the variables on the left. 448 | 449 | >元组赋值:一种赋值语句,等号右侧用一个序列,左侧为一个变量构成的元组。右侧的内容先进行运算,然后这些元素会赋值给左侧的变量。 450 | 451 | gather: 452 | The operation of assembling a variable-length argument tuple. 453 | 454 | >收集:变量长度可变元组添加元素的运算。 455 | 456 | scatter: 457 | The operation of treating a sequence as a list of arguments. 458 | 459 | >分散:将一个序列拆分成一系列参数组成的列表的运算。 460 | 461 | zip object: 462 | The result of calling a built-in function zip; an object that iterates through a sequence of tuples. 463 | 464 | >拉链对象:调用内置函数 zip 得到的返回结果;一个遍历元组序列的对象。 465 | 466 | iterator: 467 | An object that can iterate through a sequence, but which does not provide list operators and methods. 468 | 469 | >迭代器:迭代一个序列的对象,这种序列不能提供列表的运算和方法。 470 | 471 | data structure: 472 | A collection of related values, often organized in lists, dictionaries, tuples, etc. 473 | 474 | >数据结构:一些有关系数据的集合体,通常是列表、字典或者元组等形式。 475 | 476 | shape error: 477 | An error caused because a value has the wrong shape; that is, the wrong type or size. 478 | 479 | >结构错误:由于一个值有错误的结构而导致的错误;比如错误的类型或者大小。 480 | 481 | ## 12.10 练习 482 | ## # 练习1 483 | 484 | 写一个名为most_frequent的函数,接收一个字符串,然后用出现频率降序来打印输出字母。找一些不同语言的文本素材,然后看看不同语言情况下字母的频率变化多大。然后用你的结果与[这里](http://en.wikipedia.org/wiki/Letter_frequencies)的数据进行对比。[样例代码](http://thinkpython2.com/code/most_frequent.py)。 485 | 486 | ## # 练习2 487 | 488 | 更多变位词了! 489 | 490 | 1. 写一个函数,读取一个文件中的一个单词列表(参考9.1),然后输出所有的变位词。 491 | 492 | 493 | 下面是可能的输出样式的示范: 494 | 495 | ['deltas', 'desalt', 'lasted', 'salted', 'slated', 'staled'] 496 | ['retainers', 'ternaries'] 497 | ['generating', 'greatening'] 498 | ['resmelts', 'smelters', 'termless'] 499 | 500 | 提示:你也许可以建立一个字典,映射一个特定的字母组合到一个单词列表,单词列表中的单词可以用这些字母来拼写出来。那么问题来了,如何去表示这个字母的集合,才能让这个集合能用作字典的一个键? 501 | 502 | 2.修改一下之前的程序,让它先输出变位词列表中最长的,然后是其次长的,依此类推。 503 | 504 | 3. 在拼字游戏中,当你已经有七个字母的时候,再添加一个字母就能组成一个八个字母的单词,这就 TMD『bingo』 了(什么鬼东西?老外拼字游戏就跟狗一样,翻着恶心死了)。然后哪八个字母组合起来最可能得到 bingo?提示:有七个。(简直就是狗一样的题目,麻烦死了,这里数据结构大家学会了就好了。)[样例代码](http://thinkpython2.com/code/anagram_sets.py). 505 | 506 | ## # 练习3 507 | 508 | 两个单词,如果其中一个通过调换两个字母位置就能成为另外一个,就成了一个『交换对』。协议额函数来找到词典中所有的这样的交换对。提示:不用测试所有的词对,不用测试所有可能的替换方案。[样例代码](http://thinkpython2.com/code/metathesis.py)。 鸣谢:本练习受启发于[这里](http://puzzlers.org)的一个例子。 509 | 510 | ## # 练习4 511 | 512 | 接下来又是一个[汽车广播字谜](http://www.cartalk.com/content/puzzlers): 513 | 514 | 515 | 一个英文单词,每次去掉一个字母,又还是一个正确的英文单词,这是什么词? 516 | 517 | 然后接下来字母可以从头去掉,也可以从末尾去掉,或者从中间,但不能重新排列其他字母。每次去掉一个字母,都会的到一个新的英文单词。然后最终会得到一个字母,也还是一个英文单词,这个单词也能在词典中找到。符合这样要求的单词有多少?最长的是哪个? 518 | 519 | 520 | 给你一个合适的小例子:Sprite。这个词就满足上面的条件。把 r 去掉了是 spite,去掉结尾的 e 是 spit,去掉 s 得到的是 pit,it,然后是 I。 521 | 522 | 523 | 写一个函数找到所有的这样的词,然后找到其中最长的一个。 524 | 525 | 526 | 这个练习比一般的练习难以些,所以下面是一些提示: 527 | 528 | 1. 你也许需要写一个函数,接收一个单词然后计算一下这个单词去掉一个字母能得到单词组成的列表。列表中这些单词就如同原始单词的孩子一样。 529 | 530 | 2. 只要一个 单词的孩子还可以缩减,那这个单词本身就亏缩减。你可以认为空字符串是可以缩减的,这样来作为一个基准条件。 531 | 532 | 3. 我上面提供的 words.txt 这个词表,不包含单个字母的单词。所以你需要自行添加 I、a 以及空字符串上去。 533 | 534 | 4. 要提高程序性能的话,你最好存储住已经算出来能被继续缩减的单词。[样例代码](http://thinkpython2.com/code/reducible.py)。 535 | 536 | -------------------------------------------------------------------------------- /chapter13.md: -------------------------------------------------------------------------------- 1 | # 第十三章 案例学习:数据结构的选择 2 | 3 | 到现在为止,你已经学过 Python 中最核心的数据结构了,也学过了一些与之对应的各种算法了。如果你想要对算法进行深入的了解,就可以来读一下第十三章。但不读这一章也可以继续;无论什么时候读都可以,感兴趣了就来看即可。 4 | 5 | 本章通过一个案例和一些练习,来讲解一下如何选择和使用数据结构。 6 | 7 | ## 13.1 词频统计 8 | 9 | 跟往常一样,你最起码也得先自己尝试做一下这些练习,然后再看参考答案。 10 | 11 | ## # 练习1 12 | 13 | 写一个读取文件的程序,把每一行拆分成一个个词,去掉空白和标点符号,然后把所有单词都转换成小写字母的。 14 | 15 | 16 | 提示:字符串模块 string 提供了一个名为 whitespace 的字符串,包含了空格、跳表符、另起一行等等,然后还有个 punctuation 模块,包含了各种标点符号的字符。咱们可以试试让 Python 把标点符号都给显示一下: 17 | 18 | ```Python 19 | >>> import string 20 | >>>string.punctuation 21 | '!"# $%&'()*+,-./:;<=>?@[\]^_`{|}~' 22 | ``` 23 | 24 | 另外你也可以试试字符串模块的其他方法,比如 strip、replace 以及 translate。 25 | 26 | ## # 练习2 27 | 28 | 访问[古登堡计划网站] (http://gutenberg.org),然后下载一个你最喜欢的公有领域的书,要下载纯文本格式的哈。 29 | 30 | 31 | 修改一下刚才上一个练习你写的程序,让这个程序能读取你下载的这本书,跳过文件开头部分的信息,继续以上个练习中的方式来处理一下整本书的正文。 32 | 33 | 34 | 然后再修改一下程序,让程序能统计一下整本书的总单词数目,以及每个单词出现的次数。 35 | 36 | 37 | 输出一下这本书中不重复的单词的个数。对比一下不同作者、不同地域的书籍。哪个作者的词汇量最丰富? 38 | 39 | ## # 练习3 40 | 41 | 再接着修改程序,输出一下每本书中最频繁出现的20个词。 42 | 43 | ## # 练习4 44 | 45 | 接着修改,让程序能读取一个单词列表(参考9.1),然后输出一下所有包含在书中,但不包含于单词列表中的单词。看看这些单词中有多少是排版错误的?有多少是本应被单词列表包含的常用单词?有多少是很晦涩艰深的罕见词汇? 46 | 47 | ## 13.2 随机数 48 | 49 | 输入相同的情况下,大多数计算机程序每次都会给出相同的输出,这也叫做确定性。确定性通常是一件好事,因为我们都希望同样的运算产生同样的结构。但有时候为了一些特定用途,咱们就需要让计算机能有不可预测性。比如游戏等等,有很多很多这样的情景。 50 | 51 | 52 | 然而,想让一个程序真正变成不可预测的,也是很难的,但好在有办法能让程序看上去不太确定。其中一种方法就是通过算法来产生假随机数。假随机数,顾名思义就知道不是真正随机的了,因为它们是通过一种确定性的运算来得到的,但这些数字看上去是随机的,很难与真正的随机数区分。 53 | 54 | 55 | (译者注:这里大家很容易一带而过,而不去探究到底怎样能确定是真随机数。实际上随机数是否能得到以及是否存在会影响哲学判断,可知论和不可知论等等。那么就建议大家思考和搜索一下,随机数算法产生的随机数和真正随机数有什么本质的区别,以及是否有办法得到真正的随机数。如果有,如何得到呢?) 56 | 57 | 58 | random 模块提供了生成假随机数的函数(从这里开始,咱们就用随机数来简称假随机数了哈)。 59 | 60 | 61 | 函数 random 返回一个在0.0到1.0的前闭后开区间(就是包括0.0但不包括1.0,这个特性在 Python 随处都是,比如序列的索引等等)的随机数。每次调用 random,就会得到一个很长的数列中的下一个数。如下这个循环就是一个例子了: 62 | 63 | ```Python 64 | import random for i in range(10): 65 | x = random.random() 66 | print(x) 67 | ``` 68 | 69 | randint函数接收两个参数作为下界和上界,然后返回一个二者之间的整数,这个整数可以是下界或者上界。 70 | 71 | ```Python 72 | >>> random.randint(5, 10) 73 | 5 74 | >>> random.randint(5, 10) 75 | 9 76 | ``` 77 | 78 | choice 函数可以用来从一个序列中随机选出一个元素: 79 | 80 | ```Python 81 | >>> t = [1, 2, 3] 82 | >>> random.choice(t) 83 | 2 84 | >>> random.choice(t) 85 | 3 86 | ``` 87 | 88 | random 模块还提供了其他一些函数,可以计算某些连续分布的随机值,比如Gaussian高斯分布, exponential指数分布, gamma γ分布等等。 89 | 90 | ## # 练习5 91 | 92 | 写一个名为 choose_from_hist 的函数,用这个函数来处理一下11.2中定义的那个histogram函数,从histogram 的值当中随机选择一个,这个选择的概率按照比例来定。比如下面这个histogram: 93 | 94 | ```Python 95 | >>> t = ['a', 'a', 'b'] 96 | >>> hist = histogram(t) 97 | >>> hist 98 | {'a': 2, 'b': 1} 99 | ``` 100 | 101 | 你的函数就应该返回a 的概率为2/3,返回b 的概率为1/3 102 | 103 | ## 13.3 词频 104 | 105 | 你得先把前面的练习作一下,然后再继续哈。可以从[这里](http://thinkpython2.com/code/analyze_book1.py)下载我的样例代码。 106 | 107 | 108 | 此外还要下载[这个](http://thinkpython2.com/code/emma.txt)。 109 | 110 | 111 | 下面这个程序先读取一个文件,然后对该文件中的词频进行了统计: 112 | 113 | ```Python 114 | import string 115 | def process_file(filename): 116 | hist = dict() 117 | fp = open(filename) 118 | for line in fp: 119 | process_line(line, hist) 120 | return hist 121 | def process_line(line, hist): 122 | line = line.replace('-', ' ') 123 | for word in line.split(): 124 | word = word.strip(string.punctuation + string.whitespace) 125 | word = word.lower() 126 | hist[word] = hist.get(word, 0) + 1 127 | hist = process_file('emma.txt') 128 | ``` 129 | 130 | 上面这个程序读取的是 emma.txt 这个文件,该文件是简奥斯汀的小说《艾玛》。 131 | 132 | process_file这个函数遍历整个文件,逐行读取,然后把每行的内容发给process_line函数。词频统计函数 hist 在该程序中是一个累加器。 133 | 134 | process_line使用字符串的方法 replace把各种连字符都用空格替换,然后用 split 方法把整行打散成一个字符串列表。程序遍历整个单词列表,然后用 strip 和 lower 这两个方法移除了标点符号,并且把所有字母都转换成小写的。(一定要记住,这里说的『转换』是图方便而已,实际上并不能转换,要记住字符串是不可以修改的,strip 和 lower 这些方法都是返回了新的字符串,一定要记得!) 135 | 136 | 最终,process_line 函数通过建立新项或者累加已有项,对字频统计 histogram 进行了更新。 137 | 138 | 要计算整个文件中的单词总数,就可以把 histogram 中的所有频数加到一起就可以了: 139 | 140 | ```Python 141 | def total_words(hist): 142 | return sum(hist.values()) 143 | ``` 144 | 145 | 不重复的单词的数目也就是字典中项的个数了: 146 | 147 | ```Python 148 | def different_words(hist): 149 | return len(hist) 150 | ``` 151 | 152 | 输出结果的代码如下: 153 | 154 | ```Python 155 | print('Total number of words:', total_words(hist)) 156 | print('Number of different words:', different_words(hist)) 157 | ``` 158 | 159 | 结果如下所示: 160 | 161 | ```Python 162 | Total number of words: 161080 163 | Number of different words: 7214 164 | ``` 165 | ## 13.4 最常用的单词 166 | 167 | 要找到最常用的词,可以做一个元组列表,每一个元组包含一个单词和该单词出现的次数,然后整理一下这个列表,就可以了。 168 | 169 | 下面的函数就接收了词频统计结果,然后返回一个『单词-次数』元组组成的列表: 170 | 171 | ```Python 172 | def most_common(hist): 173 | t = [] 174 | for key, value in hist.items(): 175 | t.append((value, key)) 176 | t.sort(reverse=True) 177 | return t 178 | ``` 179 | 180 | 这些元组中,要先考虑词频,返回的列表因此根据词频来排序。下面是一个输出最常用单词的循环体: 181 | 182 | ```Python 183 | t = most_common(hist) 184 | print('The most common words are:') 185 | for freq, word in t[:10]: 186 | print(word, freq, sep='\t') 187 | ``` 188 | 189 | 此处用了关键词 sep 来让 print 输出的时候以一个tab跳表符来作为分隔,而不是一个空格,这样第二列就会对齐。下面就是对《艾玛》这本小说的统计结果: 190 | 191 | 192 | (译者注:这个效果在 Python 下很明显,此处 markdown 我刚开始熟悉,不清楚咋实现。) 193 | 194 | ```Python 195 | The most common words are: 196 | to 5242 197 | the 5205 198 | and 4897 199 | of 4295 200 | i 3191 201 | a 3130 202 | it 2529 203 | her 2483 204 | was 2400 205 | she 2364 206 | ``` 207 | 208 | 如果使用 sort 函数的 key 参数,上面的代码还可以进一步简化。如果你好奇的话,可以进一步阅读一下[说明](https://wiki.python.org/moin/HowTo/Sorting) 209 | 210 | ## 13.5 可选的参数 211 | 212 | 咱们已经看过好多有可选参数的内置函数和方法了。实际上咱们自己也可以写,写这种有可选参数的自定义函数。比如下面就是一个根据词频数据来统计最常用单词的函数: 213 | 214 | ```Python 215 | def print_most_common(hist, num=10): 216 | t = most_common(hist) 217 | print('The most common words are:') 218 | for freq, word in t[:num]: 219 | print(word, freq, sep='\t') 220 | ``` 221 | 222 | 上面这个函数中,第一个参数是必须输入的;而第二个参数就是可选的了。第二个参数 num 的默认值是10. 223 | 224 | 225 | 如果只提供第一个参数: 226 | 227 | ```Python 228 | print_most_common(hist) 229 | ``` 230 | 231 | 这样 num 就用默认值了。如果提供两个参数: 232 | 233 | ```Python 234 | print_most_common(hist, 20) 235 | ``` 236 | 237 | 这样 num 就用参数值来赋值了。换句话说,可选参数可以覆盖默认值。 238 | 239 | 240 | 如果一个函数同时含有必需参数和可选参数,就必须在定义函数的时候,把必需参数全都放到前面,而可选的参数要放到后面。 241 | 242 | ## 13.6 字典减法 243 | 244 | 有的单词存在于书当中,但没有包含在文件 words.txt 的单词列表中,找这些单词就有点难了,你估计已经意识到了,这是一种集合的减法;也就是要从一个集合(也就是书)中所有不被另一个集合(也就是单词列表)包含的单词。 245 | 246 | 247 | 下面的代码中定义的 subtrac t这个函数,接收两个字典 d1和 d2,然后返回一个新字典,这个新字典包含所有 d1中包含而 d2中不包含的键。键值就无所谓了,就都设置为空即可。 248 | 249 | ```Python 250 | def subtract(d1, d2): 251 | res = dict() 252 | for key in d1: 253 | if key not in d2: 254 | res[key] = None 255 | return res 256 | ``` 257 | 258 | 要找到书中含有而words.txt 中不含有的单词,就可以用 process_file 函数来建立一个 words.txt 的词频统计,然后用 subtract 函数来相减: 259 | 260 | ```Python 261 | words = process_file('words.txt') 262 | diff = subtract(hist, words) 263 | print("Words in the book that aren't in the word list:") 264 | for word in diff.keys(): 265 | print(word, end=' ') 266 | ``` 267 | 268 | 下面依然还是对《艾玛》得到的结果: 269 | 270 | ```Python 271 | Words in the book that aren't in the word list: 272 | rencontre 273 | jane's 274 | blanche 275 | woodhouses 276 | disingenuousness 277 | friend's 278 | venice 279 | apartment 280 | ... 281 | ``` 282 | 283 | 这些单词有的是名字或者所有格之类的。另外的一些,比如『rencontre』,都是现在不怎么常用的了。不过也确实有一些单词是挺常用的,挺应该被列表所包含的! 284 | 285 | ## # 练习6 286 | 287 | Python 提供了一个数据结构叫 set(集合),该类型提供了很多常见的集合运算。可以在19.5阅读一下,或者阅读一下[这里的官方文档](http://docs.python.org/3/library/stdtypes.html# types-set)。 288 | 289 | 290 | 写一个程序吧,用集合的减法,来找一下书中包含而列表中不包含的单词吧。 [样例代码](http://thinkpython2.com/code/analyze_book2.py)。 291 | 292 | ## 13.7 随机单词 293 | 294 | 要从词频数据中选一个随机的单词,最简单的算法就是根据已知的单词频率来将每个单词复制相对应的个数的副本,然后组建成一个列表,从列表中选择单词: 295 | 296 | ```Python 297 | def random_word(h): 298 | t = [] 299 | for word, freq in h.items(): 300 | t.extend([word] * freq) 301 | return random.choice(t) 302 | ``` 303 | 304 | 上面代码中的[word] * freq表达式建立了一个列表,列表中字符串单词的出现次数即其原来的词频数。extend 方法和 append 方法相似,区别是前者的参数是一个序列,而后者是单独的元素。 305 | 306 | 上面这个算法确实能用,但效率实在不怎么好;每次选择随机单词的时候,程序都要重建列表,这个列表就和源书一样大了。很显然,一次性建立列表,而多次使用该列表来进行选择,这样能有明显改善,但列表依然还是很大。 307 | 308 | 309 | 备选的思路如下: 310 | 311 | 1. 用键来存储书中单词的列表。 312 | 313 | 2. 再建立一个列表,该列表包含所有词频的累加总和(参考练习2)。该列表的最后一个元素是书中所有单词的总数 n。 314 | 315 | 3. 选择一个1到 n 之间的随机数。使用折半法搜索(参考练习10),找到随机数在累计总和中所在位置的索引值。 316 | 317 | 4. 用该索引值来找到单词列表中对应的单词。 318 | 319 | ## # 练习7 320 | 321 | 写一个程序,用上面说的算法来从一本书中随机挑选单词。[样例代码](http://thinkpython2.com/code/analyze_book3.py)。 322 | 323 | ## 13.8 马科夫分析法 324 | 325 | 如果让你从一本书中随机挑选一些单词,这些单词都能理解,但估计难以成为一句话: 326 | 327 | ```Python 328 | this the small regard harriet which knightley's it most things 329 | ``` 330 | 331 | 一系列随机次很少能组成整句的话,因为这些单词连接起来并没有什么关系。例如,成句的话中,冠词 the 后面应该是跟着形容词或者名词,而不应该是动词或者副词。 332 | 333 | 衡量单词之间关系的一种方法就是马科夫分析法,这一方法就是:对给定的单词序列,分析一个词跟着另一个词后面出现的概率。比如,Eric, the Half a Bee这首歌的开头: 334 | 335 | ```Python 336 | Half a bee, philosophically, 337 | Must, ipso facto, half not be. 338 | But half the bee has got to be 339 | Vis a vis, its entity. D’you see? 340 | But can a bee be said to be 341 | Or not to be an entire bee 342 | When half the bee is not a bee 343 | Due to some ancient injury? 344 | ``` 345 | 346 | 在上面的文本中,『half the』这个词组后面总是跟着『bee』,但词组『the bee』后面可以是 『has』,也可以是 『is』。 347 | 348 | 马科夫分析的结果是从每个前缀(比如『half the』和『the bee』)到所有可能的后缀(比如『has』和『is』)的映射。 349 | 350 | 有了这一映射,你就可以制造随机文本了,用任意的前缀开头,然后从可能的后缀中随机选一个。下一次就把前缀的末尾和新的后缀结合起来,作为新的前缀,然后重复上面的步骤。 351 | 352 | 例如,你用前缀『Half a』来开始,那接下来的就必须是『bee』了,因为这个前缀只在文本中出现了一次。接下来,第二次了,前缀就变成了『a bee』了,所以接下来的后缀可以是『philosophically』, 『be』或者『due』。 353 | 354 | 在这个例子中,前缀的长度总是两个单词,但你可以以任意长度的前缀来进行马科夫分析。 355 | 356 | ## # 练习8 357 | 358 | Markov analysis 马科夫分析: 359 | 360 | 1. 写一个程序,读取文件中的文本,然后进行马科夫分析。结果应该是一个字典,从前缀到一个可能的后缀组成的序列的映射。这个序列可以是列表,元组,也可以是字典;你自己来选择合适的类型来写就好。你可以用两个单词长度的前缀来测试你的程序,但应该让程序能够兼容其他长度的前缀。 361 | 362 | 2. 在上面的程序中添加一个函数,基于马科夫分析来生成随机文本。下面是用《艾玛》使用两个单词长度的前缀来生成的一个随机文本样例: 363 | 364 | ```Python 365 | He was very clever, be it sweetness or be angry, ashamed or only amused, at such a stroke. She had never thought of Hannah till you were never meant for me?" "I cannot make speeches, Emma:" he soon cut it all himself. 366 | ``` 367 | 这个例子中,我保留了单词中连接的标点符号。得到的结果在语法上基本是正确的,但也还差点。语义上,这些单词连起来也还能有一些意义,但也不咋对劲。 368 | 369 | 如果增加前缀的单词长度会怎么样?随机文本是不是读起来更通顺呢? 370 | 371 | 3. O一旦你的程序能用了,你可以试试混搭一下:如果你把两本以上的书合并起来,生成的随机文本就会以很有趣的方式从多种来源混合单词和短语来生成随机文本。 372 | 373 | 引用:这个案例研究是基于Kernighan 和 Pike 在1999年由Addison-Wesley出版社出版的《The Practice of Programming》一书中的一个例子。 374 | 375 | 376 | 你应该自己独立尝试一下这些练习,然后再继续;然后你可以下载[我的样例代码](http://thinkpython2.com/code/markov.py)。另外你可能需要下载 [《艾玛》这本书的文本文件](http://thinkpython2.com/code/emma.txt)。 377 | 378 | ## 13.9 数据结构 379 | 380 | 使用马科夫分析来生成随机文本挺有意思的,但这个练习还有另外一个要点:数据结构的选择。在前面这些练习中,你必须要选择以下内容: 381 | 382 | * 如何表示前缀。 383 | 384 | * 如何表示可能后缀的集合。 385 | 386 | * 如何表示每个前缀与对应的后缀集合之间的映射。 387 | 388 | 最后一个最简单了:明显就应该用字典了,这样来把每个键映射到对应的多个值。 389 | 390 | 前缀的选择,明显可以使用字符串、字符串列表,或者字符串元组。 391 | 392 | 后缀的先泽,要么是用列表,要么就用咱们之前写过的词频函数 histogram(这个也是个字典)。 393 | 394 | 该咋选呢?第一步就是想一下,每种数据结构都需要用到哪些运算。比如对前缀来说,咱们就得能删掉头部,然后在尾部添加新词。例如,加入现在的前缀是『Half a』,接下来的单词是『bee』,就得能够组成下一个前缀,也就是『a bee』。 395 | 396 | 你的首选估计就是列表了,因为列表很容易增加和剔除元素,但我们还需要能用前缀做字典中的键,所以列表就不合格了。那就剩元组了,元组没法添加和删除元素,但可以用加法运算符来建立新的元组。 397 | 398 | ```Python 399 | def shift(prefix, word): 400 | return prefix[1:] + (word,) 401 | ``` 402 | 上面这个 shift 函数,接收一个单词的元组,也就是前缀,然后还接收一个字符串,也就是单词了,然后形成一个新的元组,就是把原有的前缀去掉头部,用新单词拼接到尾部。 403 | 404 | 对后缀的集合来说,我们需要进行的运算包括添加新的后缀(或者增加一个已有后缀的频次),然后选择一个随机的后缀。 405 | 406 | 添加新后缀,无论是用列表还是用词频字典,实现起来都一样容易。不过从列表中选择一个随机元素很容易;但从词频字典中选择随机元素实现起来就不太有效率了(参考练习7)。 407 | 408 | 目前为止,我们说完了实现难度,但对数据结构的选择还要考虑一些其他的因素。比如运行时间。有时候要考虑理论上的原因来考虑,最好的数据结构要比其他的快;例如我之前提到了 in 运算符在字典中要比列表中速度快,最起码当元素数量增多的时候会体现出来。 409 | 410 | 但一般情况下,咱们不能提前知道哪一种实现方法的速度更快。所以就可以两种都写出来,然后运行对比一下,看看到底哪个快。这种方法就叫对比测试。另外一种方法是选一个实现起来最简单的数据结构,然后看看运行速度是不是符合问题的要求。如果可以,就不用再改进了。如果速度不够快,就亏用到一些工具,比如 profile 模块,来判断程序的哪些部分消耗了最多的运行时间。 411 | 412 | 此外还要考虑的一个因素就是存储空间了。比如用一个词频字典作为后缀集合就可能省一些存储空间,因为无论这些单词在稳重出现了多少次,在该字典中每个单词只存储一次。有的情况下,节省空间也能让你的程序运行更快,此外在一些极端情况下,比如内存耗尽了,你的程序就根本无法运行了。不过对大多数应用来说,都是优先考虑运行时间,存储空间只是次要因素了。 413 | 414 | 最后再考虑一下:上文中,我已经暗示了,咱们选择某种数据结构,要兼顾分析和生成。但这二者是分开的步骤,所以也可以分析的时候用一种数据结构,而生成的时候再转换成另外一种结构。只要生成时候节省的时间胜过转换所花费的时间,相权衡之下依然是划算的。 415 | 416 | ## 13.10 调试 417 | 418 | 调试一个程序的时候,尤其是遇到特别严峻的问题的时候,有以下五个步骤一定要做好: 419 | 420 | * 阅读代码: 421 | 422 | 好好检查代码,多读几次,好好看看代码所表述的内容是不是跟你的设想相一致。 423 | 424 | * 运行程序: 425 | 426 | 做一些修改,然后运行各个版本来对比实验一下。通常来说,只要你在程序对应的位置加上输出,问题就能比较明确了,不过有时候你还是得搭建一些脚手架代码来帮忙找错误。 427 | 428 | * 反复思考: 429 | 430 | 多花点时间去思考!想下到底是什么类型的错误:语法,运行,还是语义错误?从错误信息以及程序的输出能得到什么信息?想想哪种错误能引起你所看到的问题?问题出现之前的那一次你做了什么修改? 431 | 432 | * 小黄鸭调试法: 433 | 434 | 如果你对另外一个人解释问题,你有时候就能在问完问题之前就找到答案。通常你根本不用找另外一个人;就根一个橡胶鸭子说就可以了。这就是很著名的所谓小黄鸭调试法的起源了。我可不是瞎编的哈;看[这里的解释](https://en.wikipedia.org/wiki/Rubber_duck_debugging)。 435 | 436 | * 以退为进: 437 | 438 | 有时候,最佳的策略反而就是后撤,取消最近的修改,一直到程序恢复工作,并且你能清楚理解。然后再重头来改进。 439 | 440 | 新手程序员经常会在上面这些步骤中的某一项上卡壳,然后忘了其他的步骤。上面的每一步都有各自的失灵情况。 441 | 442 | 比如,错误很典型的情况下,阅读代码也许有效,但如果错误是概念上误解导致的,这就没啥用了。如果你不理解你程序的功能,你就算读上一百测也找不到错误,因为是你脑中的理解有错误。 443 | 444 | 在你进行小规模的简单测试的时候,进行试验会有用。但如果不思考和阅读代码,你就可以陷入到我称之为『随机走路编程』的陷阱中,这种过程就是随机做一些修改,一直到程序工作位置。毋庸置疑,这种随机修改肯定得浪费好多时间的。 445 | 446 | 最重要的就是思考,一定要花时间去思考。调试就像是一种实验科学。你至少应该对问题的本质有一种假设。如果有两种或者两种以上的可能性,就要设计个测试,来逐个排除可能性。 447 | 448 | 然而一旦错误特别多了,再好的调试技术也不管用的,程序太大太复杂也会容易有类似情况。所以有时候最好的方法就是以退为进,简化一下程序,直到能工作了,并且你能理解整个程序了为止。 449 | 450 | 新手程序员经常不愿意后撤,因为他们不情愿删掉一行代码(哪怕是错误的代码)。可以这样,复制一下整个代码到另外一个文件中做个备份,然后再删减,这样是不是感觉好些。然后你可以再复制回来的。 451 | 452 | 找到一个困难问题的解决方法,需要阅读、测试、分析,有时候还要后撤。如果你在某一步骤中卡住了,试试其他方法。 453 | 454 | ## 13.11 Glossary 术语列表 455 | deterministic: 456 | Pertaining to a program that does the same thing each time it runs, given the same inputs. 457 | 458 | >确定性:给定同样的输出,程序每次运行结果都相同。 459 | 460 | pseudorandom: 461 | Pertaining to a sequence of numbers that appears to be random, but is generated by a deterministic program. 462 | 463 | >假随机数:一段数字序列中的数,看上去似乎是随机的,但实际上也是由确定的算法来生成的。 464 | 465 | default value: 466 | The value given to an optional parameter if no argument is provided. 467 | 468 | >默认值:如果不对可选参数进行赋值的话,该参数会用默认设置的值。 469 | 470 | override: 471 | To replace a default value with an argument. 472 | 473 | >覆盖:用户在调用函数的时候给可选参数提供了参数,这个参数就覆盖掉默认值。 474 | 475 | benchmarking: 476 | The process of choosing between data structures by implementing alternatives and testing them on a sample of the possible inputs. 477 | 478 | >对比测试: 479 | 480 | rubber duck debugging: 481 | Debugging by explaining your problem to an inanimate object such as a rubber duck. Articulating the problem can help you solve it, even if the rubber duck doesn’t know Python. 482 | 483 | >小黄鸭调试法:对一个无生命的对象来解释你的问题,比如小黄鸭之类的,这样来调试。描述清楚问题很有助于解决问题,所以虽然小黄鸭并不会理解 Python 也不要紧。 484 | 485 | ## 13.12 练习 486 | ## # 练习9 487 | 单词的『排名』就是在一个单词列表中,按照出现频率而排的位置:最常见的单词就排名第一了,第二常见的就排第二,依此类推。 488 | 489 | [Zipf定律](http://en.wikipedia.org/wiki/Zipf's_law) 描述了自然语言中排名和频率的关系。该定律预言了排名 r 与词频 f 之间的关系如下: 490 | 491 | $$ 492 | f = cr^{−s} 493 | $$ 494 | 495 | 这里的 s 和 c 都是参数,依据语言和文本而定。如果对等式两边同时取对数,得到如下公式: 496 | 497 | $$ 498 | \log f = \log c − s*\log r 499 | $$ 500 | 501 | (译者注:Zipf定律是美国学者G.K.齐普夫提出的。可以表述为:在自然语言的语料库里,一个单词出现的频率与它在频率表里的排名成反比。) 502 | 503 | 因此如果你将 log f 和 log r 进行二维坐标系投点,就应该得到一条直线,斜率是-s,截距是 log c。 504 | 505 | 写一个程序,从一个文件中读取文本,统计单词频率,然后每个单词一行来输出,按照词频的降序,同时输出一下 log f 和 log r。 506 | 507 | 选一种投图程序,把结果进行投图,然后检查一下是否为一条直线。 508 | 509 | 能否估计一下 s 的值呢? 510 | 511 | [样例代码](http://thinkpython2.com/code/zipf.py)。要运行刚刚这个代码的话,你需要有投图模块 matplotlib。如果你安装了 Anaconda,就已经有 matplotlib 了;或者你就可能需要安装一下了。 512 | 513 | (译者注:matplotlib 的安装方法有很多,比如 pip install matplotlib 或者 easy_install -U matplotlib) 514 | 515 | -------------------------------------------------------------------------------- /chapter14.md: -------------------------------------------------------------------------------- 1 | # 第十四章 文件 2 | 3 | 本章介绍的内容是『持久的』程序,就是把数据进行永久存储,本章介绍了永久存储的不同种类,比如文件与数据库。 4 | 5 | ## 14.1 持久 6 | 7 | 目前为止我们见过的程序大多是很短暂的,它们往往只是运行那么一会,然后产生一些输出,等运行结束了,它们的数据就也都没了。如果你再次运行一个程序,又要从头开始了。 8 | 9 | 另外的一些程序就是持久的:它们运行时间很长(甚至一直在运行);这些程序还会至少永久保存一部分数据(比如存在硬盘上等等);然后如果程序关闭了或者重新开始了,也能从之前停留的状态继续工作。 10 | 11 | 这种有持久性的程序的例子很多,比如操作系统,几乎只要电脑开着,操作系统就要运行;再比如网站服务器,也是要一直开着,等待来自网络上的请求。 12 | 13 | 程序保存数据最简单的方法莫过于读写文本文件。之前我们已经见过一些读取文本文件的程序了;本章中我们会来见识一下写出文本的程序。 14 | 15 | 另一种方法是把程序的状态存到数据库里面。在本章我会演示一种简单的数据库,以及一个 pickle 模块,这个模块大大简化了保存程序数据的过程。 16 | 17 | ## 14.2 读写文件 18 | 19 | 文本文件就是一系列的字符串,存储在一个永久介质中,比如硬盘、闪存或者光盘之类的东西里面。 20 | 21 | 在9.1的时候我们就看到过如何打开和读取一个文件了。 22 | 23 | 要写入一个文件,就必须要在打开它的时候用『w』作为第二个参数(译者注:w 就是 write 的意思了): 24 | 25 | ```Python 26 | >>> fout = open('output.txt', 'w') 27 | ``` 28 | 29 | 如果文件已经存在了,这样用写入的模式来打开,会把旧的文件都清除掉,然后重新写入文件,所以一定要小心!如果文件不存在,程序就会创建一个新的。 30 | 31 | open 函数会返回一个文件对象,文件对象会提供各种方法来处理文件。write 这个方法就把数据写入到文件中了。 32 | 33 | ```Python 34 | >>> line1 = "This here's the wattle,\n" 35 | >>> fout.write(line1) 36 | 24 37 | ``` 38 | 39 | 返回值是已写入字符的数量。文件对象会记录所在位置,所以如果你再次调用write方法,会从文件结尾的地方继续添加新的内容。 40 | 41 | ```Python 42 | >>> line2 = "the emblem of our land.\n" 43 | >>> fout.write(line2) 44 | 24 45 | ``` 46 | 47 | 写完文件之后,你需要用 close 方法来关闭文件。 48 | 49 | ```Python 50 | >>> fout.close() 51 | ``` 52 | 如果不 close 这个文件,就要等你的程序运行结束退出的时候,它自己才关闭了。 53 | 54 | ## 14.3 格式运算符 55 | 56 | write 方法必须用字符串来做参数,所以如果要把其他类型的值写入文件,就得先转换成字符串才行。最简单的方法就是用 str函数: 57 | 58 | ```Python 59 | >>> x = 52 60 | >>> fout.write(str(x)) 61 | ``` 62 | 63 | 另外一个方法就是用格式运算符,也就是百分号%。在用于整数的时候,百分号%是取余数的运算符。但当第一个运算对象是字符串的时候,百分号%就成了格式运算符了。 64 | 65 | 66 | 第一个运算对象也就是说明格式的字符串,包含一个或者更多的格式序列,规定了第二个运算对象的输出格式。返回的结果就是格式化后的字符串了。 67 | 68 | 69 | 例如,'%d'这个格式序列的意思就是第二个运算对象要被格式化成为一个十进制的整数: 70 | 71 | ```Python 72 | >>> camels = 42 73 | >>> '%d' % camels 74 | '42' 75 | ``` 76 | 你看,经过格式化后,结果就是字符串'42'了,而不是再是整数值42了。 77 | 78 | 这种格式化序列可以放到一个字符串的任何一个位置,这样就可以在一句话里面嵌入一个值了: 79 | 80 | ```Python 81 | >>> 'I have spotted %d camels.' % camels 82 | 'I have spotted 42 camels.' 83 | ``` 84 | 85 | 如果格式化序列有一个以上了,那么第二个参数就必须是一个元组了。每个格式序列对应元组当中的一个元素,次序相同。 86 | 87 | 下面的例子中,用了'%d'来格式化输出整型值,用'%g'来格式化浮点数,'%s'就是给字符串用的了。 88 | 89 | ```Python 90 | >>> 'In %d years I have spotted %g %s.' % (3, 0.1, 'camels') 91 | 'In 3 years I have spotted 0.1 camels.' 92 | ``` 93 | 这就要注意了,如果字符串中格式化序列有多个,那个数一定要和后面的元组中元素数量相等才行。另外格式化序列与元组中元素的类型也必须一样: 94 | 95 | ```Python 96 | >>> '%d %d %d' % (1, 2) 97 | TypeError: not enough arguments for format string 98 | >>> '%d' % 'dollars' 99 | TypeError: %d format: a number is required, not str 100 | ``` 101 | 102 | 第一个例子中,后面元组的元素数量缺一个,所以报错了;第二个例子中,元组里面的元素类型与前面格式不匹配,所以也报错了。 103 | 104 | 想要对格式运算符进行深入了解,可以点击[这里](https://docs.python.org/3/library/stdtypes.html# printf-style-string-formatting)。然后还有一种功能更强大的替代方法,就是用字符串的格式化方法 format,可以点击[这里](https://docs.python.org/3/library/stdtypes.html# str.format)来了解更多细节。 105 | 106 | ## 14.4 文件名与路径 107 | 108 | 文件都是按照目录(也叫文件夹)来组织存放的。每一个运行着的程序都有一个当前目录,也就是用来处理绝大多数运算和操作的默认目录。比如当你打开一个文件来读取内容的时候,Python 就从当前目录先来查找这个文件了。 109 | 110 | 提供函数来处理文件和目录的是 os 模块(os 就是 operating system即操作系统的缩写)。 111 | 112 | ```Python 113 | >>> import os 114 | >>> cwd = os.getcwd() 115 | >>> cwd 116 | '/home/dinsdale' 117 | ``` 118 | cwd 代表的是『current working directory』(即当前工作目录)的缩写。刚刚这个例子中返回的结果是/home/dinsdale,这就是一个名字叫 dinsdale 的人的个人账户所在位置了。 119 | 120 | 像是’/home/dinsdale’这样表示一个文件或者目录的字符串就叫做路径。 121 | 122 | 一个简单的文件名,比如 memo.txt 也可以被当做路径,但这是相对路径,因为这种路径是指代了文件与当前工作目录的相对位置。如果当前目录是/home/dinsdale,那么 memo.txt 这个文件名指代的就是/home/dinsdale/memo.txt 这个文件了。 123 | 124 | 用右斜杠/开头的路径不依赖当前目录;这就叫做绝对路径。要找到一个文件的绝对路径,可以用 os.path.abspath: 125 | 126 | ```Python 127 | >>> os.path.abspath('memo.txt') 128 | '/home/dinsdale/memo.txt' 129 | ``` 130 | os.path 提供了其他一些函数,可以处理文件名和路径。比如 os.path.exists 会检查一个文件或者目录是否存在: 131 | 132 | ```Python 133 | >>> os.path.exists('memo.txt') 134 | True 135 | ``` 136 | 137 | 如果存在,os.path.isdir 可以来检查一下对象是不是一个目录: 138 | 139 | ```Python 140 | >>> os.path.isdir('memo.txt') 141 | False 142 | >>> os.path.isdir('/home/dinsdale') 143 | True 144 | ``` 145 | 146 | 同理,os.path.isfile 就可以检查对象是不是一个文件了。 147 | 148 | os.listdir 会返回指定目录内的文件(以及次级目录)列表。 149 | 150 | ```Python 151 | >>> os.listdir(cwd) 152 | ['music', 'photos', 'memo.txt'] 153 | ``` 154 | 155 | 为了展示一下这些函数的用法,下面这个例子中,walks 这个函数就遍历了一个目录,然后输出了所有该目录下的文件的名字,并且在该目录下的所有子目录中递归调用自身。 156 | 157 | ```Python 158 | def walk(dirname): 159 | for name in os.listdir(dirname): 160 | path = os.path.join(dirname, name) 161 | if os.path.isfile(path): 162 | print(path) 163 | else: 164 | walk(path) 165 | ``` 166 | 167 | os.path.join 接收一个目录和一个文件名做参数,然后把它们拼接成一个完整的路径。 168 | 169 | 170 | os 模块还提供了一个叫 walk 的函数,与上面这个函数很像,功能要更强大一些。做一个练习吧,读一下文档,然后用这个 walk 函数来输出给定目录中的文件名以及子目录的名字。可以从[这里](http://thinkpython2.com/code/walk.py)下载我的样例代码。 171 | 172 | ## 14.5 捕获异常 173 | 174 | 读写文件的时候有很多容易出错的地方。如果你要打开的文件不存在,就会得到一个 IOerror: 175 | 176 | ```Python 177 | >>> fin = open('bad_file') 178 | IOError: [Errno 2] No such file or directory: 'bad_file' 179 | ``` 180 | 181 | 如果你要读取一个文件却没有权限,就得到一个权限错误permissionError: 182 | 183 | ```Python 184 | >>> fout = open('/etc/passwd', 'w') 185 | PermissionError: [Errno 13] Permission denied: '/etc/passwd' 186 | ``` 187 | 188 | 如果你把一个目录错当做文件来打开,就会得到下面这种IsADirectoryError错误了: 189 | 190 | ```Python 191 | >>> fin = open('/home') 192 | IsADirectoryError: [Errno 21] Is a directory: '/home' 193 | ``` 194 | 195 | 你可以用像是os.path.exists、os.path.isfile 等等这类的函数来避免上面这些错误,不过这就需要很长时间,还要检查很多代码(比如“Errno 21”就表明有至少21处地方有可能存在错误)。 196 | 197 | 198 | 所以更好的办法是提前检查,用 try 语句,这种语句就是用来处理异常情况的。其语法形式就跟 if...else 语句是差不多的: 199 | 200 | ```Python 201 | try: 202 | fin = open('bad_file') 203 | except: 204 | print('Something went wrong.') 205 | ``` 206 | 207 | Python 会先执行 try 后面的语句。如果运行正常,就会跳过 except 语句,然后继续运行。如果除了异常,就会跳出 try 语句,然后运行 except 语句中的代码。 208 | 209 | 210 | 这种用 try 语句来处理异常的方法,就叫异常捕获。上面的例子中,except 语句中的输出信息并没有什么用。一般情况,得到异常之后,你可以选择解决掉这个问题或者再重试一下,或者就以正常状态退出程序了。 211 | 212 | ## 14.6 数据库 213 | 214 | 数据库是一个用来管理已存储数据的文件。很多数据库都以类似字典的形式来管理数据,就是从键到键值成对映射。数据库和字典的最大区别就在于数据库是存储在磁盘(或者其他永久性存储设备中),所以程序运行结束退出后,数据库依然存在。 215 | 216 | 217 | (译者注:这里作者为了便于理解,对数据库的概念进行了极度的简化,实际上数据库的类型、模式、功能等等都与字典有很大不同,比如有关系型数据库和非关系型数据库,还有分布式的和单一文件式的等等。如果有兴趣对数据库进行进一步了解,译者推荐一本书:SQLite Python Tutorial。) 218 | 219 | dbm 模块提供了一个创建和更新数据库文件的交互接口。下面这个例子中,我创建了一个数据库,其中的内容是图像文件的标题。 220 | 221 | 打开数据库文件就跟打开其他文件差不多: 222 | 223 | ```Python 224 | >>> import dbm 225 | >>> db = dbm.open('captions', 'c') 226 | ``` 227 | 228 | 后面这个 c 是一个模式,意思是如果该数据库不存在就创建一个新的。得到的返回结果就是一个数据库对象了,用起来很多的运算都跟字典很像。 229 | 230 | 创建一个新的项的时候,dbm 就会对数据库文件进行更新了。 231 | 232 | ```Python 233 | >>> db['cleese.png'] = 'Photo of John Cleese.' 234 | ``` 235 | 236 | 读取里面的某一项的时候,dbm 就读取数据库文件: 237 | 238 | ```Python 239 | >>>db['cleese.png'] 240 | b'Photo of John Cleese.' 241 | ``` 242 | 243 | 上面的代码返回的结果是一个二进制对象,这也就是开头有个 b 的原因了。二进制对象就跟字符串在很多方面都挺像的。以后对 Python 的学习深入了之后,这种区别就变得很重要了,不过现在还不要紧,咱们就忽略掉。 244 | 245 | 246 | 如果对一个已经存在值的键进行赋值,dbm 就会把旧的值替换成新的值: 247 | 248 | ```Python 249 | >>> db['cleese.png'] = 'Photo of John Cleese doing a silly walk.' 250 | >>> db['cleese.png'] 251 | b'Photo of John Cleese doing a silly walk.' 252 | ``` 253 | 254 | 字典的一些方法,比如 keys 和 items,是不能用于数据库对象的。但用一个 for 循环来迭代是可以的: 255 | 256 | ```Python 257 | for key in db: 258 | print(key, db[key]) 259 | ``` 260 | 261 | 然后就同其他文件一样,用完了之后你得用 close 方法关闭数据库: 262 | 263 | ```Python 264 | >>> db.close() 265 | ``` 266 | ## 14.7 Pickle模块 267 | 268 | dbm 的局限就在于键和键值必须是字符串或者二进制。如果用其他类型数据,就得到错误了。 269 | 270 | 271 | 这时候就可以用 pickle 模块了。该模块可以把几乎所有类型的对象翻译成字符串模式,以便存储在数据库中,然后用的时候还可以把字符串再翻译回来。 272 | 273 | 274 | pickle.dumps 接收一个对象做参数,然后返回一个字符串形式的内容翻译(dumps 就是『dump string』的缩写): 275 | 276 | ```Python 277 | >>> import pickle 278 | >>> t = [1, 2, 3] 279 | >>> pickle.dumps(t) 280 | b'\x80\x03]q\x00(K\x01K\x02K\x03e.' 281 | ``` 282 | 这种格式让人读起来挺复杂;这种设计能让 pickle 模块解译起来比较容易。pickle.lods("load string")就又会把原来的对象解译出来: 283 | 284 | ```Python 285 | >>> t1 = [1, 2, 3] 286 | >>> s = pickle.dumps(t1) 287 | >>> t2 = pickle.loads(s) 288 | >>> t2 289 | [1, 2, 3] 290 | ``` 291 | 292 | 这里要注意了,新的对象与旧的有一样的值,但(通常)并不是同一个对象: 293 | 294 | ```Python 295 | >>> t1 == t2 296 | True 297 | >>> t1 is t2 298 | False 299 | ``` 300 | 换句话说,就是说 pickle 解译的过程就如同复制了原有对象一样。 301 | 302 | 有 pickle了,就可以把非字符串的数据也存到数据库里面了。实际上这种结合方式特别普遍,已经封装到一个叫shelve的模块中了。 303 | 304 | ## 14.8 管道 305 | 306 | 大多数操作系统都提供了一个命令行接口,也被称作『shell』。Shell 通常提供了很多基础的命令,能够来搜索文件系统,以及启动应用软件。比如,在 Unix 下面,就可以通过 cd 命令来切换目录,用 ls 命令来显示一个目录下的内容,如果装了火狐浏览器,就可以输入 firefox 来启动浏览器了。 307 | 308 | 在 shell 下能够启动的所有程序,也都可以在 Python 中启动,这要用到一个 pipe 对象,这个直接翻译意思为管道的对象可以理解为 Python 到操作系统的 Shell 进行通信的途径,一个 pipe 对象就代表了一个运行的程序。 309 | 310 | 举个例子吧,Unix 的 ls -l 命令通常会用长文件名格式来显示当前目录的内容。在 Python 中就可以用 os.open 来启动它: 311 | 312 | ```Python 313 | >>> cmd = 'ls -l' 314 | >>> fp = os.popen(cmd) 315 | ``` 316 | 317 | 参数 cmd 是包含了 shell 命令的一个字符串。返回的结果是一个对象,用起来就像是一个打开了的文件一样。 318 | 319 | 可以读取ls 进程的输出,用 readline 的话每次读取一行,用 read 的话就一次性全部读取: 320 | 321 | ```Python 322 | >>> res = fp.read() 323 | ``` 324 | 用完之后要关闭,这点也跟文件一样: 325 | 326 | ```Python 327 | >>> stat = fp.close() 328 | >>> print(stat) 329 | None 330 | ``` 331 | 返回值是 ls 这个进程的最终状态;None 的意思就是正常退出(没有错误)。 332 | 333 | 举个例子,大多数 Unix 系统都提供了一个计算 md5sum 的函数,会读取一个文件的内容,然后计算一个『checksum』(校验值)。你可以点击[这里](http://en.wikipedia.org/wiki/Md5)阅读更多相关内容。 334 | 335 | 这个命令可以很有效地检查两个文件是否有相同内容。两个不同内容产生同样的校验值的可能性是很小的(实际上在宇宙坍塌之前都没戏)。 336 | 337 | 你就可以用一个 pipe 来从 Python 启动运行 md5sum,然后获取结果: 338 | 339 | ```Python 340 | >>> filename = 'book.tex' 341 | >>> cmd = 'md5sum ' + filename 342 | >>> fp = os.popen(cmd) 343 | >>> res = fp.read() 344 | >>> stat = fp.close() 345 | >>> print(res) 346 | 1e0033f0ed0656636de0d75144ba32e0 book.tex 347 | >>> print(stat) 348 | None 349 | ``` 350 | ## 14.9 编写模块 351 | 352 | 任何包含 Python 代码的文件都可以作为模块被导入使用。举个例子,假设你有一个名字叫 wc.py 的文件,里面代码如下: 353 | 354 | ```Python 355 | def linecount(filename): 356 | count = 0 357 | for line in open(filename): 358 | count += 1 359 | return count 360 | print(linecount('wc.py')) 361 | ``` 362 | 363 | 如果运行这个程序,程序就会读取自己本身,然后输出文件中的行数,也就是7行了。你还可以导入这个模块,如下所示: 364 | 365 | ```Python 366 | >>> import wc 367 | 7 368 | ``` 369 | 370 | 现在你就有一个模块对象 wc 了: 371 | 372 | ```Python 373 | >>> wc 374 | 375 | ``` 376 | 377 | 该模块提供了数行数的函数linecount: 378 | 379 | ```Python 380 | >>> wc.linecount('wc.py') 381 | 7 382 | ``` 383 | 384 | 你看,你就可以这样来为 Python 写模块了。 385 | 386 | 当然这个例子中有个小问题,就是导入模块的时候,模块内代码在最后一行对自身进行了测试。 387 | 388 | 一般情况你导入一个模块,模块只是定义了新的函数,但不会去主动运行自己内部的函数。 389 | 390 | 以模块方式导入使用的程序一般用下面这样的惯用形式: 391 | 392 | ```Python 393 | if __name__ == '__main__': 394 | print(linecount('wc.py')) 395 | ``` 396 | 397 | __name__ 是一个内置变量,当程序开始运行的时候被设置。如果程序是作为脚本来运行的,__name__ 的值就是'__main__';这样的话,if条件满足,测试代码就会运行。而如果该代码被用作模块导入了,if 条件不满足,测试的代码就不会运行了。 398 | 399 | 做个联系吧,把上面的例子输入到一个名为 wc.py 的文件中,然后作为脚本运行。然后再运行 Python 解释器,然后导入 wc 作为模块。看看作为模块导入的时候__name__ 的值是什么? 400 | 401 | 警告:如果你导入了一个已经导入过的模块,Python 是不会有任何提示的。Python 并不会重新读取模块文件,即便该文件又被修改过也是如此。 402 | 403 | 所以如果你想要重新加载一个模块,你可以用内置函数 reload,但这个也不太靠谱,所以最靠谱的办法莫过于重启解释器,然后再次导入该模块。 404 | 405 | ## 14.10 调试 406 | 407 | 读写文件的时候,你可能会碰到空格导致的问题。这些问题很难解决,因为空格、跳表以及换行,平常就难以用眼睛看出来: 408 | 409 | ```Python 410 | >>> s = '1 2\t 3\n 4' 411 | >>> print(s) 412 | 1 2 3 413 | 4 414 | ``` 415 | 这时候就可以用内置函数 repr 来帮忙。它接收任意对象作为参数,然后返回一个该对象的字符串表示。对于字符串,该函数可以把空格字符转成反斜杠序列: 416 | 417 | ```Python 418 | >>> print(repr(s)) 419 | '1 2\t 3\n 4' 420 | ``` 421 | 该函数的功能对调试来说很有帮助。 422 | 423 | 另外一个问题就是不同操作系统可能用不同字符表示行尾。 424 | 425 | 有的用一个换行符,也就是\n。有的用一个返回字符,也就是\r。有的两个都亏。如果你把文件在不同操作系统只见移动,这种不兼容性就可能导致问题了。 426 | 427 | 对大多数操作系统,都有一些应用软件来进行格式转换。你可以在[这里](http://en.wikipedia.org/wiki/Newline)查找一下(并且阅读关于该问题的更多细节)。当然,你也可以自己写一个转换工具了。 428 | 429 | (译者注:译者这里也鼓励大家,一般的小工具,自己有时间有精力的话完全可以尝试着自己写一写,对自己是个磨练,也有利于对语言进行进一步的熟悉。这里再推荐一本书:Automate the Boring Stuff with,作者是 Al Sweigart。该书里面提到了很多常用的任务用 Python 来实现。) 430 | 431 | ## 14.11 Glossary 术语列表 432 | persistent: 433 | Pertaining to a program that runs indefinitely and keeps at least some of its data in permanent storage. 434 | 435 | >持久性:指一个程序可以随时运行,然后可以存储一部分数据到永久介质中。 436 | 437 | format operator: 438 | An operator, %, that takes a format string and a tuple and generates a string that includes the elements of the tuple formatted as specified by the format string. 439 | 440 | >格式运算符:%运算符,处理字符串和元组,然后生成一个包含元组中元素的字符串,根据给定的格式字符串进行格式化。 441 | 442 | format string: 443 | A string, used with the format operator, that contains format sequences. 444 | 445 | >格式字符串:用于格式运算符的一个字符串,内含格式序列。 446 | 447 | format sequence: 448 | A sequence of characters in a format string, like %d, that specifies how a value should be formatted. 449 | 450 | >格式序列:格式字符串内的一串字符,比如%d,规定了一个值如何进行格式化。 451 | 452 | text file: 453 | A sequence of characters stored in permanent storage like a hard drive. 454 | 455 | >文本文件:磁盘中永久存储的一个文件,内容为一系列的字符。 456 | 457 | directory: 458 | A named collection of files, also called a folder. 459 | 460 | >目录:有名字的文件集合,也叫做文件夹。 461 | 462 | path: 463 | A string that identifies a file. 464 | 465 | >路径:指向某个文件的字符串。 466 | 467 | relative path: 468 | A path that starts from the current directory. 469 | 470 | >相对路径:从当前目录开始,到目标文件的路径。 471 | 472 | absolute path: 473 | A path that starts from the topmost directory in the file system. 474 | 475 | >绝对路径:从文件系统最底层的根目录开始,到目标文件的路径。 476 | 477 | catch: 478 | To prevent an exception from terminating a program using the try and except statements. 479 | 480 | >抛出异常:为了避免意外错误中止程序,使用 try 和 except 语句来处理异常。 481 | 482 | database: 483 | A file whose contents are organized like a dictionary with keys that correspond to values. 484 | 485 | >数据库:一个文件,全部内容以类似字典的方式来组织,为键与对应的键值。 486 | 487 | bytes object: 488 | An object similar to a string. 489 | 490 | >二进制对象:暂时就当作是根字符串差不多的对象就可以了。 491 | 492 | shell: 493 | A program that allows users to type commands and then executes them by starting other programs. 494 | 495 | > shell:一个程序,允许用户与操作系统进行交互,可以输入命令,然后启动一些其他程序来执行。 496 | 497 | pipe object: 498 | An object that represents a running program, allowing a Python program to run commands and read the results. 499 | 500 | >管道对象:代表了一个正在运行的程序的对象,允许一个 Python 程序运行命令并读取运行结果。 501 | 502 | ## 14.12 练习 503 | ## # 练习1 504 | 505 | 写一个函数,名为 sed,接收一个目标字符串,一个替换字符串,然后两个文件名;读取第一个文件,然后把内容写入到第二个文件中,如果第二个文件不存在,就创建一个。如果目标字符串在文件中出现了,就用替换字符串把它替换掉。 506 | 507 | 如果在打开、读取、写入或者关闭文件的时候发生了错误了,你的程序应该要捕获异常,然后输出错误信息,然后再退出。[样例代码](http://thinkpython2.com/code/sed.py)。 508 | 509 | ## # 练习2 510 | 如果你从 [这里](http://thinkpython2.com/code/anagram_sets.py)下载了我的样例代码,你会发现该程序创建了一个字典,建立了从一个有序字母字符串到一个单词列表的映射,列表中的单词可以由这些字母拼成。例如'opst'就映射到了列表 [’opts’, ’post’, ’pots’, ’spot’, ’stop’, ’tops’]. 511 | 512 | 写一个模块,导入 anagram_sets 然后提供两个函数:store_anagrams 可以把相同字母异序词词典存储到一个『shelf』;read_anagrams 可以查找一个词,返回一个由其 相同字母异序词 组成的列表。 513 | 514 | [样例代码](http://thinkpython2.com/code/anagram_db.py)。 515 | 516 | ## # 练习3 517 | 现在有很多 MP3文件的一个大集合里面,一定有很多同一首歌重复了,然后存在不同的目录或者保存的名字不同。本次练习的目的就是要找到这些重复的内容。 518 | 519 | 1. 首先写一个程序,搜索一个目录并且递归搜索所有子目录,然后返回一个全部给定后缀(比如.mp3)的文件的路径。提示:os.path 提供了一些函数,能用来处理文件和路径名称。 520 | 521 | 2. 要识别重复文件,要用到 md5sum 函数来对每一个文件计算一个『校验值』。如果两个文件校验值相同,那很可能就是有同样的内容了。 522 | 523 | 3. 为了保险起见,再用 Unix 的 diff 命令来检查一下。[样例代码](http://thinkpython2.com/code/find_duplicates.py)。 524 | 525 | ________________________________________ 526 | 备注1 527 | popen is deprecated now, which means we are supposed to stop using it and start using the subprocess module. But for simple cases, I find subprocess more complicated than necessary. So I am going to keep using popen until they take it away. 528 | 529 | >注意,popen 已经不被支持了,这就意味着咱们不应该再用它了,然后要用新的 subprocess 模块。不过为了让案例更简单明了,还是用了 popen,引起我发现 subprocess 过于复杂,而且也没太大必要。所以我就打算一直用着 popen,直到这个方法被废弃移除不能使用了再说了。 530 | -------------------------------------------------------------------------------- /chapter15.md: -------------------------------------------------------------------------------- 1 | # 第十五章 类和对象 2 | 3 | 到目前为止,你应该已经知道如何用函数来整理代码,以及用内置类型来组织数据了。接下来的一步就是要学习『面向对象编程』了,这种编程方法中,用户可以自定义类型来同时对代码和数据进行整理。面向对象编程是一个很大的题目;要有好几章才能讲出个大概。 4 | 5 | 6 | 本章的样例代码可以在[这里](http://thinkpython2.com/code/Point1.py)来下载,练习题对应的样例代码可以在[这里](http://thinkpython2.com/code/Point1_soln.py)下载。 7 | 8 | ## 15.1 用户自定义类型 9 | 10 | 我们已经用过很多 Python 的内置类型了;现在我们就要来定义一个新的类型了。作为样例,我们会创建一个叫 Point 的类,用于表示一个二维空间中的点。 11 | 12 | 13 | 数学符号上对点的表述一般是一个括号内有两个坐标,坐标用逗号分隔开。比如,(0,0)就表示为原点,(x,y)就表示了该点从原点向右偏移 x,向上偏移 y。 14 | 15 | 16 | 我们可以用好几种方法来在 Python 中表示一个点: 17 | 18 | • 我们可以把坐标存储成两个单独的值,x 和 y。 19 | 20 | • 还可以把坐标存储成列表或者元组的元素。 21 | 22 | • 还可以创建一个新的类型来用对象表示点。 23 | 24 | 25 | 创建新的类型要比其他方法更复杂一点,不过也有一些优势,等会我们就会发现了。 26 | 27 | 28 | 用户自定义的类型也被叫做一个类。一个类的定义大概是如下所示的样子: 29 | 30 | ```Python 31 | class Point: 32 | """Represents a point in 2-D space.""" 33 | ``` 34 | 35 | 头部代码的意思是表示新建的类名字叫 Point。然后类的体内有一个文档字符串,解释类的用途。在类的定义内部可以定义各种变量和方法,等会再来详细学习一下这些内容哈。 36 | 37 | 38 | 声明一个名为 Point 的类,就可以创建该类的一个对象。 39 | 40 | ```Python 41 | >>> Point 42 | 43 | ``` 44 | 因为 Point 是在顶层位置定义的,所以全名就是__main__.Point。 45 | 46 | 类的对象就像是一个创建对象的工厂。要创建一个 Point,就可以像调用函数一样调用 Point。 47 | 48 | ```Python 49 | >>> blank = Point() 50 | >>> blank 51 | <__main__.Point object at 0xb7e9d3ac> 52 | ``` 53 | 返回值是到一个 Point 对象的引用,刚刚赋值为空白了。 54 | 55 | 创建一个新的对象也叫做实例化,这个对象就是类的一个实例。 56 | 57 | 用 Print 输出一个实例的时候,Python 会告诉你该实例所属的类,以及在内存中存储的位置(前缀为0x 意味着下面的这些数值是十六进制的。) 58 | 59 | 每一个对象都是某一个类的一个实例,所以『对象』和『实例』可以互换来使用。不过本章我还是都使用『实例』这个词,这样就更能体现出咱们在谈论的是用户定义的类型。 60 | 61 | ## 15.2 属性 62 | 63 | 用点号可以给实例进行赋值: 64 | 65 | ```Python 66 | >>> blank.x = 3.0 67 | >>> blank.y = 4.0 68 | ``` 69 | 这一语法形式就和从模块中选取变量的语法是相似的,比如 math.pi 或者 string.whitespace。然而在本章这种情况下,我们用点号实现的是对一个对象中某些特定名称的元素进行赋值。这些元素也叫做属性。 70 | 71 | 『Attribute』作为名词的发音要把重音放在第一个音节,而做动词的时候是重音放到第二音节。 72 | 73 | 下面的图表展示了上面这些赋值的结果。用于展示一个类及其属性的状态图也叫做类图;比如图15.1就是一例。 74 | 75 | ________________________________________ 76 | ![Figure 15.1: Object diagram](./images/figure15.1.jpg) 77 | Figure 15.1: Object diagram. 78 | ________________________________________ 79 | 80 | 变量 blank 指代的是一个 Point 对象,该对象包含两个属性。每个属性都指代了一个浮点数。 81 | 82 | 83 | 读取属性值可以用如下这样的语法: 84 | 85 | ```Python 86 | >>> blank.y 87 | 4.0 88 | >>> x = blank.x 89 | >>> x 90 | 3.0 91 | ``` 92 | 这里的表达式 blank.x 的意思是,『到 blank 所指代的对象中,读取 x 的值。』在这个例子中,我们把这个值赋值给一个名为 x 的变量。这里的变量 x 和类的属性x 并不冲突。 93 | 94 | 点号可以随意在任意表达式中使用。比如下面这个例子: 95 | 96 | ```Python 97 | >>> '(%g, %g)' % (blank.x, blank.y) 98 | '(3.0, 4.0)' 99 | >>> distance = math.sqrt(blank.x**2 + blank.y**2) 100 | >>> distance 101 | 5.0 102 | ``` 103 | 104 | 你还可以把实例作为一个参数来使用。比如下面这样: 105 | 106 | ```Python 107 | def print_point(p): 108 | print('(%g, %g)' % (p.x, p.y)) 109 | ``` 110 | 111 | print_point 这个函数就接收了一个点作为参数,然后显示点的数值位置。你可以把刚刚那个 blank 作为参数传过去来试试: 112 | 113 | ```Python 114 | >>> print_point(blank) 115 | (3.0, 4.0) 116 | ``` 117 | 118 | 在函数内部,p 是blank 的一个别名,所以如果函数内部对 p 进行了修改,blank 也会发生相应的改变。 119 | 120 | 121 | 做个练习,写一个名为 distance_between_points 的函数,接收两个点作为参数,然后返回两点之间的距离。 122 | 123 | ## 15.3 矩形 124 | 125 | 有时候一个类中的属性应该如何设置是很明显的,不过有的时候就得好好考虑一下了。比如,假设你要设计一个表示矩形的类。你要用什么样的属性来确定一个矩形的位置和大小呢?可以忽略角度;来让情况更简单一些,就只考虑矩形是横向的或者纵向的。 126 | 127 | 至少有两种方案备选: 128 | 129 | • 确定矩形的一个顶点(或者中心)所在位置,还有宽度和高度。 130 | 131 | • 确定对角线上的两个顶点所在位置。 132 | 133 | 现在还很难说这两者哪一个更好,那么咱们先用第一个方案来做个例子。 134 | 135 | 下面就是类的定义: 136 | 137 | ```Python 138 | class Rectangle: 139 | """Represents a rectangle. 140 | attributes: width, height, corner. 141 | """ 142 | ``` 143 | 文档字符串中列出了属性:width 和 height 是数值;corner 是一个点对象,用来表示左下角顶点。 144 | 145 | 要表示一个矩形,必须初始化一个矩形对象,然后对其属性进行赋值: 146 | 147 | ```Python 148 | box = Rectangle() 149 | box.width = 100.0 150 | box.height = 200.0 151 | box.corner = Point() 152 | box.corner.x = 0.0 153 | box.corner.y = 0.0 154 | ``` 155 | 156 | 表达式 box.corner.x 的意思是,『到 box 指代的对象中,选择名为 corner 的属性;然后到这个点对象中,选取名为 x 的属性值。』 157 | 158 | ________________________________________ 159 | ![Figure 15.2: Object diagram](./images/figure15.2.jpg) 160 | Figure 15.2: Object diagram. 161 | ________________________________________ 162 | 163 | 图15.2展示了这个对象的状态图。一个类去作为另外一个类的属性,就叫做嵌入。 164 | 165 | ## 15.4 多个实例作返回值 166 | 167 | 函数返回实例。比如 find_center 就接收一个 Rectangle (矩阵)对象作为参数,然后以一个Point(点)对象的形式返回矩形中心位置的坐标所在点: 168 | 169 | ```Python 170 | def find_center(rect): 171 | p = Point() 172 | p.x = rect.corner.x + rect.width/2 173 | p.y = rect.corner.y + rect.height/2 174 | return p 175 | ``` 176 | 177 | 下面这个例子中,box 作为一个参数传递给了 find_center 函数,然后结果赋值给了点 center: 178 | 179 | ```Python 180 | >>> center = find_center(box) 181 | >>> print_point(center) 182 | (50, 100) 183 | ``` 184 | ## 15.5 对象可以修改 185 | 通过对一个对象的属性进行赋值就可以修改该对象的状态了。比如,要改变一个举行的大小而不改变位置,就可以只修改宽度和高度,如下所示: 186 | 187 | ```Python 188 | box.width = box.width + 50 189 | box.height = box.height + 100 190 | ``` 191 | 192 | 你还可以写专门的函数来修改对象。比如grow_rectangle这个函数就接收一个矩形对象和dwidth 与 dheight两个数值,然后把这两个数值加到矩形的宽度和高度值上。 193 | 194 | ```Python 195 | def grow_rectangle(rect, dwidth, dheight): 196 | rect.width += dwidth 197 | rect.height += dheight 198 | ``` 199 | 200 | 下面的例子展示了具体的效果: 201 | 202 | ```Python 203 | >>> box.width, box.height 204 | (150.0, 300.0) 205 | >>> grow_rectangle(box, 50, 100) 206 | >>> box.width, box.height 207 | (200.0, 400.0) 208 | ``` 209 | 210 | 在函数的内部,rect 是 box 的一个别名,所以当函数修改了 rect 的时候,box 就得到了相应的修改。 211 | 212 | 做个练习,写一个名为 move_rectangle 的函数,接收一个矩形和dx 与 dy 两个数值。函数要改变矩形所在位置,具体的改变方法为对左下角顶点坐标的 x 和 y 分别加上 dx 和 dy 的值。 213 | 214 | ## 15.6 复制 215 | 216 | 别名有可能让程序读起来有困难,因为在一个位置做出的修改有可能导致另外一个位置发生不可预知的情况。这样也很难去追踪指向一个对象的所有变量。 217 | 218 | 所以就可以不用别名,而用复制对象的方法。copy 模块包含了一个名叫 copy 的函数,可以复制任意对象: 219 | 220 | ```Python 221 | >>> p1 = Point() 222 | >>> p1.x = 3.0 223 | >>> p1.y = 4.0 224 | >>> import copy 225 | >>> p2 = copy.copy(p1) 226 | ``` 227 | 228 | p1和 p2包含的数据是相同的,但并不是同一个点对象。 229 | 230 | ```Python 231 | >>> print_point(p1) 232 | (3, 4) 233 | >>> print_point(p2) 234 | (3, 4) 235 | >>> p1 is p2 236 | False 237 | >>> p1 == p2 238 | False 239 | ``` 240 | is 运算符表明 p1和 p2不是同一个对象,这就是我们所预料的。但你可能本想着是==运算符应该得到的是 True 因为这两个点包含的数据是一样的。这样的话你就会很失望地发现对于实例来说,==运算符的默认行为就跟 is 运算符是一样的;它也还是检查对象的身份,而不是对象的相等性。这是因为你用的是用户自定义的类型,Python 不值得如何去衡量是否相等。至少是现在还不能。 241 | 242 | (译者注:==运算符的实现需要运算符重载,也就是多态的一种,来实现,也就是对用户自定义类型,需要用户自定义运算符,而不能简单地继续用内置运算符。因为自定义类型的运算是 Python 没法确定的,得用户自己来确定。) 243 | 244 | 如果你用 copy.copy 复制了一个矩形,你会发现该函数复制了矩形对象,但没有复制内嵌的点对象。 245 | 246 | ```Python 247 | >>> box2 = copy.copy(box) 248 | >>> box2 is box 249 | False 250 | >>> box2.corner is box.corner 251 | True 252 | ``` 253 | ________________________________________ 254 | ![Figure 15.3: Object diagram](./images/figure15.3.jpg) 255 | Figure 15.3: Object diagram. 256 | ________________________________________ 257 | 图15.3展示了此时的类图的情况。这种运算叫做浅复制,因为复制了对象与对象内包含的所有引用,但不复制内嵌的对象。 258 | 259 | 对于大多数应用来说,这并不是你的本来目的。在本节的样例中,对复制过的一个矩形进行 grow_rrectangle 函数运算,并不会影响另外一个,但使用 move_rectangle 就会对两个都有影响!这种行为就很让人疑惑,也容易带来错误。 260 | 261 | 所幸的是 copy 模块还提供了一个名为 deepcopy (深复制)的方法,这样就能把内嵌的对象也复制了。你肯定不会奇怪了,这种运算就叫深复制了。 262 | 263 | ```Python 264 | >>> box3 = copy.deepcopy(box) 265 | >>> box3 is box 266 | False 267 | >>> box3.corner is box.corner 268 | False 269 | ``` 270 | box3和 box 就是完全隔绝开,没有公用内嵌对象,彻底不会相互干扰的两个对象了。 271 | 272 | 做个练习吧,写一个新版本的 move_rectangle,创建并返回一个新的矩形,而不是修改旧有的矩形。 273 | 274 | ## 15.7 调试 275 | 当你开始使用对象的时候,你就容易遇到一些新的异常。如果你试图读取一个不存在的属性,就会得到一个属性错误AttributeError: 276 | 277 | ```Python 278 | >>> p = Point() 279 | >>> p.x = 3 280 | >>> p.y = 4 281 | >>> p.z 282 | AttributeError: Point instance has no attribute 'z' 283 | ``` 284 | 285 | 如果不确定一个对象是什么类型,可以『问』一下: 286 | 287 | ```Python 288 | >>> type(p) 289 | 290 | ``` 291 | 还可以用 isinstance 函数来检查一下一个对象是否为某一个类的实例: 292 | 293 | ```Python 294 | >>> isinstance(p, Point) 295 | True 296 | ``` 297 | 如果不确定某一对象是否有一个特定的属性,可以用内置函数 hasattr: 298 | 299 | ```Python 300 | >>> hasattr(p, 'x') 301 | True 302 | >>> hasattr(p, 'z') 303 | False 304 | ``` 305 | hasattr 的第一个参数可以是任意一个对象;第二个参数是一个字符串,就是要判断是否存在的属性名字。 306 | 307 | 用 try 语句也可以试验一个对象是否有你需要的属性: 308 | 309 | ```Python 310 | try: 311 | x = p.x 312 | except AttributeError: 313 | x = 0 314 | ``` 315 | 这样写一些处理不同类型变量的函数就更容易了;关于这一话题的更多内容会在17.9中展开。 316 | 317 | ## 15.8 Glossary 术语列表 318 | class: 319 | A programmer-defined type. A class definition creates a new class object. 320 | 321 | >类:用户定义的类型。一个类的声明建立了一个新的类的对象。 322 | 323 | class object: 324 | An object that contains information about a programmer-defined type. The class object can be used to create instances of the type. 325 | 326 | >类的对象:包含了用户自定义类型相关信息的一个对象。可以用于创建类的一个实例。 327 | 328 | instance: 329 | An object that belongs to a class. 330 | 331 | >实例:术语某一个类的一个对象。 332 | 333 | instantiate: 334 | To create a new object. 335 | 336 | >实例化:创建一个新的对象。 337 | 338 | attribute: 339 | One of the named values associated with an object. 340 | 341 | >属性:一个对象内附属的数值的名字。 342 | 343 | embedded object: 344 | An object that is stored as an attribute of another object. 345 | 346 | >内嵌对象:一个对象作为属性存储在另一个对象内。 347 | 348 | shallow copy: 349 | To copy the contents of an object, including any references to embedded objects; implemented by the copy function in the copy module. 350 | 351 | >浅复制:复制一个对象中除了内嵌对象之外的所有引用;通过 copy 模块的 copy 函数来实现。 352 | 353 | deep copy: 354 | To copy the contents of an object as well as any embedded objects, and any objects embedded in them, and so on; implemented by the deepcopy function in the copy module. 355 | 356 | >深复制:复制一个对象的所有内容,包括内嵌对象,以及内嵌对象中的所有内嵌对象等等;通过 copy 模块的 deepcopy 函数来实现。 357 | 358 | object diagram: 359 | A diagram that shows objects, their attributes, and the values of the attributes. 360 | 361 | >类图:一种图解,用于展示类与类中的属性以及属性的值。 362 | 363 | ## 15.9 练习 364 | ## # 练习1 365 | 写一个名为 Circle 的类的定义,属性为圆心center和半径radius,center 是一个点对象,半径是一个数值。 366 | 367 | 实例化一个 Circle 的对象,表示一个圆,圆心在(150,100),半径为75。 368 | 369 | 写一个名为 point_in_circle 的函数,接收一个 Circle 和一个 Point 对象作为参数,如果点在圆内或者圆的线上就返回True。 370 | 371 | 写一个名为 rect_in_circle 的函数,接收一个 Circle 和一个 Rectangle 对象作为参数,如果矩形的边都内含或者内切在圆内,就返回 True。 372 | 373 | 写一个名为 rect_circle_overlap 的函数,接收一个 Circle 和一个 Rectangle 对象作为参数,如果矩形任意一个顶点在圆内就返回 True。或者写个更有挑战性的版本,如果矩形有任意部分包含在圆内就返回 True。 374 | 375 | [样例代码](http://thinkpython2.com/code/Circle.py)。 376 | 377 | ## # 练习2 378 | 写一个名为 draw_rect的函数,接收一个 Turtle 对象和一个 Rectangle 对象作为参数,用 Turtle 画出这个矩形。可以参考一下第四章对 Turtle 对象使用的样例。 379 | 380 | 写一个名为 draw_circle 的函数,接收一个 Turtle 对象和一个 Circle 对象,画个圆这次。 381 | 382 | [样例代码](http://thinkpython2.com/code/draw.py)。 383 | -------------------------------------------------------------------------------- /chapter16.md: -------------------------------------------------------------------------------- 1 | # 第十六章 类和函数 2 | 3 | 现在我们已经知道如何创建新类型了,下一步就要写一些函数了,这些函数用自定义类型做参数和返回值。在本章中还提供了一种函数式编程的模式,以及两种新的程序开发规划方式。 4 | 5 | 本章的样例代码可以在[这里](http://thinkpython2.com/code/Time1.py)下载。然后练习题的样例代码可以在[这里](http://thinkpython2.com/code/Time1_soln.py)下载到。 6 | 7 | ## 16.1 时间 8 | 下面又是一个自定义类型的例子,这次咱们定义一个叫做 Time 的类,记录下当日的时间。 9 | 10 | 类的定义是如下这样: 11 | 12 | ```Python 13 | class Time: 14 | """Represents the time of day. 15 | attributes: hour, minute, second """ 16 | ``` 17 | 我们可以建立一个新的 Time 对象,然后对时分秒分别进行赋值: 18 | 19 | ```Python 20 | time = Time() 21 | time.hour = 11 22 | time.minute = 59 23 | time.second = 30 24 | ``` 25 | 26 | 这个 Time 对象的状态图如图16.1所示。 27 | 28 | 29 | 下面做个练习,写一个名为print_time 的函数,接收一个 Time 对象,然后以时:分:秒的格式来输出。提示:格式序列'%.2d'就会用两位来输出一个整数,第一位可以为0。 30 | 31 | 32 | 写一个布尔函数,名为 is_after,接收两个 Time 对象,分别为 t1和 t2,然后如果 t1在时间上位于 t2的后面,就返回真,否则返回假。难度提高一下:不要用 if 语句,看你能搞定不。 33 | 34 | ________________________________________ 35 | ![Figure 16.1: Object diagram.](./images/figure16.1.jpg) 36 | Figure 16.1: Object diagram. 37 | ________________________________________ 38 | ## 16.2 纯函数 39 | 40 | 后面的这些章节中,我们要写两个函数来对 time 进行加法操作。这两个函数展示了两种函数类型:纯函数和修改器。写这两个函数的过程中,也体现了我即将讲到的一种新的开发模式:原型和补丁模式,这种方法就是在处理复杂问题的时候,先从简单的原型开始,然后逐渐解决复杂的内容。 41 | 42 | 43 | 下面这段代码就是 add_time 函数的一个原型: 44 | 45 | ```Python 46 | def add_time(t1, t2): 47 | sum = Time() 48 | sum.hour = t1.hour + t2.hour 49 | sum.minute = t1.minute + t2.minute 50 | sum.second = t1.second + t2.second 51 | return sum 52 | ``` 53 | 54 | 这个函数新建了一个新的 Time 对象,初始化了所有的值,然后返回了一个对新对象的引用。这种函数叫纯函数,因为这种函数并不修改传来做参数的对象,也没有什么效果,比如显示值啊或者让用户输入啊等等,而只是返回一个值而已。 55 | 56 | 下面就来测试一下这个函数,我将建立两个 Time 对象,start 包含了一个电影的开始时间,比如《巨蟒与圣杯》(译者注:1975年喜剧电影。Python的创造者Guido van Rossum特别喜欢这个喜剧团体:巨蟒飞行马戏团(Monty Python’s Flying Circus ),所以命名为 Python。),然后 duration(汉译就是持续时间)包含了该电影的时长,《巨蟒与圣杯》这部电影是一小时三十五分钟。add_time 函数就会算出电影结束的时间。 57 | 58 | ```Python 59 | >>> start = Time() 60 | >>> start.hour = 9 61 | >>> start.minute = 45 62 | >>> start.second = 0 63 | >>> duration = Time() 64 | >>> duration.hour = 1 65 | >>> duration.minute = 35 66 | >>> duration.second = 0 67 | >>> done = add_time(start, duration) 68 | >>> print_time(done) 69 | 10:80:00 70 | ``` 71 | 72 | 很明显,10点80分00秒这样的时间肯定不是你想要的结果。问题就出在了函数不值得如何应对时分秒的六十位进位,所以超过60的时候没进位就这样了。所以我们得把超出六十秒的进位到分,超过六十分的进位到小时。 73 | 74 | 75 | 76 | 下面这个是改进版本: 77 | 78 | ```Python 79 | def add_time(t1, t2): 80 | sum = Time() 81 | sum.hour = t1.hour + t2.hour 82 | sum.minute = t1.minute + t2.minute 83 | sum.second = t1.second + t2.second 84 | if sum.second >= 60: 85 | sum.second -= 60 86 | sum.minute += 1 87 | if sum.minute >= 60: 88 | sum.minute -= 60 89 | sum.hour += 1 90 | return sum 91 | ``` 92 | 93 | 这回函数正确工作了,但代码也开始变多了。稍后我们就能看到一个短一些的替代方案。 94 | 95 | ## 16.3 修改器 96 | 有时候需要对作为参数的对象进行一些修改。这时候这些修改就可以被调用者察觉。这样工作的函数就叫修改器了。 97 | 98 | increment 函数,增加给定的秒数到一个 Time 对象,就可以被改写成一个修改器。 99 | 100 | 下面是个简单的版本: 101 | 102 | ```Python 103 | def increment(time, seconds): 104 | time.second += seconds 105 | if time.second >= 60: 106 | time.second -= 60 107 | time.minute += 1 108 | if time.minute >= 60: 109 | time.minute -= 60 110 | time.hour += 1 111 | ``` 112 | 113 | 第一行代码进行了最简单的运算;后面的代码是用来应对我们之前讨论过的特例情况。 114 | 115 | 116 | 那么这个函数正确么?秒数超过六十会怎么样? 117 | 118 | 119 | 很明显,秒数超过六十的时候,就需要运行不只一次了;必须一直运行,之道 time.second 的值小于六十了才行。有一种办法就是把 if 语句换成 while 语句。这样就可以解决这个问题了,但效率不太高。 120 | 121 | 做个练习,写一个正确的 increment 函数,并且要不包含任何循环。 122 | 123 | 124 | 能用修改器实现的功能也都能用纯函数来实现。实际上有的编程语言只允许纯函数。有证据表明,与修改器相比,使用修改器能够更快地开发,而且不容易出错误。但修改器往往很方便好用,而函数式的程序一般效率要差一些。 125 | 126 | 127 | 总的来说,我还是建议你写纯函数,除非用修改器有特别显著的好处。这种模式也叫做函数式编程。 128 | 129 | 130 | 做个练习,写一个用纯函数实现的 increment,创建并返回一个新的 Time 对象,而不是修改参数。 131 | 132 | ## 16.4 原型与规划 133 | 134 | 这次我演示的开发规划就是『原型与补丁模式』。对每个函数,我都先谢了一个简单的原型,只进行基本的运算,然后测试一下,接下来逐步修补错误。 135 | 136 | 这种模式很有效率,尤其是在你对问题的理解不是很深入的时候。不过渐进式的修改也会产生过分复杂的代码——因为要应对很多特例情况,而且也不太靠靠——因为不好确定你是否找到了所有的错误。 137 | 138 | 另一种模式就是设计规划开发,这种情况下对问题的深入透彻的理解就让开发容易很多了。本节中的根本性认识就在于 TIme 对象实际上是一个三位的六十进制数字(参考 [这里的解释](http://en.wikipedia.org/wiki/Sexagesimal)。)!秒数也就是个位,分数也就是六十位,小时数就是三千六百位。 139 | 140 | 这样当我们写 add_time 和 increment 函数的时候,用60进制来进行计算就很有效率。 141 | 142 | 这一观察表明有另外一种方法来解决整个问题——我们可以把 Time 对象转换成整数,然后因为计算机最擅长整数运算,这样就有优势了。 143 | 144 | 下面这个函数就把 Times 转换成了整数: 145 | 146 | ```Python 147 | def time_to_int(time): 148 | minutes = time.hour * 60 + time.minute 149 | seconds = minutes * 60 + time.second 150 | return seconds 151 | ``` 152 | 然后下面这个函数是反过来的,把整数转换成 Time(还记得 divmod 么,使用第一个数除以第二个数,返回的是除数和余数组成的元组。) 153 | 154 | ```Python 155 | def int_to_time(seconds): 156 | time = Time() 157 | minutes, time.second = divmod(seconds, 60) 158 | time.hour, time.minute = divmod(minutes, 60) 159 | return time 160 | ``` 161 | 你最好先考虑好了,然后多进行几次测试运行,然后要确保这些函数都是正确的。比如你就可以试着用很多个 x 的值来运算time_to_int(int_to_time(x)) == x。这也是连贯性检测的一个例子。 162 | 163 | 一旦你确定这些函数都没问题,就可以用它们来重写一下 add_time 这个函数了: 164 | 165 | ```Python 166 | def add_time(t1, t2): 167 | seconds = time_to_int(t1) + time_to_int(t2) 168 | return int_to_time(seconds) 169 | ``` 170 | 171 | 这个版本就比最开始那个版本短多了,也更容易去检验了。接下来就做个联系吧,用time_to_int 和 int_to_time 这两个函数来重写一下 increment。 172 | 173 | 174 | 在一定程度上,从六十进制到十进制的来回转换,远远比计算时间要麻烦的多。进制转换要更加抽象很多;我们处理时间计算的直觉要更好些。 175 | 176 | 177 | 然而,如果我们有足够的远见,把时间值当做六十进制的数值来对待,然后写出一些转换函数(比如 time_to_int 和 int_to_time),就能让程序变得更短,可读性更好,调试更容易,也更加可靠。 178 | 179 | 180 | 而且后续添加功能也更容易了。比如,假设要对两个时间对象进行相减来求二者之间的持续时间。简单版本的方法就是要用借位的减法。而使用转换函数的版本就更容易了,也更不容易出错。 181 | 182 | 183 | 有意思的事,有时候以困难模式来写一个程序(比如用更加泛化的模式),反而能让开发更简单(因为这样就减少了特例情况,也减少了出错误的概率了。) 184 | 185 | ## 16.5 调试 186 | 187 | 对于一个 Time 对象来说,只要分和秒的值在0-60的前闭后开区间(即可以为0但不可以为60),并且小时数为正数,就是格式正确的。小时和分钟都应该是整数,但秒是可以为小数的。 188 | 189 | 像这些要求也叫约束条件,因为通常都得满足这些条件才行。反过来说,如果这些条件没满足,就有可能是程序中某处存在错误了。 190 | 191 | 写一些检测约束条件的代码,能够帮助找出这些错误,并且找到导致错误的原因。例如,你亏写一个名字未 calid_time 的函数,接收一个 Time 对象,然后如果该对象不满足约束条件就返回 False: 192 | 193 | ```Python 194 | def valid_time(time): 195 | if time.hour < 0 or time.minute < 0 or time.second < 0: 196 | return False 197 | if time.minute >= 60 or time.second >= 60: 198 | return False 199 | return True 200 | ``` 201 | 202 | 然后在每个自定义函数的开头部位,你就可以检测一下参数,来确保这些参数没有错误: 203 | 204 | ```Python 205 | def add_time(t1, t2): 206 | if not valid_time(t1) or not valid_time(t2): 207 | raise ValueError('invalid Time object in add_time') 208 | seconds = time_to_int(t1) + time_to_int(t2) 209 | return int_to_time(seconds) 210 | ``` 211 | 212 | 或者你也可以用一个 assert 语句,这个语句也是检测给定的约束条件的,如果出现错误就会抛出一个异常: 213 | 214 | ```Python 215 | def add_time(t1, t2): 216 | assert valid_time(t1) and valid_time(t2) 217 | seconds = time_to_int(t1) + time_to_int(t2) 218 | return int_to_time(seconds) 219 | ``` 220 | 221 | assert 语句是很有用的,可以用来区分条件语句的用途,将 assert 这种用于检查错误的语句与常规的条件语句在代码上进行区分。 222 | 223 | ## 16.6 Glossary 术语列表 224 | prototype and patch: 225 | A development plan that involves writing a rough draft of a program, testing, and correcting errors as they are found. 226 | 227 | >原型和补丁模式:一种开发模式,先写一个程序的草稿,然后测试,再改正发现的错误,这样逐步演化的开发模式。 228 | 229 | designed development: 230 | A development plan that involves high-level insight into the problem and more planning than incremental development or prototype development. 231 | 232 | >设计规划开发:这种开发模式要求对所面对问题的高程度的深刻理解,相比渐进式开发和原型增补模式要更具有计划性。 233 | 234 | pure function: 235 | A function that does not modify any of the objects it receives as arguments. Most pure functions are fruitful. 236 | 237 | >纯函数:不修改参数对象的函数。这种函数多数是有返回值的函数。 238 | 239 | modifier: 240 | A function that changes one or more of the objects it receives as arguments. Most modifiers are void; that is, they return None. 241 | 242 | >修改器:修改参数对象的函数。大多数这样的函数都是无返回值的,也就是返回的都是 None。 243 | 244 | functional programming style: 245 | A style of program design in which the majority of functions are pure. 246 | 247 | >函数式编程模式:一种程序设计模式,主要特征为大多数函数都是纯函数。 248 | 249 | invariant: 250 | A condition that should always be true during the execution of a program. 251 | 252 | >约束条件:在程序运行过程中,应该一直为真的条件。 253 | 254 | assert statement: 255 | A statement that check a condition and raises an exception if it fails. 256 | 257 | >assert 语句:一种检查错误的语句,检查一个条件,如果不满足就抛出异常。 258 | 259 | ## 16.7 练习 260 | 本章的例子可以在 [这里](http://thinkpython2.com/code/Time1.py)下载;练习题的答案可以在[这里](http://thinkpython2.com/code/Time1_soln.py)下载。 261 | 262 | ## # 练习1 263 | 写一个函数,名为mul_time,接收一个Time 对象和一个数值,返回一个二者相乘得到的新的Time 对象。 264 | 265 | 然后用 mul_time 这个函数写一个函数,接收一个 Time 对象,代表着一个比赛的结束时间,还有一个数值,代表比赛距离,然后返回一个表示了平均步调(单位距离花费的时间)的新的 Time 对象。 266 | 267 | ## # 练习2 268 | datetime 模块提供了一些 time 对象,和本章的 Time 对象很相似,但前者提供了更多的方法和运算符。读一读[这里的文档] [Here](http://docs.python.org/3/library/datetime.html)吧。 269 | 270 | 1. 用 datetime 模块来写一个函数,获取当前日期,然后输出今天是星期几。 271 | 272 | 2.写一个函数,要求输入生日,然后输出用户的年龄以及距离下一个生日的日、时、分、秒数。 273 | 274 | 3.有的两个人在不同日期出生,会在某一天,一个人的年龄是另外一个人年龄的两杯。这一天就叫做他们的双倍日。写一个函数,接收两个生日,然后计算双倍日。 275 | 276 | 4. 再来点有挑战性的,写一个更通用的版本,来计算一下一个人的年龄为另外一个人年龄 n 倍时候的日期。 277 | 278 | [样例代码](http://thinkpython2.com/code/double.py)。 279 | -------------------------------------------------------------------------------- /chapter17.md: -------------------------------------------------------------------------------- 1 | # 第十七章 类和方法 2 | 3 | 前两章我们已经用到了Python 的一些面向对象的特性了,但那写程序实际上并不算是真正面向对象的,因为它们并没能够表现出用户自定义类型与对这些类型进行运算的函数之间的关系。所以接下来的移步就是要把这些函数转换成方法,让这些关系更明确。 4 | 5 | 6 | 本章的样例代码可以在 [这里下载](http://thinkpython2.com/code/Time2.py),然后练习题的样例代码可以在[这里下载](http://thinkpython2.com/code/Point2_soln.py)。 7 | 8 | ## 17.1 面向对象的特性 9 | 10 | Python 是一种面向对象的编程语言,这就意味着它提供了一些支持面向对象编程的功能,有以下这些特点: 11 | 12 | • 程序包含类和方法的定义。 13 | 14 | • 大多数运算都以对象运算的形式来实现。 15 | 16 | • 对象往往代表着现实世界中的事物,方法则相对应地代表着现实世界中事物之间的相互作用。 17 | 18 | 例如,第16章中定义的 Time 类就代表了人们生活中计算一天时间的方法,然后当时咱们写的那些函数就对应着人们对时间的处理。同理,在第15章定义的 Point 和 Rectangle 类就对应着现实中的数学概念上的点和矩形。 19 | 20 | 到此为止,我们还没有用到 Python 提供的用于面向对象编程的高级功能。这些高级功能并不是严格意义上必须使用的;它们大多是提供了一些我们已经实现的功能的一种备选的语法形式。不过在很多情况下,这种备选的模式更加简洁,也能更加准确地表述程序的结构。 21 | 22 | 例如,在 Time1.py 里面,类的定义和后面的函数定义就没有啥明显的练习。测试一下就会发现,每一个后续的函数里面都至少用了一个 Time 对象作为参数。 23 | 24 | 这样的观察结果就表明可以使用方法;方法是某一特定的类所附带的函数。之前我们看到过字符串、列表、字典以及元组的一些方法。在本章,咱们将要给用户自定义类型写一些方法。 25 | 26 | 方法在语义上与函数是完全相同的,但在语法上有两点不同: 27 | 28 | • 方法要定义在一个类定义内部,这样能保证方法和类之间的关系明确。 29 | 30 | • 调用一个方法的语法与调用函数的语法不一样。 31 | 32 | 在接下来的章节中,我们就要把之前两章写过的一些函数改写成方法。这种转换是纯机械的;你就遵守一系列步骤就可以实现了。如果你对二者之间的相互转化很熟悉了,你就可以根据情况自己选择是用函数还是用方法。 33 | 34 | ## 17.2 输出对象 35 | 36 | 在16.1,我们定义过一个名为Time 的类,当时写过月名为 print_time 的函数: 37 | 38 | ```Python 39 | class Time: 40 | """Represents the time of day.""" 41 | def print_time(time): 42 | print('%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second)) 43 | ``` 44 | 45 | 要调用这个函数,就必须给传递过去一个TIme 对象作为参数: 46 | 47 | ```Python 48 | >>> start = Time() 49 | >>> start.hour = 9 50 | >>> start.minute = 45 51 | >>> start.second = 00 52 | >>> print_time(start) 53 | 09:45:00 54 | ``` 55 | 56 | 要让 print_time 成为一个方法,只需要把函数定义内容放到类定义里面去。一定要注意缩进的变化哈。 57 | 58 | ```Python 59 | class Time: 60 | def print_time(time): 61 | print('%.2d:%.2d:%.2d' % (time.hour, time.minute, time.second)) 62 | ``` 63 | 64 | 现在就有两种方法来调用 print_time 这个函数了。第一种就是用函数的语法(一般大家不这么用): 65 | 66 | ```Python 67 | >>> Time.print_time(start) 68 | 09:45:00 69 | ``` 70 | 71 | 上面这里用到了点号,Time 是类的名字,pritn_time 是方法的名字。start 就是传过去的一个参数了。 72 | 73 | 74 | 另外一种形式就是用方法的语法(这个形式更简洁很多): 75 | 76 | ```Python 77 | >>> start.print_time() 78 | 09:45:00 79 | ``` 80 | 81 | 在上面这里也用了点号,print_time 依然还是方法名字,然后 start 是调用方法所在的对象,也叫做主语。这里就如同句子中的主语一样,方法调用的主语就是方法的归属者。 82 | 83 | 84 | 在方法内部,主语被用作第一个参数,所以在上面的例子中中,start 就被赋值给了 time。 85 | 86 | 87 | 按照惯例,方法的第一个参数也叫做 self,所以刚刚的 print_time 函数可以以如下这种更通用的形式来写: 88 | 89 | ```Python 90 | class Time: 91 | def print_time(self): 92 | print('%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)) 93 | ``` 94 | The reason for this convention is an implicit metaphor: 95 | 96 | >这种改写还有更深层次的意义: 97 | 98 | • 函数调用的语法里面,print_time(start),就表明了函数是主动的。这句语句的意思就相当于说,『嘿,print_time 函数!给你这个对象,你来打印输出一下。』 99 | 100 | • 在面向对象的编程中,对象是主动的。方法的调用,比如 start.rint_time(),就像是说,『嘿,start,你打印输出一下你自己』 101 | 102 | 103 | 看上去这样改了之后客气了不少,实际上不止如此,还有更多用处,只是不太明显。目前我们看到过的例子里面,这样改写还没有造成什么区别。但是有的时候,从函数转为对象,能够让函数(或者方法)更加通用,也让程序更容易维护,还便于代码的重用。 104 | 105 | 106 | 做个练习吧,重写一下 time_to_int(参见16.4),把这个函数写成一个方法。你也可以试着把 int_to_time 也携程方法,不过这可能不太行得通,因为把这个函数改成方法的话,没有对象来调用方法。 107 | 108 | ## 17.3 另外一个例子 109 | 110 | 下面是 increment 函数(参见16.4)被改写成的方法: 111 | 112 | ```Python 113 | # inside class Time: 114 | def increment(self, seconds): 115 | seconds += self.time_to_int() 116 | return int_to_time(seconds) 117 | ``` 118 | 119 | 这一版本的前提是 time_to_int 已经被改写成方法了。另外也要注意到,这是一个纯函数,而不是修改器。 120 | 121 | 122 | >下面是调用 increment 的示范: 123 | 124 | ```Python 125 | >>> start.print_time() 126 | 09:45:00 127 | >>> end = start.increment(1337) 128 | >>> end.print_time() 129 | 10:07:17 130 | ``` 131 | 132 | 主语,start,用自己(self)赋值给第一个参数。然后参数,1337,赋值给了第二个参数,秒值seconds。 133 | 134 | 135 | 这种表述挺混乱,如果弄错了就更麻烦了。比如,如果你用两个参数调用了 increment 函数,你会得到如下的错误: 136 | 137 | ```Python 138 | >>> end = start.increment(1337, 460) 139 | TypeError: increment() takes 2 positional arguments but 3 were given 140 | ``` 141 | 142 | 这个错误信息刚开始看上去还挺不好理解,因为括号里面确实是只有两个参数。不过实际上主语也会把自己当做一个参数,所以总共实际上是有三个参数了。 143 | 另外,有一种参数叫位置参数,就是没有参数的名字;这种参数就和关键字参数不同了。下面这个函数调用中: 144 | 145 | ```Bash 146 | sketch(parrot, cage, dead=True) 147 | ``` 148 | 149 | parrot 和 cage 都是位置参数,dead 是关键字参数。 150 | 151 | ## 17.4 更复杂点的例子 152 | 重写 is_after(参见16.1),这就比较有难度了,因为这个函数接收两个 Time 对象作为参数。在这个情况下,一般就把第一个参数命名为 self,第二个命名为 other: 153 | 154 | ```Python 155 | # inside class Time: 156 | def is_after(self, other): 157 | return self.time_to_int() > other.time_to_int() 158 | ``` 159 | 要使用这个方法,就必须在一个对象后面调用,然后用另外一个对象作为参数: 160 | 161 | ```Python 162 | >>> end.is_after(start) 163 | True 164 | ``` 165 | 这里就体现出一种语法上的好处了,因为读起来基本就根英语是一样的嗯:『end is after start?』 166 | 167 | ## 17.5 init方法 168 | 169 | init 方法(就是对『initialization』的缩写,初始化的意思,这个方法相当于C++中的构造函数)是一种特殊的方法,在对象被实例化的时候被调用。这个方法的全名是__init__(两个下划线,然后是 init,然后还是两个下划线)。在 Time 类当中,init 方法示例如下: 170 | 171 | ```Python 172 | # inside class Time: 173 | def __init__(self, hour=0, minute=0, second=0): 174 | self.hour = hour 175 | self.minute = minute 176 | self.second = second 177 | ``` 178 | 179 | 一般情况下,init 方法里面的参数与属性变量的名字是相同的。下面这个语句 180 | 181 | ```Python 182 | self.hour = hour 183 | ``` 184 | 185 | 就存储了参数 hour 的值,赋给了属性变量hour本身。 186 | 187 | 这些参数都是可选的,所以如果你调用 Time 但不给任何参数,得到的就是默认值。 188 | 189 | ```Python 190 | >>> time = Time() 191 | >>> time.print_time() 192 | 00:00:00 193 | ``` 194 | 195 | 如果你提供一个参数,就先覆盖 hour 的值: 196 | 197 | ```Python 198 | >>> time = Time (9) 199 | >>> time.print_time() 200 | 09:00:00 201 | 202 | 提供两个参数,就先后覆盖了 hour 和 minute 的值。 203 | 204 | ```Python 205 | >>> time = Time(9, 45) 206 | >>> time.print_time() 207 | 09:45:00 208 | ``` 209 | 210 | 如果你给出三个参数,就依次覆盖掉所有三个默认值了。 211 | 212 | 213 | 做一个练习,写一个 Point 类的 init 方法,接收 x 和 y 作为可选参数,然后赋值给对应的属性。 214 | 215 | ## 17.6 __str__方法 216 | 217 | __str__ 是一种特殊的方法,就跟__init__差不多,str 方法是接收一个对象,返回一个代表该对象的字符串。 218 | 219 | 例如,下面就是Time 对象的一个 str 方法: 220 | 221 | ```Python 222 | # inside class Time: 223 | def __str__(self): 224 | return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second) 225 | ``` 226 | 这样当你用 print 打印输出一个对象的时候,Python 就会调用这个 str 方法: 227 | 228 | ```Python 229 | >>> time = Time(9, 45) 230 | >>> print(time) 09:45:00 231 | ``` 232 | 233 | 写一个新的类的时候,总要先写出来 init 方法,这样有利于简化对象的初始化,还要写个 str 方法,这个方法在调试的时候很有用。 234 | 做个练习,写一下 Point 这个类的 str 方法。创建一个 Point 对象,然后用 print 输出一下。 235 | 236 | ## 17.7 运算符重载 237 | 238 | 通过定义一些特定的方法,咱们就能针对自定义类型,让运算符有特定的作用。比如,如果你在 Time 类中定义了一个名字为__add__的方法,你就可以对 Time 对象使用『+』加号运算符。 239 | 240 | ```Python 241 | # inside class Time: 242 | def __add__(self, other): 243 | seconds = self.time_to_int() + other.time_to_int() 244 | return int_to_time(seconds) 245 | ``` 246 | 使用方法如下所示: 247 | 248 | ```Python 249 | >>> start = Time(9, 45) 250 | >>> duration = Time(1, 35) 251 | >>> print(start + duration) 252 | 11:20:00 253 | ``` 254 | 当你针对 Time 对象使用加号运算符的时候,Python 就会调用你刚刚自定义的 add 方法。当你用 print 输出结果的时候,Python 调用的是你自定义的 str 方法。所以实际上面这个简单的例子背后可不简单。 255 | 256 | 针对用户自定义类型,让运算符有相应的行为,这就叫做运算符重载。Python 当中每一个运算符都有一个对应的方法,比如__add__。更多内容可以看一下 [这里的文档](http://docs.python.org/3/reference/datamodel.html# specialnames)。 257 | 258 | 做个练习,给 Point 类写一个加法的方法。 259 | 260 | ## 17.8 根据对象类型进行运算 261 | 262 | 在前面的章节中,我们把两个 Time 对象进行了相加,但也许有时候需要把一个整数加到 Time 对象上面。下面这一个版本的__add__方法就能够实现检查类型,然后调用add_time 方法或者是 increment 方法: 263 | 264 | ```Python 265 | # inside class Time: 266 | def __add__(self, other): 267 | if isinstance(other, Time): 268 | return self.add_time(other) 269 | else: 270 | return self.increment(other) 271 | def add_time(self, other): 272 | seconds = self.time_to_int() + other.time_to_int() 273 | return int_to_time(seconds) 274 | def increment(self, seconds): 275 | seconds += self.time_to_int() 276 | return int_to_time(seconds) 277 | ``` 278 | 279 | 280 | 内置函数isinstance 接收一个值和一个类的对象,如果该值是这个类的一个实例,就会返回真。 281 | 282 | 283 | 如果拿来相加的是一个 Time 对象,__add__就会调用 add_time 方法。其他情况下,程序会把参数当做一个数字,然后就调用 increment 方法。这种运算就是根据对象进行的,因为在针对不同类型参数的时候,运算符会进行不同的计算。 284 | 285 | 286 | 下面的例子中,就展示了用不同类型变量来相加的效果: 287 | 288 | ```Python 289 | >>> start = Time(9, 45) 290 | >>> duration = Time(1, 35) 291 | >>> print(start + duration) 292 | 11:20:00 293 | >>> print(start + 1337) 294 | 10:07:17 295 | ``` 296 | 297 | 然而不幸的是,这个加法运算不满足交换率。如果整数放到首位,就会得到如下所示的错误了: 298 | 299 | ```Python 300 | >>> print(1337 + start) 301 | TypeError: unsupported operand type(s) for +: 'int' and 'instance' 302 | ``` 303 | 304 | 这里的问题就在于,Python 并没有让一个 Time 对象来加一个整数,而是去调用了整形的加法去把一个 Time 对象加到整数上面去,这就用系统原本的加法,而这个加法不能处理 Time 对象。有一个很聪明的方法来解决这个问题:用一个特殊的方法__radd__,这个方法的意思就是『右加』。在一个 Time 对象出现在加号运算符右侧的时候,该方法就会被调用了。下面就是这个方法的定义: 305 | 306 | ```Python 307 | # inside class Time: 308 | def __radd__(self, other): 309 | return self.__add__(other) 310 | ``` 311 | And here’s how it’s used: 312 | 313 | >使用如下所示: 314 | 315 | ```Python 316 | >>> print(1337 + start) 317 | 10:07:17 318 | ``` 319 | 320 | 做个练习,为 Point 类来写一个加法的方法,要求能处理Point 对象或者一个元组: 321 | 322 | • 如果第二个运算数是一个Point,该方法就应该返回一个新的Point,新点的横纵坐标分别为两个点坐标相加。 323 | 324 | • 如果第二个运算数是一个元组,该方法就要把元组中第一个元素加到横坐标上,把第二个元素加到纵坐标上面,然后用计算出来的坐标返回一个新的点。 325 | 326 | ## 17.9 多态 327 | 328 | 在必要的时候,根据类型运算还是很有用的,不过(还好)并不总需要这么做。一般你都可以把函数写成能处理不同类型参数的,这样就不用这么麻烦了。 329 | 330 | 331 | 我们之前为字符串写的很多函数,也都可以用到其他序列类型上面。比如在11.2我们用 histogram 来统计一个单词中每个字母出现的次数。 332 | 333 | ```Python 334 | def histogram(s): 335 | d = dict() 336 | for c in s: 337 | if c not in d: 338 | d[c] = 1 339 | else: 340 | d[c] = d[c]+1 341 | return d 342 | ``` 343 | 344 | 这个函数也可以用于列表、元组,甚至字典,只要s 的元素是散列的,就能用做 d 当中的键。 345 | 346 | ```Python 347 | >>> t = ['spam', 'egg', 'spam', 'spam', 'bacon', 'spam'] 348 | >>> histogram(t) 349 | {'bacon': 1, 'egg': 1, 'spam': 4} 350 | ``` 351 | 352 | 针对不同类型都可以运行的函数,就是多态的了。多态能够有利于代码复用。比如内置的函数 sum,是用来把一个序列中所有的元素加起来,就可以适用于所有能够相加的序列元素。 353 | 354 | 355 | Time 对象有了自己的加法方法,就可以与 sum 函数来配合使用了: 356 | 357 | ```Python 358 | >>> t1 = Time(7, 43) 359 | >>> t2 = Time(7, 41) 360 | >>> t3 = Time(7, 37) 361 | >>> total = sum([t1, t2, t3]) 362 | >>> print(total) 363 | 23:01:00 364 | ``` 365 | 总的来说,如果一个函数内的所有运算都可以处理某一类型,这个函数就适用于这一类型了。 366 | 367 | 最好就是无心插柳柳成荫的这种多态,这种情况下你会发现某个之前写过的函数可以用来处理一个之前没有计划到的类型。 368 | 369 | ## 17.10 调试 370 | 371 | 在程序运行的任意时刻都可以给对象增加属性,不过如果你有多个同类对象却又不具有相同的属性,就容易出错了。所以最好在对象实例化的时候就全部用 init 方法初始化对象的全部属性。 372 | 373 | 374 | 如果你不确定一个对象是否有某个特定的属性,你可以用内置的 hasattr 函数来尝试一下(参考15.7)。 375 | 376 | 377 | 另外一种读取属性的方法是用内置函数 vars,这个函数会接收一个对象,然后返回一个字典,字典中的键值对就是属性名的字符串与对应的值。 378 | 379 | ```Python 380 | >>> p = Point(3, 4) 381 | >>> vars(p) 382 | {'y': 4, 'x': 3} 383 | ``` 384 | 出于调试目的,你估计也会发现下面这个函数随时用一下会带来很多便利: 385 | 386 | ```Python 387 | def print_attributes(obj): 388 | for attr in vars(obj): 389 | print(attr, getattr(obj, attr)) 390 | ``` 391 | 内置函数 getattr 会接收一个对象和一个属性名字(以字符串形式),然后返回该属性的值。 392 | 393 | ## 17.11 接口和实现 394 | 395 | 面向对象编程设计的目的之一就是让软件更容易维护,这就意味着当系统中其他部分发生改变的时候依然能让程序运行,然后可以修改程序去符合新的需求。 396 | 397 | 398 | 实现这一目标的程序设计原则就是要让接口和实现分开。对于对象来说,这就意味着一个类包含的方法要不能被属性表达方式的变化所影响。 399 | 400 | 401 | 比如,在本章我们建立了一个表示一天中时间的类。该类提供的方法包括 time_to_int, is_after, 和 add_time。 402 | 403 | 404 | 我们可以用几种不同方式来实现这些方法。这些实现的细节依赖于我们如何去表示时间。在本章,一个 Time 对象的属性为时分秒三个变量。 405 | 406 | 407 | 还有一种替代方案,我们就可以把这些属性替换为一个单个的整形变量,表示从午夜零点到当前时间的秒的数目。这种实现方法可以让一些方法更简单,比如 is_after,但也让其他方法更难写了。 408 | 409 | 410 | 当你创建一个新的类之后,你可能会发现有更好的实现方式。如果一个程序的其他部位在用你的类,这时候再来改造接口可能就要消耗很多时间,也容易遇到很多错误了。 411 | 412 | 413 | 但如果你仔细地设计好接口,你在改变实现的时候就不用去折腾了,这就意味着你程序的其他部位都不需要改动了。 414 | 415 | ## 17.12 Glossary 术语列表 416 | object-oriented language: 417 | A language that provides features, such as programmer-defined types and methods, that facilitate object-oriented programming. 418 | 419 | >面向对象的编程语言:提供面向对象功能的语言,比如用户自定义类型和方法,有利于实现面向对象编程。 420 | 421 | object-oriented programming: 422 | A style of programming in which data and the operations that manipulate it are organized into classes and methods. 423 | 424 | >面向对象编程:一种编程模式,数据和运算都被封装进类和方法之中。 425 | 426 | method: 427 | A function that is defined inside a class definition and is invoked on instances of that class. 428 | 429 | >方法:类内所包含的函数就叫方法,可以在类的接口中被调用。 430 | 431 | subject: 432 | The object a method is invoked on. 433 | 434 | >主语:调用方法的对象。 435 | 436 | positional argument: 437 | An argument that does not include a parameter name, so it is not a keyword argument. 438 | 439 | >位置参数:一种参数,没有参数名字,不是关键字参数。 440 | 441 | operator overloading: 442 | Changing the behavior of an operator like + so it works with a programmer-defined type. 443 | 444 | >运算符重载:像+加号这样的运算符,在处理用户自定义类型的时候改变为相应的运算。 445 | 446 | type-based dispatch: 447 | A programming pattern that checks the type of an operand and invokes different functions for different types. 448 | 449 | >按类型处理:一种编程模式,检查运算数的类型,然后根据类型调用不同的函数来进行运算。 450 | 451 | polymorphic: 452 | Pertaining to a function that can work with more than one type. 453 | 454 | >多态:一个函数能处理多种类型的特征,就叫做多态。 455 | 456 | information hiding: 457 | The principle that the interface provided by an object should not depend on its implementation, in particular the representation of its attributes. 458 | 459 | >信息隐藏:一种开发原则,一个对象提供的接口应该独立于其实现,尤其是不受对象属性设置变化的影响。 460 | 461 | ## 17.13 练习 462 | ## # 练习1 463 | 464 | 从[这里](http://thinkpython2.com/code/Time2.py)下载本章的代码。把 Time 中的属性改变成一个单独的整型变量,用来表示自从午夜至今的秒数。然后修改一下各个方法(以及 int_to_time 函数),让所有功能都能在新的实现下正常工作。尽量就让自己不用去更改main 当中的测试代码。你改完之后,输出应该与之前相同。[样例代码](http://thinkpython2.com/code/Time2_soln.py) 465 | 466 | ## # 练习2 467 | 468 | 这个练习是一个广为流传的寓言故事,其中包含了一个使用 Python的时候最常见但也是最难发现的错误。写一个名为袋鼠的类的定义,要求有如下的方法: 469 | 470 | 1. 一个__init__方法,用来初始化一个名为 puntch_contents(就是袋鼠的袋子中内容的意思) 的属性,把该属性初始化为一个空列表。 471 | 472 | 2. 一个名为put_in_pouch 的方法,接收任意类型的一个对象,把这个对象放进 pouch_contents 中。 473 | 474 | 3. 一个__str__方法,返回袋鼠对象的字符串表示,以及袋鼠袋子中的内容。 475 | 476 | 通过建立两个袋鼠对象来测试一下你的代码,把它们俩分别命名为 kanga 和 roo,然后把roo 添加到 kanga 的袋子中。 477 | 478 | 下载[这个代码](http://thinkpython2.com/code/BadKangaroo.py)。里面包含了上面这个练习的一个样例代码,但这个代码有很大很悲催的 bug。找出这个 bug 然后改过来吧。 479 | 480 | 如果你搞不定了,可以下载[这个代码](http://thinkpython2.com/code/GoodKangaroo.py),这个代码中解释了整个问题,并且提供了一个可行的解决方案。 481 | 482 | -------------------------------------------------------------------------------- /chapter19.md: -------------------------------------------------------------------------------- 1 | # 第十九章 更多功能 2 | 我在本书中的一个目标就是尽量少教你 Python(译者注:而要多教编程)。有的时候完成一个目的有两种方法,我都会只选择一种而不提其他的。或者有的时候我就把第二个方法放到练习里面。 3 | 4 | 现在我就要往回倒车一下,捡起一些当时略过的重要内容来给大家讲一下。Python 提供了很多并非必须的功能—你完全可以不用这些功能也能写出很好的代码—但用这些功能有时候能让你的代码更加简洁,可读性更强,或者更有效率,甚至有时候能兼顾这三个方面。 5 | 6 | ## 19.1 条件表达式 7 | 8 | 在5.4中,我们见到了条件语句。条件语句往往用于二选一的情况下;比如: 9 | 10 | ```Python 11 | if x > 0: 12 | y = math.log(x) 13 | else: 14 | y = float('nan') 15 | ``` 16 | 17 | 这个语句检查了 x 是否为正数。如果为正数,程序就计算对数值 math.log。如果非正,对数函数会返回一个值错误 ValueError。要避免因此而导致程序异常退出,咱们就生成了一个『NaN』,这个符号是一个特别的浮点数的值,表示的意思是『不是一个数』。 18 | 19 | 20 | 用一个条件表达式能让这个语句更简洁: 21 | 22 | ```Python 23 | y = math.log(x) if x > 0 else float('nan') 24 | ``` 25 | 26 | 上面这行代码读起来就跟英语一样了:『如果 x 大于0就让 y 等于 x 的对数;否则的话就返回 Nan』。 27 | 28 | 29 | 递归函数有时候也可以用这种条件表达式来改写。例如下面就是分形函数 factorial 的一个递归版本: 30 | 31 | ```Python 32 | def factorial(n): 33 | if n == 0: 34 | return 1 35 | else: 36 | return n * factorial(n-1) 37 | ``` 38 | 39 | 我们可以这样改写: 40 | 41 | ```Python 42 | def factorial(n): 43 | return 1 if n == 0 else return n * factorial(n-1) 44 | ``` 45 | 46 | 条件表达式还可以用于处理可选参数。例如下面就是练习2中 GoodKangaroo 类的 init 方法: 47 | 48 | ```Python 49 | def __init__(self, name, contents=None): 50 | self.name = name 51 | if contents == None: 52 | contents = [] 53 | self.pouch_contents = contents 54 | ``` 55 | 56 | 我们可以这样来改写: 57 | 58 | ```Python 59 | def __init__(self, name, contents=None): 60 | self.name = name 61 | self.pouch_contents = [] 62 | if contents == None else contents 63 | ``` 64 | 65 | 一般来讲,你可以用条件表达式来替换掉条件语句,无论这些语句的分支是返回语句或者是赋值语句。 66 | 67 | ## 19.2 列表推导 68 | 69 | 在10.7当中,我们看到了映射和过滤模式。例如,下面这个函数接收一个字符串列表,然后将每一个元素都用字符串方法 capitalize 处理成大写的,然后返回一个新的字符串列表: 70 | 71 | ```Python 72 | def capitalize_all(t): 73 | res = [] 74 | for s in t: 75 | res.append(s.capitalize()) 76 | return res 77 | ``` 78 | 79 | 用列表推导就可以将上面的代码写得更简洁: 80 | 81 | ```Python 82 | def capitalize_all(t): 83 | return [s.capitalize() for s in t] 84 | ``` 85 | 86 | 方括号的意思是我们正在建立一个新的列表。方括号内的表达式确定了此新列表中的元素,然后 for 语句表明我们要遍历的序列。 87 | 88 | 89 | 列表推导的语法有点复杂,就因为这个循环变量,在上面例子中是 s,这个 s 在我们定义之前就出现在语句中了。 90 | 91 | 92 | 列表推导也可以用到滤波中。例如,下面的函数从 t 中选择了大写的元素,然后返回成一个新的列表: 93 | 94 | ```Python 95 | def only_upper(t): 96 | res = [] 97 | for s in t: 98 | if s.isupper(): 99 | res.append(s) 100 | return res 101 | ``` 102 | 103 | 咱们可以用列表推导来重写这个函数: 104 | 105 | ```Python 106 | def only_upper(t): 107 | return [s for s in t if s.isupper()] 108 | ``` 109 | 110 | 列表推导很简洁,也很容易阅读,至少在简单的表达式上是这样。这些语句的执行也往往比同样情况下的 for 循环更快一些,有时候甚至快很多。所以如果你因为我没有早些给你讲而发怒,我也能理解。 111 | 112 | 113 | 但是,我也要辩护一下,列表推导会导致调试非常困难,因为你不能在循环内部放 print 语句了。我建议你只去在一些简单的地方使用,要确保你第一次写出来就能保证代码正常工作。也就是说初学者就还是别用为好。 114 | 115 | ## 19.3 生成器表达式 116 | 117 | 生成器表达式与列表推导相似,用的不是方括号,而是圆括号: 118 | 119 | ```Python 120 | >>> g = (x**2 for x in range(5)) 121 | >>> g 122 | at 0x7f4c45a786c0> 123 | ``` 124 | 125 | 上面这样运行得到的结果就是一个生成器对象,用来遍历一个值的序列。但与列表推导不同的是,生成器表达式并不会立即计算出所有的值;它要等着被调用。内置函数 next 会从生成器中得到下一个值: 126 | 127 | ```Python 128 | >>> next(g) 129 | 0 130 | >>> next(g) 131 | 1 132 | ``` 133 | 134 | 当程序运行到序列末尾的时候,next 函数就会抛出一个停止遍历的异常。你也可以用一个 for 循环来遍历所有的值: 135 | 136 | ```Python 137 | >>> for val in g: ... 138 | print(val) 139 | 4 140 | 9 141 | 16 142 | ``` 143 | 144 | 生成器对象能够追踪在序列中的位置,所以 for 语句就会在 next 函数退出的地方开始。一旦生成器使用完毕了,接下来就要抛出一个停止异常了: 145 | 146 | ```Python 147 | >>> next(g) 148 | StopIteration 149 | ``` 150 | 151 | 生成器表达式多用于求和、求最大或者最小这样的函数中: 152 | 153 | ```Python 154 | >>> sum(x**2 for x in range(5)) 155 | 30 156 | ``` 157 | ## 19.4 any和all 158 | 159 | Python 提供了一个名为 any 的内置函数,该函数接收一个布尔值序列,只要里面有任意一个是真,就返回真。该函数适用于列表: 160 | 161 | ```Python 162 | >>> any([False, False, True]) 163 | True 164 | ``` 165 | 166 | 但这个函数多用于生成器表达式中: 167 | 168 | ```Python 169 | >>> any(letter == 't' for letter in 'monty') 170 | True 171 | ``` 172 | 173 | 这个例子没多大用,因为效果和 in 运算符是一样的。但我们能用 any 函数来改写我们在9.3中写的一些搜索函数。例如,我们可以用如下的方式来改写 avoids: 174 | 175 | ```Python 176 | def avoids(word, forbidden): 177 | return not any(letter in forbidden for letter in word) 178 | ``` 179 | 180 | 这样这个函数读起来基本就跟英语一样了。 181 | 182 | 183 | 用 any 函数和生成器表达式来配合会很有效率,因为只要发现真值程序就会停止了,所以并不需要对整个序列进行运算。 184 | 185 | 186 | Python 还提供了另外一个内置函数 all,该函数在整个序列都是真的情况下才返回真。 187 | 188 | 189 | 做个练习,用 all 来改写一下9.3中的uses_all 函数。 190 | 191 | ## 19.5 集合 192 | 193 | 在13.6中,我用了字典来查找存在于文档中而不存在于词汇列表中的词汇。我写的这个函数接收两个参数,一个是 d1是包含了文档中的词作为键,另外一个是 d2包含了词汇列表。程序会返回一个字典,这个字典包含的键存在于 d1而不在 d2中。 194 | 195 | ```Python 196 | def subtract(d1, d2): 197 | res = dict() 198 | for key in d1: 199 | if key not in d2: 200 | res[key] = None 201 | return res 202 | ``` 203 | 204 | 在这些字典中,键值都是 None,因为根本没有使用。结果就是,浪费了一些存储空间。 205 | 206 | 207 | Python 还提供了另一个内置类型,名为 set(也就是集合的意思),其性质就是有字典的键而无键值。 208 | 209 | 210 | 对集合中添加元素是很快的;对集合成员进行检查也很快。此外集合还提供了一些方法和运算符来进行常见的集合运算。 211 | 212 | 213 | 例如,集合的减法就可以用一个名为 difference 的方法,或者就用减号-。所以我们可以把 subtract 改写成如下形式: 214 | 215 | ```Python 216 | def subtract(d1, d2): 217 | return set(d1) - set(d2) 218 | ``` 219 | 220 | 上面这个函数的结果就是一个集合而不是一个字典,但对于遍历等等运算来说,用起来都一样的。 221 | 222 | 223 | 本书中的一些练习都可以通过使用集合而改写成更精简更高效的形式。例如,下面的代码就是 has_duplicates 的一个实现方案,来自练习7,用的是字典: 224 | 225 | ```Python 226 | def has_duplicates(t): 227 | d = {} 228 | for x in t: 229 | if x in d: 230 | return True 231 | d[x] = True 232 | return False 233 | ``` 234 | 235 | 当一个元素第一次出现的时候,就被添加到字典中。如果同一个元素又出现了,该函数就返回真。 236 | 237 | 用集合的话,我们就能把该函数写成如下形式: 238 | 239 | ```Python 240 | def has_duplicates(t): 241 | return len(set(t)) < len(t) 242 | ``` 243 | 一个元素在一个集合中只能出现一次,所以如果一个元素在 t 中出现次数超过一次,集合会比 t 规模小一些。如果没有重复,集合的规模就应该和 t 一样大。 244 | 245 | 我们还能用集合来做一些第九章的练习。例如,下面就是用一个循环实现的一个版本的 uses_only: 246 | 247 | ```Python 248 | def uses_only(word, available): 249 | for letter in word: 250 | if letter not in available: 251 | return False 252 | return True 253 | ``` 254 | uses_only 会检查 word 中的所有字母是否出现在 available 中。我们可以用如下方法重写: 255 | 256 | ```Python 257 | def uses_only(word, available): 258 | return set(word) <= set(available) 259 | ``` 260 | 这里的<=运算符会检查一个集合是否切另外一个集合的子集或者相等,如果 word 中所有的字符都出现在 available 中就返回真。 261 | 262 | ## 19.6 计数器 263 | 264 | 计数器跟集合相似,除了一点,就是如果计数器中元素出现的次数超过一次,计数器会记录下出现的次数。如果你对数学上多重集的概念有所了解,就会知道计数器是一种对多重集的表示方式。 265 | 266 | 计数器定义在一个名为 collections 的标准模块中,所以你必须先导入一下。你可以用字符串,列表或者任何支持遍历的类型来初始化一个计数器: 267 | 268 | ```Python 269 | >>> from collections import Counter 270 | >>> count = Counter('parrot')>>> count 271 | Counter({'r': 2, 't': 1, 'o': 1, 'p': 1, 'a': 1}) 272 | ``` 273 | 计数器的用法与字典在很多方面都相似;二者都映射了每个键到出现的次数上。在字典中,键必须是散列的。 274 | 275 | 与字典不同的是,当你读取一个不存在的元素的时候,计数器并不会抛出异常。相反的,这时候程序会返回0: 276 | 277 | ```Python 278 | >>> count['d'] 279 | 0 280 | ``` 281 | 282 | 我们可以用计数器来重写一下练习6中的这个 is_anagram 函数: 283 | 284 | ```Python 285 | def is_anagram(word1, word2): 286 | return Counter(word1) == Counter(word2) 287 | ``` 288 | 289 | 如果两个单词是换位词,他们包含同样个数的同样字母,所以他们的计数器是相等的。 290 | 291 | 、计数器提供了一些方法和运算器来运行类似集合的运算,包括加法,剪发,合并和交集等等。此外还提供了一个最常用的方法,most_common,该方法会返回一个由值-出现概率组成的数据对的列表,按照概率从高到低排列: 292 | 293 | ```Python 294 | >>> count = Counter('parrot') 295 | >>> for val, freq in count.most_common(3): ... 296 | print(val, freq) 297 | r 2 298 | p 1 299 | a 1 300 | ``` 301 | ## 19.7 默认字典 302 | 303 | collection 模块还提供了一个默认字典,与普通字典的区别在于当你读取一个不存在的键的时候,程序会添加上一个新值给这个键。 304 | 305 | 306 | 当你创建一个默认字典的时候,就提供了一个能创建新值的函数。用来创建新对象的函数也被叫做工厂。内置的创建列表、集合以及其他类型的函数都可以被用作工厂: 307 | 308 | ```Python 309 | >>> from collections import defaultdict 310 | >>> d = defaultdict(list) 311 | ``` 312 | 313 | 要注意到这里的参数是一个列表,是一个类的对象,而不是 list(),带括号的就是一个新列表了。这个创建新值的函数只有当你试图读取一个不存在的键的时候才会被调用。 314 | 315 | ```Python 316 | >>> t = d['new key'] 317 | >>> t [] 318 | ``` 319 | 320 | 新的这个我们称之为 t 的列表,也会被添加到字典中。所以如果我们修改 t,这种修改也会在 d 中出现。 321 | 322 | ```Python 323 | >>> t.append('new value') 324 | >>> d 325 | defaultdict(, {'new key': ['new value']}) 326 | ``` 327 | 328 | 所以如果你要用列表组成字典的话,你就可以多用默认字典来写出更简洁的代码。你可以在[这里](http://thinkpython2.com/code/anagram_sets.py)下载我给练习2提供的样例代码,其中我建立了一个字典,字典中建立了从一个字母字符串到一个可以由这些字母拼成的单词的映射。例如,『opst』就映射到了列表[’opts’, ’post’, ’pots’, ’spot’, ’stop’, ’tops’]。 329 | 330 | 331 | 下面就是原版的代码: 332 | 333 | ```Python 334 | def all_anagrams(filename): 335 | d = {} 336 | for line in open(filename): 337 | word = line.strip().lower() 338 | t = signature(word) 339 | if t not in d: 340 | d[t] = [word] 341 | else: 342 | d[t].append(word) 343 | return d 344 | ``` 345 | 346 | 用默认集合就可以简化一下,就如你在练习2中用过的那样: 347 | 348 | ```Python 349 | def all_anagrams(filename): 350 | d = {} 351 | for line in open(filename): 352 | word = line.strip().lower() 353 | t = signature(word) 354 | d.setdefault(t, []).append(word) 355 | return d 356 | ``` 357 | 358 | 这个代码有一个不足,就是每次都要建立一个新列表,而不论是否需要创建。对于列表来说,这倒不要紧,不过如果工厂函数比较复杂的话,这就麻烦了。 359 | 360 | 361 | 这时候咱们就可以用默认字典来避免这个问题并且简化代码: 362 | 363 | ```Python 364 | def all_anagrams(filename): 365 | d = defaultdict(list) 366 | for line in open(filename): 367 | word = line.strip().lower() 368 | t = signature(word) 369 | d[t].append(word) 370 | return d 371 | ``` 372 | 373 | 你可以从[这里](http://thinkpython2.com/code/PokerHandSoln.py)下载我给练习3写的样例代码,该代码中在 has_straightflush函数用的是默认集合。这份代码的不足就在于每次循环都要创建一个 Hand 对象,而不论是否必要。做个练习,用默认字典来该写一下这个程序。 374 | 375 | ## 19.8 命名元组 376 | 377 | 很多简单的类就是一些相关值的集合。例如在15章中定义的 Point 类中就包含两个数值,x 和 y。当你这样定义一个类的时候,你通常要写一个 init 方法和一个 str 方法: 378 | 379 | ```Python 380 | class Point: 381 | def __init__(self, x=0, y=0): 382 | self.x = x 383 | self.y = y 384 | def __str__(self): 385 | return '(%g, %g)' % (self.x, self.y) 386 | ``` 387 | 388 | 要传达这么小规模的信息却要用这么多代码。Python 提供了一个更简单的方式来做类似的事情: 389 | 390 | ```Python 391 | from collections import namedtuple 392 | Point = namedtuple('Point', ['x', 'y']) 393 | ``` 394 | 395 | 第一个参数是你要写的类的名字。第二个是 Point 对象需要有的属性列表,为字符串。命名元组返回的值是一个类的对象。 396 | 397 | ```Python 398 | >>> Point 399 | 400 | ``` 401 | 402 | Point 会自动提供诸如 init 和 str 之类的方法,所以就不用再去写了。 403 | 404 | 要建立一个 Point 对象,你就可以用 Point 类作为一个函数用: 405 | 406 | ```Python 407 | >>> p = Point(1, 2) 408 | >>> p 409 | Point(x=1, y=2) 410 | ``` 411 | 412 | init 方法把参数赋值给按照你设定来命名的属性。 str 方法输出整个 Point 类及其属性的一个字符串表达。 413 | 414 | 你可以用名字来读取命名元组中的元素: 415 | 416 | ```Python 417 | >>> p.x, p.y 418 | (1, 2) 419 | ``` 420 | 421 | 但你也可以把命名元组当做元组来用: 422 | 423 | ```Python 424 | >>> p[0], p[1] 425 | (1, 2) 426 | >>> x, y = p 427 | >>> x, y 428 | (1, 2) 429 | ``` 430 | 431 | 命名元组提供了定义简单类的快捷方式。缺点就是这些简单的类不能总保持简单的状态。有时候你可能想给一个命名元组添加方法。这时候你就得定义一个新类来继承命名元组: 432 | 433 | ```Python 434 | class Pointier(Point): 435 | # add more methods here 436 | ``` 437 | Or you could switch to a conventional class definition. 438 | 439 | >或者你可以把命名元组转换成一个常规的类的定义。 440 | 441 | ## 19.9 收集关键词参数 442 | 443 | 在12.4中,我们已经学过了如何写将参数收集到一个元组中的函数: 444 | 445 | ```Python 446 | def printall(*args): 447 | print(args) 448 | ``` 449 | 450 | 这种函数可以用任意数量的位置参数(就是无关键词的参数)来调用。 451 | 452 | ```Python 453 | >>> printall(1, 2.0, '3') 454 | (1, 2.0, '3') 455 | ``` 456 | 但*运算符并不能收集关键词参数: 457 | 458 | ```Python 459 | >>> printall(1, 2.0, third='3') 460 | TypeError: printall() got an unexpected keyword argument 'third' 461 | ``` 462 | To gather keyword arguments, you can use the ** operator: 463 | 464 | >要收集关键词参数,你就可以用**运算符: 465 | 466 | ```Python 467 | def printall(*args, **kwargs): 468 | print(args, kwargs) 469 | ``` 470 | 你可以用任意名字来命名这里的关键词收集参数,不过通常大家都用kwargs。得到的结果是一个字典,映射了关键词键名与键值: 471 | 472 | ```Python 473 | >>> printall(1, 2.0, third='3') 474 | >>> (1, 2.0) {'third': '3'} 475 | ``` 476 | 477 | 如果你有一个关键词和值组成的字典,你就可以用散射运算符,**来调用一个函数: 478 | 479 | ```Python 480 | >>> d = dict(x=1, y=2) 481 | >>> Point(**d) 482 | Point(x=1, y=2) 483 | ``` 484 | 485 | 不用散射运算符的话,函数会把 d 当做一个单独的位置参数,所以就会把 d 赋值股额 x,然后出错,因为没有给 y 赋值: 486 | 487 | ```Python 488 | >>> d = dict(x=1, y=2) 489 | >>> Point(d) 490 | Traceback (most recent call last): File "", line 1, in TypeError: __new__() missing 1 required positional argument: 'y' 491 | ``` 492 | 493 | 当你写一些有大量参数的函数的时候,就可以创建和使用一些字典,这样能把各种常用选项弄清。 494 | 495 | ## 19.10 Glossary 术语列表 496 | conditional expression: 497 | An expression that has one of two values, depending on a condition. 498 | 499 | >条件表达式:一种根据一个条件来从两个值中选一个的表达式。 500 | 501 | list comprehension: 502 | An expression with a for loop in square brackets that yields a new list. 503 | 504 | >列表推导:一种用表达式, 方括号内有一个for 循环,生成一个新的列表。 505 | 506 | generator expression: 507 | An expression with a for loop in parentheses that yields a generator object. 508 | 509 | >生成器表达式:一种表达式,圆括号内放一个 for 循环,产生一个生成器对象。 510 | 511 | multiset: 512 | A mathematical entity that represents a mapping between the elements of a set and the number of times they appear. 513 | 514 | >多重集:一个数学上的概念,表示了一种从集合中元素到出现次数只见的映射关系。 515 | 516 | factory: 517 | A function, usually passed as a parameter, used to create objects. 518 | 519 | >工厂:一个函数,通常作为参数传递,用来产生对象。 520 | 521 | ## 19.11 练习 522 | ## # 练习1 523 | 524 | 下面的函数是递归地计算二项式系数的。 525 | 526 | ```Python 527 | def binomial_coeff(n, k): 528 | """Compute the binomial coefficient "n choose k". 529 | n: number of trials 530 | k: number of successes 531 | returns: int """ 532 | if k == 0: 533 | return 1 534 | if n == 0: 535 | return 0 536 | res = binomial_coeff(n-1, k) + binomial_coeff(n-1, k-1) 537 | return res 538 | ``` 539 | 540 | 用网状条件语句来重写一下函数体。 541 | 542 | 543 | 一点提示:这个函数并不是很有效率,因为总是要一遍一遍地计算同样的值。你可以通过存储已有结果(参考11.6)来提高效率。但你会发现如果你用条件表达式实现,就会导致这种记忆更困难。 544 | 545 | -------------------------------------------------------------------------------- /chapter2.md: -------------------------------------------------------------------------------- 1 | # 第二章 变量,表达式,语句 2 | 3 | 编程语言最强大的功能就是操作变量。变量就是一个有值的代号。 4 | 5 | ## 2.1 赋值语句 6 | 7 | 赋值语句的作用是创建一个新的变量,并且赋值给这个变量: 8 | 9 | ```python 10 | >>> message = 'And now for something completely different' 11 | >>> n = 17 12 | >>> pi = 3.141592653589793 13 | ``` 14 | 15 | 上面就是三个赋值语句的例子。第一个是把一个字符串复制给名叫message的新变量;第二个将n赋值为整数17;第三个把圆周率的一个近似值赋给了pi这个变量。 16 | 17 | 18 | 平常大家在纸上对变量赋值的方法就是写下名字,然后一个箭头指向它的值。这种图解叫做状态图,因为它能指明各个变量存储的是什么内容。下图就展示了上面例子中赋值语句的结果。 19 | 20 | ________________________________________ 21 | ![Figure 2.1: State diagram.](./images/figure2.1.jpg) 22 | Figure 2.1: State diagram. 23 | ________________________________________ 24 | ## 2.2 变量名称 25 | 26 | 编程的人总得给变量起个有一定意义的名字才能记得住,一般情况就用名字来表示这个变量的用途了。 27 | 28 | 变量名称你随便起多长都可以的。包含字母或者数字都行,但是不能用数字来开头。大写字母也能用,不过还是建议都用小写字母来给变量命名,这个比较传统哈。 29 | 30 | 变量名里面可以有下划线_,一般在多个单词组成的变量名里面往往用到下划线,比如your_name等等。 31 | 32 | 你要是给变量起名不合规则,就会出现语法错误提示了: 33 | 34 | 35 | ```Python 36 | >>> 76trombones = 'big parade' 37 | SyntaxError: invalid syntax 38 | >>> more@ = 1000000 39 | SyntaxError: invalid syntax 40 | >>> class = 'Advanced Theoretical Zymurgy' 41 | SyntaxError: invalid syntax 42 | ``` 43 | 第一个数字开头所以不合规则,第二个有非法字符@,第三个这个class咋不行呢?好奇吧? 44 | 45 | 因为classs是Python里面的一个关键词啦。解释器要用关键词来识别程序的结构,这些关键词是不能用来做变量名的。 46 | 47 | 48 | 以下是Python3的关键词哈: 49 | 50 | * False class finally is 51 | * return None continue for lambda 52 | * try True def from nonlocal 53 | * while and del global not 54 | * with as elif if or 55 | * yield assert else import pass 56 | * break except in raise 57 | 58 | 你不用去记忆这些哈。因为一般大多数的开发环境里面,关键词都会有区别于普通代码的颜色提示你,你要是用他们做变量名了,一看就会知道的。 59 | 60 | ## 2.3 表达式和语句 61 | 62 | 表达式是数值,变量和操作符的组合。单个值本身也被当作一个表达式,变量也是如此,下面这些例子都是一些正确表达式: 63 | 64 | ```Python 65 | >>> 42 66 | 42 67 | >>> n 68 | 17 69 | >>> n + 25 70 | 42 71 | ``` 72 | 73 | 当你在提示符后面敲出一个表达式,解释器就判断一下,他会找到这个表达式的值。在本节的例子中,n的值是17,所以n+25就是42了。 74 | 75 | 76 | 语句是一组具有某些效果的代码,比如创建变量,或者显示值。 77 | 78 | ```Python 79 | >>> n = 17 80 | >>> print(n) 81 | ``` 82 | 83 | 上面第一个就是赋值语句,给n赋值。第二行是显示n的值。 84 | 85 | 86 | 输入语句的时候,解释器会执行它,就是会按照语句所说的去做。一般语句是没有值的。 87 | 88 | ## 2.4 脚本模式 89 | 90 | 以上我们一直在用Python的交互模式,就是直接咱们人跟解释器来交互。开始学的时候这样挺好的,但如果你要想一次运行多行代码,这样就很不方便了。 91 | 92 | 93 | 所以就有另一种选择了,把代码保存成脚本,然后用脚本模式让解释器来运行这些脚本。通常Python脚本文件的扩展名是.py。 94 | 95 | 96 | 如果你知道怎么创建和运行脚本,那就尽管在自己电脑上尝试好了。否则我就建议你还是用PythonAnywhere。关于脚本模式的介绍我放到网上了,打开[这个链接](http://tinyurl.com/thinkpython2e)去看下哈。 97 | 98 | 99 | Python两种模式都支持,所以你可以先用交互模式做点测试,然后再写成脚本。但是两种模式之间有些区别的,所以可能也挺麻烦。 100 | 101 | 举个例子哈,比如咱们把Python当计算器用,你输入以下内容: 102 | 103 | ```Python 104 | >>> miles = 26.2 105 | >>> miles * 1.61 106 | 42.182 107 | ``` 108 | 109 | 第一行给miles这个变量赋初值(译者注:26.2英里是马拉松比赛全程长度),但是看着没啥效果。第二行是一个表达式,解释器会计算这个表达式,然后把结果输出。结果就是把马拉松全程长度从英里换算成公里,答案是42公里哈。 110 | 111 | 112 | 不过你要是直接把这些代码存成脚本然后运行,是啥都看不到的,没有输出。在脚本模式表达式是没有明显效果的。Python确实会计算这些表达式,但不显示结果,想看到结果你就得告诉他输出一下: 113 | 114 | ```Python 115 | miles = 26.2 116 | print(miles * 1.61) 117 | ``` 118 | 119 | 这种情况开始还挺让人混乱的。 120 | 121 | 122 | 脚本一般都是包含了一系列的语句。如果语句超过一条,每个语句执行的时候都会显示结果。比如下面这个: 123 | 124 | 125 | ```python 126 | print(1) 127 | x = 2 128 | print(x) 129 | ``` 130 | produces the output 131 | 132 | 输出的结果如下 133 | 134 | ```Python 135 | 1 136 | 2 137 | ``` 138 | 139 | 赋值语句是不会有任何输出的。 140 | 检查下你理解了没哈,把下面这些语句输入到Python解释器,看看会发生什么: 141 | 142 | ```Python 143 | 5 x = 5 x + 1 144 | ``` 145 | 现在再把同样的语句输入到脚本中,然后用Python来运行一下。看看输出是啥样的?把脚本中的表达式修改一下,每一个都加一个打印语句再试试。 146 | 147 | ## 2.5 运算符优先级 148 | 149 | 表达式可能会包含不止一个运算符,这些不同的运算先后次序就是运算符的优先级。对于数学运算符来说,Python就遵循着数学上的规则。下面这个PEMDAS、是用来记忆这些优先规则的好方法: 150 | 151 | * 括号内的内容最优先,大家可以用括号来强制某系表达式有限计算。所以2\*\*(3-1)就等于4了,(1+1)\*\*(5-2)就是2的立方,等于8。使用括号也有助于让你的表达式读起来更好理解,比如(minute * 100) / 60,这个也不影响计算结果,不过看起来易于理解。 152 | 153 | * 除了括号,所有运算符中,乘方最优先,所以1 + 2\*\*3的结果是9而不是27,2\*3\*\*2结果是18,而不是36。 154 | 155 | * 乘除运算比加减优先,译者认为大家都知道了,这个我就不细说了。 156 | 157 | * 同类运算符从左往右来进行,乘方除外。这个也不细说了,很简单。 158 | 159 | 我不会花很大力气来记忆这些运算符的优先级。如果我怕记不住弄错了,就用括号来让优先级明确一下就好。 160 | 161 | ## 2.6 字符串操作 162 | 163 | 一般情况下,咱们不能对字符串进行数学运算的,即使字符串看上去像是数字也不行,所以以下这些都是非法操作: 164 | 165 | ```Python 166 | '2'-'1' 167 | 'eggs'/'easy' 168 | 'third'*'a charm' 169 | ``` 170 | 171 | 不过+和*可以用在字符串上面。 172 | 173 | 174 | +加号的意思就是字符串拼接了,会把两个字符串拼到一起,如下所示: 175 | 176 | ```Python 177 | >>> first = 'throat' 178 | >>> second = 'warbler' 179 | >>> first + second 180 | throatwarbler 181 | ``` 182 | 183 | 星号也就是乘法运算符也可以用在字符串上面,效果就是重复。比如'Spam'*3 结果就是 184 | 185 | 'SpamSpamSpam',重复了三次。需要注意的是字符串必须用整数去乘。 186 | 187 | 这种加法和乘法实际上就是拼接和重复的意思。 188 | 189 | ## 2.7 注释 190 | 191 | 程序会越来越庞大,也越复杂了,读起来就会更难了。公式语言很密集,靠阅读来理解代码,总是很困难的。 192 | 193 | 为了解决阅读的困难,咱们就可以添加一些笔记到代码中,把程序的功能用自然语言来解释一下。这种笔记就叫注释了,使用井号# 来开头的: 194 | 195 | ```Python 196 | # compute the percentage of the hour that has elapsed percentage = (minute * 100) / 60 197 | ``` 198 | 199 | 注释可以另起一行,也可以放到行末尾: 200 | 201 | ```Python 202 | percentage = (minute * 100) / 60 # percentage of an hour 203 | ``` 204 | 205 | 井号# 后面的内容都会被忽略,因此不会影响程序的运行结果。 206 | 207 | 208 | 一般注释都是用来解释代码的一些不明显的特性。一般情况下读代码的人应该能理解代码的功能是什么,所以用注释多是要解释这样做的目的是什么。 209 | 210 | 211 | 下面这个注释就显然是多余的,根本没必要: 212 | 213 | ```Python 214 | v = 5 # assign 5 to v 215 | ``` 216 | 217 | 下面这种注释包含了重要信息,就很重要了: 218 | 219 | ```python 220 | v = 5 # velocity in meters/second. 221 | ``` 222 | 223 | 变量命名得当的话,就没必要用太多注释了,不过名字要是太长了,表达式读起来也挺麻烦,所以就得权衡着来了。 224 | 225 | ## 2.8 调试 226 | 227 | 程序一般会有三种错误:语法错误,运行错误和语义错误。区分这三种错误有助于更快速地追踪错误。 228 | 229 | * 语法错误Syntax error: 230 | 231 | 语法是指程序的结构和规则。比如括号要成对用。如果你的程序有某个地方出现了语法错误,Python会显示出错信息并退出,程序就不能运行了。最开始学习编程的这段时间,你遇到的最常见的估计就是这种情况。等你经验多了,基本就犯的少了,而且也很容易发现了。 232 | 233 | * 运行错误Runtime error: 234 | 235 | 第二种错误就是运行错误,显而易见了,就是直到运行的时候才会出现的错误。这种错误也被叫做异常,因为一般表示一些意外的尤其是比较糟糕的情况发生了。 236 | 237 | * 语义错误Semantic error: 238 | 239 | 第三种就是语义错误,顾名思义,是跟意义相关。这种错误是指你的程序运行没问题,也不产生错误信息,但不能正确工作。程序可能做一些和设计目的不同的事情。发现语义错误特别不容易,需要你仔细回顾代码和程序输出,要搞清楚到底程序做了什么。 240 | 241 | ## 2.9 Glossary 术语列表 242 | variable: 243 | A name that refers to a value. 244 | 变量:有值的量。 245 | 246 | >assignment: 247 | 248 | A statement that assigns a value to a variable. 249 | 250 | >赋值:给一个变量赋予值。 251 | 252 | state diagram: 253 | A graphical representation of a set of variables and the values they refer to. 254 | 255 | >状态图:图形化表征每个变量的值。 256 | 257 | keyword: 258 | A reserved word that is used to parse a program; you cannot use keywords like if, def, and while as variable names. 259 | 260 | >关键词:系统保留的用于解析程序的词,不能用关键词当做变量名。 261 | 262 | operand: 263 | One of the values on which an operator operates. 264 | 265 | >运算数:运算符来进行运算操作的数值。 266 | 267 | expression: 268 | A combination of variables, operators, and values that represents a single result. 269 | 270 | >表达式:一组变量、运算数的组合,会产生单值作为结果。 271 | 272 | evaluate: 273 | To simplify an expression by performing the operations in order to yield a single value. 274 | 275 | >求解:把表达式所表示的运算计算出来,得到一个单独的值。 276 | 277 | statement: 278 | A section of code that represents a command or action. So far, the statements we have seen are assignments and print statements. 279 | 280 | >声明:一组表示一种命令或者动作的代码,目前我们了解的只有赋值语句和打印语句。 281 | 282 | execute: 283 | To run a statement and do what it says. 284 | 285 | >运行:将一条语句进行运行。 286 | 287 | interactive mode: 288 | A way of using the Python interpreter by typing code at the prompt. 289 | 290 | >交互模式:在提示符后输入代码,让解释器来运行代码的模式。 291 | 292 | script mode: 293 | A way of using the Python interpreter to read code from a script and run it. 294 | 295 | >脚本模式:将代码保存成脚本文件,用解释器运行的模式。 296 | 297 | script: 298 | A program stored in a file. 299 | 300 | >脚本:程序以文本形式存成的文件。 301 | 302 | order of operations: 303 | Rules governing the order in which expressions involving multiple operators and operands are evaluated. 304 | 305 | >运算符优先级:不同运算符和运算数进行计算的优先顺序。 306 | 307 | concatenate: 308 | To join two operands end-to-end. 309 | 310 | >拼接:把两个运算对象相互连接到一起。 311 | 312 | comment: 313 | Information in a program that is meant for other programmers (or anyone reading the source code) and has no effect on the execution of the program. 314 | 315 | >注释:程序中用来解释代码含义和运行效果的备注信息,通常给阅读代码的人准备的。 316 | 317 | syntax error: 318 | An error in a program that makes it impossible to parse (and therefore impossible to interpret). 319 | 320 | >语法错误:程序语法上的错误,导致程序不能被解释器解译,就不能运行了。 321 | 322 | exception: 323 | An error that is detected while the program is running. 324 | 325 | >异常:程序运行的时候被探测到的错误。 326 | 327 | semantics: 328 | The meaning of a program. 329 | 330 | >语义:程序的意义。 331 | 332 | semantic error: 333 | An error in a program that makes it do something other than what the programmer intended. 334 | 335 | >语义错误:程序运行的结果和料想的不一样,没有完成设计的功能,而是干了点其他的事情。 336 | 337 | ## 2.10 练习 338 | ## # 练习1 339 | 340 | 像上一章一样,按我建议的,不论学了什么新内容,你都试着在交互模式上故意犯点错误,看看会怎么样。 341 | 342 | * 我们都看到了n=42是可以的,那42=n怎么样? 343 | 344 | * 再试试x=y=1呢? 345 | 346 | * 有的语言每个语句结尾都必须有个单引号或者分号,试试在Python句末放个会咋样? 347 | 348 | * 句尾放个句号试试呢? 349 | 350 | * 数学上你可以把x和y相乘写成xy,Python里面你这么试试看? 351 | 352 | ## # 练习2 353 | 354 | 把Python解释器当做计算器来做下面的练习: 355 | 356 | 1. 球体体积是三分之四倍的圆周率乘以半径立方,求半径为5的球体体积。 357 | 2. 假如一本书的封面标价是24.95美元,书店打六折。第一本运费花费3美元,后续每增加一本的运费是75美分。问买60本一共得花多少钱呢? 358 | 3. 我早上六点五十二分出门离家,以8:15的节奏跑了一英里,又以7:12的节奏跑了三英里,然后又是8:15的节奏跑一英里,回到家吃饭是几点? 359 | -------------------------------------------------------------------------------- /chapter3.md: -------------------------------------------------------------------------------- 1 | # 第三章 函数 2 | 3 | 在编程的语境下,“函数”这个词的意思是对一系列语句的组合,这些语句共同完成一种运算。定义函数的时候,你要给这个函数指定一个名字,另外还好写出这些进行运算的语句。定义完成后,就可以通过函数名来“调用”函数。 4 | 5 | ## 3.1 函数调用 6 | 7 | 此前我们已经见识过函数调用的一个例子了: 8 | 9 | ```Python 10 | >>> type(42) 11 | 12 | ``` 13 | 14 | 这个函数的名字就是tpye,括号里面的表达式叫做函数的参数。这个函数的结果是返回参数的类型。 15 | 16 | 一般来说,函数都要“传入”一个参数,“返回”一个结果。结果也被叫做返回值。Python提供了一些转换数值类型的函数。比如int这个函数就可以把值转换成整形,但不是什么都能转的,遇到不能转换的就会报错了,如下所示: 17 | 18 | ```Python 19 | >>> int('32') 20 | 32 21 | >>> int('Hello') 22 | ValueError: invalid literal for int(): Hello 23 | ``` 24 | 25 | int这个函数能把浮点数转成整形,但不是很完美,小数部分就都给砍掉了。 26 | 27 | ```Python 28 | >>> int(3.99999) 29 | 3 30 | >>> int(-2.3) 31 | -2 32 | ``` 33 | 34 | float能把整形和字符串转变成浮点数: 35 | 36 | ```Python 37 | >>> float(32) 38 | 32.0 39 | >>> float('3.14159') 40 | 3.14159 41 | ``` 42 | 43 | 最后来看下,str可以把参数转变成字符串: 44 | 45 | ```Python 46 | >>> str(32) 47 | '32' 48 | >>> str(3.14159) 49 | '3.14159' 50 | ``` 51 | ## 3.2 数学函数 52 | 53 | 54 | Python内置了一个数学模块,这一模块提供了绝大部分常用的数学函数。模块就是一系列相关函数的集合成的文件。 55 | 56 | 在使用模块中的函数之前,必须先要导入这个模块,使用导入语句: 57 | 58 | ```Python 59 | >>> import math 60 | ``` 61 | 这个语句建立了一个模块对象,名字叫做math。如果你让这个模块对象显示一下,你就会得到与之相关的信息了: 62 | 63 | ```Python 64 | >>> math 65 | 66 | ``` 67 | 68 | 模块对象包含了一些已经定义好的函数和变量。指定模块名和函数名,要用点(也就是英文的句号)来连接模块名和函数名,就可以调用指定的函数了。 69 | 70 | ```Python 71 | >>> ratio = signal_power / noise_power 72 | >>> decibels = 10 * math.log10(ratio) 73 | >>> radians = 0.7 74 | >>> height = math.sin(radians) 75 | ``` 76 | 77 | 第一个例子用了数学的log10的函数,来计算信噪比的分贝值(假设信号强度和噪音强度都已知了)。数学模块同时也提供了log,用自然底数e取对数的函数。 78 | 79 | 第二个例子是对弧度值计算正弦值。通过变量名你应该能推测出正弦以及其他的三角函数(比如余弦、正切等等)都要用弧度值作为参数。所以要把角度的值从度转换成弧度,方法就是除以180然后再乘以圆周率π: 80 | 81 | ```Python 82 | >>> degrees = 45 83 | >>> radians = degrees / 180.0 * math.pi 84 | >>> math.sin(radians) 85 | 0.707106781187 86 | ``` 87 | 88 | math.pi这个表达式从数学模块中得到π的一个大概精确到15位的近似值,存成一个浮点数。 89 | 90 | 91 | 了解了三角函数之后,你可以用试着把2的平方根除以二,然后对比一下这个结果和上一个结果: 92 | 93 | ```Python 94 | >>> math.sqrt(2) / 2.0 95 | 0.707106781187 96 | ``` 97 | 98 | >译者注:画一个三角形就知道了,45度角两直角边是单位1,斜边必然是2的平方根了,对应的正弦余弦也都是这个值了。大家应该能理解吧? 99 | 100 | ## 3.3 组合 101 | 102 | 目前为止,我们已经见识了一个程序所需要的大部分元素了:变量、表达式、语句。不过咱们都是一个个单独看到的,还没有把它们结合起来试试。 103 | 104 | 一门编程语言最有用的功能莫过于能够用一个个小模块来拼接创作。例如函数的参数可以是任何一种表达式,包括代数运算符: 105 | 106 | ```Python 107 | x = math.sin(degrees / 360.0 * 2 * math.pi) ß 108 | ``` 109 | 再或者函数的调用本身也可以作为参数: 110 | 111 | ```Python 112 | x = math.exp(math.log(x+1)) 113 | ``` 114 | 115 | 你可以在任何地方放一个值,放任何一个表达式,只有一个例外:一个声明语句的左边必须是变量名。任何其他的表达式放到等号左边都会导致语法错误(当然也有例外,等会再给介绍)。 116 | 117 | ```Python 118 | >>> minutes = hours * 60 # right 119 | >>> hours * 60 = minutes # wrong! 120 | SyntaxError: can't assign to operator 121 | ``` 122 | 123 | >译者注:上述例子里面把表达式复制为变量是不行的,所说的例外估计是指尤达大师命名法,这个后面看到再说。 124 | 125 | ## 3.4 自定义函数 126 | 127 | 目前我们学到了一些Python自带的函数,自己定义新的函数也是可以的。函数定义要指定这个新函数的名字,还需要一系列语句放到这个函数里面,当调用这个函数的时候,就会运行这些语句了。 128 | 129 | ```Python 130 | def print_lyrics(): 131 | print("I'm a lumberjack, and I'm okay.") 132 | print("I sleep all night and I work all day.") 133 | ``` 134 | 135 | 这里的def就是一个关键词,意思是这是在定义一个函数。函数的名字就是print_lyrics,函数的命名规则和变量命名规则基本差不多,都是字幕梳子或者下划线,但是不能用数字打头。另外也不能用关键词做函数名,还要注意尽量避免函数名和变量名发生重复。 136 | 137 | 138 | 函数名后面的括号是空的,意思是这个函数不需要参数。 139 | 140 | 141 | 函数定义的第一行叫做头部,剩下的叫做函数体。函数头部的末尾必须有一个冒号,函数体必须是相对函数头部有缩进的,距离行首相对于函数头要有四个空格的距离。函数体可以有任意长度的语句。 142 | 143 | (译者注:缩进是Python最强制的要求,本书的翻译用的MarkDown在生成的时候可能未必能够完美缩进,所以大家多注意一下自己调整哈,这个超级重要!) 144 | 145 | 在打印语句中,要打印的字符串需要用双引号括着。单引号和双引号效果一样,除非是字符串中已经出现了单引号,大家一般都是用单引号的。 146 | 147 | 148 | 所有的引号都必须是键盘上直接是引号的那个"键,无论是单引号还是双引号。就是回车键左边那个。“Curly quotes”这种引号,在Python里面是非法的。 149 | 150 | 151 | 如果你在交互模式下面定义函数,解释器会显示三个小点来提醒你定义还没有完成: 152 | 153 | ```Python 154 | >>> def print_lyrics(): 155 | ... 156 | print("I'm a lumberjack, and I'm okay.") ... 157 | print("I sleep all night and I work all day.") ... 158 | ``` 159 | 在函数定义完毕的结尾,必须输入一行空白行。定义函数会创建一个函数类的对象,有type函数。 160 | 161 | ```Python 162 | >>> print(print_lyrics) 163 | 164 | >>> type(print_lyrics) 165 | 166 | ``` 167 | 168 | 调用新函数的语法和调用内置函数是一样的: 169 | 170 | ```Python 171 | >>> print_lyrics() 172 | I'm a lumberjack, and I'm okay. I sleep all night and I work all day. 173 | ``` 174 | 175 | 一旦你定义了一个函数,就可以在其它函数里面来调用这个函数。比如咱们重复一下刚刚讨论的,写一个叫做重repeat_lyrics的函数。 176 | 177 | ```Python 178 | def repeat_lyrics(): 179 | print_lyrics() 180 | 181 | ``` 182 | 183 | 然后调用一下这个函数: 184 | 185 | ```Python 186 | >>> repeat_lyrics() 187 | 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. 188 | ``` 189 | 190 | 当然了,实际这首歌可不是这样的哈。 191 | 192 | ## 3.5 定义并使用 193 | 194 | 把前面这些小块的代码来整合一下,整体上程序看着大概是这样的: 195 | 196 | ```Python 197 | def print_lyrics(): 198 | print("I'm a lumberjack, and I'm okay.") 199 | print("I sleep all night and I work all day.") 200 | def repeat_lyrics(): 201 | print_lyrics() 202 | repeat_lyrics() 203 | ``` 204 | 205 | 这个程序包含两个函数的定义:print_lyrics以及repeat_lyrics,函数定义的执行就和其他语句一样,但是效果是创建函数对象。函数定义中的语句直到函数被调用的时候才会运行,函数的定义本身不会有任何输出。 206 | 207 | 208 | 如你所愿了,你可以建立一个函数,然后运行一下试试了。换种说法就是,在调用之前一定要先把函数定义好。 209 | 210 | 211 | 作为练习,把这个程序的最后一行放到顶部,这样函数调用就在函数定义之前了。运行一下看看出错的信息是什么。 212 | 213 | 214 | 然后再把函数调用放到底部,把print_lyrics这个函数的定义放到repeat_lyrics这个函数的后面。再看看这次运行会出现什么样子? 215 | 216 | ## 3.6 运行流程 217 | 218 | 为了确保一个函数在首次被调用之前已经定义,你必须要之道语句运行的顺序,也就是所谓『运行流程』。 219 | 220 | 221 | 一个Python程序都是从第一个语句开始运行的。从首至尾,每次运行一个语句。 222 | 223 | 224 | 函数的定义并不会改变程序的运行流程,但要注意,函数体内部的语句只有在函数被调用的时候才会运行。 225 | 226 | 227 | 函数调用就像是运行流程有了绕道的行为。没有直接去执行下一个语句,运行流跳入到函数体内,运行里面的语句,然后再回来从离开的地方继续执行。 228 | 229 | 230 | 这么解释很简明易懂了,只要你记住一个函数可以调用另一个就行。在一个函数的中间,程序有可能必须运行一下其他函数中的语句。所以运行新的函数的时候,程序可能也必须运行其他的函数! 231 | 232 | (译者注:看着很绕嘴,其实蛮简单的,就是跳出跳入互相调用而已。) 233 | 234 | 幸运的是,Python很善于追踪应该执行的位置,所以每次一个函数执行完毕了,程序都会回到当时跳出的位置,然后继续运行。等执行到了程序的末尾,就终止了。 235 | 236 | 总的来说,你阅读一个程序的时候,并不一定总是要从头到尾来读的。有时候你要按照运行流程来读才更好理解。 237 | 238 | ## 3.7 形式参数和实际参数 239 | (译者注:这里提到的形参和实参实际上是传值方式的区别,这个在最基本的编程入门课程中老师应该都比较强调的。实际参数就是调用函数时候传给他的那个参数;而形式参数可以理解为函数内部定义用的参数。老外对这个的思辩也很多。这里我先不说太多,翻译着再看。 240 | 大家可以去网上多搜索一下,比如在[StackOverflow](http://stackoverflow.com/questions/1788923/parameter-vs-argument)和[MSDN](https://msdn.microsoft.com/en-us/library/9kewt1b3.aspx)) 241 | 242 | 我们已经看到了一些函数了,他们都需要实际参数。比如当你调用数学的正弦函数的时候你就需要给它一个数值作为实际参数。有的函数需要一个以上的实际参数,比如幂指数函数需要两个,一个是底数,一个是幂次。 243 | 244 | 245 | 在函数里面,实际参数会被赋值给形式参数。下面就是一个使用单个实际参数的函数的定义: 246 | 247 | ```Python 248 | def print_twice(bruce): 249 | print(bruce) 250 | print(bruce) 251 | 252 | ``` 253 | 254 | 这个函数把传来的实际参数的值赋给了一个名字叫做burce的形式参数。当函数被调用的时候,就会打印出形式参数的值两次(无论是什么内容)。任何能打印的值都适用于这个函数。 255 | 256 | ```Python 257 | >>> print_twice('Spam') 258 | Spam 259 | Spam 260 | >>> print_twice(42) 261 | 42 262 | 42 263 | >>> print_twice(math.pi) 264 | 3.14159265359 265 | 3.14159265359 266 | ``` 267 | 268 | 适用于Python内置函数的组合规则对自定义的函数也是适用的,所以我们可以把表达式作为实际参数: 269 | ```Python 270 | >>> print_twice('Spam '*4) 271 | Spam Spam Spam Spam 272 | Spam Spam Spam Spam 273 | >>> print_twice(math.cos(math.pi)) 274 | -1.0 275 | -1.0 276 | ``` 277 | 278 | 实际参数在函数被调用之前要先被运算一下,所以上面例子中作为实际参数的两个表达式都是在print_twice函数调用之前仅计算了一次。 279 | 280 | 当然了,也可以用变量做实际参数了: 281 | 282 | ```Python 283 | >>> michael = 'Eric, the half a bee.' 284 | >>> print_twice(michael) 285 | Eric, the half a bee. 286 | Eric, the half a bee. 287 | ``` 288 | 289 | 咱们传递给函数的这个实际参数是一个变量,这个变量名michael和函数内部的形式参数bruce没有任何关系。在程序主体内部参数传过去就行了,参数名字对于函数内部没有作用;比如在这个print_twice函数里面,任何传来的值,在这个print_twice函数体内,都被叫做bruce。 290 | 291 | (译者注:这里要跟大家解释一下,传递参数的时候用的是实际参数,是把这个实际参数的值交给调用的函数,函数内部接收这个值,可以命名成任意其他名字的形式参数,差不多就这么个意思了。) 292 | 293 | ## 3.8 函数内部变量和形参都是局部的 294 | 295 | 在函数内部建立一个变量,这个变量是仅在函数体内部才存在。例如: 296 | 297 | ```Python 298 | def cat_twice(part1, part2): 299 | cat = part1 + part2 300 | print_twice(cat) 301 | 302 | ``` 303 | 304 | 这个函数得到两个实参,把它们连接起来,然后调用print_twice函数来输出结果两次。 305 | 306 | ```Python 307 | >>> line1 = 'Bing tiddle ' 308 | >>> line2 = 'tiddle bang.' 309 | >>> cat_twice(line1, line2) 310 | Bing tiddle tiddle bang. 311 | Bing tiddle tiddle bang. 312 | ``` 313 | 314 | 当cat_twice运行完毕了,这个名字叫做cat的变量就销毁了。咱们再尝试着打印它一下,就会得到异常: 315 | 316 | ```Python 317 | >>> print(cat) 318 | NameError: name 'cat' is not defined 319 | ``` 320 | 321 | 形式参数也是局部起作用的。例如在print_twice这个函数之外,是不存在bruce这个变量的。 322 | 323 | (译者注:当然你可以在函数外定义一个同名变量叫做bruce,但这两个没有关系,大家可以动手自己试试,这也是作者所鼓励的一种探索思维。) 324 | 325 | ## 3.9 栈图 326 | 327 | 要追踪一个变量能在哪些位置使用,咱们就可以画个图表来实现,这种图表叫做栈图。栈图和我们之前提到的状态图有些相似,也会表征每个变量的值,不同的是栈图还会标识出每个变量所属的函数。 328 | 329 | 330 | 每个函数都用一个框架来表示。框架的边上要标明函数的名字,框内填写函数内部的形参和变量。上文中样例代码的栈图如下图3.1所示。 331 | 332 | ![Figure 3.1: Stack diagram.](./images/figure3.1.jpg) 333 | 图3.1 栈图 334 | 335 | 336 | 一个栈中的这些框也表示了函数调用的关系等等。在上面这个例子中,print_twice被cat_twice调用了两次,而cat_twice被__main__这个函数调用。__main__这个函数很特殊,属于最外层框架,也被叫做主函数。当你在所有函数之外建立一个变量的时候,这个变量就属于主函数所有。 337 | 338 | 339 | 每个形式参数都保存了所对应的实际参数的值。因此part1的值和line1一样,part2的值就和line2一样,同理可知bruce的值就和cat一样了。 340 | 341 | 342 | 如果函数调用的时候出错了,Python会打印出这个出错函数的名字,调用这个出错函数的函数名,以及调用这个调用了出错函数的函数的函数名,一直追溯到主函数。(译者注:好绕口哈。。。就是会溯源回去啦。) 343 | 344 | 345 | 例如,如果你想在print_twice这个函数中读取cat的值,就会得到一个变量名错误: 346 | 347 | ```Python 348 | Traceback (innermost last): 349 | File "test.py", line 13, in __main__ 350 | cat_twice(line1, line2) 351 | File "test.py", line 5, in cat_twice 352 | print_twice(cat) 353 | File "test.py", line 9, in print_twice 354 | print(cat) 355 | NameError: name 'cat' is not defined 356 | ``` 357 | 358 | 这个一系列的函数列表,就是一个追溯了。这回告诉你哪个程序文件出了错误,哪一行出了错误,以及当时哪些函数在运行。还会告诉你引起错误的代码所在行号。(译者注:这个简直太棒了,大家一定要留心这个功能以及出错提示,以后要用来解决很多bug呢。) 359 | 360 | 361 | 追溯中对函数顺序的排列是同栈图的方框顺序一样的。当前运行的函数会放在最底部。 362 | 363 | ## 3.10 有返回值的函数 和 无返回值的函数 364 | 365 | 咱们用过的一些函数,比如数学的函数,都会返回各种结果;也没别的好名字,就叫他们有返回值函数。其他的函数,比如print_twice,都是进行一些操作,但不返回值。那就叫做无返回值函数好了。 366 | 367 | 368 | 当你调用一个有返回值的函数的时候,一般总是要利用一下结果的;比如,你可能需要把结果赋值给某个变量,然后在表达式里面来使用一下: 369 | 370 | ```Python 371 | x = math.cos(radians) 372 | golden = (math.sqrt(5) + 1) / 2 373 | ``` 374 | 375 | 当你在交互模式调用一个函数的时候,Python会显示结果: 376 | 377 | ```Python 378 | >>> math.sqrt(5) 379 | 2.2360679774997898 380 | >>> math.sqrt(5) 381 | 2.2360679774997898 382 | ``` 383 | 384 | 如果是脚本模式,你运行一个有返回值的函数,但没有利用这个返回值,这个返回值就会永远丢失了!(译者注:只要有返回值就一定要利用!) 385 | 386 | ```Python 387 | math.sqrt(5) 388 | ``` 389 | 390 | 这个脚本计算了5的平方根,但没存储下来,也没有显示出来,所以就根本没用了。 391 | 392 | 393 | 无返回值的函数要么就是屏幕上显示出一些内容,要么就有其他的功能,但就是没有返回值。如果你把这种函数的结果返回给一个变量,就会的到特殊的值:空。 394 | 395 | ```Python 396 | >>> result = print_twice('Bing') 397 | Bing Bing 398 | >>> print(result) 399 | None 400 | ``` 401 | 402 | 这种None是空值的意思,和字符串'None'是不一样的。是一种特殊的值,并且有自己的类型。(译者注,就相当于null了。) 403 | 404 | ```Python 405 | >>> print(type(None)) 406 | 407 | ``` 408 | 我们目前为止写的函数还都是无返回值的。接下来的新的章节里面,咱们就要开始写一些有返回值的函数了。 409 | 410 | ## 3.11 为啥要用函数? 411 | 412 | 为什么要费这么多力气来把程序划分成一个个函数呢?这么麻烦值得么?原因如下: 413 | 414 | * 创建一个新的函数,你就可以把一组语句用一个名字来命名,这样你的程序读起来就清晰多了,后期维护调试也方便。 415 | 416 | * 函数的出现能够避免代码冗余,程序内的一些重复的内容就会简化了,变得更小巧。而且在后期进行修改的时候,你只要改函数中的一处地方就可以了,很方便。 417 | 418 | * 把长的程序切分成一个个函数,你就可以一步步来debug调试,每次只应对一小部分就可以,然后把它们组合起来就可以用了。 419 | 420 | * 精细设计的函数会对很多程序都有用处。一旦你写好了并且除了错,这种函数代码可以再利用。 421 | 422 | ## 3.12 调试 423 | 424 | 给程序调试是你应当掌握的最关键技能之一了。尽管调试的过程会有挫败感,也依然是最满足智力,最有挑战性,也是编程过程中最有趣的一个项目了。 425 | 426 | 某种程度上,调试像是侦探工作一样。你面对着很多线索,必须推断出导致当前结果的整个过程和事件。 427 | 428 | 调试也有点像一门实验科学。一旦你有了一个关于所出现的错误的想法,你就修改一下程序再试试看。如果你的假设是正确的,你就能够预料到修改导致的结果,这样在编程的水平上,你就上了一层台阶了,距离让程序工作起来也更近了。 429 | 430 | 如果你的推测是错误的,你必须提出新的来。就像夏洛克.福尔摩斯之处的那样,『当你剔除了所有那些不可能,剩下的无论多么荒谬,都必然是真相。』(引自柯南道尔的小说《福尔摩斯探案:四签名》) 431 | 432 | 对于一些人来说,编程和调试是一回事。也就是说,编程就是对一个程序逐渐进行调试,一直到程序按照设想工作为止。这种思想意味着你要从一段能工作的程序来起步,一点点做小修改和调试。 433 | 434 | 例如,Linux是一个有上百万行代码的操作系统,但最早它起源于Linus Torvalsd的一段小代码。这个小程序是作者用来探索Intel的80386芯片的。根据Larry Greenfield回忆,『Linus早起的项目就是很小的一个程序,这个程序能够在输出AAAA和BBBB之间进行转换。这后来就发展除了Linux了。』(引用自Linux用户参考手册beta1版) 435 | 436 | ## 3.13 Glossary 术语列表 437 | function: 438 | A named sequence of statements that performs some useful operation. Functions may or may not take arguments and may or may not produce a result. 439 | 440 | >函数:一系列有序语句的组合,有自己的名字,并且用在某些特定用途。可以要求输入参数,也可以没有参数,可以返回值,也可以没有返回值。 441 | 442 | function definition: 443 | A statement that creates a new function, specifying its name, parameters, and the statements it contains. 444 | 445 | >函数定义:创建新函数的语句,确定函数的名字,形式参数,以及函数内部的语句。 446 | 447 | function object: 448 | A value created by a function definition. The name of the function is a variable that refers to a function object. 449 | 450 | >函数对象:由函数定义所创建的值,函数名字指代了这一函数对象。 451 | 452 | header: 453 | The first line of a function definition. 454 | 455 | >函数头:函数定义的第一行。 456 | 457 | body: 458 | The sequence of statements inside a function definition. 459 | 460 | >函数体:函数定义内部的一系列有序语句。 461 | 462 | parameter: 463 | A name used inside a function to refer to the value passed as an argument. 464 | 465 | >形式参数:用来在函数内部接收实际参数传来的值,并被函数在函数内部使用。 466 | 467 | function call: 468 | A statement that runs a function. It consists of the function name followed by an argument list in parentheses. 469 | 470 | >函数调用:运行某个函数的语句。包括了函数的名字以及括号,括号内放函数需要的实际参数。 471 | 472 | argument: 473 | A value provided to a function when the function is called. This value is assigned to the corresponding parameter in the function. 474 | 475 | >实际参数:当函数被调用的时候,提供给函数的值。这个值会被函数接收,赋给函数内部的形式参数。 476 | 477 | local variable: 478 | A variable defined inside a function. A local variable can only be used inside its function. 479 | 480 | >局部变量:函数体内定义的变量。局部变量只在函数内部有效。 481 | 482 | return value: 483 | The result of a function. If a function call is used as an expression, the return value is the value of the expression. 484 | 485 | >返回值:函数返回的结果。如果一个函数调用被用作了表达式,这个返回值就是这个表达式所代表的值。 486 | 487 | fruitful function: 488 | A function that returns a value. 489 | 490 | >有返回值函数:返回一个值作为返回值的函数。 491 | 492 | void function: 493 | A function that always returns None. 494 | 495 | >无返回值函数:不返回值,只返回一个空None的函数。 496 | 497 | None: 498 | A special value returned by void functions. 499 | 500 | >空值:无返回值函数所返回的一种特殊的值。 501 | 502 | module: 503 | A file that contains a collection of related functions and other definitions. 504 | 505 | >模块:包含一系列相关函数以及其他一些定义的文件。 506 | 507 | import statement: 508 | A statement that reads a module file and creates a module object. 509 | 510 | >导入语句:读取模块并且创建一个模块对象的语句。 511 | 512 | module object: 513 | A value created by an import statement that provides access to the values defined in a module. 514 | 515 | >模块对象:导入语句创建的一个值,允许访问模块所定义的值。 516 | 517 | dot notation: 518 | The syntax for calling a function in another module by specifying the module name followed by a dot (period) and the function name. 519 | 520 | >点符号:调用某一个模块的某一函数的语法形式,就是模块名后加一个点,也就是英文的句号,再加函数名。 521 | 522 | composition: 523 | Using an expression as part of a larger expression, or a statement as part of a larger statement. 524 | 525 | >组合:把表达式作为更大的表达式的一部分,或者把语句作为更大语句的一部分。 526 | 527 | flow of execution: 528 | The order statements run in. 529 | 530 | >运行流程:语句运行的先后次序。 531 | 532 | stack diagram: 533 | A graphical representation of a stack of functions, their variables, and the values they refer to. 534 | 535 | >栈图:对函数关系、变量内容及结构的图形化表示。 536 | 537 | frame: 538 | A box in a stack diagram that represents a function call. It contains the local variables and parameters of the function. 539 | 540 | >框架:栈图中的方框,表示了一次函数调用。包括函数的局部变量和形式参数。 541 | 542 | traceback: 543 | A list of the functions that are executing, printed when an exception occurs. 544 | 545 | >追踪:对运行中函数的列表,当有异常的时候就会输出。 546 | 547 | ## 3.14 练习 548 | ## # 练习1 549 | 550 | 写一个名叫right_justify的函数,形式参数是名为s的字符串,将字符串打印,前面流出足够的空格,让字符串最后一个字幕在第70列显示。 551 | 552 | ```Python 553 | >>> right_justify('monty') monty 554 | ``` 555 | 556 | 提示:使用字符拼接和重复来实现。另外Python还提供了内置的名字叫做len的函数,可以返回一个字符串的长度,比如len('monty')的值就是5了。 557 | 558 | ## # 练习2 559 | 560 | 你可以把一个函数对象作为一个值赋给一个变量或者作为一个实际参数来传递给其他函数。比如,do_twice就是一个把其他函数对象当做参数的函数,它的功能是调用对象函数两次: 561 | 562 | ```Python 563 | def do_twice(f): 564 | f() 565 | f() 566 | ``` 567 | 568 | 下面是另一个例子,这里用了do_twice来调用一个名叫print_spam的函数两次。 569 | 570 | ```Python 571 | def print_spam(): 572 | print('spam') 573 | do_twice(print_spam) 574 | ``` 575 | 1.把上面的例子写成脚本然后试一下。 576 | 577 | 2.修改一下do_twice这个函数,让它接收两个实际参数,一个是函数对象,一个是值,调用对象函数两次,并且赋这个值给对象函数作为实际参数。 578 | 579 | 3.把print_twice这个函数的定义复制到你的脚本里面,去本章开头找一下这个例子哈。 580 | 581 | 4.用修改过的这个do_twice来调用print_twice两次,用字符串『spam』传递过去作为实际参数。 582 | 583 | 5.定义一个新的函数,名字叫做do_four,使用一个函数对象和一个值作为实际参数,调用这个对象函数四次,传递这个值作过去为对象函数的一个形式参数。这个函数体内只要有两个语句就够了,而不是四个。 584 | 585 | [样例代码](http://thinkpython2.com/code/do_four.py): 586 | 587 | ## # 3 练习三 588 | 589 | 注意:这个练习应该只用咱们目前学习过的语句和其他功能来实现。 590 | 591 | 1.写一个函数,输出如下: 592 | 593 | ![](http://7xnq2o.com1.z0.glb.clouddn.com/ThinkPythonExcise3.14.png) 594 | 595 | 提示:要一次打印超过一行,可以用逗号分隔一下就能换行了。如下所示: 596 | 597 | ```Python 598 | print('+', '-') 599 | ``` 600 | 601 | 默认情况下,print会打印到下一行,你可以手动覆盖掉这个行为,在末尾输出一个空格就可以了: 602 | 603 | ```Python 604 | print('+', end=' ') 605 | print('-') 606 | ``` 607 | 608 | 上面的语句输出结果就是:'+ -'。 609 | 610 | 611 | 没有参数的print语句会把当前的行结束,去下一行。 612 | 613 | 2.写一个四行四列的小网格绘制的程序。 614 | 615 | [样例](http://thinkpython2.com/code/grid.py) 616 | 617 | 此练习基于Oualline的书《实践C语言编程》第三版,O'Reilly出版社,1997年版 618 | -------------------------------------------------------------------------------- /chapter4.md: -------------------------------------------------------------------------------- 1 | # 第四章 案例学习:交互设计 2 | 3 | 本章会提供一个案例,用于展示如何却设计一些共同工作的函数。 4 | 5 | 本章介绍了小乌龟这个模块,这允许你用小龟的图形功能来制作一些图形。乌龟模块在大部分的Python中都有安装,不过如果你在线使用PythnAnywhere,你就无法运行这些乌龟样例了(至少我写这本教材的时候还不行)。 6 | 7 | (译者注:都学到第四章了,你还不本地安装个Python也太说不过去了吧。) 8 | 9 | 如果你已经安装了Python在你的电脑上,你就能运行这些例子了。没安装的话呢,这就是安装的好时机了呗。我已经把相关介绍放到网页上面了,[点击访问](http://tinyurl.com/thinkpython2e)。 10 | 11 | 12 | 本章代码样例可以点击[此链接](http://thinkpython2.com/code/polygon.py)来下载了。 13 | 14 | ## 4.1 乌龟模块 15 | 16 | 要检查你是不是已经安装了这个乌龟模块,你要打开Python解释器来输入如下内容: 17 | 18 | ```Python 19 | >>> import turtle 20 | >>> bob = turtle.Turtle() 21 | ``` 22 | 23 | 运行上述例子的时候,应该就能新建一个小窗口,还有个小箭头象征小乌龟。如果有的话就对了,把窗口关掉吧先。 24 | 25 | 建立一个叫做mypolygon.py的文件,在里面输入如下内容: 26 | 27 | ```Python 28 | import turtle 29 | bob = turtle.Turtle() 30 | print(bob) 31 | turtle.mainloop() 32 | ``` 33 | 这个小乌龟模块(记着是小写的t)提供了一个叫做Turtle(注意这里是大写的,大小写要去分!)的函数,这个函数会创建一个Turtle对象,我们把它赋值给bob这个变量。打印一下bob就能显示如下内容: 34 | 35 | ```Bash 36 | 37 | ``` 38 | 39 | 这就意味着bob已经指向了模块turtle中所定义的Turtle类的一个对象。 40 | 41 | 42 | mainloop这个函数是告诉窗口等用户来做些事情,当然本次尝试的情况下用户也就是关闭窗口而已了。 43 | 44 | 一旦你创建了一个Trutle,你就可以调用一些方法让他在窗口中移动。方法跟函数有点相似,但语法的使用稍微不太一样。比如你可以让小乌龟往前走: 45 | 46 | ```Python 47 | bob.fd(100) 48 | ``` 49 | fd这个方法,是turtle类这个叫做bob的对象所包含的。调用这个方法就像是做出一个请求一样:你再让bob向前移动。fd这个方法的参数是像素数距离,所以实际的大小依赖于你显示器的情况了。 50 | 51 | 52 | Turtle对象中还有一些其他方法,比如bk是后退,lt是左转,rt是右转。lt和rt用偏转角度做参数。 53 | 54 | 55 | 另外,每个Turtle都相当于带着笔,可以落下或者抬起;如果笔落下了,Turtle移动的时候就会留下轨迹了。抬笔落笔的方法缩写粉笔嗯是pu和pd。 56 | 57 | 58 | 画一个直角,就要把下面这些线加到程序里面(当然要先创建一个bob并且在此之前运行mainloop): 59 | 60 | ```Python 61 | bob.fd(100) 62 | bob.lt(90) 63 | bob.fd(100) 64 | ``` 65 | 66 | 运行这个程序,你就能看到bob先向东再往北,后面就留下了两根互相垂直的线段了。 67 | 68 | 69 | 现在修改一下程序,去画一个正方形。这个程序运行不好的话就不要继续后面的章节! 70 | 71 | ## 4.2 简单的重复 72 | 73 | 你估计会写出如下的内容: 74 | 75 | ```Python 76 | bob.fd(100) 77 | bob.lt(90) 78 | bob.fd(100) 79 | bob.lt(90) 80 | bob.fd(100) 81 | bob.lt(90) 82 | bob.fd(100) 83 | ``` 84 | 85 | 上面这个太麻烦了,咱们可以用一个for语句来让这个过程更简洁。把下面的代码添加到mypolygon.py中然后运行一下: 86 | 87 | ```Python 88 | for i in range(4): 89 | print('Hello!') 90 | ``` 91 | 92 | 你将会看到这样的输出: 93 | 94 | ```Bash 95 | Hello! 96 | Hello! 97 | Hello! 98 | Hello! 99 | ``` 100 | 101 | 这就是for语句的最简单的一种应用;以后我们会看到更多。不过当前这种简单的足够你来重构一下你的正方形绘制程序了。不达目的不罢休,不要跳过困难哈,一定要编写出来这个再进行后面的内容。 102 | 103 | 104 | 这就是一个用for语句来画正方形的语句: 105 | 106 | ```Python 107 | for i in range(4): 108 | bob.fd(100) 109 | bob.lt(90) 110 | ``` 111 | 112 | for语句的语法跟函数定义有点相似。有一个头部,头部的结尾要用冒号,然后还有一个缩进的循环体。循环体可以包含任意多的语句。 113 | 114 | for语句也被叫做循环,因为运行流程会重复执行循环体。在本节的例子中,循环进行了四次。 115 | 116 | 这次的正方形绘制代码实际上和之前的少有不同了,因为在画完了最后一个边之后,多了一次转向。多出来的这部分需要消耗额外的时间,但简化了下次我们来循环进行绘制的过程。这个版本的代码也有一个额外的效果:让小乌龟回到起点,朝着初始方向。 117 | 118 | ## 4.3 练习 119 | 120 | 下面是一系列使用TurtleWorld的练习。主要就是比较有意思,不过也有一些训练的作用。你做这些练习的时候,一定要注意考虑这些训练的作用。 121 | 122 | 练习后面是有一些样例的解决方案的,所以你要做完了再往后看,至少你得试试,不会做了看看答案也行哈。 123 | 124 | 1.写一个函数叫做square(译者注:就是正方形的意思),有一个名叫t的参数,这个t是一个turtle。用这个turtle来画一个正方形。写一个函数调用,把bob作为参数传递给square,然后再运行这个程序。 125 | 126 | 2.给这个square函数再加一个参数,叫做length(译者注:长度)。把函数体修改一下,让长度length赋值给各个边的长度,然后修改一下调用函数的代码,再提供一个这个对应长度的参数。再次运行一下,用一系列不同的长度值来测试一下你的程序。 127 | 128 | 3.复制一下square这个函数,把名字改成polygon(译者注:意思为多边形)。另外添加一个参数叫做n,然后修改函数体,让函数实现画一个正n边的多边形。提示:正n多边形的外角为360/n度。 129 | 130 | 4.在写一个叫做circle(译者注:圆)的函数,也用一个turtle类的对象t,以及一个半径r,作为参数,画一个近似的圆,通过调用polygon函数来近似实现,用适当的边长和边数。用不同的半径值来测试一下你的函数。 131 | 132 | 提示:算出圆的周长,确保边长乘以边数的值(近似)等于圆周长。 133 | 134 | 5.在circle基础上做一个叫做arc的函数,在circle的基础上添加一个angle(译者注:角度)变量,用这个角度值来确定画多大的一个圆弧。用度做单位,当angle等于360度的时候,arc函数就应当画出一个整团了。 135 | 136 | ## 4.4 封装 137 | 138 | 第一个练习让你把正方形绘制的代码定义到一个函数里面,然后调用这个函数,传入一个turtle对象作为参数。下面就是个例子了: 139 | 140 | ```Python 141 | def square(t): 142 | for i in range(4): 143 | t.fd(100) 144 | t.lt(90) 145 | square(bob) 146 | ``` 147 | 148 | 在最内部的语句里面,fd和lt缩进了两次,这个意思是他们是for循环的循环体内部成员,而for循环本身缩进了一次,说明for语句被包含在函数的定义当中。接下来的那行square(bob),紧靠左侧,没有缩进,这说明for循环和函数定义都结束了。 149 | 150 | 在函数体内部,t所指代的就是小乌龟bob,因此让t来左转九十度的效果完全等同于让bob来左转九十度。本文中没有把形式参数的名字设置成bob,这是为啥呢?是因为用t可以指代任意一个小乌龟,不仅仅是bob,所以你就能再创建另一个小乌龟,把它传递给square这个函数作为实际参数: 151 | 152 | ```Python 153 | alice = Turtle() 154 | square(alice) 155 | ``` 156 | 157 | 用函数的形式把一段代码包装起来,叫做封装。这样有一个好处,就是给代码起了个名字,有类似文档说明的功能,更好理解了。另外一个好处是下次重复使用这段代码的时候,再次调用函数就可以了,这比复制粘贴函数体可方便多了。 158 | 159 | ## 4.5 泛化 160 | 161 | 下一步就是给square函数添加一个长度参数了。下面是样例: 162 | 163 | ```Python 164 | def square(t, length): 165 | for i in range(4): 166 | t.fd(length) 167 | t.lt(90) 168 | square(bob, 100) 169 | ``` 170 | 171 | 给函数添加参数,就叫做泛化,因为者可以让函数的功能更广泛:在之前的版本中,square这个函数画出来的正方形总是一个尺寸的;在这个新版本里面,可以自定义边长了。 172 | 173 | 下一步也还是泛化。这次就是不光要画正方形了,要画一个多边形,可以指定边数的。下面是样例: 174 | 175 | ```Python 176 | def polygon(t, n, length): 177 | angle = 360 / n 178 | for i in range(n): 179 | t.fd(length) 180 | t.lt(angle) 181 | polygon(bob, 7, 70) 182 | ``` 183 | 184 | 这个例子画了一个每个边长度都为70像素的七边形。 185 | 186 | 如果你用Python2的话,角度可能因为整除而导致的偏差。简单的解决方法就是用360.0来除以n而不是用360,这就是用浮点数替代了原来的整形,结果就是一个浮点数了。 187 | 188 | 189 | 当一个函数有超过一个数据参数的时候,很容易忘掉这些参数都是什么,或者忘掉他们的顺序。为了避免这个情况,可以把形式参数的名字包含在一个实际参数列表中: 190 | 191 | ```Python 192 | polygon(bob, n=7, length=70) 193 | ``` 194 | 195 | 这些列表叫做关键参数列表,因为他们把形式参数的名字作为关键词包含了进来。(注意区别这里的关键词可不是Python语言的关键词哈!这里就是字面意思,很关键的词。) 196 | 197 | 这种语法结构让程序更容易被人读懂。也能提醒实际参数和形式参数的使用过程:调用一个函数的时候,把实际参数的值赋给了形式参数。 198 | 199 | ## 4.6 接口设计 200 | 201 | 下一步就是写circle这个函数了,需要半径r作为一个参数。下面是一个简单的样例,使用polygon函数来画一个50边形,来接近一个圆: 202 | 203 | ```Python 204 | import math 205 | def circle(t, r): 206 | circumference = 2 * math.pi * r 207 | n = 50 208 | length = circumference / n 209 | polygon(t, n, length) 210 | ``` 211 | 212 | 第一行计算了圆的周长,使用2乘以圆周率再乘以半径r。这个计算用到了圆周率,所以要导入math模块。通常都要把导入语句放到整个脚本的开头。 213 | 214 | n是我们用来逼近一个圆所用的线段数量,所以length就是每一个线段的长度了。polygon画一个50边的多边形,来近似做一个半径为r的圆。 215 | 216 | 这种方案的一个局限性就是n是常数,就意味着对于一些大尺寸的圆,线段数目就太多了,而对小的圆,又浪费了很多小线段。解决的方法就是进一步扩展函数,让函数把n也作为一个参数。这就亏让用户(调用circle函数的任何人)有更多决定权,可以控制所用的线段数量,当然,接口就不那么简洁了。 217 | 218 | 函数的接口就是关于它如何工作的一个概述:都有什么变量?函数实现什么功能?以及返回值是什么?允许调用者随意操作而不用处理一些无关紧要的细节,这种函数接口就是简洁的。 219 | 220 | 在本节的例子中,r包含于接口内,因为要用它来确定所画圆的大小。n就不那么合适了,因为它是用来处理如何具体绘制一个圆的。 221 | 222 | 与其让接口复杂冗余,更好的思路是让n根据周长来自适应一个合适的值: 223 | 224 | ```Python 225 | def circle(t, r): 226 | circumference = 2 * math.pi * r 227 | n = int(circumference / 3) + 1 228 | length = circumference / n 229 | polygon(t, n, length) 230 | ``` 231 | 232 | 现在线段个数就是周长的三分之一了,因此每段线段的长度近似为3,这个大小可以让圆看着不错,也对任意大小的圆都适用了。 233 | 234 | ## 4.7 重构 235 | 236 | 当我写circle这个函数的时候,我能利用多边形函数polygon是因为一个足够多边的多边形和圆很接近。但圆弧就不太适合这个思路了;我们不能用多边形或者圆来画一个圆弧。 237 | 238 | 239 | 一个替代的方法就是把polygon修改一下,转换成圆弧。结果大概如下所示: 240 | 241 | ```Python 242 | def arc(t, r, angle): 243 | arc_length = 2 * math.pi * r * angle / 360 244 | n = int(arc_length / 3) + 1 245 | step_length = arc_length / n 246 | step_angle = angle / n 247 | for i in range(n): 248 | t.fd(step_length) 249 | t.lt(step_angle) 250 | ``` 251 | 252 | 这个函数的后半段看着和多边形那个还挺像的,但必须修改一下接口才能重利用多边形的代码。我们在多边形函数上增加angle(角度)作为第三个参数,但继续叫多边形就不太合适了,因为不闭合啊!所以就改名叫它多段线polyline: 253 | 254 | ```Python 255 | def polyline(t, n, length, angle): 256 | for i in range(n): 257 | t.fd(length) 258 | t.lt(angle) 259 | ``` 260 | 261 | 现在就可以用多段线polyline来重写多边形polygon和圆弧arc: 262 | 263 | ```Python 264 | def polygon(t, n, length): 265 | angle = 360.0 / n 266 | polyline(t, n, length, angle) 267 | def arc(t, r, angle): 268 | arc_length = 2 * math.pi * r * angle / 360 269 | n = int(arc_length / 3) + 1 270 | step_length = arc_length / n 271 | step_angle = float(angle) / n 272 | polyline(t, n, step_length, step_angle) 273 | ``` 274 | 275 | 最终,咱们就可以用圆弧arc来重写circle的实现了: 276 | 277 | ```Python 278 | def circle(t, r): 279 | arc(t, r, 360) 280 | ``` 281 | 282 | 这个过程中,改进了接口设计,增强了代码再利用,这就叫做重构。在本节的这个例子中,我们先是注意到圆弧arc和多边形polygon有相似的代码,所以我们把他们都用多段线polyline来实现。 283 | 284 | 如果我们事先进行了计划,估计就会先写出多段线函数polyline,然后就不用重构了,但大家在开始一个项目之前往往不一定了解的那么清楚。一旦开始编码了,你就逐渐更理解其中的问题了。有时候重构就意味着你已经学到了新的内容了。 285 | 286 | ## 4.8 开发计划 287 | 288 | 开发计划是写程序的一系列过程。我们本章所用的就是『封装-泛化』的模式。这一过程的步骤如下: 289 | 290 | 1. 开始写一个特别小的程序,没有函数定义。 291 | 292 | 2. 一旦有你的程序能用了,确定一下实现功能的这部分有练习的语句,封装成函数,并命名一下。 293 | 294 | 3. 通过逐步给这个函数增加参数的方式来泛化。 295 | 296 | 4. 重复1-3步骤,一直到你有了一系列能工作的函数为止。把函数复制粘贴出来,避免重复输入或者修改了。 297 | 298 | 5. 看看是不是有通过重构来改进函数的可能。比如,假设你在一些地方看到了相似的代码,就可以把这部分代码做成一个函数。 299 | 300 | 这个模式有一些缺点,我们后续会看到一些替代的方式,但这个模式是很有用的,尤其对耐饿实现不值得怎么去把程序分成多个函数的情况。 301 | 302 | ## 4.9 文档字符串 303 | 304 | 文档字符串是指:在函数开头部位,解释函数的交互接口的字符串,doc是文档documentation的缩写。下面是一个例子: 305 | 306 | ```Python 307 | def polyline(t, n, length, angle): 308 | """ 309 | Draws n line segments with the given length and angle (in degrees) between them. 310 | t is a turtle. """ 311 | for i in range(n): 312 | t.fd(length) 313 | t.lt(angle) 314 | ``` 315 | 316 | 一般情况下,所有文档字符串都是三重引用字符串,也被叫做多行字符串,因为三重的单引号表示允许这个字符串是多行的。 317 | 318 | 这些文字很简洁,但都包含了一些关键的信息,这些信息对于函数使用者来说至关重要。这些信息简要解释了函数的用途(不会说细节,也不会说如何实现)。文档解释了每个参数对函数行为的影响,以及各自的类型(一般在不是显而易见的情况下就给解释了)。 319 | 320 | 写这种文档,对交互接口的设计来说,是至关重要的。设计良好的交互接口应该很容易解释明白;如果你的函数有一个特别不好解释了,估计这个函数的交互设计还存在需要改进的地方。 321 | 322 | ## 4.10 调试 323 | 324 | 一个交互接口,就像是函数和调用者的一个中间人。调用者提供特定的参数,函数完成特定的任务。 325 | 326 | 例如,polyline这个多段线函数,需要四个实际参数:t必须是一个Turtle小乌龟;n(边数)必须是一个整形;length(长度)应该是一个正数;angle(角度)必须是一个以度为单位的角度值。 327 | 328 | 这些要求叫做『前置条件』,因为要在函数开始运行之前就要实现才行。相应的在函数的结尾那里的条件叫『后置条件』。后置条件包含函数的预期效果(如画线段)和其他作用(如移动海龟或进行其他改动)。 329 | 330 | 前置条件是准备给函数调用者的。如果调用者违背了(妥当标注的)前置条件,然后函数不能正常工作,这个bug就会反馈在函数调用者上,而不是函数本身。 331 | 332 | 如果前置条件得到了满足,而后置条件未能满足,这个bug就是函数的了。所以如果你的前后置条件都弄清晰,对调试很有帮助。 333 | 334 | ## 4.11 Glossary 术语列表 335 | method: 336 | A function that is associated with an object and called using dot notation. 337 | 338 | >方法:某个类中一个对象所具有的函数,用点连接来进行调用。 339 | 340 | loop: 341 | A part of a program that can run repeatedly. 342 | 343 | >循环:程序中重复运行的一部分。 344 | 345 | encapsulation: 346 | The process of transforming a sequence of statements into a function definition. 347 | 348 | >封装:把一系列相关的语句整理定义成一个函数的过程。 349 | 350 | generalization: 351 | The process of replacing something unnecessarily specific (like a number) with something appropriately general (like a variable or parameter). 352 | 353 | >泛化:把一些不必要的内容用更广泛通用的内容来替换掉的过程,比如把一个数字替换成了一个变量或者参数。 354 | 355 | keyword argument: 356 | An argument that includes the name of the parameter as a “keyword”. 357 | 358 | >关键词参数:一种特殊的实际参数,把形式参数的名字作为关键词包含在内。 359 | 360 | interface: 361 | A description of how to use a function, including the name and descriptions of the arguments and return value. 362 | 363 | >交互接口:对如何使用一个函数的描述,包括了函数名,以及对实际参数和返回值的描述。 364 | 365 | refactoring: 366 | The process of modifying a working program to improve function interfaces and other qualities of the code. 367 | 368 | >重构:对一份能工作的程序进行修改,改进函数交互接口以及提高代码其他方面质量的过程。 369 | 370 | development plan: 371 | A process for writing programs. 372 | 373 | >开发计划:写程序的过程。 374 | 375 | docstring: 376 | A string that appears at the top of a function definition to document the function’s interface. 377 | 378 | >文档字符串:一个在函数定义的顶部的字符串,讲解函数的交互接口。 379 | 380 | precondition: 381 | A requirement that should be satisfied by the caller before a function starts. 382 | 383 | >前置条件:函数开始之前,调用者应当满足的要求。 384 | 385 | postcondition: 386 | A requirement that should be satisfied by the function before it ends. 387 | 388 | >后置条件:函数结束之前应该满足的一些要求。 389 | 390 | ## 4.12 练习 391 | ## # 练习1 392 | 点击下面这个链接[下载代码](http://thinkpython2.com/code/polygon.py)。 393 | 394 | 1. 画一个栈图,表明运行函数circle(bob,radius)时候程序的状态。你可以手算一下,或者把输出语句加到代码上。 395 | 396 | 2. 4.7小节中的那个版本的arc函数并不太精确,因为对圆进行线性逼近总会超过真实情况。结果就是小乌龟总会距离正确位置偏离一些像素。我的样例给出了一种降低这种误差程度的方法。阅读一下代码,看你能不能理解。如果你画一个图标,也许就能明白代码是怎么工作的了。 397 | 398 | ________________________________________ 399 | ![Turtle flowers and Turtle pies](./images/figure4.1-4.2.jpg) 400 | Figure 4.1: Turtle flowers. 401 | Figure 4.2: Turtle pies. 402 | ________________________________________ 403 | ## # 练习2 404 | 写一系列的合适的函数组合,画出图4.1所示的花图案。 405 | 406 | [样例]( http://thinkpython2.com/code/flower.py) 407 | [以及此链接文件](http://thinkpython2.com/code/polygon.py) 408 | ________________________________________ 409 | 410 | ________________________________________ 411 | ## # 练习3 412 | 413 | 写一系列的合适的函数组合,画出图4.2所示的形状。 414 | 415 | [样例](http://thinkpython2.com/code/pie.py) 416 | 417 | ## # 练习4 418 | 419 | 字母表当中的字母都可以用一定数量的基本元素来构建,比如竖直或者水平的线条,以及一些曲线。设计一个能用最小数量的基本元素画出来的字母表,然后写个函数来画字母出来。 420 | 421 | 你应当为没一个字母写一个函数,名字就比如draw_a,draw_b等等,然后把你的函数放到一个叫做letters.py的文件中。你可以从这个[链接](http://thinkpython2.com/code/typewriter.py) 下载一个乌龟打字机来帮你检测一下代码。 422 | 423 | 你可以参考这里的[样例](http://thinkpython2.com/code/letters.py);同时还需要[这些](http://thinkpython2.com/code/polygon.py)。 424 | 425 | ## 练习5 426 | 去[Wiki百科](http://en.wikipedia.org/wiki/Spiral)看一下螺旋线的相关内容;然后写个程序来画阿基米德曲线(曲线中的一种)。[样例](http://thinkpython2.com/code/spiral.py) 427 | 428 | -------------------------------------------------------------------------------- /chapter5.md: -------------------------------------------------------------------------------- 1 | # 第五章 条件循环 2 | 3 | 本章的主题是if语句,就是条件判断,会对应程序的不同状态来执行不同的代码。但首先我要介绍两种新的运算符:floor(地板除法,舍弃小数位)和modulus(求模,取余数) 4 | 5 | ## 5.1 地板除和求模 6 | 7 | floor除法,中文有一种翻译是地板除法,挺难听,不过凑活了,运算符是两个右斜杠://,与传统除法不同,地板除法会把运算结果的小数位舍弃,返回整值。例如,加入一部电影的时间长度是105分钟。你可能想要知道这部电影用小时来计算是多长。传统的除法运算如下,会返回一个浮点小数: 8 | 9 | ```Python 10 | >>> minutes = 105 11 | >>> minutes / 60 12 | 1.75 13 | ``` 14 | 15 | 不过一般咱们不写有小数的小时数。地板除法返回的就是整的小时数,舍弃掉小数位: 16 | 17 | ```Python 18 | >>> minutes = 105 19 | >>> hours = minutes // 60 20 | >>> hours 21 | 1 22 | ``` 23 | 24 | 想要知道舍弃那部分的长度,可以用分钟数减去这么一个小时,然后剩下的分钟数就是了: 25 | 26 | ```Python 27 | >>> remainder = minutes - hours * 60 28 | >>> remainder 29 | 45 30 | ``` 31 | 另外一个方法就是使用求模运算符了,百分号%就是了,求模运算就是求余数,会把两个数相除然后返回余数。 32 | 33 | ```Python 34 | >>> remainder = minutes % 60 35 | >>> remainder 36 | 45 37 | ``` 38 | 求模运算符的作用远不止如此。比如你可以用求模来判断一个数能否被另一个数整除——比如x%y如果等于0了,那就是意味着x能被y整除了。 39 | 40 | 另外你也可以从一个数上取最右侧的一位或多位数字。比如,x%10就会得出x最右边的数字,也就是x的个位数字。同样的道理,用x%100得到的就是右面两位数字了。 41 | 42 | 如果你用Python2的话,除法是不一样的。在两边都是整形的时候,常规除法运算符/就会进行地板除法,而两边只要有一侧是浮点数就会进行浮点除法。 43 | 44 | ## 5.2 布尔表达式 45 | 46 | 布尔表达式是一种非对即错的表达式,只有这么两个值,true(真)或者false(假)。下面的例子都用了双等号运算符,这个运算符会判断两边的值是否相等,相等就是True,不相等就是False: 47 | 48 | ```Python 49 | >>> 5 == 5 50 | True 51 | >>> 5 == 6 52 | False 53 | ``` 54 | True和False都是特殊的值,属于bool布尔类型;它们俩不是字符串: 55 | 56 | ```Python 57 | >>> type(True) 58 | 59 | >>> type(False) 60 | 61 | ``` 62 | 双等号运算符是关系运算符的一种,其他关系运算符如下: 63 | 64 | ```Python 65 | x != y # x is not equal to y 二者相等 66 | x > y # x is greater than y 前者更大 67 | x > y # x is greater than y 前者更大 68 | x < y # x is less than y 前者更小 69 | x >= y # x is greater than or equal to y 大于等于 70 | x >= y # x is greater than or equal to y 大于等于 71 | x <= y # x is less than or equal to y 小于等于 72 | ``` 73 | 74 | 虽然这些运算符你可能很熟悉了,但一定要注意Python里面的符号和数学上的符号有一定区别。常见的错误就是混淆了等号=和双等号==。一定要记住单等号=是一个赋值运算符,而双等号==是关系运算符。另外要注意就是大于等于或者小于等于都是等号放到大于号或者小于号的后面,顺序别弄反。 75 | 76 | ## 5.3 逻辑运算符 77 | 78 | 逻辑运算符有三种:且,或以及非。这三种运算符的意思和字面意思差不多。比如x>0且x<10,仅当x在0到10之间的时候才为真。 79 | 80 | 81 | n%2 == 0 或 n%3 == 0,只要条件有一个成立就是真,就是说这个可以被2或3整除就行了。 82 | 83 | 84 | 最后说这个非运算,是针对布尔表达式的,非(x>y)为真,那么x>y就是假的,意味着x小于等于y。 85 | 86 | 严格来说,逻辑运算符的运算对象应该必须是布尔表达式,不过Python就不太严格。任何非零变量都会被认为是真: 87 | 88 | ```Python 89 | >>> 42 and True 90 | True 91 | ``` 92 | 这种灵活性特别有用,不过有的情况下也容易引起混淆。建议你尽量不要这样用,除非你很熟练了。 93 | 94 | ## 5.4 条件执行 95 | 96 | 有用的程序必然要有条件检查判断的功能,根据不同条件要让程序有相应的行为。条件语句就让咱们能够实现这种判断。最简单的就是if语句了: 97 | 98 | ```Python 99 | if x > 0: 100 | print('x is positive') 101 | ``` 102 | 103 | if后面的布尔表达式就叫做条件。如果条件为真,随后缩进的语句就运行。如果条件为假,就不运行。 104 | 105 | 106 | if语句与函数定义的结构基本一样:一个头部,后面跟着缩进的语句。这样的语句叫做复合语句。 107 | 108 | 109 | 、复合语句中语句体内的语句数量是不限制的,但至少要有一个。有的时候会遇到一个语句体内不放语句的情况,比如空出来用来后续补充。这种情况下,你就可以用pass语句,就是啥也不会做的。 110 | 111 | ```Python 112 | if x < 0: 113 | pass # TODO: need to handle negative values! 114 | ``` 115 | ## 5.5 选择执行 116 | 117 | if语句的第二种形式就是『选择执行』,这种情况下会存在两种备选的语句,根据条件来判断执行哪一个。语法如下所示: 118 | 119 | ```Python 120 | if x % 2 == 0: 121 | print('x is even') 122 | else: 123 | print('x is odd') 124 | ``` 125 | I 126 | 如果x除以2的余数为0,x就是一个偶数了,程序就会显示对应的信息。如果条件不成立,那就运行第二条语句。这里条件非真即假,只有两个选择。这些选择也叫『分支』,因为在运行流程上产生了不同的分支。 127 | 128 | ## 5.6 链式条件 129 | 130 | 有时我们要面对的可能性不只有两种,需要更多的分支。这时候可以使用连锁条件来实现: 131 | 132 | ```Python 133 | if x < y: 134 | print('x is less than y') 135 | elif x > y: 136 | print('x is greater than y') 137 | else: 138 | print('x and y are equal') 139 | ``` 140 | 141 | elif是『else if』的缩写。这回也还是只会有一个分支的语句会被运行。elif语句的数量是无限制的。如果有else语句的话,这个else语句必须放到整个条件链的末尾,不过else语句并不是必须有的。 142 | 143 | ```Python 144 | if choice == 'a': 145 | draw_a() 146 | elif choice == 'b': 147 | draw_b() 148 | elif choice == 'c': 149 | draw_c() 150 | ``` 151 | 152 | 每一个条件都会依次被检查。如果第一个是假,下一个就会被检查,依此类推。如果有一个为真了,相应的分支语句就运行了,这些条件判断的语句就都结束了。如果有一个以上的条件为真,只有先出现的为真的条件所对应的分支语句会运行。 153 | 154 | ## 5.7 嵌套条件 155 | 156 | 一个条件判断也可以嵌套在另一个条件判断内。上一节的例子可以改写成如下: 157 | 158 | ```Python 159 | if x == y: 160 | print('x and y are equal') 161 | else: 162 | if x < y: 163 | print('x is less than y') 164 | else: 165 | print('x is greater than y') 166 | ``` 167 | 168 | 外部的条件判断包含两个分支。第一个分支只有一个简单的语句。第二个分支包含了另外一重条件判断,这个内部条件判断有两个分支。这两个分支都是简单的语句,他们的位置也可以继续放条件判断语句的。 169 | 170 | 171 | 虽然语句的缩进会让代码结构看着比较清晰明显,但嵌套的条件语句读起来还是有点难度。所以建议你如果可以的话,尽量避免使用嵌套的条件判断。 172 | 173 | 174 | 逻辑运算符有时候对简化嵌套条件判断很有用。比如下面这个代码就能改写成更简单的版本: 175 | 176 | ```Python 177 | if 0 < x: 178 | if x < 10: 179 | print('x is a positive single-digit number.') 180 | ``` 181 | 182 | 上面的例子中,只有两个条件都满足了才会运行print语句,所以就用逻辑运算符来实现同样的效果即可: 183 | 184 | ```Python 185 | if 0 < x and x < 10: 186 | print('x is a positive single-digit number.') 187 | ``` 188 | 189 | 这种条件下,Python提供了更简洁的表达方法: 190 | 191 | ```Python 192 | if 0 < x < 10: 193 | print('x is a positive single-digit number.') 194 | ``` 195 | (译者注:Python的这种友善度远远超过了C和C++,这也是为何我一直建议国内高校用Python取代C++来给本科生和研究生做编程入门课程。) 196 | ## 5.8 递归运算 197 | 198 | 一个函数可以去调用另一个函数;函数来调用自己也是允许的。这就是递归,是程序最神奇的功能之一,现在可能还不好理解为什么,那么来看看下面这个函数为例: 199 | 200 | ```Python 201 | def countdown(n): 202 | if n <= 0: 203 | print('Blastoff!') 204 | else: 205 | print(n) 206 | countdown(n-1) 207 | ``` 208 | 209 | 如果n为0或者负数,程序会输出『Blastoff!』。其他情况下,程序会调用自身来运行,以自身参数n减去1为参数。如果像下面这样调用这个函数会怎么样? 210 | 211 | ```Bash 212 | >>> countdown(3) 213 | ``` 214 | 215 | 开始时候函数参数n是3,大于0,输出n的值3,然后调用自身,用n-1也就是2作为参数。。。 216 | 217 | 接下来的函数参数n是2,大于0,输出n的值2,然后调用自身,用n-1也就是1作为参数。。。 218 | 219 | 再往下去函数参数n是1,大于0,输出n的值1,然后调用自身,用n-1也就是0作为参数。。。 220 | 221 | 最后这次函数参数n是0,等于0了,输出『Blastoff!』,然后返回。 222 | 223 | n=1的时候的countdown也执行完了,返回。 224 | 225 | n=2的时候的countdown也执行完了,返回。 226 | 227 | n=3的时候的countdown也执行完了,返回。 228 | 229 | (译者注:这时候一定要注意不是输出字符串就完毕了,要返回的每一个层次的函数调用者。这里不理解的话说明对函数调用的过程掌握的不透彻,一定要好好想仔细了。) 230 | 接下来你就回到主函数__main__里面了。所以总的输出会如下所示: 231 | 232 | ```Bash 233 | 3 234 | 2 235 | 1 236 | Blastoff! 237 | ``` 238 | 239 | 调用自身的函数就是递归的;执行这种函数的过程就叫递归运算。 240 | 241 | 242 | 我们再写一个用print把一个字符串s显示n次的例子: 243 | 244 | ```Python 245 | def print_n(s, n): 246 | if n <= 0: 247 | return 248 | print(s) 249 | print_n(s, n-1) 250 | s="Python is good" 251 | n=4 252 | print_n(s, n) 253 | ``` 254 | 255 | 如果n小于等于0了,返回语句return就会终止函数的运行。运行流程立即返回到函数调用者,函数其余各行的代码也都不会执行。 256 | 257 | 258 | 函数其余部分的代码很容易理解:print一下s,然后调用自身,用n-1做参数来继续运行,这样就额外对s进行了n-1次的显示。所以输出的行数是1+(n-1),最终一共有n行输出。 259 | 260 | 上面这种简单的例子,实际上用for循环更简单。不过后面我们就会遇到一些用for循环不太好写的例子了,这些情况往往用递归更简单,所以早点学习下递归是有好处的。 261 | 262 | ## 5.9 递归函数的栈图 263 | 264 | 在本书的第三章第九节,我们用栈图来表征函数调用过程中程序的状态。同样是这种栈图,将有助于给大家展示递归函数的运行过程。 265 | 266 | 267 | 每次有一个函数被调用的时候,Python都会创建一个框架来包含这个函数的局部变量和形式参数。对于递归函数来说,可能会在栈中同时生成多层次的框架。 268 | 269 | 270 | 图5.1展示了前面样例中coundown函数在n=3的时候的栈图。 271 | 272 | ________________________________________ 273 | ![Figure 5.1: Stack diagram.](./images/figure5.1.jpg) 274 | Figure 5.1: Stack diagram. 275 | ________________________________________ 276 | 277 | 栈图的开头依然是主函数__main__。这里主函数是空的,因为我们没有在主函数里面创建变量或者传递参数进去。 278 | 279 | 280 | 四个coundown方框中形式参数n的值都是不同的。在栈图底部是n=0的时候,也叫基准条件。这时候不再进行递归调用,也就没有更多框架了。 281 | 282 | 283 | 下面练习一下,画一个print_n函数的栈图,让s为字符串『Hello』,n为2。然后写一个函数,名字为do_n,使用一个操作对象和一个数字n作为实际参数,给出一个n作为次数来调用这个函数。 284 | 285 | ## 5.10 无穷递归 286 | 287 | 如果一个递归一直都不能到达基准条件,那就会持续不断地进行自我调用,程序也就永远不会终止了。这就叫无穷递归,一般这都不是个好事情哈。下面就是一个无穷递归的最简单的例子: 288 | 289 | ```Python 290 | def recurse(): 291 | recurse() 292 | ``` 293 | 294 | 在大多数的开发环境下,无穷递归的程序并不会真的永远运行下去。Python会在函数达到允许递归的最大层次后返回一个错误信息: 295 | 296 | ```Bash 297 | File "", line 2, in recurse 298 | RuntimeError: Maximum recursion depth exceeded 299 | ``` 300 | 301 | 这个追踪会我们之前看到的长很多。这种错误出现的时候,栈中都已经有1000层递归框架了! 302 | 303 | 304 | 如果你意外写出来一个无穷递归的代码,好好检查一下你的函数,一定要确保有一个基准条件来停止递归调用。如果存在了基准条件,检查一下一定要确保能使之成立。 305 | 306 | ## 5.11 键盘输入 307 | 308 | 目前为止咱们写过的程序还都没有接收过用户的输入。这写程序每次都是做一些同样的事情。 309 | 310 | Python提供了内置的一个函数,名叫input,这个函数会停止程序运行,等待用户来输入一些内容。用户按下ESC或者Enter回车键,程序就恢复运行,input函数就把用户输入的内容作为字符串返回。在Python2里面,同样的函数名字不同,叫做raw_input。 311 | 312 | ```Bash 313 | >>> text = input() 314 | What are you waiting for? 315 | >>> text 316 | What are you waiting for? 317 | ``` 318 | 319 | 在用户输入内容之前,最好显示一些提示,来告诉用户需要输入什么内容。input函数能够把提示内容作为参数: 320 | 321 | ```Bash 322 | >>> name = input('What...is your name?\n') 323 | What...is your name? 324 | Arthur, King of the Britons! 325 | >>> name 326 | Arthur, King of the Britons! 327 | ``` 328 | 329 | 提示内容末尾的\n表示要新建一行,这是一个特殊的字符,表示换行。因为有了换行字符,所以用户输入就跑到了提示内容下面去了。 330 | 331 | 如果你想要用户来输入一个整形变量,可以把返回的值手动转换一下: 332 | 333 | ```Bash 334 | >>> prompt = 'What...is the airspeed velocity of an unladen swallow?\n' 335 | >>> speed = input(prompt) 336 | What...is the airspeed velocity of an unladen swallow? 337 | 42 338 | >>> int(speed) 339 | 42 340 | ``` 341 | 342 | 如果用户输入的是其他内容,而不是一串数字,就会得到一个错误了: 343 | 344 | ```Python 345 | >>> speed = input(prompt) 346 | What...is the airspeed velocity of an unladen swallow? 347 | What do you mean, an African or a European swallow? 348 | >>> int(speed) ValueError: invalid literal for int() with base 10 349 | ``` 350 | 351 | 稍后我们再来看看如何应对这种错误。 352 | 353 | ## 5.12 调试 354 | 355 | 当语法错误或者运行错误出现的时候,错误信息会包含很多有用的信息,不过信息量太大,太繁杂。最有用的也就下面这两类: 356 | 357 | * 错误的类型是什么,以及 358 | 359 | * 错误的位置在哪里。 360 | 361 | 362 | 363 | 364 | ```Bash 365 | >>> x = 5 366 | >>> y = 6 367 | File "", line 1 368 | y = 6 369 | ^ 370 | IndentationError: unexpected indent 371 | ``` 372 | 373 | 这个例子里面,错误的地方是第二行开头用一个空格来缩进了。但这个错误是指向y的,这就有点误导了。一般情况下,错误信息都会表示出发现问题的位置,但具体的错误可能是在此位置之前的代码引起的,有的时候甚至是前一行。 374 | 375 | 同样情况也发生在运行错误的情况下。假设你试着用分贝为单位来计算信噪比。 376 | 377 | 公式为: 378 | ![](http://7xnq2o.com1.z0.glb.clouddn.com/ThinkPython%E4%BF%A1%E5%99%AA%E6%AF%94.jpg) 379 | 380 | 在Python,你可能像下面这样写: 381 | 382 | ```Python 383 | import math 384 | signal_power = 9 385 | noise_power = 10 386 | ratio = signal_power // noise_power 387 | decibels = 10 * math.log10(ratio) print(decibels) 388 | ``` 389 | 390 | 运行这个程序,你就会得到如下错误信息: 391 | 392 | ```Bash 393 | Traceback (most recent call last): 394 | File "snr.py", line 5, in ? 395 | decibels = 10 * math.log10(ratio) 396 | ValueError: math domain error 397 | ``` 398 | 399 | 这个错误信息提示第五行,但那一行实际上并没有错。要找到真正的错误,就要输出一下ratio的值来看一下,结果发现是0了。那问题实际是在第四行,应该用浮点除法,结果多打了一个右斜杠,弄成了地板除法,才导致的错误。 400 | 401 | 402 | 所以你得花点时间仔细阅读错误信息,但不要轻易就认为出错信息说的内容都是完全正确可靠的。 403 | 404 | ## 5.13 Glossary 术语列表 405 | floor division: 406 | An operator, denoted //, that divides two numbers and rounds down (toward zero) to an integer. 407 | 408 | >地板除法:一种运算符,双右斜杠,把两个数相除,舍弃小数位,结果为整形。 409 | 410 | modulus operator: 411 | An operator, denoted with a percent sign (%), that works on integers and returns the remainder when one number is divided by another. 412 | 413 | >求模取余:一种运算符,百分号%,对整形起作用,返回两个数字相除的余数。 414 | 415 | boolean expression: 416 | An expression whose value is either True or False. 417 | 418 | >布尔表达式:一种值为真或假的表达式。 419 | 420 | relational operator: 421 | One of the operators that compares its operands: ==, !=, >, <, >=, and <=. 422 | 423 | >关系运算符:对比运算对象关系的运算符:==相等, !=不等, >大于, <小于, >=大于等于, 以及<=小于等于。 424 | 425 | logical operator: 426 | One of the operators that combines boolean expressions: and, or, and not. 427 | 428 | >逻辑运算符:把布尔表达式连接起来的运算符:and且,or或,以及not非。 429 | 430 | conditional statement: 431 | A statement that controls the flow of execution depending on some condition. 432 | 433 | >条件语句:控制运行流程的语句,根据不同条件有不同语句去运行。 434 | 435 | condition: 436 | The boolean expression in a conditional statement that determines which branch runs. 437 | 438 | >条件:条件语句所适用的布尔表达式,根据真假来决定运行分支。 439 | 440 | compound statement: 441 | A statement that consists of a header and a body. The header ends with a colon (:). The body is indented relative to the header. 442 | 443 | >复合语句:包含头部与语句体的一套语句组合。头部要有冒号做结尾,语句体相对于头部要有一次缩进。 444 | 445 | branch: 446 | One of the alternative sequences of statements in a conditional statement. 447 | 448 | >分支:条件语句当中备选的一系列语句。 449 | 450 | chained conditional: 451 | A conditional statement with a series of alternative branches. 452 | 453 | >链式条件:一系列可选分支构成的条件语句。 454 | 455 | nested conditional: 456 | A conditional statement that appears in one of the branches of another conditional statement. 457 | 458 | >嵌套条件:条件语句分支中继续包含次级条件语句的情况。 459 | 460 | return statement: 461 | A statement that causes a function to end immediately and return to the caller. 462 | 463 | >返回语句:一种特殊的语句,功能是终止当前函数,立即跳出到函数调用者。 464 | 465 | recursion: 466 | The process of calling the function that is currently executing. 467 | 468 | >递归:函数对自身进行调用的过程。 469 | 470 | base case: 471 | A conditional branch in a recursive function that does not make a recursive call. 472 | 473 | >基准条件:递归函数中一个条件分支,要实现终止递归调用。 474 | 475 | infinite recursion: 476 | A recursion that doesn’t have a base case, or never reaches it. Eventually, an infinite recursion causes a runtime error. 477 | 478 | >无穷递归:一个没有基准条件的递归,或者永远无法达到基准条件的递归。一般无穷递归总会引起运行错误。 479 | 480 | ## 5.14 练习 481 | ## # 练习1 482 | 483 | time模块提供了一个名字同样叫做time的函数,会返回当前格林威治时间的时间戳,就是以某一个时间点作为初始参考值。在Unix系统中,时间戳的参考值是1970年1月1号。 484 | 485 | 486 | (译者注:时间戳就是系统当前时间相对于1970.1.1 00:00:00以秒计算的偏移量,时间戳是惟一的。) 487 | 488 | ```Bash 489 | >>> import time 490 | >>> time.time() 1437746094.5735958 491 | ``` 492 | 493 | 写一个脚本,读取当前的时间,把这个时间转换以天为单位,剩余部分转换成小时-分钟-秒的形式,加上参考时间以来的天数。 494 | 495 | ## # 练习2 496 | 497 | 费马大定理内容为,a、b、c、n均为正整数,在n大于2的情况,下面的等式关系不成立: 498 | 499 | 500 | 1. 写一个函数,名叫check_fermat,这个函数有四个形式参数:a、b、c以及n,检查一下费马大定理是否成立,看看在n大于2的情况下下列等式 501 | 502 | ![](http://7xnq2o.com1.z0.glb.clouddn.com/ThinkPython%E8%B4%B9%E9%A9%AC.jpg) 503 | 504 | 是否成立。 505 | 506 | 2. 要求程序输出『Holy smokes, Fermat was wrong!』或者『No, that doesn’t work.』 507 | 508 | 3. 写一个函数来提醒用户要输入a、b、c和n的值,然后把输入值转换为整形变量,接着用check_fermat这个函数来检查他们是否违背了费马大定理。 509 | 510 | ## # 练习3 511 | 512 | 给你三根木棍,你能不能把它们拼成三角形呢?比如一个木棍是12英寸长,另外两个是1英寸长,这两根短的就不够长,无法拼成三角形了。 513 | 514 | 515 | (译者注:1英寸=2.54厘米)对于任意的三个长度,有一个简单的方法来检测它们能否拼成三角形: 516 | 517 | 只要三个木棍中有任意一个的长度大于其他两个的和,就拼不成三角形了。必须要任意一个长度都小于两边和才能拼成三角形。(如果两边长等于第三边,就只能组成所谓『退化三角形』了。译者注:实际上这不就成了线段了么?) 518 | 519 | 1. 写一个叫做is_triangle的函数,用三个整形变量为实际参数,函数根据你输入的值能否拼成三角形来判断输出『Yes』或者『No』。 520 | 521 | 2. 写一个函数来提示下用户,要输入三遍长度,把它们转换成整形,用is_triangle函数来检测这些给定长度的边能否组成三角形。 522 | 523 | ## # 4 练习4 524 | 525 | 下面的代码输出会是什么?画一个栈图来表示一下如下例子中程序输出结果时候的状态。 526 | 527 | ```Python 528 | def recurse(n, s): 529 | if n == 0: 530 | print(s) 531 | else: 532 | recurse(n-1, n+s) 533 | recurse(3, 0) 534 | ``` 535 | 1. recurse(-1, 0)这样的调用函数会有什么效果? 536 | 537 | 2. 为这个函数写一个文档字符串,解释一下用法(仅此而已)。 538 | 539 | 接下来的练习用到了第四章我们提到过的turtle小乌龟模块。 540 | 541 | ## # 练习5 542 | 543 | 阅读下面的函数,看看你能否弄清楚函数的作用。运行一下试试(参考第四章里面的例子来酌情修改代码)。 544 | 545 | ```Python 546 | def draw(t, length, n): 547 | if n == 0: 548 | return 549 | angle = 50 550 | t.fd(length*n) 551 | t.lt(angle) 552 | draw(t, length, n-1) 553 | t.rt(2*angle) 554 | draw(t, length, n-1) 555 | t.lt(angle) 556 | t.bk(length*n) 557 | ``` 558 | ________________________________________ 559 | ![Figure 5.2: A Koch curve.](./images/figure5.2.jpg) 560 | Figure 5.2: A Koch curve. 561 | ________________________________________ 562 | 563 | ## # 6 练习6 564 | 565 | Koch科赫曲线是一种分形曲线,外观如图5.2所示。要画长度为x的这种曲线,你要做的步骤如下: 566 | 567 | 1. 画一个长度为三分之一x的Koch曲线。 568 | 569 | 2. 左转60度。 570 | 571 | 3. 画一个长度为三分之一x的Koch曲线。 572 | 573 | 4. 右转120度。 574 | 575 | 5. 画一个长度为三分之一x的Koch曲线。 576 | 577 | 6. 左转60度。 578 | 579 | 7. 画一个长度为三分之一x的Koch曲线。 580 | 581 | 582 | 特例是当x小于3的时候:这种情况下,你就可以只画一个长度为x的直线段。 583 | 584 | 1. 写一个叫做koch的函数,用一个小乌龟turtle以及一个长度length做形式参数,用这个小乌龟来画给定长度length的Koch曲线。 585 | 586 | 2. 写一个叫做snowflake的函数,画三个Koch曲线来制作一个雪花的轮廓。[参考代码](http://thinkpython2.com/code/koch.py) 587 | 588 | 3. The Koch curve can be generalized in several ways. See [here](http://en.wikipedia.org/wiki/Koch_snowflake) for examples and implement your favorite. 589 | 590 | 生成Koch曲线的方法还有很多。点击 [这里](http://en.wikipedia.org/wiki/Koch_snowflake)来查看更多的例子,探索一下看看你喜欢哪个。 591 | 592 | -------------------------------------------------------------------------------- /chapter6.md: -------------------------------------------------------------------------------- 1 | # 第六章 有返回值的函数 2 | 3 | 我们已经用过的很多Python的函数,比如数学函数,都会有返回值。但我们写过的函数都是无返回值的:他们实现一些效果,比如输出一些值,或者移动小乌龟,但他们就是不返回值。 4 | 5 | ## 6.1 返回值 6 | 7 | 对函数进行调用,就会产生一个返回的值,我们一般把这个值赋给某个变量,或者放进表达式中来用。 8 | 9 | ```Python 10 | e = math.exp(1.0) 11 | height = radius * math.sin(radians) 12 | ``` 13 | 14 | 目前为止,我们写过的函数都没有返回值。简单说是没有返回值,更确切的讲,这些函数的返回值是空(None)。 15 | 16 | 在本章,我们总算要写一些有返回值的函数了。第一个例子就是一个计算给定半径的圆的面积的函数: 17 | 18 | ```Python 19 | def area(radius): 20 | a = math.pi * radius**2 21 | return a 22 | ``` 23 | 24 | 返回语句我们之前已经遇到过了,但在有返回值的函数里面,返回语句可以包含表达式。这个返回语句的意思是:立即返回下面这个表达式作为返回值。返回语句里面的表达式可以随便多复杂都行,所以刚刚那个计算面积的函数我们可以精简改写成以下形式: 25 | 26 | ```Python 27 | def area(radius): 28 | return math.pi * radius**2 29 | ``` 30 | 31 | 另外,有一些临时变量可以让后续的调试过程更简单。所以有时候可以多设置几条返回语句,每一条都对应一种情况。 32 | 33 | ```Python 34 | def absolute_value(x): 35 | if x < 0: 36 | return -x 37 | else: 38 | return x 39 | ``` 40 | 41 | 因为这些返回语句对应的是不同条件,因此实际上最终只会有一个返回动作执行。 42 | 43 | 44 | 返回语句运行的时候,函数就结束了,也不会运行任何其他的语句了。返回语句后面的代码,执行流程里所有其他的位置都无法再触碰了,这种代码叫做『死亡代码』。在有返回值的函数里面,建议要确认一下每一种存在的可能,来让函数触发一个返回语句。下面例子中: 45 | 46 | ```Python 47 | def absolute_value(x): 48 | if x < 0: 49 | return -x 50 | if x > 0: 51 | return x 52 | ``` 53 | 54 | 这个函数就是错误的,因为一旦x等于0了,两个条件都没满足,没有触发返回语句,函数就结束了。执行流程走完这个函数之后,返回的就是空(None),而本应该返回0的绝对值的。 55 | 56 | ```Python 57 | >>> absolute_value(0) 58 | >>> absolute_value(0) 59 | None 60 | ``` 61 | 62 | 顺便说一下,Python内置函数就有一个叫abs的,就是用来求绝对值的。 63 | 64 | 65 | 然后练习一下把,写一个比较大小的函数,用两个之x和y作为参数,如果x大于y返回1,相等返回0,x小于y返回-1. 66 | 67 | ## 6.2 增量式开发 68 | 69 | 写一些复杂函数的时候,你会发现要花很多时间调试。 70 | 71 | 要应对越来越复杂的程序,你不妨来试试增量式开发的办法。增量式开发的目的是避免长时间的调试过程,一点点对已有的小规模代码进行增补和测试。 72 | 73 | $$distance = \sqrt{(x_2 − x_1)^2 + (y_2 − y_1)^2}$$ 74 | 75 | 首先大家来想一下用Python来计算两点距离的函数应该是什么样。换句话说,输入的参数是什么,输出的返回值是什么? 76 | 77 | 78 | 这个案例里面,输入的应该是两个点的坐标,平面上就是四个数字了。返回的值是两点间的距离,就是一个浮点数了。 79 | 80 | ```Python 81 | def distance(x1, y1, x2, y2): 82 | return 0.0 83 | ``` 84 | 85 | 当然了,上面这个版本的代码肯定算不出距离了;不管输入什么都会返回0了。但这个函数语法上正确,而且可以运行,这样在程序过于复杂的情况之前就能及时测试了。 86 | 87 | 88 | 要测试一下这个新函数,就用简单的参数来调用一下吧: 89 | 90 | ```Python 91 | >>> distance(1, 2, 4, 6) 92 | 0.0 93 | ``` 94 | 95 | 我选择这些数值,水平的距离就是3,竖直距离就是4;这样的话,平面距离就应该是5了,是一个3-4-5三角形的斜边长了。我们已经知道正确结果应该是什么了,这样对测试来说很有帮助。 96 | 97 | 98 | 现在我们已经确认过了,这个函数在语法上是正确的,接下来我们就可以在函数体内添加代码了。下一步先添加一下求x2-x1和y2-y1的值的内容。接下来的版本里面,就把它们存在一些临时变量里面,然后输出一下。 99 | 100 | ```Python 101 | def distance(x1, y1, x2, y2): 102 | dx = x2 - x1 103 | dy = y2 - y1 104 | print('dx is', dx) 105 | print('dy is', dy) 106 | return 0.0 107 | ``` 108 | 109 | 这个函数如果工作的话,应该显示出'dx is 3'和'dy is 4'。如果成功显示了,我们就知道函数已经得到了正确的实际参数,并且正确进行了初步的运算。如果没有显示,只要检查一下这么几行代码就可以了。接下来,就要计算dx和dy的平方和了。 110 | 111 | ```Python 112 | def distance(x1, y1, x2, y2): 113 | dx = x2 - x1 114 | dy = y2 - y1 115 | dsquared = dx**2 + dy**2 116 | print('dsquared is: ', dsquared) 117 | return 0.0 118 | ``` 119 | 120 | 在这一步,咱们依然亏运行一下程序,来检查输出,输出的应该是25。输出正确的话,最后一步就是用math.sqrt这个函数来计算并返回结果: 121 | 122 | ```Python 123 | def distance(x1, y1, x2, y2): 124 | dx = x2 - x1 125 | dy = y2 - y1 126 | dsquared = dx**2 + dy**2 127 | result = math.sqrt(dsquared) 128 | return result 129 | ``` 130 | 131 | 如果程序工作没问题,就搞定了。否则你可能就需要用print输出一下最终计算结果,然后再返回这个值。 132 | 133 | 134 | 此函数的最终版本在运行的时候是不需要显示任何内容的;这个函数只需要返回一个值。我们写得这些print打印语句都是用来调试的,但一旦程序能正常工作了,就应该把print语句去掉。这些print代码也叫『脚手架代码』因为是用来构建程序的,但不会被存放在最终版本的程序中。 135 | 136 | 137 | 当你动手的时候,每次建议只添加一两行代码。等你经验更多了,你发现自己可能就能够驾驭大块代码了。不论如何,增量式开发总还是能帮你节省很多调试消耗的时间。 138 | 139 | 140 | 这个过程的核心如下: 141 | 142 | 1. 一定要用一个能工作的程序来开始,每次逐渐添加一些细小增补。在任何时候遇到错误,都应该弄明白错误的位置。 143 | 144 | 2. 用一些变量来存储中间值,这样你可以显示一下这些值,来检查一下。 145 | 146 | 3. 程序一旦能工作了,你就应该把一些发挥『脚手架作用』的代码删掉,并且把重复的语句改写成精简版本,但尽量别让程序变得难以阅读。 147 | 148 | 做个练习吧,用这种增量式开发的思路来写一个叫做hypotenuse(斜边)的函数,接收两个数值作为给定两边长,求以两边长为直角边的直角三角形斜边的长度。做练习的时候记得要记录好开发的各个阶段。 149 | 150 | ## 6.3 组合 151 | 你现在应该已经能够在一个函数里面调用另外一个函数了。下面我们写一个函数作为例子,这个函数需要两个点,一个是圆心,一个是圆周上面的点,函数要用来计算这个圆的面积。 152 | 153 | 假设圆心的坐标存成一对变量:xc和yc,圆周上一点存成一对变量:xp和yp。第一步就是算出来这个圆的半径,也就是这两个点之间的距离。我们就用之前写过的那个distance的函数来完成这件事: 154 | 155 | ```Python 156 | radius = distance(xc, yc, xp, yp) 157 | ``` 158 | 下一步就是根据计算出来的半径来算圆的面积;计算面积的函数我们也写过了: 159 | 160 | ```Python 161 | result = area(radius) 162 | ``` 163 | 164 | 把上述的步骤组合在一个函数里面: 165 | 166 | ```Python 167 | def circle_area(xc, yc, xp, yp): 168 | radius = distance(xc, yc, xp, yp) 169 | result = area(radius) 170 | return result 171 | ``` 172 | 173 | 临时变量radius和result是用于开发和调试用的,只要程序能正常工作了,就可以把它们都精简下去: 174 | 175 | ```Python 176 | def circle_area(xc, yc, xp, yp): 177 | return area(distance(xc, yc, xp, yp)) 178 | ``` 179 | ## 6.4 布尔函数 180 | 181 | 函数也可以返回布尔值,这种情况便于隐藏函数内部的复杂测试。例如: 182 | 183 | ```Python 184 | def is_divisible(x, y): 185 | if x % y == 0: 186 | return True 187 | else: 188 | return False 189 | ``` 190 | 191 | 一般情况下都给这种布尔函数起个独特的名字,比如要有判断意味的提示;is_divisible这个函数就去判断x能否被y整除而对应地返回真或假。 192 | 193 | ```Python 194 | >>> is_divisible(6, 4) 195 | False 196 | >>> is_divisible(6, 3) 197 | True 198 | ``` 199 | 双等号运算符的返回结果是一个布尔值,所以我们可以用下面的方法来简化刚刚的函数: 200 | 201 | ```Python 202 | def is_divisible(x, y): 203 | return x % y == 0 204 | ``` 205 | 布尔函数经常用于条件语句: 206 | 207 | ```Python 208 | if is_divisible(x, y): 209 | print('x is divisible by y') 210 | ``` 211 | 212 | 可以用于写如下这种代码: 213 | 214 | ```Python 215 | if is_divisible(x, y) == True: 216 | print('x is divisible by y' 217 | ``` 218 | 219 | 但额外的比较并没有必要。 220 | 221 | 222 | 做一个练习,写一个函数is_between(x, y, z),如果x ≤ y ≤z则返回真,其他情况返回假。 223 | 224 | ## 6.5 更多递归 225 | 226 | 我们目前学过的知识Python的一小部分子集,不过这部分子集本身已经是一套完整的编程语言了,这就意味着所有能计算的东西都可以用这部分子集来表达。实际上任何程序都可以改写成只用你所学到的这部分Python特性的代码。(当然你还需要一些额外的代码来控制设备,比如鼠标、磁盘等等,但也就这么多额外需要而已。) 227 | 228 | 229 | 阿兰图灵最先证明了这个说法,他是最早的计算机科学家之一,有人会认为他更是一个数学家,不过早起的计算机科学家也都是作为数学家来起步的。因此这个观点也被叫做图灵理论。关于对此更全面也更准确的讨论,我推荐一本Michael Sipser的书:Introduction to the Theory of Computation 计算方法导论。 230 | 231 | 232 | 为了让你能更清晰地认识到自己当前学过的这些内容能用来做什么,我们会看看一些数学的函数,这些函数都是递归定义的。递归定义与循环定义有些相似,就是函数的定义体内包含了对所定义内容的引用。一一个完全循环的定义并不会有太大用。 233 | 234 | vorpal: 235 | An adjective used to describe something that is vorpal. 236 | 237 | >刺穿的:用来描述被刺穿的形容词。 238 | 239 | 240 | 你在词典里面看到上面这种定义,一定很郁闷。然而如果你查一下阶乘函数的定义,你估计会得到如下结果: 241 | 242 | 0! = 1 243 | n! = n (n−1)! 244 | 245 | 这个定义表明了0的阶乘为1,然后对任意一个数n的阶乘,是n与n-1阶乘相乘。 246 | 247 | 248 | 所以3的阶乘就是3乘以2的阶乘,而2的阶乘就是2乘以1的阶乘,1的阶乘是1乘以0的阶乘。算到一起,3的阶乘就等于3*2*1*1,就是6了。 249 | 250 | 251 | 如果要给某种对象写一个递归的定义,就可以用Python程序来实现。第一步是要来确定形式参数是什么。在这种情况下要明确阶乘所用到的都应该是整形: 252 | 253 | ```Python 254 | def factorial(n): 255 | ``` 256 | 257 | 如果传来的实际参数碰巧是0,要返回1: 258 | 259 | ```Python 260 | def factorial(n): 261 | if n == 0: 262 | return 1 263 | ``` 264 | 265 | 其他情况就有意思了,我们必须用递归的方式来调用n-1的阶乘,然后用它来乘以n: 266 | 267 | ```Python 268 | def factorial(n): 269 | if n == 0: 270 | return 1 271 | else: 272 | recurse = factorial(n-1) 273 | result = n * recurse 274 | return result 275 | ``` 276 | 277 | 这个程序的运行流程和5.8里面的那个倒计时有点相似。我们用3作为参数来调用一下这个阶乘函数试试: 278 | 279 | 280 | 3不是0,所以分支一下,计算n-1的阶乘。。。 281 | 282 | 283 | 2不是0,所以分支一下,计算n-1的阶乘。。。 284 | 285 | 286 | 1不是0,所以分支一下,计算n-1的阶乘。。。 287 | 288 | 289 | 到0了,就返回1给进行递归的分支。 290 | 291 | 292 | 返回值1与1相乘,结果再次返回。 293 | 294 | 返回值1与2相乘,结果再次返回。 295 | 296 | 2的返回值再与n也就是3想成,得到的结果是6,就成了整个流程最终得到的答案。 297 | 298 | 图6.1表明了这一系列函数调用过程中的栈图。 299 | 300 | ________________________________________ 301 | ![Figure 6.1: Stack diagram](./images/figure6.1.jpg) 302 | Figure 6.1: Stack diagram. 303 | ________________________________________ 304 | 305 | 306 | 307 | ## 6.6 信仰之跃 308 | 309 | 跟随着运行流程是阅读程序的一种方法,但很快就容易弄糊涂。另外一个方法,我称之为『思维跳跃』。当你遇到一个函数调用的时候,你不用去追踪具体的执行流程,而是假设这个函数工作正常并且返回正确的结果。 310 | 311 | 312 | 实际上你已经联系过这种思维跳跃了,就在你使用内置函数的时候。当你调用math.cos或者math.exp的时候,你并没有仔细查看这些函数的函数体。你就假设他们都工作,因为写这些内置函数的人都是很可靠的编程人员。 313 | 314 | 315 | 你调用自己写的函数时候也是同样道理。比如在6.4部分,我们谢了这个叫做is_divisible的函数来判断一个数能否被另外一个数整除。一旦我们通过分析代码和做测试来确定了这个函数没有问题,我们就可以直接使用这个函数了,不用去理会函数体内部细节了。 316 | 317 | 318 | 对于递归函数而言也是同样道理。当你进行递归调用的时候,并不用追踪执行流程,你只需要假设递归调用正常工作,返回正确结果,然后你可以问问自己:『假设我能算出来n-1的阶乘,我能否计算出n的阶乘呢?』很显然你是可以的,乘以n就可以了。 319 | 320 | 321 | 当然了,当你还没写完一个函数的时候就假设它正常工作确实有点奇怪,不过这也是我们称之为『思维飞跃』的原因了,你总得飞跃一下。 322 | 323 | ## 6.7 斐波拉契数列 324 | 325 | 计算阶乘之后,我们来看看斐波拉契数列,这是一个广泛应用于展示递归定义的数学函数,[定义](http://en.wikipedia.org/wiki/Fibonacci_number如下: 326 | 327 | ```Tex 328 | fibonacci(0) = 0 329 | fibonacci(1) = 1 330 | fibonacci(n) = fibonacci(n−1) + fibonacci(n−2) 331 | ``` 332 | 333 | 翻译成Python的语言大概如下这样: 334 | 335 | ```Python 336 | def fibonacci (n): 337 | if n == 0: 338 | return 0 339 | elif n == 1: 340 | return 1 341 | else: 342 | return fibonacci(n-1) + fibonacci(n-2) 343 | ``` 344 | 345 | 跃』的方法,如果你假设两个递归调用都正常工作,整个过程就很明确了,你就得到正确答案加到一起即可。 346 | 347 | ## 6.8 检查类型 348 | 349 | 如果我们让阶乘使用1.5做参数会咋样? 350 | 351 | ```Python 352 | >>> factorial(1.5) 353 | RuntimeError: Maximum recursion depth exceeded 354 | ``` 355 | 356 | 看上去就像是无穷递归一样。为什么会这样?因为这个函数的基准条件是n等于0。但如果n不是一个整形变量,就会无法达到基准条件,然后无穷递归下去。 357 | 358 | 359 | 在第一次递归调用的时候,传递的n值是0.5.下一步就是-0.5. 360 | 361 | 362 | 从这开始,这个值就越来越小(就成了更小的负数了)但永远都不会到0. 363 | 364 | 365 | 我们有两种选择。我们可以尝试着把阶乘函数改写成可以用浮点数做参数的,或者也可以让阶乘函数检查一下参数类型。第一种选择就会写出一个伽玛函数,这已经超越了本书的范围。所以我们用第二种方法。 366 | 367 | (译者注:伽玛函数(Gamma函数),也叫欧拉第二积分,是阶乘函数在实数与复数上扩展的一类函数。该函数在分析学、概率论、偏微分方程和组合数学中有重要的应用。与之有密切联系的函数是贝塔函数,也叫第一类欧拉积分。) 368 | 369 | 我们可以用内置的isinstance函数来判断参数的类型。我们也还得确定一下参数得是大于0的: 370 | 371 | ```Python 372 | def factorial (n): 373 | if not isinstance(n, int): 374 | print('Factorial is only defined for integers.') 375 | return None 376 | elif n < 0: 377 | print('Factorial is not defined for negative integers.') 378 | return None 379 | elif n == 0: 380 | return 1 381 | else: 382 | return n * factorial(n-1) 383 | ``` 384 | 第一个基准条件用来处理非整数;第二个用来处理负整数。在小数或者负数做参数的时候,函数就会输出错误信息,返回空到调用出来表明出错了: 385 | 386 | ```Python 387 | >>> factorial('fred') 388 | Factorial is only defined for integers. None 389 | >>> factorial(-2) 390 | Factorial is not defined for negative integers. None 391 | ``` 392 | 393 | 如果两个检查都通过了,就能确定n是正整数或者0,就可以保证递归能够正确进行和终止了。 394 | 395 | 这个程序展示了一种叫做『守卫』的模式。前两个条件就扮演了守卫的角色,避免了那些引起错误的变量。这些守卫能够保证我们的代码运行正确。 396 | 397 | 在11.4我们还会看到更多的灵活的处理方式,会输出错误信息,并上报异常。 398 | 399 | ## 6.9 调试 400 | 401 | 把大的程序切分成小块的函数,就自然为我们调试建立了一个个的检查点。在不工作的函数里面,有几种导致错误的可能: 402 | 403 | * 函数接收的参数可能有错,前置条件没有满足。 404 | 405 | * 函数本身可能有错,后置条件没有满足。 406 | 407 | * 返回值或者返回值使用的方法可能有错。 408 | 409 | 410 | 要去除第一种情况,你要在函数开头的时候添加一个print语句,来输出一下参数的值(最好加上类型)。或者你可以写一份代码来明确检查一下前置条件是否满足。 411 | 412 | 如果形式参数看上去没问题,在每一个返回语句之前都加上print语句,显示一下要返回的值。如果可以的话,尽量亲自去检查一下这些结果,自己算一算。尽量调用有值的函数,这样检查结果更方便些(比如在6.2中那样。) 413 | 414 | 如果函数看着没啥问题,就检查一下函数的调用,来检查一下返回值是不是有用到,确保返回值被正确使用。 415 | 416 | 417 | 在函数的开头结尾添加输出语句,能够确保整个执行流程更加可视化。比如下面就是一个有输出版本的阶乘函数: 418 | 419 | ```Python 420 | def factorial(n): 421 | space = ' ' * (4 * n) 422 | print(space, 'factorial', n) 423 | if n == 0: 424 | print(space, 'returning 1') 425 | return 1 426 | else: 427 | recurse = factorial(n-1) 428 | result = n * recurse 429 | print(space, 'returning', result) 430 | return result 431 | ``` 432 | 433 | space在这里是一串空格的字符串,是用来缩进输出的。下面就是4的阶乘得到的结果: 434 | 435 | ```Python 436 | factorial 4 437 | factorial 3 438 | factorial 2 439 | factorial 1 440 | factorial 0 441 | returning 1 442 | returning 1 443 | returning 2 444 | returning 6 445 | returning 24 446 | ``` 447 | 如果你对执行流程比较困惑,这种输出会有一定帮助。有效率地进行脚手架开发是需要时间的,但稍微利用一下这种思路,反而能够节省调试用的时间。 448 | 449 | ## 6.10 Glossary 术语列表 450 | temporary variable: 451 | A variable used to store an intermediate value in a complex calculation. 452 | 453 | >临时变量:用来在复杂运算过程中存储一些中间值的变量。 454 | 455 | dead code: 456 | Part of a program that can never run, often because it appears after a return statement. 457 | 458 | >无效代码:一部分不会被运行的代码,一般是书现在了返回语句之后。 459 | 460 | incremental development: 461 | A program development plan intended to avoid debugging by adding and testing only a small amount of code at a time. 462 | 463 | >渐进式开发:程序开发的一种方式,每次对已有的能工作的代码进行小规模的增补修改来降低调试的精力消耗。 464 | 465 | scaffolding: 466 | Code that is used during program development but is not part of the final version. 467 | 468 | >脚手架代码:在程序开发期间使用的代码,但最终版本并不会包含这些代码。 469 | 470 | guardian: 471 | A programming pattern that uses a conditional statement to check for and handle circumstances that might cause an error. 472 | 473 | >守卫:一种编程模式。使用一些条件语句来检验和处理一些有可能导致错误的情况。 474 | 475 | ## 6.11 练习 476 | ## # 练习1 477 | 478 | 为下面的程序画栈图。程序输出会是什么样的? 479 | 480 | ```Python 481 | def b(z): 482 | prod = a(z, z) 483 | print(z, prod) 484 | return prod 485 | def a(x, y): 486 | x = x + 1 487 | return x * y 488 | def c(x, y, z): 489 | total = x + y + z 490 | square = b(total)**2 491 | return square 492 | x = 1 493 | y = x + 1 494 | print(c(x, y+3, x+y)) 495 | ``` 496 | ## # 练习2 497 | 498 | Ackermann阿克曼函数的定义如下: 499 | 500 | ```Python 501 | A(m, n) = n+1 if m = 0 502 | A(m−1, 1) if m > 0 and n = 0 503 | A(m−1, A(m, n−1)) if m > 0 and n > 0. 504 | ``` 505 | 506 | 看一下[这个连接](http://en.wikipedia.org/wiki/Ackermann_function)。写一个叫做ack的函数,实现上面这个阿克曼函数。用你写出的函数来计算ack(3, 4),结果应该是125.看看m和n更大一些会怎么样。[样例代码](http://thinkpython2.com/code/ackermann.py). 507 | 508 | ## # 练习3 509 | 510 | 回文的词特点是正序和倒序拼写相同给,比如noon以及redivider。用递归的思路来看,回文词的收尾相同,中间部分是回文词。 511 | 512 | 513 | 下面的函数是把字符串作为实际参数,然后返回函数的头部、尾部以及中间字母: 514 | 515 | ```Python 516 | def first(word): 517 | return word[0] 518 | def last(word): 519 | return word[-1] 520 | def middle(word): 521 | return word[1:-1] 522 | ``` 523 | 524 | 第八章我们再看看他们是到底怎么工作的。 525 | 526 | 1. 把这些函数输入到一个名字叫做palindrome.py的文件中,测试一下输出。 527 | 528 | 529 | 如果中间你使用一个只有两个字符的字符串会怎么样?一个字符的怎么样? 530 | 531 | 空字符串,比如『』没有任何字母的,怎么样? 532 | 533 | 2. 写一个名叫is_palindrome的函数,使用字符串作为实际参数,根据字符串是否为回文词来返回真假。机主,你可以用内置的len函数来检查字符串的长度。 534 | 535 | ## # 练习4 536 | 一个数字a为b的权(power),如果a能够被b整除,并且a/b是b的权。写一个叫做is_power 的函数接收a和b作为形式参数,如果a是b的权就返回真。注意:要考虑好基准条件。 537 | 538 | ## # 练习5 539 | a和b的最大公约数是指能同时将这两个数整除而没有余数的数当中的最大值。 540 | 541 | 找最大公约数的一种方法是观察,如果当r是a除以b的余数,那么a和b的最大公约数与b和r的最大公约数相等。基准条件是a和0的最大公约数为a。 542 | 543 | 写一个有名叫gcd的函数,用a和b两个形式参数,返回他们的最大公约数。 544 | 545 | 致谢:这个练习借鉴了Abelson和Sussman的 计算机程序结构和解译 一书。 546 | 547 | -------------------------------------------------------------------------------- /chapter7.md: -------------------------------------------------------------------------------- 1 | # 第七章 迭代 2 | 3 | 这一章我们讲迭代,简单说就是指重复去运行一部分代码。在5.8的时候我们接触了一种迭代——递归。在4.2我们还学了另外一种迭代——for循环。在本章,我们会见到新的迭代方式:whie语句。但我要先再稍微讲一下变量赋值。 4 | 5 | ## 7.1 再赋值 6 | 7 | 你可能已经发现了,对同一个变量可以多次进行赋值。一次新的赋值使得已有的变量获得新的值(也就不再有旧的值了。) 8 | 9 | 10 | >(译者注:这个其实中文很好理解,英文当中词汇逻辑关系比较紧密,但灵活程度不如中文高啊。) 11 | 12 | ```Python 13 | >>> x = 5 14 | >>> x 15 | 5 16 | >>> x = 7 17 | >>> x 18 | 7 19 | ``` 20 | 21 | 第一次显示x的值,是5,第二次,就是7了。 22 | 23 | 图7.1表示了再赋值的操作在状态图中的样子。 24 | 25 | 这里我就要强调一下大家常发生的误解。因为Python使用单等号(=)来赋值,所以有的人会以为像a=b这样的语句就如同数学上的表达一样来表达两者相等,这种想法是错误的! 26 | 27 | 首先,数学上的等号所表示的相等是一个对称的关系,而Python中等号的赋值操作是不对称的。比如,在数学上,如果a=7,那么7=a。而在Python,a=7这个是合乎语法的,而7=a是错误的。 28 | 29 | (译者注:这里就是说Python中等号是一种单向的运算,就是把等号右边的值赋给等号左边的变量,而Python中其实也有数学上相等判断的表达式,就是双等号(==),这个是有对称性的,就是说a==b,那么b==a,或者a==3,3==a也可以。) 30 | 31 | 另外在数学上,一个相等关系要么是真,要么是假。比如a=b,那么a就会永远等于b。在Python里面,赋值语句可以让两个变量相等,但可以不始终都相等,如下所示: 32 | 33 | ```Python 34 | >>> a = 5 35 | >>> b = a # a and b are now equal a和b相等了 36 | >>> a = 3 # a and b are no longer equal 现在a和b就不相等了 37 | >>> b 38 | 5 39 | ``` 40 | 41 | 第三行改变了a的值,但没有改变b的值,所以它们就不再相等了。 42 | 43 | 44 | 对变量进行再赋值总是很有用的,但你用的时候要做好备注和提示。如果变量的值频繁变化,就可能让代码难以阅读和调试。 45 | 46 | ________________________________________ 47 | ![Figure 7.1: State diagram.](./images/figure7.1.jpg) 48 | Figure 7.1: State diagram. 49 | ________________________________________ 50 | ## 7.2 更新变量 51 | 52 | 最常见的一种再赋值就是对变量进行更新,这种情况下新的值是在旧值基础上进行修改得到的。 53 | 54 | ```Python 55 | >>> x = x + 1 56 | ``` 57 | 上面的语句的意思是:获取x当前的值,在此基础上加1,然后把结果作为新值赋给x。如果你对不存在的变量进行更新,你就会得到错误了,因为Python要先进行等号右边的运算,然后才能赋值给x。 58 | 59 | ```Python 60 | >>> x = x + 1 61 | NameError: name 'x' is not defined 62 | ``` 63 | 64 | 在你更新一个变量之前,你要先初始化一下,一般就是简单赋值一下就可以了: 65 | 66 | ```Python 67 | >>> x = 0 68 | >>> x = x + 1 69 | ``` 70 | 71 | 对一个变量每次加1也可以叫做一种递增,每次减去1就可以叫做递减了。 72 | 73 | ## 7.3 循环:While语句 74 | 75 | 计算机经常被用来自动执行一些重复的任务。重复同样的或者相似的任务,而不出错,这是计算机特别擅长的事情,咱们人就做不到了。在一个计算机程序里面,重复操作也被叫做迭代。 76 | 77 | 78 | 我们已经见过两种使用了递归来进行迭代的函数:倒计时函数countdown,以及n次输出函数print_n。Python还提供了一些功能来简化迭代,因为迭代用的很普遍。其中一个就是我们在4.2中见到的for循环语句。往后我们还要复习一下它。另外一个就是while循环语句。下面就是一个使用了while循环语句来实现的倒计时函数countdown: 79 | 80 | ```Python 81 | def countdown(n): 82 | while n > 0: 83 | print(n) 84 | n = n - 1 85 | print('Blastoff!') 86 | ``` 87 | 88 | while循环语句读起来很容易,几乎就像是英语一样简单。这个函数的意思是:当n大于0,就输出n的值,然后对n减1,到n等于0的时候,就输出单词『Blastoff』。 89 | 90 | 91 | 更正式一点,下面是一个while循环语句的执行流程: 92 | 93 | 1. 判断循环条件的真假。 94 | 95 | 2. 如果是假的,退出while语句,继续运行后面的语句。 96 | 97 | 3. 如果条件为真,执行循环体,然后再调回到第一步。 98 | 99 | 这种类型的运行流程叫做循环,因为第三步会循环到第一步。循环体内要改变一个或者更多变量的值,这样循环条件最终才能变成假,然后循环才能终止。 100 | 101 | 否则的话,条件不能为假,循环不能停止,这就叫做无限循环。计算机科学家有一个笑话,就是看到洗发液的说明:起泡,冲洗,重复;这就是一个无限循环。 102 | 103 | 在倒计时函数countdown里面,咱们能够保证有办法让循环终止:只要n小于等于0了,循环就不进行了。否则的话,n每次就会通过循环来递减,最终还是能到0的。 104 | 105 | 其他一些循环就不那么好描述了。比如: 106 | 107 | ```Python 108 | def sequence(n): 109 | while n != 1: 110 | print(n) 111 | if n % 2 == 0: # n is even 112 | n = n / 2 113 | else: # n is odd 114 | n = n*3 + 1 115 | ``` 116 | 这个循环的判断条件是n不等于1,所以循环一直进行,直到n等于1了,条件为假,就不再循环了。 117 | 118 | 每次循环的时候,程序都输出n的值,然后检查一下是偶数还是奇数。如果是偶数,就把n除以2。如果是奇数,就把n替换为n乘以3再加1的值。比如让这个函数用3做参数,也就是sequence(3),得到的n的值就依次为:3, 10, 5, 16, 8, 4, 2, 1. 119 | 120 | 有时候n在增加,有时候n在降低,所以没有明显证据表明n最终会到1而程序停止。对于一些特定的n值,我们能够确保循环的终止。例如如果起始值是一个2的倍数,n每次循环过后依然是偶数,直到到达1位置。之前的例子都最终得到了一个数列,从16开始的就是了。 121 | 122 | 真正的难题是,我们能否证明这个程序对任意正数的n值都能终止循环。目前为止,没有人能够证明或者否定这个命题。 123 | 124 | 参考[维基百科](http://en.wikipedia.org/wiki/Collatz_conjecture) 125 | 做一个练习,把5.8里面的那个n次打印函数print_n用迭代的方法来实现。 126 | 127 | ## 7.4 中断 128 | 129 | 有时候你不知道啥时候终止循环,可能正好在中间循环体的时候要终止了。这时候你就可以用break语句来跳出循环。 130 | 比如,假设你要让用户输入一些内容,当他们输入done的时候结束。你就可以用如下的方法实现: 131 | 132 | ```Python 133 | while True: 134 | line = input('> ') 135 | if line == 'done': 136 | break 137 | print(line) 138 | print('Done!') 139 | ``` 140 | 141 | 循环条件就是true,意味条件总是真的,所以循环就一直进行,一直到触发了break语句才跳出。 142 | 143 | 144 | 每次循环的时候,程序都会有一个大于号>来提示用户。如果用输入了done,break语句就会让程序跳出循环。否则的话程序会重复用户输入的内容,然后回到循环的头部。下面就是一个简单的运行例子: 145 | 146 | ```Python 147 | >>>not done 148 | >>>not done 149 | >>>done 150 | Done! 151 | ``` 152 | 153 | 这种while循环的写法很常见,因为这样你可以在循环的任何一个部位对条件进行检测,而不仅仅是循环的头部,你可以确定地表达循环停止的条件(在这种情况下就停止了),而不是消极地暗示『程序会一直运行,直到某种情况』。 154 | 155 | ## 7.5 平方根 156 | 157 | 循环经常被用于进行数值运算的程序中,这种程序往往是有一个近似值作为初始值,然后逐渐迭代去改进以接近真实值。 158 | 159 | 160 | 比如,可以用牛顿法来计算平方根。加入你要知道一个数a的平方根。如果你用任意一个估计值x来开始,你可以用下面的公式获得一个更接近的值: 161 | 162 | $$y = \frac{x + \frac{a}{x}}{2}$$ 163 | 164 | 比如,如果a是3,x设为3: 165 | 166 | ```Python 167 | >>> a = 4 168 | >>> x = 3 169 | >>> y = (x + a/x) / 2 170 | >>> y 171 | 2.16666666667 172 | ``` 173 | 174 | 得到的结果比初始值3更接近真实值(4的平方根是2)。如果我们用这个结果做新的估计值来重复这个操作,结果就更加接近了: 175 | 176 | ```Python 177 | >>> x = y 178 | >>> y = (x + a/x) / 2 179 | >>> y 180 | 2.00641025641 181 | ``` 182 | 183 | 这样进行一些次重复之后,估计值就几乎很准确了: 184 | 185 | ```Python 186 | >>> x = y 187 | >>> y = (x + a/x) / 2 188 | >>> y 2.00001024003 189 | >>> x = y 190 | >>> y = (x + a/x) / 2 191 | >>> y 192 | 2.00000000003 193 | ``` 194 | 195 | 一般情况下,我们不能提前知道到达正确结果需要多长时间,但是当估计值不再有明显变化的时候我们就知道了: 196 | 197 | ```Python 198 | >>> x = y 199 | >>> y = (x + a/x) / 2 200 | >>> y 201 | 2.0 202 | ``` 203 | 204 | 当y和x相等的时候,我们就可以停止了。下面这个循环中,用一个初始值x来开始循环,然后进行改进,一直到x的值不再变化为止: 205 | 206 | ```Python 207 | while True: 208 | print(x) 209 | y = (x + a/x) / 2 210 | if y == x: 211 | break 212 | x = y 213 | ``` 214 | 215 | 对大多数值来说,这个循环都挺管用的,但一般来说用浮点数来测试等式是很危险的。浮点数的值只能是近似正确:大多数的有理数,比如1/3,以及无理数,比如根号2,都不能用浮点数来准确表达的。 216 | 217 | 相比之下,与其对比x和y是否精确相等,倒不如以下方法更安全:用内置的绝对值函数来计算一下差值的绝对值,也叫做数量级。 218 | 219 | ```Python 220 | if abs(y-x) < epsilon: 221 | break 222 | ``` 223 | 这里可以让epsilon的值为like 0.0000001,差值比这个小就说明已经足够接近了。 224 | 225 | ## 7.6 算法 226 | 227 | 牛顿法是算法的一个例子:通过一系列机械的步骤来解决一类问题(在本章中是用来计算平方根)。 228 | 229 | 要理解算法是什么,先从一些不是算法的内容来开始也许会有所帮助。当你学个位数字乘法的时候,你可能要背下来整个乘法表。实际上你记住了100个特定的算式。这种知识就不是算法。 230 | 231 | 但如果你很『懒』,你就可能会有一些小技巧。比如找到一个n与9的乘积,你可以把n-1写成第一位,10-n写成第二位。这个技巧是应对任何个位数字乘以9的算式。这就是一个算法了! 232 | 233 | 类似地,你学过的进位的加法,借位的减法,以及长除法,都是算法。这些算法的一个共同特点就是不需要任何智力就能进行。它们都是机械的过程,每一步都跟随上一步,遵循着很简单的一套规则。 234 | 235 | 执行算法是很无聊的,但设计算法很有趣,是智力上的一种挑战,也是计算机科学的核心部分。 236 | 237 | 有的事情人们平时做起来很简单,甚至都不用思考,这些事情往往最难用算法来表达。比如理解自然语言就是个例子。我们都能理解自然语言,但目前为止还没有人能解释我们到底是怎么做到的,至少没有人把这个过程归纳出算法的形式。 238 | 239 | ## 7.7 调试 240 | 241 | 现在你已经开始写一些比较大的程序了,你可能发现自己比原来花更多时间来调试了。代码越多,也就意味着出错的可能也越大了,bug也有了更多的藏身之处了。 242 | 243 | 『对折调试』是一种节省调试时间的方法。比如,如果你的程序有100行,你检查一遍就要大概100步了。而对折方法就是把程序分成两半。看程序中间位置,或者靠近中间位置的,检查一些中间值。在这些位置添加一些print语句(或者其他能够能起到验证效果的东西),然后运行程序。 244 | 245 | 如果中间点检查出错了,那必然是程序的前半部分有问题。如果中间点没调试,那问题就是在后半段了。 246 | 247 | 每次你都这样检查,你就让你要检查的代码数量减半了。一般六步之后(远小于100次了),理论上你就差不多已经到代码的末尾一两行了。 248 | 249 | 在实际操作当中,程序中间位置并不是总那么明确,也未必就很容易去检查。所以不用数行数来找确定的中间点。相反的,只要考虑一下程序中哪些地方容易调试,然后哪些地方进行检验比较容易就行了。然后你就在你考虑好的位置检验一下看看bug是在那个位置之前还是之后。 250 | 251 | ## 7.8 Glossary 术语列表 252 | reassignment: 253 | Assigning a new value to a variable that already exists. 254 | 255 | >再赋值:对一个已经存在的有值变量赋予一个新的值。 256 | 257 | update: 258 | An assignment where the new value of the variable depends on the old. 259 | 260 | >更新:根据一个变量的旧值,进行一定的修改,再赋值给这个变量。 261 | 262 | initialization: 263 | An assignment that gives an initial value to a variable that will be updated. 264 | 265 | >初始化:给一个变量初始值,以便于后续进行更新。 266 | 267 | increment: 268 | An update that increases the value of a variable (often by one). 269 | 270 | >递增:每次给一个变量增加一定的值(一般是加1) 271 | 272 | decrement: 273 | An update that decreases the value of a variable. 274 | 275 | >递减:每次给一个变量减去一定的值。 276 | 277 | iteration: 278 | Repeated execution of a set of statements using either a recursive function call or a loop. 279 | 280 | >迭代:重复执行一系列语句,使用递归函数调用的方式,或者循环的方式。 281 | 282 | infinite loop: 283 | A loop in which the terminating condition is never satisfied. 284 | 285 | >无限循环:终止条件永远无法满足的循环。 286 | 287 | algorithm: 288 | A general process for solving a category of problems. 289 | 290 | >算法:解决某一类问题的一系列通用的步骤。 291 | 292 | ## 7.9 练习 293 | ## # 练习1 294 | 295 | 从7.5复制一个循环,然后改写成名字叫做mysqrt的函数,该函数用一个a作为参数,选择一个适当的起始值x,然后返回a的平方根的近似值。 296 | 297 | 测试这个函数,写一个叫做test_suqare_root的函数来输出以下这样的表格: 298 | ![](http://7xnq2o.com1.z0.glb.clouddn.com/ThinkPython%E5%B9%B3%E6%96%B9%E6%A0%B9.jpg) 299 | 300 | 301 | 第一列是数a;第二列是咱们自己写的函数mysqrt计算出来的平方根,第三行是用Python内置的math.sqrt函数计算的平方根,最后一行是这两者的差值的绝对值。 302 | 303 | ## # 练习2 304 | 305 | Python的内置函数eval接收字符串作为参数,然后用Python的解释器来运行。例如: 306 | 307 | ```Python 308 | >>> eval('1 + 2 * 3') 309 | 7 310 | >>> import math 311 | >>> eval('math.sqrt(5)') 312 | 2.2360679774997898 313 | >>> eval('type(math.pi)') 314 | 315 | ``` 316 | 317 | 写一个叫做eval_loop的函数,交互地提醒用户,获取输入,然后用eval对输入进行运算,把结果打印出来。 318 | 319 | 320 | 这个程序要一直运行,直到用户输入『done』才停止,然后输出最后一次计算的表达式的值。 321 | 322 | ## 练习3 323 | 324 | 传奇的数学家拉马努金发现了一个无穷级数(1914年的论文),能够用来计算圆周率倒数的近似值: 325 | 326 | 327 | ![](http://7xnq2o.com1.z0.glb.clouddn.com/ThinkPython7.e3.jpg) 328 | 329 | 330 | (译者注:这位拉马努金是一位非常杰出的数学家,自学成才,以数论为主要研究内容,可惜33岁的时候就英年早逝。他被哈代誉为超越希尔伯特的天才。) 331 | 332 | 写一个名叫estimate_pi的函数,用上面这个方程来计算并返回一个圆周率π的近似值。要使用一个while循环来计算出总和的每一位,最后一位要小于10的-15次方。你可以对比一下计算结果和Python内置的math.pi。 333 | 334 | 335 | >[样例代码](http://thinkpython2.com/code/pi.py) 336 | -------------------------------------------------------------------------------- /chapter8.md: -------------------------------------------------------------------------------- 1 | # 第八章 字符串 2 | 3 | 字符串和整形、浮点数以及布尔值很不一样。一个字符串是一个序列,意味着是对其他值的有序排列。在本章你将学到如何读取字符串中的字符,你还会学到一些字符串相关的方法。 4 | 5 | ## 8.1 字符串是序列 6 | 7 | 字符串就是一串有序的字符。你可以通过方括号操作符,每次去访问字符串中的一个字符: 8 | 9 | ```Python 10 | >>> fruit = 'banana' 11 | >>> letter = fruit[1] 12 | ``` 13 | 第二个语句选择了 fruit 这个字符串的序号为1的字符,并把这个字符赋值给了 letter 这个变量。 14 | 15 | (译者注:思考一下这里的 letter 是一个什么类型的变量。) 16 | 17 | 方括号内的内容叫做索引。索引指示了你所指定的字符串中字符的位置(就跟名字差不多)。 18 | 19 | 20 | 但你可能发现得到的结果和你预期的有点不一样: 21 | 22 | ```Python 23 | >>> letter 24 | 'a' 25 | ``` 26 | 27 | 大多数人都认为banana 的第『1』个字符应该是 b,而不是 a。但对于计算机科学家来说,索引是字符串从头的偏移量,所以真正的首字母偏移量应该是0. 28 | 29 | ```Python 30 | >>> letter = fruit[0] 31 | >>> letter 32 | 'b' 33 | ``` 34 | 35 | 所以 b 就是字符串 banana 的第『0』个字符,而 a 是第『1』个,n 就是第『2』个了。 36 | 37 | 你可以在方括号内的索引中使用变量和表达式: 38 | 39 | ```Python 40 | >>> i = 1 41 | >>> fruit[i] 42 | 'a' 43 | >>> fruit[i+1] 44 | 'n' 45 | ``` 46 | 47 | 但要注意的事,索引的值必须是整形的。否则你就会遇到类型错误了: 48 | 49 | ```Python 50 | >>> letter = fruit[1.5] 51 | TypeError: string indices must be integers 52 | ``` 53 | ## 8.2 len 长度 54 | 55 | len 是一个内置函数,会返回一个字符串中字符的长度: 56 | 57 | ```Python 58 | >>> fruit = 'banana' 59 | >>> len(fruit) 6 60 | ``` 61 | 62 | 要得到一个字符串的最后一个字符,你可能会想到去利用 len 函数: 63 | 64 | ```Python 65 | >>> length = len(fruit) 66 | >>> last = fruit[length] 67 | IndexError: string index out of range 68 | ``` 69 | 出现索引错误的原因就是banana 这个字符串在第『6』个位置是没有字母的。因为我们从0开始数,所以这一共6个字母的顺序是0到5号。因此要得到最后一次字符,你需要在字符串长度的基础上减去1才行: 70 | 71 | ```Python 72 | >>> last = fruit[length-1] 73 | >>> last 74 | 'a' 75 | ``` 76 | 或者你也可以用负数索引,意思就是从字符串的末尾向前数几位。fruit[-1]这个表达式给你最后一个字符,fruit[-2]给出倒数第二个,依此类推。 77 | 78 | ## 8.3 用 for 循环遍历字符串 79 | 80 | 很多计算过程都需要每次从一个字符串中拿一个字符。一般都是从头开始,依次得到每个字符,然后做点处理,然后一直到末尾。这种处理模式叫遍历。写一个遍历可以使用 while 循环: 81 | 82 | ```Python 83 | index = 0 84 | while index < len(fruit): 85 | letter = fruit[index] 86 | print(letter) 87 | index = index + 1 88 | ``` 89 | 90 | 这个循环遍历了整个字符串,然后它再把买一个字符显示在一行上面。循环条件是 index 这个变量小于字符串 fruit 的长度,所以当 index 与字符串长度相等的时候,条件就不成立了,循环体就不运行了。最后一个字符被获取的时候,index 正好是len(fruit)-1,这就已经是该字符串的最后一个字符了。 91 | 92 | 下面就练习一下了,写一个函数,接收一个字符串做参数,然后倒序显示每一个字符,每行显示一个。 93 | 94 | 另外一种遍历的方法就是 for 循环了: 95 | 96 | ```Python 97 | for letter in fruit: 98 | print(letter) 99 | ``` 100 | 101 | 每次循环之后,字符串中的下一个字符都会赋值给变量 letter。循环在进行到没有字符剩余的时候就停止了。 102 | 103 | 104 | 下面的例子展示了如何使用级联(字符串加法)以及一个 for 循环来生成一个简单的序列(用字母表顺序)。 105 | 106 | 107 | 在 Robert McCloskey 的一本名叫《Make Way for Ducklings》的书中,小鸭子的名字依次为:Jack, Kack, Lack, Mack, Nack, Ouack, Pack, 和Quack。下面这个循环会依次输出他们的名字: 108 | 109 | ```Python 110 | prefixes = 'JKLMNOPQ' 111 | suffix = 'ack' 112 | for letter in prefixes: 113 | print(letter + suffix) 114 | ``` 115 | 116 | 输出结果如下: 117 | 118 | ```Python 119 | Jack Kack Lack Mack Nack Oack Pack Qack 120 | ``` 121 | 122 | 当然了,有点不准确的地方,因为有“Ouack”和 “Quack”两处拼写错了。做个练习,修改一下程序,改正这个错误。 123 | 124 | ## 8.4 字符串切片 125 | 126 | 字符串的一段叫做切片。从字符串中选择一部分做切片,与选择一个字符有些相似: 127 | 128 | ```Python 129 | >>> s = 'Monty Python' 130 | >>> s[0:5] 131 | 'Monty' 132 | >>> s[6:12] 133 | 'Python' 134 | ``` 135 | 136 | [n:m]这种操作符,会返回字符串中从第『n』个到第『m』个的字符,包含开头的第『n』个,但不包含末尾的第『m』个。这个设计可能有点违背直觉,但可能有助于想象这个切片在字符串中的方向,如图8.1。 137 | 138 | ________________________________________ 139 | ![Figure 8.1](./images/figure8.1.jpg) 140 | Figure 8.1: Slice indices. 141 | ________________________________________ 142 | 143 | 如果你忽略了第一个索引(就是冒号前面的那个),切片会默认从字符串头部开始。如果你忽略了第二个索引,切片会一直包含到最后一位: 144 | 145 | ```Python 146 | >>> fruit = 'banana' 147 | >>> fruit[:3] 148 | 'ban' 149 | >>> fruit[3:] 150 | 'ana' 151 | ``` 152 | 如果两个索引相等,得到的就是空字符串了,用两个单引号来表示: 153 | 154 | ```Python 155 | >>> fruit = 'banana' 156 | >>> fruit[3:3] 157 | '' 158 | ``` 159 | 空字符串不包含字符,长度为0,除此之外,都与其他字符串是一样的。 160 | 161 | 那么来练习一下,你觉得 fruit[:]这个是什么意思?在程序中试试吧。 162 | 163 | ## 8.5 字符串不可修改 164 | 165 | 大家总是有可能想试试把方括号在赋值表达式的等号左侧,试图去更改字符串中的某一个字符。比如: 166 | 167 | ```Python 168 | >>> greeting = 'Hello, world!' 169 | >>> greeting[0] = 'J' 170 | TypeError: 'str' object does not support item assignment 171 | ``` 172 | 173 | 『object』是对象的意思,这里指的是字符串类 string,然后『item』是指你试图赋值的字符串中的字符。目前来说,一个对象就跟一个值差不多,但后续在第十章第十节我们再对这个定义进行详细讨论。 174 | 175 | 176 | 产生上述错误的原因是字符串是不能被修改的,这意味着你不能对一个已经存在的字符串进行任何改动。你顶多也就能建立一个新字符串,新字符串可以基于旧字符串进行一些改动。 177 | 178 | ```Python 179 | >>> greeting = 'Hello, world!' 180 | >>> new_greeting = 'J' + greeting[1:] 181 | >>> new_greeting 182 | 'Jello, world!' 183 | ``` 184 | 185 | 上面的例子中,对 greeting 这个字符串进行了切片,然后添加了一个新的首字母过去。这并不会对原始字符串有任何影响。(译者注:也就是 greeting 这个字符串的值依然是原来的值,是不可改变的。) 186 | 187 | ## 8.6 搜索 188 | 下面这个函数是干啥的? 189 | 190 | ```Python 191 | def find(word, letter): 192 | index = 0 193 | while index < len(word): 194 | if word[index] == letter: 195 | return index 196 | index = index + 1 197 | return -1 198 | ``` 199 | 200 | 201 | ```Python 202 | # 改进的find函数,利用列表收集字母letter在单词word中出现的全部位置. 203 | def find(word, letter): 204 | index = 0 205 | 206 | result_list=[] 207 | 208 | while index < len(word): 209 | if word[index] == letter: 210 | 211 | result_list.append(index) 212 | 213 | index = index + 1 214 | 215 | return result_list 216 | 217 | find('banana','a') 218 | ``` 219 | 220 | 简单来说,find 函数,也就是查找,是方括号操作符[]的逆运算。方括号是知道索引然后提取对应的字符,而查找函数是选定一个字符去查找这个字符出现的索引位置。如果字符没有被找到,函数就返回-1。 221 | 222 | 223 | 这是我们见过的第一个返回语句位于循环体内的例子。如果word[index]等于letter,函数就跳出循环立刻返回。如果字符在字符串里面没出现,程序正常退出循环并且返回-1。 224 | 225 | 这种计算-遍历一个序列然后返回我们要找的东西的模式就叫做搜索了。 226 | 227 | 做个练习,修改一下 find 函数,加入第三个参数,这个参数为查找开始的字符串位置。 228 | 229 | ## 8.7 循环和计数 230 | 231 | 下面这个程序计算了字母 a 在一个字符串中出现的次数: 232 | 233 | ```Python 234 | word = 'banana' 235 | count = 0 236 | for letter in word: 237 | if letter == 'a': 238 | count = count + 1 239 | print(count) 240 | ``` 241 | 242 | 这一程序展示了另外一种计算模式,叫做计数。变量 count 被初始化为0,然后每次在字符串中找到一个 a,就让 count 加1.当循环退出的时候,count 就包含了 a 出现的总次数。 243 | 244 | 做个练习,把上面的代码封装进一个名叫 count 的函数中,泛化一下,一遍让他接收任何字符串和字母作为参数。 245 | 246 | 然后再重写一下这个函数,这次不再让它遍历整个字符串,而使用上一节中练习的三参数版本的 find 函数。 247 | 248 | ## 8.8 字符串方法 249 | 250 | 字符串提供了一些方法,这些方法能够进行很多有用的操作。方法和函数有些类似,也接收参数然后返回一个值,但语法稍微不同。比如,upper 这个方法就读取一个字符串,返回一个全部为大写字母的新字符串。 251 | 252 | 与函数的 upper(word)语法不同,方法的语法是 word.upper()。 253 | 254 | ```Python 255 | >>> word = 'banana' 256 | >>> new_word = word.upper() 257 | >>> new_word 258 | 'BANANA' 259 | ``` 260 | 261 | 这种用点号分隔的方法表明了使用的方法名字为 upper,使用这个方法的字符串的名字为 word。后面括号里面是空白的,表示这个方法不接收参数。 262 | 263 | A method call is called an invocation;方法的调用被叫做——调用(译者注:中文都混淆成调用,英文里面 invocation 和 invoke 都有祈祷的意思,和 call 有显著的意义差别,但中文都混淆成调用,这种例子不胜枚举,所以大家尽量多读原版作品。);在这里,我们就说调用了 word 的 upper 方法。 264 | 265 | 266 | 结果我们发现string 有一个方法叫做 find,跟我们写过的函数 find 有惊人的相似: 267 | 268 | ```Python 269 | >>> word = 'banana' 270 | >>> index = word.find('a') 271 | >>> index 272 | 1 273 | ``` 274 | 275 | 在这里我们调用了 word 的 find 方法,然后给定了我们要找的字母 a 作为一个参数。 276 | 277 | 实际上,这个 find 方法比我们的 find 函数功能更通用;它不仅能查找字符,还能查找字符串: 278 | 279 | ```Python 280 | >>> word.find('na') 281 | 2 282 | ``` 283 | 默认情况下 find 方法从字符串的开头来查找,不过可以给它一个第二个参数,让它从指定位置查找: 284 | 285 | ```Python 286 | >>> word.find('na', 3) 287 | 4 288 | ``` 289 | 290 | 这是一个可选参数的例子;find 方法还能接收第三个参数,可以指定查找终止的位置: 291 | 292 | ```Python 293 | >>> name = 'bob' 294 | >>> name.find('b', 1, 2) 295 | -1 296 | ``` 297 | 298 | 这个搜索失败了,因为 b 并没有在索引1到2且不包括2的字符中间出现。搜索到指定的第三个变量作为索引的位置,但不包括该位置,这就让 find 方法与切片操作符相一致。 299 | 300 | ## 8.9 运算符 in 301 | 302 | in 这个词在字符串操作中是一个布尔操作符,它读取两个字符串,如果前者的字符串为后者所包含,就返回真,否则为假: 303 | 304 | ```Python 305 | >>> 'a' in 'banana' 306 | True 307 | >>> 'seed' in 'banana' 308 | False 309 | ``` 310 | 举个例子,下面的函数显示所有同时在 word1和 word2当中出现的字母: 311 | 312 | ```Python 313 | def in_both(word1, word2): 314 | for letter in word1: 315 | if letter in word2: 316 | print(letter) 317 | ``` 318 | 选好变量名的话,Python 有时候读起来就跟英语差不多。你读一下这个循环,就能发现,『对第一个 word 当中的每一个字母letter,如果这个字母也在第二个 word 当中出现,就输出这个字母 letter。』 319 | 320 | ```Python 321 | >>> in_both('apples', 'oranges') 322 | a e s 323 | ``` 324 | ## 8.10 字符串比较 325 | 326 | 关系运算符对于字符串来说也可用。比如可以看看两个字符串是不是相等: 327 | 328 | ```Python 329 | if word == 'banana': 330 | print('All right, bananas.') 331 | ``` 332 | 333 | 其他的关系运算符可以来把字符串按照字母表顺序排列: 334 | 335 | ```Python 336 | if word < 'banana': 337 | print('Your word, ' + word + ', comes before banana.') 338 | elif word > 'banana': 339 | print('Your word, ' + word + ', comes after banana.') 340 | else: 341 | print('All right, bananas.') 342 | ``` 343 | 344 | Python 对大小写字母的处理与人类常规思路不同。所有大写字母都在小写字母之前,所以顺序上应该是:Your word,然后是 Pineapple,然后才是 banana。 345 | 346 | 347 | 一个解决这个问题的普遍方法是把字符串转换为标准格式,比如都转成小写的,然后再进行比较。一定要记得哈,以免你遇到一个用 Pineapple 武装着自己的家伙的时候手足无措。 348 | 349 | ## 8.11 调试 350 | 351 | 使用索引来遍历一个序列中的值的时候,弄清楚遍历的开头和结尾很不容易。下面这个函数用来对比两个单词,如果一个是另一个的倒序就返回真,但这个函数代码中有两处错误: 352 | 353 | ```Python 354 | def is_reverse(word1, word2): 355 | if len(word1) != len(word2): 356 | return False 357 | i = 0 358 | j = len(word2) 359 | while j > 0: 360 | if word1[i] != word2[j]: 361 | return False 362 | i = i+1 363 | j = j-1 364 | return True 365 | ``` 366 | 367 | 第一个 if 语句是检查两个词的长度是否一样。如果不一样长,当场就返回假。对函数其余部分,我们假设两个单词一样长。这里用到了守卫模式,在第6章第8节我们提到过。 368 | 369 | 370 | i 和 j 都是索引:i 从头到尾遍历单词 word1,而 j 逆向遍历单词word2.如果我们发现两个字母不匹配,就可以立即返回假。如果经过整个循环,所有字母都匹配,就返回真。 371 | 372 | 如果我们用这个函数来处理单词『pots』和『stop』,我们希望函数返回真,但得到的却是索引错误: 373 | 374 | ```Python 375 | >>> is_reverse('pots', 'stop') 376 | ... File "reverse.py", line 15, in is_reverse if word1[i] != word2[j]: IndexError: string index out of range 377 | ``` 378 | 379 | 为了改正这个错误,第一步就是在出错的那行之前先输出索引的值。 380 | 381 | ```Python 382 | while j > 0: 383 | print(i, j) # print here 384 | if word1[i] != word2[j]: 385 | return False 386 | i = i+1 387 | j = j-1 388 | ``` 389 | 390 | 然后我再次运行函数,得到更多信息了: 391 | 392 | ```Python 393 | >>> is_reverse('pots', 'stop') 394 | 0 4 395 | ... IndexError: string index out of range 396 | ``` 397 | 398 | 第一次循环完毕的时候,j 的值是4,这超出了『pots』这个字符串的范围了(译者注:应该是0-3)。最后一个索引应该是3,所以 j 的初始值应该是 len(word2)-1。 399 | 400 | ```Python 401 | >>> is_reverse('pots', 'stop') 402 | 0 3 1 2 2 1 403 | True 404 | ``` 405 | 406 | 这次我们得到了正确的结果,但似乎循环只走了三次,这有点奇怪。为了弄明白带到怎么回事,我们可以画一个状态图。在第一次迭代的过程中,is_reverse 的框架如图8.2所示。 407 | 408 | ________________________________________ 409 | ![Figure 8.2](./images/figure8.2.jpg) 410 | Figure 8.2: State diagram. 411 | ________________________________________ 412 | 413 | 我通过设置变量框架中添加虚线表明,i和j的值显示在人物word1and word2拿许可证。 414 | 415 | 从这个图上运行的程序,文件,更改这些值I和J在每一次迭代过程。发现并解决此函数中的二次错误。 416 | 417 | ## 8.12 Glossary 术语列表 418 | object: 419 | Something a variable can refer to. For now, you can use “object” and “value” interchangeably. 420 | 421 | >对象:一个值能够指代的东西。目前为止,你可以把对象和值暂且作为一码事来理解。 422 | 423 | sequence: 424 | An ordered collection of values where each value is identified by an integer index. 425 | 426 | >序列:一系列值的有序排列,每一个值都有一个唯一的整数序号。 427 | 428 | item: 429 | One of the values in a sequence. 430 | 431 | >元素:一列数值序列当中的一个值。 432 | 433 | index: 434 | An integer value used to select an item in a sequence, such as a character in a string. In Python indices start from 0. 435 | 436 | >索引:一个整数值,用来指代一个序列中的特定一个元素,比如在字符串里面就指代一个字符。在 Python 里面索引从0开始计数。 437 | 438 | slice: 439 | A part of a string specified by a range of indices. 440 | 441 | >切片:字符串的一部分,通过一个索引区间来取得。 442 | 443 | empty string: 444 | A string with no characters and length 0, represented by two quotation marks. 445 | 446 | >空字符串:没有字符的字符串,长度为0,用两个单引号表示。 447 | 448 | immutable: 449 | The property of a sequence whose items cannot be changed. 450 | 451 | >不可更改:一个序列中所有元素不能被改变的性质。 452 | 453 | traverse: 454 | To iterate through the items in a sequence, performing a similar operation on each. 455 | 456 | >遍历:在一个序列中依次对每一个元素进行某种相似运算的过程。 457 | 458 | search: 459 | A pattern of traversal that stops when it finds what it is looking for. 460 | 461 | >搜索:一种遍历的模式,找到要找的内容的时候就停止。 462 | 463 | counter: 464 | A variable used to count something, usually initialized to zero and then incremented. 465 | 466 | >计数:一种用来统计某种东西数量的变量,一般初始化为0,然后逐次递增。 467 | 468 | invocation: 469 | A statement that calls a method. 470 | 471 | >方法调用:调用方法的语句。 472 | 473 | optional argument: 474 | A function or method argument that is not required. 475 | 476 | >可选参数:一个函数或者方法中有一些参数是可选的,非必需的。 477 | 478 | ## 8.13 练习 479 | ## # 练习1 480 | 481 | 阅读 [这里](http://docs.python.org/3/library/stdtypes.html# string-methods)关于字符串的文档。你也许会想要试试其中一些方法,来确保你理解它们的意义。比如 strip 和 replace 都特别有用。 482 | 483 | 文档的语法有可能不太好理解。比如在find 这个方法中,方括号表示了可选参数。所以 sub 是必须的参数,但 start 是可选的,如果你包含了 start,end 就是可选的了。 484 | 485 | ## # 练习2 486 | 字符串有个方法叫 count,与咱们在8.7中写的 count 函数很相似。 阅读一下这个方法的文档,然后写一个调用这个方法的代码,统计一下 banana 这个单词中 a 出现的次数 。 487 | 488 | ## # 练习3 489 | 字符串切片可以使用第三个索引,作为步长来使用;步长的意思就是取字符的间距。一个步长为2的意思就是每隔一个取一个字符;3的意思就是每次取第三个,以此类推。 490 | 491 | ```Python 492 | >>> fruit = 'banana' 493 | >>> fruit[0:5:2] 494 | 'bnn' 495 | ``` 496 | 步长如果为-1,意思就是倒序读取字符串,所以[::-1]这个切片就会生成一个逆序的字符串了。 497 | 498 | 使用这个方法把练习三当中的is_palindrome写成一个一行代码的版本。 499 | 500 | ## # 练习4 501 | 下面这些函数都试图检查一个字符串是不是包含小写字母,但他们当中肯定有些是错的。描述一下每个函数真正的行为(假设参数是一个字符串)。 502 | 503 | ```Python 504 | def any_lowercase1(s): 505 | for c in s: 506 | if c.islower(): 507 | return True 508 | else: 509 | return False 510 | def any_lowercase2(s): 511 | for c in s: 512 | if 'c'.islower(): 513 | return 'True' 514 | else: 515 | return 'False' 516 | def any_lowercase3(s): 517 | for c in s: 518 | flag = c.islower() 519 | return flag 520 | def any_lowercase4(s): 521 | flag = False 522 | for c in s: 523 | flag = flag or c.islower() 524 | return flag 525 | def any_lowercase5(s): 526 | for c in s: 527 | if not c.islower(): 528 | return False 529 | return True 530 | ``` 531 | ## # 练习5 532 | 533 | 凯撒密码是一种简单的加密方法,用的方法是把每个字母进行特定数量的移位。对一个字母移位就是把它根据字母表的顺序来增减对应,如果到末尾位数不够就从开头算剩余的位数,『A』移位3就是『D』,而『Z』移位1就是『A』了。 534 | 535 | 要对一个词进行移位,要把每个字母都移动同样的数量。比如『cheer』这个单词移位7就是『jolly』,而『melon』移位-10就是『cubed』。在电影《2001 太空漫游》中,飞船的电脑叫 HAL,就是 IBM 移位-1。 536 | 537 | 写一个名叫 rotate_word 的函数,接收一个字符串和一个整形为参数,返回将源字符串移位该整数位得到的新字符串。 538 | 539 | 你也许会用得上内置函数 ord,它把字符转换成数值代码,然后还有个 chr 是用来把数值代码转换成字符。字母表中的字母都被编译成跟字母表中同样的顺序了,所以如下所示: 540 | 541 | ```Python 542 | >>> ord('c') - ord('a') 543 | 2 544 | ``` 545 | 546 | c 是字母表中的第『2』个(译者注:从0开始数哈)的位置,所以上述结果是2。但注意:大写字母的数值代码是和小写的不一样的。 547 | 548 | 网上很多有冒犯意义的玩笑都是用 ROT13加密的,也就是移位13的凯撒密码。如果你不太介意,找一下这些密码解密一下吧。[样例代码](http://thinkpython2.com/code/rotate.py). 549 | 550 | -------------------------------------------------------------------------------- /chapter9.md: -------------------------------------------------------------------------------- 1 | # 第九章 案例学习:单词游戏 2 | 3 | 本章我们进行第二个案例学习,这一案例中涉及到了用搜索具有某些特征的单词来猜谜。比如,我们会发现英语中最长的回文词,然后搜索那些按照单词表顺序排列字母的单词。我还会给出一种新的程序开发计划:降低问题的复杂性和难度,还原到以前解决的问题。 4 | 5 | ## 9.1 读取字符列表 6 | 7 | 本章练习中,咱们需要用一个英语词汇列表。网上有很多,不过最适合我们的列表并且是共有领域的,莫过于 Grady Ward这份词汇表,这是Moby词典计划的一部分(点击[此链接访问详情](http://wikipedia.org/wiki/Moby_Project))。这是一份113,809个公认的字谜表;也就是公认可以用于字谜游戏以及其他文字游戏的单词。在 Moby 的词汇项目中,该词表的文件名为113809of.fic;你可以下载一份副本,这里名字简化成 words.txt 了,下载地址[在这里](http://thinkpython2.com/code/words.txt)。 8 | 9 | 10 | 这个文件就是纯文本,所以你可以用文本编辑器打开一下,不过也可以用 Python 来读取。Python 内置了一个叫open 的函数,接收文件名做参数,返回一个文件对象,你可以用它来读取文件。 11 | 12 | ```Python 13 | >>> fin = open('words.txt') 14 | ``` 15 | 16 | fin 是一个用来表示输入的文件的常用名字。这个文件对象提供了好几种读取的方法,包括逐行读取,这种方法是读取文本中的一整行直到结尾,然后把读取的内容作为字符串返回: 17 | 18 | ```Python 19 | >>> fin.readline() 20 | 'aa\r\n' 21 | ``` 22 | 23 | 这一列当中的第一个词就是『aa』了,这是一种**熔岩**(译者注:“aa”是夏威夷词汇,音“阿阿”,用来描述表面粗糙的熔岩流。译者本人就是地学专业的,都很少接触这个词,本教材作者真博学啊)。后面跟着的\r\n 的意思代表着有两个转义字符,一个是回车,一个是换行,这样把这个单词从下一个单词分隔开来。 24 | 25 | 文件对象会记录这个单词在源文件中的位置,所以下次你再调用 readline 的时候,就能得到下一个词了: 26 | 27 | ```Python 28 | >>> fin.readline() 29 | 'aah\r\n' 30 | ``` 31 | 32 | 下一个词是『aah』,这完全是一个正规的词汇,不要怪异眼神看我哦。另外如果转义字符让你很烦,咱们可以稍加修改来去掉它,用字符串的 strip 方法即可: 33 | 34 | ```Python 35 | >>> line = fin.readline() 36 | >>> word = line.strip() 37 | >>> word 38 | 'aahed' 39 | ``` 40 | 41 | 在 for 循环中也可以使用文件对象。下面的这个程序读取整个 words.txt 文件,然后每行输出一个词: 42 | 43 | ```Python 44 | fin = open('words.txt') 45 | for line in fin: 46 | word = line.strip() 47 | print(word) 48 | ``` 49 | ## 9.2 练习 50 | 51 | 下面这些练习都有样例代码。不过你最好在看答案之前先自己对每个练习都尝试一下。 52 | 53 | ## # 练习1 54 | 55 | 写一个程序读取 words.txt,然后只输出超过20个字母长度的词(这个长度不包括转义字符)。 56 | 57 | ## # 练习2 58 | 59 | 在1939年,作家厄尔尼斯特·文森特·莱特曾经写过一篇5万字的小说《葛士比》,里面没有一个字母e。因为在英语中 e 是用的次数最多的字母,所以这很不容易的。事实上,不使用最常见的字符,都很难想出一个简单的想法。一开始很慢,不过仔细一些,经过几个小时的训练之后,你就逐渐能做到了。 60 | 61 | 好了,我不扯淡了。 写一个名字叫做 has_no_e 的函数,如果给定词汇不含有 e 就返回真,否则为假。 62 | 63 | 修改一下上一节的程序代码,让它只打印单词表中没有 e 的词汇,并且统计一下这些词汇在总数中的百分比例。 64 | 65 | ## # 练习3 66 | 67 | 写一个名叫 avoids 的函数,接收一个单词和一个禁用字母组合的字符串,如果单词不含有该字符串中的任何字母,就返回真。 修改一下程序代码,提示用户输入一个禁用字母组合的字符串,然后输入不含有这些字母的单词数目。你能找到5个被禁用字母组合,排除单词数最少吗? 68 | 69 | ## # 练习4 70 | 71 | 写一个名叫uses_only的函数,接收一个单词和一个字母字符串,如果单词仅包含该字符串中的字母,就返回真。你能仅仅用 acefhlo 这几个字母造句子么?或者试试『Hoe alfalfa』? 72 | 73 | ## # 练习5 74 | 75 | 写一个名字叫uses_all的函数,接收一个单词和一个必需字母组合的字符串,如果单词对必需字母组合中的字母至少都用了一次就返回真。有多少单词都用到了所有的元音字母 aeiou?aeiouy的呢? 76 | 77 | ## # 练习6 78 | 写一个名字叫is_abecedarian的函数,如果单词中所有字母都是按照字母表顺序出现就返回真(重叠字母也是允许的)。有多少这样的单词? 79 | 80 | ## 9.3 搜索 81 | 82 | 刚刚的那些练习都有一些相似之处:都可以用我们在8.6学过的搜索来解决。下面是一个最简化的例子: 83 | 84 | ```Python 85 | def has_no_e(word): 86 | for letter in word: 87 | if letter == 'e': 88 | return False 89 | return True 90 | ``` 91 | 这个 for 循环遍历了单词的所有字母。如果找到了字母e,就立即返回假;否则就到下一个字母。如果正常退出了循环,意味着我们没找到 e,就返回真。 92 | 93 | 你可以使用 in 运算符,把这个函数写得更精简,我之所以用一个稍显麻烦的版本,是为了说明搜索模式的逻辑过程。 94 | 95 | 96 | avoids 是一个更通用版本的has_no_e函数的实现,它的结构是一样的: 97 | 98 | ```Python 99 | def avoids(word, forbidden): 100 | for letter in word: 101 | if letter in forbidden: 102 | return False 103 | return True 104 | ``` 105 | 只要找到了禁用字母就可以立即返回假;如果运行到了循环末尾,就返回真。 106 | 107 | 108 | uses_only与之相似,无非是条件与之相反了而已。 109 | 110 | ```Python 111 | def uses_only(word, available): 112 | for letter in word: 113 | if letter not in available: 114 | return False 115 | return True 116 | ``` 117 | 118 | 这次不是有一个禁用字母列表,我们这次用一个可用字母列表。如果在单词中发现不在可用字母列表中的,就返回假了。 119 | 120 | uses_all这个函数与之也相似,不过我们转换了单词和字母字符串的角色: 121 | 122 | ```Python 123 | def uses_all(word, required): 124 | for letter in required: 125 | if letter not in word: 126 | return False 127 | return True 128 | ``` 129 | 这次并没有遍历单词中的所有字母,循环遍历了所有指定的字母。如果有任何指定字母没有在单词中出新啊,就返回假。如果你已经像计算机科学家一样思考了,你就应该已经发现了uses_all是对之前我们解决过问题的一个实例,你已经写过这个代码了: 130 | 131 | ```Python 132 | def uses_all(word, required): 133 | return uses_only(required, word) 134 | ``` 135 | 、这就是一种新的程序开发规划模式,就是降低问题的复杂性和难度,还原到以前解决的问题,意思是你发现正在面对的问题是之前解决过的问题的一个实例,就可以用已经存在的方案来解决。 136 | 137 | ## 9.4 用索引循环 138 | 139 | 上面的章节中我写了各种用 for 循环的函数,因为当时只需要字符串中的字符;这就不需要理会索引。 140 | 141 | 但is_abecedarian这个函数中,我们需要对比临近的两个字母,所以用 for 循环就不那么好写了: 142 | 143 | ```Python 144 | def is_abecedarian(word): 145 | previous = word[0] 146 | for c in word: 147 | if c < previous: 148 | return False 149 | previous = c 150 | return True 151 | ``` 152 | 153 | 一种很好的替代思路就是使用递归: 154 | 155 | ```Python 156 | def is_abecedarian(word): 157 | if len(word) <= 1: 158 | return True 159 | if word[0] > word[1]: 160 | return False 161 | return is_abecedarian(word[1:]) 162 | ``` 163 | 164 | 另外一种方法是用 while 循环: 165 | 166 | ```Python 167 | def is_abecedarian(word): 168 | i = 0 169 | while i < len(word)-1: 170 | if word[i+1] < word[i]: 171 | return False 172 | i = i+1 173 | return True 174 | ``` 175 | 176 | 循环开始于 i 等于0,然后在 i 等于len(word)-1的时候结束。每次通过循环的时候,都对比第 i 个字符(你可以就当是当前字符)与第 i+1个字符(就当作下一个字符)。 177 | 178 | 179 | 如果下一个字符比当前字符小(字母表排列顺序在当前字符前面),我们就发现这个不符合字母表顺序了,跳出返回假就可以了。 180 | 181 | 182 | 如果一直到循环结尾都没有发现问题,这个词就通过检验了。为了确信循环正确结束了,可以拿单词『flossy』作为例子来试试。单词长度是6,所以循环终止的时候 i 应该是4,也就是倒数第二个位置。在最后一次循环中,比较的是倒数第二个和最后一个字母,这正是符合我们设计的。 183 | 184 | 185 | 下面这个是练习3的is_palindrome的一个版本,使用了两个索引;一个从头开始一直到结尾;另外一个从末尾开始逆序进行。 186 | 187 | ```Python 188 | def is_palindrome(word): 189 | i = 0 190 | j = len(word)-1 191 | while i程序测试可以用来表明 bug 的存在,但永远不能表明 bug 不存在。 227 | 228 | 229 | >— Edsger W. Dijkstra 230 | 231 | ## 9.6 Glossary 术语列表 232 | file object: 233 | A value that represents an open file. 234 | 235 | >文件对象:代表了一份打开的文件的值。 236 | 237 | reduction to a previously-solved problem: 238 | A way of solving a problem by expressing it as an instance of a previously-solved problem. 239 | 240 | >降低问题的复杂性和难度,还原到以前解决的问题:一种解决问题的方法,把问题表达成过去解决过问题的一个特例。 241 | 242 | special case: 243 | A test case that is a typical or non-obvious (and less likely to be handled correctly). 244 | 245 | >特殊案例:很典型或者不明显的测试用的案例,往往都很不容易正确处理。 246 | 247 | ## 9.7 练习 248 | ## # 练习7 249 | [这个问题](http://www.cartalk.com/content/puzzlers)基于一个谜语,这个谜语在广播节目 Car Talk 上面播放过: 250 | 251 | 给我一个有三个连续双字母的单词。我会给你一对基本符合的单词,但并不符合。例如, committee 这个单词,C O M M I T E。如果不是有单独的一个 i 在里面,就基本完美了,或者Mississippi 这个词:M I S I S I P I。如果把这些个 i 都去掉就好了。但有一个词正好是三个重叠字母,而且据我所知这个词可能是唯一一个这样的词。当然有有可能这种单词有五百多个呢,但我只能想到一个。是哪个词呢?写个程序来找一下这个词吧。 252 | 253 | [样例代码](http://thinkpython2.com/code/cartalk1.py) 254 | 255 | ## # 练习8 256 | [这个](http://www.cartalk.com/content/puzzlers)又是一个 Car Talk 谜语: 257 | 258 | 259 | 有一天我在高速路上开着车,碰巧看了眼里程表。和大多数里程表一样,是六位数字的,单位是英里。加入我的车跑过300,000英里了,看到的结果就是3-0-0-0-0-0. 260 | 261 | 我那天看到的很有趣,我看到后四位是回文的;就是说后四位正序和逆序读是一样的。例如5-4-4-5就是一个回文数,所以我的里程表可能读书就是3-1-5-4-4-5. 262 | 263 | 过了一英里之后,后五位数字是回文的了。举个例子,可能读书就是3-6-5-4-5-6。又过了一英里,六个数字中间的数字是回文数了。准备好更复杂的了么?又过了一英里,整个六位数都是回文的了! 264 | 265 | 那么问题俩了:我最开始看到的里程表的度数应该是多少? 266 | 267 | 写个 Python 程序来检测一下所有的六位数,然后输出一下满足这些要求的数字。 [样例代码](http://thinkpython2.com/code/cartalk2.py) 268 | 269 | ## # 练习9 270 | [这个](http://www.cartalk.com/content/puzzlers)又是一个 Car Talk 谜语,你可以用搜索来解决: 271 | 272 | 最近我看望了妈妈,然后我们发现我的年龄反过来正好是她的年龄。例如,假如她是73岁,我就是37岁了。我们好奇这种情况发生多少次,但中间叉开了话题,没有想出来这个问题的答案。 273 | 274 | 我回家之后,我发现到目前位置我们的年龄互为逆序已经是六次了,我还发现如果我们幸运的话过几年又会有一次,如果我们特别幸运,就还会再有一次这样情况。换句话说,就是总共能有八次。那么问题来了:我现在多大了? 275 | 276 | 写一个 Python 程序,搜索一下这个谜语的解。提示一下:你可能发现字符串的 zfill 方法很有用哦。 277 | 278 | [样例代码](http://thinkpython2.com/code/cartalk3.py) 279 | 280 | -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/cover.jpg -------------------------------------------------------------------------------- /images/figure10.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure10.1.jpg -------------------------------------------------------------------------------- /images/figure10.2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure10.2.jpg -------------------------------------------------------------------------------- /images/figure10.3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure10.3.jpg -------------------------------------------------------------------------------- /images/figure10.4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure10.4.jpg -------------------------------------------------------------------------------- /images/figure10.5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure10.5.jpg -------------------------------------------------------------------------------- /images/figure11.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure11.1.jpg -------------------------------------------------------------------------------- /images/figure11.2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure11.2.jpg -------------------------------------------------------------------------------- /images/figure12.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure12.1.jpg -------------------------------------------------------------------------------- /images/figure12.2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure12.2.jpg -------------------------------------------------------------------------------- /images/figure15.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure15.1.jpg -------------------------------------------------------------------------------- /images/figure15.2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure15.2.jpg -------------------------------------------------------------------------------- /images/figure15.3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure15.3.jpg -------------------------------------------------------------------------------- /images/figure16.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure16.1.jpg -------------------------------------------------------------------------------- /images/figure18.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure18.1.jpg -------------------------------------------------------------------------------- /images/figure18.2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure18.2.jpg -------------------------------------------------------------------------------- /images/figure2.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure2.1.jpg -------------------------------------------------------------------------------- /images/figure3.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure3.1.jpg -------------------------------------------------------------------------------- /images/figure4.1-4.2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure4.1-4.2.jpg -------------------------------------------------------------------------------- /images/figure5.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure5.1.jpg -------------------------------------------------------------------------------- /images/figure5.2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure5.2.jpg -------------------------------------------------------------------------------- /images/figure6.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure6.1.jpg -------------------------------------------------------------------------------- /images/figure7.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure7.1.jpg -------------------------------------------------------------------------------- /images/figure8.1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure8.1.jpg -------------------------------------------------------------------------------- /images/figure8.2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cycleuser/ThinkPython2-CN/870549347f8e1782b9c3528a8fc5de5c8c201fff/images/figure8.2.jpg --------------------------------------------------------------------------------