diff --git a/sicp/2/1.md b/sicp/2/1.md index bd35010..455bd4d 100644 --- a/sicp/2/1.md +++ b/sicp/2/1.md @@ -8,32 +8,32 @@ 对应:Lab 04、Cats ::: -在第一章中,我们主要讨论了计算过程和函数在程序设计中的作用。我们学习了怎么使用原始数据(数字)和原始操作(算术),怎么通过组合和控制形成复合函数,以及怎么通过给程序命名来创建函数抽象。我们还了解到:高阶函数使我们能够根据通用的计算方法进行操作和推理,从而增强了语言的功能。这就是编程的本质。 +第一章聚焦于**计算过程**与**函数的作用**:用原始数据与原始操作构造表达式,用组合与控制搭建复合函数,用命名建立抽象,并感受高阶函数在提炼通用模式时的威力。编程的核心是把复杂过程拆解成清晰的组合和抽象。 -本章重点介绍数据:我们研究的技术使我们可以表示和操作许多不同领域的信息。由于互联网的爆炸式增长,所有人都可以在网上免费获取大量结构化信息,并且计算也可以应用于范围广泛的不同问题。有效使用内置数据类型和用户定义的数据类型是数据处理型应用(data processing applications)的基础。 +这一章的主角换成了**数据**。我们要学习如何表示、组合、抽象和处理各种信息。互联网让海量结构化数据随手可得,计算也因此渗透到几乎所有领域。掌握内置数据类型并能自定义数据类型,是所有数据处理应用的基石。 ## 2.1.1 原始数据类型 -Python 中的每个值都有一个类(class)来确定它的类型。拥有相同类的值,行为也相同。例如,整数 1 和 2 都是 `int` 类的实例,我们就可以使用相似的方法进行处理。例如,它们都可以取反或与另一个整数相加。内置的 `type` 函数允许我们检查任何值的类。 +每个 Python 值都有一个类(class)来确定其类型。同一类的值行为一致,例如整数 1 和 2 都是 `int`,可以用相同方式运算。内置的 `type` 函数能查看任意值的类。 ```py >>> type(2) ``` -到目前为止,我们使用的值都是 Python 语言中内置的少量的原始数据类型的实例。原始数据类型具有以下属性: +我们目前用到的几乎都是内置的少量**原始数据类型**。它们的共同点: -1. 有一些可以求解为原始数据类型的表达式,被称为字面量(literals)。 -2. 有用于操作原始类型值的内置函数和操作符。 +1. 有能直接求值的**字面量**(literal)。 +2. 语言内置了操作该类型的函数与运算符。 -`int` 类是用于表示整数的原始数据类型。整数字面量(相邻的数字序列)会求解为 `int` 值,并且数学运算符可以操作这种值。 +例如,`int` 表示整数。整数字面量求值后就是 `int`,算术运算符可直接作用其上。 ```py >>> 12 + 3000000000000000000000000 3000000000000000000000012 ``` -Python 包含三种原始数字类型:整数(`int`)、浮点数(`float`)和复数(`complex`)。 +Python 还内置两种数字类型:浮点数(`float`)和复数(`complex`)。 ```py >>> type(1.5) @@ -42,9 +42,9 @@ Python 包含三种原始数字类型:整数(`int`)、浮点数(`float` ``` -浮点数:“Float”这个名字来源于 Python 和许多其他编程语言中对实数的表示方式:就是“具有浮动的小数点”的值。虽然关于数字如何表示的细节不是本文的主题,但了解 `int` 和 `float` 对象之间的一些区别是很重要的。特别是,`int` 对象可以精确地表示整数,没有任何近似处理或大小限制。另一方面,`float` 对象可以表示很大范围内的小数,但并不是所有的数字都能被精确表示,它有最小值和最大值之分。因此,`float` 值应被视为真实值的近似值,它们只能保证有限的精度,组合 `float` 值可能会导致近似误差;如果不进行近似处理,下面两个表达式的计算结果均为 7。 +**浮点数**:“float” 得名于许多语言的小数点“浮动”表示。本书不深入底层表示,但要记住 `int` 与 `float` 的差别:`int` 可以**精确**表示任意大的整数;`float` 覆盖范围更广但是近似值,有上下界。多个 `float` 运算会累积误差;理论上都等于 7 的表达式可能产生不同近似。 -> 译者注:不同于其他的编程语言,Python3 中的 `int` 值是无界的,也就是说它可以存储任意大小的数,具体可以查看 [PEP 237](https://peps.python.org/pep-0237/)。 +> 译者注:不同于很多语言,Python 3 的 `int` 无上限,可存储任意大的整数,详见 [PEP 237](https://peps.python.org/pep-0237/)。 ```py >>> 7 / 3 * 3 @@ -53,7 +53,7 @@ Python 包含三种原始数字类型:整数(`int`)、浮点数(`float` 6.999999999999999 ``` -尽管上式是 `int` 值的组合,但一个 `int` 值除以另一个 `int` 值,却会得到一个 `float` 值:一个截断的有限近似值,相当于两个整数相除的实际比值。 +即便除法的两边都是 `int`,结果也会变成 `float`,得到真实比值的近似。 ```py >>> type(1/3) @@ -62,15 +62,15 @@ Python 包含三种原始数字类型:整数(`int`)、浮点数(`float` 0.3333333333333333 ``` -当我们使用等式测试时,这种近似就会出现问题。 +在做相等性比较时,这种近似就会显形。 ```py ->>> 1/3 == 0.333333333333333312345 # 请注意浮点数近似 +>>> 1/3 == 0.333333333333333312345 # 注意浮点近似 True ``` -`int` 和 `float` 类之间的细微差别对编写程序有着广泛的影响,因此,这也是程序员必须熟记的细节。幸运的是,原始数据类型的数量很少,这无疑减少了精通一门编程语言所需的记忆量。此外,这些细节在许多编程语言中都是一致的,它们会由 [IEEE 754 浮点标准](http://en.wikipedia.org/wiki/IEEE_floating_point) 等社区指南强制要求执行。 +`int` 与 `float` 的细微差异会影响程序正确性,务必牢记。好消息是原始类型很少,学习成本低;而且许多语言在这些细节上保持一致,例如遵循 [IEEE 754 浮点标准](http://en.wikipedia.org/wiki/IEEE_floating_point)。 -非数值类型(Non-numeric types):值可以表示许多其他类型的数据,比如声音、图像、位置、网址、网络连接等等。它们中间的少数可以用原始数据类型表示,例如用于值 `True` 和 `False` 的 `bool` 类,其他大多数值的类型必须由程序员使用我们将在本章中学习到的组合和抽象方法来定义。 +**非数值类型**:值还可以代表声音、图像、地理位置、网址、网络连接等。部分可以直接用原始类型表示,如 `bool` 的 `True` 与 `False`;更多类型需要用本章将学到的“组合与抽象”自行定义。 -下面几节将介绍更多 Python 的原始数据类型,并将重点介绍它们在创建数据抽象方面所起到的作用。那些对更多细节感兴趣的人,可以查看在线书籍 Dive Into Python 3 中的一个关于 [原始数据类型](http://getpython3.com/diveintopython3/native-datatypes.html) 的章节,它给出了所有 Python 的原始数据类型以及如何操作它们的实用概述,包括大量的使用示例和实用技巧。 +接下来的小节会继续介绍 Python 的其他原始类型,并说明它们如何支撑数据抽象。想了解更全面的行为与示例,可参阅《Dive Into Python 3》的 [原始数据类型](http://getpython3.com/diveintopython3/native-datatypes.html) 章节。 diff --git a/sicp/2/2.md b/sicp/2/2.md index e768566..600d1fc 100644 --- a/sicp/2/2.md +++ b/sicp/2/2.md @@ -8,25 +8,23 @@ 对应:无 ::: -当我们希望在程序中表示世界上广泛的事物时,会发现它们中的大多数都具有复合结构。比如地理位置具有经纬度坐标。为了表示位置,我们希望我们的编程语言能够经度和纬度耦合在一起形成一对复合数据,使它能够作为单个概念单元被程序操作,同时也能作为可以单独考虑的两个部分。 +在程序中表示现实世界的各种事物时,绝大多数都具备复合结构。比如地理位置由经纬度组成。我们希望语言能把经度和纬度绑定成一对复合数据,既能当作单个概念操作,也能按两个部分分别处理。 -使用复合数据可以使程序更加模块化。如果我们能够将地理位置作为整体值进行操作,那么我们就可以将计算位置的程序部分与位置如何表示的细节隔离开来,这种将“数据表示”与“数据处理”的程序隔离的通用技术是一种强大的设计方法,称为数据抽象。数据抽象会使程序更易于设计、维护和修改。 +复合数据让程序更模块化。如果能把位置当作整体值来操作,就能把计算位置的逻辑与位置的表示方式隔离开来。这种把“数据表示”与“数据处理”分离的设计叫做**数据抽象**,能显著提升程序的可设计性、可维护性与可修改性。 -数据抽象与函数抽象类似。当我们创建一个函数抽象时,函数实现的细节可以被隐藏,而特定的函数本身可以被替换为具有相同整体行为的任何其他函数。换句话说,我们可以创建一个抽象来将函数的使用方式与实现细节分离。类似地,数据抽象可以将复合数据值的使用方式与其构造细节隔离开来。 +数据抽象与函数抽象同理。定义函数抽象时可以隐藏实现细节,任何具备相同行为的实现都能替换;也就是说,使用函数与函数的内部实现被解耦。类似地,数据抽象把复合数据的使用方式与其构造细节隔离。 -数据抽象的基本思想是构建程序,以便它们对抽象数据进行操作。也就是说,我们的程序应该以尽可能少的假设来使用数据,同时要将具体的数据表示定义为程序的独立部分。 - -程序的“操作抽象数据”和“定义具体表示”两个部分,会由一组根据具体表示来实现抽象数据的函数相连。为了说明该技术,我们将思考如何设计一组用于操作有理数的函数。 +核心思想是让程序操作抽象数据,对具体表示做最少假设,同时把数据的具体表示定义在独立部分。程序中“操作抽象数据”与“定义具体表示”两部分由一组函数衔接,这些函数根据具体表示实现抽象数据。为展示这一点,我们来设计一组操作有理数的函数。 ## 2.2.1 示例:有理数 -有理数是整数的比值,并且有理数是实数的一个重要子类。 `1/3` 或 `17/29` 等有理数通常写为: +有理数是整数的比值,是实数的重要子集。`1/3` 或 `17/29` 一般写成: ```py <分子>/<分母> ``` -其中 `<分子>` 和 `<分母>` 都是整数值的占位符,这两个部分能够准确表示有理数的值。实际上的整数除法会产生 `float` 近似值,失去整数的精确精度。 +其中 `<分子>` 与 `<分母>` 都是整数占位符,这两个部分能精确表示一个有理数的值。直接做整数除法会产生 `float` 近似,失去整数的精度。 ```py >>> 1/3 @@ -35,15 +33,15 @@ True ``` -但是,我们可以通过将分子和分母组合在一起来创建有理数的精确表示。 +我们可以把分子和分母组合起来,得到有理数的精确表示。 -通过使用函数抽象,我们可以在实现程序的某些部分之前开始高效地编程。我们首先假设已经存在了一个从分子和分母构造有理数的方法,再假设有方法得到一个给定有理数的分子和分母。进一步假设得到以下三个函数: +利用函数抽象,我们可以在未实现细节前先搭好程序骨架。先假设已有从分子分母构造有理数的方法,也能分别取出它们。设想有这三个函数: - `rational(n, d)` 返回分子为 `n`、分母为 `d` 的有理数 - `numer(x)` 返回有理数 `x` 的分子 - `denom(x)` 返回有理数 `x` 的分母 -我们在这里使用了一个强大的程序设计策略:一厢情愿(wishful thinking)。即使我们还没有想好有理数是如何表示的,或者函数 `numer`、`denom` 和 `rational` 应该如何实现。但是如果我们确实定义了这三个函数,我们就可以进行加法、乘法、打印和测试有理数是否相等: +这里用到一个强大的策略:**一厢情愿(wishful thinking)**。即便还不知道有理数的表示或 `numer`、`denom`、`rational` 的实现,只要假设它们存在,就能先写出加法、乘法、打印以及相等性判断: ```py >>> def add_rationals(x, y): @@ -61,18 +59,18 @@ True return numer(x) * denom(y) == numer(y) * denom(x) ``` -现在我们有了选择器函数 `numer` 和 `denom` 以及构造函数 `rational` 定义的有理数运算,但还没有定义这些函数。我们需要某种方法将分子和分母粘合在一起形成一个复合值。 +现在有了基于 `numer`、`denom`、`rational` 的运算,但这些函数还没实现。我们需要一种方式把分子和分母粘合成复合值。 ## 2.2.2 对 -为了使我们能够实现具体的数据抽象,Python 提供了一个名为 `list` 列表的复合结构,可以通过将表达式放在以逗号分隔的方括号内来构造。这样的表达式称为列表字面量。 +为了实现具体的数据抽象,Python 提供了复合结构 `list`,可通过逗号分隔的方括号构造,称为列表字面量。 ```py >>> [10, 20] [10, 20] ``` -可以通过两种方式访问 ​​ 列表元素。第一种方法是通过我们熟悉的多重赋值方法,它将列表解构为单个元素并将每个元素与不同的名称绑定。 +访问列表元素有两种方式。其一是多重赋值,把列表解构成单个元素并分别绑定名称。 ```py >>> pair = [10, 20] @@ -85,7 +83,7 @@ True 20 ``` -访问列表中元素的第二种方法是通过元素选择运算符,也使用方括号表示。与列表字面量不同,直接跟在另一个表达式之后的方括号表达式不会计算为 `list` 值,而是从前面表达式的值中选择一个元素。 +第二种是元素选择运算符,也用方括号。跟在表达式后的方括号不会生成列表,而是从前面表达式的值里选出某个元素。 ```py >>> pair[0] @@ -94,9 +92,9 @@ True 20 ``` -Python 中的列表(以及大多数其他编程语言中的序列)是从 0 开始索引的,这意味着索引 0 选择第一个元素,索引 1 选择第二个元素,以此类推。对于这种索引约定的一种直觉是,索引表示元素距列表开头的偏移量。 +Python(以及多数语言)的序列从 0 开始索引:0 是第一个元素,1 是第二个,以此类推。直觉上,索引表示元素距开头的偏移。 -元素选择运算符的等效函数称为 `getitem` ,它也使用 0 索引位置从列表中选择元素。 +等价的函数是 `getitem`,同样用 0 索引选择元素。 ```py >>> from operator import getitem @@ -106,9 +104,9 @@ Python 中的列表(以及大多数其他编程语言中的序列)是从 0 20 ``` -双元素列表并不是 Python 中表示对的唯一方法。将两个值捆绑在一起成为一个值的任何方式都可以被认为是一对。列表是一种常用的方法,它也可以包含两个以上的元素,我们将在本章后面进行探讨。 +双元素列表不是唯一的“对”表示。只要能把两个值捆成一个值,都可视作一对。列表是常用方式,也能容纳多于两个元素,本章稍后会继续介绍。 -代表有理数:我们现在可以将有理数表示为两个整数的对:一个分子和一个分母。 +表示有理数:现在可以把有理数表示成两个整数的对:分子与分母。 ```py >>> def rational(n, d): diff --git a/sicp/2/3.md b/sicp/2/3.md index cd4cf9b..8b0f47e 100644 --- a/sicp/2/3.md +++ b/sicp/2/3.md @@ -8,7 +8,7 @@ 对应:Lab 04、Disc 04、Lab 05、Disc 05 ::: -序列(sequence)是一组有顺序的值的集合,是计算机科学中的一个强大且基本的抽象概念。序列并不是特定内置类型或抽象数据表示的实例,而是一个包含不同类型数据间共享行为的集合。也就是说,序列有很多种类,但它们都具有共同的行为。特别是: +**序列(sequence)**是一组有顺序的值的集合,是计算机科学中强大且基础的抽象。它不是某个特定内置类型或表示,而是一类共享行为的集合:种类很多,行为一致,尤其: - **长度(Length)**:序列的长度是有限的,空序列的长度为 0。 @@ -18,7 +18,7 @@ Python 包含几种内置的序列数据类型,其中最重要的是列表(l ## 2.3.1 列表 -列表(list)是一个可以有任意长度的序列。列表有大量的内置行为,以及用于表达这些行为的特定语法。我们已经见过列表字面量(list literal),它的计算结果是一个 `list` 实例,以及一个计算结果为列表中元素值的元素选择表达式。`list` 内置的 `len` 函数返回序列的长度。如下,`digits` 是一个包含四个元素的列表,索引为 3 的元素是 8。 +列表(list)可以有任意长度,内置了大量行为与语法。我们已经见过列表字面量和求出元素值的元素选择表达式。`len` 返回序列长度。下例中 `digits` 有四个元素,索引 3 是 8。 ```python >>> digits = [1, 8, 2, 8] @@ -28,14 +28,14 @@ Python 包含几种内置的序列数据类型,其中最重要的是列表(l 8 ``` -此外,多个列表间可以相加,并且列表可以乘以整数。对于序列来说,加法和乘法并不是作用在内部元素上的,而是对序列自身进行组合和复制。也就是说,`operator` 模块中的 `add` 函数(和 `+` 运算符)会生成一个为传入列表串联的新列表。`operator` 中的 `mul` 函数(和 `*` 运算符)可接收原列表和整数 k 来返回一个内容为原列表内容 k 次重复的新列表。 +多个列表可以相加,列表也能乘以整数。对序列来说,加法与乘法作用在序列自身的拼接与复制上。`operator.add`(`+`)会串联列表,`operator.mul`(`*`)接收列表和整数 k,返回元素重复 k 次的新列表。 ```python >>> [2, 7] + digits * 2 [2, 7, 1, 8, 2, 8, 1, 8, 2, 8] ``` -任何值都可以包含在一个列表中,包括另一个列表。在嵌套列表中可以应用多次元素选择,以选择深度嵌套的元素。 +任何值都能放入列表,包括另一个列表。嵌套列表可多次元素选择以取得更深层元素。 ```python >>> pairs = [[10, 20], [30, 40]] @@ -47,9 +47,9 @@ Python 包含几种内置的序列数据类型,其中最重要的是列表(l ## 2.3.2 序列遍历 -在许多情况下,我们希望依次遍历序列的元素并根据元素值执行一些计算。这种情况十分常见,所以 Python 提供了一个额外的控制语句来处理序列的数据:`for` 循环语句。 +我们常常需要依次遍历序列并按元素做计算,所以 Python 提供了额外的控制语句:`for` 循环。 -考虑统计一个值在序列中出现了多少次的问题。我们可以使用 `while` 循环实现一个函数。 +考虑统计某个值在序列中出现多少次,可以用 `while` 写一个函数。 ```python >>> def count(s, value): @@ -64,7 +64,7 @@ Python 包含几种内置的序列数据类型,其中最重要的是列表(l 2 ``` -Python 的 `for` 循环可以通过直接遍历元素值来简化函数,相比 `while` 循环无需引入变量名 `index`。 +`for` 循环直接遍历元素,省去显式索引。 ```python >>> def count(s, value): @@ -92,9 +92,9 @@ for in : 1. 将当前帧的 `` 绑定到该元素值 2. 执行 `` -此执行过程中使用了可迭代值。列表是序列的一种,而序列是可迭代值,它们中的元素按其顺序进行迭代。Python 还包括其它可迭代类型,但我们现在将重点介绍序列。术语 "iterable" 的一般定义在第 4 章中关于迭代器的部分。 +执行过程中用到了可迭代值。列表是序列,序列是可迭代的,元素按顺序被遍历。Python 还有其他可迭代类型,但此处聚焦序列;“iterable” 的一般定义在第 4 章讲迭代器时给出。 -这个计算过程中的一个重要结果是:执行 `for` 语句后,`` 将绑定到序列的最后一个元素。所以 `for` 循环引入了另一种可以通过语句更新环境的方法。 +一个重要结果是:执行 `for` 语句后,`` 会绑定到序列的最后一个元素,因此 `for` 也能更新环境。 **序列解包(Sequence unpacking)**:程序中的一个常见情况是序列的元素也是序列,但所有内部序列的长度是固定相同的。`for` 循环可以在头部的 `` 中包含多个名称,来将每个元素序列“解包”到各自的元素中。 @@ -115,9 +115,9 @@ for in : 2 ``` -这种将多个名称绑定到固定长度序列中的多个值的模式称为序列解包(sequence unpacking),这与赋值语句中将多个名称绑定到多个值的模式类似。(译者注:如在 _1.2.4 名称与环境_ 中出现过的 `x, y = 3, 4.5`) +这种把多个名称绑定到固定长度序列多个值的模式称为**序列解包(sequence unpacking)**,类似赋值语句中的 `x, y = 3, 4.5`。 -**范围(Ranges)**:`range` 是 Python 中的另一种内置序列类型,用于表示整数范围。范围是用 `range` 创建的,它有两个整数参数:起始值和结束值加一。(译者注:其实可以有三个参数,第三个参数为步长,感兴趣可以自行搜索) +**范围(Ranges)**:`range` 是表示整数范围的内置序列,用起始值和结束值加一创建(也可传步长)。 ```python >>> range(1, 10) # 包括 1,但不包括 10 diff --git a/sicp/2/4.md b/sicp/2/4.md index 6b454d0..b1ff6a9 100644 --- a/sicp/2/4.md +++ b/sicp/2/4.md @@ -8,36 +8,36 @@ 对应:HW 05、Disc 04、Exam Prep 03 ::: -我们已经见识到了抽象在帮助我们应对复杂的大型系统时的重要性,但高效编程还需要一些系统化的原则,从而帮助我们更好地设计整个应用程序。具体来说,我们需要一些策略来模块化一个大型系统。所谓模块化,即将整个系统划分为独立维护开发,但又相互关联的模块。 +抽象能帮助我们处理复杂系统,但高效编程还需要系统化原则来设计模块化应用。模块化意味着把系统切成可独立维护又彼此关联的部分。 -在创建模块化项目时,一个非常有用的实践是引入可能随时间改变状态的数据。在这种情况下,一个单独的数据对象可以表示一些忽略整个程序其它部分而独立演变的事物。可变对象的行为可能会受到其自身历史状态的影响,就像真实世界中的事物一样。面向对象编程 (object-oriented programming) 的核心就是向数据添加状态。 +构建模块化项目时,引入**随时间可变的数据**很有用。单个数据对象可以代表某个独立演化的事物,行为可能受自身历史影响,像真实世界一样。**面向对象编程**的核心就是给数据加上状态。 ## 2.4.1 对象隐喻 -在本书的开头,我们区分了函数和数据的概念:函数发起某些操作,而数据是被操作的一方。但是当我们将函数引入到数据中时,我们就知道数据本身也可以有行为。函数可以被当作数据进行操作,同时也可以被调用来执行某些行为。 +在开篇我们区分了函数与数据:函数发起操作,数据被操作。但当函数被当作数据时,数据本身也能拥有行为。函数既可被操作,又可被调用。 -对象 (objects) 将数据的值和行为结合到了一起。对象可以直接表示某些信息,也可以用自身的表现行为来表达想表达的东西。一个对象具体应该怎么和其它对象进行交互,都被封装并绑定到了该对象自身的某些值上。当我们试图打印某个对象时,它自己知道应该如何以文字的形式表示自己。如果一个对象由多个部分组成,它知道应该怎么根据实际情况对外展示那些不同的部分。对象既是数据信息又是操作流程,它把二者结合到一起,从而表达复杂事物的属性、交互和行为。 +**对象(object)**把数据的值与行为结合起来。对象既能直接表示信息,也能用自身的行为来表达。它与其他对象的交互方式被封装在自己的值中。打印对象时,它知道如何呈现自身;若由多个部分组成,它知道按情境展示各部分。对象既是数据又是操作流程,用来刻画复杂事物的属性、交互与行为。 -在 Python 中,对象的行为是通过特定的语法和术语实现的,以日期为例: +在 Python 中,对象的行为通过特定语法表达。以日期为例: ```python >>> from datetime import date ``` -`date` 这个名称是一个 `class` 类,正如我们所见,一个类代表了一种值。具体的日期被称为这个日期类的实例对象。要想构建一个实例对象,可以用特定的参数去调用该类得到: +`date` 是一个 `class`,类代表一种值。具体日期是该类的实例,调用类并传参数即可构造: ```python >>> tues = date(2014, 5, 13) ``` -尽管 `tues` 是用基础数字构建出来的,但它具有日期的能力。举个例子,用另一个日期减掉它,我们可以得到一个时间间隔。我们可以打印一下这个间隔: +尽管 `tues` 由基础数字构成,但它具备日期的能力。用另一个日期减去它能得到时间间隔,并可打印: ```python >>> print(date(2014, 5, 19) - tues) 6 days, 0:00:00 ``` -对象有属性(attributes)的概念,可以理解为该对象中某个值的名字。和其它许多编程语言一样,我们在 Python 中使用点语法来访问一个对象中的某个属性。 +对象有**属性(attributes)**,可理解为对象内部某个值的名称。与很多语言一样,Python 用点语法访问属性: ```python >>> . @@ -45,23 +45,23 @@ 在上面的代码中,`` 表示一个对象,`` 表示该对象中对某个属性的名称。 -与之前介绍的其它变量名称不同,这些属性名称无法在运行环境中直接访问。属性名称是对象实例所独有的,只能通过点语法来访问。 +与一般名称不同,属性名不能直接在环境中访问,是对象实例私有的,只能用点语法取用。 ```python >>> tues.year 2014 ``` -对象还有方法(methods)的概念,其实也是属性,只不过该属性的值是函数。对象知道如何执行这些方法。具体实现起来,方法就是根据其自身的输入参数以及它所在的对象来计算特定结果的函数。举例来说,`strftime` (string format of time) 方法接受一个参数,该参数描述了具体的时间展示格式(e.g., %A 表示以完整格式返回星期)。 +对象还有**方法(methods)**,本质是值为函数的属性。对象知道如何执行这些方法:方法根据自身所在的对象和传入参数计算结果。比如 `strftime` 接收一个描述时间展示格式的参数(如 %A 表示完整星期名)。 ```python >>> tues.strftime('%A, %B %d') 'Tuesday, May 13' ``` -要计算 `strftime` 的返回值需要两个输入:期望展示的时间格式,以及 `tues` 中包含的日期信息。这个方法内部已经有了处理日期相关的逻辑,并且能够返回我们期望的结果。我们从来没有说过 2014 年 5 月 13 日是星期二,但是日期这个类本身就有这种能力,它能够知道一个特定的日期应该是星期几。通过把数据和行为绑定到一起, Python 为我们提供了一个已经完全抽象好的、可靠的 date 对象。 +计算 `strftime` 的结果需要展示格式和 `tues` 的日期信息。方法内部有处理日期的逻辑,能返回期望结果。我们并未显式声明 2014 年 5 月 13 日是星期二,但日期类有能力知道。数据与行为的绑定,让 Python 提供了抽象良好的可靠 date 对象。 -不仅 date 是对象,我们之前提到的数字、字符串、列表、区间等都是对象。它们本身表示数据,同时还拥有它们所代表的数据的行为。它们还有属性和方法。举例来说,字符串有一系列帮助我们处理文本的方法。 +不仅 date 是对象,数字、字符串、列表、区间等也是。它们既表示数据,也拥有相应的行为,还有属性与方法。比如字符串提供一系列文本处理方法。 ```python >>> '1234'.isnumeric() @@ -72,17 +72,17 @@ True True ``` -实际上,Python 中所有的值都是对象。也就是说,所有的值都有行为和属性,它们拥有它们所代表的数据的行为。 +实际上 Python 中所有值都是对象,都有行为和属性,具备其所代表数据的能力。 ## 2.4.2 序列对象 -像数字这样的基本数据类型的实例是不可变(immutable)的。它们所代表的值,在程序运行期间是不可以更改的。 另一方面,列表就是可变的(mutable)。 +数字等基本类型的实例是**不可变(immutable)**的;列表则是**可变(mutable)**的。 -可变数据用来表示那些会在程序运行期间发生变化的数据。时间每天都在流逝,虽然一个人在一天天地长大、变老,或者有一些其它什么变化,但是这个人还是这个人,这一点是没有发生变化的。类似地,一个对象也可能通过某些操作更改自身的属性。举例来说,一个列表中的数据是可能会发生变化的。大部分变化的发生,都是通过调用列表实例的方法来触发的。 +可变数据用来表示运行过程中会改变的事物。时间流逝,人会变老,但人仍是同一个人;类似地,对象可以通过操作更改自身属性。列表中的数据就可能变化,多数变化由调用列表方法触发。 我们可以通过一个简单的扑克牌游戏来介绍一些操作列表的方法。下面代码中的注释解释了每次变更后带来的影响。 -大约在公元 9 世纪前后,中国发明了扑克牌。在最早的扑克牌中,只有三种花色,分别代表了当时货币的面额: +大约公元 9 世纪,中国发明了扑克牌,最初只有三种花色,代表当时的货币面额: ```python >>> chinese = ['coin', 'string', 'myriad'] # 一组字符串列表 @@ -97,7 +97,7 @@ True >>> suits.remove('string') # 从列表中移除第一个与参数相同的元素 ``` -随着时间推移,又额外演变出了另外三种花色: +随着时间推移,又演变出了另外三种花色: ```python >>> suits.append('cup') # 在列表最后插入一个元素 diff --git a/sicp/2/5.md b/sicp/2/5.md index 649f378..536f812 100644 --- a/sicp/2/5.md +++ b/sicp/2/5.md @@ -8,25 +8,25 @@ 对应:Disc 05、HW 04、Lab 06、Ants ::: -面向对象编程(OOP)是一种组织程序的方法,它将本章介绍的许多思想结合在一起。与数据抽象中的函数一样,类创建了在使用和实现数据之间的抽象屏障。与调度字典(dispatch dictionaries)一样,对象响应行为请求。与可变数据结构一样,对象具有无法从全局环境直接访问的本地状态。Python 对象系统提供了方便的语法来促进使用这些技术来组织程序。这种语法的大部分在其他面向对象的编程语言之间共享。 +**面向对象编程(OOP)**是一种组织程序的方式,糅合了本章的多种思想。像数据抽象一样,类在使用与实现之间立起抽象屏障;像调度字典,对象会响应行为请求;像可变结构,对象有外部无法直接访问的本地状态。Python 的对象系统提供了方便的语法来运用这些技术,这些语法在其他面向对象语言中也普遍存在。 -对象系统提供的不仅仅是便利。它为设计程序提供了一个新的隐喻,其中几个独立的代理在计算机内交互。每个对象都将本地状态和行为捆绑在一起,从而抽象出两者的复杂性。对象相互通信,并且由于它们的交互而计算有用的结果。对象不仅传递消息,而且还在相同类型的其他对象之间共享行为,并从相关类型继承特征。 +对象系统带来的不只是便利,它提供了一种新的设计隐喻:一组独立的代理在程序中交互。每个对象把本地状态和行为打包,从而屏蔽复杂性;对象相互通信,靠交互计算出有用结果。对象不仅传递消息,还在同类对象间共享行为,并能从相关类型继承特征。 -面向对象编程(OOP)的范式有自己的词汇来支持对象隐喻。我们已经看到,对象(object)是具有方法和属性的数据值,可通过点表达式(dot notation)访问。每个对象(object)也有一个类型,称为其类(class)。为了创建新类型的数据,我们实现了新类。 +OOP 有自己的一套术语支撑这种隐喻。对象(object)是带方法和属性的数据值,通过点表达式访问。每个对象都有一个类型,即其类(class)。要创建新类型的数据,就实现新的类。 ## 2.5.1 对象和类 -类就像一个模板,对象是按照模板(类)生成的实例。到目前为止我们使用的对象都有内置类,但也可以创建新的用户定义类。类定义指定在该类的对象之间共享的属性和方法。我们将通过重新访问银行账户的例子来介绍类语句。 +类像模板,对象是按模板生成的实例。到目前为止我们用的对象都有内置类,但也可以自定义类。类定义指定同类对象共享的属性和方法。我们继续用银行账户示例引入 `class` 语句。 -在引入本地状态时,我们看到银行账户要具有 `balance` 的可变值。银行帐户对象应具有 `withdraw` 方法,用于更新帐户余额并返回请求的金额(如果可用)。要完成抽象:一个银行账户应该能够返回其当前的 `balance` ,返回账户 `holder` 的名称,以及 `deposit` 的金额。 +此前在讨论本地状态时,银行账户需要可变的 `balance`。账户对象应有 `withdraw` 方法来更新余额并在可行时返回提款金额。抽象上,一个账户还应能返回当前 `balance`、账户 `holder` 的姓名,并处理 `deposit`。 -`Account` 类允许我们创建多个银行账户实例。创建新对象实例的操作称为实例化类。Python 中用于实例化类的语法与调用函数的语法相同。在这种情况下,我们用参数 `Kirk` 调用 `Account` ,即帐户持有人的姓名。 +`Account` 类让我们能创建多个账户实例。创建新对象实例的操作称为实例化,语法与调用函数相同。这里我们用 `Kirk` 作为账户持有人调用 `Account`。 ```python >>> a = Account('Kirk') ``` -对象的属性是与对象关联的名称 - 值对,可通过点表达式访问。对于特定对象,其有特定值的属性,(而不是类的所有对象)称为实例属性。每个 `Account` 都有自己的余额和账户持有人姓名,这是实例属性的示例。在更广泛的编程社区中,实例属性也可以称为字段、属性或实例变量。 +对象的属性是与对象关联的名称-值对,通过点表达式访问。只属于某个对象而非整个类的称为实例属性。每个 `Account` 有自己的余额和持有人姓名,这就是实例属性,常被称为字段或实例变量。 ```python >>> a.holder @@ -35,14 +35,14 @@ 0 ``` -对对象进行操作或执行特定于对象的计算的函数称为方法。方法的返回值和副作用可以依赖于并更改对象的其他属性。例如, `deposit` 是我们 `Account` 对象 `a` 的方法。它需要一个参数,即要存入的金额,更改对象的 `balance` 属性,并返回结果余额。 +对对象执行特定计算的函数叫方法,返回值和副作用可以依赖并修改对象的属性。比如 `deposit` 是 `Account` 对象 `a` 的方法,需要一个金额参数,修改 `balance` 并返回结果余额。 ```python >>> a.deposit(15) 15 ``` -我们说方法是在特定对象上调用的。调用 `withdraw` 方法的结果是,要么批准提款并扣除金额,要么拒绝请求并返回错误消息。 +方法是在特定对象上调用的。调用 `withdraw` 可能批准提款并扣款,也可能拒绝并返回错误消息。 ```python >>> a.withdraw(10) # withdraw 方法返回扣除后的金额 @@ -57,18 +57,18 @@ ## 2.5.2 类的定义 -`class` 语句可以创建自定义类,类体里面又包含多条子语句。类语句定义类名,类体包含一组语句来定义类的属性。 +`class` 语句创建自定义类,类体包含多条子语句。类语句定义类名,类体中的语句定义类的属性。 ```python class : ``` -执行类语句,将创建一个新类,并在当前环境的第一帧中绑定 `` 。然后执行类体里面的语句。在 `class` 的 `` 中 `def` 或赋值语句中绑定的任何名称都会创建或修改类的属性。 +执行类语句会创建一个新类,并在当前环境第一帧绑定 ``,然后执行类体。类体中 `def` 或赋值绑定的名称会创建或修改类属性。 -类通常通过操作类属性来进行设计,这些属性是与该类的每个实例关联的名称 - 值对。类通过定义一个初始化对象的方法来指定特定对象的实例属性。例如,初始化 `Account` 类的对象的一部分是为它分配一个 0 的起始余额。 +类通常通过类属性来设计,这些属性是与每个实例关联的名称-值对。类通过定义初始化方法来设定实例属性,例如给 `Account` 对象分配 0 的初始余额。 -`class` 语句中的 `` 包含 `def` 语句,`def` 语句为类的对象定义新方法。初始化对象的方法在 `Python` 中有一个特殊的名称 `__init__` (“init”的每一侧都有两个下划线),称为类的构造函数(constructor)。 +类体的 `def` 语句为对象定义新方法。初始化方法在 Python 中名为 `__init__`(两侧各两个下划线),称为构造函数(constructor)。 ```python >>> class Account: @@ -77,9 +77,9 @@ class : self.holder = account_holder ``` -`Account` 的 `__init__` 方法有两个形式参数。第一个 `self` 绑定到新创建的 `Account` 对象。第二个参数 `account_holder` 绑定到调用类进行实例化时传递给类的参数。 +`Account` 的 `__init__` 有两个形参。`self` 绑定到新创建的对象,`account_holder` 绑定实例化时传入的参数。 -构造函数将实例属性名称 `balance` 绑定到 0。它还将属性名称 `holder` 绑定到名称 `account_holder` 的值。形式参数 `account_holder` 是 `__init__` 方法中的本地名称。另一方面,通过最终赋值语句绑定的名称 `holder` 仍然存在,因为它使用点表达式存储为 `self` 的属性。 +构造函数把实例属性 `balance` 设为 0,把属性 `holder` 设为 `account_holder` 的值。`account_holder` 是 `__init__` 的局部名称;而通过点表达式绑定的 `holder` 保存在 `self` 的属性中。 定义 `Account` 类后,我们可以实例化它。 @@ -87,7 +87,7 @@ class : >>> a = Account('Kirk') ``` -上面的语句调用 `Account` 类创建一个新对象,这个对象是 `Account` 的一个实例,然后使用两个参数调用构造函数 `__init__` : 新创建的对象和字符串“Kirk” 。一般来说,我们使用参数名称 `self` 作为构造函数的第一个参数,它会自动绑定到正在实例化的对象。几乎所有的 Python 代码都遵守这个规定。 +上面的语句调用 `Account` 创建新对象,得到 `Account` 的一个实例,然后用两个参数调用 `__init__`:新对象和字符串 `"Kirk"`。通常把构造函数的第一个参数命名为 `self`,它会自动绑定到实例化的对象,几乎所有 Python 代码都遵循这一约定。 现在,我们可以使用符号点来访问对象的 `balance` 和 `holder` 。 diff --git a/sicp/2/6.md b/sicp/2/6.md index 7941577..933f4f9 100644 --- a/sicp/2/6.md +++ b/sicp/2/6.md @@ -8,19 +8,19 @@ 对应:无 ::: -在面向对象编程范式(object-oriented programming paradigm)中工作时,我们使用对象的隐喻来指导程序的组织。大部分关于数据表示和操作的逻辑都通过类声明来表达。在本节中,我们将看到类和对象本身可以使用函数和字典来表示。通过以这种方式实现对象系统的目的是为了说明使用对象的隐喻并不需要特定的编程语言。即使在没有内置对象系统的编程语言中,程序也可以是面向对象的。 +在面向对象编程范式中,我们用对象的隐喻来组织程序。多数数据表示与操作逻辑都用类声明表达。本节将展示类和对象也能用函数与字典来表示,说明对象隐喻不依赖特定语言:即便没有内置对象系统的语言,程序也可以是面向对象的。 -为了实现对象,我们将放弃点表示法(它需要内置语言支持),而是创建行为方式与内置对象系统的元素非常相似的调度字典。我们已经看到了如何通过调度字典实现消息传递的行为。为了完全实现对象系统,我们在实例、类和基类之间发送消息,它们都是包含属性的字典。 +为实现对象,我们不用点表示(需语言支持),而是创建行为类似内置对象系统的调度字典。之前已见过用调度字典实现消息传递。要完整实现对象系统,我们在实例、类、基类之间发送消息,它们都是携带属性的字典。 -我们不会实现完整的 Python 对象系统,其中包括我们在本文中未涵盖的功能(例如元类(meta-class)和静态方法)。相反,我们将重点放在没有多重继承和内省行为(例如返回实例的类)的用户定义类上。我们的实现不打算严格遵循 Python 类型系统的规范。相反,它旨在实现支持对象隐喻的核心功能。 +我们不会实现完整的 Python 对象系统(如元类、静态方法等未涉及特性)。重点放在无多重继承且无内省行为(如返回实例的类)的用户定义类上。实现目标是支持对象隐喻的核心功能,而非完全符合 Python 类型系统规范。 ## 2.6.1 实例 -我们从实例开始。实例拥有可以被设置并检索的具名属性,例如一个银行账户的实例 account 拥有具名属性 balance。我们使用调度字典实现一个实例,这个调度字典可以响应设置和获取("get" and "set")属性值的消息。属性本身存储在一个名为 **attributes** 的本地字典中。 +从实例开始。实例有可设置和获取的具名属性,例如账户实例 `account` 的 `balance`。我们用调度字典实现实例,响应设置与获取("get"、"set")属性值的消息,属性存储在名为 **attributes** 的本地字典中。 -正如我们在前面的章节所看到的,字典本身也是抽象的数据类型。我们使用函数实现数据对,使用数据对实现列表,然后使用列表实现字典。当我们使用字典实现一个对象系统时,牢记我们也可以仅仅只使用函数来实现对象。 +如前所示,字典本身也是抽象数据类型。我们用函数实现数据对,再用数据对实现列表,再用列表实现字典。用字典实现对象系统时,要记得仅用函数也能实现对象。 -在开始我们的实现之前,假定我们有一个类的实现,它可以查出任何不属于实例的名称。我们将一个类作为参数传递给 `make_instance` 的形参 `cls`。 +开始实现前,假定已有一个类的实现,能查找实例没有的名称。类作为参数传给 `make_instance` 的形参 `cls`。 ```python >>> def make_instance(cls): @@ -38,9 +38,9 @@ return instance ``` -`instance` 是一个能够响应 `get` 和 `set` 消息的调度字典。`set` 消息和 Python 对象系统中的属性赋值操作相对应:所有已经赋值的属性直接存储在对象的本地属性字典中。在 `get` 消息中,如果 `name` 没有出现在本地 `attributes` 字典中,则会在类中查找。如果从类中查找返回的值是一个函数,则这个函数必须被绑定到实例。 +`instance` 是能响应 `get` 和 `set` 的调度字典。`set` 对应 Python 中的属性赋值:已赋值的属性直接存储在本地属性字典。`get` 时,如果 `name` 不在本地 `attributes`,就去类里查找;若类里返回的是函数,需要把它绑定到实例。 -对于绑定方法值。`make_instance` 中的 `get` 消息使用 `get_value` 函数在类中找到一个具名属性,然后调用 `bind_method` 函数。只有在这个具名属性是一个函数值时才会绑定一个方法,从函数值创建一个绑定方法值时,它会将 `instance` 作为第一个参数插入到函数值中,从而创建绑定方法值。 +绑定方法值时,`make_instance` 的 `get` 通过 `get_value` 在类中找到属性,再调用 `bind_method`。只有属性值是函数才绑定:创建绑定方法时,会把 `instance` 作为第一个参数插入函数值。 ```python >>> def bind_method(value, instance): @@ -57,7 +57,7 @@ ## 2.6.2 类 -不论是在 Python 的对象系统中还是在我们自己正在实现的对象系统中,都认为类也是一个对象。为了简单起见,我们可以说类这个对象并没有属于它的类类型(实际在 Python 中,类确实拥有它的类类型,几乎所有的类都共享同一个类类型,叫做 `type`)一个类可以响应 `get` 和 `set` 消息,以及 `new` 消息。 +无论是 Python 的对象系统还是我们的实现,类也被视为对象。为简单起见,我们可以说类对象没有属于它的类类型(在 Python 中确实有,几乎所有类共享 `type`)。类可以响应 `get`、`set` 和 `new` 消息。 ```python >>> def make_class(attributes, base_class=None): @@ -75,9 +75,9 @@ return cls ``` -不像实例那样,类的 `get` 函数在没有找到属性时并不会查询它的类,而是查询它的基类(base_class)。类不需要进行方法绑定。 +与实例不同,类的 `get` 找不到属性时不会查询它的类,而是查询基类(base_class)。类不需要方法绑定。 -对于初始化,`make_class` 中的 `new` 函数会调用 `init_instance`,这个方法首先会创建一个新的实例,然后调用一个叫做 `__init__` 的方法。 +初始化时,`make_class` 的 `new` 调用 `init_instance`:先创建实例,再调用名为 `__init__` 的方法。 ```python >>> def init_instance(cls, *args): @@ -89,15 +89,15 @@ return instance ``` -这个最终的函数完成了我们的对象系统。我们现在有了实例,它们会在本地 `set` 设置属性,但是在 `get` 获取属性时它们会转而回退到类中。实例从类中查询名称后,会将它自己绑定到函数值以此来创建方法。最后,类可以创建新的实例并且在创建后马上调用它们的 `__init__` 构造器函数。 +这个函数补全了对象系统:实例用 `set` 在本地设置属性,`get` 时回退到类;从类查得函数后绑定自身生成方法;类能创建新实例并立即调用其 `__init__`。 -在这个对象系统中,唯一应该被用户调用的函数是 `make_class`。所有其他的功能都是通过消息传递实现的。类似地,Python 的对象系统通过 `class` 语句调用,并且其他所有功能通过点表达式和对类的调用来实现。 +在这个对象系统中,唯一供用户调用的函数是 `make_class`,其他功能都通过消息传递实现。类似地,Python 的对象系统通过 `class` 语句启动,其他功能靠点表达式和类调用完成。 ## 2.6.3 使用已经实现的对象 我们现在重新使用前面的章节中的银行账户的例子。我们将使用我们自己实现的对象系统来创建一个 `Account` 类,一个 `CheckingAccount` 子类,以及为他们各自创建一个实例。 -`Account` 类通过 `make_account_class` 函数创建,这个函数和 Python 中的 `class` 语句有着相似结构的,但是最后调用了 `make_class`。 +`Account` 类通过 `make_account_class` 创建,该函数结构类似 Python 的 `class` 语句,但最终调用 `make_class`。 ```python >>> def make_account_class(): diff --git a/sicp/2/7.md b/sicp/2/7.md index 6472285..e1bee73 100644 --- a/sicp/2/7.md +++ b/sicp/2/7.md @@ -8,17 +8,17 @@ 对应:Ants、Disc 09 ::: -对象系统允许程序员更高效地建立并使用抽象数据描述。其也设计为允许在同一个程序中存在多种抽象数据表现形式。 +对象系统让程序员更高效地建立并使用抽象数据描述,也允许同一程序存在多种数据表示。 -对象抽象的一个核心概念就是泛型函数,这种函数能够接受多种不同类型的值。我们将思考三种不同的用于实现泛型函数的技术:共享接口,类型派发和类型强制转换。在建立这些概念的过程中,我们也会发现一些 Python 对象系统的特性,这些特性支持泛型函数的创建。 +对象抽象的核心之一是**泛型函数**,能接受多种类型的值。我们将讨论三种实现技术:共享接口、类型派发和类型强制转换。建立这些概念时,也会看到 Python 对象系统支持泛型函数的特性。 ## 2.7.1 字符串转换 -为了高效地展示数据,一个对象值应该像它代表的数据一样进行行为,包括产生一个它自己的字符串表示。在像 Python 这样的交互式语言中,数据值的字符串表示是特别重要的,在交互式会话中,它可以自动地展示表达式的值的字符串形式。 +要高效展示数据,对象应像其代表的数据那样行为,包括生成自身的字符串表示。在 Python 这样的交互式语言中,字符串表示尤为重要,交互会话会自动显示表达式值的字符串形式。 -字符串值为人们提供了一种相互传递信息的基本媒介。字符序列可以渲染在屏幕上、打印在纸上、大声阅读出来、转换成盲文或者以莫斯码广播。字符串也是编程的基础,因为他们可以表示 Python 表达式。 +字符串是人类传递信息的基本媒介:可显示、打印、朗读、转盲文或用摩斯码广播。字符串也是编程基础,因为它能表示 Python 表达式。 -Python 规定所有的对象都应该生成两个不同的字符串表示:一种是人类可读的文本,另一种是 Python 可解释的表示式。字符串的构造函数,即 `str`,返回一个人类可读的字符串。如果可能,`repr` 函数返回一个 Python 可解释的表达式,该表达式的求值结果与原对象相同。`repr` 的文档字符串(docstring)解释了这个特性: +Python 规定所有对象都应生成两种字符串表示:一种人类可读,另一种可被 Python 解释。`str` 构造器返回人类可读字符串;`repr` 若可行则返回求值后与原对象相同的表达式。`repr` 的文档字符串说明了这一点: > _repr(object) -> string_ > @@ -39,14 +39,14 @@ Python 规定所有的对象都应该生成两个不同的字符串表示:一 12000000000000.0 ``` -在一些情况下,不存在对原始值的字符串表示时,Python 通常生成一个被尖括号包围的描述。 +有时无法提供可重建的表示,Python 会生成尖括号包围的描述。 ```python >>> repr(min) '' ``` -`str` 构造器通常与 `repr` 行为一致,但是在某些情况下它会提供一个更容易解释的文本表示。例如,我们可以看到 `str` 和 `repr` 对于日期对象的不同展示。 +`str` 通常与 `repr` 一致,但有时提供更易读的文本。例如日期对象的展示: ```python >>> from datetime import date @@ -57,9 +57,9 @@ Python 规定所有的对象都应该生成两个不同的字符串表示:一 '2011-09-12' ``` -定义 `repr` 函数带来了一个新的挑战:我们想要它正确地应用于所有的数据类型,即使是那些实现 `repr` 时还不存在的类型。我们希望它是一个通用的或者多态(polymorphic)的函数,可以被应用于数据的多种(多)不同形式(态)。 +定义 `repr` 的挑战在于让它适用于所有类型,甚至实现时尚不存在的类型;我们希望它是通用的、多态的。 -在这情况下,对象系统提供了一种优雅的解决方案:`repr` 函数总是在其参数值上调用一个名为 `__repr__` 的方法。 +对象系统给出优雅解法:`repr` 总是在参数值上调用名为 `__repr__` 的方法。 ```python >>> tues.__repr__() @@ -75,15 +75,15 @@ Python 规定所有的对象都应该生成两个不同的字符串表示:一 '2011-09-12' ``` -这些能够应对多种类型的函数(多态函数)是一个更通用原则的例子:某些函数应该能够适用于多种数据类型。此外,创建这样一个函数的一种方式是使用在每个类中都有不同定义的共享属性名称,这就意味着这些函数在不同的类中会有不同的行为。 +这种能应对多种类型的函数(多态函数)体现了一个原则:某些函数应能作用于多种数据类型。实现方式之一是使用各类都定义的共享属性名,使函数在不同类中呈现不同实现。 ## 2.7.2 专用方法 -在 Python 中,某些特殊名称会在特殊情况下被 Python 解释器调用。例如,类的 `__init__` 方法会在对象被创建时自动调用。`__str__` 方法会在打印时自动调用,`__repr__` 方法会在交互式环境显示其值的时候自动调用。 +在 Python 中,一些特殊名称会在特定场景由解释器调用。例如对象创建时自动调用 `__init__`,打印时调用 `__str__`,交互环境显示值时调用 `__repr__`。 -在 Python 中有一些为其他行为而准备的特殊名称。下面介绍其中某些常用的。 +还有为其他行为预留的特殊名称,常见的如下。 -真值(True)和假值(False)。我们之前已经看到,数字类型在 Python 中拥有真值:更准确地说,0 是一个假值而其他所有数字都是真值。实际上 Python 中的所有对象都拥有真假值。默认情况下,用户定义类的对象被认为是真值,但是专门的 `__bool__` 方法可以用于覆盖这种行为。如果一个对象定义了 `__bool__` 方法,那么 Python 就会调用这个方法来确定它的真假值。 +真值(True/False)。之前看到数字的真值:0 为假,其他数字为真。实际上所有对象都有真假值。默认用户定义类的对象为真,但可通过 `__bool__` 覆盖;若定义了 `__bool__`,Python 会调用它决定真假。 举一个例子,假设我们想让一个只有 0 存款的账号为假值。我们可以为 `Account` 添加一个 `__bool__` 方法来实现这种行为。 @@ -91,7 +91,7 @@ Python 规定所有的对象都应该生成两个不同的字符串表示:一 >>> Account.__bool__ = lambda self: self.balance != 0 ``` -我们可以调用 `bool` 构造器来看一个对象的真假值,同时我们也可以在布尔上下文中使用任何对象。 +可以调用 `bool` 查看对象真假,也可在布尔上下文使用任何对象。 ```python >>> bool(Account('Jack')) @@ -101,7 +101,7 @@ False Jack has nothing ``` -序列操作。我们已经知道使用 `len` 函数可以确定序列的长度。 +序列操作。`len` 用于确定序列长度。 ```python >>> len('Go Bears!') @@ -115,7 +115,7 @@ Jack has nothing 9 ``` -如果序列没有提供 `__bool__` 方法,那么 Python 会使用序列的长度来确定其真假值。空的序列是假值,而非空序列是真值。 +若序列未提供 `__bool__`,Python 用长度决定真假:空为假,非空为真。 ```python >>> bool('') @@ -135,7 +135,7 @@ True 'B' ``` -可调用对象。在 Python 中函数是一等对象,因此它们被作为数据进行传递,并且像其他对象那样拥有属性。Python 还允许我们定义像函数一样可以被“调用的对象”,只要在对象中包含一个 `__call__` 方法。通过这个方法,我们可以定义一个行为像高阶函数的类。 +可调用对象。函数是一等对象,可作为数据传递并拥有属性。Python 允许定义“可调用的对象”:对象实现 `__call__` 方法即可,由此可定义行为像高阶函数的类。 举个例子,思考下面这个高阶函数,它返回一个函数,这个函数将一个常量值加到其参数上。 diff --git a/sicp/2/8.md b/sicp/2/8.md index 7bd7121..16e0c1a 100644 --- a/sicp/2/8.md +++ b/sicp/2/8.md @@ -8,13 +8,13 @@ 对应:无 ::: -决定如何表示和处理数据通常受到替代方案效率的影响。效率指的是表示或处理所使用的计算资源,例如计算函数结果或表示对象所需的时间和内存量。这些数量可以根据实现的细节而大大不同。 +选择如何表示和处理数据时,常要权衡效率。效率指计算所用的资源,例如求函数结果或表示对象所需的时间与内存,这些数量会因实现细节而差异巨大。 ## 2.8.1 测量效率 -测量程序运行所需的时间或消耗的内存确切值是具有挑战性的,因为结果取决于计算机配置的许多细节。更可靠地表征程序的效率的方法是测量某些事件发生的次数,例如函数调用次数。 +精确测量运行时间或内存很难,因为结果取决于机器配置等诸多细节。更可靠的方式是统计某些事件发生的次数,例如函数调用次数。 -让我们回到我们第一个树递归函数,即用于计算斐波那契数列中的数字的 `fib` 函数。 +回到第一个树递归函数 `fib`。 ```py >>> def fib(n): @@ -28,13 +28,13 @@ 5 ``` -考虑计算 `fib(6)` 时的计算模式,如下所示,为了计算 `fib(5)`,我们计算 `fib(3)` 和 `fib(4)`。而要计算 `fib(3)`,我们需要计算 `fib(1)` 和 `fib(2)`。总体而言,这个演化过程看起来像一棵树。每个蓝色圆点表示在遍历这棵树时计算出的一个斐波那契数的完成计算。 +计算 `fib(6)` 的模式如图:为求 `fib(5)` 需要 `fib(3)` 与 `fib(4)`,求 `fib(3)` 又需 `fib(1)` 与 `fib(2)`。整体演化像一棵树,遍历时每个蓝点表示某个斐波那契数完成计算。 ![fib](/sicp/fib.png) -这个函数作为一个典型的树递归函数具有教学意义,但它是计算斐波那契数的一种极其低效的方式,因为它进行了大量的冗余计算。计算 `fib(3)` 的整个过程被重复执行。 +它很有教学意义,但计算斐波那契数极其低效,存在大量冗余:`fib(3)` 的整个过程会被重复。 -我们可以测量这种低效性。这个高阶的 `count` 函数返回一个与其参数等效的函数,并且还维护一个 `call_count` 属性。通过这种方式,我们可以检查 `fib` 函数被调用的次数。 +我们可以测量这种低效。高阶函数 `count` 返回与参数等效的函数,并维护 `call_count` 属性,用以统计调用次数。 ```py >>> def count(f): @@ -45,7 +45,7 @@ return counted ``` -通过计算对 `fib` 函数的调用次数,我们可以发现所需的调用次数增长速度比斐波那契数列本身还要快。这种调用的快速增长是树递归函数的特征。 +统计调用次数可见其增长比斐波那契数列本身还快,这是树递归的典型特征。 ```py >>> fib = count(fib) @@ -55,15 +55,15 @@ 10946 ``` -**空间**。要了解函数的空间需求,我们通常必须指定在计算的环境模型中如何使用、保留和回收内存。在计算表达式时,解释器会保存所有活动的环境,以及这些环境引用的所有值和帧。我们称一个环境是活动的,如果它为正在计算的某个表达式提供了评估上下文。每当为其创建第一个帧的函数调用最终返回时,环境将变为非活动状态。 +**空间**。要了解函数的空间需求,需要说明环境模型如何使用、保留和回收内存。计算表达式时,解释器保存所有活动环境及其引用的值和帧。环境为某个表达式提供求值上下文即为活动;当创建其首帧的调用返回时,环境变为非活动。 -例如,在计算 `fib` 时,解释器按照之前显示的顺序计算每个值,遍历树的结构。为此,它只需要跟踪在计算的任何时刻位于当前节点上方的那些节点。用于计算其他分支的内存可以被回收,因为它不会影响未来的计算。总的来说,树递归函数所需的空间将与树的最大深度成比例。 +例如计算 `fib` 时,解释器按树结构顺序计算各值。它只需跟踪当前节点之上的节点,其他分支所用内存可回收,因为不再影响未来计算。树递归所需空间与树的最大深度成正比。 以下的图表描述了计算 `fib(3)` 时所创建的环境。在计算初始应用 `fib` 的返回表达式时,表达式 `fib(n-2)` 被计算,得到一个值为 0。一旦这个值被计算出来,相应的环境帧(被标记为灰色)就不再需要:它不是一个活动环境的一部分。因此,一个良好设计的解释器可以回收用于存储该帧的内存。另一方面,如果解释器当前正在计算 `fib(n-1)` ,则通过这个 `fib` 应用(其中 n 为 2)所创建的环境是活动的。反过来,最初用于将 `fib` 应用于 3 的环境是活动的,因为其返回值尚未被计算出来。 -高阶函数 `count_frames` 用于跟踪尚未返回的函数调用次数 `open_count` 。它通过在计算过程中记录当前活动的函数调用次数来实现。`max_count` 属性是 `open_count` 曾经达到的最大值,它对应于在计算过程中同时处于活动状态的最大帧数。 +高阶函数 `count_frames` 用于跟踪未返回的调用次数 `open_count`,记录计算过程中活动调用数。`max_count` 是 `open_count` 曾达到的最大值,对应同时活动的最大帧数。 ```py >>> def count_frames(f): @@ -89,13 +89,13 @@ 24 ``` -总结一下, `fib` 函数的空间要求(以活动帧数衡量)比输入小一个单位,这往往是较小的。而以递归调用次数衡量的时间要求比输出大,这往往是巨大的。 +总结:`fib` 的空间需求(按活动帧数)比输入小 1,通常较小;而按递归调用次数衡量的时间需求比输出大,通常巨大。 ## 2.8.2 记忆化 -树递归的计算过程通常可以通过记忆化(Memoization)来提高效率,这是一种增加递归函数效率的强大技术。记忆化函数会保存之前接收到的参数的返回值。如果第二次调用 `fib(25)` ,它不会再通过递归重新计算返回值,而是直接返回已经计算好的结果。 +树递归通常可用**记忆化(memoization)**提效:函数会保存已见参数的返回值,第二次调用如 `fib(25)` 就直接返回缓存。 -记忆化可以自然地表达为一个高阶函数,也可以用作装饰器。下面的定义创建了一个缓存(cache),用于存储先前计算过的结果,索引是它们计算所用的参数。使用字典作为缓存的数据结构要求被记忆化的函数的参数是不可变的。 +记忆化可自然写成高阶函数或装饰器。下面创建缓存(cache)存储已计算结果,索引为参数;用字典做缓存要求参数不可变。 ```py >>> def memo(f): @@ -130,34 +130,34 @@ ## 2.8.3 增长阶数 -计算过程在消耗计算资源(时间和空间)的速率上可能有很大的差异,正如之前的例子所展示的那样。然而,准确地确定调用函数时将使用多少空间或时间是一项非常困难的任务,这取决于许多因素。分析一个计算过程的有用方法是将其与一组具有类似需求的过程分类。一种有用的分类是该过程的增长阶(Orders of Growth),它以简单的术语表达了计算过程的资源需求随输入的函数增长。 +计算过程消耗资源的速率差异巨大,前面的例子已显示这一点。准确估计调用使用的空间或时间很难,取决于诸多因素。一个有用的分析方法是按需求相似的过程分类,常用分类是**增长阶(orders of growth)**,用简单术语描述资源需求随输入的增长。 -为了介绍增长阶的概念,我们将分析下面的函数 `count_factors` ,它用于计算能够整除输入 $n$ 的整数的数量。该函数尝试将 $n$ 除以小于等于其平方根的每个整数。这个实现利用了以下事实:如果 $k$ 能够整除 $n$ ,且 $k < \sqrt{n}$ ,那么必然存在另一个因子 $j = n / k$,使得 $j > \sqrt{n}$。 +为介绍增长阶,分析函数 `count_factors`:它计算能整除输入 $n$ 的整数个数,尝试用每个不超过平方根的整数去除 $n$。实现利用了:若 $k$ 整除 $n$ 且 $k<\sqrt{n}$,必有另一因子 $j=n/k$,且 $j>\sqrt{n}$。 -计算 `count_factors` 所需的时间是多少?准确的答案会因不同的计算机而异,但我们可以对涉及的计算量做出一些有用的一般观察。这个过程执行 `while` 语句的次数是小于等于 $\sqrt{n}$ 的最大整数。在 `while` 语句之前和之后的语句分别执行一次。因此,总共执行的语句数为 $w*\sqrt{n}+v$ ,其中 $w$ 是 `while` 循环体中的语句数, $v$ 是 `while` 循环外的语句数。虽然这不是一个精确的公式,但它通常能够描述作为输入 $n$ 的函数而要计算的时间量。 +计算 `count_factors` 需要多少时间?精确答案因机器而异,但可以做一些普遍观察:`while` 执行次数是小于等于 $\sqrt{n}$ 的最大整数;`while` 前后的语句各执行一次。总执行语句数约为 $w*\sqrt{n}+v$,其中 $w` 是循环体语句数,`v` 是循环外语句数。虽然不精确,但足以描述时间随输入的函数关系。 -要获得更精确的描述是很困难的。常量 $w$ 和 $v$ 实际上并不是常数,因为对因子的赋值语句有时会被执行,有时不会。增长阶分析使我们能够忽略这些细节,而是着重于增长的一般趋势。特别是, `count_factors` 的增长阶表达了以 $\sqrt{n}$ 速度计算 `count_factors(n)` 所需的时间,其中可能存在一些常量因子的误差范围。 +更精确的描述很难。$w$ 与 $v$ 并非恒定,因为因子相关赋值有时执行、有时不执行。增长阶分析让我们忽略这些细节,关注整体趋势。特别地,`count_factors` 的增长阶表明其时间以 $\sqrt{n}$ 级别增长,允许常数因子的误差。 -**Theta 符号(Theta Notation)** 是一种用于表示算法的渐进性能的数学符号。在 Theta 符号中,我们考虑一个参数 $n$ ,它衡量了某个计算过程的输入规模大小,并且用 $R(n)$ 表示该过程对于输入规模 $n$ 所需的某种资源量。在我们之前的例子中,我们将 $n$ 视为要计算给定函数的数值,但还有其他可能性。例如,如果我们的目标是计算一个数的平方根的近似值,我们可以将 $n$ 视为所需的有效位数。 +**Theta 符号(Theta notation)** 用于表示算法的渐进性能。设参数 $n$ 衡量输入规模,用 $R(n)$ 表示对规模 $n$ 所需的资源量。在前例中,$n$ 是要求值的数字,亦可有其他定义,例如把 $n$ 视为平方根近似的有效位数。 -$R(n)$ 可以表示所使用的内存量、执行的基本机器步骤数量等等。在每次执行固定数量步骤的计算机中,计算表达式所需的时间将与在计算过程中执行的基本步骤数量成正比。 +$R(n)$ 可表示内存用量、基本步骤数等。在每步耗时固定的机器上,计算时间与执行的基本步骤数成正比。 -我们可以说如果存在正常数 $k_1$ 和 $k_2$ (与 $n$ 无关),使得对于任何大于某个最小值 $m$ 的 $n$ ,成立如下不等式: +若存在与 $n$ 无关的正常数 $k_1$、$k_2$,使得对所有大于某最小值 $m$ 的 $n$ 满足 -$$k1⋅f(n)≤R(n)≤k2⋅f(n)$$ +$$k_1⋅f(n)≤R(n)≤k_2⋅f(n)$$ -我们称 $R(n)$ 的增长阶为 $Θ(f(n))$ ,用 $R(n)=Θ(f(n))$ 表示(读作“_theta of f(n)_”)。换句话说,对于大的 $n$ , $R(n)$ 的值总是夹在两个与 $f(n)$ 成比例的值之间: +则称 $R(n)$ 的增长阶为 $Θ(f(n))$,记作 $R(n)=Θ(f(n))$(读作 “theta of f(n)”)。也就是说,对大 $n$,$R(n)$ 夹在与 $f(n)$ 成比例的上下界之间: -- 一个下界 $k1 ⋅ f(n)$ 和 -- 一个上界 $k2 ⋅ f(n)$ +- 下界 $k_1⋅f(n)$ +- 上界 $k_2⋅f(n)$ -通过检查函数体,我们可以应用这个定义来展示计算 `count_factors(n)` 所需的步骤数量随着 $Θ(\sqrt{n})$ 增长 +检查函数体可用此定义展示 `count_factors(n)` 的步骤数量以 $Θ(\sqrt{n})$ 增长。 -首先,我们选择 $k_1=1$ 和 $m=0$ ,以便下界表明对于任何 $n>0$ ,`count_factors(n)` 至少需要 $1⋅\sqrt{n}$ 步。在 `while` 循环之外至少有 4 行被执行,每行至少需要 1 步来执行。在 `while` 循环体内至少有 2 行被执行,还有 `while` 头本身。所有这些都需要至少 1 步。 `while` 循环体至少被执行 $\sqrt{n}-1$ 次。组合这些下界,我们可以看到该过程至少需要 $4+3⋅(\sqrt{n}−1)$ 步,这总是大于 $k_1⋅\sqrt{n}$ 。 +下界:取 $k_1=1$、$m=0$,对任意 $n>0$,`count_factors(n)` 至少需 $1⋅\sqrt{n}$ 步。`while` 外至少执行 4 行,每行至少 1 步;`while` 体内至少 2 行,加上 `while` 头,每次至少 1 步;循环体至少执行 $\sqrt{n}-1$ 次。合计至少 $4+3⋅(\sqrt{n}-1)$ 步,始终大于 $k_1⋅\sqrt{n}$。 -其次,我们可以验证上界。我们假设 `count_factors` 函数体内的任何一行最多需要 $p$ 步。尽管这个假设对于 Python 中的每一行代码都不成立,但在这种情况下是成立的。然后,计算 `count_factors(n)` 最多可能需要 $p⋅(5+4\sqrt{n})$ 步,因为在 `while` 循环之外有 5 行代码,在循环内有 4 行代码(包括 `while` 头)。即使每个 `if` 头都评估为 true,这个上界仍然成立。最后,如果我们选择 $k_2=5p$ ,那么所需的步骤总是小于 $k_2⋅\sqrt{n}$ 。我们的论证到这里完成了。 +上界:假设函数体任一行至多需 $p$ 步(此处成立)。则 `count_factors(n)` 最多需 $p⋅(5+4\sqrt{n})$ 步:`while` 外 5 行,循环内 4 行(含 `while` 头),即便每个 `if` 都为真,上界仍成立。取 $k_2=5p$,所需步骤始终小于 $k_2⋅\sqrt{n}$。证明完成。 ## 2.8.4 示例:指数运算 diff --git a/sicp/2/9.md b/sicp/2/9.md index eee6ff3..60ef32f 100644 --- a/sicp/2/9.md +++ b/sicp/2/9.md @@ -8,15 +8,15 @@ 对应:HW 05、Lab 08、Disc 08 ::: -对象可以以其他的对象作为自己的属性值。当这个类下的对象实例有一个属性的值还属于这个类时,这个对象就是一个递归对象。 +对象可以把其他对象作为属性。当某类的实例有个属性值也是这个类的实例时,它就是**递归对象**。 ## 2.9.1 类:链表 -在之前的章节中我们已经提到过,链表 (Linked List) 由两个部分组成:第一个元素和链表剩下的部分。而剩下的这部分链表它本身就是个链表,这就是链表的递归定义法。其中,一个比较特殊的情况是空链表——他没有第一个元素和剩下的部分。 +链表(Linked List)由两部分组成:第一个元素和剩余部分,而剩余部分本身也是链表,这是链表的递归定义。特殊情况是空链表——没有首元素也没有剩余部分。 -链表是一种序列 (sequence):它具有有限的长度并且支持通过索引选择元素。 +链表是一种序列(sequence):长度有限,支持按索引取元素。 -现在我们可以实现具有相同行为的类。在现在这个版本中,我们将使用专用方法名来定义它的行为,专用方法允许我们的类可以使用 Python 内置的 `len` 函数和元素选择操作符(方括号或 `operator.getitem` )。这些内置函数将调用类的专用方法:长度由 `__len__` 计算,元素选择由 `__getitem__` 计算。空链表由一个长度为 0 且没有元素的空元组表示。 +现在可以实现具备相同行为的类。本版用专用方法定义行为,使类可配合内置 `len` 与元素选择(方括号或 `operator.getitem`)。这些内置函数会调用类的专用方法:长度由 `__len__` 计算,元素选择由 `__getitem__` 计算。空链表用长度为 0 的空元组表示。 ```py >>> class Link: @@ -40,11 +40,11 @@ 4 ``` -以上的 `__len__` 和 `__getitem__` 的定义都是递归的。Python 的内置函数 `len` 在它接收属于用户定义的类的实例时调用了 `__len__` 方法。类似地,元素选择操作符则会调用 `__getitem__` 方法。因此,这两个方法定义的主体中都会间接的调用他们自己。对于 `__len__` 来说,基线条件 (base case) 就是当 `self.rest` 计算得到一个空元组,也就是 `Link.empty` 时,此时长度为 0。 +`__len__` 和 `__getitem__` 的定义都是递归的。`len` 接收用户自定义类实例时会调用 `__len__`,元素选择也会调用 `__getitem__`,因此方法体会间接递归。`__len__` 的基线条件是 `self.rest` 为 `Link.empty`,此时长度为 0。 内置的 `isinstance` 函数返回第一个参数的类型是否属于或者继承自第二个参数。`isinstance(rest, Link)` 在 `rest` 是 `Link` 的实例或 `Link` 的子类的实例时为 `True`。 -我们对于链表的类定义已经很完备了,但是现在我们还无法直观地看到 `Link` 的实例。为了方便之后的调试工作,我们再定义一个函数去将一个 `Link` 实例转换为一个字符串表达式。 +类定义已完善,但还不能直观看到 `Link` 实例。为便于调试,定义一个函数把 `Link` 实例转换为字符串表达式。 ```py >>> def link_expression(s): @@ -58,7 +58,7 @@ 'Link(3, Link(4, Link(5)))' ``` -用这个方法去展示一个链表实在是太方便了,以至于我想在任何需要展示一个 `Link` 的实例的时候都用上它。为了达到这个美好愿景,我们可以将函数 `link_expression` 作为专用方法 `__repr__` 的值。Python 在展示一个用户定义的类的实例时,会调用它们的 `__repr__` 方法。 +展示链表如此方便,以至于希望随处都用它。可以把 `link_expression` 设为专用方法 `__repr__`,Python 展示用户定义类实例时会调用 `__repr__`。 ```py >>> Link.__repr__ = link_expression @@ -66,7 +66,7 @@ Link(3, Link(4, Link(5))) ``` -`Link` 类具有闭包性质 (closure property)。就像列表的元素可以是列表一样,一个 `Link` 实例的第一个元素也可以是一个 `Link` 实例。 +`Link` 类具有**闭包性质(closure property)**:如同列表元素可以是列表,一个 `Link` 的第一个元素也可以是 `Link`。 ```py >>> s_first = Link(s, Link(6)) @@ -85,7 +85,7 @@ Link(Link(3, Link(4, Link(5))), Link(6)) 5 ``` -递归函数特别适合操作链表。比如,递归函数 `extend_link` 建立了一个新的链表,这个新链表是由链表 `s` 和其后边跟着的链表 `t` 组成的。将这个函数作为 `Link` 类的方法 `__add__` 就可以仿真内置列表的加法运算。 +递归函数特别适合操作链表。比如递归函数 `extend_link` 构造新链表,将链表 `s` 与其后的 `t` 串接。把它作为 `Link` 的 `__add__` 方法即可仿真内置列表加法。 ```py >>> def extend_link(s, t): @@ -112,7 +112,7 @@ Link(3, Link(4, Link(5, Link(3, Link(4, Link(5)))))) Link(9, Link(16, Link(25))) ``` -函数 `filter_link` 返回了一个链表,这个链表过滤掉了原链表 `s` 中使函数 `f` 不返回真的元素,留下了其余的元素。通过组合使用 `map_link` 和 `filter_link`,我们可以达到和列表推导式相同的逻辑过程和结果。 +函数 `filter_link` 返回过滤后的链表,去掉使 `f` 为假的元素,保留其余元素。组合 `map_link` 与 `filter_link`,即可得到与列表推导式相同的逻辑与结果。 ```py >>> def filter_link(f, s): @@ -131,7 +131,7 @@ Link(9, Link(25)) [9, 25] ``` -函数 `join_link` 递归的构造了一个 包含着所有在链表里的元素,并且这些元素被字符串 `separator` 分开 的字符串。这个结果相较于 `link_expression` 来说就更加简练。 +函数 `join_link` 递归构造一个字符串,包含链表所有元素并用 `separator` 分隔,相比 `link_expression` 更简练。 ```py >>> def join_link(s, separator): @@ -145,16 +145,16 @@ Link(9, Link(25)) '3, 4, 5' ``` -**递归构造 (Recursive Construction).** 当以增量方式构造序列时,链表特别有用,这种情况在递归计算中经常出现 +**递归构造(Recursive construction)**。增量构造序列时链表尤其有用,这在递归计算中常见。 -第一章我们介绍过的函数 `count_partitions` 通过树递归计算了使用大小最大为 `m` 的数对整数 `n` 进行分区的方法的个数。通过序列,我们还可以使用类似的过程显式枚举这些分区。 +第一章的 `count_partitions` 用树递归计算用不超过 `m` 的数将整数 `n` 分区的方法数。用序列也能以类似过程显式枚举这些分区。 -与计数方法个数的方法相同,我们利用相同的递归统计方法:将一个数 `n` 在最大数限制为 `m` 下分区包含两种情况: +分区遵循同样的递归:在最大数不超过 `m` 下划分 `n` 有两种情况: -1. 用 `m` 及以内的整数划分 `n-m` -2. 用 `m-1` 及以内的整数划分 `n` +1. 用不超过 `m` 的整数划分 `n-m` +2. 用不超过 `m-1` 的整数划分 `n` -对于基线情况,我们知道 0 只有一个分区方法,而划分一个负整数或使用小于 1 的数划分是不可能的。 +基线:0 只有一种分区;分区负数或用小于 1 的数分区不可能。 ```py >>> def partitions(n, m): @@ -172,9 +172,9 @@ Link(9, Link(25)) return with_m + without_m ``` -在递归情况下,我们构造两个分区子列表。第一个使用 `m`,因此我们将 `m` 添加到 `using_m` 的结果的每个元素中以形成 `with_m`。 +递归情况下,我们构造两个分区子列表。第一个使用 `m`,于是将 `m` 添加到 `using_m` 的每个元素形成 `with_m`。 -分区的结果是高度嵌套的:是一个链表的链表。使用带有适当分隔符的 `join_link`,我们可以以易读的方式显示各个分区。 +分区结果高度嵌套:链表的链表。用带合适分隔符的 `join_link` 可以以易读方式展示各个分区。 ```py >>> def print_partitions(n, m):