├── .gitignore ├── 1.1.md ├── 1.2.md ├── 1.3.md ├── 1.4.md ├── 1.5.md ├── 1.6.md ├── 2.1.md ├── 2.2.md ├── 2.3.md ├── 2.4.md ├── 2.5.md ├── 2.6.md ├── 2.7.md ├── 3.1.md ├── 3.2.md ├── 3.3.md ├── 3.4.md ├── 3.5.md ├── 3.6.md ├── README.md ├── SUMMARY.md ├── ch1.md ├── ch2.md ├── ch3.md ├── ch4.md ├── ch5.md ├── cover.jpg ├── img ├── 20160907175856.jpg ├── barriers.png ├── clientserver.png ├── constraints.png ├── coroutine.png ├── curves.png ├── deadlock.png ├── eval_apply.png ├── evaluate_square.png ├── evaluate_sum_squares_0.png ├── evaluate_sum_squares_1.png ├── evaluate_sum_squares_3.png ├── expression_tree.png ├── fact.png ├── factorial_machine.png ├── fib.png ├── fib_env.png ├── function_abs.png ├── function_print.png ├── getitem_rlist_0.png ├── getitem_rlist_1.png ├── getitem_rlist_2.png ├── global_frame.png ├── global_frame_assignment.png ├── global_frame_def.png ├── interface.png ├── iter_improve_apply.png ├── iter_improve_global.png ├── lists.png ├── logo_apply.png ├── logo_eval.png ├── multiple_inheritance.png ├── nested_pairs.png ├── newton.png ├── nonlocal_assign.png ├── nonlocal_call.png ├── nonlocal_call2.png ├── nonlocal_corefer.png ├── nonlocal_def.png ├── nonlocal_def2.png ├── nonlocal_recall.png ├── pair.png ├── pi_sum.png ├── pig_latin.png ├── produce-filter-consume.png ├── qr_alipay.png ├── read-match-coroutine.png ├── scope.png ├── sequence.png ├── set_trees.png ├── sier.png ├── square_root.png ├── square_root_update.png ├── star.png ├── subroutine.png ├── tree.png ├── universal_machine.png ├── vector-math1.png ├── vector-math2.png └── vector-math3.png └── styles └── ebook.css /.gitignore: -------------------------------------------------------------------------------- 1 | _book 2 | -------------------------------------------------------------------------------- /1.1.md: -------------------------------------------------------------------------------- 1 | # 1.1 引言 2 | 3 | > 来源:[1.1 Introduction](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/functions.html#introduction) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 计算机科学是一个极其宽泛的学科。全球的分布式系统、人工智能、机器人、图形、安全、科学计算,计算机体系结构和许多新兴的二级领域,每年都会由于新技术和新发现而扩展。计算机科学的快速发展广泛影响了人类生活。商业、通信、科学、艺术、休闲和政治都被计算机领域彻底改造。 10 | 11 | 计算机科学的巨大生产力可能只是因为它构建在一系列优雅且强大的基础概念上。所有计算都以表达信息、指定处理它所需的逻辑、以及设计管理逻辑复杂性的抽象作为开始。对这些基础的掌握需要我们精确理解计算机如何解释程序以及执行计算过程。 12 | 13 | 这些基础概念在伯克利长期教授,使用由Harold Abelson、Gerald Jay Sussman和Julie Sussman创作的经典教科书《计算机科学的构造与解释》(SICP)。这个讲义大量借鉴了这本书,原作者慷慨地使它可用于改编和复用。 14 | 15 | 我们的智力之旅一旦出发就不能回头了,我们也永远都不应该对此有所期待。 16 | 17 | > 我们将要学习计算过程的概念。计算过程是计算机中的抽象事物。在演化中,过程操纵着叫做数据的其它事物。过程的演化由叫做程序的一系列规则主导。人们创造程序来主导过程。实际上,我们使用我们的咒语来凭空创造出计算机的灵魂。 18 | 19 | > 我们用于创造过程的程序就像巫师的魔法。它们由一些古怪且深奥的编程语言中的符号表达式所组成,这些语言指定了我们想让过程执行的任务。 20 | 21 | > 在一台工作正确的计算机上,计算过程准确且严谨地执行程序。所以,就像巫师的学徒那样,程序员新手必须学会理解和预测他们的魔法产生的结果。 22 | 23 | > --Abelson & Sussman, SICP (1993) 24 | 25 | ## 1.1.1 在Python中编程 26 | 27 | > 语言并不是你学到的东西,而是你参与的东西。 28 | 29 | > --[Arika Okrent](http://arikaokrent.com/) 30 | 31 | 为了定义计算过程,我们需要一种编程语言,最好是一种许多人和大量计算机都能懂的语言。这门课中,我们将会使用[Python](http://docs.python.org/py3k/)语言。 32 | 33 | Python是一种广泛使用的编程语言,并且在许多职业中都有它的爱好者:Web程序员、游戏工程师、科学家、学者,甚至新编程语言的设计师。当你学习Python时,你就加入到了一个数百万人的开发者社群。开发者社群是一个极其重要的组织:成员可以互相帮助来解决问题,分享他们的代码和经验,以及一起开发软件和工具。投入的成员经常由于他们的贡献而出名,并且收到广泛的尊重。也许有一天你会被提名为Python开发者精英。 34 | 35 | Python语言自身就是一个[大型志愿者社群](http://www.python.org/psf/members/)的产物,并且为其贡献者的[多元化](http://python.org/community/diversity/)而自豪。这种语言在20世纪80年代末由[Guido van Rossum](http://en.wikipedia.org/wiki/Guido_van_Rossum)设计并首次实现。他的[Python3教程](http://docs.python.org/py3k/tutorial/appetite.html)的第一章解释了为什么Python在当今众多语言之中如此流行。 36 | 37 | Python适用于作为教学语言,因为纵观它的历史,Python的开发者强调了Python代码对人类的解释性,并在[Python之禅](http://www.python.org/dev/peps/pep-0020/)中美观、简约和可读的原则下进一步加强。Python尤其适用于课堂,因为它宽泛的特性支持大量的不同编程风格,我们将要探索它们。在Python中编程没有单一的解法,但是有一些习俗在开发者社群之间流传,它们可以使现有程序的阅读、理解,以及扩展变得容易。所以,Python的灵活性和易学性的组合可以让学生们探索许多编程范式,之后将它们新学到的知识用于数千个[正在开发的项目](http://pypi.python.org/pypi)中。 38 | 39 | 这些讲义通过使用抽象设计的技巧和严谨的计算模型,来快速介绍Python的特性。此外,这些讲义提供了Python编程的实践简介,包含一些高级语言特性和展示示例。通过这门课,学习Python将会变成自然而然的事情。 40 | 41 | 然而,Python是一门生态丰富的语言,带有大量特性和用法。我们讲到基本的计算机科学概念时,会刻意慢慢地介绍他们。对于有经验的学生,他们打算一口气学完语言的所有细节,我们推荐他们阅读Mark Pilgrim的书[Dive Into Python 3](http://diveintopython3.ep.io/),它在网上可以免费阅读。这本书的主题跟这门课极其不同,但是这本书包含了许多关于使用Python的宝贵的实用信息。事先告知:不像这些讲义,Dive Into Python 3需要一些编程经验。 42 | 43 | 开始在Python中编程的最佳方法就是直接和解释器交互。这一章会描述如何安装Python3,使用解释器开始交互式会话,以及开始编程。 44 | 45 | ## 1.1.2 安装Python3 46 | 47 | 就像所有伟大的软件一样,Python具有许多版本。这门课会使用Python3最新的稳定版本(本书编写时是3.2)。许多计算机都已经安装了Python的旧版本,但是它们可能不满足这门课。你应该可以在这门课上使用任何能安装Python3的计算机。不要担心,Python是免费的。 48 | 49 | Dive Into Python 3拥有一个为所有主流平台准备的详细的[安装指南](http://diveintopython3.ep.io/installing-python.html)。这个指南多次提到了Python3.1,但是你最好安装3.2(虽然它们的差异在这门课中非常微小)。EECS学院的所有教学机都已经安装了Python3.2。 50 | 51 | ## 1.1.3 交互式会话 52 | 53 | 在Python交互式会话中,你可以在提示符`>>>`之后键入一些Python代码。Python解释器读取并求出你输入的东西,并执行你的各种命令。 54 | 55 | 有几种开始交互式会话的途径,并且具有不同的特性。把它们尝试一遍来找出你最喜欢的方式。它们全部都在背后使用了相同的解释器(CPython)。 56 | 57 | + 最简单且最普遍的方式就是运行Python3应用。在终端提示符后(Mac/Unix/Linux)键入`python3`,或者在Windows上打开Python3应用。(译者注:Windows上设置完Python的环境变量之后,就可以在`cmd`或PowerShell中执行相同操作了。) 58 | + 有一个更加用户友好的应用叫做Idle3(`idle3`),可用于学习这门语言。Idle会高亮你的代码(叫做语法高亮),弹出使用提示,并且标记一些错误的来源。Idle总是由Python自带,所以你已经安装它了。 59 | + Emacs编辑器可以在它的某个缓冲区中运行交互式会话。虽然它学习起来有些挑战,Emacs是个强大且多功能的编辑器,适用于任何语言。请阅读61A的Emacs教程来开始。许多程序员投入大量时间来学习Emacs,之后他们就不再切换编辑器了。 60 | 61 | 在所有情况中,如果你看见了Python提示符`>>>`,你就成功开启了交互式会话。这些讲义使用提示符来展示示例,同时带有一些输入。 62 | 63 | ```py 64 | >>> 2 + 2 65 | 4 66 | ``` 67 | 68 | 控制:每个会话都保留了你的历史输入。为了访问这些历史,需要按下`-P`(上一个)和`-N`(下一个)。`-D`会退出会话,这会清除所有历史。 69 | 70 | ## 1.1.4 第一个例子 71 | 72 | > 想像会把不知名的事物用一种形式呈现出来,诗人的笔再使它们具有如实的形象,空虚的无物也会有了居处和名字。 73 | 74 | > --威廉·莎士比亚,《仲夏夜之梦》 75 | 76 | 为了介绍Python,我们会从一个使用多个语言特性的例子开始。下一节中,我们会从零开始,一步一步构建整个语言。你可以将这章视为即将到来的特性的预览。 77 | 78 | Python拥有常见编程功能的内建支持,例如文本操作、显示图形以及互联网通信。导入语句 79 | 80 | ```py 81 | >>> from urllib.request import urlopen 82 | ``` 83 | 84 | 为访问互联网上的数据加载功能。特别是,它提供了叫做`urlopen`的函数,可以访问到统一资源定位器(URL)处的内容,它是互联网上的某个位置。 85 | 86 | **语句和表达式:**Python代码包含语句和表达式。广泛地说,计算机程序包含的语句 87 | 88 | 1. 计算某个值 89 | 2. 或执行某个操作 90 | 91 | 语句通常用于描述操作。当Python解释器执行语句时,它执行相应操作。另一方面,表达式通常描述产生值的运算。当Python求解表达式时,就会计算出它的值。这一章介绍了几种表达式和语句。 92 | 93 | 赋值语句 94 | 95 | ```py 96 | >>> shakespeare = urlopen('http://inst.eecs.berkeley.edu/~cs61a/fa11/shakespeare.txt') 97 | ``` 98 | 99 | 将名称`shakespeare`和后面的表达式的值关联起来。这个表达式在URL上调用`urlopen`函数,URL包含了莎士比亚的37个剧本的完整文本,在单个文本文件中。 100 | 101 | **函数:**函数封装了操作数据的逻辑。Web地址是一块数据,莎士比亚的剧本文本是另一块数据。前者产生后者的过程可能有些复杂,但是我们可以只通过一个表达式来调用它们,因为复杂性都塞进函数里了。函数是这一章的主要话题。 102 | 103 | 另一个赋值语句 104 | 105 | ```py 106 | >>> words = set(shakespeare.read().decode().split()) 107 | ``` 108 | 109 | 将名称`words`关联到出现在莎士比亚剧本中的所有去重词汇的集合,总计33,721个。这个命令链调用了`read`、`decode`和`split`,每个都操作衔接的计算实体:从URL读取的数据、解码为文本的数据、以及分割为单词的文本。所有这些单词都放在`set`中。 110 | 111 | **对象:**集合是一种对象,它支持取交和测试成员的操作。对象整合了数据和操作数据的逻辑,并以一种隐藏其复杂性的方式。对象是第二章的主要话题。 112 | 113 | 表达式 114 | 115 | ```py 116 | >>> {w for w in words if len(w) >= 5 and w[::-1] in words} 117 | {'madam', 'stink', 'leets', 'rever', 'drawer', 'stops', 'sessa', 118 | 'repaid', 'speed', 'redder', 'devil', 'minim', 'spots', 'asses', 119 | 'refer', 'lived', 'keels', 'diaper', 'sleek', 'steel', 'leper', 120 | 'level', 'deeps', 'repel', 'reward', 'knits'} 121 | ``` 122 | 123 | 是一个复合表达式,求出正序或倒序出现的“莎士比亚词汇”集合。神秘的记号`w[::-1]`遍历单词中的每个字符,然而`-1`表明倒序遍历(`::`表示第一个和最后一个单词都使用默认值)。当你在交互式会话中输入表达式时,Python会在随后打印出它的值,就像上面那样。 124 | 125 | **解释器:**复合表达式的求解需要可预测的过程来精确执行解释器的代码。执行这个过程,并求解复合表达式和语句的程序就叫解释器。解释器的设计与实现是第三章的主要话题。 126 | 127 | 与其它计算机程序相比,编程语言的解释器通常比较独特。Python在意图上并没有按照莎士比亚或者回文来设计,但是它极大的灵活性让我们用极少的代码处理大量文本。 128 | 129 | 最后,我们会发现,所有这些核心概念都是紧密相关的:函数是对象,对象是函数,解释器是二者的实例。然而,对这些概念,以及它们在代码组织中的作用的清晰理解,是掌握编程艺术的关键。 130 | 131 | ## 1.1.5 实践指南 132 | 133 | Python正在等待你的命令。你应当探索这门语言,即使你可能不知道完整的词汇和结构。但是,要为错误做好准备。虽然计算机极其迅速和灵活,它们也十分古板。在[斯坦福的导论课](http://www.stanford.edu/class/cs101/code-introduction.html)中,计算机的本性描述为 134 | 135 | > 计算机的基本等式是:`计算机 = 强大 + 笨拙` 136 | 137 | > 计算机非常强大,能够迅速搜索大量数据。计算机每秒可以执行数十亿次操作,其中每个操作都非常简单。 138 | 139 | > 计算机也非常笨拙和脆弱。它们所做的操作十分古板、简单和机械化。计算机缺少任何类似真实洞察力的事情...它并不像电影中的HAL 9000。如果不出意外,你不应被计算机吓到,就像它拥有某种大脑一样。它在背后非常机械化。 140 | 141 | > 程序是一个人使用他的真实洞察力来构建出的一些实用的东西,它由这些简单的小操作所组成。 142 | 143 | > —Francisco Cai & Nick Parlante, 斯坦福 CS101 144 | 145 | 在你实验Python解释器的时候,你会马上意识到计算机的古板:即使最小的拼写和格式修改都会导致非预期的输出和错误。 146 | 147 | 学习解释错误和诊断非预期错误的原因叫做调试(debugging)。它的一些指导原则是: 148 | 149 | 1. 逐步测试:每个写好的程序都由小型的组件模块组成,这些组件可以独立测试。尽快测试你写好的任何东西来及早捕获错误,并且从你的组件中获得自信。 150 | 2. 隔离错误:复杂程序的输出、表达式、或语句中的错误,通常可以归于特定的组件模块。当尝试诊断问题时,在你能够尝试修正错误之前,一定要将它跟踪到最小的代码片段。 151 | 3. 检查假设:解释器将你的指令执行为文字 -- 不多也不少。当一些代码不匹配程序员所相信的(或所假设的)行为,它们的输出就会是非预期的。了解你的假设,之后专注于验证你的假设是否整理来调试。 152 | 4. 询问他人:你并不是一个人!如果你不理解某个错误信息,可以询问朋友、导师或者搜索引擎。如果你隔离了一个错误,但是不知道如何改正,可以让其它人来看一看。在小组问题解决中,会分享一大堆有价值的编程知识。 153 | 154 | 逐步测试、模块化设计、明确假设和团队作业是贯穿这门课的主题。但愿它们也能够一直伴随你的计算机科学生涯。 155 | 156 | 157 | -------------------------------------------------------------------------------- /1.2.md: -------------------------------------------------------------------------------- 1 | # 1.2 编程元素 2 | 3 | > 来源:[1.2 The Elements of Programming](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/functions.html#the-elements-of-programming) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 编程语言是操作计算机来执行任务的手段,它也在我们组织关于过程的想法中,作为一种框架。程序用于在编程社群的成员之间交流这些想法。所以,程序必须为人类阅读而编写,并且仅仅碰巧可以让机器执行。 10 | 11 | 当我们描述一种语言时,我们应该特别注意这种语言的手段,来将简单的想法组合为更复杂的想法。每个强大的语言都拥有用于完成下列任务的机制: 12 | 13 | + 基本的表达式和语句,它们由语言提供,表示最简单的构建代码块。 14 | + 组合的手段,复杂的元素由简单的元素通过它来构建,以及 15 | + 抽象的手段,复杂的元素可以通过它来命名,以及作为整体来操作。 16 | 17 | 在编程中,我们处理两种元素:函数和数据。(不久之后我们就会探索它们并不是真的非常不同。)不正式地说,数据是我们想要操作的东西,函数描述了操作数据的规则。所以,任何强大的编程语言都应该能描述基本数据和基本函数,并且应该拥有组合和抽象二者的方式。 18 | 19 | ## 1.2.1 表达式 20 | 21 | 在实验 Python 解释器之后,我们现在必须重新开始,按照顺序一步步地探索 Python 语言。如果示例看上去很简单,要有耐心 -- 更刺激的东西还在后面。 22 | 23 | 我们以基本表达式作为开始。一种基本表达式就是数值。更精确地说,是你键入的,由 10 进制数字表示的数值组成的表达式。 24 | 25 | ```py 26 | >>> 42 27 | 42 28 | ``` 29 | 30 | 表达式表示的数值也许会和算数运算符组合,来形成复合表达式,解释器会求出它: 31 | 32 | ```py 33 | >>> -1 - -1 34 | 0 35 | >>> 1/2 + 1/4 + 1/8 + 1/16 + 1/32 + 1/64 + 1/128 36 | 0.9921875 37 | ``` 38 | 39 | 这些算术表达式使用了中缀符号,其中运算符(例如`+`、`-`、`*`、`/`)出现在操作数(数值)中间。Python包含许多方法来形成复合表达式。我们不会尝试立即将它们列举出来,而是在进行中介绍新的表达式形式,以及它们支持的语言特性。 40 | 41 | ## 1.2.2 调用表达式 42 | 43 | 最重要的复合表达式就是调用表达式,它在一些参数上调用函数。回忆代数中,函数的数学概念是一些输入值到输出值的映射。例如,`max`函数将它的输入映射到单个输出,输出是输入中的最大值。Python 中的函数不仅仅是输入输出的映射,它表述了计算过程。但是,Python 表示函数的方式和数学中相同。 44 | 45 | ```py 46 | >>> max(7.5, 9.5) 47 | 9.5 48 | ``` 49 | 50 | 调用表达式拥有子表达式:运算符在圆括号之前,圆括号包含逗号分隔的操作数。运算符必须是个函数,操作数可以是任何值。这里它们都是数值。当求解这个调用表达式时,我们说`max`函数以参数 7.5 和 9.5 调用,并且返回 9.5。 51 | 52 | 调用表达式中的参数的顺序极其重要。例如,函数`pow`计算第一个参数的第二个参数次方。 53 | 54 | ```py 55 | >>> pow(100, 2) 56 | 10000 57 | >>> pow(2, 100) 58 | 1267650600228229401496703205376 59 | ``` 60 | 61 | 函数符号比中缀符号的数学惯例有很多优点。首先,函数可以接受任何数量的参数: 62 | 63 | ```py 64 | >>> max(1, -2, 3, -4) 65 | 3 66 | ``` 67 | 68 | 不会产生任何歧义,因为函数的名称永远在参数前面。 69 | 70 | 其次,函数符号可以以直接的方式扩展为嵌套表达式,其中元素本身是复合表达式。在嵌套的调用表达式中,不像嵌套的中缀表达式,嵌套结构在圆括号中非常明显。 71 | 72 | ```py 73 | >>> max(min(1, -2), min(pow(3, 5), -4)) 74 | -2 75 | ``` 76 | 77 | (理论上)这种嵌套没有任何限制,并且 Python 解释器可以解释任何复杂的表达式。然而,人们可能会被多级嵌套搞晕。你作为程序员的一个重要作用就是构造你自己、你的同伴以及其它在未来可能会阅读你代码的人可以解释的表达式。 78 | 79 | 最后,数学符号在形式上多种多样:星号表示乘法,上标表示乘方,横杠表示除法,屋顶和侧壁表示开方。这些符号中一些非常难以打出来。但是,所有这些复杂事物可以通过调用表达式的符号来统一。虽然 Python 通过中缀符号(比如`+`和`-`)支持常见的数学运算符,任何运算符都可以表示为带有名字的函数。 80 | 81 | ## 1.2.3 导入库函数 82 | 83 | Python 定义了大量的函数,包括上一节提到的运算符函数,但是通常不能使用它们的名字,这样做是为了避免混乱。反之,它将已知的函数和其它东西组织在模块中,这些模块组成了 Python 库。需要导入它们来使用这些元素。例如,`math`模块提供了大量的常用数学函数: 84 | 85 | ```py 86 | >>> from math import sqrt, exp 87 | >>> sqrt(256) 88 | 16.0 89 | >>> exp(1) 90 | 2.718281828459045 91 | ``` 92 | 93 | `operator`模块提供了中缀运算符对应的函数: 94 | 95 | ```py 96 | >>> from operator import add, sub, mul 97 | >>> add(14, 28) 98 | 42 99 | >>> sub(100, mul(7, add(8, 4))) 100 | 16 101 | ``` 102 | 103 | `import`语句标明了模块名称(例如`operator`或`math`),之后列出被导入模块的具名属性(例如`sqrt`和`exp`)。 104 | 105 | [Python 3 库文档](http://docs.python.org/py3k/library/index.html)列出了定义在每个模块中的函数,例如[数学模块](http://docs.python.org/py3k/library/math.html)。然而,这个文档为了解整个语言的开发者编写。到现在为止,你可能发现使用函数做实验会比阅读文档告诉你更多它的行为。当你更熟悉 Python 语言和词汇时,这个文档就变成了一份有价值的参考来源。 106 | 107 | ## 1.2.4 名称和环境 108 | 109 | 编程语言的要素之一是它提供的手段,用于使用名称来引用计算对象。如果一个值被给予了名称,我们就说这个名称绑定到了值上面。 110 | 111 | 在 Python 中,我们可以使用赋值语句来建立新的绑定,它包含`=`左边的名称和右边的值。 112 | 113 | ```py 114 | >>> radius = 10 115 | >>> radius 116 | 10 117 | >>> 2 * radius 118 | 20 119 | ``` 120 | 121 | 名称也可以通过`import`语句绑定: 122 | 123 | ```py 124 | >>> from math import pi 125 | >>> pi * 71 / 223 126 | 1.0002380197528042 127 | ``` 128 | 129 | 我们也可以在一个语句中将多个值赋给多个名称,其中名称和表达式由逗号分隔: 130 | 131 | ```py 132 | >>> area, circumference = pi * radius * radius, 2 * pi * radius 133 | >>> area 134 | 314.1592653589793 135 | >>> circumference 136 | 62.83185307179586 137 | ``` 138 | 139 | `=`符号在 Python(以及许多其它语言)中叫做赋值运算符。赋值是 Python 中的最简单的抽象手段,因为它使我们可以使用最简单的名称来引用复合操作的结果,例如上面计算的`area`。这样,复杂的程序可以由复杂性递增的计算对象一步一步构建, 140 | 141 | 将名称绑定到值上,以及随后通过名称来检索这些值的可能,意味着解释器必须维护某种内存来跟踪这些名称和值的绑定。这些内存叫做环境。 142 | 143 | 名称也可以绑定到函数。例如,名称`max`绑定到了我们曾经用过的`max`函数上。函数不像数值,不易于渲染成文本,所以 Python 使用识别描述来代替,当我们打印函数时: 144 | 145 | ```py 146 | >>> max 147 | 148 | ``` 149 | 150 | 我们可以使用赋值运算符来给现有函数起新的名字: 151 | 152 | ```py 153 | >>> f = max 154 | >>> f 155 | 156 | >>> f(3, 4) 157 | 4 158 | ``` 159 | 160 | 成功的赋值语句可以将名称绑定到新的值: 161 | 162 | ```py 163 | >>> f = 2 164 | >>> f 165 | 2 166 | ``` 167 | 168 | 在 Python 中,通过赋值绑定的名称通常叫做变量名称,因为它们在执行程序期间可以绑定到许多不同的值上面。 169 | 170 | ## 1.2.5 嵌套表达式的求解 171 | 172 | 我们这章的目标之一是隔离程序化思考相关的问题。作为一个例子,考虑嵌套表达式的求解,解释器自己会遵循一个过程: 173 | 174 | 为了求出调用表达式,Python 会执行下列事情: 175 | 176 | + 求出运算符和操作数子表达式,之后 177 | + 在值为操作数子表达式的参数上调用值为运算符子表达式的函数。 178 | 179 | 这个简单的过程大体上展示了一些过程上的重点。第一步表明为了完成调用表达式的求值过程,我们首先必须求出其它表达式。所以,求值过程本质上是递归的,也就是说,它会调用其自身作为步骤之一。 180 | 181 | 例如,求出 182 | 183 | ```py 184 | >>> mul(add(2, mul(4, 6)), add(3, 5)) 185 | 208 186 | ``` 187 | 188 | 需要应用四次求值过程。如果我们将每个需要求解的表达式抽出来,我们可以可视化这一过程的层次结构: 189 | 190 | ![](img/expression_tree.png) 191 | 192 | 这个示例叫做表达式树。在计算机科学中,树从顶端向下生长。每一点上的对象叫做节点。这里它们是表达式和它们的值。 193 | 194 | 求出根节点,也就是整个表达式,需要首先求出枝干节点,也就是子表达式。叶子节点(也就是没有子节点的节点)的表达式表示函数或数值。内部节点分为两部分:表示我们想要应用的求值规则的调用表达式,以及表达式的结果。观察这棵树中的求值,我们可以想象操作数的值向上流动,从叶子节点开始,在更高的层上融合。 195 | 196 | 接下来,观察第一步的重复应用,这会将我们带到需要求值的地方,并不是调用表达式,而是基本表达式,例如数字(比如`2`),以及名称(比如`add`),我们需要规定下列事物来谨慎对待基本的东西: 197 | 198 | + 数字求值为它标明的数值, 199 | + 名称求值为当前环境中这个名称所关联的值 200 | 201 | 要注意环境的关键作用是决定表达式中符号的含义。Python 中,在不指定任何环境信息,来提供名称`x`(以及名称`add`)的含义的情况下,谈到这样一个表达式的值没有意义: 202 | 203 | ```py 204 | >>> add(x, 1) 205 | ``` 206 | 207 | 环境提供了求值所发生的上下文,它在我们理解程序执行中起到重要作用。 208 | 209 | 这个求值过程并不符合所有 Python 代码的求解,仅仅是调用表达式、数字和名称。例如,它并不能处理赋值语句。 210 | 211 | ```py 212 | >>> x = 3 213 | ``` 214 | 215 | 的执行并不返回任何值,也不求解任何参数上的函数,因为赋值的目的是将一个名称绑定到一个值上。通常,语句不会被求值,而是被执行,它们不产生值,但是会改变一些东西。每种语句或表达式都有自己的求值或执行过程,我们会在涉及时逐步介绍。 216 | 217 | 218 | 注:当我们说“数字求值为数值”的时候,我们的实际意思是 Python 解释器将数字求解为数值。Python 的解释器使编程语言具有了这个意义。假设解释器是一个固定的程序,行为总是一致,我们就可以说数字(以及表达式)自己在 Python 程序的上下文中会求解为值。 219 | 220 | ## 1.2.6 函数图解 221 | 222 | 当我们继续构建求值的形式模型时,我们会发现解释器内部状态的图解有助于我们跟踪求值过程的发展。这些图解的必要部分是函数的表示。 223 | 224 | **纯函数:**具有一些输入(参数)以及返回一些输出(调用结果)的函数。内建函数 225 | 226 | ```py 227 | >>> abs(-2) 228 | 2 229 | ``` 230 | 231 | 可以描述为接受输入并产生输出的小型机器。 232 | 233 | ![](img/function_abs.png) 234 | 235 | `abs`是纯函数。纯函数具有一个特性,调用它们时除了返回一个值之外没有其它效果。 236 | 237 | **非纯函数:**除了返回一个值之外,调用非纯函数会产生副作用,这会改变解释器或计算机的一些状态。一个普遍的副作用就是在返回值之外生成额外的输出,例如使用`print`函数: 238 | 239 | 240 | ```py 241 | >>> print(-2) 242 | -2 243 | >>> print(1, 2, 3) 244 | 1 2 3 245 | ``` 246 | 247 | 虽然这些例子中的`print`和`abs`看起来很像,但它们本质上以不同方式工作。`print`的返回值永远是`None`,它是一个 Python 特殊值,表示没有任何东西。Python 交互式解释器并不会自动打印`None`值。这里,`print`自己打印了输出,作为调用中的副作用。 248 | 249 | ![](img/function_print.png) 250 | 251 | 调用`print`的嵌套表达式会凸显出它的非纯特性: 252 | 253 | ```py 254 | >>> print(print(1), print(2)) 255 | 1 256 | 2 257 | None None 258 | ``` 259 | 260 | 如果你发现自己不能预料到这个输出,画出表达式树来弄清为什么这个表达式的求值会产生奇怪的输出。 261 | 262 | 要当心`print`!它的返回值为`None`,意味着它不应该在赋值语句中用作表达式: 263 | 264 | ```py 265 | >>> two = print(2) 266 | 2 267 | >>> print(two) 268 | None 269 | ``` 270 | 271 | **签名:**不同函数具有不同的允许接受的参数数量。为了跟踪这些必备条件,我们需要以一种展示函数名称和参数名称的方式,画出每个函数。`abs`函数值接受一个叫作`number`的参数,向它提供更多或更少的参数会产生错误。`print`函数可以接受任意数量的参数,所以它渲染为`print(...)`。函数的可接受参数的描述叫做函数的签名。 272 | 273 | -------------------------------------------------------------------------------- /1.3.md: -------------------------------------------------------------------------------- 1 | # 1.3 定义新的函数 2 | 3 | > 来源:[1.3 Defining New Functions](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/functions.html#defining-new-functions) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 我们已经在 Python 中认识了一些在任何强大的编程语言中都会出现的元素: 10 | 11 | 1. 数值是内建数据,算数运算是函数。 12 | 2. 嵌套函数提供了组合操作的手段。 13 | 3. 名称到值的绑定提供了有限的抽象手段。 14 | 15 | 现在我们将要了解函数定义,一个更加强大的抽象技巧,名称通过它可以绑定到复合操作上,并可以作为一个单元来引用。 16 | 17 | 我们通过如何表达“平方”这个概念来开始。我们可能会说,“对一个数求平方就是将这个数乘上它自己”。在 Python 中就是: 18 | 19 | ```py 20 | >>> def square(x): 21 | return mul(x, x) 22 | ``` 23 | 24 | 这定义了一个新的函数,并赋予了名称`square`。这个用户定义的函数并不内建于解释器。它表示将一个数乘上自己的复合操作。定义中的`x`叫做形式参数,它为被乘的东西提供一个名称。这个定义创建了用户定义的函数,并且将它关联到名称`square`上。 25 | 26 | 函数定义包含`def`语句,它标明了``(名称)和一列带有名字的``(形式参数)。之后,`return`(返回)语句叫做函数体,指定了函数的``(返回表达式),它是函数无论什么时候调用都需要求值的表达式。 27 | 28 | ```py 29 | def (): 30 | return 31 | ``` 32 | 33 | 第二行必须缩进!按照惯例我们应该缩进四个空格,而不是一个Tab,返回表达式并不是立即求值,它储存为新定义函数的一部分,并且只在函数最终调用时会被求出。(很快我们就会看到缩进区域可以跨越多行。) 34 | 35 | 定义了`square`之后,我们使用调用表达式来调用它: 36 | 37 | ```py 38 | >>> square(21) 39 | 441 40 | >>> square(add(2, 5)) 41 | 49 42 | >>> square(square(3)) 43 | 81 44 | ``` 45 | 46 | 我们也可以在构建其它函数时,将`square`用作构建块。列入,我们可以轻易定义`sum_squares`函数,它接受两个数值作为参数,并返回它们的平方和: 47 | 48 | ```py 49 | >>> def sum_squares(x, y): 50 | return add(square(x), square(y)) 51 | >>> sum_squares(3, 4) 52 | 25 53 | ``` 54 | 55 | 用户定义的函数和内建函数以同种方法使用。确实,我们不可能在`sum_squares`的定义中分辨出`square`是否构建于解释器中,从模块导入还是由用户定义。 56 | 57 | ## 1.3.1 环境 58 | 59 | 我们的 Python 子集已经足够复杂了,但程序的含义还不是非常明显。如果形式参数和内建函数具有相同名称会如何呢?两个函数是否能共享名称而不会产生混乱呢?为了解决这些疑问,我们必须详细描述环境。 60 | 61 | 表达式求值所在的环境由帧的序列组成,它们可以表述为一些盒子。每一帧都包含了一些绑定,它们将名称和对应的值关联起来。全局帧只有一个,它包含所有内建函数的名称绑定(只展示了`abs`和`max`)。我们使用地球符号来表示全局。 62 | 63 | ![](img/global_frame.png) 64 | 65 | 赋值和导入语句会向当前环境的第一个帧添加条目。到目前为止,我们的环境只包含全局帧。 66 | 67 | ```py 68 | >>> from math import pi 69 | >>> tau = 2 * pi 70 | ``` 71 | 72 | ![](img/global_frame_assignment.png) 73 | 74 | `def`语句也将绑定绑定到由定义创建的函数上。定义`square`之后的环境如图所示: 75 | 76 | ![](img/global_frame_def.png) 77 | 78 | 这些环境图示展示了当前环境中的绑定,以及它们所绑定的值(并不是任何帧的一部分)。要注意函数名称是重复的,一个在帧中,另一个是函数的一部分。这一重复是有意的,许多不同的名字可能会引用相同函数,但是函数本身只有一个内在名称。但是,在环境中由名称检索值只检查名称绑定。函数的内在名称不在名称检索中起作用。在我们之前看到的例子中: 79 | 80 | ```py 81 | >>> f = max 82 | >>> f 83 | 84 | ``` 85 | 86 | 名称`max`是函数的内在名称,以及打印`f`时我们看到的名称。此外,名称`max`和`f`在全局环境中都绑定到了相同函数上。 87 | 88 | 在我们介绍 Python 的附加特性时,我们需要扩展这些图示。每次我们这样做的时候,我们都会列出图示可以表达的新特性。 89 | 90 | **新的环境特性:**赋值和用户定义的函数定义。 91 | 92 | ## 1.3.2 调用用户定义的函数 93 | 94 | 为了求出运算符为用户定义函数的调用表达式,Python 解释器遵循与求出运算符为内建函数的表达式相似的过程。也就是说,解释器求出操作数表达式,并且对产生的实参调用具名函数。 95 | 96 | 调用用户定义的函数的行为引入了第二个局部帧,它只能由函数来访问。为了对一些实参调用用户定义的函数: 97 | 98 | 1. 在新的局部帧中,将实参绑定到函数的形式参数上。 99 | 2. 在当前帧的开头以及全局帧的末尾求出函数体。 100 | 101 | 函数体求值所在的环境由两个帧组成:第一个是局部帧,包含参数绑定,之后是全局帧,包含其它所有东西。每个函数示例都有自己的独立局部帧。 102 | 103 | ![](img/evaluate_square.png) 104 | 105 | 这张图包含两个不同的 Python 解释器层面:当前的环境,以及表达式树的一部分,它和要求值的代码的当前一行相关。我们描述了调用表达式的求值,用户定义的函数(蓝色)表示为两部分的圆角矩形。点线箭头表示哪个环境用于在每个部分求解表达式。 106 | 107 | + 上半部分展示了调用表达式的求值。这个调用表达式并不在任何函数里面,所以他在全局环境中求值。所以,任何里面的名称(例如`square`)都会在全局帧中检索。 108 | + 下半部分展示了`square`函数的函数体。它的返回表达式在上面的步骤1引入的新环境中求值,它将`square`的形式参数`x`的名称绑定到实参的值`-2`上。 109 | 110 | 环境中帧的顺序会影响由表达式中的名称检索返回的值。我们之前说名称求解为当前环境中与这个名称关联的值。我们现在可以更精确一些: 111 | 112 | + 名称求解为当前环境中,最先发现该名称的帧中,绑定到这个名称的值。 113 | 114 | 我们关于环境、名称和函数的概念框架建立了求值模型,虽然一些机制的细节仍旧没有指明(例如绑定如何实现),我们的模型在描述解释器如何求解调用表示上,变得更准确和正确。在第三章我们会看到这一模型如何用作一个蓝图来实现编程语言的可工作的解释器。 115 | 116 | **新的环境特性:**函数调用。 117 | 118 | ## 1.3.3 示例:调用用户定义的函数 119 | 120 | 让我们再一次考虑两个简单的定义: 121 | 122 | ```py 123 | >>> from operator import add, mul 124 | >>> def square(x): 125 | return mul(x, x) 126 | >>> def sum_squares(x, y): 127 | return add(square(x), square(y)) 128 | ``` 129 | 130 | ![](img/evaluate_sum_squares_0.png) 131 | 132 | 以及求解下列调用表达式的过程: 133 | 134 | ```py 135 | >>> sum_squares(5, 12) 136 | 169 137 | ``` 138 | 139 | Python 首先会求出名称`sum_squares`,它在全局帧绑定了用户定义的函数。基本的数字表达式 5 和 12 求值为它们所表达的数值。 140 | 141 | 之后,Python 调用了`sum_squares`,它引入了局部帧,将`x`绑定为 5,将`y`绑定为 12。 142 | 143 | ![](img/evaluate_sum_squares_1.png) 144 | 145 | 这张图中,局部帧指向它的后继,全局帧。所有局部帧必须指向某个先导,这些链接定义了当前环境中的帧序列。 146 | 147 | `sum_square`的函数体包含下列调用表达式: 148 | 149 | ``` 150 | add ( square(x) , square(y) ) 151 | ________ _________ _________ 152 | "operator" "operand 0" "operand 1" 153 | ``` 154 | 155 | 全部三个子表达式在当前环境中求值,它开始于标记为`sum_squares`的帧。运算符字表达式`add`是全局帧中发现的名称,绑定到了内建的加法函数上。两个操作数子表达式必须在加法函数调用之前依次求值。两个操作数都在当前环境中求值,开始于标记为`sum_squares`的帧。在下面的环境图示中,我们把这一帧叫做`A`,并且将指向这一帧的箭头同时替换为标签`A`。 156 | 157 | 在使用这个局部帧的情况下,函数体表达式`mul(x, x)`求值为 25。 158 | 159 | 我们的求值过程现在轮到了操作数 1,`y`的值为 12。Python 再次求出`square`的函数体。这次引入了另一个局部环境帧,将`x`绑定为 12。所以,操作数 1 求值为 144。 160 | 161 | ![](img/evaluate_sum_squares_3.png) 162 | 163 | 最后,对实参 25 和 144 调用加法会产生`sum_squares`函数体的最终值:169。 164 | 165 | 这张图虽然复杂,但是用于展示我们目前为止发展出的许多基础概念。名称绑定到值上面,它延伸到许多局部帧中,局部帧在唯一的全局帧之上,全局帧包含共享名称。表达式为树形结构,以及每次子表达式包含用户定义函数的调用时,环境必须被扩展。 166 | 167 | 所有这些机制的存在确保了名称在表达式中正确的地方解析为正确的值。这个例子展示了为什么我们的模型需要所引入的复杂性。所有三个局部帧都包含名称`x`的绑定。但是这个名称在不同的帧中绑定到了不同的值上。局部帧分离了这些名称。 168 | 169 | ## 1.3.4 局部名称 170 | 171 | 函数实现的细节之一是实现者对形式参数名称的选择不应影响函数行为。所以,下面的函数应具有相同的行为: 172 | 173 | ```py 174 | >>> def square(x): 175 | return mul(x, x) 176 | >>> def square(y): 177 | return mul(y, y) 178 | ``` 179 | 180 | 这个原则 -- 也就是函数应不依赖于编写者选择的参数名称 -- 对编程语言来说具有重要的结果。最简单的结果就是函数参数名称应保留在函数体的局部范围中。 181 | 182 | 如果参数不位于相应函数的局部范围中,`square`的参数`x`可能和`sum_squares`中的参数`x`产生混乱。严格来说,这并不是问题所在:不同局部帧中的`x`的绑定是不相关的。我们的计算模型具有严谨的设计来确保这种独立性。 183 | 184 | 我们说局部名称的作用域被限制在定义它的用户定义函数的函数体中。当一个名称不能再被访问时,它就离开了作用域。作用域的行为并不是我们模型的新事实,它是环境的工作方式的结果。 185 | 186 | ## 1.3.5 实践指南:选择名称 187 | 188 | 可修改的名称并不代表形式参数的名称完全不重要。反之,选择良好的函数和参数名称对于函数定义的人类可解释性是必要的。 189 | 190 | 下面的准则派生于 [Python 的代码风格指南](http://www.python.org/dev/peps/pep-0008),可被所有(非反叛)Python 程序员作为指南。一些共享的约定会使社区成员之间的沟通变得容易。遵循这些约定有一些副作用,我会发现你的代码在内部变得一致。 191 | 192 | 1. 函数名称应该小写,以下划线分隔。提倡描述性的名称。 193 | 2. 函数名称通常反映解释器向参数应用的操作(例如`print`、`add`、`square`),或者结果(例如`max`、`abs`、`sum`)。 194 | 3. 参数名称应小写,以下划线分隔。提倡单个词的名称。 195 | 4. 参数名称应该反映参数在函数中的作用,并不仅仅是满足的值的类型。 196 | 5. 当作用非常明确时,单个字母的参数名称可以接受,但是永远不要使用`l`(小写的`L`)和`O`(大写的`o`),或者`I`(大写的`i`)来避免和数字混淆。 197 | 198 | 周期性对你编写的程序复查这些准则,不用多久你的名称会变得十分 Python 化。 199 | 200 | ## 1.3.6 作为抽象的函数 201 | 202 | 虽然`sum_squares`十分简单,但是它演示了用户定义函数的最强大的特性。`sum_squares`函数使用`square`函数定义,但是仅仅依赖于`square`定义在输入参数和输出值之间的关系。 203 | 204 | 我们可以编写`sum_squares`,而不用考虑如何计算一个数值的平方。平方计算的细节被隐藏了,并可以在之后考虑。确实,在`sum_squares`看来,`square`并不是一个特定的函数体,而是某个函数的抽象,也就是所谓的函数式抽象。在这个层级的抽象中,任何能计算平方的函数都是等价的。 205 | 206 | 所以,仅仅考虑返回值的情况下,下面两个计算平方的函数是难以区分的。每个都接受数值参数并且产生那个数的平方作为返回值。 207 | 208 | ```py 209 | >>> def square(x): 210 | return mul(x, x) 211 | >>> def square(x): 212 | return mul(x, x-1) + x 213 | ``` 214 | 215 | 换句话说,函数定义应该能够隐藏细节。函数的用户可能不能自己编写函数,但是可以从其它程序员那里获得它作为“黑盒”。用户不应该需要知道如何实现来调用。Python 库拥有这个特性。许多开发者使用在这里定义的函数,但是很少有人看过它们的实现。实际上,许多 Python 库的实现并不完全用 Python 编写,而是 C 语言。 216 | 217 | ## 1.3.7 运算符 218 | 219 | 算术运算符(例如`+`和`-`)在我们的第一个例子中提供了组合手段。但是我们还需要为包含这些运算符的表达式定义求值过程。 220 | 221 | 每个带有中缀运算符的 Python 表达式都有自己的求值过程,但是你通常可以认为他们是调用表达式的快捷方式。当你看到 222 | 223 | ```py 224 | >>> 2 + 3 225 | 5 226 | ``` 227 | 228 | 的时候,可以简单认为它是 229 | 230 | ```py 231 | >>> add(2, 3) 232 | 5 233 | ``` 234 | 235 | 的快捷方式。 236 | 237 | 中缀记号可以嵌套,就像调用表达式那样。Python 运算符优先级中采用了常规的数学规则,它指导了如何解释带有多种运算符的复合表达式。 238 | 239 | ```py 240 | >>> 2 + 3 * 4 + 5 241 | 19 242 | ``` 243 | 244 | 和下面的表达式的求值结果相同 245 | 246 | ```py 247 | >>> add(add(2, mul(3, 4)) , 5) 248 | 19 249 | ``` 250 | 251 | 调用表达式的嵌套比运算符版本更加明显。Python 也允许括号括起来的子表达式,来覆盖通常的优先级规则,或者使表达式的嵌套结构更加明显: 252 | 253 | ```py 254 | >>> (2 + 3) * (4 + 5) 255 | 45 256 | ``` 257 | 258 | 和下面的表达式的求值结果相同 259 | 260 | ```py 261 | >>> mul(add(2, 3), add(4, 5)) 262 | 45 263 | ``` 264 | 265 | 你应该在你的程序中自由使用这些运算符和括号。对于简单的算术运算,Python 在惯例上倾向于运算符而不是调用表达式。 266 | -------------------------------------------------------------------------------- /1.4.md: -------------------------------------------------------------------------------- 1 | # 1.4 实践指南:函数的艺术 2 | 3 | > 来源:[1.4 Practical Guidance: The Art of the Function](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/functions.html#practical-guidance-the-art-of-the-function) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 函数是所有程序的要素,无论规模大小,并且在编程语言中作为我们表达计算过程的主要媒介。目前为止,我们讨论了函数的形式特性,以及它们如何使用。我们现在跳转到如何编写良好的函数这一话题。 10 | 11 | + 每个函数都应该只做一个任务。这个任务可以使用短小的名称来定义,使用一行文本来标识。顺序执行多个任务的函数应该拆分在多个函数中。 12 | + 不要重复劳动(DRY)是软件工程的中心法则。所谓的DRY原则规定多个代码段不应该描述重复的逻辑。反之,逻辑应该只实现一次,指定一个名称,并且多次使用。如果你发现自己在复制粘贴一段代码,你可能发现了一个使用函数抽象的机会。 13 | + 函数应该定义得通常一些,准确来说,平方并不是在 Python 库中,因为它是`pow`函数的一个特例,这个函数计算任何数的任何次方。 14 | 15 | 这些准则提升代码的可读性,减少错误数量,并且通常使编写的代码总数最小。将复杂的任务拆分为简洁的函数是一个技巧,它需要一些经验来掌握。幸运的是,Python 提供了一些特性来支持你的努力。 16 | 17 | ## 1.4.1 文档字符串 18 | 19 | 函数定义通常包含描述这个函数的文档,叫做文档字符串,它必须在函数体中缩进。文档字符串通常使用三个引号。第一行描述函数的任务。随后的一些行描述参数,并且澄清函数的行为: 20 | 21 | ```py 22 | >>> def pressure(v, t, n): 23 | """Compute the pressure in pascals of an ideal gas. 24 | 25 | Applies the ideal gas law: http://en.wikipedia.org/wiki/Ideal_gas_law 26 | 27 | v -- volume of gas, in cubic meters 28 | t -- absolute temperature in degrees kelvin 29 | n -- particles of gas 30 | """ 31 | k = 1.38e-23 # Boltzmann's constant 32 | return n * k * t / v 33 | ``` 34 | 35 | 当你以函数名称作为参数来调用`help`时,你会看到它的文档字符串(按下`q`来退出 Python 帮助)。 36 | 37 | ```py 38 | >>> help(pressure) 39 | ``` 40 | 41 | 编写 Python 程序时,除了最简单的函数之外,都要包含文档字符串。要记住,代码只编写一次,但是会阅读多次。Python 文档包含了[文档字符串准则](http://www.python.org/dev/peps/pep-0257/),它在不同的 Python 项目中保持一致。 42 | 43 | ## 1.4.2 参数默认值 44 | 45 | 定义普通函数的结果之一就是额外参数的引入。具有许多参数的函数调用起来非常麻烦,也难以阅读。 46 | 47 | 在 Python 中,我们可以为函数的参数提供默认值。调用这个函数时,带有默认值的参数是可选的。如果它们没有提供,默认值就会绑定到形式参数的名称上。例如,如果某个应用通常用来计算一摩尔粒子的压强,这个值就可以设为默认: 48 | 49 | ```py 50 | >>> k_b=1.38e-23 # Boltzmann's constant 51 | >>> def pressure(v, t, n=6.022e23): 52 | """Compute the pressure in pascals of an ideal gas. 53 | 54 | v -- volume of gas, in cubic meters 55 | t -- absolute temperature in degrees kelvin 56 | n -- particles of gas (default: one mole) 57 | """ 58 | return n * k_b * t / v 59 | >>> pressure(1, 273.15) 60 | 2269.974834 61 | ``` 62 | 63 | 这里,`pressure`的定义接受三个参数,但是在调用表达式中只提供了两个。这种情况下,`n`的值通过`def`语句的默认值获得(它看起来像对`n`的赋值,虽然就像这个讨论暗示的那样,更大程度上它是条件赋值)。 64 | 65 | 作为准则,用于函数体的大多数数据值应该表示为具名参数的默认值,这样便于查看,以及被函数调用者修改。一些值永远不会改变,就像基本常数`k_b`,应该定义在全局帧中。 66 | -------------------------------------------------------------------------------- /1.5.md: -------------------------------------------------------------------------------- 1 | # 1.5 控制 2 | 3 | > 来源:[1.5 Control](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/functions.html#control) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 我们现在可以定义的函数能力有限,因为我们还不知道一种方法来进行测试,并且根据测试结果来执行不同的操作。控制语句可以让我们完成这件事。它们不像严格的求值子表达式那样从左向右编写,并且可以从它们控制解释器下一步做什么当中得到它们的名称。这可能基于表达式的值。 10 | 11 | ## 1.5.1 语句 12 | 13 | 目前为止,我们已经初步思考了如何求出表达式。然而,我们已经看到了三种语句:赋值、`def`和`return`语句。这些 Python 代码并不是表达式,虽然它们中的一部分是表达式。 14 | 15 | 要强调的是,语句的值是不相干的(或不存在的),我们使用执行而不是求值来描述语句。 每个语句都描述了对解释器状态的一些改变,执行语句会应用这些改变。像我们之前看到的`return`和赋值语句那样,语句的执行涉及到求解所包含的子表达式。 16 | 17 | 表达式也可以作为语句执行,其中它们会被求值,但是它们的值会舍弃。执行纯函数没有什么副作用,但是执行非纯函数会产生效果作为函数调用的结果。 18 | 19 | 考虑下面这个例子: 20 | 21 | ```py 22 | >>> def square(x): 23 | mul(x, x) # Watch out! This call doesn't return a value. 24 | ``` 25 | 26 | 这是有效的 Python 代码,但是并不是想表达的意思。函数体由表达式组成。表达式本身是个有效的语句,但是语句的效果是,`mul`函数被调用了,然后结果被舍弃了。如果你希望对表达式的结果做一些事情,你需要这样做:使用赋值语句来储存它,或者使用`return`语句将它返回: 27 | 28 | ```py 29 | >>> def square(x): 30 | return mul(x, x) 31 | ``` 32 | 33 | 有时编写一个函数体是表达式的函数是有意义的,例如调用类似`print`的非纯函数: 34 | 35 | ```py 36 | >>> def print_square(x): 37 | print(square(x)) 38 | ``` 39 | 40 | 在最高层级上,Python 解释器的工作就是执行由语句组成的程序。但是,许多有意思的计算工作来源于求解表达式。语句管理程序中不同表达式之间的关系,以及它们的结果会怎么样。 41 | 42 | ## 1.5.2 复合语句 43 | 44 | 通常,Python 的代码是语句的序列。一条简单的语句是一行不以分号结束的代码。复合语句之所以这么命名,因为它是其它(简单或复合)语句的复合。复合语句一般占据多行,并且以一行以冒号结尾的头部开始,它标识了语句的类型。同时,一个头部和一组缩进的代码叫做子句(或从句)。复合语句由一个或多个子句组成。 45 | 46 | ``` 47 |
: 48 | 49 | 50 | ... 51 | : 52 | 53 | 54 | ... 55 | ... 56 | ``` 57 | 58 | 我们可以这样理解我们已经见到的语句: 59 | 60 | + 表达式、返回语句和赋值语句都是简单语句。 61 | + `def`语句是复合语句。`def`头部之后的组定义了函数体。 62 | 63 | 为每种头部特化的求值规则指导了组内的语句什么时候以及是否会被执行。我们说头部控制语句组。例如,在`def`语句的例子中,我们看到返回表达式并不会立即求值,而是储存起来用于以后的使用,当所定义的函数最终调用时就会求值。 64 | 65 | 我们现在也能理解多行的程序了。 66 | 67 | + 执行语句序列需要执行第一条语句。如果这个语句不是重定向控制,之后执行语句序列的剩余部分,如果存在的话。 68 | 69 | 这个定义揭示出递归定义“序列”的基本结构:一个序列可以划分为它的第一个元素和其余元素。语句序列的“剩余”部分也是一个语句序列。所以我们可以递归应用这个执行规则。这个序列作为递归数据结构的看法会在随后的章节中再次出现。 70 | 71 | 这一规则的重要结果就是语句顺序执行,但是随后的语句可能永远不会执行到,因为有重定向控制。 72 | 73 | **实践指南:**在缩进代码组时,所有行必须以相同数量以及相同方式缩进(空格而不是Tab)。任何缩进的变动都会导致错误。 74 | 75 | ## 1.5.3 定义函数 II:局部赋值 76 | 77 | 一开始我们说,用户定义函数的函数体只由带有一个返回表达式的一个返回语句组成。实际上,函数可以定义为操作的序列,不仅仅是一条表达式。Python 复合语句的结构自然让我们将函数体的概念扩展为多个语句。 78 | 79 | 无论用户定义的函数何时被调用,定义中的子句序列在局部环境内执行。`return`语句会重定向控制:无论什么时候执行`return`语句,函数调用的流程都会中止,返回表达式的值会作为被调用函数的返回值。 80 | 81 | 于是,赋值语句现在可以出现在函数体中。例如,这个函数以第一个数的百分数形式,返回两个数量的绝对值,并使用了两步运算: 82 | 83 | ```py 84 | >>> def percent_difference(x, y): 85 | difference = abs(x-y) 86 | return 100 * difference / x 87 | >>> percent_difference(40, 50) 88 | 25.0 89 | ``` 90 | 91 | 赋值语句的效果是在当前环境的第一个帧上,将名字绑定到值上。于是,函数体内的赋值语句不会影响全局帧。函数只能操作局部作用域的现象是创建模块化程序的关键,其中纯函数只通过它们接受和返回的值与外界交互。 92 | 93 | 当然,`percent_difference`函数也可以写成一个表达式,就像下面这样,但是返回表达式会更加复杂: 94 | 95 | ```py 96 | >>> def percent_difference(x, y): 97 | return 100 * abs(x-y) / x 98 | ``` 99 | 100 | 目前为止,局部赋值并不会增加函数定义的表现力。当它和控制语句组合时,才会这样。此外,局部赋值也可以将名称赋为间接量,在理清复杂表达式的含义时起到关键作用。 101 | 102 | **新的环境特性:**局部赋值。 103 | 104 | ## 1.5.4 条件语句 105 | 106 | Python 拥有内建的绝对值函数: 107 | 108 | ```py 109 | >>> abs(-2) 110 | 2 111 | ``` 112 | 113 | 我们希望自己能够实现这个函数,但是我们当前不能直接定义函数来执行测试并做出选择。我们希望表达出,如果`x`是正的,`abs(x)`返回`x`,如果`x`是 0,`abx(x)`返回 0,否则`abs(x)`返回`-x`。Python 中,我们可以使用条件语句来表达这种选择。 114 | 115 | ```py 116 | >>> def absolute_value(x): 117 | """Compute abs(x).""" 118 | if x > 0: 119 | return x 120 | elif x == 0: 121 | return 0 122 | else: 123 | return -x 124 | 125 | >>> absolute_value(-2) == abs(-2) 126 | True 127 | ``` 128 | 129 | `absolute_value`的实现展示了一些重要的事情: 130 | 131 | **条件语句。**Python 中的条件语句包含一系列的头部和语句组:一个必要的`if`子句,可选的`elif`子句序列,和最后可选的`else`子句: 132 | 133 | ```py 134 | if : 135 | 136 | elif : 137 | 138 | else: 139 | 140 | ``` 141 | 142 | 当执行条件语句时,每个子句都按顺序处理: 143 | 144 | 1. 求出头部中的表达式。 145 | 2. 如果它为真,执行语句组。之后,跳过条件语句中随后的所有子句。 146 | 147 | 如果能到达`else`子句(仅当所有`if`和`elif`表达式值为假时),它的语句组才会被执行。 148 | 149 | **布尔上下文。**上面过程的执行提到了“假值”和“真值”。条件块头部语句中的表达式也叫作布尔上下文:它们值的真假对控制流很重要,但在另一方面,它们的值永远不会被赋值或返回。Python 包含了多种假值,包括 0、`None`和布尔值`False`。所有其他数值都是真值。在第二章中,我们就会看到每个 Python 中的原始数据类型都是真值或假值。 150 | 151 | **布尔值。**Python 有两种布尔值,叫做`True`和`False`。布尔值表示了逻辑表达式中的真值。内建的比较运算符,`>`、`<`、`>=`、`<=`、`==`、`!=`,返回这些值。 152 | 153 | ```py 154 | >>> 4 < 2 155 | False 156 | >>> 5 >= 5 157 | True 158 | ``` 159 | 160 | 第二个例子读作“5 大于等于 5”,对应`operator`模块中的函数`ge`。 161 | 162 | ```py 163 | >>> 0 == -0 164 | True 165 | ``` 166 | 167 | 最后的例子读作“0 等于 -0”,对应`operator`模块的`eq`函数。要注意 Python 区分赋值(`=`)和相等测试(`==`)。许多语言中都有这个惯例。 168 | 169 | **布尔运算符。**Python 也内建了三个基本的逻辑运算符: 170 | 171 | ```py 172 | >>> True and False 173 | False 174 | >>> True or False 175 | True 176 | >>> not False 177 | True 178 | ``` 179 | 180 | 逻辑表达式拥有对应的求值过程。这些过程揭示了逻辑表达式的真值有时可以不执行全部子表达式而确定,这个特性叫做短路。 181 | 182 | 为了求出表达式` and `: 183 | 184 | 1. 求出子表达式``。 185 | 2. 如果结果`v`是假值,那么表达式求值为`v`。 186 | 3. 否则表达式的值为子表达式``。 187 | 188 | 为了求出表达式` or `: 189 | 190 | 1. 求出子表达式``。 191 | 2. 如果结果`v`是真值,那么表达式求值为`v`。 192 | 3. 否则表达式的值为子表达式``。 193 | 194 | 为了求出表达式`not `: 195 | 196 | 1. 求出``,如果值是`True`那么返回值是假值,如果为`False`则反之。 197 | 198 | 这些值、规则和运算符向我们提供了一种组合测试结果的方式。执行测试以及返回布尔值的函数通常以`is`开头,并不带下划线(例如`isfinite`、`isdigit`、`isinstance`等等)。 199 | 200 | ## 1.5.5 迭代 201 | 202 | 除了选择要执行的语句,控制语句还用于表达重复操作。如果我们编写的每一行代码都只执行一次,程序会变得非常没有生产力。只有通过语句的重复执行,我们才可以释放计算机的潜力,使我们更加强大。我们已经看到了重复的一种形式:一个函数可以多次调用,虽然它只定义一次。迭代控制结构是另一种将相同语句执行多次的机制。 203 | 204 | 考虑斐波那契数列,其中每个数值都是前两个的和: 205 | 206 | ``` 207 | 0, 1, 1, 2, 3, 5, 8, 13, 21, ... 208 | ``` 209 | 210 | 每个值都通过重复使用“前两个值的和”的规则构造。为了构造第 n 个值,我们需要跟踪我们创建了多少个值(`k`),以及第 k 个值(`curr`)和它的上一个值(`pred`),像这样: 211 | 212 | ```py 213 | >>> def fib(n): 214 | """Compute the nth Fibonacci number, for n >= 2.""" 215 | pred, curr = 0, 1 # Fibonacci numbers 216 | k = 2 # Position of curr in the sequence 217 | while k < n: 218 | pred, curr = curr, pred + curr # Re-bind pred and curr 219 | k = k + 1 # Re-bind k 220 | return curr 221 | >>> fib(8) 222 | 13 223 | ``` 224 | 225 | 要记住逗号在赋值语句中分隔了多个名称和值。这一行: 226 | 227 | ```py 228 | pred, curr = curr, pred + curr 229 | ``` 230 | 231 | 具有将`curr`的值重新绑定到名称`pred`上,以及将`pred + curr`的值重新绑定到`curr`上的效果。所有`=`右边的表达式会在绑定发生之前求出来。 232 | 233 | `while`子句包含一个头部表达式,之后是语句组: 234 | 235 | ```py 236 | while : 237 | 238 | ``` 239 | 240 | 为了执行`while`子句: 241 | 242 | 1. 求出头部表达式。 243 | 2. 如果它为真,执行语句组,之后返回到步骤 1。 244 | 245 | 在步骤 2 中,整个`while`子句的语句组在头部表达式再次求值之前被执行。 246 | 247 | 为了防止`while`子句的语句组无限执行,它应该总是在每次通过时修改环境的状态。 248 | 249 | 不终止的`while`语句叫做无限循环。按下`-C`可以强制让 Python 停止循环。 250 | 251 | ## 1.5.6 实践指南:测试 252 | 253 | 函数的测试是验证函数的行为是否符合预期的操作。我们的函数现在已经足够复杂了,我们需要开始测试我们的实现。 254 | 255 | 测试是系统化执行这个验证的机制。测试通常写为另一个函数,这个函数包含一个或多个被测函数的样例调用。返回值之后会和预期结果进行比对。不像大多数通用的函数,测试涉及到挑选特殊的参数值,并使用它来验证调用。测试也可作为文档:它们展示了如何调用函数,以及什么参数值是合理的。 256 | 257 | 要注意我们也将“测试”这个词用于`if`或`while`语句的头部中作为一种技术术语。当我们将“测试”这个词用作表达式,或者用作一种验证机制时,它应该在语境中十分明显。 258 | 259 | **断言。**程序员使用`assert`语句来验证预期,例如测试函数的输出。`assert`语句在布尔上下文中只有一个表达式,后面是带引号的一行文本(单引号或双引号都可以,但是要一致)如果表达式求值为假,它就会显示。 260 | 261 | ```py 262 | >>> assert fib(8) == 13, 'The 8th Fibonacci number should be 13' 263 | ``` 264 | 265 | 当被断言的表达式求值为真时,断言语句的执行没有任何效果。当它是假时,`asset`会造成执行中断。 266 | 267 | 为`fib`编写的`test`函数测试了几个参数,包含`n`的极限值: 268 | 269 | ```py 270 | >>> def fib_test(): 271 | assert fib(2) == 1, 'The 2nd Fibonacci number should be 1' 272 | assert fib(3) == 1, 'The 3nd Fibonacci number should be 1' 273 | assert fib(50) == 7778742049, 'Error at the 50th Fibonacci number' 274 | ``` 275 | 276 | 在文件中而不是直接在解释器中编写 Python 时,测试可以写在同一个文件,或者后缀为`_test.py`的相邻文件中。 277 | 278 | **Doctest。**Python 提供了一个便利的方法,将简单的测试直接写到函数的文档字符串内。文档字符串的第一行应该包含单行的函数描述,后面是一个空行。参数和行为的详细描述可以跟随在后面。此外,文档字符串可以包含调用该函数的简单交互式会话: 279 | 280 | ```py 281 | >>> def sum_naturals(n): 282 | """Return the sum of the first n natural numbers 283 | 284 | >>> sum_naturals(10) 285 | 55 286 | >>> sum_naturals(100) 287 | 5050 288 | """ 289 | total, k = 0, 1 290 | while k <= n: 291 | total, k = total + k, k + 1 292 | return total 293 | ``` 294 | 295 | 之后,可以使用[ doctest 模块](http://docs.python.org/py3k/library/doctest.html)来验证交互。下面的`globals`函数返回全局变量的表示,解释器需要它来求解表达式。 296 | 297 | ```py 298 | >>> from doctest import run_docstring_examples 299 | >>> run_docstring_examples(sum_naturals, globals()) 300 | ``` 301 | 302 | 在文件中编写 Python 时,可以通过以下面的命令行选项启动 Python 来运行一个文档中的所有 doctest。 303 | 304 | ```sh 305 | python3 -m doctest 306 | ``` 307 | 308 | 高效测试的关键是在实现新的函数之后(甚至是之前)立即编写(以及执行)测试。只调用一个函数的测试叫做单元测试。详尽的单元测试是良好程序设计的标志。 309 | 310 | -------------------------------------------------------------------------------- /1.6.md: -------------------------------------------------------------------------------- 1 | # 1.6 高阶函数 2 | 3 | > 来源:[1.6 Higher-Order Functions](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/functions.html#higher-order-functions) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 我们已经看到,函数实际上是描述复合操作的抽象,这些操作不依赖于它们的参数值。在`square`中, 10 | 11 | ```py 12 | >>> def square(x): 13 | return x * x 14 | ``` 15 | 16 | 我们不会谈论特定数值的平方,而是一个获得任何数值平方的方法。当然,我们可以不定义这个函数来使用它,通过始终编写这样的表达式: 17 | 18 | ```py 19 | >>> 3 * 3 20 | 9 21 | >>> 5 * 5 22 | 25 23 | ``` 24 | 25 | 并且永远不会显式提及`square`。这种实践适合类似`square`的简单操作。但是对于更加复杂的操作会变得困难。通常,缺少函数定义会对我们非常不利,它会强迫我们始终工作在特定操作的层级上,这在语言中非常原始(这个例子中是乘法),而不是高级操作。我们应该从强大的编程语言索取的东西之一,是通过将名称赋为常用模式来构建抽象的能力,以及之后直接使用抽象的能力。函数提供了这种能力。 26 | 27 | 我们将会在下个例子中看到,代码中会反复出现一些常见的编程模式,但是使用一些不同函数来实现。这些模式也可以被抽象和给予名称。 28 | 29 | 为了将特定的通用模式表达为具名概念,我们需要构造可以接受其他函数作为参数的函数,或者将函数作为返回值的函数。操作函数的函数叫做高阶函数。这一节展示了高阶函数可用作强大的抽象机制,极大提升语言的表现力。 30 | 31 | ## 1.6.1 作为参数的函数 32 | 33 | 考虑下面三个函数,它们都计算总和。第一个,`sum_naturals`,计算截至`n`的自然数的和: 34 | 35 | ```py 36 | >>> def sum_naturals(n): 37 | total, k = 0, 1 38 | while k <= n: 39 | total, k = total + k, k + 1 40 | return total 41 | >>> sum_naturals(100) 42 | 5050 43 | ``` 44 | 45 | 第二个,`sum_cubes`,计算截至`n`的自然数的立方和: 46 | 47 | ```py 48 | >>> def sum_cubes(n): 49 | total, k = 0, 1 50 | while k <= n: 51 | total, k = total + pow(k, 3), k + 1 52 | return total 53 | >>> sum_cubes(100) 54 | 25502500 55 | ``` 56 | 57 | 第三个,计算这个级数中式子的和: 58 | 59 | ![](img/pi_sum.png) 60 | 61 | 它会慢慢收敛于`pi`。 62 | 63 | ```py 64 | >>> def pi_sum(n): 65 | total, k = 0, 1 66 | while k <= n: 67 | total, k = total + 8 / (k * (k + 2)), k + 4 68 | return total 69 | >>> pi_sum(100) 70 | 3.121594652591009 71 | ``` 72 | 73 | 这三个函数在背后都具有相同模式。它们大部分相同,只是名字、用于计算被加项的`k`的函数,以及提供`k`的下一个值的函数不同。我们可以通过向相同的模板中填充槽位来生成每个函数: 74 | 75 | ```py 76 | def (n): 77 | total, k = 0, 1 78 | while k <= n: 79 | total, k = total + (k), (k) 80 | return total 81 | ``` 82 | 83 | 这个通用模板的出现是一个强有力的证据,证明有一个实用抽象正在等着我们表现出来。这些函数的每一个都是式子的求和。作为程序的设计者,我们希望我们的语言足够强大,便于我们编写函数来自我表达求和的概念,而不仅仅是计算特定和的函数。我们可以在 Python 中使用上面展示的通用模板,并且把槽位变成形式参数来轻易完成它。 84 | 85 | ```py 86 | >>> def summation(n, term, next): 87 | total, k = 0, 1 88 | while k <= n: 89 | total, k = total + term(k), next(k) 90 | return total 91 | ``` 92 | 93 | 要注意`summation`接受上界`n`,以及函数`term`和`next`作为参数。我们可以像任何函数那样使用`summation`,它简洁地表达了求和。 94 | 95 | ```py 96 | >>> def cube(k): 97 | return pow(k, 3) 98 | >>> def successor(k): 99 | return k + 1 100 | >>> def sum_cubes(n): 101 | return summation(n, cube, successor) 102 | >>> sum_cubes(3) 103 | 36 104 | ``` 105 | 106 | 使用`identity `函数来返回参数自己,我们就可以对整数求和: 107 | 108 | ```py 109 | >>> def identity(k): 110 | return k 111 | >>> def sum_naturals(n): 112 | return summation(n, identity, successor) 113 | >>> sum_naturals(10) 114 | 55 115 | ``` 116 | 117 | 我们也可以逐步定义`pi_sum`,使用我们的`summation`抽象来组合组件。 118 | 119 | ```py 120 | >>> def pi_term(k): 121 | denominator = k * (k + 2) 122 | return 8 / denominator 123 | >>> def pi_next(k): 124 | return k + 4 125 | >>> def pi_sum(n): 126 | return summation(n, pi_term, pi_next) 127 | >>> pi_sum(1e6) 128 | 3.1415906535898936 129 | ``` 130 | 131 | ## 1.6.2 作为一般方法的函数 132 | 133 | 我们引入的用户定义函数作为一种数值运算的抽象模式,便于使它们独立于涉及到的特定数值。使用高阶函数,我们开始寻找更强大的抽象类型:一些函数表达了计算的一般方法,独立于它们调用的特定函数。 134 | 135 | 尽管函数的意义在概念上扩展了,我们对于如何求解调用表达式的环境模型也优雅地延伸到了高阶函数,没有任何改变。当一个用户定义函数以一些实参调用时,形式参数会在最新的局部帧中绑定实参的值(它们可能是函数)。 136 | 137 | 考虑下面的例子,它实现了迭代改进的一般方法,并且可以用于计算[黄金比例](http://www.geom.uiuc.edu/~demo5337/s97b/art.htm)。迭代改进算法以一个方程的解的`guess`(推测值)开始。它重复调用`update`函数来改进这个推测值,并且调用`test`来检查是否当前的`guess`“足够接近”所认为的正确值。 138 | 139 | ```py 140 | >>> def iter_improve(update, test, guess=1): 141 | while not test(guess): 142 | guess = update(guess) 143 | return guess 144 | ``` 145 | 146 | `test`函数通常检查两个函数`f`和`g`在`guess`值上是否彼此接近。测试`f(x)`是否接近于`g(x)`也是计算的一般方法。 147 | 148 | ```py 149 | >>> def near(x, f, g): 150 | return approx_eq(f(x), g(x)) 151 | ``` 152 | 153 | 程序中测试相似性的一个常见方式是将数值差的绝对值与一个微小的公差值相比: 154 | 155 | ```py 156 | >>> def approx_eq(x, y, tolerance=1e-5): 157 | return abs(x - y) < tolerance 158 | ``` 159 | 160 | 黄金比例,通常叫做`phi`,是经常出现在自然、艺术、和建筑中的数值。它可以通过`iter_improve`使用`golden_update`来计算,并且在它的后继等于它的平方时收敛。 161 | 162 | ```py 163 | >>> def golden_update(guess): 164 | return 1/guess + 1 165 | >>> def golden_test(guess): 166 | return near(guess, square, successor) 167 | ``` 168 | 169 | 这里,我们已经向全局帧添加了多个绑定。函数值的描述为了简短而有所删节: 170 | 171 | ![](img/iter_improve_global.png) 172 | 173 | 使用`golden_update`和`golden_test`参数来调用`iter_improve`会计算出黄金比例的近似值。 174 | 175 | ```py 176 | >>> iter_improve(golden_update, golden_test) 177 | 1.6180371352785146 178 | ``` 179 | 180 | 通过跟踪我们的求值过程的步骤,我们就可以观察结果如何计算。首先,`iter_improve`的局部帧以`update`、`test`和`guess`构建。在`iter_improve`的函数体中,名称`test`绑定到`golden_test`上,它在初始值`guess`上调用。之后,`golden_test`调用`near`,创建第三个局部帧,它将形式参数`f`和`g`绑定到`square`和`successor`上。 181 | 182 | ![](img/iter_improve_apply.png) 183 | 184 | 完成`near`的求值之后,我们看到`golden_test`为`False`,因为 1 并不非常接近于 2。所以,`while`子句代码组内的求值过程,以及这个机制的过程会重复多次。 185 | 186 | 这个扩展后的例子展示了计算机科学中两个相关的重要概念。首先,命名和函数允许我们抽象而远离大量的复杂性。当每个函数定义不重要时,由求值过程触发的计算过程是相当复杂的,并且我们甚至不能展示所有东西。其次,基于事实,我们拥有了非常通用的求值过程,小的组件组合在复杂的过程中。理解这个过程便于我们验证和检查我们创建的程序。 187 | 188 | 像通常一样,我们的新的一般方法`iter_improve`需要测试来检查正确性。黄金比例可以提供这样一个测试,因为它也有一个闭式解,我们可以将它与迭代结果进行比较。 189 | 190 | ```py 191 | >>> phi = 1/2 + pow(5, 1/2)/2 192 | >>> def near_test(): 193 | assert near(phi, square, successor), 'phi * phi is not near phi + 1' 194 | >>> def iter_improve_test(): 195 | approx_phi = iter_improve(golden_update, golden_test) 196 | assert approx_eq(phi, approx_phi), 'phi differs from its approximation' 197 | ``` 198 | 199 | **新的环境特性:**高阶函数。 200 | 201 | **附加部分:**我们在测试的证明中遗漏了一步。求出公差值`e`的范围,使得如果`tolerance`为`e`的`near(x, square, successor)`值为真,那么使用相同公差值的`approx_eq(phi, x)`值为真。 202 | 203 | ## 1.6.3 定义函数 III:嵌套定义 204 | 205 | 上面的例子演示了将函数作为参数传递的能力如何提高了编程语言的表现力。每个通用的概念或方程都能映射为自己的小型函数,这一方式的一个负面效果是全局帧会被小型函数弄乱。另一个问题是我们限制于特定函数的签名:`iter_improve `的`update`参数必须只接受一个参数。Python 中,嵌套函数的定义解决了这些问题,但是需要我们重新修改我们的模型。 206 | 207 | 让我们考虑一个新问题:计算一个数的平方根。重复调用下面的更新操作会收敛于`x`的平方根: 208 | 209 | ```py 210 | >>> def average(x, y): 211 | return (x + y)/2 212 | >>> def sqrt_update(guess, x): 213 | return average(guess, x/guess) 214 | ``` 215 | 216 | 这个带有两个参数的更新函数和`iter_improve`不兼容,并且它只提供了一个介值。我们实际上只关心最后的平方根。这些问题的解决方案是把函数放到其他定义的函数体中。 217 | 218 | ```py 219 | >>> def square_root(x): 220 | def update(guess): 221 | return average(guess, x/guess) 222 | def test(guess): 223 | return approx_eq(square(guess), x) 224 | return iter_improve(update, test) 225 | ``` 226 | 227 | 就像局部赋值,局部的`def`语句仅仅影响当前的局部帧。这些函数仅仅当`square_root`求值时在作用域内。和求值过程一致,局部的`def`语句在`square_root`调用之前并不会求值。 228 | 229 | **词法作用域。**局部定义的函数也可以访问它们定义所在作用域的名称绑定。这个例子中,`update`引用了名称`x`,它是外层函数`square_root`的一个形参。这种在嵌套函数中共享名称的规则叫做词法作用域。严格来说,内部函数能够访问定义所在环境(而不是调用所在位置)的名称。 230 | 231 | 我们需要两个对我们环境的扩展来兼容词法作用域。 232 | 233 | 1. 每个用户定义的函数都有一个关联环境:它的定义所在的环境。 234 | 2. 当一个用户定义的函数调用时,它的局部帧扩展于函数所关联的环境。 235 | 236 | 回到`square_root`,所有函数都在全局环境中定义,所以它们都关联到全局环境,当我们求解`square_root`的前两个子句时,我们创建了关联到局部环境的函数。在 237 | 238 | ```py 239 | >>> square_root(256) 240 | 16.00000000000039 241 | ``` 242 | 243 | 的调用中,环境首先添加了`square_root`的局部帧,并且求出`def`语句`update`和`test`(只展示了`update`): 244 | 245 | ![](img/square_root.png) 246 | 247 | 随后,`update`的名称解析到这个新定义的函数上,它是向`iter_improve`传入的参数。在`iter_improve`的函数体中,我们必须以初始值 1 调用`update`函数。最后的这个调用以一开始只含有`g`的局部帧创建了`update`的环境,但是之前的`square_root`帧上仍旧含有`x`的绑定。 248 | 249 | ![](img/square_root_update.png) 250 | 251 | 这个求值过程中,最重要的部分是函数所关联的环境变成了局部帧,它是函数求值的地方。这个改变在图中以蓝色箭头高亮。 252 | 253 | 以这种方式,`update`的函数体能够解析名称`x`。所以我们意识到了词法作用域的两个关键优势。 254 | 255 | + 局部函数的名称并不影响定义所在函数外部的名称,因为局部函数的名称绑定到了定义处的当前局部环境中,而不是全局环境。 256 | + 局部函数可以访问外层函数的环境。这是因为局部函数的函数体的求值环境扩展于定义处的求值环境。 257 | 258 | `update`函数自带了一些数据:也就是在定义处环境中的数据。因为它以这种方式封装信息,局部定义的函数通常叫做闭包。 259 | 260 | **新的环境特性:**局部函数定义。 261 | 262 | ## 1.6.4 作为返回值的函数 263 | 264 | 我们的程序可以通过创建返回值是它们本身的函数,获得更高的表现力。带有词法作用域的编程语言的一个重要特性就是,局部定义函数在它们返回时仍旧持有所关联的环境。下面的例子展示了这一特性的作用。 265 | 266 | 在定义了许多简单函数之后,`composition`是包含在我们的编程语言中的自然组合法。也就是说,提供两个函数`f(x)`和`g(x)`,我们可能希望定义`h(x) = f(g(x))`。我们可以使用现有工具来定义复合函数: 267 | 268 | ```py 269 | >>> def compose1(f, g): 270 | def h(x): 271 | return f(g(x)) 272 | return h 273 | >>> add_one_and_square = compose1(square, successor) 274 | >>> add_one_and_square(12) 275 | 169 276 | ``` 277 | 278 | `compose1`中的`1`表明复合函数和返回值都只接受一个参数。这种命名惯例并不由解释器强制,`1`只是函数名称的一部分。 279 | 280 | 这里,我们开始观察我们在计算的复杂模型中投入的回报。我们的环境模型不需要任何修改就能支持以这种方式返回函数的能力。 281 | 282 | ## 1.6.5 Lambda 表达式 283 | 284 | 目前为止,每次我们打算定义新的函数时,我们都会给它一个名称。但是对于其它类型的表达式,我们不需要将一个间接产物关联到名称上。也就是说,我们可以计算`a*b + c*d`,而不需要给子表达式`a*b`或`c*d`,或者整个表达式来命名。Python 中,我们可以使用 Lambda 表达式凭空创建函数,它会求值为匿名函数。Lambda 表达式是函数体具有单个返回表达式的函数,不允许出现赋值和控制语句。 285 | 286 | Lambda 表达式十分受限:它们仅仅可用于简单的单行函数,求解和返回一个表达式。在它们适用的特殊情形中,Lambda 表达式具有强大的表现力。 287 | 288 | ```py 289 | >>> def compose1(f,g): 290 | return lambda x: f(g(x)) 291 | ``` 292 | 293 | 我们可以通过构造相应的英文语句来理解 Lambda 表达式: 294 | 295 | ```py 296 | lambda x : f(g(x)) 297 | "A function that takes x and returns f(g(x))" 298 | ``` 299 | 300 | 一些程序员发现使用 Lambda 表达式作为匿名函数非常简短和直接。但是,复合的 Lambda 表达式非常难以辨认,尽管它们很简洁。下面的定义是是正确的,但是许多程序员不能很快地理解它: 301 | 302 | ```py 303 | >>> compose1 = lambda f,g: lambda x: f(g(x)) 304 | ``` 305 | 306 | 通常,Python 的代码风格倾向于显式的`def`语句而不是 Lambda 表达式,但是允许它们在简单函数作为参数或返回值的情况下使用。 307 | 308 | 这种风格规范不是准则,你可以想怎么写就怎么写,但是,在你编写程序时,要考虑某一天可能会阅读你的程序的人们。如果你可以让你的程序更易于理解,你就帮了人们一个忙。 309 | 310 | Lambda 的术语是一个历史的偶然结果,来源于手写的数学符号和早期打字系统限制的不兼容。 311 | 312 | > 使用 lambda 来引入过程或函数看起来是不正当的。这个符号要追溯到 Alonzo Church,他在 20 世纪 30 年代开始使用“帽子”符号;他把平方函数记为`ŷ . y × y`。但是失败的打字员将这个帽子移到了参数左边,并且把它改成了大写的 lambda:`Λy . y × y`;之后大写的 lambda 就变成了小写,现在我们就会在数学书里看到`λy . y × y`,以及在 Lisp 里看到`(lambda (y) (* y y))`。 313 | 314 | > -- Peter Norvig (norvig.com/lispy2.html) 315 | 316 | 尽管它的词源不同寻常,Lambda 表达式和函数调用相应的形式语言,以及 Lambda 演算都成为了计算机科学概念的基础,并在 Python 编程社区广泛传播。当我们学习解释器的设计时,我们将会在第三章中重新碰到这个话题。 317 | 318 | ## 1.6.6 示例:牛顿法 319 | 320 | 最后的扩展示例展示了函数值、局部定义和 Lambda 表达式如何一起工作来简明地表达通常的概念。 321 | 322 | 牛顿法是一个传统的迭代方法,用于寻找使数学函数返回值为零的参数。这些值叫做一元数学函数的根。寻找一个函数的根通常等价于求解一个相关的数学方程。 323 | 324 | + 16 的平方根是满足`square(x) - 16 = 0`的`x`值。 325 | + 以 2 为底 32 的对数(例如 2 与某个指数的幂为 32)是满足`pow(2, x) - 32 = 0`的`x`值。 326 | 327 | 所以,求根的通用方法会向我们提供算法来计算平方根和对数。而且,我们想要计算根的等式只包含简单操作:乘法和乘方。 328 | 329 | 在我们继续之前有个注解:我们知道如何计算平方根和对数,这个事实很容易当做自然的事情。并不只是 Python,你的手机和计算机,可能甚至你的手表都可以为你做这件事。但是,学习计算机科学的一部分是弄懂这些数如何计算,而且,这里展示的通用方法可以用于求解大量方程,而不仅仅是内建于 Python 的东西。 330 | 331 | 在开始理解牛顿法之前,我们可以开始编程了。这就是函数抽象的威力。我们简单地将之前的语句翻译成代码: 332 | 333 | ```py 334 | >>> def square_root(a): 335 | return find_root(lambda x: square(x) - a) 336 | >>> def logarithm(a, base=2): 337 | return find_root(lambda x: pow(base, x) - a) 338 | ``` 339 | 340 | 当然,在我们定义`find_root`之前,现在还不能调用任何函数,所以我们需要理解牛顿法如何工作。 341 | 342 | 牛顿法也是一个迭代改进算法:它会改进任何可导函数的根的推测值。要注意我们感兴趣的两个函数都是平滑的。对于 343 | 344 | + `f(x) = square(x) - 16`(细线) 345 | + `f(x) = pow(2, x) - 32`(粗线) 346 | 347 | 在二维平面上画出`x`对`f(x)`的图像,它展示了两个函数都产生了光滑的曲线,它们在某个点穿过了 0。 348 | 349 | ![](img/curves.png) 350 | 351 | 由于它们是光滑的(可导的),这些曲线可以通过任何点上的直线来近似。牛顿法根据这些线性的近似值来寻找函数的根。 352 | 353 | 想象经过点`(x, f(x))`的一条直线,它与函数`f(x)`的曲线在这一点的斜率相同。这样的直线叫做切线,它的斜率叫做`f`在`x`上的导数。 354 | 355 | 这条直线的斜率是函数值改变量与函数参数改变量的比值。所以,按照`f(x)`除以这个斜率来平移`x`,就会得到切线到达 0 时的`x`值。 356 | 357 | ![](img/newton.png) 358 | 359 | 我们的牛顿更新操作表达了跟随这条切线到零的计算过程。我们通过在非常小的区间上计算函数斜率来近似得到函数的导数。 360 | 361 | ```py 362 | >>> def approx_derivative(f, x, delta=1e-5): 363 | df = f(x + delta) - f(x) 364 | return df/delta 365 | >>> def newton_update(f): 366 | def update(x): 367 | return x - f(x) / approx_derivative(f, x) 368 | return update 369 | ``` 370 | 371 | 最后,我们可以定义基于`newton_update`(我们的迭代改进算法)的`find_root`函数,以及一个测试来观察`f(x)`是否接近于 0。我们提供了一个较大的初始推测值来提升`logarithm`的性能。 372 | 373 | ```py 374 | >>> def find_root(f, initial_guess=10): 375 | def test(x): 376 | return approx_eq(f(x), 0) 377 | return iter_improve(newton_update(f), test, initial_guess) 378 | >>> square_root(16) 379 | 4.000000000026422 380 | >>> logarithm(32, 2) 381 | 5.000000094858201 382 | ``` 383 | 384 | 当你实验牛顿法时,要注意它不总是收敛的。`iter_improve`的初始推测值必须足够接近于根,而且函数必须满足各种条件。虽然具有这些缺陷,牛顿法是一个用于解决微分方程的强大的通用计算方法。实际上,非常快速的对数算法和大整数除法也采用这个技巧的变体。 385 | 386 | ## 1.6.7 抽象和一等函数 387 | 388 | 这一节的开始,我们以观察用户定义函数作为关键的抽象技巧,因为它们让我们能够将计算的通用方法表达为编程语言中的显式元素。现在我们已经看到了高阶函数如何让我们操作这些通用方法来进一步创建抽象。 389 | 390 | 作为程序员,我们应该留意识别程序中低级抽象的机会,在它们之上构建,并泛化它们来创建更加强大的抽象。这并不是说,一个人应该总是尽可能以最抽象的方式来编程;专家级程序员知道如何选择合适于他们任务的抽象级别。但是能够基于这些抽象来思考,以便我们在新的上下文中能使用它们十分重要。高阶函数的重要性是,它允许我们更加明显地将这些抽象表达为编程语言中的元素,使它们能够处理其它的计算元素。 391 | 392 | 通常,编程语言会限制操作计算元素的途径。带有最少限制的元素被称为具有一等地位。一些一等元素的“权利和特权”是: 393 | 394 | 1. 它们可以绑定到名称。 395 | 2. 它们可以作为参数向函数传递。 396 | 3. 它们可以作为函数的返回值返回。 397 | 4. 它们可以包含在好素具结构中。 398 | 399 | Python 总是给予函数一等地位,所产生的表现力的收益是巨大的。另一方面,控制结构不能做到:你不能像使用`sum`那样将`if`传给一个函数。 400 | 401 | ## 1.6.8 函数装饰器 402 | 403 | Python 提供了特殊的语法,将高阶函数用作执行`def`语句的一部分,叫做装饰器。 404 | 405 | 406 | ```py 407 | >>> def trace1(fn): 408 | def wrapped(x): 409 | print('-> ', fn, '(', x, ')') 410 | return fn(x) 411 | return wrapped 412 | >>> @trace1 413 | def triple(x): 414 | return 3 * x 415 | >>> triple(12) 416 | -> ( 12 ) 417 | 36 418 | ``` 419 | 420 | 这个例子中,定义了高阶函数`trace1`,它返回一个函数,这个函数在调用它的参数之前执行`print`语句来输出参数。`triple`的`def`语句拥有一个注解,`@trace1`,它会影响`def`的执行规则。像通常一样,函数`triple`被创建了,但是,`triple`的名称并没有绑定到这个函数上,而是绑定到了在新定义的函数`triple`上调用`trace1`的返回函数值上。在代码中,这个装饰器等价于: 421 | 422 | ```py 423 | >>> def triple(x): 424 | return 3 * x 425 | >>> triple = trace1(triple) 426 | ``` 427 | 428 | **附加部分:**实际规则是,装饰器符号`@`可以放在表达式前面(`@trace1`仅仅是一个简单的表达式,由单一名称组成)。任何产生合适的值的表达式都可以。例如,使用合适的值,你可以定义装饰器`check_range`,使用`@check_range(1, 10)`来装饰函数定义,这会检查函数的结果来确保它们是 1 到 10 的整数。调用`check_range(1,10)`会返回一个函数,之后它会用在新定义的函数上,在新定义的函数绑定到`def`语句中的名称之前。感兴趣的同学可以阅读 Ariel Ortiz 编写的[一篇装饰器的简短教程](http://programmingbits.pythonblogs.com/27_programmingbits/archive/50_function_decorators.html)来了解更多的例子。 429 | -------------------------------------------------------------------------------- /2.1.md: -------------------------------------------------------------------------------- 1 | # 2.1 引言 2 | 3 | > 来源:[2.1 Introduction](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/objects.html#introduction) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 在第一章中,我们专注于计算过程,以及程序设计中函数的作用。我们看到了如何使用原始数据(数值)和原始操作(算术运算),如何通过组合和控制来形成复合函数,以及如何通过给予过程名称来创建函数抽象。我们也看到了高阶函数通过操作通用计算方法来提升语言的威力。这是编程的本质。 10 | 11 | 这一章会专注于数据。数据允许我们通过使用已经获得的计算工具,表示和操作与世界有关的信息。脱离数据结构的编程可能会满足于探索数学特性,但是真实世界的情况,比如文档、关系、城市和气候模式,都拥有复杂的结构,它最好使用复合数据类型来表现。归功于互联网的高速发展,关于世界的大量结构信息可以免费从网上获得。 12 | 13 | ## 2.1.1 对象隐喻 14 | 15 | 在这门课的开始,我们区分了函数和数据:函数执行操作,而数据被操作。当我们在数据中包含函数值时,我们承认数据也拥有行为。函数可以像数据一样被操作,但是也可以被调用来执行计算。 16 | 17 | 在这门课中,对象作为我们对数据值的核心编程隐喻,它同样拥有行为。对象表示信息,但是同时和它们所表示的抽象概念行为一致。对象如何和其它对象交互的逻辑,和编码对象值的信息绑定在一起。在打印对象时,它知道如何以字母和数字把自己拼写出来。如果一个对象由几部分组成,它知道如何按照要求展示这些部分。对象既是信息也是过程,它们绑定在一起来展示复杂事物的属性、交互和行为。 18 | 19 | Python 中所实现的对象隐喻具有特定的对象语法和相关的术语,我们会使用示例来介绍。日期(`date`)就是一种简单对象。 20 | 21 | ```py 22 | >>> from datetime import date 23 | ``` 24 | 25 | `date`的名字绑定到了一个类上面。类表示一类对象。独立的日期叫做这个类的实例,它们可以通过像函数那样在参数上调用这个类来构造,这些参数描述了实例。 26 | 27 | 28 | ```py 29 | >>> today = date(2011, 9, 12) 30 | ``` 31 | 32 | 虽然`today`从原始数值中构造,它的行为就像日期那样。例如,将它与另一个日期相减会得到时间差,它可以通过调用`str`来展示为一行文本: 33 | 34 | ```py 35 | >>> str(date(2011, 12, 2) - today) 36 | '81 days, 0:00:00' 37 | ``` 38 | 39 | 对象拥有属性,它们是带有名字的值,也是对象的一部分。Python 中,我们使用点运算符来访问对象属性: 40 | 41 | ``` 42 | . 43 | ``` 44 | 45 | 上面的``求值为对象,``是对象的某个属性名称。 46 | 47 | 不像我们之前见过的名称,这些属性名称在一般的环境中不可用。反之,属性名称是点运算符之前的对象实例的特定部分。 48 | 49 | ```py 50 | >>> today.year 51 | 2011 52 | ``` 53 | 54 | 对象也拥有方法,它是值为函数的属性。在隐喻上,对象“知道”如何执行这些方法。方法从它们的参数和对象中计算出它们的结果。例如,`today`的`strftime`方法接受一个指定如何展示日期的参数(例如`%A`表示星期几应该以全称拼写)。 55 | 56 | ```py 57 | >>> today.strftime('%A, %B %d') 58 | 'Monday, September 12' 59 | ``` 60 | 61 | 计算`strftime`的返回值需要两个输入:描述输出格式的字符串,以及绑定到`today`的日期信息。这个方法使用日期特定的逻辑来产生结果。我们从不会说 2011 年九月十二日是星期一,但是知道一个人的工作日是日期的一部分。通过绑定行为和信息,Python 对象提供了可靠、独立的日期抽象。 62 | 63 | 点运算符在 Python 中提供了另一种组合表达式。点运算符拥有定义好的求值过程。但是,点运算符如何求值的精确解释,要等到我们引入面向对象编程的完整范式,在几节之后。 64 | 65 | 即使我们还不能精确描述对象如何工作,我们还是可以开始将数据看做对象,因为 Python 中万物皆对象。 66 | 67 | ## 2.1.2 原始数据类型 68 | 69 | Python 中每个对象都拥有一个类型。`type`函数可以让我们查看对象的类型。 70 | 71 | ```py 72 | >>> type(today) 73 | 74 | ``` 75 | 76 | 目前为止,我们学过的对象类型只有数值、函数、布尔值和现在的日期。我们也碰到了集合和字符串,但是需要更深入地学习它们。有许多其它的对象类型 -- 声音、图像、位置、数据连接等等 -- 它们的多数可以通过组合和抽象的手段来定义,我们在这一章会研究它们。Python 只有一小部分内建于语言的原始或原生数据类型。 77 | 78 | 原始数据类型具有以下特性: 79 | 80 | 1. 原始表达式可以计算这些类型的对象,叫做字面值。 81 | 2. 内建的函数、运算符和方法可以操作这些对象。 82 | 83 | 像我们看到的那样,数值是原始类型,数字字面值求值为数值,算术运算符操作数值对象: 84 | 85 | ```py 86 | >>> 12 + 3000000000000000000000000 87 | 3000000000000000000000012 88 | ``` 89 | 90 | 实际上,Python 包含了三个原始数值类型:整数(`int`)、实数(`float`)和复数(`complex`)。 91 | 92 | ```py 93 | >>> type(2) 94 | 95 | >>> type(1.5) 96 | 97 | >>> type(1+1j) 98 | 99 | ``` 100 | 101 | 名称`float`来源于实数在 Python 中表示的方式:“浮点”表示。虽然数值表示的细节不是这门课的话题,一些`int`和`float`对象的高层差异仍然很重要。特别是,`int`对象只能表示整数,但是表示得更精确,不带有任何近似。另一方面,`float`对象可以表示很大范围内的分数,但是不能表示所有有理数。然而,浮点对象通常用于近似表示实数和有理数,舍入到某个有效数字的数值。 102 | 103 | **扩展阅读。**下面的章节介绍了更多的 Python 原始数据类型,专注于它们在创建实用数据抽象中的作用。Dive Into Python 3 中的[原始数据类型](http://diveintopython3.ep.io/native-datatypes.html)一章提供了所有 Python 数据类型的实用概览,以及如何高效使用它们,还包含了许多使用示例和实践提示。你现在并不需要阅读它,但是要考虑将它作为宝贵的参考。 104 | -------------------------------------------------------------------------------- /2.2.md: -------------------------------------------------------------------------------- 1 | # 2.2 数据抽象 2 | 3 | > 来源:[2.2 Data Abstraction](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/objects.html#introduction) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 由于我们希望在程序中表达世界中的大量事物,我们发现它们的大多数都具有复合结构。日期是年月日,地理位置是精度和纬度。为了表示位置,我们希望程序语言具有将精度和纬度“粘合”为一对数据的能力 -- 也就是一个复合数据结构 -- 使我们的程序能够以一种方式操作数据,将位置看做单个概念单元,它拥有两个部分。 10 | 11 | 复合数据的使用也让我们增加程序的模块性。如果我们可以直接将地理位置看做对象来操作,我们就可以将程序的各个部分分离,它们根据这些值如何表示来从本质上处理这些值。将某个部分从程序中分离的一般技巧是一种叫做数据抽象的强大的设计方法论。这个部分用于处理数据表示,而程序用于操作数据。数据抽象使程序更易于设计、维护和修改。 12 | 13 | 数据抽象的特征类似于函数抽象。当我们创建函数抽象时,函数如何实现的细节被隐藏了,而且特定的函数本身可以被任何具有相同行为的函数替换。换句话说,我们可以构造抽象来使函数的使用方式和函数的实现细节分离。与之相似,数据抽象是一种方法论,使我们将复合数据对象的使用细节与它的构造方式隔离。 14 | 15 | 数据抽象的基本概念是构造操作抽象数据的程序。也就是说,我们的程序应该以一种方式来使用数据,对数据做出尽可能少的假设。同时,需要定义具体的数据表示,独立于使用数据的程序。我们系统中这两部分的接口是一系列函数,叫做选择器和构造器,它们基于具体表示实现了抽象数据。为了演示这个技巧,我们需要考虑如何设计一系列函数来操作有理数。 16 | 17 | 当你阅读下一节时,要记住当今编写的多数 Python 代码使用了非常高级的抽象数据类型,它们内建于语言中,比如类、字典和列表。由于我们正在了解这些抽象的工作原理,我们自己不能使用它们。所以,我们会编写一些不那么 Python 化的代码 -- 它并不是在语言中实现我们的概念的通常方式。但是,我们所编写的代码出于教育目的,它展示了这些抽象如何构建。要记住计算机科学并不只是学习如何使用编程语言,也学习它们的工作原理。 18 | 19 | ## 2.2.1 示例:有理数的算术 20 | 21 | 有理数可表示为整数的比值,并且它组成了实数的一个重要子类。类似于`1/3`或者`17/29`的有理数通常可编写为: 22 | 23 | ``` 24 | / 25 | ``` 26 | 27 | 其中,``和``都是值为整数的占位符。有理数的值需要两部分来描述。 28 | 29 | 有理数在计算机科学中很重要,因为它们就像整数那样,可以准确表示。无理数(比如`pi` 或者 `e` 或者 `sqrt(2)`)会使用有限的二元展开代替为近似值。所以在原则上,有理数的处理应该让我们避免算术中的近似误差。 30 | 31 | 但是,一旦我们真正将分子与分母相除,我们就会只剩下截断的小数近似值: 32 | 33 | ```py 34 | >>> 1/3 35 | 0.3333333333333333 36 | ``` 37 | 38 | 当我们开始执行测试时,这个近似值的问题就会出现: 39 | 40 | ```py 41 | >>> 1/3 == 0.333333333333333300000 # Beware of approximations 42 | True 43 | ``` 44 | 45 | 计算机如何将实数近似为定长的小数扩展,是另一门课的话题。这里的重要概念是,通过将有理数表示为整数的比值,我们能够完全避免近似问题。所以出于精确,我们希望将分子和分母分离,但是将它们看做一个单元。 46 | 47 | 我们从函数抽象中了解到,我们可以在了解某些部分的实现之前开始编出东西来。让我们一开始假设我们已经拥有一种从分子和分母中构造有理数的方式。我们也假设,给定一个有理数,我们都有办法来提取(或选中)它的分子和分母。让我们进一步假设,构造器和选择器以下面三个函数来提供: 48 | 49 | + `make_rat(n, d)`返回分子为`n`和分母为`d`的有理数。 50 | + `numer(x)`返回有理数`x`的分子。 51 | + `denom(x)`返回有理数`x`的分母。 52 | 53 | 我们在这里正在使用一个强大的合成策略:心想事成。我们并没有说有理数如何表示,或者`numer`、`denom`和`make_rat`如何实现。即使这样,如果我们拥有了这三个函数,我们就可以执行加法、乘法,以及测试有理数的相等性,通过调用它们: 54 | 55 | ```py 56 | >>> def add_rat(x, y): 57 | nx, dx = numer(x), denom(x) 58 | ny, dy = numer(y), denom(y) 59 | return make_rat(nx * dy + ny * dx, dx * dy) 60 | >>> def mul_rat(x, y): 61 | return make_rat(numer(x) * numer(y), denom(x) * denom(y)) 62 | >>> def eq_rat(x, y): 63 | return numer(x) * denom(y) == numer(y) * denom(x) 64 | ``` 65 | 66 | 现在我们拥有了由选择器函数`numer`和`denom`,以及构造器函数`make_rat`定义的有理数操作。但是我们还没有定义这些函数。我们需要以某种方式来将分子和分母粘合为一个单元。 67 | 68 | ## 2.2.2 元组 69 | 70 | 为了实现我们的数据抽象的具体层面,Python 提供了一种复合数据结构叫做`tuple`,它可以由逗号分隔的值来构造。虽然并不是严格要求,圆括号通常在元组周围。 71 | 72 | ```py 73 | >>> (1, 2) 74 | (1, 2) 75 | ``` 76 | 77 | 元组的元素可以由两种方式解构。第一种是我们熟悉的多重赋值: 78 | 79 | ```py 80 | >>> pair = (1, 2) 81 | >>> pair 82 | (1, 2) 83 | >>> x, y = pair 84 | >>> x 85 | 1 86 | >>> y 87 | 2 88 | ``` 89 | 90 | 实际上,多重赋值的本质是创建和解构元组。 91 | 92 | 访问元组元素的第二种方式是通过下标运算符,写作方括号: 93 | 94 | ```py 95 | >>> pair[0] 96 | 1 97 | >>> pair[1] 98 | 2 99 | ``` 100 | 101 | Python 中的元组(以及多数其它编程语言中的序列)下标都以 0 开始,也就是说,下标 0 表示第一个元素,下标 1 表示第二个元素,以此类推。我们对这个下标惯例的直觉是,下标表示一个元素距离元组开头有多远。 102 | 103 | 与元素选择操作等价的函数叫做`getitem`,它也使用下标以 0 开始的位置来在元组中选择元素。 104 | 105 | 元素是原始类型,也就是说 Python 的内建运算符可以操作它们。我们不久之后再来看元素的完整特性。现在,我们只对元组如何作为胶水来实现抽象数据类型感兴趣。 106 | 107 | **表示有理数。**元素提供了一个自然的方式来将有理数实现为一对整数:分子和分母。我们可以通过操作二元组来实现我们的有理数构造器和选择器函数。 108 | 109 | ```py 110 | >>> def make_rat(n, d): 111 | return (n, d) 112 | >>> def numer(x): 113 | return getitem(x, 0) 114 | >>> def denom(x): 115 | return getitem(x, 1) 116 | ``` 117 | 118 | 用于打印有理数的函数完成了我们对抽象数据结构的实现。 119 | 120 | ```py 121 | >>> def str_rat(x): 122 | """Return a string 'n/d' for numerator n and denominator d.""" 123 | return '{0}/{1}'.format(numer(x), denom(x)) 124 | ``` 125 | 126 | 将它与我们之前定义的算术运算放在一起,我们可以使用我们定义的函数来操作有理数了。 127 | 128 | ```py 129 | >>> half = make_rat(1, 2) 130 | >>> str_rat(half) 131 | '1/2' 132 | >>> third = make_rat(1, 3) 133 | >>> str_rat(mul_rat(half, third)) 134 | '1/6' 135 | >>> str_rat(add_rat(third, third)) 136 | '6/9' 137 | ``` 138 | 139 | 就像最后的例子所展示的那样,我们的有理数实现并没有将有理数化为最简。我们可以通过修改`make_rat`来补救。如果我们拥有用于计算两个整数的最大公约数的函数,我们可以在构造一对整数之前将分子和分母化为最简。这可以使用许多实用工具,例如 Python 库中的现存函数。 140 | 141 | ```py 142 | >>> from fractions import gcd 143 | >>> def make_rat(n, d): 144 | g = gcd(n, d) 145 | return (n//g, d//g) 146 | ``` 147 | 148 | 双斜杠运算符`//`表示整数除法,它会向下取整除法结果的小数部分。由于我们知道`g`能整除`n`和`d`,整数除法正好适用于这里。现在我们的 149 | 150 | ```py 151 | >>> str_rat(add_rat(third, third)) 152 | '2/3' 153 | ``` 154 | 155 | 符合要求。这个修改只通过修改构造器来完成,并没有修改任何实现实际算术运算的函数。 156 | 157 | **扩展阅读。**上面的`str_rat`实现使用了格式化字符串,它包含了值的占位符。如何使用格式化字符串和`format`方法的细节请见 Dive Into Python 3 的[格式化字符串](http://diveintopython3.ep.io/strings.html#formatting-strings)一节。 158 | 159 | ## 2.2.3 抽象界限 160 | 161 | 在以更多复合数据和数据抽象的例子继续之前,让我们思考一些由有理数示例产生的问题。我们使用构造器`make_rat`和选择器`numer`和`denom`定义了操作。通常,数据抽象的底层概念是,基于某个值的类型的操作如何表达,为这个值的类型确定一组基本的操作。之后使用这些操作来操作数据。 162 | 163 | 我们可以将有理数系统想象为一系列层级。 164 | 165 | ![](img/barriers.png) 166 | 167 | 平行线表示隔离系统不同层级的界限。每一层上,界限分离了使用数据抽象的函数(上面)和实现数据抽象的函数(下面)。使用有理数的程序仅仅通过算术函数来操作它们:`add_rat`、`mul_rat`和`eq_rat`。相应地,这些函数仅仅由构造器和选择器`make_rat`、`numer`和`and denom`来实现,它们本身由元组实现。元组如何实现的字节和其它层级没有关系,只要元组支持选择器和构造器的实现。 168 | 169 | 每一层上,盒子中的函数强制划分了抽象的边界,因为它们仅仅依赖于上层的表现(通过使用)和底层的实现(通过定义)。这样,抽象界限可以表现为一系列函数。 170 | 171 | 抽象界限具有许多好处。一个好处就是,它们使程序更易于维护和修改。很少的函数依赖于特定的表现,当一个人希望修改表现时,不需要做很多修改。 172 | 173 | ## 2.2.4 数据属性 174 | 175 | 我们通过实现算术运算来开始实现有理数,实现为这三个非特定函数:`make_rat`、`numer`和`denom`。这里,我们可以认为已经定义了数据对象 -- 分子、分母和有理数 -- 上的运算,它们的行为由这三个函数规定。 176 | 177 | 但是数据意味着什么?我们还不能说“提供的选择器和构造器实现了任何东西”。我们需要保证这些函数一起规定了正确的行为。也就是说,如果我们从整数`n`和`d`中构造了有理数`x`,那么`numer(x)/denom(x)`应该等于`n/d`。 178 | 179 | 通常,我们可以将抽象数据类型当做一些选择器和构造器的集合,并带有一些行为条件。只要满足了行为条件(比如上面的除法特性),这些函数就组成了数据类型的有效表示。 180 | 181 | 这个观点可以用在其他数据类型上,例如我们为实现有理数而使用的二元组。我们实际上不会谈论元组是什么,而是谈论由语言提供的,用于操作和创建元组的运算符。我们现在可以描述二元组的行为条件,二元组通常叫做偶对,在表示有理数的问题中有所涉及。 182 | 183 | 为了实现有理数,我们需要一种两个整数的粘合形式,它具有下列行为: 184 | 185 | + 如果一个偶对`p`由`x`和`y`构造,那么`getitem_pair(p, 0)`返回`x`,`getitem_pair(p, 1)`返回`y`。 186 | 187 | 我们可以实现`make_pair`和`getitem_pair`,它们和元组一样满足这个描述: 188 | 189 | ```py 190 | >>> def make_pair(x, y): 191 | """Return a function that behaves like a pair.""" 192 | def dispatch(m): 193 | if m == 0: 194 | return x 195 | elif m == 1: 196 | return y 197 | return dispatch 198 | >>> def getitem_pair(p, i): 199 | """Return the element at index i of pair p.""" 200 | return p(i) 201 | ``` 202 | 203 | 使用这个实现,我们可以创建和操作偶对: 204 | 205 | ```py 206 | >>> p = make_pair(1, 2) 207 | >>> getitem_pair(p, 0) 208 | 1 209 | >>> getitem_pair(p, 1) 210 | 2 211 | ``` 212 | 213 | 这个函数的用法不同于任何直观上的,数据应该是什么的概念。而且,这些函数满足于在我们的程序中表示复合数据。 214 | 215 | 需要注意的微妙的一点是,由`make_pair`返回的值是叫做`dispatch`的函数,它接受参数`m`并返回`x`或`y`。之后,`getitem_pair`调用了这个函数来获取合适的值。我们在这一章中会多次返回这个调度函数的话题。 216 | 217 | 这个偶对的函数表示并不是 Python 实际的工作机制(元组实现得更直接,出于性能因素),但是它可以以这种方式工作。这个函数表示虽然不是很明显,但是是一种足够完美来表示偶对的方式,因为它满足了偶对唯一需要满足的条件。这个例子也表明,将函数当做值来操作的能力,提供给我们表示复合数据的能力。 218 | -------------------------------------------------------------------------------- /2.3.md: -------------------------------------------------------------------------------- 1 | # 2.3 序列 2 | 3 | > 来源:[2.3 Sequences](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/objects.html#sequences) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 序列是数据值的顺序容器。不像偶对只有两个元素,序列可以拥有任意(但是有限)个有序元素。 10 | 11 | 序列在计算机科学中是强大而基本的抽象。例如,如果我们使用序列,我们就可以列出伯克利的每个学生,或者世界上的每所大学,或者每所大学中的每个学生。我们可以列出上过的每一门课,提交的每个作业,或者得到的每个成绩。序列抽象让数千个数据驱动的程序影响着我们每天的生活。 12 | 13 | 序列不是特定的抽象数据类型,而是不同类型共有的一组行为。也就是说,它们是许多序列种类,但是都有一定的属性。特别地, 14 | 15 | **长度。**序列拥有有限的长度。 16 | 17 | **元素选择。**序列的每个元素都拥有相应的非负整数作为下标,它小于序列长度,以第一个元素的 0 开始。 18 | 19 | 不像抽象数据类型,我们并没有阐述如何构造序列。序列抽象是一组行为,它们并没有完全指定类型(例如,使用构造器和选择器),但是可以在多种类型中共享。序列提供了一个抽象层级,将特定程序如何操作序列类型的细节隐藏。 20 | 21 | 这一节中,我们开发了一个特定的抽象数据类型,它可以实现序列抽象。我们之后介绍实现相同抽象的 Python 内建类型。 22 | 23 | ## 2.3.1 嵌套偶对 24 | 25 | 对于有理数,我们使用二元组将两个整数对象配对,之后展示了我们可以同样通过函数来实现偶对。这种情况下,每个我们构造的偶对的元素都是整数。然而,就像表达式,元组可以嵌套。每个偶对的元素本身也可以是偶对,这个特性在实现偶对的任意一个方法,元组或调度函数中都有效。 26 | 27 | 可视化偶对的一个标准方法 -- 这里也就是偶对`(1,2)` -- 叫做盒子和指针记号。每个值,复合或原始,都描述为指向盒子的指针。原始值的盒子只包含那个值的表示。例如,数值的盒子只包含数字。偶对的盒子实际上是两个盒子:左边的部分(箭头指向的)包含偶对的第一个元素,右边的部分包含第二个。 28 | 29 | ![](img/pair.png) 30 | 31 | 嵌套元素的 Python 表达式: 32 | 33 | ```py 34 | >>> ((1, 2), (3, 4)) 35 | ((1, 2), (3, 4)) 36 | ``` 37 | 38 | 具有下面的结构: 39 | 40 | ![](img/nested_pairs.png) 41 | 42 | 使用元组作为其它元组元素的能力,提供了我们编程语言中的一个新的组合手段。我们将这种将元组以这种方式嵌套的能力叫做元组数据类型的封闭性。通常,如果组合结果自己可以使用相同的方式组合,组合数据值的方式就满足封闭性。封闭性在任何组合手段中都是核心能力,因为它允许我们创建层次数据结构 -- 结构由多个部分组成,它们自己也由多个部分组成,以此类推。我们在第三章会探索一些层次结构。现在,我们考虑一个特定的重要结构。 43 | 44 | ## 2.3.2 递归列表 45 | 46 | 我们可以使用嵌套偶对来构建任意长度的元素列表,它让我们能够实现抽象序列。下面的图展示了四元素列表`1, 2, 3, 4`的递归表示: 47 | 48 | ![](img/sequence.png) 49 | 50 | 这个列表由一系列偶对表示。每个偶对的第一个元素是列表中的元素,而第二个元素是用于表示列表其余部分的偶对。最后一个偶对的第二个元素是`None`,它表明列表到末尾了。我们可以使用嵌套的元组字面值来构造这个结构: 51 | 52 | ```py 53 | >>> (1, (2, (3, (4, None)))) 54 | (1, (2, (3, (4, None)))) 55 | ``` 56 | 57 | 这个嵌套的结构通常对应了一种非常实用的序列思考方式,我们在 Python 解释器的执行规则中已经见过它了。一个非空序列可以划分为: 58 | 59 | + 它的第一个元素,以及 60 | + 序列的其余部分。 61 | 62 | 序列的其余部分本身就是一个(可能为空的)序列。我们将序列的这种看法叫做递归,因为序列包含其它序列作为第二个组成部分。 63 | 64 | 由于我们的列表表示是递归的,我们在实现中叫它`rlist`,以便不会和 Python 内建的`list`类型混淆,我们会稍后在这一章介绍它。一个递归列表可以由第一个元素和列表的剩余部分构造。`None`值表示空的递归列表。 65 | 66 | ```py 67 | >>> empty_rlist = None 68 | >>> def make_rlist(first, rest): 69 | """Make a recursive list from its first element and the rest.""" 70 | return (first, rest) 71 | >>> def first(s): 72 | """Return the first element of a recursive list s.""" 73 | return s[0] 74 | >>> def rest(s): 75 | """Return the rest of the elements of a recursive list s.""" 76 | return s[1] 77 | ``` 78 | 79 | 这两个选择器和一个构造器,以及一个常量共同实现了抽象数据类型的递归列表。递归列表唯一的行为条件是,就像偶对那样,它的构造器和选择器是相反的函数。 80 | 81 | + 如果一个递归列表`s`由元素`f`和列表`r`构造,那么`first(s)`返回`f`,并且`rest(s)`返回`r`。 82 | 83 | 我们可以使用构造器和选择器来操作递归列表。 84 | 85 | ```py 86 | >>> counts = make_rlist(1, make_rlist(2, make_rlist(3, make_rlist(4, empty_rlist)))) 87 | >>> first(counts) 88 | 1 89 | >>> rest(counts) 90 | (2, (3, (4, None))) 91 | ``` 92 | 93 | 递归列表可以按序储存元素序列,但是它还没有实现序列的抽象。使用我们已经定义的数据类型抽象,我们就可以实现描述两个序列的行为:长度和元素选择。 94 | 95 | ```py 96 | >>> def len_rlist(s): 97 | """Return the length of recursive list s.""" 98 | length = 0 99 | while s != empty_rlist: 100 | s, length = rest(s), length + 1 101 | return length 102 | >>> def getitem_rlist(s, i): 103 | """Return the element at index i of recursive list s.""" 104 | while i > 0: 105 | s, i = rest(s), i - 1 106 | return first(s) 107 | ``` 108 | 109 | 现在,我们可以将递归列表用作序列了: 110 | 111 | ```py 112 | >>> len_rlist(counts) 113 | 4 114 | >>> getitem_rlist(counts, 1) # The second item has index 1 115 | 2 116 | ``` 117 | 118 | 两个实现都是可迭代的。它们隔离了嵌套偶对的每个层级,直到列表的末尾(在`len_rlist`中),或者到达了想要的元素(在`getitem_rlist`中)。 119 | 120 | 下面的一系列环境图示展示了迭代过程,`getitem_rlist`通过它找到了递归列表中下标`1`中的元素`2`。 121 | 122 | ![](img/getitem_rlist_0.png) 123 | 124 | `while`头部中的表达式求值为真,这会导致`while`语句组中的赋值语句被执行: 125 | 126 | ![](img/getitem_rlist_1.png) 127 | 128 | 这里,局部名称`s`现在指向以原列表第二个元素开始的子列表。现在,`while`头中的表达式求值为假,于是 Python 会求出`getitem_rlist`最后一行中返回语句中的表达式。 129 | 130 | ![](img/getitem_rlist_2.png) 131 | 132 | 最后的环境图示展示了调用`first`的局部帧,它包含绑定到相同子列表的`s`。`first`函数挑选出值`2`并返回了它,完成了`getitem_rlist`的调用。 133 | 134 | 这个例子演示了递归列表计算的常见模式,其中迭代的每一步都操作原列表的一个逐渐变短的后缀。寻找递归列表的长度和元素的渐进式处理过程需要一些时间来计算。(第三章中,我们会学会描述这种函数的计算时间。)Python 的内建序列类型以不同方式实现,它对于计算序列长度和获取元素并不具有大量的计算开销。 135 | 136 | ## 2.3.2 元组 II 137 | 138 | 实际上,我们引入用于形成原始偶对的`tuple`类型本身就是完整的序列类型。元组比起我们以函数式实现的偶对抽象数据结构,本质上提供了更多功能。 139 | 140 | 元组具有任意的长度,并且也拥有序列抽象的两个基本行为:长度和元素选择。下面的`digits`是一个四元素元组。 141 | 142 | ```py 143 | >>> digits = (1, 8, 2, 8) 144 | >>> len(digits) 145 | 4 146 | >>> digits[3] 147 | 8 148 | ``` 149 | 150 | 此外,元素可以彼此相加以及与整数相乘。对于元组,加法和乘法操作并不对元素相加或相乘,而是组合和重复元组本身。也就是说,`operator`模块中的`add`函数(以及`+`运算符)返回两个被加参数连接成的新元组。`operator`模块中的`mul`函数(以及`*`运算符)接受整数`k`和元组,并返回含有元组参数`k`个副本的新元组。 151 | 152 | ```py 153 | >>> (2, 7) + digits * 2 154 | (2, 7, 1, 8, 2, 8, 1, 8, 2, 8) 155 | ``` 156 | 157 | **映射。**将一个元组变换为另一个元组的强大手段是在每个元素上调用函数,并收集结果。这一计算的常用形式叫做在序列上映射函数,对应内建函数`map`。`map`的结果是一个本身不是序列的对象,但是可以通过调用`tuple`来转换为序列。它是元组的构造器。 158 | 159 | ```py 160 | >>> alternates = (-1, 2, -3, 4, -5) 161 | >>> tuple(map(abs, alternates)) 162 | (1, 2, 3, 4, 5) 163 | ``` 164 | 165 | `map`函数非常重要,因为它依赖于序列抽象:我们不需要关心底层元组的结构,只需要能够独立访问每个元素,以便将它作为参数传入用于映射的函数中(这里是`abs`)。 166 | 167 | ## 2.3.4 序列迭代 168 | 169 | 映射本身就是通用计算模式的一个实例:在序列中迭代所有元素。为了在序列上映射函数,我们不仅仅需要选择特定的元素,还要依次选择每个元素。这个模式非常普遍,Python 拥有额外的控制语句来处理序列数据:`for`语句。 170 | 171 | 考虑一个问题,计算一个值在序列中出现了多少次。我们可以使用`while`循环实现一个函数来计算这个数量。 172 | 173 | ```py 174 | >>> def count(s, value): 175 | """Count the number of occurrences of value in sequence s.""" 176 | total, index = 0, 0 177 | while index < len(s): 178 | if s[index] == value: 179 | total = total + 1 180 | index = index + 1 181 | return total 182 | >>> count(digits, 8) 183 | 2 184 | ``` 185 | 186 | Python `for`语句可以通过直接迭代元素值来简化这个函数体,完全不需要引入`index`。例如(原文是`For example`,为双关语),我们可以写成: 187 | 188 | ```py 189 | >>> def count(s, value): 190 | """Count the number of occurrences of value in sequence s.""" 191 | total = 0 192 | for elem in s: 193 | if elem == value: 194 | total = total + 1 195 | return total 196 | >>> count(digits, 8) 197 | 2 198 | ``` 199 | 200 | `for`语句按照以下过程来执行: 201 | 202 | 1. 求出头部表达式``,它必须产生一个可迭代的值。 203 | 2. 对于序列中的每个元素值,按顺序: 204 | 1. 在局部环境中将变量名``绑定到这个值上。 205 | 2. 执行语句组``。 206 | 207 | 步骤 1 引用了可迭代的值。序列是可迭代的,它们的元素可看做迭代的顺序。Python 的确拥有其他可迭代类型,但是我们现在只关注序列。术语“可迭代对象”的一般定义会在第四章的迭代器一节中出现。 208 | 209 | 这个求值过程的一个重要结果是,在`for`语句执行完毕之后,``会绑定到序列的最后一个元素上。这个`for`循环引入了另一种方式,其中局部环境可以由语句来更新。 210 | 211 | **序列解构。**程序中的一个常见模式是,序列的元素本身就是序列,但是具有固定的长度。`for`语句可在头部中包含多个名称,将每个元素序列“解构”为各个元素。例如,我们拥有一个偶对(也就是二元组)的序列: 212 | 213 | ```py 214 | >>> pairs = ((1, 2), (2, 2), (2, 3), (4, 4)) 215 | ``` 216 | 217 | 下面的`for`语句的头部带有两个名词,会将每个名称`x`和`y`分别绑定到每个偶对的第一个和第二个元素上。 218 | 219 | ```py 220 | >>> for x, y in pairs: 221 | if x == y: 222 | same_count = same_count + 1 223 | >>> same_count 224 | 2 225 | ``` 226 | 227 | 这个绑定多个名称到定长序列中多个值的模式,叫做序列解构。它的模式和我们在赋值语句中看到的,将多个名称绑定到多个值的模式相同。 228 | 229 | **范围。**`range`是另一种 Python 的内建序列类型,它表示一个整数范围。范围可以使用`range`函数来创建,它接受两个整数参数:所得范围的第一个数值和最后一个数值加一。 230 | 231 | ```py 232 | >>> range(1, 10) # Includes 1, but not 10 233 | range(1, 10) 234 | ``` 235 | 236 | 在范围上调用`tuple`构造器会创建与范围具有相同元素的元组,使元素易于查看。 237 | 238 | ```py 239 | >>> tuple(range(5, 8)) 240 | (5, 6, 7) 241 | ``` 242 | 243 | 如果只提供了一个元素,它会解释为最后一个数值加一,范围开始于 0。 244 | 245 | ```py 246 | >>> total = 0 247 | >>> for k in range(5, 8): 248 | total = total + k 249 | >>> total 250 | 18 251 | ``` 252 | 253 | 常见的惯例是将单下划线字符用于`for`头部,如果这个名称在语句组中不会使用。 254 | 255 | ```py 256 | >>> for _ in range(3): 257 | print('Go Bears!') 258 | 259 | Go Bears! 260 | Go Bears! 261 | Go Bears! 262 | ``` 263 | 264 | 要注意对解释器来说,下划线只是另一个名称,但是在程序员中具有固定含义,它表明这个名称不应出现在任何表达式中。 265 | 266 | ## 2.3.5 序列抽象 267 | 268 | 我们已经介绍了两种原生数据类型,它们实现了序列抽象:元组和范围。两个都满足这一章开始时的条件:长度和元素选择。Python 还包含了两种序列类型的行为,它们扩展了序列抽象。 269 | 270 | **成员性。**可以测试一个值在序列中的成员性。Python 拥有两个操作符`in`和`not in`,取决于元素是否在序列中出现而求值为`True`和`False`。 271 | 272 | ```py 273 | >>> digits 274 | (1, 8, 2, 8) 275 | >>> 2 in digits 276 | True 277 | >>> 1828 not in digits 278 | True 279 | ``` 280 | 281 | 所有序列都有叫做`index`和`count`的方法,它会返回序列中某个值的下标(或者数量)。 282 | 283 | **切片。**序列包含其中的子序列。我们在开发我们的嵌套偶对实现时观察到了这一点,它将序列切分为它的第一个元素和其余部分。序列的切片是原序列的任何部分,由一对整数指定。就像`range`构造器那样,第一个整数表示切片的起始下标,第二个表示结束下标加一。 284 | 285 | Python 中,序列切片的表示类似于元素选择,使用方括号。冒号分割了起始和结束下标。任何边界上的省略都被当作极限值:起始下标为 0,结束下标是序列长度。 286 | 287 | ```py 288 | >>> digits[0:2] 289 | (1, 8) 290 | >>> digits[1:] 291 | (8, 2, 8) 292 | ``` 293 | 294 | Python 序列抽象的这些额外行为的枚举,给我们了一个机会来反思数据抽象通常由什么构成。抽象的丰富性(也就是说它包含行为的多少)非常重要。对于使用抽象的用户,额外的行为很有帮助,另一方面,满足新类型抽象的丰富需求是个挑战。为了确保我们的递归列表实现支持这些额外的行为,需要一些工作量。另一个抽象丰富性的负面结果是,它们需要用户长时间学习。 295 | 296 | 序列拥有丰富的抽象,因为它们在计算中无处不在,所以学习一些复杂的行为是合理的。通常,多数用户定义的抽象应该尽可能简单。 297 | 298 | **扩展阅读。**切片符号接受很多特殊情况,例如负的起始值,结束值和步长。Dive Into Python 3 中有一节叫做[列表切片](http://diveintopython3.ep.io/native-datatypes.html#slicinglists),完整描述了它。这一章中,我们只会用到上面描述的基本特性。 299 | 300 | ## 2.3.6 字符串 301 | 302 | 文本值可能比数值对计算机科学来说更基本。作为一个例子,Python 程序以文本编写和储存。Python 中原生的文本数据类型叫做字符串,相应的构造器是`str`。 303 | 304 | 关于字符串在 Python 中如何表示和操作有许多细节。字符串是丰富抽象的另一个示例,程序员需要满足一些实质性要求来掌握。这一节是字符串基本行为的摘要。 305 | 306 | 字符串字面值可以表达任意文本,被单引号或者双引号包围。 307 | 308 | ```py 309 | >>> 'I am string!' 310 | 'I am string!' 311 | >>> "I've got an apostrophe" 312 | "I've got an apostrophe" 313 | >>> '您好' 314 | '您好' 315 | ``` 316 | 317 | 我们已经在代码中见过字符串了,在`print`的调用中作为文档字符串,以及在`assert`语句中作为错误信息。 318 | 319 | 字符串满足两个基本的序列条件,我们在这一节开始介绍过它们:它们拥有长度并且支持元素选择。 320 | 321 | ```py 322 | >>> city = 'Berkeley' 323 | >>> len(city) 324 | 8 325 | >>> city[3] 326 | 'k' 327 | ``` 328 | 329 | 字符串的元素本身就是包含单一字符的字符串。字符是字母表中的任意单一字符,标点符号,或者其它符号。不像许多其它编程语言那样,Python 没有单独的字符类型,任何文本都是字符串,表示单一字符的字符串长度为 1、 330 | 331 | 就像元组,字符串可以通过加法和乘法来组合: 332 | 333 | ```py 334 | >>> city = 'Berkeley' 335 | >>> len(city) 336 | 8 337 | >>> city[3] 338 | 'k' 339 | ``` 340 | 341 | 字符串的行为不同于 Python 中其它序列类型。字符串抽象没有实现我们为元组和范围描述的完整序列抽象。特别地,字符串上实现了成员性运算符`in`,但是与序列上的实现具有完全不同的行为。它匹配子字符串而不是元素。 342 | 343 | ```py 344 | >>> 'here' in "Where's Waldo?" 345 | True 346 | ``` 347 | 348 | 与之相似,字符串上的`count`和`index`方法接受子串作为参数,而不是单一字符。`count`的行为有细微差别,它统计字符串中非重叠字串的出现次数。 349 | 350 | ```py 351 | >>> 'Mississippi'.count('i') 352 | 4 353 | >>> 'Mississippi'.count('issi') 354 | 1 355 | ``` 356 | 357 | **多行文本。**字符串并不限制于单行文本,三个引号分隔的字符串字面值可以跨越多行。我们已经在文档字符串中使用了三个引号。 358 | 359 | ```py 360 | >>> """The Zen of Python 361 | claims, Readability counts. 362 | Read more: import this.""" 363 | 'The Zen of Python\nclaims, "Readability counts."\nRead more: import this.' 364 | ``` 365 | 366 | 在上面的打印结果中,`\n`(叫做“反斜杠加 n”)是表示新行的单一元素。虽然它表示为两个字符(反斜杠和 n)。它在长度和元素选择上被认为是单个字符。 367 | 368 | **字符串强制。**字符串可以从 Python 的任何对象通过以某个对象值作为参数调用`str`构造函数来创建,这个字符串的特性对于从多种类型的对象中构造描述性字符串非常实用。 369 | 370 | ```py 371 | >>> str(2) + ' is an element of ' + str(digits) 372 | '2 is an element of (1, 8, 2, 8)' 373 | ``` 374 | 375 | `str`函数可以以任何类型的参数调用,并返回合适的值,这个机制是后面的泛用函数的主题。 376 | 377 | **方法。**字符串在 Python 中的行为非常具有生产力,因为大量的方法都返回字符串的变体或者搜索其内容。一部分这些方法由下面的示例介绍。 378 | 379 | ```py 380 | >>> '1234'.isnumeric() 381 | True 382 | >>> 'rOBERT dE nIRO'.swapcase() 383 | 'Robert De Niro' 384 | >>> 'snakeyes'.upper().endswith('YES') 385 | True 386 | ``` 387 | 388 | **扩展阅读。**计算机中的文本编码是个复杂的话题。这一章中,我们会移走字符串如何表示的细节,但是,对许多应用来说,字符串如何由计算机编码的特定细节是必要的知识。[Dive Into Python 3 的 4.1 ~ 4.3 节](http://diveintopython3.ep.io/strings.html)提供了字符编码和 Unicode 的描述。 389 | 390 | ## 2.3.7 接口约定 391 | 392 | 在复合数据的处理中,我们强调了数据抽象如何让我们设计程序而不陷入数据表示的细节,以及抽象如何为我们保留灵活性来尝试备用表示。这一节中,我们引入了另一种强大的设计原则来处理数据结构 -- 接口约定的用法。 393 | 394 | 接口约定使在许多组件模块中共享的数据格式,它可以混合和匹配来展示数据。例如,如果我们拥有多个函数,它们全部接受序列作为参数并且返回序列值,我们就可以把它们每一个用于上一个的输出上,并选择任意一种顺序。这样,我们就可以通过将函数链接成流水线,来创建一个复杂的过程,每个函数都是简单而专一的。 395 | 396 | 这一节有两个目的,来介绍以接口约定组织程序的概念,以及展示模块化序列处理的示例。 397 | 398 | 考虑下面两个问题,它们首次出现,并且只和序列的使用相关。 399 | 400 | 1. 对前`n`个斐波那契数中的偶数求和。 401 | 2. 列出一个名称中的所有缩写字母,它包含每个大写单词的首字母。 402 | 403 | 这些问题是有关系的,因为它们可以解构为简单的操作,它们接受序列作为输入,并产出序列作为输出。而且,这些操作是序列上的计算的一般方法的实例。让我们思考第一个问题,它可以解构为下面的步骤: 404 | 405 | ``` 406 | enumerate map filter accumulate 407 | ----------- --- ------ ---------- 408 | naturals(n) fib iseven sum 409 | ``` 410 | 411 | 下面的`fib`函数计算了斐波那契数(现在使用了`for`语句更新了第一章中的定义)。 412 | 413 | ```py 414 | >>> def fib(k): 415 | """Compute the kth Fibonacci number.""" 416 | prev, curr = 1, 0 # curr is the first Fibonacci number. 417 | for _ in range(k - 1): 418 | prev, curr = curr, prev + curr 419 | return curr 420 | ``` 421 | 422 | 谓词`iseven`可以使用整数取余运算符`%`来定义。 423 | 424 | ```py 425 | >>> def iseven(n): 426 | return n % 2 == 0 427 | ``` 428 | 429 | `map`和`filter`函数是序列操作,我们已经见过了`map`,它在序列中的每个元素上调用函数并且收集结果。`filter`函数接受序列,并且返回序列中谓词为真的元素。两个函数都返回间接对象,`map`和`filter`对象,它们是可以转换为元组或求和的可迭代对象。 430 | 431 | ```py 432 | >>> nums = (5, 6, -7, -8, 9) 433 | >>> tuple(filter(iseven, nums)) 434 | (6, -8) 435 | >>> sum(map(abs, nums)) 436 | 35 437 | ``` 438 | 439 | 现在我们可以实现`even_fib`,第一个问题的解,使用`map`、`filter`和`sum`。 440 | 441 | ```py 442 | >>> def sum_even_fibs(n): 443 | """Sum the first n even Fibonacci numbers.""" 444 | return sum(filter(iseven, map(fib, range(1, n+1)))) 445 | >>> sum_even_fibs(20) 446 | 3382 447 | ``` 448 | 449 | 现在,让我们思考第二个问题。它可以解构为序列操作的流水线,包含`map`和`filter`。 450 | 451 | ``` 452 | enumerate filter map accumulate 453 | --------- ------ ----- ---------- 454 | words iscap first tuple 455 | ``` 456 | 457 | 字符串中的单词可以通过字符串对象上的`split`方法来枚举,默认以空格分割。 458 | 459 | ```py 460 | >>> tuple('Spaces between words'.split()) 461 | ('Spaces', 'between', 'words') 462 | ``` 463 | 464 | 单词的第一个字母可以使用选择运算符来获取,确定一个单词是否大写的谓词可以使用内建谓词`isupper`定义。 465 | 466 | ```py 467 | >>> def first(s): 468 | return s[0] 469 | >>> def iscap(s): 470 | return len(s) > 0 and s[0].isupper() 471 | ``` 472 | 473 | 这里,我们的缩写函数可以使用`map`和`filter`定义。 474 | 475 | ```py 476 | >>> def acronym(name): 477 | """Return a tuple of the letters that form the acronym for name.""" 478 | return tuple(map(first, filter(iscap, name.split()))) 479 | >>> acronym('University of California Berkeley Undergraduate Graphics Group') 480 | ('U', 'C', 'B', 'U', 'G', 'G') 481 | ``` 482 | 483 | 这些不同问题的相似解法展示了如何使用通用的计算模式,例如映射、过滤和累计,来组合序列的接口约定上的操作。序列抽象让我们编写出这些简明的解法。 484 | 485 | 将程序表达为序列操作有助于我们设计模块化的程序。也就是说,我们的设计由组合相关的独立片段构建,每个片段都对序列进行转换。通常,我们可以通过提供带有接口约定的标准组件库来鼓励模块化设计,接口约定以灵活的方式连接这些组件。 486 | 487 | **生成器表达式。**Python 语言包含第二个处理序列的途径,叫做生成器表达式。它提供了与`map`和`reduce`相似的功能,但是需要更少的函数定义。 488 | 489 | 生成器表达式组合了过滤和映射的概念,并集成于单一的表达式中,以下面的形式: 490 | 491 | ``` 492 | for in if 493 | ``` 494 | 495 | 为了求出生成器表达式,Python 先求出``,它必须返回一个可迭代值。之后,对于每个元素,按顺序将元素值绑定到``,求出过滤器表达式,如果它产生真值,就会求出映射表达式。 496 | 497 | 生成器表达式的求解结果值本身是个可迭代值。累计函数,比如`tuple`、`sum`、`max`和`min`可以将返回的对象作为参数。 498 | 499 | ```py 500 | >>> def acronym(name): 501 | return tuple(w[0] for w in name.split() if iscap(w)) 502 | >>> def sum_even_fibs(n): 503 | return sum(fib(k) for k in range(1, n+1) if fib(k) % 2 == 0) 504 | ``` 505 | 506 | 生成器表达式是使用可迭代(例如序列)接口约定的特化语法。这些表达式包含了`map`和`filter`的大部分功能,但是避免了被调用函数的实际创建(或者,顺便也避免了环境帧的创建需要调用这些函数)。 507 | 508 | **归约。**在我们的示例中,我们使用特定的函数来累计结果,例如`tuple`或者`sum`。函数式编程语言(包括 Python)包含通用的高阶累加器,具有多种名称。Python 在`functools`模块中包含`reduce`,它对序列中的元素从左到右依次调用二元函数,将序列归约为一个值。下面的表达式计算了五个因数的积。 509 | 510 | ```py 511 | >>> from operator import mul 512 | >>> from functools import reduce 513 | >>> reduce(mul, (1, 2, 3, 4, 5)) 514 | 120 515 | ``` 516 | 517 | 使用这个更普遍的累计形式,除了求和之外,我们也可以计算斐波那契数列中奇数的积,将序列用作接口约定。 518 | 519 | ```py 520 | >>> def product_even_fibs(n): 521 | """Return the product of the first n even Fibonacci numbers, except 0.""" 522 | return reduce(mul, filter(iseven, map(fib, range(2, n+1)))) 523 | >>> product_even_fibs(20) 524 | 123476336640 525 | ``` 526 | 527 | 与`map`、`filter`和`reduce`对应的高阶过程的组合会再一次在第四章出现,在我们思考多台计算机之间的分布式计算方法的时候。 528 | -------------------------------------------------------------------------------- /2.5.md: -------------------------------------------------------------------------------- 1 | # 2.5 面向对象编程 2 | 3 | > 来源:[2.5 Object-Oriented Programming](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/objects.html#object-oriented-programming) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 面向对象编程(OOP)是一种用于组织程序的方法,它组合了这一章引入的许多概念。就像抽象数据类型那样,对象创建了数据使用和实现之间的抽象界限。类似消息传递中的分发字典,对象响应行为请求。就像可变的数据结构,对象拥有局部状态,并且不能直接从全局环境访问。Python 对象系统提供了新的语法,更易于为组织程序实现所有这些实用的技巧。 10 | 11 | 但是对象系统不仅仅提供了便利;它也为程序设计添加了新的隐喻,其中程序中的几个部分彼此交互。每个对象将局部状态和行为绑定,以一种方式在数据抽象背后隐藏二者的复杂性。我们的约束程序的例子通过在约束和连接器之前传递消息,产生了这种隐喻。Python 对象系统使用新的途径扩展了这种隐喻,来表达程序的不同部分如何互相关联,以及互相通信。对象不仅仅会传递消息,还会和其它相同类型的对象共享行为,以及从相关的类型那里继承特性。 12 | 13 | 面向对象编程的范式使用自己的词汇来强化对象隐喻。我们已经看到了,对象是拥有方法和属性的数据值,可以通过点运算符来访问。每个对象都拥有一个类型,叫做类。Python 中可以定义新的类,就像定义函数那样。 14 | 15 | ## 2.5.1 对象和类 16 | 17 | 类可以用作所有类型为该类的对象的模板。每个对象都是某个特定类的实例。我们目前使用的对象都拥有内建类型,但是我们可以定义新的类,就像定义函数那样。类的定义规定了在该类的对象之间共享的属性和方法。我们会通过重新观察银行账户的例子,来介绍类的语句。 18 | 19 | 在介绍局部状态时,我们看到,银行账户可以自然地建模为拥有`balance`的可变值。银行账户对象应该拥有`withdraw`方法,在可用的情况下,它会更新账户余额,并返回所请求的金额。我们希望添加一些额外的行为来完善账户抽象:银行账户应该能够返回它的当前余额,返回账户持有者的名称,以及接受存款。 20 | 21 | `Account`类允许我们创建银行账户的多个实例。创建新对象实例的动作被称为实例化该类。Python 中实例化某个类的语法类似于函数的调用语句。这里,我们使用参数`'Jim'`(账户持有者的名称)来调用`Account`。 22 | 23 | ```py 24 | >>> a = Account('Jim') 25 | ``` 26 | 27 | 对象的属性是和对象关联的名值对,它可以通过点运算符来访问。属性特定于具体的对象,而不是类的所有对象,也叫做实例属性。每个`Account`对象都拥有自己的余额和账户持有者名称,它们是实例属性的一个例子。在更宽泛的编程社群中,实例属性可能也叫做字段、属性或者实例变量。 28 | 29 | ```py 30 | >>> a.holder 31 | 'Jim' 32 | >>> a.balance 33 | 0 34 | ``` 35 | 36 | 操作对象或执行对象特定计算的函数叫做方法。方法的副作用和返回值可能依赖或改变对象的其它属性。例如,`deposit`是`Account`对象`a`上的方法。它接受一个参数,即需要存入的金额,修改对象的`balance`属性,并返回产生的余额。 37 | 38 | ```py 39 | >>> a.deposit(15) 40 | 15 41 | ``` 42 | 43 | 在 OOP 中,我们说方法可以在特定对象上调用。作为调用`withdraw`方法的结果,要么取钱成功,余额减少并返回,要么请求被拒绝,账户打印出错误信息。 44 | 45 | ```py 46 | >>> a.withdraw(10) # The withdraw method returns the balance after withdrawal 47 | 5 48 | >>> a.balance # The balance attribute has changed 49 | 5 50 | >>> a.withdraw(10) 51 | 'Insufficient funds' 52 | ``` 53 | 54 | 像上面展示的那样,方法的行为取决于对象属性的改变。两次以相同参数对`withdraw`的调用返回了不同的结果。 55 | 56 | ## 2.5.2 类的定义 57 | 58 | 用户定义的类由`class`语句创建,它只包含单个子句。类的语句定义了类的名称和基类(会在继承那一节讨论),之后包含了定义类属性的语句组: 59 | 60 | 61 | ```py 62 | class (): 63 | 64 | ``` 65 | 66 | 当类的语句被执行时,新的类会被创建,并且在当前环境第一帧绑定到``上。之后会执行语句组。任何名称都会在`class`语句的``中绑定,通过`def`或赋值语句,创建或修改类的属性。 67 | 68 | 类通常围绕实例属性来组织,实例属性是名值对,不和类本身关联但和类的每个对象关联。通过为实例化新对象定义方法,类规定了它的对象的实例属性。 69 | 70 | `class`语句的``部分包含`def`语句,它们为该类的对象定义了新的方法。用于实例化对象的方法在 Python 中拥有特殊的名称,`__init__`(`init`两边分别有两个下划线),它叫做类的构造器。 71 | 72 | ```py 73 | >>> class Account(object): 74 | def __init__(self, account_holder): 75 | self.balance = 0 76 | self.holder = account_holder 77 | ``` 78 | 79 | `Account`的`__init__`方法有两个形参。第一个是`self`,绑定到新创建的`Account`对象上。第二个参数,`account_holder`,在被调用来实例化的时候,绑定到传给该类的参数上。 80 | 81 | 构造器将实例属性名称`balance`与`0`绑定。它也将属性名称`holder`绑定到`account_holder`上。形参`account_holder`是`__init__`方法的局部名称。另一方面,通过最后一个赋值语句绑定的名称`holder`是一直存在的,因为它使用点运算符被存储为`self`的属性。 82 | 83 | 定义了`Account`类之后,我们就可以实例化它: 84 | 85 | ```py 86 | >>> a = Account('Jim') 87 | ``` 88 | 89 | 这个对`Account`类的“调用”创建了新的对象,它是`Account`的实例,之后以两个参数调用了构造函数`__init__`:新创建的对象和字符串`'Jim'`。按照惯例,我们使用名称`self`来命名构造器的第一个参数,因为它绑定到了被实例化的对象上。这个惯例在几乎所有 Python 代码中都适用。 90 | 91 | 现在,我们可以使用点运算符来访问对象的`balance`和`holder`。 92 | 93 | ```py 94 | >>> a.balance 95 | 0 96 | >>> a.holder 97 | 'Jim' 98 | ``` 99 | 100 | **身份。**每个新的账户实例都有自己的余额属性,它的值独立于相同类的其它对象。 101 | 102 | ```py 103 | >>> b = Account('Jack') 104 | >>> b.balance = 200 105 | >>> [acc.balance for acc in (a, b)] 106 | [0, 200] 107 | ``` 108 | 109 | 为了强化这种隔离,每个用户定义类的实例对象都有个独特的身份。对象身份使用`is`和`is not`运算符来比较。 110 | 111 | ```py 112 | >>> a is a 113 | True 114 | >>> a is not b 115 | True 116 | ``` 117 | 118 | 虽然由同一个调用来构造,绑定到`a`和`b`的对象并不相同。通常,使用赋值将对象绑定到新名称并不会创建新的对象。 119 | 120 | ```py 121 | >>> c = a 122 | >>> c is a 123 | True 124 | ``` 125 | 126 | 用户定义类的新对象只在类(比如`Account`)使用调用表达式被实例化的时候创建。 127 | 128 | **方法。**对象方法也由`class`语句组中的`def`语句定义。下面,`deposit`和`withdraw`都被定义为`Account`类的对象上的方法: 129 | 130 | ```py 131 | >>> class Account(object): 132 | def __init__(self, account_holder): 133 | self.balance = 0 134 | self.holder = account_holder 135 | def deposit(self, amount): 136 | self.balance = self.balance + amount 137 | return self.balance 138 | def withdraw(self, amount): 139 | if amount > self.balance: 140 | return 'Insufficient funds' 141 | self.balance = self.balance - amount 142 | return self.balance 143 | ``` 144 | 145 | 虽然方法定义和函数定义在声明方式上并没有区别,方法定义有不同的效果。由`class`语句中的`def`语句创建的函数值绑定到了声明的名称上,但是只在类的局部绑定为一个属性。这个值可以使用点运算符在类的实例上作为方法来调用。 146 | 147 | 每个方法定义同样包含特殊的首个参数`self`,它绑定到方法所调用的对象上。例如,让我们假设`deposit`在特定的`Account`对象上调用,并且传递了一个对象值:要存入的金额。对象本身绑定到了`self`上,而参数绑定到了`amount`上。所有被调用的方法能够通过`self`参数来访问对象,所以它们可以访问并操作对象的状态。 148 | 149 | 为了调用这些方法,我们再次使用点运算符,就像下面这样: 150 | 151 | ```py 152 | >>> tom_account = Account('Tom') 153 | >>> tom_account.deposit(100) 154 | 100 155 | >>> tom_account.withdraw(90) 156 | 10 157 | >>> tom_account.withdraw(90) 158 | 'Insufficient funds' 159 | >>> tom_account.holder 160 | 'Tom' 161 | ``` 162 | 163 | 当一个方法通过点运算符调用时,对象本身(这个例子中绑定到了`tom_account`)起到了双重作用。首先,它决定了`withdraw`意味着哪个名称;`withdraw`并不是环境中的名称,而是`Account`类局部的名称。其次,当`withdraw`方法调用时,它绑定到了第一个参数`self`上。求解点运算符的详细过程会在下一节中展示。 164 | 165 | ## 2.5.3 消息传递和点表达式 166 | 167 | 方法定义在类中,而实例属性通常在构造器中赋值,二者都是面向对象编程的基本元素。这两个概念很大程度上类似于数据值的消息传递实现中的分发字典。对象使用点运算符接受消息,但是消息并不是任意的、值为字符串的键,而是类的局部名称。对象也拥有具名的局部状态值(实例属性),但是这个状态可以使用点运算符访问和操作,并不需要在实现中使用`nonlocal`语句。 168 | 169 | 消息传递的核心概念,就是数据值应该通过响应消息而拥有行为,这些消息和它们所表示的抽象类型相关。点运算符是 Python 的语法特征,它形成了消息传递的隐喻。使用带有内建对象系统语言的优点是,消息传递能够和其它语言特性,例如赋值语句无缝对接。我们并不需要不同的消息来“获取”和“设置”关联到局部属性名称的值;语言的语法允许我们直接使用消息名称。 170 | 171 | **点表达式。**类似`tom_account.deposit`的代码片段叫做点表达式。点表达式包含一个表达式,一个点和一个名称: 172 | 173 | ``` 174 | . 175 | ``` 176 | 177 | ``可为任意的 Python 有效表达式,但是``必须是个简单的名称(而不是求值为`name`的表达式)。点表达式会使用提供的``,对值为``的对象求出属性的值。 178 | 179 | 内建的函数`getattr`也会按名称返回对象的属性。它是等价于点运算符的函数。使用`getattr`,我们就能使用字符串来查找某个属性,就像分发字典那样: 180 | 181 | ```py 182 | >>> getattr(tom_account, 'balance') 183 | 10 184 | ``` 185 | 186 | 我们也可以使用`hasattr`测试对象是否拥有某个具名属性: 187 | 188 | ```py 189 | >>> hasattr(tom_account, 'deposit') 190 | True 191 | ``` 192 | 193 | 对象的属性包含所有实例属性,以及所有定义在类中的属性(包括方法)。方法是需要特别处理的类的属性。 194 | 195 | **方法和函数。**当一个方法在对象上调用时,对象隐式地作为第一个参数传递给方法。也就是说,点运算符左边值为``的对象,会自动传给点运算符右边的方法,作为第一个参数。所以,对象绑定到了参数`self`上。 196 | 197 | 为了自动实现`self`的绑定,Python 区分函数和绑定方法。我们已经在这门课的开始创建了前者,而后者在方法调用时将对象和函数组合到一起。绑定方法的值已经将第一个函数关联到所调用的实例,当方法调用时实例会被命名为`self`。 198 | 199 | 通过在点运算符的返回值上调用`type`,我们可以在交互式解释器中看到它们的差异。作为类的属性,方法只是个函数,但是作为实例属性,它是绑定方法: 200 | 201 | ```py 202 | >>> type(Account.deposit) 203 | 204 | >>> type(tom_account.deposit) 205 | 206 | ``` 207 | 208 | 这两个结果的唯一不同点是,前者是个标准的二元函数,带有参数`self`和`amount`。后者是一元方法,当方法被调用时,名称`self`自动绑定到了名为`tom_account`的对象上,而名称`amount`会被绑定到传递给方法的参数上。这两个值,无论函数值或绑定方法的值,都和相同的`deposit`函数体所关联。 209 | 210 | 我们可以以两种方式调用`deposit`:作为函数或作为绑定方法。在前者的例子中,我们必须为`self`参数显式提供实参。而对于后者,`self`参数已经自动绑定了。 211 | 212 | ```py 213 | >>> Account.deposit(tom_account, 1001) # The deposit function requires 2 arguments 214 | 1011 215 | >>> tom_account.deposit(1000) # The deposit method takes 1 argument 216 | 2011 217 | ``` 218 | 219 | 函数`getattr`的表现就像运算符那样:它的第一个参数是对象,而第二个参数(名称)是定义在类中的方法。之后,`getattr`返回绑定方法的值。另一方面,如果第一个参数是个类,`getattr`会直接返回属性值,它仅仅是个函数。 220 | 221 | **实践指南:命名惯例。**类名称通常以首字母大写来编写(也叫作驼峰拼写法,因为名称中间的大写字母像驼峰)。方法名称遵循函数命名的惯例,使用以下划线分隔的小写字母。 222 | 223 | 有的时候,有些实例变量和方法的维护和对象的一致性相关,我们不想让用户看到或使用它们。它们并不是由类定义的一部分抽象,而是一部分实现。Python 的惯例规定,如果属性名称以下划线开始,它只能在方法或类中访问,而不能被类的用户访问。 224 | 225 | ## 2.5.4 类属性 226 | 227 | 有些属性值在特定类的所有对象之间共享。这样的属性关联到类本身,而不是类的任何独立实例。例如,让我们假设银行以固定的利率对余额支付利息。这个利率可能会改变,但是它是在所有账户中共享的单一值。 228 | 229 | 类属性由`class`语句组中的赋值语句创建,位于任何方法定义之外。在更宽泛的开发者社群中,类属性也被叫做类变量或静态变量。下面的类语句以名称`interest`为`Account`创建了类属性。 230 | 231 | ```py 232 | >>> class Account(object): 233 | interest = 0.02 # A class attribute 234 | def __init__(self, account_holder): 235 | self.balance = 0 236 | self.holder = account_holder 237 | # Additional methods would be defined here 238 | ``` 239 | 240 | 这个属性仍旧可以通过类的任何实例来访问。 241 | 242 | ```py 243 | >>> tom_account = Account('Tom') 244 | >>> jim_account = Account('Jim') 245 | >>> tom_account.interest 246 | 0.02 247 | >>> jim_account.interest 248 | 0.02 249 | ``` 250 | 251 | 但是,对类属性的单一赋值语句会改变所有该类实例上的属性值。 252 | 253 | ```py 254 | >>> Account.interest = 0.04 255 | >>> tom_account.interest 256 | 0.04 257 | >>> jim_account.interest 258 | 0.04 259 | ``` 260 | 261 | **属性名称。**我们已经在我们的对象系统中引入了足够的复杂性,我们需要规定名称如何解析为特定的属性。毕竟,我们可以轻易拥有同名的类属性和实例属性。 262 | 263 | 像我们看到的那样,点运算符由表达式、点和名称组成: 264 | 265 | ``` 266 | . 267 | ``` 268 | 269 | 为了求解点表达式: 270 | 271 | 1. 求出点左边的``,会产生点运算符的对象。 272 | 2. ``会和对象的实例属性匹配;如果该名称的属性存在,会返回它的值。 273 | 3. 如果``不存在于实例属性,那么会在类中查找``,这会产生类的属性值。 274 | 4. 这个值会被返回,如果它是个函数,则会返回绑定方法。 275 | 276 | 在这个求值过程中,实例属性在类的属性之前查找,就像局部名称具有高于全局的优先级。定义在类中的方法,在求值过程的第三步绑定到了点运算符的对象上。在类中查找名称的过程有额外的差异,在我们引入类继承的时候就会出现。 277 | 278 | **赋值。**所有包含点运算符的赋值语句都会作用于右边的对象。如果对象是个实例,那么赋值就会设置实例属性。如果对象是个类,那么赋值会设置类属性。作为这条规则的结果,对对象属性的赋值不能影响类的属性。下面的例子展示了这个区别。 279 | 280 | 如果我们向账户实例的具名属性`interest`赋值,我们会创建属性的新实例,它和现有的类属性具有相同名称。 281 | 282 | ```py 283 | >>> jim_account.interest = 0.08 284 | ``` 285 | 286 | 这个属性值会通过点运算符返回: 287 | 288 | ```py 289 | >>> jim_account.interest 290 | 0.08 291 | ``` 292 | 293 | 但是,类属性`interest`会保持为原始值,它可以通过所有其他账户返回。 294 | 295 | ```py 296 | >>> tom_account.interest 297 | 0.04 298 | ``` 299 | 300 | 类属性`interest`的改动会影响`tom_account`,但是`jim_account`的实例属性不受影响。 301 | 302 | ```py 303 | >>> Account.interest = 0.05 # changing the class attribute 304 | >>> tom_account.interest # changes instances without like-named instance attributes 305 | 0.05 306 | >>> jim_account.interest # but the existing instance attribute is unaffected 307 | 0.08 308 | ``` 309 | 310 | ## 2.5.5 继承 311 | 312 | 在使用 OOP 范式时,我们通常会发现,不同的抽象数据结构是相关的。特别是,我们发现相似的类在特化的程度上有区别。两个类可能拥有相似的属性,但是一个表示另一个的特殊情况。 313 | 314 | 例如,我们可能希望实现一个活期账户,它不同于标准的账户。活期账户对每笔取款都收取额外的 $1,并且具有较低的利率。这里,我们演示上述行为: 315 | 316 | ```py 317 | >>> ch = CheckingAccount('Tom') 318 | >>> ch.interest # Lower interest rate for checking accounts 319 | 0.01 320 | >>> ch.deposit(20) # Deposits are the same 321 | 20 322 | >>> ch.withdraw(5) # withdrawals decrease balance by an extra charge 323 | 14 324 | ``` 325 | 326 | `CheckingAccount`是`Account`的特化。在 OOP 的术语中,通用的账户会作为`CheckingAccount`的基类,而`CheckingAccount`是`Account`的子类(术语“父类”和“超类”通常等同于“基类”,而“派生类”通常等同于“子类”)。 327 | 328 | 子类继承了基类的属性,但是可能覆盖特定属性,包括特定的方法。使用继承,我们只需要关注基类和子类之间有什么不同。任何我们在子类未指定的东西会自动假设和基类中相同。 329 | 330 | 继承也在对象隐喻中有重要作用,不仅仅是一种实用的组织方式。继承意味着在类之间表达“is-a”关系,它和“has-a”关系相反。活期账户是(is-a)一种特殊类型的账户,所以让`CheckingAccount`继承`Account`是继承的合理使用。另一方面,银行拥有(has-a)所管理的银行账户的列表,所以二者都不应继承另一个。反之,账户对象的列表应该自然地表现为银行账户的实例属性。 331 | 332 | ## 2.5.6 使用继承 333 | 334 | 我们通过将基类放置到类名称后面的圆括号内来指定继承。首先,我们提供`Account`类的完整实现,也包含类和方法的文档字符串。 335 | 336 | ```py 337 | >>> class Account(object): 338 | """A bank account that has a non-negative balance.""" 339 | interest = 0.02 340 | def __init__(self, account_holder): 341 | self.balance = 0 342 | self.holder = account_holder 343 | def deposit(self, amount): 344 | """Increase the account balance by amount and return the new balance.""" 345 | self.balance = self.balance + amount 346 | return self.balance 347 | def withdraw(self, amount): 348 | """Decrease the account balance by amount and return the new balance.""" 349 | if amount > self.balance: 350 | return 'Insufficient funds' 351 | self.balance = self.balance - amount 352 | return self.balance 353 | ``` 354 | 355 | `CheckingAccount`的完整实现在下面: 356 | 357 | ```py 358 | >>> class CheckingAccount(Account): 359 | """A bank account that charges for withdrawals.""" 360 | withdraw_charge = 1 361 | interest = 0.01 362 | def withdraw(self, amount): 363 | return Account.withdraw(self, amount + self.withdraw_charge) 364 | ``` 365 | 366 | 这里,我们引入了类属性`withdraw_charge`,它特定于`CheckingAccount`类。我们将一个更低的值赋给`interest`属性。我们也定义了新的`withdraw`方法来覆盖定义在`Account`对象中的行为。类语句组中没有更多的语句,所有其它行为都从基类`Account`中继承。 367 | 368 | ```py 369 | >>> checking = CheckingAccount('Sam') 370 | >>> checking.deposit(10) 371 | 10 372 | >>> checking.withdraw(5) 373 | 4 374 | >>> checking.interest 375 | 0.01 376 | ``` 377 | 378 | `checking.deposit`表达式是用于存款的绑定方法,它定义在`Account`类中,当 Python 解析点表达式中的名称时,实例上并没有这个属性,它会在类中查找该名称。实际上,在类中“查找名称”的行为会在原始对象的类的继承链中的每个基类中查找。我们可以递归定义这个过程,为了在类中查找名称: 379 | 380 | 1. 如果类中有带有这个名称的属性,返回属性值。 381 | 2. 否则,如果有基类的话,在基类中查找该名称。 382 | 383 | 在`deposit`中,Python 会首先在实例中查找名称,之后在`CheckingAccount`类中。最后,它会在`Account`中查找,这里是`deposit`定义的地方。根据我们对点运算符的求值规则,由于`deposit`是在`checking`实例的类中查找到的函数,点运算符求值为绑定方法。这个方法以参数`10`调用,这会以绑定到`checking`对象的`self`和绑定到`10`的`amount`调用`deposit`方法。 384 | 385 | 对象的类会始终保持不变。即使`deposit`方法在`Account`类中找到,`deposit`以绑定到`CheckingAccount`实例的`self`调用,而不是`Account`的实例。 386 | 387 | > 译者注:`CheckingAccount`的实例也是`Account`的实例,这个说法是有问题的。 388 | 389 | **调用祖先。**被覆盖的属性仍然可以通过类对象来访问。例如,我们可以通过以包含`withdraw_charge`的参数调用`Account`的`withdraw`方法,来实现`CheckingAccount`的`withdraw`方法。 390 | 391 | 要注意我们调用`self.withdraw_charge`而不是等价的`CheckingAccount.withdraw_charge`。前者的好处就是继承自`CheckingAccount`的类可能会覆盖支取费用。如果是这样的话,我们希望我们的`withdraw`实现使用新的值而不是旧的值。 392 | 393 | ## 2.5.7 多重继承 394 | 395 | Python 支持子类从多个基类继承属性的概念,这是一种叫做多重继承的语言特性。 396 | 397 | 假设我们从`Account`继承了`SavingsAccount`,每次存钱的时候向客户收取一笔小费用。 398 | 399 | ```py 400 | >>> class SavingsAccount(Account): 401 | deposit_charge = 2 402 | def deposit(self, amount): 403 | return Account.deposit(self, amount - self.deposit_charge) 404 | ``` 405 | 406 | 之后,一个聪明的总经理设想了`AsSeenOnTVAccount`,它拥有`CheckingAccount`和`SavingsAccount`的最佳特性:支取和存入的费用,以及较低的利率。它将储蓄账户和活期存款账户合二为一!“如果我们构建了它”,总经理解释道,“一些人会注册并支付所有这些费用。甚至我们会给他们一美元。” 407 | 408 | ```py 409 | >>> class AsSeenOnTVAccount(CheckingAccount, SavingsAccount): 410 | def __init__(self, account_holder): 411 | self.holder = account_holder 412 | self.balance = 1 # A free dollar! 413 | ``` 414 | 415 | 实际上,这个实现就完整了。存款和取款都需要费用,使用了定义在`CheckingAccount`和`SavingsAccount`中的相应函数。 416 | 417 | ```py 418 | >>> such_a_deal = AsSeenOnTVAccount("John") 419 | >>> such_a_deal.balance 420 | 1 421 | >>> such_a_deal.deposit(20) # $2 fee from SavingsAccount.deposit 422 | 19 423 | >>> such_a_deal.withdraw(5) # $1 fee from CheckingAccount.withdraw 424 | 13 425 | ``` 426 | 427 | 就像预期那样,没有歧义的引用会正确解析: 428 | 429 | ```py 430 | >>> such_a_deal.deposit_charge 431 | 2 432 | >>> such_a_deal.withdraw_charge 433 | 1 434 | ``` 435 | 436 | 但是如果引用有歧义呢,比如`withdraw`方法的引用,它定义在`Account`和`CheckingAccount`中?下面的图展示了`AsSeenOnTVAccount`类的继承图。每个箭头都从子类指向基类。 437 | 438 | ![](img/multiple_inheritance.png) 439 | 440 | 对于像这样的简单“菱形”,Python 从左到右解析名称,之后向上。这个例子中,Python 按下列顺序检查名称,直到找到了具有该名称的属性: 441 | 442 | ``` 443 | AsSeenOnTVAccount, CheckingAccount, SavingsAccount, Account, object 444 | ``` 445 | 446 | 继承顺序的问题没有正确的解法,因为我们可能会给某个派生类高于其他类的优先级。但是,任何支持多重继承的编程语言必须始终选择同一个顺序,便于语言的用户预测程序的行为。 447 | 448 | **扩展阅读。**Python 使用一种叫做 C3 Method Resolution Ordering 的递归算法来解析名称。任何类的方法解析顺序都使用所有类上的`mro`方法来查询。 449 | 450 | ```py 451 | >>> [c.__name__ for c in AsSeenOnTVAccount.mro()] 452 | ['AsSeenOnTVAccount', 'CheckingAccount', 'SavingsAccount', 'Account', 'object'] 453 | ``` 454 | 455 | 这个用于查询方法解析顺序的算法并不是这门课的主题,但是 Python 的原作者使用一篇[原文章的引用](http://python-history.blogspot.com/2010/06/method-resolution-order.html)来描述它。 456 | 457 | ## 2.5.8 对象的作用 458 | 459 | Python 对象系统为使数据抽象和消息传递更加便捷和灵活而设计。类、方法、继承和点运算符的特化语法都可以让我们在程序中形成对象隐喻,它能够提升我们组织大型程序的能力。 460 | 461 | 特别是,我们希望我们的对象系统在不同层面上促进关注分离。每个程序中的对象都封装和管理程序状态的一部分,每个类语句都定义了一些函数,它们实现了程序总体逻辑的一部分。抽象界限强制了大型程序不同层面之间的边界。 462 | 463 | 面向对象编程适合于对系统建模,这些系统拥有相互分离并交互的部分。例如,不同用户在社交网络中互动,不同角色在游戏中互动,以及不同图形在物理模拟中互动。在表现这种系统的时候,程序中的对象通常自然地映射为被建模系统中的对象,类用于表现它们的类型和关系。 464 | 465 | 另一方面,类可能不会提供用于实现特定的抽象的最佳机制。函数式抽象提供了更加自然的隐喻,用于表现输入和输出的关系。一个人不应该强迫自己把程序中的每个细微的逻辑都塞到类里面,尤其是当定义独立函数来操作数据变得十分自然的时候。函数也强制了关注分离。 466 | 467 | 类似 Python 的多范式语言允许程序员为合适的问题匹配合适的范式。为了简化程序,或使程序模块化,确定何时引入新的类,而不是新的函数,是软件工程中的重要设计技巧,这需要仔细关注。 468 | -------------------------------------------------------------------------------- /2.6.md: -------------------------------------------------------------------------------- 1 | # 2.6 实现类和对象 2 | 3 | > 来源:[2.6 Implementing Classes and Objects](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/objects.html#implementing-classes-and-objects) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 在使用面向对象编程范式时,我们使用对象隐喻来指导程序的组织。数据表示和操作的大部分逻辑都表达在类的定义中。在这一节中,我们会看到,类和对象本身可以使用函数和字典来表示。以这种方式实现对象系统的目的是展示使用对象隐喻并不需要特殊的编程语言。即使编程语言没有面向对象系统,程序照样可以面向对象。 10 | 11 | 为了实现对象,我们需要抛弃点运算符(它需要语言的内建支持),并创建分发字典,它的行为和内建对象系统的元素差不多。我们已经看到如何通过分发字典实现消息传递行为。为了完整实现对象系统,我们需要在实例、类和基类之间发送消息,它们全部都是含有属性的字典。 12 | 13 | 我们不会实现整个 Python 对象系统,它包含这篇文章没有涉及到的特性(比如元类和静态方法)。我们会专注于用户定义的类,不带有多重继承和内省行为(比如返回实例的类)。我们的实现并不遵循 Python 类型系统的明确规定。反之,它为实现对象隐喻的核心功能而设计。 14 | 15 | ## 2.6.1 实例 16 | 17 | 我们从实例开始。实例拥有具名属性,例如账户余额,它可以被设置或获取。我们使用分发字典来实现实例,它会响应“get”和“set”属性值消息。属性本身保存在叫做`attributes`的局部字典中。 18 | 19 | 就像我们在这一章的前面看到的那样,字典本身是抽象数据类型。我们使用列表来实现字典,我们使用偶对来实现列表,并且我们使用函数来实现偶对。就像我们以字典实现对象系统那样,要注意我们能够仅仅使用函数来实现对象。 20 | 21 | 为了开始我们的实现,我们假设我们拥有一个类实现,它可以查找任何不是实例部分的名称。我们将类作为参数`cls`传递给`make_instance`。 22 | 23 | ```py 24 | >>> def make_instance(cls): 25 | """Return a new object instance, which is a dispatch dictionary.""" 26 | def get_value(name): 27 | if name in attributes: 28 | return attributes[name] 29 | else: 30 | value = cls['get'](name) 31 | return bind_method(value, instance) 32 | def set_value(name, value): 33 | attributes[name] = value 34 | attributes = {} 35 | instance = {'get': get_value, 'set': set_value} 36 | return instance 37 | ``` 38 | 39 | `instance`是分发字典,它响应消息`get`和`set`。`set`消息对应 Python 对象系统的属性赋值:所有赋值的属性都直接储存在对象的局部属性字典中。在`get`中,如果`name`在局部`attributes`字典中不存在,那么它会在类中寻找。如果`cls`返回的`value`为函数,它必须绑定到实例上。 40 | 41 | **绑定方法值。**`make_instance`中的`get_value `使用`get`寻找类中的具名属性,之后调用`bind_method`。方法的绑定只在函数值上调用,并且它会通过将实例插入为第一个参数,从函数值创建绑定方法的值。 42 | 43 | ```py 44 | >>> def bind_method(value, instance): 45 | """Return a bound method if value is callable, or value otherwise.""" 46 | if callable(value): 47 | def method(*args): 48 | return value(instance, *args) 49 | return method 50 | else: 51 | return value 52 | ``` 53 | 54 | 当方法被调用时,第一个参数`self`通过这个定义绑定到了`instance`的值上。 55 | 56 | ## 2.6.2 类 57 | 58 | 类也是对象,在 Python 对象系统和我们这里实现的系统中都是如此。为了简化,我们假设类自己并没有类(在 Python 中,类本身也有类,几乎所有类都共享相同的类,叫做`type`)。类可以接受`get`和`set`消息,以及`new`消息。 59 | 60 | ```py 61 | >>> def make_class(attributes, base_class=None): 62 | """Return a new class, which is a dispatch dictionary.""" 63 | def get_value(name): 64 | if name in attributes: 65 | return attributes[name] 66 | elif base_class is not None: 67 | return base_class['get'](name) 68 | def set_value(name, value): 69 | attributes[name] = value 70 | def new(*args): 71 | return init_instance(cls, *args) 72 | cls = {'get': get_value, 'set': set_value, 'new': new} 73 | return cls 74 | ``` 75 | 76 | 不像实例那样,类的`get`函数在属性未找到的时候并不查询它的类,而是查询它的`base_class`。类并不需要方法绑定。 77 | 78 | **实例化。**`make_class `中的`new`函数调用了`init_instance`,它首先创建新的实例,之后调用叫做`__init__`的方法。 79 | 80 | ```py 81 | >>> def init_instance(cls, *args): 82 | """Return a new object with type cls, initialized with args.""" 83 | instance = make_instance(cls) 84 | init = cls['get']('__init__') 85 | if init: 86 | init(instance, *args) 87 | return instance 88 | ``` 89 | 90 | 最后这个函数完成了我们的对象系统。我们现在拥有了实例,它的`set`是局部的,但是`get`会回溯到它们的类中。实例在它的类中查找名称之后,它会将自己绑定到函数值上来创建方法。最后类可以创建新的(`new`)实例,并且在实例创建之后立即调用它们的`__init__`构造器。 91 | 92 | 在对象系统中,用户仅仅可以调用`create_class`,所有其他功能通过消息传递来使用。与之相似,Python 的对象系统由`class`语句来调用,它的所有其他功能都通过点表达式和对类的调用来使用。 93 | 94 | ## 2.6.3 使用所实现的对象 95 | 96 | 我们现在回到上一节银行账户的例子。使用我们实现的对象系统,我们就可以创建`Account`类,`CheckingAccount`子类和它们的实例。 97 | 98 | `Account`类通过`create_account_class `函数创建,它拥有类似于 Python `class`语句的结构,但是以`make_class`的调用结尾。 99 | 100 | ```py 101 | >>> def make_account_class(): 102 | """Return the Account class, which has deposit and withdraw methods.""" 103 | def __init__(self, account_holder): 104 | self['set']('holder', account_holder) 105 | self['set']('balance', 0) 106 | def deposit(self, amount): 107 | """Increase the account balance by amount and return the new balance.""" 108 | new_balance = self['get']('balance') + amount 109 | self['set']('balance', new_balance) 110 | return self['get']('balance') 111 | def withdraw(self, amount): 112 | """Decrease the account balance by amount and return the new balance.""" 113 | balance = self['get']('balance') 114 | if amount > balance: 115 | return 'Insufficient funds' 116 | self['set']('balance', balance - amount) 117 | return self['get']('balance') 118 | return make_class({'__init__': __init__, 119 | 'deposit': deposit, 120 | 'withdraw': withdraw, 121 | 'interest': 0.02}) 122 | ``` 123 | 124 | 在这个函数中,属性名称在最后设置。不像 Python 的`class`语句,它强制内部函数和属性名称之间的一致性。这里我们必须手动指定属性名称和值的对应关系。 125 | 126 | `Account`类最终由赋值来实例化。 127 | 128 | ```py 129 | >>> Account = make_account_class() 130 | ``` 131 | 132 | 之后,账户实例通过`new`消息来创建,它需要名称来处理新创建的账户。 133 | 134 | ```py 135 | >>> jim_acct = Account['new']('Jim') 136 | ``` 137 | 138 | 之后,`get`消息传递给`jim_acct `,来获取属性和方法。方法可以调用来更新账户余额。 139 | 140 | ```py 141 | >>> jim_acct['get']('holder') 142 | 'Jim' 143 | >>> jim_acct['get']('interest') 144 | 0.02 145 | >>> jim_acct['get']('deposit')(20) 146 | 20 147 | >>> jim_acct['get']('withdraw')(5) 148 | 15 149 | ``` 150 | 151 | 就像使用 Python 对象系统那样,设置实例的属性并不会修改类的对应属性: 152 | 153 | ```py 154 | >>> jim_acct['set']('interest', 0.04) 155 | >>> Account['get']('interest') 156 | 0.02 157 | ``` 158 | 159 | **继承。**我们可以创建`CheckingAccount`子类,通过覆盖类属性的子集。在这里,我们修改`withdraw`方法来收取费用,并且降低了利率。 160 | 161 | ```py 162 | >>> def make_checking_account_class(): 163 | """Return the CheckingAccount class, which imposes a $1 withdrawal fee.""" 164 | def withdraw(self, amount): 165 | return Account['get']('withdraw')(self, amount + 1) 166 | return make_class({'withdraw': withdraw, 'interest': 0.01}, Account) 167 | ``` 168 | 169 | 在这个实现中,我们在子类的`withdraw `中调用了基类`Account`的`withdraw`函数,就像在 Python 内建对象系统那样。我们可以创建子类本身和它的实例,就像之前那样: 170 | 171 | ```py 172 | >>> CheckingAccount = make_checking_account_class() 173 | >>> jack_acct = CheckingAccount['new']('Jack') 174 | ``` 175 | 176 | 它们的行为相似,构造函数也一样。每笔取款都会在特殊的`withdraw`函数中收费 $1,并且`interest`也拥有新的较低值。 177 | 178 | ```py 179 | >>> jack_acct['get']('interest') 180 | 0.01 181 | >>> jack_acct['get']('deposit')(20) 182 | 20 183 | >>> jack_acct['get']('withdraw')(5) 184 | 14 185 | ``` 186 | 187 | 我们的构建在字典上的对象系统十分类似于 Python 内建对象系统的实现。Python 中,任何用户定义类的实例,都有个特殊的`__dict__`属性,将对象的局部实例属性储存在字典中,就像我们的`attributes`字典那样。Python 的区别在于,它区分特定的特殊方法,这些方法和内建函数交互来确保那些函数能正常处理许多不同类型的参数。操作不同类型参数的函数是下一节的主题。 188 | -------------------------------------------------------------------------------- /2.7.md: -------------------------------------------------------------------------------- 1 | # 2.7 泛用方法 2 | 3 | > 来源:[2.7 Generic Operations](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/objects.html#generic-operations) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 这一章中我们引入了复合数据类型,以及由构造器和选择器实现的数据抽象机制。使用消息传递,我们就能使抽象数据类型直接拥有行为。使用对象隐喻,我们可以将数据的表示和用于操作数据的方法绑定在一起,从而使数据驱动的程序模块化,并带有局部状态。 10 | 11 | 但是,我们仍然必须展示,我们的对象系统允许我们在大型程序中灵活组合不同类型的对象。点运算符的消息传递仅仅是一种用于使用多个对象构建组合表达式的方式。这一节中,我们会探索一些用于组合和操作不同类型对象的方式。 12 | 13 | ## 2.7.1 字符串转换 14 | 15 | 我们在这一章最开始说,对象值的行为应该类似它所表达的数据,包括产生它自己的字符串表示。数据值的字符串表示在类似 Python 的交互式语言中尤其重要,其中“读取-求值-打印”的循环需要每个值都拥有某种字符串表示形式。 16 | 17 | 字符串值为人们的信息交流提供了基础的媒介。字符序列可以在屏幕上渲染,打印到纸上,大声朗读,转换为盲文,或者以莫尔兹码广播。字符串对编程而言也非常基础,因为它们可以表示 Python 表达式。对于一个对象,我们可能希望生成一个字符串,当作为 Python 表达式解释时,求值为等价的对象。 18 | 19 | Python 规定,所有对象都应该能够产生两种不同的字符串表示:一种是人类可解释的文本,另一种是 Python 可解释的表达式。字符串的构造函数`str`返回人类可读的字符串。在可能的情况下,`repr`函数返回一个 Python 表达式,它可以求值为等价的对象。`repr`的文档字符串解释了这个特性: 20 | 21 | ``` 22 | repr(object) -> string 23 | 24 | Return the canonical string representation of the object. 25 | For most object types, eval(repr(object)) == object. 26 | ``` 27 | 28 | 在表达式的值上调用`repr`的结果就是 Python 在交互式会话中打印的东西。 29 | 30 | ```py 31 | >>> 12e12 32 | 12000000000000.0 33 | >>> print(repr(12e12)) 34 | 12000000000000.0 35 | ``` 36 | 37 | 在不存在任何可以求值为原始值的表达式的情况中,Python 会产生一个代理: 38 | 39 | ```py 40 | >>> repr(min) 41 | '' 42 | ``` 43 | 44 | `str`构造器通常与`repr`相同,但是有时会提供更加可解释的文本表示。例如,我们可以看到`str`和`repr`对于日期的不同: 45 | 46 | ```py 47 | >>> from datetime import date 48 | >>> today = date(2011, 9, 12) 49 | >>> repr(today) 50 | 'datetime.date(2011, 9, 12)' 51 | >>> str(today) 52 | '2011-09-12' 53 | ``` 54 | 55 | `repr`函数的定义出现了新的挑战:我们希望它对所有数据类型都正确应用,甚至是那些在`repr`实现时还不存在的类型。我们希望它像一个多态函数,可以作用于许多(多)不同形式(态)的数据。 56 | 57 | 消息传递提供了这个问题的解决方案:`repr`函数在参数上调用叫做`__repr__`的函数。 58 | 59 | ```py 60 | >>> today.__repr__() 61 | 'datetime.date(2011, 9, 12)' 62 | ``` 63 | 64 | 通过在用户定义的类上实现同一方法,我们就可以将`repr`的适用性扩展到任何我们以后创建的类。这个例子强调了消息传递的另一个普遍的好处:就是它提供了一种机制,用于将现有函数的职责范围扩展到新的对象。 65 | 66 | `str`构造器以类似的方式实现:它在参数上调用了叫做`__str__`的方法。 67 | 68 | ```py 69 | >>> today.__str__() 70 | '2011-09-12' 71 | ``` 72 | 73 | 这些多态函数是一个更普遍原则的例子:特定函数应该作用于多种数据类型。这里举例的消息传递方法仅仅是多态函数实现家族的一员。本节剩下的部分会探索一些备选方案。 74 | 75 | ## 2.7.2 多重表示 76 | 77 | 使用对象或函数的数据抽象是用于管理复杂性的强大工具。抽象数据类型允许我们在数据表示和用于操作数据的函数之间构造界限。但是,在大型程序中,对于程序中的某种数据类型,提及“底层表示”可能不总是有意义。首先,一个数据对象可能有多种实用的表示,而且我们可能希望设计能够处理多重表示的系统。 78 | 79 | 为了选取一个简单的示例,复数可以用两种几乎等价的方式来表示:直角坐标(虚部和实部)以及极坐标(模和角度)。有时直角坐标形式更加合适,而有时极坐标形式更加合适。复数以两种方式表示,而操作复数的函数可以处理每种表示,这样一个系统确实比较合理。 80 | 81 | 更重要的是,大型软件系统工程通常由许多人设计,并花费大量时间,需求的主题随时间而改变。在这样的环境中,每个人都事先同意数据表示的方案是不可能的。除了隔离使用和表示的数据抽象的界限,我们需要隔离不同设计方案的界限,以及允许不同方案在一个程序中共存。进一步,由于大型程序通常通过组合已存在的模块创建,这些模块会单独设计,我们需要一种惯例,让程序员将模块递增地组合为大型系统。也就是说,不需要重复设计或实现这些模块。 82 | 83 | 我们以最简单的复数示例开始。我们会看到,消息传递在维持“复数”对象的抽象概念时,如何让我们为复数的表示设计出分离的直角坐标和极坐标表示。我们会通过使用泛用选择器为复数定义算数函数(`add_complex`,`mul_complex`)来完成它。泛用选择器可访问复数的一部分,独立于数值表示的方式。所产生的复数系统包含两种不同类型的抽象界限。它们隔离了高阶操作和低阶表示。此外,也有一个垂直的界限,它使我们能够独立设计替代的表示。 84 | 85 | ![](img/interface.png) 86 | 87 | 作为边注,我们正在开发一个系统,它在复数上执行算数运算,作为一个简单但不现实的使用泛用操作的例子。[复数类型](http://docs.python.org/py3k/library/stdtypes.html#typesnumeric)实际上在 Python 中已经内建了,但是这个例子中我们仍然自己实现。 88 | 89 | 就像有理数那样,复数可以自然表示为偶对。复数集可以看做带有两个正交轴,实数轴和虚数轴的二维空间。根据这个观点,复数`z = x + y * i`(其中`i*i = -1`)可以看做平面上的点,它的实数为`x`,虚部为`y`。复数加法涉及到将它们的实部和虚部相加。 90 | 91 | 对复数做乘法时,将复数以极坐标表示为模和角度更加自然。两个复数的乘积是,将一个复数按照另一个的长度作为因数拉伸,之后按照另一个的角度来旋转它的所得结果。 92 | 93 | 所以,复数有两种不同表示,它们适用于不同的操作。然而,从一些人编写使用复数的程序的角度来看,数据抽象的原则表明,所有操作复数的运算都应该可用,无论计算机使用了哪个表示。 94 | 95 | **接口。**消息传递并不仅仅提供用于组装行为和数据的方式。它也允许不同的数据类型以不同方式响应相同消息。来自不同对象,产生相似行为的共享消息是抽象的有力手段。 96 | 97 | 像之前看到的那样,抽象数据类型由构造器、选择器和额外的行为条件定义。与之紧密相关的概念是接口,它是共享消息的集合,带有它们含义的规定。响应`__repr__`和`__str__`特殊方法的对象都实现了通用的接口,它们可以表示为字符串。 98 | 99 | 在复数的例子中,接口需要实现由四个消息组成的算数运算:`real`,`imag`,`magnitude`和`angle`。我们可以使用这些消息实现加法和乘法。 100 | 101 | 我们拥有两种复数的抽象数据类型,它们的构造器不同。 102 | 103 | + `ComplexRI`从实部和虚部构造复数。 104 | + `ComplexMA`从模和角度构造复数。 105 | 106 | 使用这些消息和构造器,我们可以实现复数算数: 107 | 108 | ```py 109 | >>> def add_complex(z1, z2): 110 | return ComplexRI(z1.real + z2.real, z1.imag + z2.imag) 111 | >>> def mul_complex(z1, z2): 112 | return ComplexMA(z1.magnitude * z2.magnitude, z1.angle + z2.angle) 113 | ``` 114 | 115 | 术语“抽象数据类型”(ADT)和“接口”的关系是微妙的。ADT 包含构建复杂数据类的方式,以单元操作它们,并且可以选择它们的组件。在面向对象系统中,ADT 对应一个类,虽然我们已经看到对象系统并不需要实现 ADT。接口是一组与含义关联的消息,并且它可能包含选择器,也可能不包含。概念上,ADT 描述了一类东西的完整抽象表示,而接口规定了可能在许多东西之间共享的行为。 116 | 117 | **属性(Property)。**我们希望交替使用复数的两种类型,但是对于每个数值来说,储存重复的信息比较浪费。我们希望储存实部-虚部的表示或模-角度的表示之一。 118 | 119 | Python 拥有一个简单的特性,用于从零个参数的函数凭空计算属性(Attribute)。`@property`装饰器允许函数不使用标准调用表达式语法来调用。根据实部和虚部的复数实现展示了这一点。 120 | 121 | ```py 122 | >>> from math import atan2 123 | >>> class ComplexRI(object): 124 | def __init__(self, real, imag): 125 | self.real = real 126 | self.imag = imag 127 | @property 128 | def magnitude(self): 129 | return (self.real ** 2 + self.imag ** 2) ** 0.5 130 | @property 131 | def angle(self): 132 | return atan2(self.imag, self.real) 133 | def __repr__(self): 134 | return 'ComplexRI({0}, {1})'.format(self.real, self.imag) 135 | ``` 136 | 137 | 第二种使用模和角度的实现提供了相同接口,因为它响应同一组消息。 138 | 139 | ```py 140 | >>> from math import sin, cos 141 | >>> class ComplexMA(object): 142 | def __init__(self, magnitude, angle): 143 | self.magnitude = magnitude 144 | self.angle = angle 145 | @property 146 | def real(self): 147 | return self.magnitude * cos(self.angle) 148 | @property 149 | def imag(self): 150 | return self.magnitude * sin(self.angle) 151 | def __repr__(self): 152 | return 'ComplexMA({0}, {1})'.format(self.magnitude, self.angle) 153 | ``` 154 | 155 | 实际上,我们的`add_complex`和`mul_complex`实现并没有完成;每个复数类可以用于任何算数函数的任何参数。对象系统不以任何方式显式连接(例如通过继承)这两种复数类型,这需要给个注解。我们已经通过在两个类之间共享一组通用的消息和接口,实现了复数抽象。 156 | 157 | ```py 158 | >>> from math import pi 159 | >>> add_complex(ComplexRI(1, 2), ComplexMA(2, pi/2)) 160 | ComplexRI(1.0000000000000002, 4.0) 161 | >>> mul_complex(ComplexRI(0, 1), ComplexRI(0, 1)) 162 | ComplexMA(1.0, 3.141592653589793) 163 | ``` 164 | 165 | 编码多种表示的接口拥有良好的特性。用于每个表示的类可以独立开发;它们只需要遵循它们所共享的属性名称。这个接口同时是递增的。如果另一个程序员希望向相同程序添加第三个复数表示,它们只需要使用相同属性创建另一个类。 166 | 167 | **特殊方法。**内建的算数运算符可以以一种和`repr`相同的方式扩展;它们是特殊的方法名称,对应 Python 的算数、逻辑和序列运算的运算符。 168 | 169 | 为了使我们的代码更加易读,我们可能希望在执行复数加法和乘法时直接使用`+`和`*`运算符。将下列方法添加到两个复数类中,这会让这些运算符,以及`opertor`模块中的`add`和`mul`函数可用。 170 | 171 | ```py 172 | >>> ComplexRI.__add__ = lambda self, other: add_complex(self, other) 173 | >>> ComplexMA.__add__ = lambda self, other: add_complex(self, other) 174 | >>> ComplexRI.__mul__ = lambda self, other: mul_complex(self, other) 175 | >>> ComplexMA.__mul__ = lambda self, other: mul_complex(self, other) 176 | ``` 177 | 178 | 现在,我们可以对我们的自定义类使用中缀符号。 179 | 180 | ```py 181 | >>> ComplexRI(1, 2) + ComplexMA(2, 0) 182 | ComplexRI(3.0, 2.0) 183 | >>> ComplexRI(0, 1) * ComplexRI(0, 1) 184 | ComplexMA(1.0, 3.141592653589793) 185 | ``` 186 | 187 | **扩展阅读。**为了求解含有`+`运算符的表达式,Python 会检查表达式的左操作数和右操作数上的特殊方法。首先,Python 会检查左操作数的`__add__`方法,之后检查右操作数的`__radd__`方法。如果二者之一被发现,这个方法会以另一个操作数的值作为参数调用。 188 | 189 | 在 Python 中求解含有任何类型的运算符的表达值具有相似的协议,这包括切片符号和布尔运算符。Python 文档列出了完整的[运算符的方法名称](http://docs.python.org/py3k/reference/datamodel.html#special-method-names)。Dive into Python 3 的[特殊方法名称](http://diveintopython3.ep.io/special-method-names.html)一章描述了许多用于 Python 解释器的细节。 190 | 191 | ## 2.7.3 泛用函数 192 | 193 | 我们的复数实现创建了两种数据类型,它们对于`add_complex`和`mul_complex`函数能够互相转换。现在我们要看看如何使用相同的概念,不仅仅定义不同表示上的泛用操作,也能用来定义不同种类、并且不共享通用结构的参数上的泛用操作。 194 | 195 | 我们到目前为止已定义的操作将不同的数据类型独立对待。所以,存在用于加法的独立的包,比如两个有理数或者两个复数。我们没有考虑到的是,定义类型界限之间的操作很有意义,比如将复数与有理数相加。我们经历了巨大的痛苦,引入了程序中各个部分的界限,便于让它们可被独立开发和理解。 196 | 197 | 我们希望以某种精确控制的方式引入跨类型的操作。便于在不严重违反抽象界限的情况下支持它们。在我们希望的结果之间可能有些矛盾:我们希望能够将有理数与复数相加,也希望能够使用泛用的`add`函数,正确处理所有数值类型。同时,我们希望隔离复数和有理数的细节,来维持程序的模块化。 198 | 199 | 让我们使用 Python 内建的对象系统重新编写有理数的实现。像之前一样,我们在较低层级将有理数储存为分子和分母。 200 | 201 | ```py 202 | >>> from fractions import gcd 203 | >>> class Rational(object): 204 | def __init__(self, numer, denom): 205 | g = gcd(numer, denom) 206 | self.numer = numer // g 207 | self.denom = denom // g 208 | def __repr__(self): 209 | return 'Rational({0}, {1})'.format(self.numer, self.denom) 210 | ``` 211 | 212 | 这个新的实现中的有理数的加法和乘法和之前类似。 213 | 214 | ```py 215 | >>> def add_rational(x, y): 216 | nx, dx = x.numer, x.denom 217 | ny, dy = y.numer, y.denom 218 | return Rational(nx * dy + ny * dx, dx * dy) 219 | >>> def mul_rational(x, y): 220 | return Rational(x.numer * y.numer, x.denom * y.denom) 221 | ``` 222 | 223 | **类型分发。**一种处理跨类型操作的方式是为每种可能的类型组合设计不同的函数,操作可用于这种类型。例如,我们可以扩展我们的复数实现,使其提供函数用于将复数与有理数相加。我们可以使用叫做类型分发的机制更通用地提供这个功能。 224 | 225 | 类型分发的概念是,编写一个函数,首先检测接受到的参数类型,之后执行适用于这种类型的代码。Python 中,对象类型可以使用内建的`type`函数来检测。 226 | 227 | ```py 228 | >>> def iscomplex(z): 229 | return type(z) in (ComplexRI, ComplexMA) 230 | >>> def isrational(z): 231 | return type(z) == Rational 232 | ``` 233 | 234 | 这里,我们依赖一个事实,每个对象都知道自己的类型,并且我们可以使用Python 的`type`函数来获取类型。即使`type`函数不可用,我们也能根据`Rational`,`ComplexRI`和`ComplexMA`来实现`iscomplex`和`isrational`。 235 | 236 | 现在考虑下面的`add`实现,它显式检查了两个参数的类型。我们不会在这个例子中显式使用 Python 的特殊方法(例如`__add__`)。 237 | 238 | ```py 239 | >>> def add_complex_and_rational(z, r): 240 | return ComplexRI(z.real + r.numer/r.denom, z.imag) 241 | >>> def add(z1, z2): 242 | """Add z1 and z2, which may be complex or rational.""" 243 | if iscomplex(z1) and iscomplex(z2): 244 | return add_complex(z1, z2) 245 | elif iscomplex(z1) and isrational(z2): 246 | return add_complex_and_rational(z1, z2) 247 | elif isrational(z1) and iscomplex(z2): 248 | return add_complex_and_rational(z2, z1) 249 | else: 250 | return add_rational(z1, z2) 251 | ``` 252 | 253 | 这个简单的类型分发方式并不是递增的,它使用了大量的条件语句。如果另一个数值类型包含在程序中,我们需要使用新的语句重新实现`add`。 254 | 255 | 我们可以创建更灵活的`add`实现,通过以字典实现类型分发。要想扩展`add`的灵活性,第一步是为我们的类创建一个`tag`集合,抽离两个复数集合的实现。 256 | 257 | ```py 258 | >>> def type_tag(x): 259 | return type_tag.tags[type(x)] 260 | >>> type_tag.tags = {ComplexRI: 'com', ComplexMA: 'com', Rational: 'rat'} 261 | ``` 262 | 263 | 下面,我们使用这些类型标签来索引字典,字典中储存了数值加法的不同方式。字典的键是类型标签的元素,值是类型特定的加法函数。 264 | 265 | ```py 266 | >>> def add(z1, z2): 267 | types = (type_tag(z1), type_tag(z2)) 268 | return add.implementations[types](z1, z2) 269 | ``` 270 | 271 | 这个基于字典的分发方式是递增的,因为`add.implementations`和`type_tag.tags`总是可以扩展。任何新的数值类型可以将自己“安装”到现存的系统中,通过向这些字典添加新的条目。 272 | 273 | 当我们向系统引入一些复杂性时,我们现在拥有了泛用、可扩展的`add`函数,可以处理混合类型。 274 | 275 | ```py 276 | >>> add(ComplexRI(1.5, 0), Rational(3, 2)) 277 | ComplexRI(3.0, 0) 278 | >>> add(Rational(5, 3), Rational(1, 2)) 279 | Rational(13, 6) 280 | ``` 281 | 282 | **数据导向编程。**我们基于字典的`add`实现并不是特定于加法的;它不包含任何加法的直接逻辑。它只实现了加法操作,因为我们碰巧将`implementations`字典和函数放到一起来执行加法。 283 | 284 | 更通用的泛用算数操作版本会将任意运算符作用于任意类型,并且使用字典来储存多种组合的实现。这个完全泛用的实现方法的方式叫做数据导向编程。在我们这里,我们可以实现泛用加法和乘法,而不带任何重复的逻辑。 285 | 286 | ```py 287 | >>> def apply(operator_name, x, y): 288 | tags = (type_tag(x), type_tag(y)) 289 | key = (operator_name, tags) 290 | return apply.implementations[key](x, y) 291 | ``` 292 | 293 | 在泛用的`apply`函数中,键由操作数的名称(例如`add`),和参数类型标签的元组构造。我们下面添加了对复数和有理数的乘法支持。 294 | 295 | ```py 296 | >>> def mul_complex_and_rational(z, r): 297 | return ComplexMA(z.magnitude * r.numer / r.denom, z.angle) 298 | >>> mul_rational_and_complex = lambda r, z: mul_complex_and_rational(z, r) 299 | >>> apply.implementations = {('mul', ('com', 'com')): mul_complex, 300 | ('mul', ('com', 'rat')): mul_complex_and_rational, 301 | ('mul', ('rat', 'com')): mul_rational_and_complex, 302 | ('mul', ('rat', 'rat')): mul_rational} 303 | ``` 304 | 305 | 我们也可以使用字典的`update`方法,从`add`中将加法实现添加到`apply`。 306 | 307 | ```py 308 | >>> adders = add.implementations.items() 309 | >>> apply.implementations.update({('add', tags):fn for (tags, fn) in adders}) 310 | ``` 311 | 312 | 既然已经在单一的表中支持了 8 种不同的实现,我们可以用它来更通用地操作有理数和复数。 313 | 314 | ```py 315 | >>> apply('add', ComplexRI(1.5, 0), Rational(3, 2)) 316 | ComplexRI(3.0, 0) 317 | >>> apply('mul', Rational(1, 2), ComplexMA(10, 1)) 318 | ComplexMA(5.0, 1) 319 | ``` 320 | 321 | 这个数据导向的方式管理了跨类型运算符的复杂性,但是十分麻烦。使用这个一个系统,引入新类型的开销不仅仅是为类型编写方法,还有实现跨类型操作的函数的构造和安装。这个负担比起定义类型本身的操作需要更多代码。 322 | 323 | 当类型分发机制和数据导向编程的确能创造泛用函数的递增实现时,它们就不能有效隔离实现的细节。独立数值类型的实现者需要在编程跨类型操作时考虑其他类型。组合有理数和复数严格上并不是每种类型的范围。在类型中制定一致的责任分工政策,在带有多种类型和跨类型操作的系统设计中是大势所趋。 324 | 325 | **强制转换。**在完全不相关的类型执行完全不相关的操作的一般情况中,实现显式的跨类型操作,尽管可能非常麻烦,是人们所希望的最佳方案。幸运的是,我们有时可以通过利用类型系统中隐藏的额外结构来做得更好。不同的数据类通常并不是完全独立的,可能有一些方式,一个类型的对象通过它会被看做另一种类型的对象。这个过程叫做强制转换。例如,如果我们被要求将一个有理数和一个复数通过算数来组合,我们可以将有理数看做虚部为零的复数。通过这样做,我们将问题转换为两个复数组合的问题,这可以通过`add_complex`和`mul_complex`由经典的方法处理。 326 | 327 | 通常,我们可以通过设计强制转换函数来实现这个想法。强制转换函数将一个类型的对象转换为另一个类型的等价对象。这里是一个典型的强制转换函数,它将有理数转换为虚部为零的复数。 328 | 329 | ```py 330 | >>> def rational_to_complex(x): 331 | return ComplexRI(x.numer/x.denom, 0) 332 | ``` 333 | 334 | 现在,我们可以定义强制转换函数的字典。这个字典可以在更多的数值类型引入时扩展。 335 | 336 | ```py 337 | >>> coercions = {('rat', 'com'): rational_to_complex} 338 | ``` 339 | 340 | 任意类型的数据对象不可能转换为每个其它类型的对象。例如,没有办法将任意的复数强制转换为有理数,所以在`coercions`字典中应该没有这种转换的实现。 341 | 342 | 使用`coercions`字典,我们可以编写叫做`coerce_apply`的函数,它试图将参数强制转换为相同类型的值,之后仅仅调用运算符。`coerce_apply `的实现字典不包含任何跨类型运算符的实现。 343 | 344 | ```py 345 | >>> def coerce_apply(operator_name, x, y): 346 | tx, ty = type_tag(x), type_tag(y) 347 | if tx != ty: 348 | if (tx, ty) in coercions: 349 | tx, x = ty, coercions[(tx, ty)](x) 350 | elif (ty, tx) in coercions: 351 | ty, y = tx, coercions[(ty, tx)](y) 352 | else: 353 | return 'No coercion possible.' 354 | key = (operator_name, tx) 355 | return coerce_apply.implementations[key](x, y) 356 | ``` 357 | 358 | `coerce_apply`的`implementations`仅仅需要一个类型标签,因为它们假设两个值都共享相同的类型标签。所以,我们仅仅需要四个实现来支持复数和有理数上的泛用算数。 359 | 360 | ```py 361 | >>> coerce_apply.implementations = {('mul', 'com'): mul_complex, 362 | ('mul', 'rat'): mul_rational, 363 | ('add', 'com'): add_complex, 364 | ('add', 'rat'): add_rational} 365 | ``` 366 | 367 | 就地使用这些实现,`coerce_apply `可以代替`apply`。 368 | 369 | ```py 370 | >>> coerce_apply('add', ComplexRI(1.5, 0), Rational(3, 2)) 371 | ComplexRI(3.0, 0) 372 | >>> coerce_apply('mul', Rational(1, 2), ComplexMA(10, 1)) 373 | ComplexMA(5.0, 1.0) 374 | ``` 375 | 376 | 这个强制转换的模式比起显式定义跨类型运算符的方式具有优势。虽然我们仍然需要编程强制转换函数来关联类型,我们仅仅需要为每对类型编写一个函数,而不是为每个类型组合和每个泛用方法编写不同的函数。我们所期望的是,类型间的合理转换仅仅依赖于类型本身,而不是要调用的特定操作。 377 | 378 | 强制转换的扩展会带来进一步的优势。一些更复杂的强制转换模式并不仅仅试图将一个类型强制转换为另一个,而是将两个不同类型强制转换为第三个。想一想菱形和长方形:每个都不是另一个的特例,但是两个都可以看做平行四边形。另一个强制转换的扩展是迭代的强制转换,其中一个数据类型通过媒介类型被强制转换为另一种。一个整数可以转换为一个实数,通过首先转换为有理数,接着将有理数转换为实数。这种方式的链式强制转换降低了程序所需的转换函数总数。 379 | 380 | 虽然它具有优势,强制转换也有潜在的缺陷。例如,强制转换函数在调用时会丢失信息。在我们的例子中,有理数是精确表示,但是当它们转换为复数时会变得近似。 381 | 382 | 一些编程语言拥有内建的强制转换函数。实际上,Python 的早期版本拥有对象上的`__coerce__`特殊方法。最后,内建强制转换系统的复杂性并不能支持它的使用,所以被移除了。反之,特定的操作按需强制转换它们的参数。运算符被实现为用户定义类上的特殊方法,比如`__add__`和`__mul__`。这完全取决于你,取决于用户来决定是否使用类型分发,数据导向编程,消息传递,或者强制转换来在你的程序中实现泛用函数。 383 | -------------------------------------------------------------------------------- /3.1.md: -------------------------------------------------------------------------------- 1 | # 3.1 引言 2 | 3 | > 来源:[3.1 Introduction](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/interpretation.html#introduction) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 第一章和第二章描述了编程的两个基本元素:数据和函数之间的紧密联系。我们看到了高阶函数如何将函数当做数据操作。我们也看到了数据可以使用消息传递和对象系统绑定行为。我们已经学到了组织大型程序的技巧,例如函数抽象,数据抽象,类的继承,以及泛用函数。这些核心概念构成了坚实的基础,来构建模块化,可维护和可扩展的程序。 10 | 11 | 这一章专注于编程的第三个基本元素:程序自身。Python 程序只是文本的集合。只有通过解释过程,我们才可以基于文本执行任何有意义的计算。类似 Python 的编程语言很实用,因为我们可以定义解释器,它是一个执行 Python 求值和执行过程的程序。把它看做编程中最基本的概念并不夸张。解释器只是另一个程序,它确定编程语言中表达式的意义。 12 | 13 | 接受这一概念,需要改变我们自己作为程序员的印象。我们需要将自己看做语言的设计者,而不只是由他人设计的语言用户。 14 | 15 | ## 3.1.1 编程语言 16 | 17 | 实际上,我们可以将许多程序看做一些语言的解释器。例如,上一章的约束传播器拥有自己的原语和组合方式。约束语言是十分专用的:它提供了一种声明式的方式来描述数学关系的特定种类,而不是一种用于描述计算的完全通用的语言。虽然我们已经设计了某种语言,这章的材料会极大扩展我们可解释的语言范围。 18 | 19 | 编程语言在语法结构、特性和应用领域上差别很大。在通用编程语言中,函数定义和函数调用的结构无处不在。另一方法,存在不包含对象系统、高阶函数或类似`while`和`for`语句的控制结构的强大的编程语言。为了展示语言可以有多么不同,我们会引入[Logo](http://www.cs.berkeley.edu/~bh/logo.html)作为强大并且具有表现力的编程语言的例子,它包含非常少的高级特性。 20 | 21 | 这一章中,我们会学习解释器的设计,以及在执行程序时,它们所创建的计算过程。为通用语言设计解释器的想法可能令人畏惧。毕竟,解释器是执行任何可能计算的程序,取决于它们的输入。但是,典型的解释器拥有简洁的通用结构:两个可变的递归函数,第一个求解环境中的表达式,第二个在参数上调用函数。 22 | 23 | 这些函数都是递归的,因为它们互相定义:调用函数需要求出函数体的表达式,而求出表达式可能涉及到调用一个或多个函数。这一章接下来的两节专注于递归函数和数据结构,它们是理解解释器设计的基础。这一章的结尾专注于两个新的编程语言,以及为其实现解释器的任务。 24 | -------------------------------------------------------------------------------- /3.2.md: -------------------------------------------------------------------------------- 1 | # 3.2 函数和所生成的过程 2 | 3 | > 来源:[3.2 Functions and the Processes They Generate](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/interpretation.html#functions-and-the-processes-they-generate) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 函数是计算过程的局部演化模式。它规定了过程的每个阶段如何构建在之前的阶段之上。我们希望能够创建有关过程整体行为的语句,而过程的局部演化由一个或多个函数指定。这种分析通常非常困难,但是我们至少可以试图描述一些典型的过程演化模式。 10 | 11 | 在这一章中,我们会检测一些用于简单函数所生成过程的通用“模型”。我们也会研究这些过程消耗重要的计算资源,例如时间和空间的比例。 12 | 13 | ## 3.2.1 递归函数 14 | 15 | 如果函数的函数体直接或者间接自己调用自己,那么这个函数是递归的。也就是说,递归函数的执行过程可能需要再次调用这个函数。Python 中的递归函数不需要任何特殊的语法,但是它们的确需要一些注意来正确定义。 16 | 17 | 作为递归函数的介绍,我们以将英文单词转换为它的 Pig Latin 等价形式开始。Pig Latin 是一种隐语:对英文单词使用一种简单、确定的转换来掩盖单词的含义。Thomas Jefferson 据推测是[先行者](http://goo.gl/Cwysz)。英文单词的 Pig Latin 等价形式将辅音前缀(可能为空)从开头移动到末尾,并且添加`-ay`元音。所以,`pun`会变成`unpay`,`stout`会变成`outstay`,`all`会变成`allay`。 18 | 19 | ```py 20 | >>> def pig_latin(w): 21 | """Return the Pig Latin equivalent of English word w.""" 22 | if starts_with_a_vowel(w): 23 | return w + 'ay' 24 | return pig_latin(w[1:] + w[0]) 25 | >>> def starts_with_a_vowel(w): 26 | """Return whether w begins with a vowel.""" 27 | return w[0].lower() in 'aeiou' 28 | ``` 29 | 30 | 这个定义背后的想法是,一个以辅音开头的字符串的 Pig Latin 变体和另一个字符串的 Pig Latin 变体相同:它通过将第一个字母移到末尾来创建。于是,`sending`的 Pig Latin 变体就和`endings`的变体(`endingsay`)相同。`smother`的 Pig Latin 变体和`mothers`的变体(`othersmay`)相同。而且,将辅音从开头移动到末尾会产生带有更少辅音前缀的更简单的问题。在`sending`的例子中,将`s`移动到末尾会产生以元音开头的单词,我们的任务就完成了。 31 | 32 | 即使`pig_latin`函数在它的函数体中调用,`pig_latin`的定义是完整且正确的。 33 | 34 | ```py 35 | >>> pig_latin('pun') 36 | 'unpay' 37 | ``` 38 | 39 | 能够基于函数自身来定义函数的想法可能十分令人混乱:“循环”定义如何有意义,这看起来不是很清楚,更不用说让计算机来执行定义好的过程。但是,我们能够准确理解递归函数如何使用我们的计算环境模型来成功调用。环境的图示和描述`pig_latin('pun')`求值的表达式树展示在下面: 40 | 41 | ![](img/pig_latin.png) 42 | 43 | Python 求值过程的步骤产生如下结果: 44 | 45 | 1. `pig_latin `的`def`语句 被执行,其中: 46 | 1. 使用函数体创建新的`pig_latin`函数对象,并且 47 | 2. 将名称`pig_latin`在当前(全局)帧中绑定到这个函数上。 48 | 2. `starts_with_a_vowel `的`def`语句类似地执行。 49 | 3. 求出`pig_latin('pun')`的调用表达式,通过 50 | 1. 求出运算符和操作数子表达式,通过 51 | 1. 查找绑定到`pig_latin`函数的`pig_latin`名称 52 | 2. 对字符串对象`'pun'`求出操作数字符串字面值 53 | 2. 在参数`'pun'`上调用`pig_latin`函数,通过 54 | 1. 添加扩展自全局帧的局部帧 55 | 2. 将形参`w`绑定到当前帧的实参`'pun'`上。 56 | 3. 在以当前帧起始的环境中执行`pig_latin`的函数体 57 | 1. 最开始的条件语句没有效果,因为头部表达式求值为`False` 58 | 2. 求出最后的返回表达式`pig_latin(w[1:] + w[0])`,通过 59 | 1. 查找绑定到`pig_latin`函数的`pig_latin`名称 60 | 2. 对字符串对象`'pun'`求出操作数表达式 61 | 3. 在参数`'unp'`上调用`pig_latin`,它会从`pig_latin`函数体中的条件语句组返回预期结果。 62 | 63 | 就像这个例子所展示的那样,虽然递归函数具有循环特征,他仍旧正确调用。`pig_latin`函数调用了两次,但是每次都带有不同的参数。虽然第二个调用来自`pig_latin`自己的函数体,但由名称查找函数会成功,因为名称`pig_latin`在它的函数体执行前的环境中绑定。 64 | 65 | 这个例子也展示了 Python 的递归函数的求值过程如何与递归函数交互,来产生带有许多嵌套步骤的复杂计算过程,即使函数定义本身可能包含非常少的代码行数。 66 | 67 | ## 3.2.2 剖析递归函数 68 | 69 | 许多递归函数的函数体中都存在通用模式。函数体以基本条件开始,它是一个条件语句,为需要处理的最简单的输入定义函数行为。在`pig_latin`的例子中,基本条件对任何以元音开头的单词成立。这个时候,只需要返回末尾附加`ay`的参数。一些递归函数会有多重基本条件。 70 | 71 | 基本条件之后是一个或多个递归调用。递归调用有特定的特征:它们必须简化原始问题。在`pig_latin`的例子中,`w`中最开始辅音越多,就需要越多的处理工作。在递归调用`pig_latin(w[1:] + w[0])`中,我们在一个具有更少初始辅音的单词上调用`pig_latin` -- 这就是更简化的问题。每个成功的`pig_latin`调用都会更加简化,直到满足了基本条件:一个没有初始辅音的单词。 72 | 73 | 递归调用通过逐步简化问题来表达计算。与我们在过去使用过的迭代方式相比,它们通常以不同方式来解决问题。考虑用于计算`n`的阶乘的函数`fact`,其中`fact(4)`计算了`4! = 4·3·2·1 = 24`。 74 | 75 | 使用`while`语句的自然实现会通过将每个截至`n`的正数相乘来求出结果。 76 | 77 | ```py 78 | >>> def fact_iter(n): 79 | total, k = 1, 1 80 | while k <= n: 81 | total, k = total * k, k + 1 82 | return total 83 | >>> fact_iter(4) 84 | 24 85 | ``` 86 | 87 | 另一方面,阶乘的递归实现可以以`fact(n-1)`(一个更简单的问题)来表示`fact(n)`。递归的基本条件是问题的最简形式:`fact(1)`是`1`。 88 | 89 | ```py 90 | >>> def fact(n): 91 | if n == 1: 92 | return 1 93 | return n * fact(n-1) 94 | >>> fact(4) 95 | 24 96 | ``` 97 | 98 | 函数的正确性可以轻易通过阶乘函数的标准数学定义来验证。 99 | 100 | ``` 101 | (n − 1)! = (n − 1)·(n − 2)· ... · 1 102 | n! = n·(n − 1)·(n − 2)· ... · 1 103 | n! = n·(n − 1)! 104 | ``` 105 | 106 | 这两个阶乘函数在概念上不同。迭代的函数通过将每个式子,从基本条件`1`到最终的总数逐步相乘来构造结果。另一方面,递归函数直接从最终的式子`n`和简化的问题`fact(n-1)`构造结果。 107 | 108 | 将`fact`函数应用于更简单的问题实例,来展开递归的同时,结果最终由基本条件构建。下面的图示展示了递归如何向`fact`传入`1`而终止,以及每个调用的结果如何依赖于下一个调用,直到满足了基本条件。 109 | 110 | ![](img/fact.png) 111 | 112 | 虽然我们可以使用我们的计算模型展开递归,通常把递归调用看做函数抽象更清晰一些。也就是说,我们不应该关心`fact(n-1)`如何在`fact`的函数体中实现;我们只需要相信它计算了`n-1`的阶乘。将递归调用看做函数抽象叫做递归的“信仰飞跃”(leap of faith)。我们以函数自身来定义函数,但是仅仅相信更简单的情况在验证函数正确性时会正常工作。这个例子中我们相信,`fact(n-1)`会正确计算`(n-1)!`;我们只需要检查,如果满足假设`n!`是否正确计算。这样,递归函数正确性的验证就变成了一种归纳证明。 113 | 114 | 函数`fact_iter`和`fact`也不一样,因为前者必须引入两个额外的名称,`total`和`k`,它们在递归实现中并不需要。通常,迭代函数必须维护一些局部状态,它们会在计算过程中改变。在任何迭代的时间点上,状态刻画了已完成的结果,以及未完成的工作总量。例如,当`k`为`3`且`total`为`2`时,就还剩下两个式子没有处理,`3`和`4`。另一方面,`fact`由单一参数`n`来刻画。计算的状态完全包含在表达式树的结果中,它的返回值起到`total`的作用,并且在不同的帧中将`n`绑定到不同的值上,而不是显式跟踪`k`。 115 | 116 | 递归函数可以更加依赖于解释器本身,通过将计算状态储存为表达式树和环境的一部分,而不是显式使用局部帧中的名称。出于这个原因,递归函数通常易于定义,因为我们不需要试着弄清必须在迭代中维护的局部状态。另一方面,学会弄清由递归函数实现的计算过程,需要一些练习。 117 | 118 | ## 3.2.3 树形递归 119 | 120 | 另一个递归的普遍模式叫做树形递归。例如,考虑斐波那契序列的计算,其中每个数值都是前两个的和。 121 | 122 | ```py 123 | >>> def fib(n): 124 | if n == 1: 125 | return 0 126 | if n == 2: 127 | return 1 128 | return fib(n-2) + fib(n-1) 129 | >>> fib(6) 130 | 5 131 | ``` 132 | 133 | 这个递归定义和我们之前的尝试有很大关系:它准确反映了斐波那契数的相似定义。考虑求出`fib(6)`所产生的计算模式,它展示在下面。为了计算`fib(6)`,我们需要计算`fib(5)`和`fib(4)`。为了计算`fib(5)`,我们需要计算`fib(4)`和`fib(3)`。通常,这个演化过程看起来像一棵树(下面的图并不是完整的表达式树,而是简化的过程描述;一个完整的表达式树也拥有同样的结构)。在遍历这棵树的过程中,每个蓝点都表示斐波那契数的已完成计算。 134 | 135 | ![](img/fib.png) 136 | 137 | 调用自身多次的函数叫做树形递归。以树形递归为原型编写的函数十分有用,但是用于计算斐波那契数则非常糟糕,因为它做了很多重复的计算。要注意整个`fib(4)`的计算是重复的,它几乎是一半的工作量。实际上,不难得出函数用于计算`fib(1)`和`fib(2)`(通常是树中的叶子数量)的时间是`fib(n+1)`。为了弄清楚这有多糟糕,我们可以证明`fib(n)`的值随着`n`以指数方式增长。所以,这个过程的步骤数量随输入以指数方式增长。 138 | 139 | 140 | 我们已经见过斐波那契数的迭代实现,出于便利在这里贴出来: 141 | 142 | ```py 143 | >>> def fib_iter(n): 144 | prev, curr = 1, 0 # curr is the first Fibonacci number. 145 | for _ in range(n-1): 146 | prev, curr = curr, prev + curr 147 | return curr 148 | ``` 149 | 150 | 这里我们必须维护的状态由当前值和上一个斐波那契数组成。`for`语句也显式跟踪了迭代数量。这个定义并没有像递归方式那样清晰反映斐波那契数的数学定义。但是,迭代实现中所需的计算总数只是线性,而不是指数于`n`的。甚至对于`n`的较小值,这个差异都非常大。 151 | 152 | 然而我们不应该从这个差异总结出,树形递归的过程是没有用的。当我们考虑层次数据结构,而不是数值上的操作时,我们发现树形递归是自然而强大的工具。而且,树形过程可以变得更高效。 153 | 154 | **记忆。**用于提升重复计算的递归函数效率的机制叫做记忆。记忆函数会为任何之前接受的参数储存返回值。`fib(4)`的第二次调用不会执行与第一次同样的复杂过程,而是直接返回第一次调用的已储存结果。 155 | 156 | 记忆函数可以自然表达为高阶函数,也可以用作装饰器。下面的定义为之前的已计算结果创建缓存,由被计算的参数索引。在这个实现中,这个字典的使用需要记忆函数的参数是不可变的。 157 | 158 | ```py 159 | >>> def memo(f): 160 | """Return a memoized version of single-argument function f.""" 161 | cache = {} 162 | def memoized(n): 163 | if n not in cache: 164 | cache[n] = f(n) 165 | return cache[n] 166 | return memoized 167 | >>> fib = memo(fib) 168 | >>> fib(40) 169 | 63245986 170 | ``` 171 | 172 | 由记忆函数节省的所需的计算时间总数在这个例子中是巨大的。被记忆的递归函数`fib`和迭代函数`fib_iter`都只需要线性于输入`n`的时间总数。为了计算`fib(40)`,`fib`的函数体只执行 40 次,而不是无记忆递归中的 102,334,155 次。 173 | 174 | **空间。**为了理解函数所需的空间,我们必须在我们的计算模型中规定内存如何使用,保留和回收。在求解表达式过程中,我们必须保留所有活动环境和所有这些环境引用的值和帧。如果环境为表达式树当前分支中的一些表达式提供求值上下文,那么它就是活动环境。 175 | 176 | 例如,当求值`fib`时,解释器按序计算之前的每个值,遍历树形结构。为了这样做,它只需要在计算的任何时间点,跟踪树中在当前节点之前的那些节点。用于求出剩余节点的内存可以被回收,因为它不会影响未来的计算。通常,树形递归所需空间与树的深度成正比。 177 | 178 | 下面的图示描述了由求解`fib(3)`生成的表达式树。在求解`fib`最初调用的返回表达式的过程中,`fib(n-2)`被求值,产生值`0`。一旦这个值计算出来,对应的环境帧(标为灰色)就不再需要了:它并不是活动环境的一部分。所以,一个设计良好的解释器会回收用于储存这个帧的内存。另一方面,如果解释器当前正在求解`fib(n-1)`,那么由这次`fib`调用(其中`n`为`2`)创建的环境是活动的。与之对应,最开始在`3`上调用`fib`所创建的环境也是活动的,因为这个值还没有成功计算出来。 179 | 180 | ![](img/fib_env.png) 181 | 182 | 在`memo`的例子中,只要一些名称绑定到了活动环境中的某个函数上,关联到所返回函数(它包含`cache`)的环境必须保留。`cache`字典中的条目数量随传递给`fib`的唯一参数数量线性增长,它的规模线性于输入。另一方面,迭代实现只需要两个数值来在计算过程中跟踪:`prev`和`curr`,所以是常数大小。 183 | 184 | 我们使用记忆函数的例子展示了编程中的通用模式,即通常可以通过增加所用空间来减少计算时间,反之亦然。 185 | 186 | ## 3.2.4 示例:找零 187 | 188 | 考虑下面这个问题:如果给你半美元、四分之一美元、十美分、五美分和一美分,一美元有多少种找零的方式?更通常来说,我们能不能编写一个函数,使用一系列货币的面额,计算有多少种方式为给定的金额总数找零? 189 | 190 | 这个问题可以用递归函数简单解决。假设我们认为可用的硬币类型以某种顺序排列,假设从大到小排列。 191 | 192 | 使用`n`种硬币找零的方式为: 193 | 194 | 1. 使用所有除了第一种之外的硬币为`a`找零的方式,以及 195 | 2. 使用`n`种硬币为更小的金额`a - d`找零的方式,其中`d`是第一种硬币的面额。 196 | 197 | 为了弄清楚为什么这是正确的,可以看出,找零方式可以分为两组,不使用第一种硬币的方式,和使用它们的方式。所以,找零方式的总数等于不使用第一种硬币为该金额找零的方式数量,加上使用第一种硬币至少一次的方式数量。而后者的数量等于在使用第一种硬币之后,为剩余的金额找零的方式数量。 198 | 199 | 因此,我们可以递归将给定金额的找零问题,归约为使用更少种类的硬币为更小的金额找零的问题。仔细考虑这个归约原则,并且说服自己,如果我们规定了下列基本条件,我们就可以使用它来描述算法: 200 | 201 | 1. 如果`a`正好是零,那么有一种找零方式。 202 | 2. 如果`a`小于零,那么有零种找零方式。 203 | 3. 如果`n`小于零,那么有零种找零方式。 204 | 205 | 我们可以轻易将这个描述翻译成递归函数: 206 | 207 | ```py 208 | >>> def count_change(a, kinds=(50, 25, 10, 5, 1)): 209 | """Return the number of ways to change amount a using coin kinds.""" 210 | if a == 0: 211 | return 1 212 | if a < 0 or len(kinds) == 0: 213 | return 0 214 | d = kinds[0] 215 | return count_change(a, kinds[1:]) + count_change(a - d, kinds) 216 | >>> count_change(100) 217 | 292 218 | ``` 219 | 220 | `count_change`函数生成树形递归过程,和`fib`的首个实现一样,它是重复的。它会花费很长时间来计算出`292`,除非我们记忆这个函数。另一方面,设计迭代算法来计算出结果的方式并不是那么明显,我们将它留做一个挑战。 221 | 222 | ## 3.2.5 增长度 223 | 224 | 前面的例子表明,不同过程在花费的时间和空间计算资源上有显著差异。我们用于描述这个差异的便捷方式,就是使用增长度的概念,来获得当输入变得更大时,过程所需资源的大致度量。 225 | 226 | 令`n`为度量问题规模的参数,`R(n)`为处理规模为`n`的问题的过程所需的资源总数。在我们前面的例子中,我们将`n`看做给定函数所要计算出的数值。但是还有其他可能。例如,如果我们的目标是计算某个数值的平方根近似值,我们会将`n`看做所需的有效位数的数量。通常,有一些问题相关的特性可用于分析给定的过程。与之相似,`R(n)`可用于度量所用的内存总数,所执行的基本的机器操作数量,以及其它。在一次只执行固定数量操作的计算中,用于求解表达式的所需时间,与求值过程中执行的基本机器操作数量成正比。 227 | 228 | 我们说,`R(n)`具有`Θ(f(n))`的增长度,写作`R(n)=Θ(f(n))`(读作“theta `f(n)`”),如果存在独立于`n`的常数`k1`和`k2`,那么对于任何足够大的`n`值: 229 | 230 | ``` 231 | k1·f(n) <= R(n) <= k2·f(n) 232 | ``` 233 | 234 | 也就是说,对于较大的`n`,`R(n)`的值夹在两个具有`f(n)`规模的值之间: 235 | 236 | + 下界`k1·f(n)`,以及 237 | + 上界`k2·f(n)`。 238 | 239 | 例如,计算`n!`所需的步骤数量与`n`成正比,所以这个过程的所需步骤以`Θ(n)`增长。我们也看到了,递归实现`fact`的所需空间以`Θ(n)`增长。与之相反,迭代实现`fact_iter `花费相似的步骤数量,但是所需的空间保持不变。这里,我们说这个空间以`Θ(1)`增长。 240 | 241 | 我们的树形递归的斐波那契数计算函数`fib `的步骤数量,随输入`n`指数增长。尤其是,我们可以发现,第 n 个斐波那契数是距离`φ^(n-2)/√5`的最近整数,其中`φ`是黄金比例: 242 | 243 | ``` 244 | φ = (1 + √5)/2 ≈ 1.6180 245 | ``` 246 | 247 | 我们也表示,步骤数量随返回值增长而增长,所以树形递归过程需要`Θ(φ^n)`的步骤,它的一个随`n`指数增长的函数。 248 | 249 | 增长度只提供了过程行为的大致描述。例如,需要`n^2`个步骤的过程和需要`1000·n^2`个步骤的过程,以及需要`3·n^2+10·n+17`个步骤的过程都拥有`Θ(n^2)`的增长度。在特定的情况下,增长度的分析过于粗略,不能在函数的两个可能实现中做出判断。 250 | 251 | 但是,增长度提供了实用的方法,来表示在改变问题规模的时候,我们应如何预期过程行为的改变。对于`Θ(n)`(线性)的过程,使规模加倍只会使所需的资源总数加倍。对于指数的过程,每一点问题规模的增长都会使所用资源以固定因数翻倍。接下来的例子展示了一个增长度为对数的算法,所以使问题规模加倍,只会使所需资源以固定总数增加。 252 | 253 | ## 3.2.6 示例:求幂 254 | 255 | 256 | 考虑对给定数值求幂的问题。我们希望有一个函数,它接受底数`b`和正整数指数`n`作为参数,并计算出`b^n`。一种方式就是通过递归定义: 257 | 258 | ``` 259 | b^n = b·b^(n-1) 260 | b^0 = 1 261 | ``` 262 | 263 | 这可以翻译成递归函数: 264 | 265 | ```py 266 | >>> def exp(b, n): 267 | if n == 0: 268 | return 1 269 | return b * exp(b, n-1) 270 | ``` 271 | 272 | 这是个线性的递归过程,需要`Θ(n)`的步骤和空间。就像阶乘那样,我们可以编写等价的线性迭代形式,它需要相似的步骤数量,但只需要固定的空间。 273 | 274 | ```py 275 | >>> def exp_iter(b, n): 276 | result = 1 277 | for _ in range(n): 278 | result = result * b 279 | return result 280 | ``` 281 | 282 | 我们可以以更少的步骤求幂,通过逐次平方。例如,我们这样计算`b^8`: 283 | 284 | ``` 285 | b·(b·(b·(b·(b·(b·(b·b)))))) 286 | ``` 287 | 288 | 我们可以使用三次乘法来计算它: 289 | 290 | 291 | ``` 292 | b^2 = b·b 293 | b^4 = b^2·b^2 294 | b^8 = b^4·b^4 295 | ``` 296 | 297 | 这个方法对于 2 的幂的指数工作良好。我们也可以使用这个递归规则,在求幂中利用逐步平方的优点: 298 | 299 | ![](img/20160907175856.jpg) 300 | 301 | 我们同样可以将这个方式表达为递归函数: 302 | 303 | ```py 304 | >>> def square(x): 305 | return x*x 306 | >>> def fast_exp(b, n): 307 | if n == 0: 308 | return 1 309 | if n % 2 == 0: 310 | return square(fast_exp(b, n//2)) 311 | else: 312 | return b * fast_exp(b, n-1) 313 | >>> fast_exp(2, 100) 314 | 1267650600228229401496703205376 315 | ``` 316 | 317 | `fast_exp`所生成的过程的空间和步骤数量随`n`以对数方式增长。为了弄清楚它,可以看出,使用`fast_exp`计算`b^2n`比计算`b^n`只需要一步额外的乘法操作。于是,我们能够计算的指数大小,在每次新的乘法操作时都会(近似)加倍。所以,计算`n`的指数所需乘法操作的数量,增长得像以`2`为底`n`的对数那样慢。这个过程拥有`Θ(log n)`的增长度。`Θ(log n)`和`Θ(n)`之间的差异在`n`非常大时变得显著。例如,`n`为`1000`时,`fast_exp `仅仅需要`14`个乘法操作,而不是`1000`。 318 | -------------------------------------------------------------------------------- /3.3.md: -------------------------------------------------------------------------------- 1 | # 3.3 递归数据结构 2 | 3 | > 来源:[3.3 Recursive Data Structures](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/interpretation.html#recursive-data-structures) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 在第二章中,我们引入了偶对的概念,作为一种将两个对象结合为一个对象的机制。我们展示了偶对可以使用内建元素来实现。偶对的封闭性表明偶对的每个元素本身都可以为偶对。 10 | 11 | 这种封闭性允许我们实现递归列表的数据抽象,它是我们的第一种序列类型。递归列表可以使用递归函数最为自然地操作,就像它们的名称和结构表示的那样。在这一节中,我们会讨论操作递归列表和其它递归结构的自定义的函数。 12 | 13 | ## 3.3.1 处理递归列表 14 | 15 | 递归列表结构将列表表示为首个元素和列表的剩余部分的组合。我们之前使用函数实现了递归列表,但是现在我们可以使用类来重新实现。下面,长度(`__len__`)和元素选择(`__getitem__`)被重写来展示处理递归列表的典型模式。 16 | 17 | ```py 18 | >>> class Rlist(object): 19 | """A recursive list consisting of a first element and the rest.""" 20 | class EmptyList(object): 21 | def __len__(self): 22 | return 0 23 | empty = EmptyList() 24 | def __init__(self, first, rest=empty): 25 | self.first = first 26 | self.rest = rest 27 | def __repr__(self): 28 | args = repr(self.first) 29 | if self.rest is not Rlist.empty: 30 | args += ', {0}'.format(repr(self.rest)) 31 | return 'Rlist({0})'.format(args) 32 | def __len__(self): 33 | return 1 + len(self.rest) 34 | def __getitem__(self, i): 35 | if i == 0: 36 | return self.first 37 | return self.rest[i-1] 38 | ``` 39 | 40 | `__len__`和`__getitem__`的定义实际上是递归的,虽然不是那么明显。Python 内建函数`len`在自定义对象的参数上调用时会寻找叫做`__len__`的方法。与之类似,下标运算符会寻找叫做`__getitem__`的方法。于是,这些定义最后会调用对象自身。剩余部分上的递归调用是递归列表处理的普遍模式。这个递归列表的类定义与 Python 的内建序列和打印操作能够合理交互。 41 | 42 | ```py 43 | >>> s = Rlist(1, Rlist(2, Rlist(3))) 44 | >>> s.rest 45 | Rlist(2, Rlist(3)) 46 | >>> len(s) 47 | 3 48 | >>> s[1] 49 | 2 50 | ``` 51 | 52 | 创建新列表的操作能够直接使用递归来表示。例如,我们可以定义`extend_rlist`函数,它接受两个递归列表作为参数并将二者的元素组合到新列表中。 53 | 54 | ```py 55 | >>> def extend_rlist(s1, s2): 56 | if s1 is Rlist.empty: 57 | return s2 58 | return Rlist(s1.first, extend_rlist(s1.rest, s2)) 59 | >>> extend_rlist(s.rest, s) 60 | Rlist(2, Rlist(3, Rlist(1, Rlist(2, Rlist(3))))) 61 | ``` 62 | 63 | 与之类似,在递归列表上映射函数展示了相似的模式: 64 | 65 | ```py 66 | >>> def map_rlist(s, fn): 67 | if s is Rlist.empty: 68 | return s 69 | return Rlist(fn(s.first), map_rlist(s.rest, fn)) 70 | >>> map_rlist(s, square) 71 | Rlist(1, Rlist(4, Rlist(9))) 72 | ``` 73 | 74 | 过滤操作包括额外的条件语句,但是也拥有相似的递归结构。 75 | 76 | ```py 77 | >>> def filter_rlist(s, fn): 78 | if s is Rlist.empty: 79 | return s 80 | rest = filter_rlist(s.rest, fn) 81 | if fn(s.first): 82 | return Rlist(s.first, rest) 83 | return rest 84 | >>> filter_rlist(s, lambda x: x % 2 == 1) 85 | Rlist(1, Rlist(3)) 86 | ``` 87 | 列表操作的递归实现通常不需要局部赋值或者`while`语句。反之,递归列表可以作为函数调用的结果来拆分和构造。所以,它们拥有步骤数量和所需空间的线性增长度。 88 | 89 | ## 3.3.2 层次结构 90 | 91 | 层次结构产生于数据的封闭特性,例如,元组可以包含其它元组。考虑这个数值`1`到`4`的嵌套表示。 92 | 93 | ```py 94 | >>> ((1, 2), 3, 4) 95 | ((1, 2), 3, 4) 96 | ``` 97 | 98 | 这个元组是个长度为 3 的序列,它的第一个元素也是一个元组。这个嵌套结构的盒子和指针的图示表明,它可以看做拥有四个叶子的树,每个叶子都是一个数值。 99 | 100 | ![](img/tree.png) 101 | 102 | 在树中,每个子树本身都是一棵树。作为基本条件,任何本身不是元组的元素都是一个简单的树,没有任何枝干。也就是说,所有数值都是树,就像在偶对`(1, 2)`和整个结构中那样。 103 | 104 | 递归是用于处理树形结构的自然工具,因为我们通常可以将树的操作降至枝干的操作,它会相应产生枝干的枝干的操作,以此类推,直到我们到达了树的叶子。例如,我们可以实现`count_leaves`函数,它返回树的叶子总数。 105 | 106 | ```py 107 | >>> t = ((1, 2), 3, 4) 108 | >>> count_leaves(t) 109 | 4 110 | >>> big_tree = ((t, t), 5) 111 | >>> big_tree 112 | ((((1, 2), 3, 4), ((1, 2), 3, 4)), 5) 113 | >>> count_leaves(big_tree) 114 | 9 115 | ``` 116 | 117 | 正如`map`是用于处理序列的强大工具,映射和递归一起为树的操作提供了强大而通用的计算形式。例如,我们可以使用高阶递归函数`map_tree `将树的每个叶子平方,它的结构类似于`count_leaves`。 118 | 119 | ```py 120 | >>> def map_tree(tree, fn): 121 | if type(tree) != tuple: 122 | return fn(tree) 123 | return tuple(map_tree(branch, fn) for branch in tree) 124 | >>> map_tree(big_tree, square) 125 | ((((1, 4), 9, 16), ((1, 4), 9, 16)), 25) 126 | ``` 127 | 128 | **内部值。**上面描述的树只在叶子上存在值。另一个通用的树形结构表示也在树的内部节点上存在值。我们使用类来表示这种树。 129 | 130 | ```py 131 | >>> class Tree(object): 132 | def __init__(self, entry, left=None, right=None): 133 | self.entry = entry 134 | self.left = left 135 | self.right = right 136 | def __repr__(self): 137 | args = repr(self.entry) 138 | if self.left or self.right: 139 | args += ', {0}, {1}'.format(repr(self.left), repr(self.right)) 140 | return 'Tree({0})'.format(args) 141 | ``` 142 | 143 | 例如,`Tree`类可以为`fib`的递归实现表示表达式树中计算的值。`fib`函数用于计算斐波那契数。下面的函数`fib_tree(n)`返回`Tree`,它将第 n 个斐波那契树作为`entry`,并将所有之前计算出来的斐波那契数存入它的枝干中。 144 | 145 | ```py 146 | >>> def fib_tree(n): 147 | """Return a Tree that represents a recursive Fibonacci calculation.""" 148 | if n == 1: 149 | return Tree(0) 150 | if n == 2: 151 | return Tree(1) 152 | left = fib_tree(n-2) 153 | right = fib_tree(n-1) 154 | return Tree(left.entry + right.entry, left, right) 155 | >>> fib_tree(5) 156 | Tree(3, Tree(1, Tree(0), Tree(1)), Tree(2, Tree(1), Tree(1, Tree(0), Tree(1)))) 157 | ``` 158 | 159 | 这个例子表明,表达式树可以使用树形结构编程表示。嵌套表达式和树形数据结构的联系,在我们这一章稍后对解释器设计的讨论中起到核心作用。 160 | 161 | ## 3.3.3 集合 162 | 163 | 除了列表、元组和字典之外,Python 拥有第四种容器类型,叫做`set`。集合字面值遵循元素以花括号闭合的数学表示。重复的元素在构造中会移除。集合是无序容器,所以打印出来的顺序可能和元素在集合字面值中的顺序不同。 164 | 165 | ```py 166 | >>> s = {3, 2, 1, 4, 4} 167 | >>> s 168 | {1, 2, 3, 4} 169 | ``` 170 | 171 | Python 的集合支持多种操作,包括成员测试、长度计算和标准的交集并集操作。 172 | 173 | ```py 174 | >>> 3 in s 175 | True 176 | >>> len(s) 177 | 4 178 | >>> s.union({1, 5}) 179 | {1, 2, 3, 4, 5} 180 | >>> s.intersection({6, 5, 4, 3}) 181 | {3, 4} 182 | ``` 183 | 184 | 除了`union`和`intersection`,Python 的集合还支持多种其它操作。断言`isdisjoint`、`issubset`和`issuperset`提供了集合比较操作。集合是可变的,并且可以使用`add`、`remove`、`discard`和`pop`一次修改一个元素。额外的方法提供了多元素的修改,例如`clear`和`update`。Python [集合文档](http://docs.python.org/py3k/library/stdtypes.html#set)十分详细并足够易懂。 185 | 186 | **实现集合。**抽象上,集合是不同对象的容器,支持成员测试、交集、并集和附加操作。向集合添加元素会返回新的集合,它包含原始集合的所有元素,如果没有重复的话,也包含新的元素。并集和交集运算返回出现在任意一个或两个集合中的元素构成的集合。就像任何数据抽象那样,我们可以随意实现任何集合表示上的任何函数,只要它们提供这种行为。 187 | 188 | 在这章的剩余部分,我们会考虑三个实现集合的不同方式,它们在表示上不同。我们会通过分析集合操作的增长度,刻画这些不同表示的效率。我们也会使用这一章之前的`Rlist`和`Tree`类,它们可以编写用于集合元素操作的简单而优雅的递归解决方案。 189 | 190 | **作为无序序列的集合。**一种集合的表示方式是看做没有出现多于一次的元素的序列。空集由空序列来表示。成员测试会递归遍历整个列表。 191 | 192 | ```py 193 | >>> def empty(s): 194 | return s is Rlist.empty 195 | >>> def set_contains(s, v): 196 | """Return True if and only if set s contains v.""" 197 | if empty(s): 198 | return False 199 | elif s.first == v: 200 | return True 201 | return set_contains(s.rest, v) 202 | >>> s = Rlist(1, Rlist(2, Rlist(3))) 203 | >>> set_contains(s, 2) 204 | True 205 | >>> set_contains(s, 5) 206 | False 207 | ``` 208 | 209 | 这个`set_contains `的实现需要`Θ(n)`的时间来测试元素的成员性,其中`n`是集合`s`的大小。使用这个线性时间的成员测试函数,我们可以将元素添加到集合中,也是线性时间。 210 | 211 | ```py 212 | >>> def adjoin_set(s, v): 213 | """Return a set containing all elements of s and element v.""" 214 | if set_contains(s, v): 215 | return s 216 | return Rlist(v, s) 217 | >>> t = adjoin_set(s, 4) 218 | >>> t 219 | Rlist(4, Rlist(1, Rlist(2, Rlist(3)))) 220 | ``` 221 | 222 | 那么问题来了,我们应该在设计表示时关注效率。计算两个集合`set1`和`set2`的交集需要成员测试,但是这次每个`set1`的元素必须测试`set2`中的成员性,对于两个大小为`n`的集合,这会产生步骤数量的平方增长度`Θ(n^2)`。 223 | 224 | ```py 225 | >>> def intersect_set(set1, set2): 226 | """Return a set containing all elements common to set1 and set2.""" 227 | return filter_rlist(set1, lambda v: set_contains(set2, v)) 228 | >>> intersect_set(t, map_rlist(s, square)) 229 | Rlist(4, Rlist(1)) 230 | ``` 231 | 232 | 在计算两个集合的并集时,我们必须小心避免两次包含任意一个元素。`union_set `函数也需要线性数量的成员测试,同样会产生包含`Θ(n^2)`步骤的过程。 233 | 234 | ```py 235 | >>> def union_set(set1, set2): 236 | """Return a set containing all elements either in set1 or set2.""" 237 | set1_not_set2 = filter_rlist(set1, lambda v: not set_contains(set2, v)) 238 | return extend_rlist(set1_not_set2, set2) 239 | >>> union_set(t, s) 240 | Rlist(4, Rlist(1, Rlist(2, Rlist(3)))) 241 | ``` 242 | 243 | **作为有序元组的集合。**一种加速我们的集合操作的方式是修改表示,使集合元素递增排列。为了这样做,我们需要一些比较两个对象的方式,使我们能判断哪个更大。Python 中,许多不同对象类型都可以使用`<`和`>`运算符比较,但是我们会专注于这个例子中的数值。我们会通过将元素递增排列来表示数值集合。 244 | 245 | 有序的一个优点会在`set_contains`体现:在检查对象是否存在时,我们不再需要扫描整个集合。如果我们到达了大于要寻找的元素的集合元素,我们就知道这个元素不在集合中: 246 | 247 | ```py 248 | >>> def set_contains(s, v): 249 | if empty(s) or s.first > v: 250 | return False 251 | elif s.first == v: 252 | return True 253 | return set_contains(s.rest, v) 254 | >>> set_contains(s, 0) 255 | False 256 | ``` 257 | 258 | 这节省了多少步呢?最坏的情况中,我们所寻找的元素可能是集合中最大的元素,所以步骤数量和无序表示相同。另一方面,如果我们寻找许多不同大小的元素,我们可以预料到有时我们可以在列表开头的位置停止搜索,其它情况下我们仍旧需要检测整个列表。平均上我们应该需要检测集合中一半的元素。所以,步骤数量的平均值应该是`n/2`。这还是`Θ(n)`的增长度,但是它确实会在平均上为我们节省之前实现的一半步骤数量。 259 | 260 | 我们可以通过重新实现`intersect_set`获取更加可观的速度提升。在无序表示中,这个操作需要`Θ(n^2)`的步骤,因为我们对`set1`的每个元素执行`set2`上的完整扫描。但是使用有序的实现,我们可以使用更加机智的方式。我们同时迭代两个集合,跟踪`set1`中的元素`e1`和`set2`中的元素`e2`。当`e1`和`e2`相等时,我们在交集中添加该元素。 261 | 262 | 但是,假设`e1`小于`e2`,由于`e2`比`set2`的剩余元素更小,我们可以立即推断出`e1`不会出现在`set2`剩余部分的任何位置,因此也不会出现在交集中。所以,我们不再需要考虑`e1`,我们将它丢弃并来到`set1`的下一个元素。当`e2 < e1`时,我们可以使用相似的逻辑来步进`set2`中的元素。下面是这个函数: 263 | 264 | ```py 265 | >>> def intersect_set(set1, set2): 266 | if empty(set1) or empty(set2): 267 | return Rlist.empty 268 | e1, e2 = set1.first, set2.first 269 | if e1 == e2: 270 | return Rlist(e1, intersect_set(set1.rest, set2.rest)) 271 | elif e1 < e2: 272 | return intersect_set(set1.rest, set2) 273 | elif e2 < e1: 274 | return intersect_set(set1, set2.rest) 275 | >>> intersect_set(s, s.rest) 276 | Rlist(2, Rlist(3)) 277 | ``` 278 | 279 | 为了估计这个过程所需的步骤数量,观察每一步我们都缩小了至少集合的一个元素的大小。所以,所需的步骤数量最多为`set1`和`set2`的大小之和,而不是无序表示所需的大小之积。这是`Θ(n)`而不是`Θ(n^2)`的增长度 -- 即使集合大小适中,它也是一个相当可观的加速。例如,两个大小为`100`的集合的交集需要 `200`步,而不是无序表示的 10000 步。 280 | 281 | 表示为有序序列的集合的添加和并集操作也以线性时间计算。这些实现都留做练习。 282 | 283 | **作为二叉树的集合。**我们可以比有序列表表示做得更好,通过将几个元素重新以树的形式排列。我们使用之前引入的`Tree`类。树根的`entry`持有集合的一个元素。`left`分支的元素包括所有小于树根元素的元素。`right`分支的元素包括所有大于树根元素的元素。下面的图展示了一些树,它们表示集合`{1, 3, 5, 7, 9, 11}`。相同的集合可能会以不同形式的树来表示。有效表示所需的唯一条件就是所有`left`子树的元素应该小于`entry`,并且所有`right`子树的元素应该大于它。 284 | 285 | ![](img/set_trees.png) 286 | 287 | 树形表示的优点是:假设我们打算检查`v`是否在集合中。我们通过将`v`于`entry`比较开始。如果`v`小于它,我们就知道了我们只需要搜索`left`子树。如果`v`大于它,我们只需要搜索`right`子树。现在如果树是“平衡”的,每个这些子树都约为整个的一半大小。所以,每一步中我们都可以将大小为`n`的树的搜索问题降至搜索大小为`n/2`的子树。由于树的大小在每一步减半,我们应该预料到,用户搜索树的步骤以`Θ(log n)`增长。比起之前的表示,它的速度对于大型集合有可观的提升。`set_contains `函数利用了树形集合的有序结构: 288 | 289 | ```py 290 | >>> def set_contains(s, v): 291 | if s is None: 292 | return False 293 | elif s.entry == v: 294 | return True 295 | elif s.entry < v: 296 | return set_contains(s.right, v) 297 | elif s.entry > v: 298 | return set_contains(s.left, v) 299 | ``` 300 | 301 | 向集合添加元素与之类似,并且也需要`Θ(log n)`的增长度。为了添加值`v`,我们将`v`与`entry`比较,来决定`v`应该添加到`right`还是`left`分支,以及是否已经将`v`添加到了合适的分支上。我们将这个新构造的分支与原始的`entry`和其它分支组合。如果`v`等于`entry`,我们就可以返回这个节点。如果我们被要求将`v`添加到空的树中,我们会生成一个`Tree`,它包含`v`作为`entry`,并且`left`和`right`都是空的分支。下面是这个函数: 302 | 303 | ```py 304 | >>> def adjoin_set(s, v): 305 | if s is None: 306 | return Tree(v) 307 | if s.entry == v: 308 | return s 309 | if s.entry < v: 310 | return Tree(s.entry, s.left, adjoin_set(s.right, v)) 311 | if s.entry > v: 312 | return Tree(s.entry, adjoin_set(s.left, v), s.right) 313 | 314 | >>> adjoin_set(adjoin_set(adjoin_set(None, 2), 3), 1) 315 | Tree(2, Tree(1), Tree(3)) 316 | ``` 317 | 318 | 搜索该树可以以对数步骤数量执行,我们这个叙述基于树是“平衡”的假设。也就是说,树的左子树和右子树都拥有相同数量的相应元素,使每个子树含有母树一半的元素。但是我们如何确定,我们构造的树就是平衡的呢?即使我们以一颗平衡树开始,使用`adjoin_set`添加元素也会产生不平衡的结果。由于新添加的元素位置取决于如何将元素与集合中的已有元素比较,我们可以预测,如果我们“随机”添加元素到树中,树在平均上就会趋向于平衡。 319 | 320 | 但是这不是一个保证。例如,如果我们以空集开始,并向序列中添加 1 到 7,我们就会在最后得到很不平衡的树,其中所有左子树都是空的,所以它与简单的有序列表相比并没有什么优势。一种解决这个问题的方式是定义一种操作,它将任意的树转换为具有相同元素的平衡树。我们可以在每个`adjoin_set`操作之后执行这个转换来保证我们的集合是平衡的。 321 | 322 | 交集和并集操作可以在树形集合上以线性时间执行,通过将它们转换为有序的列表,并转换回来。细节留做练习。 323 | 324 | **Python 集合实现。**Python 内建的`set`类型并没有使用上述任意一种表示。反之,Python 使用了一种实现,它的成员测试和添加操作是(近似)常量时间的,基于一种叫做哈希(散列)的机制,这是其它课程的话题。内建的 Python 集合不能包含可变的数据类型,例如列表、字典或者其它集合。为了能够嵌套集合,Python 也提供了一种内建的不可变`frozenset `类,除了可变操作和运算符之外,它拥有和`set`相同的方法。 325 | -------------------------------------------------------------------------------- /3.4.md: -------------------------------------------------------------------------------- 1 | # 3.4 异常 2 | 3 | > 来源:[3.4 Exceptions](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/interpretation.html#exceptions) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 程序员必须总是留意程序中可能出现的错误。例子数不胜数:一个函数可能不会收到它预期的信息,必需的资源可能会丢失,或者网络上的连接可能丢失。在设计系统时,程序员必须预料到可能产生的异常情况并且采取适当地措施来处理它们。 10 | 11 | 处理程序中的错误没有单一的正确方式。为提供一些持久性服务而设计的程序,例如 Web 服务器 应该对错误健壮,将它们记录到日志中为之后考虑,而且在尽可能长的时间内继续接受新的请求。另一方面,Python 解释器通过立即终止以及打印错误信息来处理错误,便于程序员在错误发生时处理它。在任何情况下,程序员必须决定程序如何对异常条件做出反应。 12 | 13 | 异常是这一节的话题,它为程序的错误处理提供了通用的机制。产生异常是一种技巧,终止程序正常执行流,发射异常情况产生的信号,并直接返回到用于响应异常情况的程序的封闭部分。Python 解释器每次在检测到语句或表达式错误时抛出异常。用户也可以使用`raise`或`assert`语句来抛出异常。 14 | 15 | **抛出异常。**异常是一个对象实例,它的类直接或间接继承自`BaseException`类。第一章引入的`assert`语句产生`AssertionError`类的异常。通常,异常实例可以使用`raise`语句来抛出。`raise`语句的通用形式在 [Python 文档](http://docs.python.org/py3k/reference/simple_stmts.html#raise)中描述。`raise`的最常见的作用是构造异常实例并抛出它。 16 | 17 | ```py 18 | >>> raise Exception('An error occurred') 19 | Traceback (most recent call last): 20 | File "", line 1, in 21 | Exception: an error occurred 22 | ``` 23 | 24 | 当异常产生时,当前代码块的语句不会继续执行。除非异常被解决了(下面会描述),解释器会直接返回到“读取-求值-打印”交互式循环中,或者在 Python 以文件参数启动的情况下会完全终止。此外,解释器会打印栈回溯,它是结构化的文本块,描述了执行分支中的一系列嵌套的活动函数,它们是异常产生的位置。在上面的例子中,文件名称``表示异常由用户在交互式会话中产生,而不是文件中的代码。 25 | 26 | **处理异常。**异常可以使用封闭的`try`语句来处理。`try`语句由多个子句组成,第一个子句以`try`开始,剩下的以`except`开始。 27 | 28 | ```py 29 | try: 30 | 31 | except as : 32 | 33 | ... 34 | ``` 35 | 36 | 当`try`语句执行时,``总是会立即执行。`except`子句组只在``执行过程中的异常产生时执行。每个`except`子句指定了需要处理的异常的特定类。例如,如果``是`AssertionError`,那么任何继承自`AssertionError`的类实例都会被处理,标识符` `绑定到所产生的异常对象上,但是这个绑定在``之外并不有效。 37 | 38 | 例如,我们可以使用`try`语句来处理异常,在异常发生时将`x`绑定为`0`。 39 | 40 | ```py 41 | >>> try: 42 | x = 1/0 43 | except ZeroDivisionError as e: 44 | print('handling a', type(e)) 45 | x = 0 46 | handling a 47 | >>> x 48 | 0 49 | ``` 50 | 51 | `try`语句能够处理产生在函数体中的异常,函数在``中调用。当异常产生时,控制流会直接跳到最近的`try`语句的能够处理该异常类型的``的主体中。 52 | 53 | ```py 54 | >>> def invert(x): 55 | result = 1/x # Raises a ZeroDivisionError if x is 0 56 | print('Never printed if x is 0') 57 | return result 58 | >>> def invert_safe(x): 59 | try: 60 | return invert(x) 61 | except ZeroDivisionError as e: 62 | return str(e) 63 | >>> invert_safe(2) 64 | Never printed if x is 0 65 | 0.5 66 | >>> invert_safe(0) 67 | 'division by zero' 68 | ``` 69 | 70 | 这个例子表明,`invert`中的`print`表达式永远不会求值,反之,控制流跳到了`handler`中的`except`子句组中。将`ZeroDivisionError e`强制转为字符串会得到由`handler: 'division by zero'`返回的人类可读的字符串。 71 | 72 | ## 3.4.1 异常对象 73 | 74 | 异常对象本身就带有属性,例如在`assert`语句中的错误信息,以及有关异常产生处的信息。用户定义的异常类可以携带额外的属性。 75 | 76 | 在第一章中,我们实现了牛顿法来寻找任何函数的零点。下面的例子定义了一个异常类,无论何时`ValueError`出现,它都返回迭代改进过程中所发现的最佳猜测值。数学错误(`ValueError`的一种)在`sqrt`在负数上调用时产生。这个异常由抛出`IterImproveError`处理,它将牛顿迭代法的最新猜测值储存为参数。 77 | 78 | 首先,我们定义了新的类,继承自`Exception`。 79 | 80 | ```py 81 | >>> class IterImproveError(Exception): 82 | def __init__(self, last_guess): 83 | self.last_guess = last_guess 84 | ``` 85 | 86 | 下面,我们定义了`IterImprove`,我们的通用迭代改进算法的一个版本。这个版本通过抛出`IterImproveError`异常,储存最新的猜测值来处理任何`ValueError`。像之前一样,`iter_improve`接受两个函数作为参数,每个函数都接受单一的数值参数。`update`函数返回新的猜测值,而`done`函数返回布尔值,表明改进是否收敛到了正确的值。 87 | 88 | ```py 89 | >>> def iter_improve(update, done, guess=1, max_updates=1000): 90 | k = 0 91 | try: 92 | while not done(guess) and k < max_updates: 93 | guess = update(guess) 94 | k = k + 1 95 | return guess 96 | except ValueError: 97 | raise IterImproveError(guess) 98 | ``` 99 | 100 | 最后,我们定义了`find_root`,它返回`iter_improve`的结果。`iter_improve`应用于由`newton_update`返回的牛顿更新函数。`newton_update`定义在第一章,在这个例子中无需任何改变。`find_root`的这个版本通过返回它的最后一个猜测之来处理`IterImproveError`。 101 | 102 | ```py 103 | >>> def find_root(f, guess=1): 104 | def done(x): 105 | return f(x) == 0 106 | try: 107 | return iter_improve(newton_update(f), done, guess) 108 | except IterImproveError as e: 109 | return e.last_guess 110 | ``` 111 | 112 | 考虑使用`find_root`来寻找`2 * x ** 2 + sqrt(x)`的零点。这个函数的一个零点是`0`,但是在任何负数上求解它会产生`ValueError`。我们第一章的牛顿法实现会产生异常,并且不能返回任何零点的猜测值。我们的修订版实现在错误之前返回了最新的猜测值。 113 | 114 | ```py 115 | >>> from math import sqrt 116 | >>> find_root(lambda x: 2*x*x + sqrt(x)) 117 | -0.030211203830201594 118 | ``` 119 | 120 | 虽然这个近似值仍旧距离正确的答案`0`很远,一些应用更倾向于这个近似值而不是`ValueError`。 121 | 122 | 异常是另一个技巧,帮助我们将程序细节划分为模块化的部分。在这个例子中,Python 的异常机制允许我们分离迭代改进的逻辑,它在`try`子句组中没有发生改变,以及错误处理的逻辑,它出现在`except`子句中。我们也会发现,异常在使用 Python 实现解释器时是个非常实用的特性。 123 | -------------------------------------------------------------------------------- /3.5.md: -------------------------------------------------------------------------------- 1 | # 3.5 组合语言的解释器 2 | 3 | > 来源:[3.5 Interpreters for Languages with Combination](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/interpretation.html#interpreters-for-languages-with-combination) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | 运行在任何现代计算机上的软件都以多种编程语言写成。其中有物理语言,例如用于特定计算机的机器语言。这些语言涉及到基于独立储存位和原始机器指令的数据表示和控制。机器语言的程序员涉及到使用提供的硬件,为资源有限的计算构建系统和功能的高效实现。高阶语言构建在机器语言之上,隐藏了表示为位集的数据,以及表示为原始指令序列的程序的细节。这些语言拥有例如过程定义的组合和抽象的手段,它们适用于组织大规模的软件系统。 10 | 11 | 元语言抽象 -- 建立了新的语言 -- 并在所有工程设计分支中起到重要作用。它对于计算机编程尤其重要,因为我们不仅仅可以在编程中构想出新的语言,我们也能够通过构建解释器来实现它们。编程语言的解释器是一个函数,它在语言的表达式上调用,执行求解表达式所需的操作。 12 | 13 | 我们现在已经开始了技术之旅,通过这种技术,编程语言可以建立在其它语言之上。我们首先会为计算器定义解释器,它是一种受限的语言,和 Python 调用表达式具有相同的语法。我们之后会从零开始开发 Scheme 和 Logo 语言的解释器,它们都是 Lisp 的方言,Lisp 是现在仍旧广泛使用的第二老的语言。我们所创建的解释器,在某种意义上,会让我们使用 Logo 编写完全通用的程序。为了这样做,它会实现我们已经在这门课中开发的求值环境模型。 14 | 15 | ## 3.5.1 计算器 16 | 17 | 我们的第一种新语言叫做计算器,一种用于加减乘除的算术运算的表达式语言。计算器拥有 Python 调用表达式的语法,但是它的运算符对于所接受的参数数量更加灵活。例如,计算器运算符`mul`和`add`可接受任何数量的参数: 18 | 19 | ```py 20 | calc> add(1, 2, 3, 4) 21 | 10 22 | calc> mul() 23 | 1 24 | ``` 25 | 26 | `sub`运算符拥有两种行为:传入一个运算符,它会对运算符取反。传入至少两个,它会从第一个参数中减掉剩余的参数。`div`运算符拥有 Python 的`operator.truediv`的语义,只接受两个参数。 27 | 28 | ```py 29 | calc> sub(10, 1, 2, 3) 30 | 4 31 | calc> sub(3) 32 | -3 33 | calc> div(15, 12) 34 | 1.25 35 | ``` 36 | 37 | 就像 Python 中那样,调用表达式的嵌套提供了计算器语言中的组合手段。为了精简符号,我们使用运算符的标准符号来代替名称: 38 | 39 | ```py 40 | calc> sub(100, mul(7, add(8, div(-12, -3)))) 41 | 16.0 42 | calc> -(100, *(7, +(8, /(-12, -3)))) 43 | 16.0 44 | ``` 45 | 46 | 我们会使用 Python 实现计算器解释器。也就是说,我们会编写 Python 程序来接受字符串作为输入,并返回求值结果。如果输入是符合要求的计算器表达式,结果为字符串,反之会产生合适的异常。计算器语言解释器的核心是叫做`calc_eval`的递归函数,它会求解树形表达式对象。 47 | 48 | **表达式树。**到目前为止,我们在描述求值过程中所引用的表达式树,还是概念上的实体。我们从没有显式将表达式树表示为程序中的数据。为了编写解释器,我们必须将表达式当做数据操作。在这一章中,许多我们之前介绍过的概念都会最终以代码实现。 49 | 50 | 计算器中的基本表达式只是一个数值,类型为`int`或`float`。所有复合表达式都是调用表达式。调用表达式表示为拥有两个属性实例的`Exp`类。计算器的`operator`总是字符串:算数运算符的名称或符号。`operands`要么是基本表达式,要么是`Exp`的实例本身。 51 | 52 | ```py 53 | >>> class Exp(object): 54 | """A call expression in Calculator.""" 55 | def __init__(self, operator, operands): 56 | self.operator = operator 57 | self.operands = operands 58 | def __repr__(self): 59 | return 'Exp({0}, {1})'.format(repr(self.operator), repr(self.operands)) 60 | def __str__(self): 61 | operand_strs = ', '.join(map(str, self.operands)) 62 | return '{0}({1})'.format(self.operator, operand_strs) 63 | ``` 64 | 65 | `Exp`实例定义了两个字符串方法。`__repr__`方法返回 Python 表达式,而`__str__`方法返回计算器表达式。 66 | 67 | ```py 68 | >>> Exp('add', [1, 2]) 69 | Exp('add', [1, 2]) 70 | >>> str(Exp('add', [1, 2])) 71 | 'add(1, 2)' 72 | >>> Exp('add', [1, Exp('mul', [2, 3, 4])]) 73 | Exp('add', [1, Exp('mul', [2, 3, 4])]) 74 | >>> str(Exp('add', [1, Exp('mul', [2, 3, 4])])) 75 | 'add(1, mul(2, 3, 4))' 76 | ``` 77 | 78 | 最后的例子演示了`Exp`类如何通过包含作为`operands`元素的`Exp`的实例,来表示表达式树中的层次结构。 79 | 80 | **求值。**`calc_eval`函数接受表达式作为参数,并返回它的值。它根据表达式的形式为表达式分类,并且指导它的求值。对于计算器来说,表达式的两种句法形式是数值或调用表达式,后者是`Exp`的实例。数值是自求值的,它们可以直接从`calc_eval`中返回。调用表达式需要使用函数。 81 | 82 | 调用表达式首先通过将`calc_eval`函数递归映射到操作数的列表,计算出参数列表来求值。之后,在第二个函数`calc_apply`中,运算符会作用于这些参数上。 83 | 84 | 计算器语言足够简单,我们可以轻易地在单一函数中表达每个运算符的使用逻辑。在`calc_apply`中,每种条件子句对应一个运算符。 85 | 86 | ```py 87 | >>> from operator import mul 88 | >>> from functools import reduce 89 | >>> def calc_apply(operator, args): 90 | """Apply the named operator to a list of args.""" 91 | if operator in ('add', '+'): 92 | return sum(args) 93 | if operator in ('sub', '-'): 94 | if len(args) == 0: 95 | raise TypeError(operator + ' requires at least 1 argument') 96 | if len(args) == 1: 97 | return -args[0] 98 | return sum(args[:1] + [-arg for arg in args[1:]]) 99 | if operator in ('mul', '*'): 100 | return reduce(mul, args, 1) 101 | if operator in ('div', '/'): 102 | if len(args) != 2: 103 | raise TypeError(operator + ' requires exactly 2 arguments') 104 | numer, denom = args 105 | return numer/denom 106 | ``` 107 | 108 | 上面,每个语句组计算了不同运算符的结果,或者当参数错误时产生合适的`TypeError`。`calc_apply`函数可以直接调用,但是必须传入值的列表作为参数,而不是运算符表达式的列表。 109 | 110 | ```py 111 | >>> calc_apply('+', [1, 2, 3]) 112 | 6 113 | >>> calc_apply('-', [10, 1, 2, 3]) 114 | 4 115 | >>> calc_apply('*', []) 116 | 1 117 | >>> calc_apply('/', [40, 5]) 118 | 8.0 119 | ``` 120 | 121 | `calc_eval`的作用是,执行合适的`calc_apply`调用,通过首先计算操作数子表达式的值,之后将它们作为参数传入`calc_apply`。于是,`calc_eval`可以接受嵌套表达式。 122 | 123 | ```py 124 | >>> e = Exp('add', [2, Exp('mul', [4, 6])]) 125 | >>> str(e) 126 | 'add(2, mul(4, 6))' 127 | >>> calc_eval(e) 128 | 26 129 | ``` 130 | 131 | `calc_eval`的结构是个类型(表达式的形式)分发的例子。第一种表达式是数值,不需要任何的额外求值步骤。通常,基本表达式不需要任何额外的求值步骤,这叫做自求值。计算器语言中唯一的自求值表达式就是数值,但是在通用语言中可能也包括字符串、布尔值,以及其它。 132 | 133 | **“读取-求值-打印”循环。**和解释器交互的典型方式是“读取-求值-打印”循环(REPL),它是一种交互模式,读取表达式、对其求值,之后为用户打印出结果。Python 交互式会话就是这种循环的例子。 134 | 135 | REPL 的实现与所使用的解释器无关。下面的`read_eval_print_loop`函数使用内建的`input`函数,从用户接受一行文本作为输入。它使用语言特定的`calc_parse`函数构建表达式树。`calc_parse`在随后的解析一节中定义。最后,它打印出对由`calc_parse`返回的表达式树调用`calc_eval`的结果。 136 | 137 | ```py 138 | >>> def read_eval_print_loop(): 139 | """Run a read-eval-print loop for calculator.""" 140 | while True: 141 | expression_tree = calc_parse(input('calc> ')) 142 | print(calc_eval(expression_tree)) 143 | ``` 144 | 145 | `read_eval_print_loop`的这个版本包含所有交互式界面的必要组件。一个样例会话可能像这样: 146 | 147 | ```py 148 | calc> mul(1, 2, 3) 149 | 6 150 | calc> add() 151 | 0 152 | calc> add(2, div(4, 8)) 153 | 2.5 154 | ``` 155 | 156 | 这个循环没有实现终端或者错误处理机制。我们可以通过向用户报告错误来改进这个界面。我们也可以允许用户通过发射键盘中断信号(`Control-C`),或文件末尾信号(`Control-D`)来退出循环。为了实现这些改进,我们将原始的`while`语句组放在`try`语句中。第一个`except`子句处理了由`calc_parse`产生的`SyntaxError`异常,也处理了由`calc_eval`产生的`TypeError`和`ZeroDivisionError`异常。 157 | 158 | ```py 159 | >>> def read_eval_print_loop(): 160 | """Run a read-eval-print loop for calculator.""" 161 | while True: 162 | try: 163 | expression_tree = calc_parse(input('calc> ')) 164 | print(calc_eval(expression_tree)) 165 | except (SyntaxError, TypeError, ZeroDivisionError) as err: 166 | print(type(err).__name__ + ':', err) 167 | except (KeyboardInterrupt, EOFError): # -D, etc. 168 | print('Calculation completed.') 169 | return 170 | ``` 171 | 172 | 这个循环实现报告错误而不退出循环。发生错误时不退出程序,而是在错误消息之后重新开始循环可以让用户回顾他们的表达式。通过导入`readline`模块,用户甚至可以使用上箭头或`Control-P`来回忆他们之前的输入。最终的结果提供了错误信息报告的界面: 173 | 174 | ```py 175 | calc> add 176 | SyntaxError: expected ( after add 177 | calc> div(5) 178 | TypeError: div requires exactly 2 arguments 179 | calc> div(1, 0) 180 | ZeroDivisionError: division by zero 181 | calc> ^DCalculation completed. 182 | ``` 183 | 184 | 在我们将解释器推广到计算器之外的语言时,我们会看到,`read_eval_print_loop`由解析函数、求值函数,和由`try`语句处理的异常类型参数化。除了这些修改之外,任何 REPL 都可以使用相同的结构来实现。 185 | 186 | ## 3.5.2 解析 187 | 188 | 解析是从原始文本输入生成表达式树的过程。解释这些表达式树是求值函数的任务,但是解析器必须提供符合格式的表达式树给求值器。解析器实际上由两个组件组成,词法分析器和语法分析器。首先,词法分析器将输入字符串拆成标记(token),它们是语言的最小语法单元,就像名称和符号那样。其次,语法分析器从这个标记序列中构建表达式树。 189 | 190 | ```py 191 | >>> def calc_parse(line): 192 | """Parse a line of calculator input and return an expression tree.""" 193 | tokens = tokenize(line) 194 | expression_tree = analyze(tokens) 195 | if len(tokens) > 0: 196 | raise SyntaxError('Extra token(s): ' + ' '.join(tokens)) 197 | return expression_tree 198 | ``` 199 | 200 | 标记序列由叫做`tokenize`的词法分析器产生,并被叫做`analyze`语法分析器使用。这里,我们定义了`calc_parse`,它只接受符合格式的计算器表达式。一些语言的解析器为接受以换行符、分号或空格分隔的多种表达式而设计。我们在引入 Logo 语言之前会推迟实现这种复杂性。 201 | 202 | **词法分析。**用于将字符串解释为标记序列的组件叫做分词器(tokenizer ),或者词法分析器。在我们的视线中,分词器是个叫做`tokenize`的函数。计算器语言由包含数值、运算符名称和运算符类型的符号(比如`+`)组成。这些符号总是由两种分隔符划分:逗号和圆括号。每个符号本身都是标记,就像每个逗号和圆括号那样。标记可以通过向输入字符串添加空格,之后在每个空格处分割字符串来分开。 203 | 204 | ```py 205 | >>> def tokenize(line): 206 | """Convert a string into a list of tokens.""" 207 | spaced = line.replace('(',' ( ').replace(')',' ) ').replace(',', ' , ') 208 | return spaced.split() 209 | ``` 210 | 211 | 对符合格式的计算器表达式分词不会损坏名称,但是会分开所有符号和分隔符。 212 | 213 | ```py 214 | >>> tokenize('add(2, mul(4, 6))') 215 | ['add', '(', '2', ',', 'mul', '(', '4', ',', '6', ')', ')'] 216 | ``` 217 | 218 | 拥有更加复合语法的语言可能需要更复杂的分词器。特别是,许多分析器会解析每种返回标记的语法类型。例如,计算机中的标记类型可能是运算符、名称、数值或分隔符。这个分类可以简化标记序列的解析。 219 | 220 | **语法分析。**将标记序列解释为表达式树的组件叫做语法分析器。在我们的实现中,语法分析由叫做`analyze`的递归函数完成。它是递归的,因为分析标记序列经常涉及到分析这些表达式树中的标记子序列,它本身作为更大的表达式树的子分支(比如操作数)。递归会生成由求值器使用的层次结构。 221 | 222 | `analyze`函数接受标记列表,以符合格式的表达式开始。它会分析第一个标记,将表示数值的字符串强制转换为数字的值。之后要考虑计算机中的两个合法表达式类型。数字标记本身就是完整的基本表达式树。复合表达式以运算符开始,之后是操作数表达式的列表,由圆括号分隔。我们以一个不检查语法错误的实现开始。 223 | 224 | ```py 225 | >>> def analyze(tokens): 226 | """Create a tree of nested lists from a sequence of tokens.""" 227 | token = analyze_token(tokens.pop(0)) 228 | if type(token) in (int, float): 229 | return token 230 | else: 231 | tokens.pop(0) # Remove ( 232 | return Exp(token, analyze_operands(tokens)) 233 | >>> def analyze_operands(tokens): 234 | """Read a list of comma-separated operands.""" 235 | operands = [] 236 | while tokens[0] != ')': 237 | if operands: 238 | tokens.pop(0) # Remove , 239 | operands.append(analyze(tokens)) 240 | tokens.pop(0) # Remove ) 241 | return operands 242 | ``` 243 | 244 | 最后,我们需要实现`analyze_token`。`analyze_token`函数将数值文本转换为数值。我们并不自己实现这个逻辑,而是依靠内建的 Python 类型转换,使用`int`和`float`构造器来将标记转换为这种类型。 245 | 246 | ```py 247 | >>> def analyze_token(token): 248 | """Return the value of token if it can be analyzed as a number, or token.""" 249 | try: 250 | return int(token) 251 | except (TypeError, ValueError): 252 | try: 253 | return float(token) 254 | except (TypeError, ValueError): 255 | return token 256 | ``` 257 | 258 | 我们的`analyze`实现就完成了。它能够正确将符合格式的计算器表达式解析为表达式树。这些树由`str`函数转换回计算器表达式。 259 | 260 | ```py 261 | >>> expression = 'add(2, mul(4, 6))' 262 | >>> analyze(tokenize(expression)) 263 | Exp('add', [2, Exp('mul', [4, 6])]) 264 | >>> str(analyze(tokenize(expression))) 265 | 'add(2, mul(4, 6))' 266 | ``` 267 | 268 | `analyse`函数只会返回符合格式的表达式树,并且它必须检测输入中的语法错误。特别是,它必须检测表达式是否完整、正确分隔,以及只含有已知的运算符。下面的修订版本确保了语法分析的每一步都找到了预期的标记。 269 | 270 | ```py 271 | >>> known_operators = ['add', 'sub', 'mul', 'div', '+', '-', '*', '/'] 272 | >>> def analyze(tokens): 273 | """Create a tree of nested lists from a sequence of tokens.""" 274 | assert_non_empty(tokens) 275 | token = analyze_token(tokens.pop(0)) 276 | if type(token) in (int, float): 277 | return token 278 | if token in known_operators: 279 | if len(tokens) == 0 or tokens.pop(0) != '(': 280 | raise SyntaxError('expected ( after ' + token) 281 | return Exp(token, analyze_operands(tokens)) 282 | else: 283 | raise SyntaxError('unexpected ' + token) 284 | >>> def analyze_operands(tokens): 285 | """Analyze a sequence of comma-separated operands.""" 286 | assert_non_empty(tokens) 287 | operands = [] 288 | while tokens[0] != ')': 289 | if operands and tokens.pop(0) != ',': 290 | raise SyntaxError('expected ,') 291 | operands.append(analyze(tokens)) 292 | assert_non_empty(tokens) 293 | tokens.pop(0) # Remove ) 294 | return elements 295 | >>> def assert_non_empty(tokens): 296 | """Raise an exception if tokens is empty.""" 297 | if len(tokens) == 0: 298 | raise SyntaxError('unexpected end of line') 299 | ``` 300 | 301 | 大量的语法错误在本质上提升了解释器的可用性。在上面,`SyntaxError `异常包含所发生的问题描述。这些错误字符串也用作这些分析函数的定义文档。 302 | 303 | 这个定义完成了我们的计算器解释器。你可以获取单独的 Python 3 源码 [`calc.py`](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/calc.py)来测试。我们的解释器对错误健壮,用户在`calc>`提示符后面的每个输入都会求值为数值,或者产生合适的错误,描述输入为什么不是符合格式的计算器表达式。 304 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SICP Python 描述 中文版 2 | 3 | 原文:[CS61A: Online Textbook](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/) 4 | 5 | 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | + [在线阅读](https://www.gitbook.com/book/wizardforcel/sicp-py/details) 8 | + [PDF格式](https://www.gitbook.com/download/pdf/book/wizardforcel/sicp-py) 9 | + [EPUB格式](https://www.gitbook.com/download/epub/book/wizardforcel/sicp-py) 10 | + [MOBI格式](https://www.gitbook.com/download/mobi/book/wizardforcel/sicp-py) 11 | + [Github](https://github.com/wizardforcel/sicp-py-zh) 12 | 13 | ## 赞助我 14 | 15 | ![](img/qr_alipay.png) 16 | 17 | ## 协议 18 | 19 | [CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 20 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | + [SICP Python 描述 中文版](README.md) 2 | + [第一章 使用函数构建抽象](ch1.md) 3 | + [1.1 引言](1.1.md) 4 | + [1.2 编程元素](1.2.md) 5 | + [1.3 定义新的函数](1.3.md) 6 | + [1.4 实践指南:函数的艺术](1.4.md) 7 | + [1.5 控制](1.5.md) 8 | + [1.6 高阶函数](1.6.md) 9 | + [第二章 使用对象构建抽象](ch2.md) 10 | + [2.1 引言](2.1.md) 11 | + [2.2 数据抽象](2.2.md) 12 | + [2.3 序列](2.3.md) 13 | + [2.4 可变数据](2.4.md) 14 | + [2.5 面向对象编程](2.5.md) 15 | + [2.6 实现类和对象](2.6.md) 16 | + [2.7 泛用方法](2.7.md) 17 | + [第三章 计算机程序的构造和解释](ch3.md) 18 | + [3.1 引言](3.1.md) 19 | + [3.2 函数和所生成的过程](3.2.md) 20 | + [3.3 递归数据结构](3.3.md) 21 | + [3.4 异常](3.4.md) 22 | + [3.5 组合语言的解释器](3.5.md) 23 | + [3.6 抽象语言的解释器](3.6.md) 24 | + [第四章 分布式和并行计算](ch4.md) 25 | + [第五章 序列和协程](ch5.md) -------------------------------------------------------------------------------- /ch1.md: -------------------------------------------------------------------------------- 1 | # 第一章 使用函数构建抽象 -------------------------------------------------------------------------------- /ch2.md: -------------------------------------------------------------------------------- 1 | # 第二章 使用对象构建抽象 -------------------------------------------------------------------------------- /ch3.md: -------------------------------------------------------------------------------- 1 | # 第三章 计算机程序的构造和解释 -------------------------------------------------------------------------------- /ch4.md: -------------------------------------------------------------------------------- 1 | # 第四章 分布式和并行计算 2 | 3 | > 来源:[Chapter 4: Distributed and Parallel Computing](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/communication.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | ## 4.1 引言 10 | 11 | 目前为止,我们专注于如何创建、解释和执行程序。在第一章中,我们学会使用函数作为组合和抽象的手段。第二章展示了如何使用数据结构和对象来表示和操作数据,以及向我们介绍了数据抽象的概念。在第三章中,我们学到了计算机程序如何解释和执行。结果是,我们理解了如何设计程序,它们在单一处理器上运行。 12 | 13 | 这一章中,我们跳转到协调多个计算机和处理器的问题。首先,我们会观察分布式系统。它们是互相连接的独立计算机,需要互相沟通来完成任务。它们可能需要协作来提供服务,共享数据,或者甚至是储存太大而不能在一台机器上装下的数据。我们会看到,计算机可以在分布式系统中起到不同作用,并且了解各种信息,计算机需要交换它们来共同工作。 14 | 15 | 接下来,我们会考虑并行计算。并行计算是这样,当一个小程序由多个处理器使用共享内存执行时,所有处理器都并行工作来使任务完成得更快。并发(或并行)引入了新的挑战,并且我们会开发新的机制来管理并发程序的复杂性。 16 | 17 | ## 4.2 分布式系统 18 | 19 | 分布式系统是自主的计算机网络,计算机互相通信来完成一个目标。分布式系统中的计算机都是独立的,并且没有物理上共享的内存或处理器。它们使用消息来和其它计算机通信,消息是网络上从一台计算机到另一台计算机传输的一段信息。消息可以用于沟通许多事情:计算机可以让其它计算机来执行一个带有特定参数的过程,它们可以发送和接受数据包,或者发送信号让其它计算机执行特定行为。 20 | 21 | 分布式系统中的计算机具有不同的作用。计算机的作用取决于系统的目标,以及计算机自身的硬件和软件属性。分布式系统中,有两种主要方式来组织计算机,一种叫客户端-服务端架构(C/S 架构),另一种叫做对等网络架构(P2P 架构)。 22 | 23 | ### 4.2.1 C/S 系统 24 | 25 | C/S 架构是一种从中心来源分发服务的方式。只有单个服务端提供服务,多台客户端和服务器通信来消耗它的产出。在这个架构中,客户端和服务端都有不同的任务。服务端的任务就是响应来自客户端的服务请求,而客户端的任务就是使用响应中提供的数据来执行一些任务。 26 | 27 | ![](img/clientserver.png) 28 | 29 | C/S 通信模型可以追溯到二十世纪七十年代 Unix 的引入,但这一模型由于现代万维网(WWW)中的使用而变得具有影响力。一个C/S 交互的例子就是在线阅读纽约时报。当`www.nytimes.com`上的服务器与浏览器客户端(比如 Firefox)通信时,它的任务就是发送回来纽约时报主页的 HTML。这可能涉及到基于发送给服务器的用户账户信息,计算个性化的内容。这意味着需要展示图片,安排视觉上的内容,展示不同的颜色、字体和图形,以及允许用户和渲染后的页面交互。 30 | 31 | 客户端和服务端的概念是强大的函数式抽象。客户端仅仅是一个提供服务的单位,可能同时对应多个客户端。客户端是消耗服务的单位。客户端并不需要知道服务如何提供的细节,或者所获取的数据如何储存和计算,服务端也不需要知道数据如何使用。 32 | 33 | 在网络上,我们认为客户端和服务端都是不同的机器,但是,一个机器上的系统也可以拥有 C/S 架构。例如,来自计算机输入设备的信号需要让运行在计算机上的程序来访问。这些程序就是客户端,消耗鼠标和键盘的输入数据。操作系统的设备驱动就是服务端,接受物理的信号并将它们提供为可用的输入。 34 | 35 | C/S 系统的一个缺陷就是,服务端是故障单点。它是唯一能够分发服务的组件。客户端的数量可以是任意的,它们可以交替,并且可以按需出现和消失。但是如果服务器崩溃了,整个系统就会停止工作。所以,由 C/S 架构创建的函数式抽象也使它具有崩溃的风险。 36 | 37 | C/S 系统的另一个缺陷是,当客户端非常多的时候,资源就变得稀缺。客户端增加系统上的命令而不贡献任何计算资源。C/S 系统不能随着不断变化的需求缩小或扩大。 38 | 39 | ### 4.2.2 P2P 系统 40 | 41 | C/S 模型适合于服务导向的情形。但是,还有其它计算目标,适合使用更加平等的分工。P2P 的术语用于描述一种分布式系统,其中劳动力分布在系统的所有组件中。所有计算机发送并接受数据,它们都贡献一些处理能力和内存。随着分布式系统的规模增长,它的资源计算能力也会增长。在 P2P 系统中,系统的所有组件都对分布式计算贡献了一些处理能力和内存。 42 | 43 | 所有参与者的劳动力的分工是 P2P 系统的识别特征。也就是说,对等者需要能够和其它人可靠地通信。为了确保消息到达预定的目的地,P2P 系统需要具有组织良好的网络结构。这些系统中的组件协作来维护足够的其它组件的位置信息并将消息发送到预定的目的地。 44 | 45 | 在一些 P2P 系统中,维护网络健康的任务由一系列特殊的组件执行。这种系统并不是纯粹的 P2P 系统,因为它们具有不同类型的组件类型,提供不同的功能。支持 P2P 网络的组件就像脚手架那样:它们有助于网络保持连接,它们维护不同计算机的位置信息,并且它们新来者来邻居中找到位置。 46 | 47 | P2P 系统的最常见应用就是数据传送和存储。对于数据传送,系统中的每台计算机都致力于网络上的数据传送。如果目标计算机是特定计算机的邻居,那台计算机就一起帮助传送数据。对于数据存储,数据集可以过于庞大,不能在任何单台计算机内装下,或者储存在单台计算机内具有风险。每台计算机都储存数据的一小部分,不同的计算机上可能会储存相同数据的多个副本。当一台计算机崩溃时,上面的数据可以由其它副本恢复,或者在更换替代品之后放回。 48 | 49 | Skype,一个音频和视频聊天服务,是采用 P2P 架构的数据传送应用的示例。当不同计算机上的两个人都使用 Skype 交谈时,它们的通信会拆成由 1 和 0 构成的数据包,并且通过 P2P 网络传播。这个网络由电脑上注册了 Skype 的其它人组成。每台计算机都知道附近其它人的位置。一台计算机通过将数据包传给它的邻居,来帮助将它传到目的地,它的邻居又将它传给其它邻居,以此类推,直到数据包到达了它预定的目的地。Skype 并不是纯粹的 P2P 系统。一个超级节点组成的脚手架网络用于用户登录和退出,维护它们的计算机的位置信息,并且修改网络结构来处理用户进入和离开。 50 | 51 | ### 4.2.3 模块化 52 | 53 | 我们刚才考虑的两个架构 -- P2P 和 C/S -- 都为强制模块化而设计。模块化是一个概念,系统的组件对其它组件来说应该是个黑盒。组件如何实现行为应该并不重要,只要它提供了一个接口:规定了输入应该产生什么输出。 54 | 55 | 在第二章中,我们在调度函数和面向对象编程的上下文中遇到了接口。这里,接口的形式为指定对象应接收的信息,以及对象应如何响应它们。例如,为了提供“表示为字符串”的接口,对象必须回复`__repr__`和`__str__`信息,并且在响应中输出合适的字符串。那些字符串的生成如何实现并不是接口的一部分。 56 | 57 | 在分布式系统中,我们必须考虑涉及到多台计算机的程序设计,所以我们将接口的概念从对象和消息扩展为整个程序。接口指定了应该接受的输入,以及应该在响应中返回给输入的输出。 58 | 59 | 接口在真实世界的任何地方都存在,我们经常习以为常。一个熟悉的例子就是 TV 遥控器。你可以买到许多牌子的遥控器或者 TV,它们都能工作。它们的唯一共同点就是“TV 遥控器”的接口。只要当你按下电院、音量、频道或者其它任何按钮(输入)时,一块电路向你的 TV 发送正确的信号(输出),它就遵循“TV 遥控器”接口。 60 | 61 | 模块化给予系统许多好处,并且是一种沉思熟虑的系统设计。首先,模块化的系统易于理解。这使它易于修改和扩展。其次,如果系统中什么地方发生错误,只需要更换有错误的组件。再者,bug 或故障可以轻易定位。如果组件的输出不符合接口的规定,而且输入是正确的,那么这个组件就是故障来源。 62 | 63 | ### 4.2.4 消息传递 64 | 65 | 在分布式系统中,组件使用消息传递来互相沟通。消息有三个必要部分:发送者、接收者和内容。发送者需要被指定,便于接受者得知哪个组件发送了信息,以及将回复发送到哪里。接收者需要被指定,便于任何协助发送消息的计算机知道发送到哪里。消息的内容是最宝贵的。取决于整个系统的函数,内容可以是一段数据、一个信号,或者一条指令,让远程计算机来以一些参数求出某个函数。 66 | 67 | 消息传递的概念和第二章的消息传递机制有很大关系,其中,调度函数或字典会响应值为字符串的信息。在程序中,发送者和接受者都由求值规则标识。但是在分布式系统中,接受者和发送者都必须显式编码进消息中。在程序中,使用字符串来控制调度函数的行为十分方便。在分布式系统中,消息需要经过网络发送,并且可能需要存放许多不同种类的信号作为“数据”,所以它们并不始终编码为字符串。但是在两种情况中,消息都服务于相同的函数。不同的组件(调度函数或计算机)交换消息来完成一个目标,它需要多个组件模块的协作。 68 | 69 | 在较高层面上,消息内容可以是复杂的数据结构,但是在较低层面上,消息只是简单的 1 和 0 的流,在网络上传输。为了变得易用,所有网络上发送的消息都需要根据一致的消息协议格式化。 70 | 71 | **消息协议**是一系列规则,用于编码和解码消息。许多消息协议规定,消息必须符合特定的格式,其中特定的比特具有固定的含义。固定的格式实现了固定的编码和解码规则来生成和读取这种格式。分布式系统中的所有组件都必须理解协议来互相通信。这样,它们就知道消息的哪个部分对应哪个信息。 72 | 73 | 消息协议并不是特定的程序或软件库。反之,它们是可以由大量程序使用的规则,甚至以不同的编程语言编写。所以,带有大量不同软件系统的计算机可以加入相同的分布式系统,只需要遵守控制这个系统的消息协议。 74 | 75 | ### 4.2.5 万维网上的消息 76 | 77 | **HTTP**(超文本传输协议的缩写)是万维网所支持的消息协议。它指定了在 Web 浏览器和服务器之间交换的消息格式。所有 Web 浏览器都使用 HTTP 协议来请求服务器上的页面,而且所有 Web 服务器都使用 HTTP 格式来发回它们的响应。 78 | 79 | 当你在 Web 浏览器上键入 URL 时,比如 ,你实际上就告诉了你的计算机,使用 "HTTP" 协议,从 "http://en.wikipedia.org/wiki/UC_Berkeley" 的服务器上请求 "wiki/UC_Berkeley" 页面。消息的发送者是你的计算机,接受者是 en.wikipedia.org,以及消息内容的格式是: 80 | 81 | ```http 82 | GET /wiki/UC_Berkeley HTTP/1.1 83 | ``` 84 | 85 | 第一个单词是请求类型,下一个单词是所请求的资源,之后是协议名称(HTTP)和版本(1.1)。(请求还有其它类型,例如 PUT、POST 和 HEAD,Web 浏览器也会使用它们。) 86 | 87 | 服务器发回了回复。这时,发送者是 en.wikipedia.org,接受者是你的计算机,消息内容的格式是由数据跟随的协议头: 88 | 89 | ```http 90 | HTTP/1.1 200 OK 91 | Date: Mon, 23 May 2011 22:38:34 GMT 92 | Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux) 93 | Last-Modified: Wed, 08 Jan 2011 23:11:55 GMT 94 | Content-Type: text/html; charset=UTF-8 95 | 96 | ... web page content ... 97 | ``` 98 | 99 | 第一行,单词 "200 OK" 表示没有发生错误。协议头下面的行提供了有关服务器的信息,日期和发回的内容类型。协议头和页面的实际内容通过一个空行来分隔。 100 | 101 | 如果你键入了错误的 Web 地址,或者点击了死链,你可能会看到类似于这个错误的消息: 102 | 103 | ``` 104 | 404 Error File Not Found 105 | ``` 106 | 107 | 它的意思是服务器发送回了一个 HTTP 协议头,以这样起始: 108 | 109 | ```http 110 | HTTP/1.1 404 Not Found 111 | ``` 112 | 113 | 一系列固定的响应代码是消息协议的普遍特性。协议的设计者试图预料通过协议发送的常用消息,并且赋为固定的代码来减少传送大小,以及建立通用的消息语义。在 HTTP 协议中,200 响应代码表示成功,而 404 表示资源没有找到的错误。其它大量[响应代码](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes)也存在于 HTTP 1.1 标准中。 114 | 115 | HTTP 是用于通信的固定格式,但是它允许传输任意的 Web 页面。其它互联网上的类似协议是 XMPP,即时消息的常用协议,以及 FTP,用于在客户端和服务器之间下载和上传文件的协议。 116 | 117 | ## 4.3 并行计算 118 | 119 | 计算机每一年都会变得越来越快。在 1965 年,英特尔联合创始人戈登·摩尔预测了计算机将如何随时间而变得越来越快。仅仅基于五个数据点,他推测,一个芯片中的晶体管数量每两年将翻一倍。近50年后,他的预测仍惊人地准确,现在称为摩尔定律。 120 | 121 | 尽管速度在爆炸式增长,计算机还是无法跟上可用数据的规模。根据一些估计,基因测序技术的进步将使可用的基因序列数据比处理器变得更快的速度还要快。换句话说,对于遗传数据,计算机变得越来越不能处理每年需要处理的问题规模,即使计算机本身变得越来越快。 122 | 123 | 为了规避对单个处理器速度的物理和机械约束,制造商正在转向另一种解决方案:多处理器。如果两个,或三个,或更多的处理器是可用的,那么许多程序可以更快地执行。当一个处理器在做一些计算的一个切面时,其他的可以在另一个切面工作。所有处理器都可以共享相同的数据,但工作并行执行。 124 | 125 | 126 | 为了能够合作,多个处理器需要能够彼此共享信息。这通过使用共享内存环境来完成。该环境中的变量、对象和数据结构对所有的进程可见。处理器在计算中的作用是执行编程语言的求值和执行规则。在一个共享内存模型中,不同的进程可能执行不同的语句,但任何语句都会影响共享环境。 127 | 128 | ### 4.3.1 共享状态的问题 129 | 130 | 多个进程之间的共享状态具有单一进程环境没有的问题。要理解其原因,让我们看看下面的简单计算: 131 | 132 | ```py 133 | x = 5 134 | x = square(x) 135 | x = x + 1 136 | ``` 137 | 138 | `x`的值是随时间变化的。起初它是 5,一段时间后它是 25,最后它是 26。在单一处理器的环境中,没有时间依赖性的问题。`x`的值在结束时总是 26。但是如果存在多个进程,就不能这样说了。假设我们并行执行了上面代码的最后两行:一个处理器执行`x = square(x)`而另一个执行`x = x + 1`。每一个这些赋值语句都包含查找当前绑定到`x`的值,然后使用新值更新绑定。让我们假设`x`是共享的,同一时间只有一个进程读取或写入。即使如此,读和写的顺序可能会有所不同。例如,下面的例子显示了两个进程的每个进程的一系列步骤,`P1`和`P2`。每一步都是简要描述的求值过程的一部分,随时间从上到下执行: 139 | 140 | ``` 141 | P1 P2 142 | read x: 5 143 | read x: 5 144 | calculate 5*5: 25 calculate 5+1: 6 145 | write 25 -> x 146 | write x-> 6 147 | ``` 148 | 149 | 150 | 在这个顺序中,`x`的最终值为 6。如果我们不协调这两个过程,我们可以得到另一个顺序的不同结果: 151 | 152 | ``` 153 | P1 P2 154 | read x: 5 155 | read x: 5 calculate 5+1: 6 156 | calculate 5*5: 25 write x->6 157 | write 25 -> x 158 | ``` 159 | 160 | 在这个顺序中,`x`将是 25。事实上存在多种可能性,这取决于进程执行代码行的顺序。`x`的最终值可能最终为 5,25,或预期值 26。 161 | 162 | 前面的例子是无价值的。`square(x)`和`x = x + 1`是简单快速的计算。我们强迫一条语句跑在另一条的后面,并不会失去太多的时间。但是什么样的情况下,并行化是必不可少的?这种情况的一个例子是银行业。在任何给定的时间,可能有成千上万的人想用他们的银行账户进行交易:他们可能想在商店刷卡,存入支票,转帐,或支付账单。即使一个帐户在同一时间也可能有活跃的多个交易。 163 | 164 | 让我们看看第二章的`make_withdraw`函数,下面是修改过的版本,在更新余额之后打印而不是返回它。我们感兴趣的是这个函数将如何并发执行。 165 | 166 | ```py 167 | >>> def make_withdraw(balance): 168 | def withdraw(amount): 169 | nonlocal balance 170 | if amount > balance: 171 | print('Insufficient funds') 172 | else: 173 | balance = balance - amount 174 | print(balance) 175 | return withdraw 176 | ``` 177 | 178 | 现在想象一下,我们以 10 美元创建一个帐户,让我们想想,如果我们从帐户中提取太多的钱会发生什么。如果我们顺序执行这些交易,我们会收到资金不足的消息。 179 | 180 | ```py 181 | >>> w = make_withdraw(10) 182 | >>> w(8) 183 | 2 184 | >>> w(7) 185 | 'Insufficient funds' 186 | ``` 187 | 188 | 但是,在并行中可以有许多不同的结果。下面展示了一种可能性: 189 | 190 | ``` 191 | P1: w(8) P2: w(7) 192 | read balance: 10 193 | read amount: 8 read balance: 10 194 | 8 > 10: False read amount: 7 195 | if False 7 > 10: False 196 | 10 - 8: 2 if False 197 | write balance -> 2 10 - 7: 3 198 | read balance: 2 write balance -> 3 199 | print 2 read balance: 3 200 | print 3 201 | ``` 202 | 203 | 这个特殊的例子给出了一个不正确结果 3。就好像`w(8)`交易从来没有发生过。其他可能的结果是 2,和`'Insufficient funds'`。这个问题的根源是:如果`P2` 在`P1`写入值前读取余额,`P2`的状态是不一致的(反之亦然)。`P2`所读取的余额值是过时的,因为`P1`打算改变它。`P2`不知道,并且会用不一致的值覆盖它。 204 | 205 | 这个例子表明,并行化的代码不像把代码行分给多个处理器来执行那样容易。变量读写的顺序相当重要。 206 | 207 | 一个保证执行正确性的有吸引力的方式是,两个修改共享数据的程序不能同时执行。不幸的是,对于银行业这将意味着,一次只可以进行一个交易,因为所有的交易都修改共享数据。直观地说,我们明白,让 2 个不同的人同时进行完全独立的帐户交易应该没有问题。不知何故,这两个操作不互相干扰,但在同一帐户上的相同方式的同时操作就相互干扰。此外,当进程不读取或写入时,让它们同时运行就没有问题。 208 | 209 | ### 4.3.2 并行计算的正确性 210 | 211 | 并行计算环境中的正确性有两个标准。第一个是,结果应该总是相同。第二个是,结果应该和串行执行的结果一致。 212 | 213 | 第一个条件表明,我们必须避免在前面的章节中所示的变化,其中在不同的方式下的交叉读写会产生不同的结果。例子中,我们从 10 美元的帐户取出了`w(8)`和`w(7)`。这个条件表明,我们必须始终返回相同的答案,独立于`P1`和`P2`的指令执行顺序。无论如何,我们必须以这样一种方式来编写我们的程序,无论他们如何相互交叉,他们应该总是产生同样的结果。 214 | 215 | 第二个条件揭示了许多可能的结果中哪个是正确的。例子中,我们从 10 美元的帐户取出了`w(8)`和`w(7)`,这个条件表明结果必须总是余额不足,而不是 2 或者 3。 216 | 217 | 当一个进程在程序的临界区影响另一个进程时,并行计算中就会出现问题。这些都是需要执行的代码部分,它们看似是单一的指令,但实际上由较小的语句组成。一个程序会以一系列原子硬件指令执行,由于处理器的设计,这些是不能被打断或分割为更小单元的指令。为了在并行的情况下表现正确,程序代码的临界区需要具有原子性,保证他们不会被任何其他代码中断。 218 | 219 | 为了强制程序临界区在并发下的原子性,需要能够在重要的时刻将进程序列化或彼此同步。序列化意味着同一时间只运行一个进程 -- 这一瞬间就好像串行执行一样。同步有两种形式。首先是互斥,进程轮流访问一个变量。其次是条件同步,在满足条件(例如其他进程完成了它们的任务)之前进程一直等待,之后继续执行。这样,当一个程序即将进入临界区时,其他进程可以一直等待到它完成,然后安全地执行。 220 | 221 | ### 4.3.3 保护共享状态:锁和信号量 222 | 223 | 在本节中讨论的所有同步和序列化方法都使用相同的基本思想。它们在共享状态中将变量用作信号,所有过程都会理解并遵守它。这是一个相同的理念,允许分布式系统中的计算机协同工作 -- 它们通过传递消息相互协调,根据每一个参与者都理解和遵守的一个协议。 224 | 225 | 这些机制不是为了保护共享状态而出现的物理障碍。相反,他们是建立相互理解的基础上。和出现在十字路口的各种方向的车辆能够安全通行一样,是同一种相互理解。这里没有物理的墙壁阻止汽车相撞,只有遵守规则,红色意味着“停止”,绿色意味着“通行”。同样,没有什么可以保护这些共享变量,除非当一个特定的信号表明轮到某个进程了,进程才会访问它们。 226 | 227 | **锁。**锁,也被称为互斥体(`mutex`),是共享对象,常用于发射共享状态被读取或修改的信号。不同的编程语言实现锁的方式不同,但是在 Python 中,一个进程可以调用`acquire()`方法来尝试获得锁的“所有权”,然后在使用完共享变量的时候调用`release()`释放它。当进程获得了一把锁,任何试图执行`acquire()`操作的其他进程都会自动等待到锁被释放。这样,同一时间只有一个进程可以获得一把锁。 228 | 229 | 对于一把保护一组特定的变量的锁,所有的进程都需要编程来遵循一个规则:一个进程不拥有特定的锁就不能访问相应的变量。实际上,所有进程都需要在锁的`acquire()`和`release()`语句之间“包装”自己对共享变量的操作。 230 | 231 | 我们可以把这个概念用于银行余额的例子中。该示例的临界区是从余额读取到写入的一组操作。我们看到,如果一个以上的进程同时执行这个区域,问题就会发生。为了保护临界区,我们需要使用一把锁。我们把这把锁称为`balance_lock`(虽然我们可以命名为任何我们喜欢的名字)。为了锁定实际保护的部分,我们必须确保试图进入这部分时调用`acquire()`获取锁,以及之后调用`release()`释放锁,这样可以轮到别人。 232 | 233 | ```py 234 | >>> from threading import Lock 235 | >>> def make_withdraw(balance): 236 | balance_lock = Lock() 237 | def withdraw(amount): 238 | nonlocal balance 239 | # try to acquire the lock 240 | balance_lock.acquire() 241 | # once successful, enter the critical section 242 | if amount > balance: 243 | print("Insufficient funds") 244 | else: 245 | balance = balance - amount 246 | print(balance) 247 | # upon exiting the critical section, release the lock 248 | balance_lock.release() 249 | ``` 250 | 251 | 如果我们建立和之前一样的情形: 252 | 253 | ```py 254 | w = make_withdraw(10) 255 | ``` 256 | 257 | 现在就可以并行执行`w(8)`和`w(7)`了: 258 | 259 | ``` 260 | P1 P2 261 | acquire balance_lock: ok 262 | read balance: 10 acquire balance_lock: wait 263 | read amount: 8 wait 264 | 8 > 10: False wait 265 | if False wait 266 | 10 - 8: 2 wait 267 | write balance -> 2 wait 268 | read balance: 2 wait 269 | print 2 wait 270 | release balance_lock wait 271 | acquire balance_lock:ok 272 | read balance: 2 273 | read amount: 7 274 | 7 > 2: True 275 | if True 276 | print 'Insufficient funds' 277 | release balance_lock 278 | ``` 279 | 280 | 我们看到了,两个进程同时进入临界区是可能的。某个进程实例获取到了`balance_lock`,另一个就得等待,直到那个进程退出了临界区,它才能开始执行。 281 | 282 | 要注意程序不会自己终止,除非`P1`释放了`balance_lock`。如果它没有释放`balance_lock`,`P2`永远不可能获取它,而是一直会等待。忘记释放获得的锁是并行编程中的一个常见错误。 283 | 284 | **信号量。**信号量是用于维持有限资源访问的信号。它们和锁类似,除了它们可以允许某个限制下的多个访问。它就像电梯一样只能够容纳几个人。一旦达到了限制,想要使用资源的进程就必须等待。其它进程释放了信号量之后,它才可以获得。 285 | 286 | 例如,假设有许多进程需要读取中心数据库服务器的数据。如果过多的进程同时访问它,它就会崩溃,所以限制连接数量就是个好主意。如果数据库只能同时支持`N=2`的连接,我们就可以以初始值`N=2`来创建信号量。 287 | 288 | ```py 289 | >>> from threading import Semaphore 290 | >>> db_semaphore = Semaphore(2) # set up the semaphore 291 | >>> database = [] 292 | >>> def insert(data): 293 | db_semaphore.acquire() # try to acquire the semaphore 294 | database.append(data) # if successful, proceed 295 | db_semaphore.release() # release the semaphore 296 | >>> insert(7) 297 | >>> insert(8) 298 | >>> insert(9) 299 | ``` 300 | 301 | 信号量的工作机制是,所有进程只在获取了信号量之后才可以访问数据库。只有`N=2`个进程可以获取信号量,其它的进程都需要等到其中一个进程释放了信号量,之后在访问数据库之前尝试获取它。 302 | 303 | ``` 304 | P1 P2 P3 305 | acquire db_semaphore: ok acquire db_semaphore: wait acquire db_semaphore: ok 306 | read data: 7 wait read data: 9 307 | append 7 to database wait append 9 to database 308 | release db_semaphore: ok acquire db_semaphore: ok release db_semaphore: ok 309 | read data: 8 310 | append 8 to database 311 | release db_semaphore: ok 312 | ``` 313 | 314 | 值为 1 的信号量的行为和锁一样。 315 | 316 | ### 4.3.4 保持同步:条件变量 317 | 318 | 条件变量在并行计算由一系列步骤组成时非常有用。进程可以使用条件变量,来用信号告知它完成了特定的步骤。之后,等待信号的其它进程就会开始它们的任务。一个需要逐步计算的例子就是大规模向量序列的计算。在计算生物学,Web 范围的计算,和图像处理及图形学中,常常需要处理非常大型(百万级元素)的向量和矩阵。想象下面的计算: 319 | 320 | ![](img/vector-math1.png) 321 | 322 | 我们可以通过将矩阵和向量按行拆分,并把每一行分配到单独的线程上,来并行处理每一步。作为上面的计算的一个实例,想象下面的简单值: 323 | 324 | ![](img/vector-math2.png) 325 | 326 | 我们将前一半(这里是第一行)分配给一个线程,后一半(第二行)分配给另一个线程: 327 | 328 | ![](img/vector-math3.png) 329 | 330 | 在伪代码中,计算是这样的: 331 | 332 | ```py 333 | def do_step_1(index): 334 | A[index] = B[index] + C[index] 335 | 336 | def do_step_2(index): 337 | V[index] = M[index] . A 338 | ``` 339 | 340 | 进程 1 执行了: 341 | 342 | ```py 343 | do_step_1(1) 344 | do_step_2(1) 345 | ``` 346 | 347 | 进程 2 执行了: 348 | 349 | ```py 350 | do_step_1(2) 351 | do_step_2(2) 352 | ``` 353 | 354 | 如果允许不带同步处理,就造成下面的不一致性: 355 | 356 | ```py 357 | P1 P2 358 | read B1: 2 359 | read C1: 0 360 | calculate 2+0: 2 361 | write 2 -> A1 read B2: 0 362 | read M1: (1 2) read C2: 5 363 | read A: (2 0) calculate 5+0: 5 364 | calculate (1 2).(2 0): 2 write 5 -> A2 365 | write 2 -> V1 read M2: (1 2) 366 | read A: (2 5) 367 | calculate (1 2).(2 5):12 368 | write 12 -> V2 369 | ``` 370 | 371 | 问题就是`V`直到所有元素计算出来时才会计算出来。但是,`P1`在`A`的所有元素计算出来之前,完成`A = B+C`并且移到`V = MA`。所以它与`M`相乘时使用了`A`的不一致的值。 372 | 373 | 我们可以使用条件变量来解决这个问题。 374 | 375 | **条件变量**是表现为信号的对象,信号表示某个条件被满足。它们通常被用于协调进程,这些进程需要在继续执行之前等待一些事情的发生。需要满足一定条件的进程可以等待一个条件变量,直到其它进程修改了条件变量来告诉它们继续执行。 376 | 377 | Python 中,任何数量的进程都可以使用`condition.wait()`方法,用信号告知它们正在等待某个条件。在调用该方法之后,它们会自动等待到其它进程调用了`condition.notify()`或`condition.notifyAll()`函数。`notify()`方法值唤醒一个进程,其它进程仍旧等待。`notifyAll()`方法唤醒所有等待中的进程。每个方法在不同情形中都很实用。 378 | 379 | 由于条件变量通常和决定条件是否为真的共享变量相联系,它们也提供了`acquire()`和`release()`方法。这些方法应该在修改可能改变条件状态的变量时使用。任何想要用信号告知条件已经改变的进程,必须首先使用`acquire()`来访问它。 380 | 381 | 在我们的例子中,在执行第二步之前必须满足的条件是,两个进程都必须完成了第一步。我们可以跟踪已经完成第一步的进程数量,以及条件是否被满足,通过引入下面两个变量: 382 | 383 | ```py 384 | step1_finished = 0 385 | start_step2 = Condition() 386 | ``` 387 | 388 | 我们在`do_step_2`的开头插入`start_step_2().wait()`。每个进程都会在完成步骤 1 之后自增`step1_finished`,但是我们只会在`step_1_finished = 2`时发送信号。下面的伪代码展示了它: 389 | 390 | ```py 391 | step1_finished = 0 392 | start_step2 = Condition() 393 | 394 | def do_step_1(index): 395 | A[index] = B[index] + C[index] 396 | # access the shared state that determines the condition status 397 | start_step2.acquire() 398 | step1_finished += 1 399 | if(step1_finished == 2): # if the condition is met 400 | start_step2.notifyAll() # send the signal 401 | #release access to shared state 402 | start_step2.release() 403 | 404 | def do_step_2(index): 405 | # wait for the condition 406 | start_step2.wait() 407 | V[index] = M[index] . A 408 | ``` 409 | 410 | 在引入条件变量之后,两个进程会一起进入步骤 2,像下面这样: 411 | 412 | ``` 413 | P1 P2 414 | read B1: 2 415 | read C1: 0 416 | calculate 2+0: 2 417 | write 2 -> A1 read B2: 0 418 | acquire start_step2: ok read C2: 5 419 | write 1 -> step1_finished calculate 5+0: 5 420 | step1_finished == 2: false write 5-> A2 421 | release start_step2: ok acquire start_step2: ok 422 | start_step2: wait write 2-> step1_finished 423 | wait step1_finished == 2: true 424 | wait notifyAll start_step_2: ok 425 | start_step2: ok start_step2:ok 426 | read M1: (1 2) read M2: (1 2) 427 | read A:(2 5) 428 | calculate (1 2). (2 5): 12 read A:(2 5) 429 | write 12->V1 calculate (1 2). (2 5): 12 430 | write 12->V2 431 | ``` 432 | 433 | 在进入`do_step_2`的时候,`P1`需要在`start_step_2`之前等待,直到`P2`自增了`step1_finished`,发现了它等于 2,之后向条件发送信号。 434 | 435 | ### 4.3.5 死锁 436 | 437 | 虽然同步方法对保护共享状态十分有效,但它们也带来了麻烦。因为它们会导致一个进程等待另一个进程,这些进程就有**死锁**的风险。死锁是一种情形,其中两个或多个进程被卡住,互相等待对方完成。我们已经提到了忘记释放某个锁如何导致进程无限卡住。但是即使`acquire()`和`release()`调用的数量正确,程序仍然会构成死锁。 438 | 439 | 死锁的来源是**循环等待**,像下面展示的这样。没有进程能够继续执行,因为它们正在等待其它进程,而其它进程也在等待它完成。 440 | 441 | ![](img/deadlock.png) 442 | 443 | 作为一个例子,我们会建立两个进程的死锁。假设有两把锁,`x_lock`和`y_lock`,并且它们像这样使用: 444 | 445 | ```py 446 | >>> x_lock = Lock() 447 | >>> y_lock = Lock() 448 | >>> x = 1 449 | >>> y = 0 450 | >>> def compute(): 451 | x_lock.acquire() 452 | y_lock.acquire() 453 | y = x + y 454 | x = x * x 455 | y_lock.release() 456 | x_lock.release() 457 | >>> def anti_compute(): 458 | y_lock.acquire() 459 | x_lock.acquire() 460 | y = y - x 461 | x = sqrt(x) 462 | x_lock.release() 463 | y_lock.release() 464 | ``` 465 | 466 | 如果`compute()`和`anti_compute()`并行执行,并且恰好像下面这样互相交错: 467 | 468 | ``` 469 | P1 P2 470 | acquire x_lock: ok acquire y_lock: ok 471 | acquire y_lock: wait acquire x_lock: wait 472 | wait wait 473 | wait wait 474 | wait wait 475 | ... ... 476 | ``` 477 | 478 | 所产生的情形就是死锁。`P1`和`P2`每个都持有一把锁,但是它们需要两把锁来执行。`P1`正在等待`P2`释放`y_lock`,而`P2`正在等待`P1`释放`x_lock`。所以,没有进程能够继续执行。 479 | -------------------------------------------------------------------------------- /ch5.md: -------------------------------------------------------------------------------- 1 | # 第五章 序列和协程 2 | 3 | > 来源:[Chapter 5: Sequences and Coroutines](http://www-inst.eecs.berkeley.edu/~cs61a/sp12/book/streams.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) 8 | 9 | ## 5.1 引言 10 | 11 | 在这一章中,我们通过开发新的工具来处理有序数据,继续讨论真实世界中的应用。在第二章中,我们介绍了序列接口,在 Python 内置的数据类型例如`tuple`和`list`中实现。序列支持两个操作:获取长度和由下标访问元素。第三章中,我们开发了序列接口的用户定义实现,用于表示递归列表的`Rlist`类。序列类型具有高效的表现力,并且可以让我们高效访问大量有序数据集。 12 | 13 | 但是,使用序列抽象表示有序数据有两个重要限制。第一个是长度为`n`的序列的要占据比例为`n`的内存总数。于是,序列越长,表示它所占的内存空间就越大。 14 | 15 | 第二个限制是,序列只能表示已知且长度有限的数据集。许多我们想要表示的有序集合并没有定义好的长度,甚至有些是无限的。两个无限序列的数学示例是正整数和斐波那契数。无限长度的有序数据集也出现在其它计算领域,例如,所有推特状态的序列每秒都在增长,所以并没有固定的长度。与之类似,经过基站发送出的电话呼叫序列,由计算机用户发出的鼠标动作序列,以及飞机上的传感器产生的加速度测量值序列,都在世界演化过程中无限扩展。 16 | 17 | 在这一章中,我们介绍了新的构造方式用于处理有序数据,它为容纳未知或无限长度的集合而设计,但仅仅使用有限的内存。我们也会讨论这些工具如何用于一种叫做协程的程序结构,来创建高效、模块化的数据处理流水线。 18 | 19 | ## 5.2 隐式序列 20 | 21 | 序列可以使用一种程序结构来表示,它不将每个元素显式储存在内存中,这是高效处理有序数据的核心概念。为了将这个概念用于实践,我们需要构造对象来提供序列中所有元素的访问,但是不要事先把所有元素计算出来并储存。 22 | 23 | 这个概念的一个简单示例就是第二章出现的`range`序列类型。`range`表示连续有界的整数序列。但是,它的每个元素并不显式在内存中表示,当元素从`range`中获取时,才被计算出来。所以,我们可以表示非常大的整数范围。只有范围的结束位置才被储存为`range`对象的一部分,元素都被凭空计算出来。 24 | 25 | ```py 26 | >>> r = range(10000, 1000000000) 27 | >>> r[45006230] 28 | 45016230 29 | ``` 30 | 31 | 这个例子中,当构造范围示例时,并不是这个范围内的所有 999,990,000 个整数都被储存。反之,范围对象将第一个元素 10,000 与下标相加 45,006,230 来产生第 45,016,230 个元素。计算所求的元素值并不从现有的表示中获取,这是惰性计算的一个例子。计算机科学将惰性作为一种重要的计算工具加以赞扬。 32 | 33 | 迭代器是提供底层有序数据集的有序访问的对象。迭代器在许多编程语言中都是内建对象,包括 Python。迭代器抽象拥有两个组成部分:一种获取底层元素序列的下一个元素的机制,以及一种标识元素序列已经到达末尾,没有更多剩余元素的机制。在带有内建对象系统的编程语言中,这个抽象通常相当于可以由类实现的特定接口。Python 的迭代器接口会在下一节中描述。 34 | 35 | 迭代器的实用性来源于一个事实,底层数据序列并不能显式在内存中表达。迭代器提供了一种机制,可以依次计算序列中的每个值,但是所有元素不需要连续储存。反之,当下个元素从迭代器获取的时候,这个元素会按照请求计算,而不是从现有的内存来源中获取。 36 | 37 | 范围可以惰性计算序列中的元素,因为序列的表示是统一的,并且任何元素都可以轻易从范围的起始和结束位置计算出来。迭代器支持更广泛的底层有序数据集的惰性生成,因为它们不需要提供底层序列任意元素的访问途径。反之,它们仅仅需要按照顺序,在每次其它元素被请求的时候,计算出序列的下一个元素。虽然不像序列可访问任意元素那样灵活(叫做随机访问),有序数据序列的顺序访问对于数据处理应用来说已经足够了。 38 | 39 | ### 5.2.1 Python 迭代器 40 | 41 | Python 迭代器接口包含两个消息。`__next__`消息向迭代器获取所表示的底层序列的下一个元素。为了对`__next__`方法调用做出回应,迭代器可以执行任何计算来获取或计算底层数据序列的下一个元素。`__next__`的调用让迭代器产生变化:它们向前移动迭代器的位置。所以多次调用`__next__`会有序返回底层序列的元素。在`__next__`的调用过程中,Python 通过`StopIteration`异常,来表示底层数据序列已经到达末尾。 42 | 43 | 下面的`Letters`类迭代了从`a`到`d`字母的底层序列。成员变量`current`储存了序列中的当前字母。`__next__`方法返回这个字母,并且使用它来计算`current`的新值。 44 | 45 | ```py 46 | >>> class Letters(object): 47 | def __init__(self): 48 | self.current = 'a' 49 | def __next__(self): 50 | if self.current > 'd': 51 | raise StopIteration 52 | result = self.current 53 | self.current = chr(ord(result)+1) 54 | return result 55 | def __iter__(self): 56 | return self 57 | ``` 58 | 59 | `__iter__`消息是 Python 迭代器所需的第二个消息。它只是简单返回迭代器,它对于提供迭代器和序列的通用接口很有用,在下一节会描述。 60 | 61 | 使用这个类,我们就可以访问序列中的字母: 62 | 63 | ```py 64 | >>> letters = Letters() 65 | >>> letters.__next__() 66 | 'a' 67 | >>> letters.__next__() 68 | 'b' 69 | >>> letters.__next__() 70 | 'c' 71 | >>> letters.__next__() 72 | 'd' 73 | >>> letters.__next__() 74 | Traceback (most recent call last): 75 | File "", line 1, in 76 | File "", line 12, in next 77 | StopIteration 78 | ``` 79 | 80 | `Letters`示例只能迭代一次。一旦`__next__()`方法产生了`StopIteration`异常,它就从此之后一直这样了。除非创建新的实例,否则没有办法来重置它。 81 | 82 | 迭代器也允许我们表示无限序列,通过实现永远不会产生`StopIteration`异常的`__next__`方法。例如,下面展示的`Positives`类迭代了正整数的无限序列: 83 | 84 | ```py 85 | >>> class Positives(object): 86 | def __init__(self): 87 | self.current = 0; 88 | def __next__(self): 89 | result = self.current 90 | self.current += 1 91 | return result 92 | def __iter__(self): 93 | return self 94 | ``` 95 | 96 | ### 5.2.2 `for`语句 97 | 98 | Python 中,序列可以通过实现`__iter__`消息用于迭代。如果一个对象表示有序数据,它可以在`for`语句中用作可迭代对象,通过回应`__iter__`消息来返回迭代器。这个迭代器应拥有`__next__()`方法,依次返回序列中的每个元素,最后到达序列末尾时产生`StopIteration`异常。 99 | 100 | ```py 101 | >>> counts = [1, 2, 3] 102 | >>> for item in counts: 103 | print(item) 104 | 1 105 | 2 106 | 3 107 | ``` 108 | 109 | 在上面的实例中,`counts`列表返回了迭代器,作为`__iter__()`方法调用的回应。`for`语句之后反复调用迭代器的`__next__()`方法,并且每次都将返回值赋给`item`。这个过程一直持续,直到迭代器产生了`StopIteration`异常,这时`for`语句就终止了。 110 | 111 | 使用我们关于迭代器的知识,我们可以拿`while`、赋值和`try`语句实现`for`语句的求值规则: 112 | 113 | ```py 114 | >>> i = counts.__iter__() 115 | >>> try: 116 | while True: 117 | item = i.__next__() 118 | print(item) 119 | except StopIteration: 120 | pass 121 | 1 122 | 2 123 | 3 124 | ``` 125 | 126 | 在上面,调用`counts`的`__iter__`方法所返回的迭代器绑定到了名称`i`上面,便于依次获取每个元素。`StopIteration`异常的处理子句不做任何事情,但是这个异常的处理提供了退出`while`循环的控制机制。 127 | 128 | ### 5.2.3 生成器和`yield`语句 129 | 130 | 上面的`Letters`和`Positives`对象需要我们引入一种新的字段,`self.current`,来跟踪序列的处理过程。在上面所示的简单序列中,这可以轻易实现。但对于复杂序列,`__next__()`很难在计算中节省空间。生成器允许我们通过利用 Python 解释器的特性定义更复杂的迭代。 131 | 132 | 生成器是由一类特殊函数,叫做生成器函数返回的迭代器。生成器函数不同于普通的函数,因为它不在函数体中包含`return`语句,而是使用`yield`语句来返回序列中的元素。 133 | 134 | 生成器不使用任何对象属性来跟踪序列的处理过程。它们控制生成器函数的执行,每次`__next__`方法调用时,它们执行到下一个`yield`语句。`Letters`迭代可以使用生成器函数实现得更加简洁。 135 | 136 | ```py 137 | >>> def letters_generator(): 138 | current = 'a' 139 | while current <= 'd': 140 | yield current 141 | current = chr(ord(current)+1) 142 | >>> for letter in letters_generator(): 143 | print(letter) 144 | a 145 | b 146 | c 147 | d 148 | ``` 149 | 150 | 即使我们永不显式定义`__iter__()`或`__next__()`方法,Python 会理解当我们使用`yield`语句时,我们打算定义生成器函数。调用时,生成器函数并不返回特定的产出值,而是返回一个生成器(一种迭代器),它自己就可以返回产出的值。生成器对象拥有`__iter__`和`__next__`方法,每个对`__next__`的调用都会从上次停留的地方继续执行生成器函数,直到另一个`yield`语句执行的地方。 151 | 152 | `__next__`第一次调用时,程序从`letters_generator`的函数体一直执行到进入`yield`语句。之后,它暂停并返回`current`值。`yield`语句并不破坏新创建的环境,而是为之后的使用保留了它。当`__next__`再次调用时,执行在它停留的地方恢复。`letters_generator`作用域中`current`和任何所绑定名称的值都会在随后的`__next__`调用中保留。 153 | 154 | 我们可以通过手动调用`__next__()`来遍历生成器: 155 | 156 | ```py 157 | >>> letters = letters_generator() 158 | >>> type(letters) 159 | 160 | >>> letters.__next__() 161 | 'a' 162 | >>> letters.__next__() 163 | 'b' 164 | >>> letters.__next__() 165 | 'c' 166 | >>> letters.__next__() 167 | 'd' 168 | >>> letters.__next__() 169 | Traceback (most recent call last): 170 | File "", line 1, in 171 | StopIteration 172 | ``` 173 | 174 | 在第一次`__next__()`调用之前,生成器并不会开始执行任何生成器函数体中的语句。 175 | 176 | ### 5.2.4 可迭代对象 177 | 178 | Python 中,迭代只会遍历一次底层序列的元素。在遍历之后,迭代器在`__next__()`调用时会产生`StopIteration`异常。许多应用需要迭代多次元素。例如,我们需要对一个列表迭代多次来枚举所有的元素偶对: 179 | 180 | 181 | ```py 182 | >>> def all_pairs(s): 183 | for item1 in s: 184 | for item2 in s: 185 | yield (item1, item2) 186 | >>> list(all_pairs([1, 2, 3])) 187 | [(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)] 188 | ``` 189 | 190 | 序列本身不是迭代器,但是它是可迭代对象。Python 的可迭代接口只包含一个消息,`__iter__`,返回一个迭代器。Python 中内建的序列类型在`__iter__`方法调用时,返回迭代器的新实例。如果一个可迭代对象在每次调用`__iter__`时返回了迭代器的新实例,那么它就能被迭代多次。 191 | 192 | 新的可迭代类可以通过实现可迭代接口来定义。例如,下面的可迭代对象`LetterIterable`类在每次调用`__iter__`时返回新的迭代器来迭代字母。 193 | 194 | ```py 195 | >>> class LetterIterable(object): 196 | def __iter__(self): 197 | current = 'a' 198 | while current <= 'd': 199 | yield current 200 | current = chr(ord(current)+1) 201 | ``` 202 | 203 | `__iter__`方法是个生成器函数,它返回一个生成器对象,产出从`'a'`到`'d'`的字母。 204 | 205 | `Letters`迭代器对象在单次迭代之后就被“用完”了,但是`LetterIterable`对象可被迭代多次。所以,`LetterIterable`示例可以用于`all_pairs`的参数。 206 | 207 | ```py 208 | >>> letters = LetterIterable() 209 | >>> all_pairs(letters).__next__() 210 | ('a', 'a') 211 | ``` 212 | 213 | ### 5.2.5 流 214 | 215 | 流提供了一种隐式表示有序数据的最终方式。流是惰性计算的递归列表。就像第三章的`Rlist`类那样,`Stream`实例可以响应对其第一个元素和剩余部分的获取请求。同样,`Stream`的剩余部分还是`Stream`。然而不像`RList`,流的剩余部分只在查找时被计算,而不是事先存储。也就是说流的剩余部分是惰性计算的。 216 | 217 | 为了完成这个惰性求值,流会储存计算剩余部分的函数。无论这个函数在什么时候调用,它的返回值都作为流的一部分,储存在叫做`_rest`的属性中。下划线表示它不应直接访问。可访问的属性`rest`是个方法,它返回流的剩余部分,并在必要时计算它。使用这个设计,流可以储存计算剩余部分的方式,而不用总是显式储存它们。 218 | 219 | ```py 220 | >>> class Stream(object): 221 | """A lazily computed recursive list.""" 222 | def __init__(self, first, compute_rest, empty=False): 223 | self.first = first 224 | self._compute_rest = compute_rest 225 | self.empty = empty 226 | self._rest = None 227 | self._computed = False 228 | @property 229 | def rest(self): 230 | """Return the rest of the stream, computing it if necessary.""" 231 | assert not self.empty, 'Empty streams have no rest.' 232 | if not self._computed: 233 | self._rest = self._compute_rest() 234 | self._computed = True 235 | return self._rest 236 | def __repr__(self): 237 | if self.empty: 238 | return '' 239 | return 'Stream({0}, )'.format(repr(self.first)) 240 | >>> Stream.empty = Stream(None, None, True) 241 | ``` 242 | 243 | 递归列表可使用嵌套表达式来定义。例如,我们可以创建`RList`,来表达`1`和`5`的序列,像下面这样: 244 | 245 | ```py 246 | >>> r = Rlist(1, Rlist(2+3, Rlist.empty)) 247 | ``` 248 | 249 | 与之类似,我们可以创建一个`Stream`来表示相同序列。`Stream`在请求剩余部分之前,并不会实际计算下一个元素`5`。 250 | 251 | ```py 252 | >>> s = Stream(1, lambda: Stream(2+3, lambda: Stream.empty)) 253 | ``` 254 | 255 | 这里,`1`是流的第一个元素,后面的`lambda`表达式是用于计算流的剩余部分的函数。被计算的流的第二个元素又是一个返回空流的函数。 256 | 257 | 访问递归列表`r`和流`s`中的元素拥有相似的过程。但是,`5`储存在了`r`之中,而对于`s`来说,它在首次被请求时通过加法来按要求计算。 258 | 259 | ```py 260 | >>> r.first 261 | 1 262 | >>> s.first 263 | 1 264 | >>> r.rest.first 265 | 5 266 | >>> s.rest.first 267 | 5 268 | >>> r.rest 269 | Rlist(5) 270 | >>> s.rest 271 | Stream(5, ) 272 | ``` 273 | 274 | 当`make_integer_stream`首次被调用时,它返回了一个流,流的`first`是序列中第一个整数(默认为`1`)。但是,`make_integer_stream`实际是递归的,因为这个流的`compute_rest`以自增的参数再次调用了`make_integer_stream`。这会让`make_integer_stream`变成递归的,同时也是惰性的。 275 | 276 | ```py 277 | >>> ints.first 278 | 1 279 | >>> ints.rest.first 280 | 2 281 | >>> ints.rest.rest 282 | Stream(3, ) 283 | ``` 284 | 285 | 无论何时请求整数流的`rest`,都仅仅递归调用`make_integer_stream`。 286 | 287 | 操作序列的相同高阶函数 -- `map`和`filter` -- 同样可应用于流,虽然它们的实现必须修改来惰性调用它们的参数函数。`map_stream`在一个流上映射函数,这会产生一个新的流。局部定义的`compute_rest`函数确保了无论什么时候`rest`被计算出来,这个函数都会在流的剩余部分上映射。 288 | 289 | ```py 290 | >>> def map_stream(fn, s): 291 | if s.empty: 292 | return s 293 | def compute_rest(): 294 | return map_stream(fn, s.rest) 295 | return Stream(fn(s.first), compute_rest) 296 | ``` 297 | 298 | 流可以通过定义`compute_rest`函数来过滤,这个函数在流的剩余部分上调用过滤器函数。如果过滤器函数拒绝了流的第一个元素,剩余部分会立即计算出来。因为`filter_stream`是递归的,剩余部分可能会多次计算直到找到了有效的`first`元素。 299 | 300 | ```py 301 | >>> def filter_stream(fn, s): 302 | if s.empty: 303 | return s 304 | def compute_rest(): 305 | return filter_stream(fn, s.rest) 306 | if fn(s.first): 307 | return Stream(s.first, compute_rest) 308 | return compute_rest() 309 | ``` 310 | 311 | `map_stream`和`filter_stream`展示了流式处理的常见模式:无论流的剩余部分何时被计算,局部定义的`compute_rest`函数都会对流的剩余部分递归调用某个处理函数。 312 | 313 | 为了观察流的内容,我们需要将其截断为有限长度,并转换为 Python `list`。 314 | 315 | ```py 316 | >>> def truncate_stream(s, k): 317 | if s.empty or k == 0: 318 | return Stream.empty 319 | def compute_rest(): 320 | return truncate_stream(s.rest, k-1) 321 | return Stream(s.first, compute_rest) 322 | >>> def stream_to_list(s): 323 | r = [] 324 | while not s.empty: 325 | r.append(s.first) 326 | s = s.rest 327 | return r 328 | ``` 329 | 330 | 这些便利的函数允许我们验证`map_stream`的实现,使用一个非常简单的例子,从`3`到`7`的整数平方。 331 | 332 | ```py 333 | >>> s = make_integer_stream(3) 334 | >>> s 335 | Stream(3, ) 336 | >>> m = map_stream(lambda x: x*x, s) 337 | >>> m 338 | Stream(9, ) 339 | >>> stream_to_list(truncate_stream(m, 5)) 340 | [9, 16, 25, 36, 49] 341 | ``` 342 | 343 | 我们可以使用我们的`filter_stream`函数来定义素数流,使用埃拉托斯特尼筛法(sieve of Eratosthenes),它对整数流进行过滤,移除第一个元素的所有倍数数值。通过成功过滤出每个素数,所有合数都从流中移除了。 344 | 345 | ```py 346 | >>> def primes(pos_stream): 347 | def not_divible(x): 348 | return x % pos_stream.first != 0 349 | def compute_rest(): 350 | return primes(filter_stream(not_divible, pos_stream.rest)) 351 | return Stream(pos_stream.first, compute_rest) 352 | ``` 353 | 354 | 通过截断`primes`流,我们可以枚举素数的任意前缀: 355 | 356 | ```py 357 | >>> p1 = primes(make_integer_stream(2)) 358 | >>> stream_to_list(truncate_stream(p1, 7)) 359 | [2, 3, 5, 7, 11, 13, 17] 360 | ``` 361 | 362 | 流和迭代器不同,因为它们可以多次传递给纯函数,并且每次都产生相同的值。素数流并没有在转换为列表之后“用完”。也就是说,在将流的前缀转换为列表之后,`p1`的第一个元素仍旧是`2`。 363 | 364 | ```py 365 | >>> p1.first 366 | 2 367 | ``` 368 | 369 | 就像递归列表提供了序列抽象的简单实现,流提供了简单、函数式的递归数据结构,它通过高阶函数的使用实现了惰性求值。 370 | 371 | ## 5.3 协程 372 | 373 | 这篇文章的大部分专注于将复杂程序解构为小型、模块化组件的技巧。当一个带有复杂行为的函数逻辑划分为几个独立的、本身为函数的步骤时,这些函数叫做辅助函数或者子过程。子过程由主函数调用,主函数负责协调子函数的使用。 374 | 375 | ![](img/subroutine.png) 376 | 377 | 这一节中,我们使用协程,引入了一种不同的方式来解构复杂的计算。它是一种针对有序数据的任务处理方式。就像子过程那样,协程会计算复杂计算的一小步。但是,在使用协程时,没有主函数来协调结果。反之,协程会自发链接到一起来组成流水线。可能有一些协程消耗输入数据,并把它发送到其它协程。也可能有一些协程,每个都对发送给它的数据执行简单的处理步骤。最后可能有另外一些协程输出最终结果。 378 | 379 | ![](img/coroutine.png) 380 | 381 | 协程和子过程的差异是概念上的:子过程在主函数中位于下级,但是协程都是平等的,它们协作组成流水线,不带有任何上级函数来负责以特定顺序调用它们。 382 | 383 | 这一节中,我们会学到 Python 如何通过`yield`和`send()`语句来支持协程的构建。之后,我们会看到协程在流水线中的不同作用,以及协程如何支持多任务。 384 | 385 | ### 5.3.1 Python 协程 386 | 387 | 在之前一节中,我们介绍了生成器函数,它使用`yield`来返回一个值。Python 的生成器函数也可以使用`(yield)`语句来接受一个值。生成器对象上有两个额外的方法:`send()`和`close()`,创建了一个模型使对象可以消耗或产出值。定义了这些对象的生成器函数叫做协程。 388 | 389 | 协程可以通过`(yield)`语句来消耗值,向像下面这样: 390 | 391 | ```py 392 | value = (yield) 393 | ``` 394 | 395 | 使用这个语法,在带参数调用对象的`send`方法之前,执行流会停留在这条语句上。 396 | 397 | ```py 398 | coroutine.send(data) 399 | ``` 400 | 401 | 之后,执行会恢复,`value`会被赋为`data`的值。为了发射计算终止的信号,我们需要使用`close()`方法来关闭协程。这会在协程内部产生`GeneratorExit`异常,它可以由`try/except`子句来捕获。 402 | 403 | 下面的例子展示了这些概念。它是一个协程,用于打印匹配所提供的模式串的字符串。 404 | 405 | ```py 406 | >>> def match(pattern): 407 | print('Looking for ' + pattern) 408 | try: 409 | while True: 410 | s = (yield) 411 | if pattern in s: 412 | print(s) 413 | except GeneratorExit: 414 | print("=== Done ===") 415 | ``` 416 | 417 | 我们可以使用一个模式串来初始化它,之后调用`__next__()`来开始执行: 418 | 419 | ```py 420 | >>> m = match("Jabberwock") 421 | >>> m.__next__() 422 | Looking for Jabberwock 423 | ``` 424 | 425 | 对`__next__()`的调用会执行函数体,所以`"Looking for jabberwock"`会被打印。语句会一直持续执行,直到遇到`line = (yield)`语句。之后,执行会暂停,并且等待一个发送给`m`的值。我们可以使用`send`来将值发送给它。 426 | 427 | ```py 428 | >>> m.send("the Jabberwock with eyes of flame") 429 | the Jabberwock with eyes of flame 430 | >>> m.send("came whiffling through the tulgey wood") 431 | >>> m.send("and burbled as it came") 432 | >>> m.close() 433 | === Done === 434 | ``` 435 | 436 | 当我们以一个值调用`m.send`时,协程`m`内部的求值会在`line = (yield)`语句处恢复,这里会把发送的值赋给`line`变量。`m`中的语句会继续求值,如果匹配的话会打印出那一行,并继续执行循环,直到再次进入`line = (yield)`。之后,`m`中的求值会暂停,并在`m.send`调用后恢复。 437 | 438 | 我们可以将使用`send()`和`yield`的函数链到一起来完成复杂的行为。例如,下面的函数将名为`text`的字符串分割为单词,并把每个单词发送给另一个协程。 439 | 440 | 每个单词都发送给了绑定到`next_coroutine`的协程,使`next_coroutine`开始执行,而且这个函数暂停并等待。它在`next_coroutine`暂停之前会一直等待,随后这个函数恢复执行,发送下一个单词或执行完毕。 441 | 442 | 如果我们将上面定义的`match`和这个函数链到一起,我们就可以创建出一个程序,只打印出匹配特定单词的单词。 443 | 444 | ```py 445 | >>> text = 'Commending spending is offending to people pending lending!' 446 | >>> matcher = match('ending') 447 | >>> matcher.__next__() 448 | Looking for ending 449 | >>> read(text, matcher) 450 | Commending 451 | spending 452 | offending 453 | pending 454 | lending! 455 | === Done === 456 | ``` 457 | 458 | `read`函数向协程`matcher`发送每个单词,协程打印出任何匹配`pattern`的输入。在`matcher`协程中,`s = (yield)`一行等待每个发送进来的单词,并且在执行到这一行之后将控制流交还给`read`。 459 | 460 | ![](img/read-match-coroutine.png) 461 | 462 | ### 5.3.2 生产、过滤和消耗 463 | 464 | 协程基于如何使用`yield`和`send()`而具有不同的作用: 465 | 466 | ![](img/produce-filter-consume.png) 467 | 468 | + **生产者**创建序列中的物品,并使用`send()`,而不是`(yield)`。 469 | + **过滤器**使用`(yield)`来消耗物品并将结果使用`send()`发送给下一个步骤。 470 | + **消费者**使用`(yield)`来消耗物品,但是从不发送。 471 | 472 | 上面的`read`函数是一个生产者的例子。它不使用`(yield)`,但是使用`send`来生产数据。函数`match`是个消费者的例子。它不使用`send`发送任何东西,但是使用`(yield)`来消耗数据。我们可以将`match`拆分为过滤器和消费者。过滤器是一个协程,只发送与它的模式相匹配的字符串。 473 | 474 | ```py 475 | >>> def match_filter(pattern, next_coroutine): 476 | print('Looking for ' + pattern) 477 | try: 478 | while True: 479 | s = (yield) 480 | if pattern in s: 481 | next_coroutine.send(s) 482 | except GeneratorExit: 483 | next_coroutine.close() 484 | ``` 485 | 486 | 消费者是一个函数,只打印出发送给它的行: 487 | 488 | ```py 489 | >>> def print_consumer(): 490 | print('Preparing to print') 491 | try: 492 | while True: 493 | line = (yield) 494 | print(line) 495 | except GeneratorExit: 496 | print("=== Done ===") 497 | ``` 498 | 499 | 当过滤器或消费者被构建时,必须调用它的`__next__`方法来开始执行: 500 | 501 | ```py 502 | >>> printer = print_consumer() 503 | >>> printer.__next__() 504 | Preparing to print 505 | >>> matcher = match_filter('pend', printer) 506 | >>> matcher.__next__() 507 | Looking for pend 508 | >>> read(text, matcher) 509 | spending 510 | pending 511 | === Done === 512 | ``` 513 | 514 | 即使名称`filter`暗示移除元素,过滤器也可以转换元素。下面的函数是个转换元素的过滤器的示例。它消耗字符串并发送一个字典,包含了每个不同的字母在字符串中的出现次数。 515 | 516 | ```py 517 | >>> def count_letters(next_coroutine): 518 | try: 519 | while True: 520 | s = (yield) 521 | counts = {letter:s.count(letter) for letter in set(s)} 522 | next_coroutine.send(counts) 523 | except GeneratorExit as e: 524 | next_coroutine.close() 525 | ``` 526 | 527 | 我们可以使用它来计算文本中最常出现的字母,并使用一个消费者,将字典合并来找出最常出现的键。 528 | 529 | ```py 530 | >>> def sum_dictionaries(): 531 | total = {} 532 | try: 533 | while True: 534 | counts = (yield) 535 | for letter, count in counts.items(): 536 | total[letter] = count + total.get(letter, 0) 537 | except GeneratorExit: 538 | max_letter = max(total.items(), key=lambda t: t[1])[0] 539 | print("Most frequent letter: " + max_letter) 540 | ``` 541 | 542 | 为了在文件上运行这个流水线,我们必须首先按行读取文件。之后,将结果发送给`count_letters`,最后发送给`sum_dictionaries`。我们可以服用`read`协程来读取文件中的行。 543 | 544 | ```py 545 | >>> s = sum_dictionaries() 546 | >>> s.__next__() 547 | >>> c = count_letters(s) 548 | >>> c.__next__() 549 | >>> read(text, c) 550 | Most frequent letter: n 551 | ``` 552 | 553 | ### 5.3.3 多任务 554 | 555 | 生产者或过滤器并不受限于唯一的下游。它可以拥有多个协程作为它的下游,并使用`send()`向它们发送数据。例如,下面是`read`的一个版本,向多个下游发送字符串中的单词: 556 | 557 | ```py 558 | >>> def read_to_many(text, coroutines): 559 | for word in text.split(): 560 | for coroutine in coroutines: 561 | coroutine.send(word) 562 | for coroutine in coroutines: 563 | coroutine.close() 564 | ``` 565 | 566 | 我们可以使用它来检测多个单词中的相同文本: 567 | 568 | ```py 569 | >>> m = match("mend") 570 | >>> m.__next__() 571 | Looking for mend 572 | >>> p = match("pe") 573 | >>> p.__next__() 574 | Looking for pe 575 | >>> read_to_many(text, [m, p]) 576 | Commending 577 | spending 578 | people 579 | pending 580 | === Done === 581 | === Done === 582 | ``` 583 | 584 | 首先,`read_to_many`在`m`上调用了`send(word)`。这个协程正在等待循环中的`text = (yield)`,之后打印出所发现的匹配,并且等待下一个`send`。之后执行流返回到了`read_to_many`,它向`p`发送相同的行。所以,`text`中的单词会按照顺序打印出来。 585 | -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/cover.jpg -------------------------------------------------------------------------------- /img/20160907175856.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/20160907175856.jpg -------------------------------------------------------------------------------- /img/barriers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/barriers.png -------------------------------------------------------------------------------- /img/clientserver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/clientserver.png -------------------------------------------------------------------------------- /img/constraints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/constraints.png -------------------------------------------------------------------------------- /img/coroutine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/coroutine.png -------------------------------------------------------------------------------- /img/curves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/curves.png -------------------------------------------------------------------------------- /img/deadlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/deadlock.png -------------------------------------------------------------------------------- /img/eval_apply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/eval_apply.png -------------------------------------------------------------------------------- /img/evaluate_square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/evaluate_square.png -------------------------------------------------------------------------------- /img/evaluate_sum_squares_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/evaluate_sum_squares_0.png -------------------------------------------------------------------------------- /img/evaluate_sum_squares_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/evaluate_sum_squares_1.png -------------------------------------------------------------------------------- /img/evaluate_sum_squares_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/evaluate_sum_squares_3.png -------------------------------------------------------------------------------- /img/expression_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/expression_tree.png -------------------------------------------------------------------------------- /img/fact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/fact.png -------------------------------------------------------------------------------- /img/factorial_machine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/factorial_machine.png -------------------------------------------------------------------------------- /img/fib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/fib.png -------------------------------------------------------------------------------- /img/fib_env.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/fib_env.png -------------------------------------------------------------------------------- /img/function_abs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/function_abs.png -------------------------------------------------------------------------------- /img/function_print.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/function_print.png -------------------------------------------------------------------------------- /img/getitem_rlist_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/getitem_rlist_0.png -------------------------------------------------------------------------------- /img/getitem_rlist_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/getitem_rlist_1.png -------------------------------------------------------------------------------- /img/getitem_rlist_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/getitem_rlist_2.png -------------------------------------------------------------------------------- /img/global_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/global_frame.png -------------------------------------------------------------------------------- /img/global_frame_assignment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/global_frame_assignment.png -------------------------------------------------------------------------------- /img/global_frame_def.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/global_frame_def.png -------------------------------------------------------------------------------- /img/interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/interface.png -------------------------------------------------------------------------------- /img/iter_improve_apply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/iter_improve_apply.png -------------------------------------------------------------------------------- /img/iter_improve_global.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/iter_improve_global.png -------------------------------------------------------------------------------- /img/lists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/lists.png -------------------------------------------------------------------------------- /img/logo_apply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/logo_apply.png -------------------------------------------------------------------------------- /img/logo_eval.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/logo_eval.png -------------------------------------------------------------------------------- /img/multiple_inheritance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/multiple_inheritance.png -------------------------------------------------------------------------------- /img/nested_pairs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/nested_pairs.png -------------------------------------------------------------------------------- /img/newton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/newton.png -------------------------------------------------------------------------------- /img/nonlocal_assign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/nonlocal_assign.png -------------------------------------------------------------------------------- /img/nonlocal_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/nonlocal_call.png -------------------------------------------------------------------------------- /img/nonlocal_call2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/nonlocal_call2.png -------------------------------------------------------------------------------- /img/nonlocal_corefer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/nonlocal_corefer.png -------------------------------------------------------------------------------- /img/nonlocal_def.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/nonlocal_def.png -------------------------------------------------------------------------------- /img/nonlocal_def2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/nonlocal_def2.png -------------------------------------------------------------------------------- /img/nonlocal_recall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/nonlocal_recall.png -------------------------------------------------------------------------------- /img/pair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/pair.png -------------------------------------------------------------------------------- /img/pi_sum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/pi_sum.png -------------------------------------------------------------------------------- /img/pig_latin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/pig_latin.png -------------------------------------------------------------------------------- /img/produce-filter-consume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/produce-filter-consume.png -------------------------------------------------------------------------------- /img/qr_alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/qr_alipay.png -------------------------------------------------------------------------------- /img/read-match-coroutine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/read-match-coroutine.png -------------------------------------------------------------------------------- /img/scope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/scope.png -------------------------------------------------------------------------------- /img/sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/sequence.png -------------------------------------------------------------------------------- /img/set_trees.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/set_trees.png -------------------------------------------------------------------------------- /img/sier.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/sier.png -------------------------------------------------------------------------------- /img/square_root.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/square_root.png -------------------------------------------------------------------------------- /img/square_root_update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/square_root_update.png -------------------------------------------------------------------------------- /img/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/star.png -------------------------------------------------------------------------------- /img/subroutine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/subroutine.png -------------------------------------------------------------------------------- /img/tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/tree.png -------------------------------------------------------------------------------- /img/universal_machine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/universal_machine.png -------------------------------------------------------------------------------- /img/vector-math1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/vector-math1.png -------------------------------------------------------------------------------- /img/vector-math2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/vector-math2.png -------------------------------------------------------------------------------- /img/vector-math3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kivy-CN/sicp-py-zh/6a48dc4d0f0558a883d6d42946f0f6d8f0ef9ad3/img/vector-math3.png -------------------------------------------------------------------------------- /styles/ebook.css: -------------------------------------------------------------------------------- 1 | /* GitHub stylesheet for MarkdownPad (http://markdownpad.com) */ 2 | /* Author: Nicolas Hery - http://nicolashery.com */ 3 | /* Version: b13fe65ca28d2e568c6ed5d7f06581183df8f2ff */ 4 | /* Source: https://github.com/nicolahery/markdownpad-github */ 5 | 6 | /* RESET 7 | =============================================================================*/ 8 | 9 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 10 | margin: 0; 11 | padding: 0; 12 | border: 0; 13 | } 14 | 15 | /* BODY 16 | =============================================================================*/ 17 | 18 | body { 19 | font-family: Helvetica, arial, freesans, clean, sans-serif; 20 | font-size: 14px; 21 | line-height: 1.6; 22 | color: #333; 23 | background-color: #fff; 24 | padding: 20px; 25 | max-width: 960px; 26 | margin: 0 auto; 27 | } 28 | 29 | body>*:first-child { 30 | margin-top: 0 !important; 31 | } 32 | 33 | body>*:last-child { 34 | margin-bottom: 0 !important; 35 | } 36 | 37 | /* BLOCKS 38 | =============================================================================*/ 39 | 40 | p, blockquote, ul, ol, dl, table, pre { 41 | margin: 15px 0; 42 | } 43 | 44 | /* HEADERS 45 | =============================================================================*/ 46 | 47 | h1, h2, h3, h4, h5, h6 { 48 | margin: 20px 0 10px; 49 | padding: 0; 50 | font-weight: bold; 51 | -webkit-font-smoothing: antialiased; 52 | } 53 | 54 | h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code { 55 | font-size: inherit; 56 | } 57 | 58 | h1 { 59 | font-size: 24px; 60 | border-bottom: 1px solid #ccc; 61 | color: #000; 62 | } 63 | 64 | h2 { 65 | font-size: 18px; 66 | color: #000; 67 | } 68 | 69 | h3 { 70 | font-size: 14px; 71 | } 72 | 73 | h4 { 74 | font-size: 14px; 75 | } 76 | 77 | h5 { 78 | font-size: 14px; 79 | } 80 | 81 | h6 { 82 | color: #777; 83 | font-size: 14px; 84 | } 85 | 86 | body>h2:first-child, body>h1:first-child, body>h1:first-child+h2, body>h3:first-child, body>h4:first-child, body>h5:first-child, body>h6:first-child { 87 | margin-top: 0; 88 | padding-top: 0; 89 | } 90 | 91 | a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 { 92 | margin-top: 0; 93 | padding-top: 0; 94 | } 95 | 96 | h1+p, h2+p, h3+p, h4+p, h5+p, h6+p { 97 | margin-top: 10px; 98 | } 99 | 100 | /* LINKS 101 | =============================================================================*/ 102 | 103 | a { 104 | color: #4183C4; 105 | text-decoration: none; 106 | } 107 | 108 | a:hover { 109 | text-decoration: underline; 110 | } 111 | 112 | /* LISTS 113 | =============================================================================*/ 114 | 115 | ul, ol { 116 | padding-left: 30px; 117 | } 118 | 119 | ul li > :first-child, 120 | ol li > :first-child, 121 | ul li ul:first-of-type, 122 | ol li ol:first-of-type, 123 | ul li ol:first-of-type, 124 | ol li ul:first-of-type { 125 | margin-top: 0px; 126 | } 127 | 128 | ul ul, ul ol, ol ol, ol ul { 129 | margin-bottom: 0; 130 | } 131 | 132 | dl { 133 | padding: 0; 134 | } 135 | 136 | dl dt { 137 | font-size: 14px; 138 | font-weight: bold; 139 | font-style: italic; 140 | padding: 0; 141 | margin: 15px 0 5px; 142 | } 143 | 144 | dl dt:first-child { 145 | padding: 0; 146 | } 147 | 148 | dl dt>:first-child { 149 | margin-top: 0px; 150 | } 151 | 152 | dl dt>:last-child { 153 | margin-bottom: 0px; 154 | } 155 | 156 | dl dd { 157 | margin: 0 0 15px; 158 | padding: 0 15px; 159 | } 160 | 161 | dl dd>:first-child { 162 | margin-top: 0px; 163 | } 164 | 165 | dl dd>:last-child { 166 | margin-bottom: 0px; 167 | } 168 | 169 | /* CODE 170 | =============================================================================*/ 171 | 172 | pre, code, tt { 173 | font-size: 12px; 174 | font-family: Consolas, "Liberation Mono", Courier, monospace; 175 | } 176 | 177 | code, tt { 178 | margin: 0 0px; 179 | padding: 0px 0px; 180 | white-space: nowrap; 181 | border: 1px solid #eaeaea; 182 | background-color: #f8f8f8; 183 | border-radius: 3px; 184 | } 185 | 186 | pre>code { 187 | margin: 0; 188 | padding: 0; 189 | white-space: pre; 190 | border: none; 191 | background: transparent; 192 | } 193 | 194 | pre { 195 | background-color: #f8f8f8; 196 | border: 1px solid #ccc; 197 | font-size: 13px; 198 | line-height: 19px; 199 | overflow: auto; 200 | padding: 6px 10px; 201 | border-radius: 3px; 202 | } 203 | 204 | pre code, pre tt { 205 | background-color: transparent; 206 | border: none; 207 | } 208 | 209 | kbd { 210 | -moz-border-bottom-colors: none; 211 | -moz-border-left-colors: none; 212 | -moz-border-right-colors: none; 213 | -moz-border-top-colors: none; 214 | background-color: #DDDDDD; 215 | background-image: linear-gradient(#F1F1F1, #DDDDDD); 216 | background-repeat: repeat-x; 217 | border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD; 218 | border-image: none; 219 | border-radius: 2px 2px 2px 2px; 220 | border-style: solid; 221 | border-width: 1px; 222 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 223 | line-height: 10px; 224 | padding: 1px 4px; 225 | } 226 | 227 | /* QUOTES 228 | =============================================================================*/ 229 | 230 | blockquote { 231 | border-left: 4px solid #DDD; 232 | padding: 0 15px; 233 | color: #777; 234 | } 235 | 236 | blockquote>:first-child { 237 | margin-top: 0px; 238 | } 239 | 240 | blockquote>:last-child { 241 | margin-bottom: 0px; 242 | } 243 | 244 | /* HORIZONTAL RULES 245 | =============================================================================*/ 246 | 247 | hr { 248 | clear: both; 249 | margin: 15px 0; 250 | height: 0px; 251 | overflow: hidden; 252 | border: none; 253 | background: transparent; 254 | border-bottom: 4px solid #ddd; 255 | padding: 0; 256 | } 257 | 258 | /* TABLES 259 | =============================================================================*/ 260 | 261 | table th { 262 | font-weight: bold; 263 | } 264 | 265 | table th, table td { 266 | border: 1px solid #ccc; 267 | padding: 6px 13px; 268 | } 269 | 270 | table tr { 271 | border-top: 1px solid #ccc; 272 | background-color: #fff; 273 | } 274 | 275 | table tr:nth-child(2n) { 276 | background-color: #f8f8f8; 277 | } 278 | 279 | /* IMAGES 280 | =============================================================================*/ 281 | 282 | img { 283 | max-width: 100% 284 | } --------------------------------------------------------------------------------