├── .gitignore
├── .vitepress
├── config.js
└── theme
│ ├── Layout.vue
│ └── index.js
├── .vscode
└── extensions.json
├── LICENSE
├── README.md
├── netlify.toml
├── package.json
├── pnpm-lock.yaml
└── sicp
├── 1
├── 1.md
├── 2.md
├── 3.md
├── 4.md
├── 5.md
├── 6.md
└── 7.md
├── 2
├── 1.md
├── 2.md
├── 3.md
├── 4.md
├── 5.md
├── 6.md
├── 7.md
├── 8.md
└── 9.md
├── 3
├── 1.md
├── 2.md
├── 3.md
├── 4.md
└── 5.md
├── 4
├── 1.md
├── 2.md
├── 3.md
├── 4.md
├── 5.md
├── 6.md
├── 7.md
└── 8.md
├── index.md
└── public
├── logo.svg
└── sicp
├── call_expression.png
├── celsius_fahrenheit_constraint.png
├── constraints.png
├── curves.png
├── deadlock.png
├── distributed_system.png
├── expression_tree.png
├── factorial_machine.png
├── fib.png
├── fib_memo.png
├── function_abs.png
├── function_print.png
├── multiple_inheritance.png
├── newton.png
├── pi_sum.png
├── set_trees.png
├── sier.png
└── star.png
/.gitignore:
--------------------------------------------------------------------------------
1 | /coverage
2 | /src/client/shared.ts
3 | /src/node/shared.ts
4 | *.log
5 | *.tgz
6 | .DS_Store
7 | .idea
8 | .temp
9 | .vite_opt_cache
10 | dist
11 | examples-temp
12 | node_modules
13 | pnpm-global
14 | cache
--------------------------------------------------------------------------------
/.vitepress/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | title: 'CS Five',
3 | titleTemplate: ':title',
4 | lang: 'zh-Hans',
5 | description: 'SICP Python 版,CS61A 教材中文翻译',
6 | lastUpdated: true,
7 | cleanUrls: true,
8 | srcDir: './sicp',
9 |
10 | head: [
11 | ['link', { rel: 'icon', href: '/logo.svg' }],
12 | [
13 | 'meta',
14 | {
15 | name: 'keywords',
16 | content: 'SICP, Python, CS61A, Composing Programs, 中文, 翻译',
17 | },
18 | ],
19 | [
20 | 'meta',
21 | {
22 | name: 'description',
23 | content: 'Composing Programs, SICP Python, CS61A Textbook 教材中文翻译版本',
24 | },
25 | ],
26 | ['meta', { name: 'author', content: 'CS Five' }],
27 | ],
28 |
29 | markdown: {
30 | theme: { light: 'github-light', dark: 'github-dark' },
31 | math: true,
32 | },
33 |
34 | vite: {
35 | optimizeDeps: {
36 | exclude: ['@nolebase/vitepress-plugin-enhanced-readabilities/client', 'vitepress', '@nolebase/ui'],
37 | },
38 | ssr: {
39 | noExternal: ['@nolebase/vitepress-plugin-enhanced-readabilities', '@nolebase/ui'],
40 | },
41 | },
42 |
43 | themeConfig: {
44 | sidebar: { '/': sidebar() },
45 | search: { provider: 'local' },
46 | socialLinks: [{ icon: 'github', link: 'https://github.com/csfive' }],
47 |
48 | logo: {
49 | src: '/logo.svg',
50 | width: 24,
51 | height: 24,
52 | },
53 |
54 | editLink: {
55 | pattern: 'https://github.com/csfive/composing-programs-zh/edit/main/sicp/:path',
56 | text: '在 GitHub 上编辑此页面',
57 | },
58 |
59 | docFooter: {
60 | prev: '上一页',
61 | next: '下一页',
62 | },
63 |
64 | footer: {
65 | message: '基于 MIT 许可发布',
66 | copyright: `版权所有 © 2022-${new Date().getFullYear()} CS Five`,
67 | },
68 |
69 | outline: {
70 | label: '页面导航',
71 | },
72 |
73 | lastUpdated: {
74 | text: '最后更新于',
75 | formatOptions: {
76 | dateStyle: 'short',
77 | timeStyle: 'medium',
78 | },
79 | },
80 |
81 | langMenuLabel: '多语言',
82 | returnToTopLabel: '回到顶部',
83 | sidebarMenuLabel: '菜单',
84 | darkModeSwitchLabel: '主题',
85 | lightModeSwitchTitle: '切换到浅色模式',
86 | darkModeSwitchTitle: '切换到深色模式',
87 | },
88 | }
89 |
90 | function sidebar() {
91 | return [
92 | { text: '简介', link: '/' },
93 | {
94 | text: '第一章:使用函数构建抽象',
95 | collapsed: false,
96 | items: [
97 | { text: '1.1 开始', link: '/1/1' },
98 | { text: '1.2 编程要素', link: '/1/2' },
99 | { text: '1.3 定义新的函数', link: '/1/3' },
100 | { text: '1.4 设计函数', link: '/1/4' },
101 | { text: '1.5 控制', link: '/1/5' },
102 | { text: '1.6 高阶函数', link: '/1/6' },
103 | { text: '1.7 递归函数', link: '/1/7' },
104 | ],
105 | },
106 | {
107 | text: '第二章:使用数据构建抽象',
108 | collapsed: true,
109 | items: [
110 | { text: '2.1 引言', link: '/2/1' },
111 | { text: '2.2 数据抽象', link: '/2/2' },
112 | { text: '2.3 序列', link: '/2/3' },
113 | { text: '2.4 可变数据', link: '/2/4' },
114 | { text: '2.5 面向对象编程', link: '/2/5' },
115 | { text: '2.6 实现类和对象', link: '/2/6' },
116 | { text: '2.7 对象抽象', link: '/2/7' },
117 | { text: '2.8 效率', link: '/2/8' },
118 | { text: '2.9 递归对象', link: '/2/9' },
119 | ],
120 | },
121 | {
122 | text: '第三章:计算机程序的解释',
123 | collapsed: true,
124 | items: [
125 | { text: '3.1 引言', link: '/3/1' },
126 | { text: '3.2 函数式编程', link: '/3/2' },
127 | { text: '3.3 异常', link: '/3/3' },
128 | { text: '3.4 组合语言的解释器', link: '/3/4' },
129 | { text: '3.5 抽象语言的解释器', link: '/3/5' },
130 | ],
131 | },
132 | {
133 | text: '第四章:数据处理',
134 | collapsed: true,
135 | items: [
136 | { text: '4.1 引言', link: '/4/1' },
137 | { text: '4.2 隐式序列', link: '/4/2' },
138 | { text: '4.3 声明式编程', link: '/4/3' },
139 | { text: '4.4 Logic 语言编程', link: '/4/4' },
140 | { text: '4.5 合一', link: '/4/5' },
141 | { text: '4.6 分布式计算', link: '/4/6' },
142 | { text: '4.7 分布式数据处理', link: '/4/7' },
143 | { text: '4.8 并行计算', link: '/4/8' },
144 | ],
145 | },
146 | ]
147 | }
148 |
--------------------------------------------------------------------------------
/.vitepress/theme/Layout.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
74 |
--------------------------------------------------------------------------------
/.vitepress/theme/index.js:
--------------------------------------------------------------------------------
1 | import { useData, useRoute } from 'vitepress'
2 | import giscusTalk from 'vitepress-plugin-comment-with-giscus'
3 | import DefaultTheme from 'vitepress/theme'
4 | import { h } from 'vue'
5 | import {
6 | NolebaseEnhancedReadabilitiesMenu,
7 | NolebaseEnhancedReadabilitiesScreenMenu,
8 | } from '@nolebase/vitepress-plugin-enhanced-readabilities/client'
9 | import Layout from './Layout.vue'
10 | import '@nolebase/vitepress-plugin-enhanced-readabilities/client/style.css'
11 |
12 | export default {
13 | ...DefaultTheme,
14 | Layout: () => {
15 | return h(Layout, null, {
16 | 'nav-bar-content-after': () => h(NolebaseEnhancedReadabilitiesMenu),
17 | 'nav-screen-content-after': () => h(NolebaseEnhancedReadabilitiesScreenMenu),
18 | })
19 | },
20 | enhanceApp(ctx) {
21 | DefaultTheme.enhanceApp(ctx)
22 | },
23 | setup() {
24 | const { frontmatter } = useData()
25 | const route = useRoute()
26 | giscusTalk(
27 | {
28 | repo: 'csfive/composing-programs-zh',
29 | repoId: 'R_kgDOI7ryQw',
30 | category: 'Announcements',
31 | categoryId: 'DIC_kwDOI7ryQ84CWSPL',
32 | mapping: 'title',
33 | strict: '1',
34 | },
35 | {
36 | frontmatter,
37 | route,
38 | },
39 | )
40 | },
41 | }
42 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["streetsidesoftware.code-spell-checker", "yzhang.markdown-all-in-one"]
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Mancuoj
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # COMPOSING PROGRAMS 
2 |
3 | 这是一个翻译项目,原书为伯克利 CS61A 的配套教材 [Composing Programs](https://www.composingprograms.com/),也是计算机一大圣经 [SICP](https://book.douban.com/subject/1148282/) 的 Python 版本。
4 |
5 | 项目现在为维护状态,如果您发现了翻译的错漏或含混之处,可以提交 issue 或者 PR,我会抽时间 review 或者修改。同时感谢所有参与翻译的同学,也感谢每一个给项目 star 的同学。
6 |
7 | ## 贡献者列表
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build.environment]
2 | NODE_VERSION = "20"
3 |
4 | [build]
5 | publish = ".vitepress/dist"
6 | command = "pnpm build"
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "dev": "vitepress dev",
4 | "build": "vitepress build",
5 | "format": "prettier --cache --write ."
6 | },
7 | "devDependencies": {
8 | "@mancuoj/prettier-config": "latest",
9 | "markdown-it-mathjax3": "latest",
10 | "prettier": "latest",
11 | "vitepress": "latest",
12 | "vitepress-plugin-comment-with-giscus": "latest",
13 | "@nolebase/vitepress-plugin-enhanced-readabilities": "latest",
14 | "vue": "latest"
15 | },
16 | "prettier": "@mancuoj/prettier-config"
17 | }
18 |
--------------------------------------------------------------------------------
/sicp/1/1.md:
--------------------------------------------------------------------------------
1 | # 1.1 开始
2 |
3 | ::: details INFO
4 | 译者:[Mancuoj](https://github.com/mancuoj)
5 |
6 | 来源:[1.1 Getting Started](https://www.composingprograms.com/pages/11-getting-started.html)
7 |
8 | 对应:HW 01
9 | :::
10 |
11 | 计算机科学是一门极其广泛的学科,每年,全球的分布式系统、人工智能、机器人、图形学、安全、科学计算、计算机体系结构以及更多新兴的二级领域都会随着计算机科学的新技术和新发现而扩展。计算机科学的迅猛发展已经深刻地改变了人类生活的各个层面。商业、通信、科学、艺术、娱乐和政治都在计算的领域中得到了新的定义。
12 |
13 | 之所以计算机科学的巨大生产力能够成为可能,都是因为其建立在一套优雅而强大的基本思想之上。所有计算都始于三点:信息的表示、处理的逻辑、设计抽象来管理逻辑的复杂性,掌握这些基础知识需要我们去精确理解计算机程序的构造和解释。
14 |
15 | 长期以来,我们都使用 Harold Abelson、Gerald Jay Sussman 和 Julie Sussman 三人编写的经典教科书《计算机程序的构造与解释》即 SICP 来教授这些基本思想,而本书大量借鉴了这本教科书。
16 |
17 | ## 1.1.1 Python 编程
18 |
19 | > A language isn't something you learn so much as something you join.
20 | >
21 | > — [Arika Okrent](http://arikaokrent.com/)
22 |
23 | 为了定义计算过程,我们需要一种被人们广泛使用和各类电脑广泛接受的编程语言,在本文中,我们将主要使用 [Python](http://docs.python.org/py3k/) 语言。
24 |
25 | Python 是一种被广泛使用的编程语言,其吸引了来自各行各业的爱好者:Web 程序员、游戏工程师、科学家、学者甚至是新语言的设计者。当你学习 Python 时,你就加入到了一个拥有着百万开发人员的社区。开发者社区是非常重要的组织:成员可以互相帮助解决问题,分享他们的项目和经验,共同开发软件和工具。经常会有一些专注的成员因其贡献而获得名誉和广泛的尊重。
26 |
27 | Python 语言本身是一个大型志愿者社区的产物,它以其贡献者的 [多元化](http://python.org/community/diversity/) 为傲。该语言由 [Guido van Rossum](http://en.wikipedia.org/wiki/Guido_van_Rossum) 在 20 世纪 80 年代末构思并实现,他在 [Python 3 教程](http://docs.python.org/py3k/tutorial/appetite.html) 的第一章中解释了 Python 在当今的众多语言中受欢迎的原因。
28 |
29 | Python 作为一种教学语言非常出色,因为在其整个历史中,Python 的开发人员一直在强调 Python 代码的人类可解释性,并在 [Python 之禅](http://www.python.org/dev/peps/pep-0020/) 的美观、简约和可读的原则下进一步加强。因为它宽泛的特性能够支持各种不同的编程风格,所以十分适合本书,我们将在之后逐一探讨这些风格。从来没有单一的 Python 编程方法,但遵守开发人员社区共享的一组约定会有助于现有程序的阅读、理解和扩展。Python 巨大的灵活性和易学性可以使学生探索许多编程范式,然后将获得的新知识应用到数以千计的 [正在进行的项目](http://pypi.python.org/pypi) 中。
30 |
31 | 这本书极力保留了 [SICP](http://mitpress.mit.edu/sicp) 的精神:通过抽象和严格的计算模型逐步介绍 Python 的特性。此外,本书还提供了 Python 的编程实践,包括一些高级语言功能和说明示例。随着阅读的进行,你会自然而然地增加使用 Python 的能力。
32 |
33 | 开始使用 Python 编程的最佳方法就是直接与解释器进行交互。本节将介绍如何安装 Python 3、如何启动与解释器的交互式会话及如何开始编程。
34 |
35 | ## 1.1.2 安装 Python 3
36 |
37 | 与所有伟大的软件一样,Python 有很多版本,而本文将使用最新稳定版本的 Python 3。许多计算机已经安装了旧版本的 Python,如 Python 2.7,它们与本文要求不符,你需要使用任意安装了 Python 3 的计算机(别担心,Python 是免费的)。
38 |
39 | 你可以从 Python 下载页面点击以 3 开头的版本下载 Python 3,并按照安装程序的说明完成安装。
40 |
41 | 如需进一步指导,请观看由 Julia Oh 创建的有关 Python 3 的 [Windows 安装](http://www.youtube.com/watch?v=54-wuFsPi0w) 和 [Mac 安装](http://www.youtube.com/watch?v=smHuBHxJdK8) 的视频教程。
42 |
43 | ## 1.1.3 交互式会话
44 |
45 | 在与 Python 的交互式会话中,你可以在提示符 `>>>` 后键入一些 Python 代码,Python 解释器会读取并执行你键入的各种命令。
46 |
47 | 要启动交互式会话,请在终端 (Mac/Unix/Linux) 中键入 `python3` 或在 Windows 中打开 Python 3 应用程序。
48 |
49 | 如果你看到了 Python 提示符 `>>>`,则已经成功启动交互式会话。我们会使用提示符和一些输入来展示示例。
50 |
51 | ```py
52 | >>> 2 + 2
53 | 4
54 | ```
55 |
56 | **交互控制**:每个会话都会保留键入内容的历史记录,可以按下 `-P`(上一个)和 `-N`(下一个)浏览该历史记录。使用 `-D` 会退出会话并丢弃此历史记录。在某些系统上,上、下箭头也可以用于循环浏览历史记录。
57 |
58 | ## 1.1.4 第一个例子
59 |
60 | > And, as imagination bodies forth
61 | >
62 | > The forms of things to unknown, and the poet's pen
63 | >
64 | > Turns them to shapes, and gives to airy nothing
65 | >
66 | > A local habitation and a name.
67 | >
68 | > — William Shakespeare, A Midsummer-Night's Dream
69 |
70 | 本节将以一个使用“多种语言特性”的示例来介绍 Python,你可以将此部分视为即将到来的功能的预览,在下一节中,我们将从头开始逐步了解整个语言。
71 |
72 | Python 内置了一些常见编程功能,例如处理文本、显示图形以及通过互联网进行通信。下面这行 Python 代码
73 |
74 | ```py
75 | >>> from urllib.request import urlopen
76 | ```
77 |
78 | 是一个 `import` 语句,它会导入一个用于“访问互联网数据”的功能,该功能特别提供了一个名为 `urlopen` 的函数,可以访问 URL(也就是访问互联网上的某个网址)上的内容。
79 |
80 | **语句和表达式**:Python 代码由表达式和语句组成,从广义上讲,计算机程序由以下指令组成
81 |
82 | 1. 计算一些值
83 | 2. 执行一些操作
84 |
85 | 语句通常描述操作,Python 解释器每执行一条语句,计算机就会执行相应的操作。另外,表达式通常用于描述计算,当 Python 计算一个表达式时,它会计算出该式的值。本章会介绍语句和表达式的几种类型。
86 |
87 | 下面的赋值语句
88 |
89 | ```py
90 | >>> shakespeare = urlopen('https://www.composingprograms.com/shakespeare.txt')
91 | ```
92 |
93 | 将名称 `shakespeare` 与 `=` 后面的表达式的值相连,这个表达式将 `urlopen` 函数应用在了一个包含莎士比亚 37 部戏剧完整文本的 URL 上。
94 |
95 | **函数**:函数封装了操作数据的逻辑。`urlopen` 就是一个函数,而网址是一个数据,莎士比亚的戏剧是另一个数据。从前者到后者的转换过程可能会很复杂,但我们可以将这种复杂性隐藏在一个函数中,从而能够使用一个简单的表达式来跳过该过程。函数是本章的主题。
96 |
97 | 另一个赋值语句
98 |
99 | ```py
100 | >>> words = set(shakespeare.read().decode().split())
101 | ```
102 |
103 | 将 `words` 与莎士比亚戏剧中出现的共 33,721 个单词的集合相连。其命令链调用了 `read`、`decode` 和 `split`,每个函数都会操作一个中间的计算实体:从 URL 中 `read`(读取)数据,然后将数据 `decode` (解码)为文本,最后将文本 `split` (拆分)为单词放在一个 `set` 中。
104 |
105 | **对象**:`set` 就是一种对象,支持如计算交集和集合关系(membership)等运算。对象无缝整合了数据以及用于操作该数据的逻辑,并隐藏了二者的复杂性。对象是第二章的主题。
106 |
107 | 最后,这个表达式
108 |
109 | ```py
110 | >>> {w for w in words if len(w) == 6 and w[::-1] in words}
111 | {'redder', 'drawer', 'reward', 'diaper', 'repaid'}
112 | ```
113 |
114 | 是一个复合表达式,它的计算结果是反向拼写同时也为单词的莎士比亚单词集合。神秘符号 `w[::-1]` 表示枚举单词中的每个字母,其中 -1 代表反向枚举。当你在交互式会话中输入表达式时,Python 会在下一行打印它的值。
115 |
116 | **解释器**:复合表达式的求解需要以一个可预测的方式来精确解释代码的过程。实现这样的过程,用于计算复合表达式的程序就称为解释器。解释器的设计和实现是第三章的主题。
117 |
118 | 与其他计算机程序相比,编程语言的解释器具有独特的通用性。Python 在设计时并不会考虑莎士比亚,但它的高度灵活性使我们能够只用少量的语句和表达式来处理大量的文本。
119 |
120 | 最后,我们会发现所有这些核心概念都是紧密相关的:函数是对象,对象是函数,解释器是二者的实例。但是,清楚地理解每一个概念及其在组织代码中的作用对于掌握编程艺术至关重要。
121 |
122 | ## 1.1.5 错误
123 |
124 | Python 正在等待你的命令。即使你可能还不了解其完整的词汇和结构,我们仍鼓励你尝试使用该语言。当然也请你为错误做好准备,因为计算机在极其快速灵活的同时也十分古板。计算机的特性在 [斯坦福的入门课程](http://web.stanford.edu/class/cs101/code-1-introduction.html) 中被描述为
125 |
126 | > The fundamental equation of computers is: `computer = powerful + stupid`
127 | >
128 | > Computers are very powerful, looking at volumes of data very quickly. Computers can perform billions of operations per second, where each operation is pretty simple.
129 | >
130 | > Computers are also shockingly stupid and fragile. The operations that they can do are extremely rigid, simple, and mechanical. The computer lacks anything like real insight ... it's nothing like the HAL 9000 from the movies. If nothing else, you should not be intimidated by the computer as if it's some sort of brain. It's very mechanical underneath it all.
131 | >
132 | > Programming is about a person using their real insight to build something useful, constructed out of these teeny, simple little operations that the computer can do.
133 | >
134 | > — Francisco Cai and Nick Parlante, Stanford CS101
135 |
136 | 当你尝试使用 Python 解释器时,计算机的古板会立即显现出来:即使是最小的拼写和格式更改也会导致预料之外的输出和错误。
137 |
138 | 学着解释错误和找到错误的原因称为调试,关于调试的一些指导原则是:
139 |
140 | 1. **增量测试**:每个编写良好的程序都由可以单独测试的小型模块化组件组成。尽快测试你已经编写的所有内容,以尽早发现问题并获得对组件的信心。
141 | 2. **隔离错误**:语句输出中的错误通常可归因于特定的模块化组件。所以在诊断问题时,先追踪错误到最小的代码片段,然后再试着修复问题。
142 | 3. **检查你的假设**:解释器会一字不漏地执行你的指示——不多也不少。当某些代码的行为与程序员假设的行为不匹配时,它们的输出就是不符合预期的。明确你的假设,然后将调试的工作集中在验证你的假设上。
143 | 4. **咨询别人**:你不是一个人!如果你不理解错误信息,请询问朋友、老师或搜索引擎。如果你已经找出了一个错误,但却不知道如何更正它,可以请其他人查看。在小组解决问题的过程中会分享很多有价值的编程知识。
144 |
145 | 增量测试、模块化设计、明确的假设和团队合作是贯穿本书的主题,希望它们也将贯穿你的计算机科学职业生涯。
146 |
--------------------------------------------------------------------------------
/sicp/1/2.md:
--------------------------------------------------------------------------------
1 | # 1.2 编程要素
2 |
3 | ::: details INFO
4 | 译者:[Mancuoj](https://github.com/mancuoj)
5 |
6 | 来源:[1.2 Elements of Programming](https://www.composingprograms.com/pages/12-elements-of-programming.html)
7 |
8 | 对应:HW 01
9 | :::
10 |
11 | 编程语言不仅是一种指挥计算机执行任务的手段,它还应该成为一种框架,使我们能够在其中组织自己有关计算过程的思想。程序也用于在编程社区的成员之间传达想法,所以程序必须是人类可以阅读的,并且“恰巧”能被机器执行。
12 |
13 | 这样,当我们描述一种语言时,就需要特别注意该语言所提供的能够将简单思想组合成复杂思想的工具。每一种强大的语言都有这样三种机制:
14 |
15 | - **原始表达式和语句**:语言所关心的最简单的个体
16 | - **组合方法**:由简单元素组合构建复合元素
17 | - **抽象方法**:命名复合元素,并将其作为单元进行操作
18 |
19 | 在编程中,我们只会处理两种元素:函数和数据(之后你会发现它们实际上并不是泾渭分明的),不那么正式的说法是:数据是我们想要操作的东西,而函数是操作这些数据的规则的描述。因此,任何强大的编程语言都必须能表达基本的数据和函数,并且提供对函数和数据进行组合和抽象的方法。
20 |
21 | ## 1.2.1 表达式
22 |
23 | 上一节中,我们完整尝试了 Python 解释器,而下面我们将重新开始,一步步地讲解 Python 语言。如果示例看起来过于简单,请耐心等待,更令人振奋的东西在后面呢。
24 |
25 | 我们从一种基本表达式“数字 number”开始,更准确地说,是你键入的,十进制数字组成的表达式。
26 |
27 | ```py
28 | >>> 42
29 | 42
30 | ```
31 |
32 | 表达式表示的数字可以与数学运算符组合形成一个复合表达式,解释器将对其进行求值:
33 |
34 | ```py
35 | >>> -1 - -1
36 | 0
37 | >>> 1/2 + 1/4 + 1/8 + 1/16 + 1/32 + 1/64 + 1/128
38 | 0.9921875
39 | ```
40 |
41 | 这些数学表达式使用中缀表示法(infix notation),运算符(例如 +、-、\* 或 /)出现在操作数之间。Python 包含许多种形成复合表达式的方法,我们会在学习中慢慢引入新的表达式形式和它们所支持的语言特性,而不是立即把它们列举出来。
42 |
43 | ## 1.2.2 调用表达式
44 |
45 | 最重要的一种复合表达式是调用表达式,它将函数运用于一些参数上。回想一下代数中的函数的数学概念:函数就是从一些输入参数到输出值的映射。例如,`max` 函数会输出一个最大的输入值,也就是将多个输入映射到了单个输出上。Python 中函数应用的方式与传统数学相同。
46 |
47 | ```py
48 | >>> max(7.5, 9.5)
49 | 9.5
50 | ```
51 |
52 | 这个调用表达式包含子表达式(subexpressions):在括号之前是一个运算符表达式,而括号里面是一个以逗号分隔的操作数表达式的列表。
53 |
54 | 
55 |
56 | 运算符指定了一个函数,在对这个调用表达式进行求值时,我们会说:使用参数 7.5 和 9.5 来调用函数 `max`,最后返回 9.5。
57 |
58 | 调用表达式中参数的顺序是很重要的。例如,`pow` 函数的第二个参数是第一个参数的幂。
59 |
60 | ```py
61 | >>> pow(100, 2)
62 | 10000
63 | >>> pow(2, 100)
64 | 1267650600228229401496703205376
65 | ```
66 |
67 | 函数符号相比传统的中缀数学符号有三个主要优点。首先,因为函数名总是在参数前面,函数可以接收任意数量的参数而不会产生歧义。
68 |
69 | ```py
70 | >>> max(1, -2, 3, -4)
71 | 3
72 | ```
73 |
74 | 其次,函数可以直接扩展为嵌套(nested)表达式,其元素本身就是复合表达式。不同于中缀复合表达式,调用表达式的嵌套结构在括号中是完全明确的。
75 |
76 | ```py
77 | >>> max(min(1, -2), min(pow(3, 5), -4))
78 | -2
79 | ```
80 |
81 | 这种嵌套的深度(理论上)没有任何限制,Python 解释器可以解释任何复杂的表达式。但人类很快就会被多层嵌套搞晕,所以作为一个程序员,你的一个重要目标就是:构造你自己、你的编程伙伴和其他任何可能阅读你代码的人都可以解释的表达式。
82 |
83 | 第三点,数学符号在形式上多种多样:星号表示乘法,上标表示幂指数,水平横杠表示除法,带有倾斜壁板的屋顶表示平方根,而其中一些符号很难被输入!但是,所有这些复杂事物都可以通过调用表达式的符号来进行统一。Python 除了支持常见的中缀数学符号(如 + 和 -)之外,其他任何运算符都可以表示为一个带有名称的函数。
84 |
85 | ## 1.2.3 导入库函数
86 |
87 | Python 定义了大量的函数,包括上一节中提到的运算符函数,但默认情况下我们不能直接使用名字来调用它们。Python 将已知函数和其他东西组织起来放入到了模块中,而这些模块共同组成了 Python 库。我们要使用的时候需要导入它们,例如,`math` 模块提供了各种熟悉的数学函数:
88 |
89 | ```py
90 | >>> from math import sqrt
91 | >>> sqrt(256)
92 | 16.0
93 | ```
94 |
95 | `operator` 模块提供了中缀运算符对应的函数:
96 |
97 | ```py
98 | >>> from operator import add, sub, mul
99 | >>> add(14, 28)
100 | 42
101 | >>> sub(100, mul(7, add(8, 4)))
102 | 16
103 | ```
104 |
105 | `import` 语句需要指定模块名称(例如 `operator` 或 `math`),然后列出要导入该模块里的具名函数(例如 `sqrt`)。一个函数被导入后就可以被多次调用。
106 |
107 | 使用运算符函数(例如 `add`)和运算符号(例如 +)之间并没有任何区别。按照惯例来说,大多数程序员使用符号和中缀表示法来表达简单的算术。
108 |
109 | [Python 3 库文档](http://docs.python.org/py3k/library/index.html) 列出了每个模块中定义的函数,例如 [math 模块](http://docs.python.org/py3k/library/math.html)。但是,该文档是为熟悉整个语言的开发人员编写的。现在来说,尝试使用函数可能会比阅读文档更能使你了解函数的行为。而当你熟悉了 Python 语言和词汇时,这个文档就将会成为你宝贵的参考资料。
110 |
111 | ## 1.2.4 名称与环境
112 |
113 | 编程语言的一个要素就是使用名称来引用计算对象,如果一个值被赋予了名称,我们说名称绑定到了值上面。
114 |
115 | 在 Python 中,我们可以使用赋值语句建立新的绑定,`=` 左边是名称,右边是值。
116 |
117 | ```py
118 | >>> radius = 10
119 | >>> radius
120 | 10
121 | >>> 2 * radius
122 | 20
123 | ```
124 |
125 | 名称也可以通过 `import` 语句绑定。
126 |
127 | ```py
128 | >>> from math import pi
129 | >>> pi * 71 / 223
130 | 1.0002380197528042
131 | ```
132 |
133 | `=` 在 Python 中称为 **赋值** 符号(即 assignment operator,许多其他语言也是如此),赋值是最简单的 **抽象** 方法,因为它允许我们使用简单名称来指代复合操作的结果,例如上面计算的 `area`。这样,复杂的程序由复杂性递增的计算对象逐步构建。
134 |
135 | 将名称与值绑定,之后通过名称检索可能的值,就意味着解释器必须维护某种内存来记录名称、值和绑定,这种内存就是环境(environment)。
136 |
137 | 名称也可以与函数绑定。例如,名称 `max` 就和我们之前使用的 `max` 函数进行了绑定。与数字不同,函数很难以文本呈现,因此当询问一个函数时,Python 会打印一个标识来描述:
138 |
139 | ```py
140 | >>> max
141 |
142 | ```
143 |
144 | 赋值语句可以为现有函数赋予新名称。
145 |
146 | ```py
147 | >>> f = max
148 | >>> f
149 |
150 | >>> f(2, 3, 4)
151 | 4
152 | ```
153 |
154 | 之后再次赋值可以将已有名称与新值绑定。
155 |
156 | ```py
157 | >>> f = 2
158 | >>> f
159 | 2
160 | ```
161 |
162 | 在 Python 中,名称通常被称为“变量名 variable names”或“变量 variables”,因为它们可以在执行程序的过程中与不同的值绑定。当一个变量通过赋值语句与一个新值绑定,它就不再绑定以前的值。你甚至可以将内置名称与新值绑定。
163 |
164 | ```py
165 | >>> max = 5
166 | >>> max
167 | 5
168 | ```
169 |
170 | 将 `max` 赋值为 5 后,名称 `max` 不再绑定函数,因此调用 `max(2, 3, 4)` 将导致错误。
171 |
172 | 执行赋值语句时,Python 会先求解 `=` 右侧的表达式,再将结果与左侧的名称绑定,所以可以在右侧表达式中引用一个已绑定的变量。
173 |
174 | ```py
175 | >>> x = 2
176 | >>> x = x + 1
177 | >>> x
178 | 3
179 | ```
180 |
181 | 还可以在单个语句中为多个变量分配值,左右都用逗号隔开。
182 |
183 | ```py
184 | >>> area, circumference = pi * radius * radius, 2 * pi * radius
185 | >>> area
186 | 314.1592653589793
187 | >>> circumference
188 | 62.83185307179586
189 | ```
190 |
191 | 更改一个变量的值不会影响其他变量。即使下列代码中 `area` 的值由最初定义的 `radius` 绑定,但改变 `radius` 的值并不能更新 `area` 的值,我们需要另一个赋值语句来更新它。
192 |
193 | ```py
194 | >>> radius = 11
195 | >>> area
196 | 314.1592653589793
197 | >>> area = pi * radius * radius
198 | 380.132711084365
199 | ```
200 |
201 | 对于多重赋值,所有 `=` 右边的表达式都会先求值,然后再与左边的名称绑定。在这个规则下,我们可以在单个语句内交换两个变量的值。
202 |
203 | ```py
204 | >>> x, y = 3, 4.5
205 | >>> y, x = x, y
206 | >>> x
207 | 4.5
208 | >>> y
209 | 3
210 | ```
211 |
212 | ## 1.2.5 求解嵌套表达式
213 |
214 | 本章的目标之一是在“以程序的角度思考”中隔离其他的问题,举一个恰当的例子,就是思考一下在求解嵌套表达式时,解释器自身的操作过程。
215 |
216 | 为了求值一个表达式,Python 将执行以下操作:
217 |
218 | - 求解运算符子表达式和操作数子表达式
219 | - 然后将操作数子表达式的值作为运算符子表达式的函数的参数
220 |
221 | 这个简单的过程也说明了有关流程的一些要点。第一步规定:为了求出调用表达式,必须首先求出其他表达式。因此,求值程序本质上是 **递归** 的,也就是说它会自己调用自己作为步骤之一。
222 |
223 | 例如,此式需要应用四次求值过程。
224 |
225 | ```py
226 | >>> sub(pow(2, add(1, 10)), pow(2, 5))
227 | 2016
228 | ```
229 |
230 | 如果把每个需要求解的表达式都抽出来,我们可以看到这个求值过程的层次结构。
231 |
232 | 
233 |
234 | 这个图叫做表达式树,在计算机科学中,树通常从上到下增长。树中每个点的对象都叫做节点。这里节点分别是表达式和表达式的值。
235 |
236 | 求解根节点(即顶部的完整表达式),需要首先求解子表达式,也就是分支节点。叶子节点(也就是没有分支的节点)表示函数或数值。内部节点有两部分:我们想要应用的求值规则的调用表达式,以及该表达式的结果。观察这棵树的求解过程,我们可以想象操作数的值会向上流动,从叶子节点开始一步步向上组合。
237 |
238 | 接下来,观察第一步的重复应用,将我们带到我们需要求解的原始表达式,而不是调用表达式,例如数字(例如 2)和名称(例如 add)。我们规定基本逻辑为:
239 |
240 | - 数字的值就是它们所表示的数值
241 | - 名称的值是环境中关联此名称的对象
242 |
243 | 注意环境在决定表达式中的符号意义上有重要作用。在 Python 中,不指定任何环境信息去谈论一个值是没有意义的,例如名称 `x` 和 `add`。环境为求解提供了上下文信息,对理解程序执行过程有着重要作用。
244 |
245 | ```py
246 | >>> add(x, 1)
247 | ```
248 |
249 | 这个求解步骤并不能对所有 Python 代码求值,它仅能求解调用表达式、数字和名称。例如,它并不能处理赋值语句。
250 |
251 | ```py
252 | >>> x = 3
253 | ```
254 |
255 | 因为赋值语句的目的是将名称与值绑定,它并不返回值,也不应用参数去求解函数。也就是说,赋值语句不被求解但“被执行”,它们只是做出一些改变但不产生值。每种类型的表达式或语句都有自己的求解或执行过程。
256 |
257 | 注意:当我们说“一个数字求解为一个数值”时,实际上是 Python 解释器将数字求解为数值,是解释器赋予了编程语言这个意义。鉴于解释器是一个始终表现一致的固定程序,我们就可以说数字(以及表达式)会在 Python 程序的上下文中被求解为值。
258 |
259 | ## 1.2.6 非纯函数 print
260 |
261 | 在本节中,我们将区分两种类型的函数。
262 |
263 | **纯函数(Pure functions)**:函数有一些输入(参数)并返回一些输出(调用返回结果)。
264 |
265 | ```py
266 | >>> abs(-2)
267 | 2
268 | ```
269 |
270 | 可以将内置函数 `abs` 描述为接受输入并产生输出的小型机器。
271 |
272 | 
273 |
274 | `abs` 就是纯函数,纯函数在调用时除了返回值外不会造成其他任何影响,而且在使用相同的参数调用纯函数时总是会返回相同的值。
275 |
276 | **非纯函数(Non-pure functions)**:除了返回值外,调用一个非纯函数还会产生其他改变解释器和计算机的状态的副作用(side effect)。一个常见的副作用就是使用 `print` 函数产生(非返回值的)额外输出。
277 |
278 | ```py
279 | >>> print(1, 2, 3)
280 | 1 2 3
281 | ```
282 |
283 | 虽然 `print` 和 `abs` 在这些例子中看起来很相似,但它们的工作方式基本不同。`print` 返回的值始终为 `None`,这是一个不代表任何内容的特殊 Python 值。而交互式 Python 解释器并不会自动打印 `None` 值,所以 `print` 函数的额外输出就是它的副作用。
284 |
285 | 
286 |
287 | 下面这个调用 `print` 的嵌套表达式就展示了非纯函数的特征。
288 |
289 | ```py
290 | >>> print(print(1), print(2))
291 | 1
292 | 2
293 | None None
294 | ```
295 |
296 | 如果你发现这个输出结果出乎你的意料,可以画一个表达式树来解释求解该表达式会产生特殊输出的原因。
297 |
298 | 小心使用 `print` 函数!它返回 `None` 意味着它不应该用于赋值语句。
299 |
300 | ```py
301 | >>> two = print(2)
302 | 2
303 | >>> print(two)
304 | None
305 | ```
306 |
307 | 纯函数不能有副作用,或是随着时间推移的改变的限制,但是对其施加这些限制会产生巨大的好处。首先,纯函数可以更可靠地组成复合调用表达式。在上面的示例中可以看到在操作数表达式中使用非纯函数 `print` 并不能返回有用的结果,但另一方面,我们已经看到 `max, pow, sqrt` 等函数可以在嵌套表达式中有效使用。
308 |
309 | 第二,纯函数往往更易于测试。相同的参数列表会返回相同的值,我们可以将其与预期的返回值进行比较。本章后面将更详细地讨论测试。
310 |
311 | 第三,第四章将说明纯函数对于编写可以同时计算多个调用表达式的并发程序来说是必不可少的。
312 |
313 | 此外,第二章会研究一系列非纯函数并描述它们的用途。
314 |
315 | 所以我们将在本章的剩余部分重点介绍纯函数的创建和使用,`print` 函数仅用于查看计算的中间结果。
316 |
--------------------------------------------------------------------------------
/sicp/1/3.md:
--------------------------------------------------------------------------------
1 | # 1.3 定义新的函数
2 |
3 | ::: details INFO
4 | 译者:[Mancuoj](https://github.com/mancuoj)
5 |
6 | 来源:[1.3 Defining New Functions](https://www.composingprograms.com/pages/13-defining-new-functions.html)
7 |
8 | 对应:Lab 01
9 | :::
10 |
11 | 我们已经在 Python 中确定了强大的编程语言中一些必须出现的要素:
12 |
13 | 1. 原始的内置数据和函数:数字和算术运算
14 | 2. 组合方式:嵌套函数
15 | 3. 受限的抽象方式:将名称与值绑定
16 |
17 | 现在我们来学习 **函数定义**,这是一种更为强大的抽象技术,通过它可以将名称与复合操作绑定为一个单元。
18 |
19 | 首先来研究一下 **平方** 的概念。我们可能会说:“平方就是一个数乘以它本身。”这在 Python 中可以表示为:
20 |
21 | ```py
22 | >>> def square(x):
23 | return mul(x, x)
24 | ```
25 |
26 | 上面的代码定义了一个名为 `square` 的新函数,这个用户定义的函数并不会内置到解释器中,它表示将某值与自身相乘的复合运算。这个定义将 `x` 作为被乘的东西的名称,称为 **形式参数**,同时也将此函数与名称 `square` 绑定。
27 |
28 | **如何定义函数**:函数定义包含 `def` 语句、`` 和一个以逗号分隔的 `` 列表,然后是一个被称为函数体的 `return` 语句,它指定了调用函数时要计算的表达式,也就是函数的 `` :
29 |
30 | ```py
31 | def ():
32 | return
33 | ```
34 |
35 | 函数的第二行 **必须** 进行缩进,大多数程序员使用四个空格。返回表达式会作为新定义的函数的一部分存储,并且仅在最终调用该函数时才进行求值。
36 |
37 | 定义了 `square` 之后,我们可以调用它:
38 |
39 | ```py
40 | >>> square(21)
41 | 441
42 | >>> square(add(2, 5))
43 | 49
44 | >>> square(square(3))
45 | 81
46 | ```
47 |
48 | 我们还可以将 `square` 作为一个构建单元来定义其他函数。例如,我们可以很容易地定义一个函数 `sum_squares`,给定任意两个数字作为参数,返回它们的平方和:
49 |
50 | ```py
51 | >>> def sum_squares(x, y):
52 | return add(square(x), square(y))
53 |
54 | >>> sum_squares(3, 4)
55 | 25
56 | ```
57 |
58 | 用户定义函数的使用方式与内置函数完全相同。实际上,从 `sum_squares` 的定义中我们并不能判断 `square` 是内置于解释器中,还是从模块中导入的,又或是用户定义的。
59 |
60 | `def` 语句和赋值语句都将名称与值绑定,并且绑定后任何之前的绑定都将丢失。例如,下面的 `g` 首先指的是一个没有参数的函数,然后是指一个数字,最后是一个含有两个参数的函数。
61 |
62 | ```py
63 | >>> def g():
64 | return 1
65 | >>> g()
66 | 1
67 | >>> g = 2
68 | >>> g
69 | 2
70 | >>> def g(h, i):
71 | return h + i
72 | >>> g(1, 2)
73 | 3
74 | ```
75 |
76 | ## 1.3.1 环境
77 |
78 | 虽然我们现在的 Python 子集已经足够复杂,但程序的含义并不明显。如果形参与内置函数同名怎么办?两个函数可以共享名称而不会混淆吗?要解决这些问题,我们必须更详细地描述环境。
79 |
80 | 求解表达式的环境由 **帧** 序列组成,它们可以被描述为一些盒子。每个帧都包含了一些 **绑定**,它们将名称与对应的值相关联。**全局** 帧(global frame)只有一个。赋值和导入语句会将条目添加到当前环境的第一帧。目前,我们的环境仅由全局帧组成。
81 |
82 |
83 |
84 | 此 **环境图** 显示了当前环境中的绑定,还有名称和值的绑定。本文中的环境图是交互式的:你可以逐步运行左侧程序的每一行,然后在右侧查看环境状态的演变。你还可以单击“Edit this code”以将示例加载到 [Online Python Tutor](https://www.composingprograms.com/tutor.html) 中,它是由 [Philip Guo](http://www.pgbovine.net/) 创建的用于生成环境图的工具。希望你能够自己去创建示例,研究对应生成的环境图。
85 |
86 | 函数也会出现在环境图中。`import` 语句将名称与内置函数绑定。`def` 语句将名称与用户自定义的函数绑定。导入 `mul` 并定义 `square` 后的结果环境如下所示:
87 |
88 |
89 |
90 | 每个函数都是一行,以 `func` 开头,后面是函数名称和形式参数。`mul` 等内置函数没有正式的参数名称,所以都是使用 `...` 代替。
91 |
92 | 函数名称重复两次,一次在环境帧中,另一次是作为函数定义的一部分。函数定义中出现的名称叫做 **内在名称(intrinsic name)**,帧中的名称叫做 **绑定名称(bound name)**。两者之间有一个区别:不同的名称可能指的是同一个函数,但该函数本身只有一个内在名称。
93 |
94 | 绑定到帧中的函数名称是在求值过程中使用,而内在名称在求值中不起作用。使用 Next 按钮逐步执行下面的示例,可以看到一旦名称 `max` 与数字值 3 绑定,它就不能再用作函数。
95 |
96 |
97 |
98 | 错误信息“TypeError: 'int' object is not callable”报告了名称 `max` (当前绑定到数字 3)是一个整数而不是函数,所以它不能用作调用表达式中的运算符。
99 |
100 | **函数签名**:每个函数允许采用的参数数量有所不同。为了跟踪这些要求,我们绘制了每个函数的名称及其形式参数。用户定义的函数 `square` 只需要 `x` 一个参数,提供或多或少的参数都将导致错误。对函数形式参数的描述被称为函数的签名。
101 |
102 | 函数 `max` 可以接受任意数量的参数,所以它被呈现为 `max(...)`。因为原始函数从未明确定义,所以无论采用多少个参数,所有的内置函数都将呈现为 `(...)`。
103 |
104 | ## 1.3.2 调用用户定义的函数
105 |
106 | 为了求出操作符为用户定义函数的调用表达式,Python 解释器遵循了以下计算过程。与其他任何调用表达式一样,解释器将对操作符和操作数表达式求值,然后用生成的实参调用具名函数。
107 |
108 | 调用用户定义的函数会引入局部帧(local frame),它只能被该函数访问。通过一些实参调用用户定义的函数:
109 |
110 | 1. 在新的局部帧中,将实参绑定到函数的形参上。
111 | 2. 在以此帧开始的环境中执行函数体。
112 |
113 | 求值函数体的环境由两个帧组成:一是包含形式参数绑定的局部帧,然后是包含其他所有内容的全局帧。函数的每个实例都有自己独立的局部帧。
114 |
115 | 为了详细说明一个例子,下面将会描述相同示例的环境图中的几个步骤。执行第一个 `import` 语句后,只有名称 `mul` 被绑定在全局帧中。
116 |
117 | 首先,执行定义函数 `square` 的语句。请注意,整个 `def` 语句是在一个步骤中处理的。直到函数被调用(而不是定义时),函数体才会被执行。
118 |
119 | 接下来,使用参数 -2 调用 `square` 函数,它会创建一个新的帧,将形式参数 `x` 与 -2 绑定。
120 |
121 |
122 |
123 | 然后在当前环境中查找名称 `x`,它由所示的两个帧组成,而在这两种情况下,`x` 的结果都是 -2,因此此 `square` 函数返回 4。
124 |
125 | `square()` 帧中的“返回值”不是名称绑定的值,而是调用创建帧的函数返回的值。
126 |
127 | 即使在这个简单的示例中,也使用了两个不同的环境。顶级表达式 `square(-2)` 在全局环境中求值,而返回表达式 `mul(x, x)` 在调用 `square` 时创建的环境中求值。虽然 `x` 和 `mul` 都在这个环境中,但在不同的帧中。
128 |
129 | 环境中帧的顺序会影响通过表达式查找名称而返回的值。我们之前说过,名称会求解为当前环境中与该名称关联的值。我们现在可以更准确地说:
130 |
131 | **名称求解(Name Evaluation)**:在环境中寻找该名称,最早找到的含有该名称的帧,其里边绑定的值就是这个名称的计算结果。
132 |
133 | 环境、名称和函数的概念框架构成了求解模型,虽然一些机械细节仍未指定(例如,如何实现绑定),但我们的模型确实精准地描述了解释器如何求解调用表达式。在第三章中,我们将看到这个模型如何作为蓝图来实现编程语言的工作解释器。
134 |
135 | ## 1.3.3 示例:调用用户定义的函数
136 |
137 | 让我们再次思考两个简单的函数定义,并说明计算用户定义函数的调用表达式的过程。
138 |
139 |
140 |
141 | Python 首先求解名称 `sum_squares` ,并将它绑定到全局帧中的用户定义的函数,而原始数值表达式 5 和 12 的计算结果为它们所代表的数字。
142 |
143 | 接下来 Python 会调用 `sum_squares` ,它引入了一个局部帧,将 `x` 绑定到 5,将 `y` 绑定到 12。
144 |
145 | `sum_squares` 的主体包含此调用表达式:
146 |
147 | ```
148 | add ( square(x) , square(y) )
149 | ________ _________ _________
150 | operator operand 0 operand 1
151 | ```
152 |
153 | 所有三个子表达式都在当前环境中计算,且始于标记为 `sum_squares()` 的帧。运算符子表达式 `add` 是在全局帧中找到的名称,它绑定到了内置的加法函数上。在调用加法之前,必须依次求解两个操作数子表达式,两个操作数都在标记为 `sum_squares` 的帧的环境中计算。
154 |
155 | 在 `operand 0` 中,`square` 在全局帧中命名了一个用户定义的函数,而 `x` 在局部帧中命名了数字 5。Python 通过引入另一个将 `x` 与 5 绑定的局部帧来调用 `square` 函数。
156 |
157 | 此环境下表达式 `mul(x, x)` 的计算结果为 25。
158 |
159 |
160 |
161 | 继续求解 `operand 1`,其中 y 的值为 12。Python 会再次对 `square` 的函数体进行求解,此时引入另一个将 `x` 与 12 绑定的局部帧,计算结果为 144。
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 标准库中也是如此。像英语的词汇一样,Python 继承了各种贡献者的词汇,所以结果并不总是一致的。
199 |
200 | ## 1.3.6 抽象函数
201 |
202 | 虽然 `sum_squares` 这个函数非常简单,但它体现了用户自定义函数最强大的特性。函数 `sum_squares` 是根据函数 `square` 定义的,但仅依赖于 `square` 在其输入参数和输出值之间定义的关系。
203 |
204 | 我们可以编写 `sum_squares` 而不用关心如何对一个数求平方。如何计算平方的细节可以被隐藏到之后再考虑。确实,对于 `sum_squares` 而言,`square` 并不是一个特定的函数体,而是一个函数的抽象,也就是所谓的函数抽象(functional abstraction)。在这个抽象层次上,任何计算平方的函数都是等价的。
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 库就具有此属性,许多开发人员使用这里定义的函数,但很少有人去探究它们的实现。
216 |
217 | **抽象函数的各个方面**:思考抽象函数的三个核心属性通常对掌握其使用很有帮助。**函数的域 domain** 是它可以接受的参数集合;**范围 range** 是它可以返回的值的集合;**意图 intent** 是计算输入和输出之间的关系(以及它可能产生的任何副作用)。通过域、范围和意图来理解函数抽象对之后能在复杂程序中正确使用它们至关重要。
218 |
219 | 例如,我们用于实现 `sum_squares` 的任何平方函数应具有以下属性:
220 |
221 | - **域** 是任意单个实数。
222 | - **范围** 是任意非负实数。
223 | - **意图** 是计算输入的平方作为输出。
224 |
225 | 这些属性不会描述函数是如何执行的,这个细节已经被抽象掉了。
226 |
227 | ## 1.3.7 运算符
228 |
229 | 数学运算符(例如 + 和 -)为我们提供了组合方法的第一个示例,但我们尚未给包含这些运算符的表达式定义求值过程。
230 |
231 | 带有中缀运算符的 Python 表达式都有自己的求值过程,但你通常可以将它们视为调用表达式的简写形式。当你看到
232 |
233 | ```py
234 | >>> 2 + 3
235 | 5
236 | ```
237 |
238 | 可以认为简单地将它理解为以下代码
239 |
240 | ```py
241 | >>> add(2, 3)
242 | 5
243 | ```
244 |
245 | 中缀表示法可以嵌套,就像调用表达式一样。Python 运算符优先级采用了正常数学规则,它规定了如何求解具有多个运算符的复合表达式。
246 |
247 | ```py
248 | >>> 2 + 3 * 4 + 5
249 | 19
250 | ```
251 |
252 | 它和以下表达式的求解结果完全相同
253 |
254 | ```py
255 | >>> add(add(2, mul(3, 4)), 5)
256 | 19
257 | ```
258 |
259 | 调用表达式中的嵌套比运算符版本更加明显,但也更难以阅读。Python 还允许使用括号对子表达式进行分组,用以覆盖正常的优先级规则,或使表达式的嵌套结构更加明显。
260 |
261 | ```py
262 | >>> (2 + 3) * (4 + 5)
263 | 45
264 | ```
265 |
266 | 它和以下表达式的求解结果完全相同
267 |
268 | ```py
269 | >>> mul(add(2, 3), add(4, 5))
270 | 45
271 | ```
272 |
273 | 对于除法,Python 提供了两个中缀运算符:`/` 和 `//`。前者是常规除法,因此即使除数可以整除被除数,它也会产生 **浮点数**(十进制小数):
274 |
275 | ```py
276 | >>> 5 / 4
277 | 1.25
278 | >>> 8 / 4
279 | 2.0
280 | ```
281 |
282 | 而后一个运算符 `//` 会将结果向下舍入到一个整数:
283 |
284 | ```py
285 | >>> 5 // 4
286 | 1
287 | >>> -5 // 4
288 | -2
289 | ```
290 |
291 | 这两个运算符算是 `truediv` 和 `floordiv` 函数的简写。
292 |
293 | ```py
294 | >>> from operator import truediv, floordiv
295 | >>> truediv(5, 4)
296 | 1.25
297 | >>> floordiv(5, 4)
298 | 1
299 | ```
300 |
301 | 你可以在程序中随意使用中缀运算符和圆括号。对于简单的数学运算,Python 惯例上更喜欢使用运算符而不是调用表达式。
302 |
--------------------------------------------------------------------------------
/sicp/1/4.md:
--------------------------------------------------------------------------------
1 | # 1.4 设计函数
2 |
3 | ::: details INFO
4 | 译者:[Mancuoj](https://github.com/mancuoj)
5 |
6 | 来源:[1.4 Designing Functions](https://www.composingprograms.com/pages/14-designing-functions.html)
7 |
8 | 对应:Lab 01
9 | :::
10 |
11 | 函数是所有程序(无论大小)的基本组成部分,并且是我们使用编程语言来表达计算过程的主要媒介。之前我们已经讨论过了函数的形式及其调用方式,而本节我们将讨论“什么是一个好函数”。从根本上说,好函数共有的品质就是:它们都强化了“函数就是抽象”的理念。
12 |
13 | - 每个函数应该只负责一个任务,且该任务要用一个简短的名称来识别,并在一行文本中进行描述。按顺序执行多个任务的函数应该分为多个函数。
14 | - 不要重复自己(Don't repeat yourself)是软件工程的核心原则。这个所谓的 DRY 原则指出,多个代码片段不应该描述重复的逻辑。相反,逻辑应该只实现一次,为其指定一个名称后多次使用。如果你发现自己正在复制粘贴一段代码,那么你可能已经找到了进行函数抽象的机会。
15 | - 定义通用的函数。比如作为 `pow` 函数的一个特例的平方函数就不在 Python 库中,因为 `pow` 函数可以将数字计算为任意次方。
16 |
17 | 这些准则提高了代码的可读性,减少了错误的数量,并且通常最大限度地减少了编写的代码总量。将复杂的任务分解为简洁的功能是一项需要经验才能掌握的技能。幸运的是,Python 提供了多种特性来支持你的工作。
18 |
19 | ## 1.4.1 文档
20 |
21 | 函数定义通常包括描述函数的文档,称为“文档字符串 docstring”,它必须在函数体中缩进。文档字符串通常使用三个引号,第一行描述函数的任务,随后的几行可以描述参数并解释函数的意图:
22 |
23 | ```py
24 | >>> def pressure(v, t, n):
25 | """计算理想气体的压力,单位为帕斯卡
26 |
27 | 使用理想气体定律:http://en.wikipedia.org/wiki/Ideal_gas_law
28 |
29 | v -- 气体体积,单位为立方米
30 | t -- 绝对温度,单位为开尔文
31 | n -- 气体粒子
32 | """
33 | k = 1.38e-23 # 玻尔兹曼常数
34 | return n * k * t / v
35 | ```
36 |
37 | 当你使用函数名称作为参数调用 `help` 时,你会看到它的文档字符串(键入 q 以退出 Python help)。
38 |
39 | ```py
40 | >>> help(pressure)
41 | ```
42 |
43 | 编写 Python 程序时,除了最简单的函数之外,都要包含文档字符串。要记住,虽然代码只编写一次,但是会在之后阅读多次。Python 文档包含了 [文档字符串准则](http://www.python.org/dev/peps/pep-0257/),它会在不同的 Python 项目中保持一致。
44 |
45 | 注释:Python 中的注释可以附加到 `#` 号后的行尾。例如,上面代码中的注释 `玻尔兹曼常数` 描述了 `k` 变量的含义。这些注释不会出现在 Python 的 `help` 中,而且会被解释器忽略,它们只为人类而存在。
46 |
47 | ## 1.4.2 参数默认值
48 |
49 | 定义通用函数的结果是引入了额外的参数。具有许多参数的函数可能调用起来很麻烦并且难以阅读。
50 |
51 | 在 Python 中,我们可以为函数的参数提供默认值。当调用该函数时,具有默认值的参数是可选的。如果未提供,则将默认值绑定到形参上。例如,如果程序通常用于计算“一摩尔”粒子的压力,则可以提供此值作为默认值:
52 |
53 | ```py
54 | >>> def pressure(v, t, n=6.022e23):
55 | """计算理想气体的压力,单位为帕斯卡
56 |
57 | 使用理想气体定律:http://en.wikipedia.org/wiki/Ideal_gas_law
58 |
59 | v -- 气体体积,单位为立方米
60 | t -- 绝对温度,单位为开尔文
61 | n -- 气体粒子,默认为一摩尔
62 | """
63 | k = 1.38e-23 # 玻尔兹曼常数
64 | return n * k * t / v
65 | ```
66 |
67 | `=` 符号在此示例中表示两种不同的含义,具体取决于使用它的上下文。在 def 语句中,`=` 不执行赋值,而是指示调用 `pressure` 函数时使用的默认值。相比之下,函数体中对 `k` 的赋值语句中将名称 `k` 与玻尔兹曼常数的近似值进行了绑定。
68 |
69 | ```py
70 | >>> pressure(1, 273.15)
71 | 2269.974834
72 | >>> pressure(1, 273.15, 3 * 6.022e23)
73 | 6809.924502
74 | ```
75 |
76 | `pressure` 函数的定义接收三个参数,但上面的第一个调用表达式中只提供了两个。在这种情况下,`n` 的值取自 `def` 语句中的默认值。如果提供了第三个参数,默认值将被忽略。
77 |
78 | 作为准则,函数主体中使用的大多数数据值都应该表示为具名参数(named arguments)的默认值,这样会使它们更易于检查,并且可以被函数调用者更改。一些永远不会改变的值,例如基本常量 `k` 可以绑定在函数体或全局帧中。
79 |
--------------------------------------------------------------------------------
/sicp/1/5.md:
--------------------------------------------------------------------------------
1 | # 1.5 控制
2 |
3 | ::: details INFO
4 | 译者:[Mancuoj](https://github.com/mancuoj)
5 |
6 | 来源:[1.5 Control](https://www.composingprograms.com/pages/15-control.html)
7 |
8 | 对应:Lab 01
9 | :::
10 |
11 | 我们现在可以定义的函数的能力十分有限,因为我们还没有引入一种方法来进行比较,并根据比较的结果执行不同的操作。控制语句将赋予我们这种能力,就是根据逻辑比较的结果来控制程序执行流程的语句。
12 |
13 | 语句与我们目前研究过的表达式有着根本的不同,它们没有值。执行一个控制语句决定了解释器接下来应该做什么,而不是计算某些东西。
14 |
15 | ## 1.5.1 语句
16 |
17 | 到目前为止,我们虽然主要思考的是如何计算求解表达式,但我们已经见过了三种语句:赋值(assignment)、 `def` 和 `return` 语句。尽管这些 Python 代码都包含表达式作为它们的一部分,但它们本身并不是表达式。
18 |
19 | 语句不会被求解,而会被执行。每个语句都描述了对解释器状态的一些更改,并且执行语句就会应用该更改。正如我们在 `return` 和赋值语句中看到的那样,执行语句可能涉及求解其包含的子表达式。
20 |
21 | 表达式也可以作为语句执行,在这种情况下,它们会被求值,但它们的值会被丢弃。执行纯函数没有效果,但执行非纯函数会因为调用函数而产生效果。
22 |
23 | 思考一下,例如:
24 |
25 | ```py
26 | >>> def square(x):
27 | mul(x, x) # 小心!此调用不返回值。
28 | ```
29 |
30 | 这个例子是有效的 Python 代码,但可能不能达到预期。函数体由一个表达式组成。表达式本身是一个有效的语句,但语句的效果是调用 `mul` 函数,然后把结果丢弃。如果你想对表达式的结果做些什么,你需要用赋值语句存储它或用 `return` 语句返回它:
31 |
32 | ```py
33 | >>> def square(x):
34 | return mul(x, x)
35 | ```
36 |
37 | 有时,在调用 `print` 等非纯函数时,拥有一个主体为表达式的函数确实有意义。
38 |
39 | ```py
40 | >>> def print_square(x):
41 | print(square(x))
42 | ```
43 |
44 | 在最高层级上,Python 解释器的工作是执行由语句组成的程序。然而,很多有趣的计算工作都来自对表达式的求值。语句用来管理程序中不同表达式之间的关系,以及它们产生的结果。
45 |
46 | ## 1.5.2 复合语句
47 |
48 | 通常,Python 代码是一系列语句。简单语句是不以冒号结尾的单行,而由其他语句(简单语句和复合语句)组成被称为复合语句。复合语句通常跨越多行,以单行头部(header)开始,并以冒号结尾,其中冒号标识语句的类型。头部和缩进的句体(suite)一起称为子句。复合语句由一个或多个子句组成:
49 |
50 | ```py
51 | :
52 |
53 |
54 | ...
55 | :
56 |
57 |
58 | ...
59 | ...
60 | ```
61 |
62 | 我们可以用这些术语来理解我们之前介绍过的语句。
63 |
64 | - 表达式、返回语句和赋值语句都是简单语句。
65 | - `def` 语句是复合语句,`def` 头后面的句体定义了函数体。
66 |
67 | 对每类 header 都有专门的求值规则来规定其何时执行以及是否执行其句体中的语句。我们说“the header controls its suite”,例如,在 `def` 语句中,`return` 表达式不会立即求值,而是存储起来供以后调用该函数时使用。
68 |
69 | 我们现在也可以理解多行程序了。
70 |
71 | - 要执行一系列语句,会先执行第一个语句。如果该语句不重定向控制,则继续执行语句序列的其余部分(如果还有的话)。
72 |
73 | 这个定义揭示了递归定义序列(sequence)的基本结构:一个序列可以分解成它的第一个元素和其余元素。语句序列的“其余部分”本身也是语句序列!因此,我们可以递归地应用这个执行规则。这种将序列视为递归的数据结构的观点将在后面的章节中再次出现。
74 |
75 | 此规则的重要结论是语句会按顺序执行,但由于重定向控制(redirected control),后面的语句可能永远不会被执行到。
76 |
77 | 实践指南:缩进句体时,所有行必须以相同的方式缩进相同的量(使用空格,而不是制表符)。缩进的任何变化都会导致错误。
78 |
79 | ## 1.5.3 定义函数 II:局部赋值
80 |
81 | 最初,我们声明用户定义函数的主体仅由包含单个返回表达式的 `return` 语句组成。事实上,函数可以定义超出单个表达式的一系列操作。
82 |
83 | 每当用户定义的函数被调用时,其句体中的子句序列将会在局部环境中执行 --> 该环境通过调用函数创建的局部帧开始。`return` 语句会重定向控制:每当执行一个 `return` 语句时,函数应用程序就会终止,`return` 表达式的值会作为被调用函数的返回值。
84 |
85 | 赋值语句可以出现在函数体内。例如,以下函数使用了两步计算,首先计算两个数的差的绝对值,然后求出它与第一个数的百分比值并返回:
86 |
87 |
88 |
89 | 赋值语句的作用是将名称与当前环境中的第一帧的值绑定。因此,函数体内的赋值语句不会影响全局帧。“函数只能操纵其局部帧”是创建模块化程序的关键,而在模块化程序中,纯函数仅通过它们接收和返回的值与外界交互。
90 |
91 | 当然, `percent_difference` 函数可以写成单个表达式,如下所示,但返回表达式会更复杂。
92 |
93 | ```py
94 | >>> def percent_difference(x, y):
95 | return 100 * abs(x-y) / x
96 | >>> percent_difference(40, 50)
97 | 25.0
98 | ```
99 |
100 | 到目前为止,局部赋值并没有增强函数定义的表达能力,而当它与其他控制语句结合时,就会增强。此外,局部赋值在“通过为中间量赋名来解释复杂表达式的含义”方面也起着至关重要的作用。
101 |
102 | ## 1.5.4 条件语句
103 |
104 | Python 有一个用于计算绝对值的内置函数。
105 |
106 | ```py
107 | >>> abs(-2)
108 | 2
109 | ```
110 |
111 | 我们希望能够自己实现这样一个函数,但是没有清晰的方法来定义一个具有比较和选择的函数。我们想表达的是,如果 `x` 为正,则 `abs(x)` 返回 `x` ;此外,如果 `x` 为 0,则 `abs(x)` 返回 0;否则,`abs(x)` 返回 `-x`。在 Python 中,我们可以用条件语句来表达这种选择。
112 |
113 |
114 |
115 | 这个 `absolute_value` 函数的实现提出了几个重要的问题:
116 |
117 | 条件语句(Conditional statement):Python 中的条件语句由一系列头部和句体组成:必需的 `if` 子句、可选的 `elif` 子句序列,最后是可选的 `else` 子句:
118 |
119 | ```py
120 | if :
121 |
122 | elif :
123 |
124 | else:
125 |
126 | ```
127 |
128 | 执行条件语句时,每个子句都会按顺序被考虑。执行条件子句的计算过程如下。
129 |
130 | 1. 求解头部的表达式
131 | 2. 如果它是真值,则执行该句体。然后,跳过条件语句中的所有后续子句。
132 |
133 | 如果到达 `else` 子句(仅当所有 `if` 和 `elif` 表达式的计算结果为假值时才会发生),则执行其句体。
134 |
135 | 布尔上下文(Boolean contexts):上面,执行过程提到了“假值 a false value”和“真值 a true value”。条件块头部语句内的表达式被称为布尔上下文:它们值的真假对控制流很重要,另外,它们的值不会被赋值或返回。Python 包含多个假值,包括 0、 `None` 和布尔值 `False`,所有其他数字都是真值。在第二章中,我们将看到 Python 中的每种内置数据都具有真值和假值。
136 |
137 | 布尔值(Boolean values):Python 有两个布尔值,分别叫做 `True` 和 `False` 。布尔值表示逻辑表达式中的真值。内置的比较运算符 >, <, > =, <=, ==, != 会返回这些值。
138 |
139 | ```py
140 | >>> 4 < 2
141 | False
142 | >>> 5 >= 5
143 | True
144 | ```
145 |
146 | 第二个例子读作“5 大于或等于 5”,对应于 `operator` 模块中的函数 `ge`。
147 |
148 | ```py
149 | >>> 0 == -0
150 | True
151 | ```
152 |
153 | 最后一个示例读作“0 等于 -0”,对应于 `operator` 模块中的 `eq`。请注意,Python 会区分赋值符号 `=` 与相等比较符号 `==`,这也是许多编程语言共享的约定。
154 |
155 | 布尔运算符(Boolean operators):Python 中还内置了三个基本的逻辑运算符:
156 |
157 | ```py
158 | >>> True and False
159 | False
160 | >>> True or False
161 | True
162 | >>> not False
163 | True
164 | ```
165 |
166 | 逻辑表达式具有相应的求值过程。而这些过程利用了这样一个理论 --> 有时,逻辑表达式的真值可以在不对其所有子表达式求值的情况下确定,这一特性称为短路(short-circuiting)。
167 |
168 | ---
169 |
170 | 求解表达式 ` and ` 的步骤如下:
171 |
172 | 1. 求解子表达式 ``。
173 | 2. 如果左边的结果为假值 v,则表达式的计算结果就是 v。
174 | 3. 否则,表达式的计算结果为子表达式 `` 的值。
175 |
176 | ---
177 |
178 | 求解表达式 ` or ` 的步骤如下:
179 |
180 | 1. 求解子表达式 ``。
181 | 2. 如果左边的结果为真值 v,则表达式的计算结果就是 v。
182 | 3. 否则,表达式的计算结果为子表达式 `` 的值。
183 |
184 | ---
185 |
186 | 求解表达式 `not ` 的步骤如下:
187 |
188 | 1. 求解 ``,如果结果为假值,则值为 `True` ,否则为 `False`。
189 |
190 | ---
191 |
192 | 这些值、规则和运算符为我们提供了一种组合比较结果的方法。执行比较并返回布尔值的函数通常以 `is` 开头,后面不跟下划线(例如 `isfinite, isdigit, isinstance` 等)。
193 |
194 | ## 1.5.5 迭代
195 |
196 | 除了选择要执行的语句外,控制语句还用于重复。如果我们编写的每一行代码只执行一次,那么编程将是一项非常低效的工作。只有通过重复执行语句,我们才能释放计算机的全部潜力。我们之前已经见过了一种重复形式:一个函数只用定义一次,就可以被多次调用。迭代控制(Iterative control)结构是另一种多次执行相同语句的机制。
197 |
198 | 思考斐波那契数列,其中每个数都是前两个数的和:
199 |
200 | $0, 1, 1, 2, 3, 5, 8, 13, 21, \cdots$
201 |
202 | 每个值都是通过重复应用 `sum-previous-two` 的规则构建的,第一个和第二个值固定为 0 和 1。
203 |
204 | 我们可以使用 `while` 语句来枚举 n 项斐波那契数列。我们需要跟踪已经创建了多少个值(`k`),和第 k 个值(`curr`)及其前身(`pred`)。单步执行此函数并观察斐波那契数如何一个一个地演化,并绑定到 curr。
205 |
206 |
207 |
208 | 请记住,单行赋值语句可以用逗号分隔多个名称和值同时赋值。该行:
209 |
210 | `pred, curr = curr, pred + curr`
211 |
212 | 将名称 `pred` 重新绑定到 `curr` 的值,同时将 `curr` 重新绑定到 `pred + curr` 的值。所有 `=` 右侧的所有表达式都会在绑定之前计算出来。
213 |
214 | 在更新左侧的绑定之前求出所有 `=` 右侧的内容 --> 这种事件顺序对于此函数的正确性至关重要。
215 |
216 | `while` 子句包含一个头部表达式,后跟一个句体:
217 |
218 | ```py
219 | while :
220 |
221 | ```
222 |
223 | 要执行 `while` 子句:
224 |
225 | 1. 求解头部的表达式。
226 | 2. 如果是真值,则执行后面的句体,然后返回第 1 步。
227 |
228 | 在第 2 步中,`while` 子句的整个句体在再次计算头部表达式之前执行。
229 |
230 | 为了防止 `while` 子句的句体无限期地执行,句体应该总是在每次循环中更改一些绑定。
231 |
232 | 不会终止的 `while` 语句被称为无限循环(infinite loop)。按 `-C` 可以强制 Python 停止循环。
233 |
234 | ## 1.5.6 测试
235 |
236 | 测试一个函数就是去验证函数的行为是否符合预期。现在我们的函数语句已经足够复杂,所以我们需要开始测试我们的实现的函数功能。
237 |
238 | 测试是一种系统地执行验证的机制。它通常采用另一个函数的形式,其中包含对一个或多个对被测试函数的调用样例,然后根据预期结果验证其返回值。与大多数旨在通用的函数不同,测试需要选择特定参数值,并使用它们验证函数调用。测试也可用作文档:去演示如何调用函数,以及如何选择合适的参数值。
239 |
240 | 断言(Assertions):程序员使用 `assert` 语句来验证是否符合预期,例如验证被测试函数的输出。`assert` 语句在布尔上下文中有一个表达式,后面是一个带引号的文本行(单引号或双引号都可以,但要保持一致),如果表达式的计算结果为假值,则显示该行。
241 |
242 | ```py
243 | >>> assert fib(8) == 13, '第八个斐波那契数应该是 13'
244 | ```
245 |
246 | 当被断言的表达式的计算结果为真值时,执行断言语句无效。而当它是假值时,`assert` 会导致错误,使程序停止执行。
247 |
248 | fib 的测试函数应该测试几个参数,包括 n 的极限值。
249 |
250 | ```py
251 | >>> def fib_test():
252 | assert fib(2) == 1, '第二个斐波那契数应该是 1'
253 | assert fib(3) == 1, '第三个斐波那契数应该是 1'
254 | assert fib(50) == 7778742049, '在第五十个斐波那契数发生 Error'
255 | ```
256 |
257 | 当在文件中而不是直接在解释器中编写 Python 时,测试通常是在同一个文件或带有后缀 `_test.py` 的相邻文件中编写的。
258 |
259 | 文档测试(Doctests):Python 提供了一种方便的方法,可以将简单的测试直接放在函数的文档字符串中。文档字符串的第一行应该包含函数的单行描述,接着是一个空行,下面可能是参数和函数意图的详细描述。此外,文档字符串可能包含调用该函数的交互式会话示例:
260 |
261 | ```py
262 | >>> def sum_naturals(n):
263 | """返回前 n 个自然数的和。
264 |
265 | >>> sum_naturals(10)
266 | 55
267 | >>> sum_naturals(100)
268 | 5050
269 | """
270 | total, k = 0, 1
271 | while k <= n:
272 | total, k = total + k, k + 1
273 | return total
274 | ```
275 |
276 | 然后,可以通过 [doctest 模块](http://docs.python.org/py3k/library/doctest.html) 来验证交互,如下。
277 |
278 | ```py
279 | >>> from doctest import testmod
280 | >>> testmod()
281 | TestResults(failed=0, attempted=2)
282 | ```
283 |
284 | 如果仅想验证单个函数的 doctest 交互,我们可以使用名为 `run_docstring_examples` 的 `doctest` 函数。不幸的是,这个函数调用起来有点复杂。第一个参数是要测试的函数;第二个参数应该始终是表达式 `globals()` 的结果,这是一个用于返回全局环境的内置函数;第三个参数 `True` 表示我们想要“详细”输出:所有测试运行的目录。
285 |
286 | ```py
287 | >>> from doctest import run_docstring_examples
288 | >>> run_docstring_examples(sum_naturals, globals(), True)
289 | Finding tests in NoName
290 | Trying:
291 | sum_naturals(10)
292 | Expecting:
293 | 55
294 | ok
295 | Trying:
296 | sum_naturals(100)
297 | Expecting:
298 | 5050
299 | ok
300 | ```
301 |
302 | 当函数的返回值与预期结果不匹配时,`run_docstring_examples` 函数会将此问题报告为测试失败。
303 |
304 | 当你在文件中编写 Python 时,可以通过使用 doctest 命令行选项启动 Python 来运行文件中的所有 doctest:
305 |
306 | ```sh
307 | python3 -m doctest
308 | ```
309 |
310 | 有效测试的关键是在实现新功能后立即编写(并运行)测试。在实现之前编写一些测试也是一种很好的做法,以便在你的脑海中有一些示例输入和输出。调用单个函数的测试称为单元测试(unit test)。详尽的单元测试是良好程序设计的标志。
311 |
--------------------------------------------------------------------------------
/sicp/1/7.md:
--------------------------------------------------------------------------------
1 | # 1.7 递归函数
2 |
3 | ::: details INFO
4 | 译者:[Mancuoj](https://github.com/mancuoj)
5 |
6 | 来源:[1.7 Recursive Functions](https://www.composingprograms.com/pages/17-recursive-functions.html)
7 |
8 | 对应:Disc 03、HW 03
9 | :::
10 |
11 | 如果函数体中直接或间接调用了函数本身,则函数称为递归(recursive)函数。也就是说,执行递归函数主体的过程中可能需要再次调用该函数。在 Python 中,递归函数不需要使用任何特殊语法,但它们确实需要一些努力来理解和创建。
12 |
13 | 我们将从编写一个对自然数的所有数字位求和的函数的样例开始。在设计递归函数时,我们需要找到可以将问题分解为更简单问题的方法。在这个示例中,可以使用运算符 `%` 和 `//` 将数字分为两部分:最后一位和除最后一位以外的所有数字。
14 |
15 | ```py
16 | >>> 18117 % 10
17 | 7
18 | >>> 18117 // 10
19 | 1811
20 | ```
21 |
22 | 18117 的数字位之和是 $1+8+1+1+7 = 18$ 。正如同我们可以拆分数字一样,我们可以将这个和分成最后一位数字 7 和除最后一位数字之外的所有数字的和 $1+8+1+1 = 11$ 。这种拆分为我们提供了一种算法:要对数字 n 的数字位求和,就将其最后一位数字 `n % 10` 与 `n // 10` 的所有数字位之和相加。其中有一种特殊情况:如果数字只有一位,那么它的数字位之和就是它本身。该算法可以使用递归函数来实现。
23 |
24 | ```py
25 | >>> def sum_digits(n):
26 | """返回正整数 n 的所有数字位之和"""
27 | if n < 10:
28 | return n
29 | else:
30 | all_but_last, last = n // 10, n % 10
31 | return sum_digits(all_but_last) + last
32 | ```
33 |
34 | 这个 `sum_digits` 函数的定义是完整且准确的。虽然 `sum_digits` 函数在自己的函数体内被调用,但数字求和问题已被细分为两个步骤:先求出除最后一个数字外的所有数字总和,再加上最后一个数字的值。这两个步骤比原问题都更简单。由于第一个步骤与原问题相同,所以该函数被称为递归函数。也就是说,`sum_digits` 函数本身就是我们实现 `sum_digits` 所需要的函数。
35 |
36 | ```py
37 | >>> sum_digits(9)
38 | 9
39 | >>> sum_digits(18117)
40 | 18
41 | >>> sum_digits(9437184)
42 | 36
43 | >>> sum_digits(11408855402054064613470328848384)
44 | 126
45 | ```
46 |
47 | 我们可以使用我们的计算环境模型来精确理解这个递归函数如何成功应用的,而且不需要添加新的规则。
48 |
49 |
50 |
51 | 当执行 `def` 语句时,名称 `sum_digits` 被绑定到一个新函数,但该函数的主体尚未执行。因此,`sum_digits` 的循环特性(circular nature)暂时还不是一个问题。然后,`sum_digits` 被传入参数 738:
52 |
53 | 1. 创建一个局部帧,将 `n` 绑定到 738,并在该帧作为起点的环境中执行 `sum_digits` 的函数体。
54 | 2. 由于 738 不小于 10,会执行第 4 行的赋值语句,将 738 分为 73 和 8。
55 | 3. 在下面的返回语句中,会以当前环境中 `all_but_last` 的值 73 调用 `sum_digits`。
56 | 4. 创建另一个将 `n` 绑定到 73 的局部帧,并在该帧作为起点的环境中再次执行 `sum_digits` 的函数体。
57 | 5. 由于 73 也不小于 10,将 73 分为 7 和 3,并以 7 调用 `sum_digits`,即 `all_but_last` 在此帧中的值。
58 | 6. 创建第三个局部帧,其中将 `n` 绑定到 7。
59 | 7. 在从这个帧开始的环境中,表达式 `n < 10` 为真,因此返回 7。
60 | 8. 在第二个局部帧中,将这个返回值 7 与 `last` 的值 3 相加,返回 10。
61 | 9. 在第一个局部帧中,将这个返回值 10 与 `last` 的值 8 相加,返回 18。
62 |
63 | ---
64 |
65 | 尽管这个递归函数具有循环特性,但它使用了不同的参数正确地应用了两次。此外,第二次应用此程序的对象是一个比第一次更简单的数字求和问题。生成调用 `sum_digits(18117)` 的环境图,可以看到每次连续的 `sum_digits` 的调用都使用了比上次更小的参数,直到最后得到个位数的输入。
66 |
67 | 这个例子还说明了具有简单函数体的函数可以通过使用递归演变成具有复杂计算过程的函数。
68 |
69 | ## 1.7.1 递归函数剖析
70 |
71 | 许多递归函数的函数体中存在着一种常见的模式。函数体会以一个基线条件(base case)开始(这是一种条件语句),它为最容易处理的输入定义了函数的行为。对于 `sum_digits` 函数而言,基线条件是接收到任意一位数的参数,我们只需返回该参数。有些递归函数会有多个基线条件。
72 |
73 | 然后,在基线条件之后,会有一个或多个递归调用。递归调用总是有一个特点:它们简化了原始问题。递归函数通过逐步简化问题来表达计算。例如,对 7 的数字求和比对 73 的数字求和更简单,而对 73 求和又比对 738 更简单。对于每个后续调用,剩余的计算量都会减少。
74 |
75 | 递归函数解决问题的方法通常不同于我们之前使用的迭代方法。思考一个计算 n 的阶乘的函数 `fact`,其中 `fact(4)` 会计算为 $4! = 4 \cdot 3 \cdot 2 \cdot 1=24$ 。
76 |
77 | 一种自然的实现方式是使用 `while` 语句将每个小于等于 n 的正整数都相乘得到结果。
78 |
79 | ```py
80 | >>> def fact_iter(n):
81 | total, k = 1, 1
82 | while k <= n:
83 | total, k = total * k, k + 1
84 | return total
85 |
86 | >>> fact_iter(4)
87 | 24
88 | ```
89 |
90 | 另一方面,阶乘的递归实现可以用 `fact(n-1)` 来表达 `fact(n)`,就是一个更简单的问题了。这个递归的基线条件是问题最简单的形式:`fact(1)` 是 1。
91 |
92 | 这两个阶乘函数在概念上有所不同。迭代函数通过在每一项中连续相乘,来构造从基线条件 1 到最终总数 n 的结果。另一方面,递归函数直接从最终项 n 和更简单的问题 `fact(n-1)` 的结果来构造出最终结果。
93 |
94 | 递归会通过 `fact` 函数的连续应用,逐步“展开 unwinds”为越来越简单的问题实例,最后从基线条件开始构造出结果。它通过将参数 1 传递给 `fact` 来结束递归;每次调用的结果取决于下一次调用,直到达到基线条件。
95 |
96 | 从阶乘的标准数学函数定义中,很容易验证这个递归函数的正确性:
97 |
98 | $$
99 | \begin{aligned}
100 | (n-1) ! & =(n-1) \cdot(n-2) \cdots \cdots 1 \\
101 | n ! & = n \cdot(n-1) \cdot(n-2) \cdots \cdots 1 \\
102 | n ! & = n \cdot(n-1) !
103 | \end{aligned}
104 | $$
105 |
106 | 虽然我们可以使用计算模型来展开递归,但将递归调用(recursive calls)视为函数抽象会更容易理解一点。也就是说,我们不用在意 `fact(n-1)` 在 `fact` 的函数体中是怎么实现的;我们只需要相信它能计算 n-1 的阶乘就好了。将递归调用看作一种函数抽象这一思想,就是所谓“递归的信仰之跃(recursive leap of faith)”。我们根据函数自身来定义一个函数,但在验证函数的正确性时,我们只需相信在更简单的情况下,函数同样能正确工作。在这个示例中,我们假设 `fact(n-1)` 能够正确计算 $(n-1)!$ ;如果假设成立,我们只需要检查 $n!$ 是否被正确计算即可。这样,验证递归函数的正确性实际上变成了一种归纳法(induction)的证明形式。
107 |
108 | 函数 `fact_iter` 和 `fact` 也有所不同,因为前者必须引入两个额外的名称 `total` 和 `k`,这在递归实现中是不需要的。一般来说,迭代函数必须在计算过程中维护一些会变化的局部状态。在迭代中的任何时刻,该状态都可以表示已完成计算的结果和剩余的待计算的量。例如,当 `k = 3`,`total = 2` 时,仍然有两项需要处理,即 3 和 4。另一方面,`fact` 的特征是它的单一参数 `n`。计算的状态完全嵌入在环境的结构中,它的返回值扮演 total 的角色,并将 `n` 绑定到不同帧中的不同值,而不是显式地跟踪 `k`。
109 |
110 | 递归函数利用调用表达式求值的规则将名称绑定到值,通常避免了在迭代期间正确分配局部名称的麻烦。由于这个原因,我们可能更容易正确地定义递归函数。但是,学着识别由递归函数演化而来的计算过程需要一定的实践练习。
111 |
112 | ## 1.7.2 互递归
113 |
114 | 当一个递归过程被划分到两个相互调用的函数中时,这两个函数被称为是互递归的(mutually recursive)。例如,思考以下非负整数的偶数和奇数定义:
115 |
116 | - 如果一个数比一个奇数大 1,那它就是偶数
117 | - 如果一个数比一个偶数大 1,那它就是奇数
118 | - 0 是偶数
119 |
120 | 使用这个定义,我们可以实现一个互递归函数来确定一个数字是偶数还是奇数:
121 |
122 |
123 |
124 | 通过打破两个函数之间的抽象边界,可以将互递归函数转换为单个递归函数。在这个例子中,可以将 `is_odd` 的函数体合并到 `is_even` 的函数体中,确保将 `is_odd` 函数体中的 n 替换为 n-1 以反映传递给它的参数:
125 |
126 | ```py
127 | >>> def is_even(n):
128 | if n == 0:
129 | return True
130 | else:
131 | if (n-1) == 0:
132 | return False
133 | else:
134 | return is_even((n-1)-1)
135 | ```
136 |
137 | 因此,互递归并不比简单递归更神秘或更强大,它只是提供了一种在复杂递归程序中维护抽象的机制。
138 |
139 | ## 1.7.3 递归函数中的打印
140 |
141 | 通过对 `print` 函数的调用,递归函数的计算过程通常可以可视化。作为示例,我们将实现一个 `cascade` 函数,该函数按从大到小再到大的顺序,打印一个数字的所有前缀。
142 |
143 | ```py
144 | >>> def cascade(n):
145 | """打印数字 n 的前缀的级联"""
146 | if n < 10:
147 | print(n)
148 | else:
149 | print(n)
150 | cascade(n//10)
151 | print(n)
152 |
153 | >>> cascade(2013)
154 | 2013
155 | 201
156 | 20
157 | 2
158 | 20
159 | 201
160 | 2013
161 | ```
162 |
163 | 在这个递归函数中,基线条件是打印出来个位数。否则,就在两个 `print` 调用之间使用递归调用。
164 |
165 | 在递归调用之前写出基线条件表达式并不是一个严格的要求。实际上,通过观察可以看到 `print(n)` 在条件语句的两个子句中重复出现,我们可以将其前置从而更简洁地表达这个函数。
166 |
167 | ```py
168 | >>> def cascade(n):
169 | """Print a cascade of prefixes of n."""
170 | print(n)
171 | if n >= 10:
172 | cascade(n//10)
173 | print(n)
174 | ```
175 |
176 | 作为另一个互递归的例子,请思考一个两人博弈的情景,桌子上最初有 n 个石子,玩家轮流从桌面上拿走一个或两个石子,拿走最后一个石子的玩家获胜。假设 Alice 和 Bob 在玩这个游戏,两个人都使用一个简单的策略:
177 |
178 | - Alice 总是取走一个石子
179 | - 如果桌子上有偶数个石子,Bob 就拿走两个石子,否则就拿走一个石子
180 |
181 | 给定 n 个初始石子且 Alice 先开始拿,谁会赢得游戏?
182 |
183 | 该问题的一个自然分解是将每个策略封装在其自己的函数中。这使我们可以修改一个策略而不会影响其他策略,保持两者之间的抽象界限(abstraction barrier)。为了融入游戏的回合制性质,这两个函数在每个回合结束时互相调用。
184 |
185 | ```py
186 | >>> def play_alice(n):
187 | if n == 0:
188 | print("Bob wins!")
189 | else:
190 | play_bob(n-1)
191 |
192 | >>> def play_bob(n):
193 | if n == 0:
194 | print("Alice wins!")
195 | elif is_even(n):
196 | play_alice(n-2)
197 | else:
198 | play_alice(n-1)
199 |
200 | >>> play_alice(20)
201 | Bob wins!
202 | ```
203 |
204 | 在函数 `play_bob` 中,我们看到多个递归调用可能会出现一个函数体中。虽然在这个例子中,每次调用 `play_bob` 最多只会调用一次 `play_alice`。在下个小节中,我们将会思考当单个函数调用同时直接进行多个递归函数调用时会发生什么。
205 |
206 | ## 1.7.4 树递归
207 |
208 | 另一种常见的计算模式称为树递归(tree recursion),在这种模式中,函数会多次调用自己。例如计算斐波那契数列,其中的每个数都是前两个数的和。
209 |
210 |
211 |
212 | 相对于我们之前的尝试,这个递归定义非常吸引人:它完全反映了我们熟悉的斐波那契数的定义。具有多个递归调用的函数称为树递归,因为每个调用都会分成多个较小的调用,每个较小的调用又会分成更小的调用,就像树枝从树干伸出一样,变得更小但数量更多。
213 |
214 | 我们已经能够不用树递归定义一个函数来计算斐波那契数列。事实上,我们以前的方法更加高效,这是本文后面讨论的主题。接下来,我们会思考一个问题,而对于这个问题,树递归的解决方案比任何迭代方案都要简单得多。
215 |
216 | ## 1.7.5 示例:[分割数]()
217 |
218 | 求正整数 n 的分割数,最大部分为 m,即 n 可以分割为不大于 m 的正整数的和,并且按递增顺序排列。例如,使用 4 作为最大数对 6 进行分割的方式有 9 种。
219 |
220 | ```
221 | 1. 6 = 2 + 4
222 | 2. 6 = 1 + 1 + 4
223 | 3. 6 = 3 + 3
224 | 4. 6 = 1 + 2 + 3
225 | 5. 6 = 1 + 1 + 1 + 3
226 | 6. 6 = 2 + 2 + 2
227 | 7. 6 = 1 + 1 + 2 + 2
228 | 8. 6 = 1 + 1 + 1 + 1 + 2
229 | 9. 6 = 1 + 1 + 1 + 1 + 1 + 1
230 | ```
231 |
232 | 我们将定义一个名为 `count_partitions(n, m)` 的函数,该函数返回使用 `m` 作为最大部分对 n 进行分割的方式的数量。这个函数有一个使用树递归的简单的解法,它基于以下的观察结果:
233 |
234 | 使用最大数为 m 的整数分割 n 的方式的数量等于
235 |
236 | 1. 使用最大数为 m 的整数分割 n-m 的方式的数量,加上
237 | 2. 使用最大数为 m-1 的整数分割 n 的方式的数量
238 |
239 | 要理解为什么上面的方法是正确的,我们可以将 n 的所有分割方式分为两组:至少包含一个 m 的和不包含 m 的。此外,第一组中的每次分割都是对 n-m 的分割,然后在最后加上 m。在上面的实例中,前两种拆分包含 4,而其余的不包含。
240 |
241 | 因此,我们可以递归地将使用最大数为 m 的整数分割 n 的问题转化为两个较简单的问题:① 使用最大数为 m 的整数分割更小的数字 n-m,以及 ② 使用最大数为 m-1 的整数分割 n。
242 |
243 | 为了实现它,我们需要指定以下的基线情况:
244 |
245 | 1. 整数 0 只有一种分割方式
246 | 2. 负整数 n 无法分割,即 0 种方式
247 | 3. 任何大于 0 的正整数 n 使用 0 或更小的部分进行分割的方式数量为 0
248 |
249 | ```py
250 | >>> def count_partitions(n, m):
251 | """计算使用最大数 m 的整数分割 n 的方式的数量"""
252 | if n == 0:
253 | return 1
254 | elif n < 0:
255 | return 0
256 | elif m == 0:
257 | return 0
258 | else:
259 | return count_partitions(n-m, m) + count_partitions(n, m-1)
260 |
261 | >>> count_partitions(6, 4)
262 | 9
263 | >>> count_partitions(5, 5)
264 | 7
265 | >>> count_partitions(10, 10)
266 | 42
267 | >>> count_partitions(15, 15)
268 | 176
269 | >>> count_partitions(20, 20)
270 | 627
271 | ```
272 |
273 | 我们可以将树递归函数视为探索不同的可能性。在这种情况下,我们探讨了使用大小为 m 的部分以及不使用这部分的可能性。第一次和第二次递归调用即对应着这些可能性。
274 |
275 | 如果不使用递归,则需要投入更多的精力来实现这个函数。有兴趣的读者可以尝试一下。
276 |
--------------------------------------------------------------------------------
/sicp/2/1.md:
--------------------------------------------------------------------------------
1 | # 2.1 引言
2 |
3 | ::: details INFO
4 | 译者:[Mancuoj](https://github.com/mancuoj)
5 |
6 | 来源:[2.1 Introduction](https://www.composingprograms.com/pages/21-introduction.html)
7 |
8 | 对应:Lab 04、Cats
9 | :::
10 |
11 | 在第一章中,我们主要讨论了计算过程和函数在程序设计中的作用。我们学习了怎么使用原始数据(数字)和原始操作(算术),怎么通过组合和控制形成复合函数,以及怎么通过给程序命名来创建函数抽象。我们还了解到:高阶函数使我们能够根据通用的计算方法进行操作和推理,从而增强了语言的功能。这就是编程的本质。
12 |
13 | 本章重点介绍数据:我们研究的技术使我们可以表示和操作许多不同领域的信息。由于互联网的爆炸式增长,所有人都可以在网上免费获取大量结构化信息,并且计算也可以应用于范围广泛的不同问题。有效使用内置数据类型和用户定义的数据类型是数据处理型应用(data processing applications)的基础。
14 |
15 | ## 2.1.1 原始数据类型
16 |
17 | Python 中的每个值都有一个类(class)来确定它的类型。拥有相同类的值,行为也相同。例如,整数 1 和 2 都是 `int` 类的实例,我们就可以使用相似的方法进行处理。例如,它们都可以取反或与另一个整数相加。内置的 `type` 函数允许我们检查任何值的类。
18 |
19 | ```py
20 | >>> type(2)
21 |
22 | ```
23 |
24 | 到目前为止,我们使用的值都是 Python 语言中内置的少量的原始数据类型的实例。原始数据类型具有以下属性:
25 |
26 | 1. 有一些可以求解为原始数据类型的表达式,被称为字面量(literals)。
27 | 2. 有用于操作原始类型值的内置函数和操作符。
28 |
29 | `int` 类是用于表示整数的原始数据类型。整数字面量(相邻的数字序列)会求解为 `int` 值,并且数学运算符可以操作这种值。
30 |
31 | ```py
32 | >>> 12 + 3000000000000000000000000
33 | 3000000000000000000000012
34 | ```
35 |
36 | Python 包含三种原始数字类型:整数(`int`)、浮点数(`float`)和复数(`complex`)。
37 |
38 | ```py
39 | >>> type(1.5)
40 |
41 | >>> type(1+1j)
42 |
43 | ```
44 |
45 | 浮点数:“Float”这个名字来源于 Python 和许多其他编程语言中对实数的表示方式:就是“具有浮动的小数点”的值。虽然关于数字如何表示的细节不是本文的主题,但了解 `int` 和 `float` 对象之间的一些区别是很重要的。特别是,`int` 对象可以精确地表示整数,没有任何近似处理或大小限制。另一方面,`float` 对象可以表示很大范围内的小数,但并不是所有的数字都能被精确表示,它有最小值和最大值之分。因此,`float` 值应被视为真实值的近似值,它们只能保证有限的精度,组合 `float` 值可能会导致近似误差;如果不进行近似处理,下面两个表达式的计算结果均为 7。
46 |
47 | > 译者注:不同于其他的编程语言,Python3 中的 `int` 值是无界的,也就是说它可以存储任意大小的数,具体可以查看 [PEP 237](https://peps.python.org/pep-0237/)。
48 |
49 | ```py
50 | >>> 7 / 3 * 3
51 | 7.0
52 | >>> 1 / 3 * 7 * 3
53 | 6.999999999999999
54 | ```
55 |
56 | 尽管上式是 `int` 值的组合,但一个 `int` 值除以另一个 `int` 值,却会得到一个 `float` 值:一个截断的有限近似值,相当于两个整数相除的实际比值。
57 |
58 | ```py
59 | >>> type(1/3)
60 |
61 | >>> 1/3
62 | 0.3333333333333333
63 | ```
64 |
65 | 当我们使用等式测试时,这种近似就会出现问题。
66 |
67 | ```py
68 | >>> 1/3 == 0.333333333333333312345 # 请注意浮点数近似
69 | True
70 | ```
71 |
72 | `int` 和 `float` 类之间的细微差别对编写程序有着广泛的影响,因此,这也是程序员必须熟记的细节。幸运的是,原始数据类型的数量很少,这无疑减少了精通一门编程语言所需的记忆量。此外,这些细节在许多编程语言中都是一致的,它们会由 [IEEE 754 浮点标准](http://en.wikipedia.org/wiki/IEEE_floating_point) 等社区指南强制要求执行。
73 |
74 | 非数值类型(Non-numeric types):值可以表示许多其他类型的数据,比如声音、图像、位置、网址、网络连接等等。它们中间的少数可以用原始数据类型表示,例如用于值 `True` 和 `False` 的 `bool` 类,其他大多数值的类型必须由程序员使用我们将在本章中学习到的组合和抽象方法来定义。
75 |
76 | 下面几节将介绍更多 Python 的原始数据类型,并将重点介绍它们在创建数据抽象方面所起到的作用。那些对更多细节感兴趣的人,可以查看在线书籍 Dive Into Python 3 中的一个关于 [原始数据类型](http://getpython3.com/diveintopython3/native-datatypes.html) 的章节,它给出了所有 Python 的原始数据类型以及如何操作它们的实用概述,包括大量的使用示例和实用技巧。
77 |
--------------------------------------------------------------------------------
/sicp/2/2.md:
--------------------------------------------------------------------------------
1 | # 2.2 数据抽象
2 |
3 | ::: details INFO
4 | 译者:[Mancuoj](https://github.com/mancuoj)
5 |
6 | 来源:[2.2 Data Abstraction](https://www.composingprograms.com/pages/22-data-abstraction.html)
7 |
8 | 对应:无
9 | :::
10 |
11 | 当我们希望在程序中表示世界上广泛的事物时,会发现它们中的大多数都具有复合结构。比如地理位置具有经纬度坐标。为了表示位置,我们希望我们的编程语言能够经度和纬度耦合在一起形成一对复合数据,使它能够作为单个概念单元被程序操作,同时也能作为可以单独考虑的两个部分。
12 |
13 | 使用复合数据可以使程序更加模块化。如果我们能够将地理位置作为整体值进行操作,那么我们就可以将计算位置的程序部分与位置如何表示的细节隔离开来,这种将“数据表示”与“数据处理”的程序隔离的通用技术是一种强大的设计方法,称为数据抽象。数据抽象会使程序更易于设计、维护和修改。
14 |
15 | 数据抽象与函数抽象类似。当我们创建一个函数抽象时,函数实现的细节可以被隐藏,而特定的函数本身可以被替换为具有相同整体行为的任何其他函数。换句话说,我们可以创建一个抽象来将函数的使用方式与实现细节分离。类似地,数据抽象可以将复合数据值的使用方式与其构造细节隔离开来。
16 |
17 | 数据抽象的基本思想是构建程序,以便它们对抽象数据进行操作。也就是说,我们的程序应该以尽可能少的假设来使用数据,同时要将具体的数据表示定义为程序的独立部分。
18 |
19 | 程序的“操作抽象数据”和“定义具体表示”两个部分,会由一组根据具体表示来实现抽象数据的函数相连。为了说明该技术,我们将思考如何设计一组用于操作有理数的函数。
20 |
21 | ## 2.2.1 示例:有理数
22 |
23 | 有理数是整数的比值,并且有理数是实数的一个重要子类。 `1/3` 或 `17/29` 等有理数通常写为:
24 |
25 | ```py
26 | <分子>/<分母>
27 | ```
28 |
29 | 其中 `<分子>` 和 `<分母>` 都是整数值的占位符,这两个部分能够准确表示有理数的值。实际上的整数除以会产生 `float` 近似值,失去整数的精确精度。
30 |
31 | ```py
32 | >>> 1/3
33 | 0.3333333333333333
34 | >>> 1/3 == 0.333333333333333300000 # 整数除法得到近似值
35 | True
36 | ```
37 |
38 | 但是,我们可以通过将分子和分母组合在一起来创建有理数的精确表示。
39 |
40 | 通过使用函数抽象,我们可以在实现程序的某些部分之前开始高效地编程。我们首先假设已经存在了一个从分子和分母构造有理数的方法,再假设有方法得到一个给定有理数的分子和分母。进一步假设得到以下三个函数:
41 |
42 | - `rational(n, d)` 返回分子为 `n`、分母为 `d` 的有理数
43 | - `numer(x)` 返回有理数 `x` 的分子
44 | - `denom(x)` 返回有理数 `x` 的分母
45 |
46 | 我们在这里使用了一个强大的程序设计策略:一厢情愿(wishful thinking)。即使我们还没有想好有理数是如何表示的,或者函数 `numer`、`denom` 和 `rational` 应该如何实现。但是如果我们确实定义了这三个函数,我们就可以进行加法、乘法、打印和测试有理数是否相等:
47 |
48 | ```py
49 | >>> def add_rationals(x, y):
50 | nx, dx = numer(x), denom(x)
51 | ny, dy = numer(y), denom(y)
52 | return rational(nx * dy + ny * dx, dx * dy)
53 |
54 | >>> def mul_rationals(x, y):
55 | return rational(numer(x) * numer(y), denom(x) * denom(y))
56 |
57 | >>> def print_rational(x):
58 | print(numer(x), '/', denom(x))
59 |
60 | >>> def rationals_are_equal(x, y):
61 | return numer(x) * denom(y) == numer(y) * denom(x)
62 | ```
63 |
64 | 现在我们有了选择器函数 `numer` 和 `denom` 以及构造函数 `rational` 定义的有理数运算,但还没有定义这些函数。我们需要某种方法将分子和分母粘合在一起形成一个复合值。
65 |
66 | ## 2.2.2 对
67 |
68 | 为了使我们能够实现具体的数据抽象,Python 提供了一个名为 `list` 列表的复合结构,可以通过将表达式放在以逗号分隔的方括号内来构造。这样的表达式称为列表字面量。
69 |
70 | ```py
71 | >>> [10, 20]
72 | [10, 20]
73 | ```
74 |
75 | 可以通过两种方式访问 列表元素。第一种方法是通过我们熟悉的多重赋值方法,它将列表解构为单个元素并将每个元素与不同的名称绑定。
76 |
77 | ```py
78 | >>> pair = [10, 20]
79 | >>> pair
80 | [10, 20]
81 | >>> x, y = pair
82 | >>> x
83 | 10
84 | >>> y
85 | 20
86 | ```
87 |
88 | 访问列表中元素的第二种方法是通过元素选择运算符,也使用方括号表示。与列表字面量不同,直接跟在另一个表达式之后的方括号表达式不会计算为 `list` 值,而是从前面表达式的值中选择一个元素。
89 |
90 | ```py
91 | >>> pair[0]
92 | 10
93 | >>> pair[1]
94 | 20
95 | ```
96 |
97 | Python 中的列表(以及大多数其他编程语言中的序列)是从 0 开始索引的,这意味着索引 0 选择第一个元素,索引 1 选择第二个元素,以此类推。对于这种索引约定的一种直觉是,索引表示元素距列表开头的偏移量。
98 |
99 | 元素选择运算符的等效函数称为 `getitem` ,它也使用 0 索引位置从列表中选择元素。
100 |
101 | ```py
102 | >>> from operator import getitem
103 | >>> getitem(pair, 0)
104 | 10
105 | >>> getitem(pair, 1)
106 | 20
107 | ```
108 |
109 | 双元素列表并不是 Python 中表示对的唯一方法。将两个值捆绑在一起成为一个值的任何方式都可以被认为是一对。列表是一种常用的方法,它也可以包含两个以上的元素,我们将在本章后面进行探讨。
110 |
111 | 代表有理数:我们现在可以将有理数表示为两个整数的对:一个分子和一个分母。
112 |
113 | ```py
114 | >>> def rational(n, d):
115 | return [n, d]
116 |
117 | >>> def numer(x):
118 | return x[0]
119 |
120 | >>> def denom(x):
121 | return x[1]
122 | ```
123 |
124 | 连同之前定义的算术运算,我们可以使用我们定义的函数来操作有理数。
125 |
126 | ```py
127 | >>> half = rational(1, 2)
128 | >>> print_rational(half)
129 | 1 / 2
130 | >>> third = rational(1, 3)
131 | >>> print_rational(mul_rationals(half, third))
132 | 1 / 6
133 | >>> print_rational(add_rationals(third, third))
134 | 6 / 9
135 | ```
136 |
137 | 如上面的示例所示,我们的有理数实现不会将有理数简化为最小项。可以通过更改 `rational` 的实现来弥补这个缺陷。如果我们有一个计算两个整数的最大公分母的函数,我们可以用它在构造对之前将分子和分母减少到最低项。与许多有用的工具一样,这样的功能已经存在于 Python 库中。
138 |
139 | ```py
140 | >>> from fractions import gcd
141 | >>> def rational(n, d):
142 | g = gcd(n, d)
143 | return (n//g, d//g)
144 | ```
145 |
146 | `//` 表示整数除法,它会将除法结果的小数部分向下舍入。因为我们知道 `g` 会将 `n` 和 `d` 均分,所以在这种情况下整数除法是精确的。这个修改后的 `rational` 实现会确保有理数以最小项表示。
147 |
148 | ```py
149 | >>> print_rational(add_rationals(third, third))
150 | 2 / 3
151 | ```
152 |
153 | 这种改进是通过更改构造函数而不更改任何实现实际算术运算的函数来实现的。
154 |
155 | ## 2.2.3 抽象屏障
156 |
157 | 在继续更多复合数据和数据抽象的示例之前,让我们考虑一下有理数示例引发的一些问题。我们根据构造函数 `rational` 和选择器函数 `numer` 和 `denom` 来定义操作。一般来说,数据抽象的基本思想是确定一组基本操作,根据这些操作可以表达对某种值的所有操作,然后仅使用这些操作来操作数据。通过以这种方式限制操作的使用,在不改变程序行为的情况下改变抽象数据的表示会容易得多。
158 |
159 | 对于有理数,程序的不同部分使用不同的操作来处理有理数,如此表中所述。
160 |
161 | | 该程序的一部分... | 把有理数当作... | 仅使用... |
162 | | -------------------------- | --------------- | ----------------------------------------------------------------- |
163 | | 使用有理数进行计算 | 整个数据值 | `add_rational, mul_rational, rationals_are_equal, print_rational` |
164 | | 创建有理数或操作有理数 | 分子和分母 | `rational, numer, denom` |
165 | | 为有理数实现选择器和构造器 | 二元列表 | 列表字面量和元素选择 |
166 |
167 | 在上面的每一层中,最后一列中的函数会强制实施抽象屏障(abstraction barrier)。这些功能会由更高层次调用,并使用较低层次的抽象实现。
168 |
169 | 当程序中有一部分本可以使用更高级别函数但却使用了低级函数时,就会违反抽象屏障。例如,计算有理数平方的函数最好用 `mul_rational` 实现,它不对有理数的实现做任何假设。
170 |
171 | ```py
172 | >>> def square_rational(x):
173 | return mul_rational(x, x)
174 | ```
175 |
176 | 直接引用分子和分母会违反一个抽象屏障。
177 |
178 | ```py
179 | >>> def square_rational_violating_once(x):
180 | return rational(numer(x) * numer(x), denom(x) * denom(x))
181 | ```
182 |
183 | 假设有理数会表示为双元素列表将违反两个抽象屏障。
184 |
185 | ```py
186 | >>> def square_rational_violating_twice(x):
187 | return [x[0] * x[0], x[1] * x[1]]
188 | ```
189 |
190 | 抽象屏障使程序更易于维护和修改。依赖于特定表示的函数越少,想要更改该表示时所需的更改就越少。计算有理数平方的所有这些实现都具有正确的行为,但只有第一个函数对未来的更改是健壮的。即使我们修改了有理数的表示,`square_rational` 函数也不需要更新。相比之下,当选择器函数或构造函数签名发生变化后,`square_rational_violating_once` 就需要更改,而只要有理数的实现发生变化,`square_rational_violating_twice` 就需要更新。
191 |
192 | ## 2.2.4 数据的属性
193 |
194 | 抽象屏障塑造了我们思考数据的方式。有理数的表示不限于任何特定的实现(例如二元素列表);它就是由 `rational` 返回的值,然后可以传递给 `numer` 和 `denom` 。此外,构造器和选择器之间必须保持适当的关系。也就是说,如果我们从整数 `n` 和 `d` 构造一个有理数 `x` ,那么 `numer(x)/denom(x)` 应该等于 `n/d` 。
195 |
196 | 通常,我们可以使用选择器和构造器的集合以及一些行为条件来表达抽象数据。只要满足行为条件(比如上面的除法属性),选择器和构造器就构成了一种数据的有效表示。抽象屏障下的实现细节可能会改变,但只要行为没有改变,那么数据抽象就仍然有效,并且使用该数据抽象编写的任何程序都将保持正确。
197 |
198 | 这种观点可以广泛应用,包括我们用来实现有理数的对。我们从来没有真正谈论什么是一对,只是语言提供了创建和操作二元列表的方法。我们需要实现一对的行为是它将两个值粘合在一起。作为一种行为条件,
199 |
200 | - 如果一对 `p` 由值 `x` 和 `y` 构成,则 `select(p, 0)` 返回 `x`, `select(p, 1)` 返回 `y`
201 |
202 | 我们实际上并不一定需要 `list` 类型来创建对,作为替代,我们可以用两个函数 `pair` 和 `select` 来实现这个描述以及一个二元列表。
203 |
204 | ```py
205 | >>> def pair(x, y):
206 | """Return a function that represents a pair."""
207 | def get(index):
208 | if index == 0:
209 | return x
210 | elif index == 1:
211 | return y
212 | return get
213 |
214 | >>> def select(p, i):
215 | """Return the element at index i of pair p."""
216 | return p(i)
217 | ```
218 |
219 | 通过这个实现,我们可以创建和操作对。
220 |
221 | ```py
222 | >>> p = pair(20, 14)
223 | >>> select(p, 0)
224 | 20
225 | >>> select(p, 1)
226 | 14
227 | ```
228 |
229 | 这种高阶函数的使用完全不符合我们对数据应该是什么的直觉概念。但尽管如此,这些函数足以在我们的程序中表示对,也足以表示复合数据。
230 |
231 | 这种表示对的函数表示的重点并不是 Python 实际上以这种方式工作(出于效率原因,列表更直接地实现),而是它可以以这种方式工作。函数表示虽然晦涩难懂,但却是表示对的一个完全合适的方法,因为它满足了表示对需要满足的唯一条件。数据抽象的实践使我们能够轻松地在表示之间切换。
232 |
--------------------------------------------------------------------------------
/sicp/2/5.md:
--------------------------------------------------------------------------------
1 | # 2.5 面向对象编程
2 |
3 | ::: details INFO
4 | 译者:[CIQi6](https://github.com/CIQi6)
5 |
6 | 来源:[2.5 Object-Oriented Programming](https://www.composingprograms.com/pages/25-object-oriented-programming.html)
7 |
8 | 对应:Disc 05、HW 04、Lab 06、Ants
9 | :::
10 |
11 | 面向对象编程(OOP)是一种组织程序的方法,它将本章介绍的许多思想结合在一起。与数据抽象中的函数一样,类创建了在使用和实现数据之间的抽象屏障。与调度字典(dispatch dictionaries)一样,对象响应行为请求。与可变数据结构一样,对象具有无法从全局环境直接访问的本地状态。Python 对象系统提供了方便的语法来促进使用这些技术来组织程序。这种语法的大部分在其他面向对象的编程语言之间共享。
12 |
13 | 对象系统提供的不仅仅是便利。它为设计程序提供了一个新的隐喻,其中几个独立的代理在计算机内交互。每个对象都以抽象两者的复杂性的方式将本地状态和行为捆绑在一起。对象相互通信,并且由于它们的交互而计算有用的结果。对象不仅传递消息,而且还在相同类型的其他对象之间共享行为,并从相关类型继承特征。
14 |
15 | 面向对象编程(OOP)的范式有自己的词汇来支持对象隐喻。我们已经看到,对象(object)是具有方法和属性的数据值,可通过点表达式(dot notation)访问。每个对象(object)也有一个类型,称为其类(class)。为了创建新类型的数据,我们实现了新类。
16 |
17 | ## 2.5.1 对象和类
18 |
19 | 类就像一个模板,对象是按照模板(类)生成的实例。到目前为止我们使用的对象都有内置类,但也可以创建新的用户定义类。类定义指定在该类的对象之间共享的属性和方法。我们将通过重新访问银行账户的例子来介绍类语句。
20 |
21 | 在引入本地状态时,我们看到银行账户要具有 `balance` 的可变值。银行帐户对象应具有 `withdraw` 方法,用于更新帐户余额并返回请求的金额(如果可用)。要完成抽象:一个银行账户应该能够返回其当前的 `balance` ,返回账户 `holder` 的名称,以及 `deposit` 的金额。
22 |
23 | `Account` 类允许我们创建多个银行账户实例。创建新对象实例的操作称为实例化类。Python 中用于实例化类的语法与调用函数的语法相同。在这种情况下,我们用参数 `Kirk` 调用 `Account` ,即帐户持有人的姓名。
24 |
25 | ```python
26 | >>> a = Account('Kirk')
27 | ```
28 |
29 | 对象的属性是与对象关联的名称 - 值对,可通过点表达式访问。对于特定对象,其有特定值的属性,(而不是类的所有对象)称为实例属性。每个 `Account` 都有自己的余额和账户持有人姓名,这是实例属性的示例。在更广泛的编程社区中,实例属性也可以称为字段、属性或实例变量。
30 |
31 | ```python
32 | >>> a.holder
33 | 'Kirk'
34 | >>> a.balance
35 | 0
36 | ```
37 |
38 | 对对象进行操作或执行特定于对象的计算的函数称为方法。方法的返回值和副作用可以依赖于并更改对象的其他属性。例如, `deposit` 是我们 `Account` 对象 `a` 的方法。它需要一个参数,即要存入的金额,更改对象的 `balance` 属性,并返回结果余额。
39 |
40 | ```python
41 | >>> a.deposit(15)
42 | 15
43 | ```
44 |
45 | 我们说方法是在特定对象上调用的。调用 `withdraw` 方法的结果是,要么批准提款并扣除金额,要么拒绝请求并返回错误消息。
46 |
47 | ```python
48 | >>> a.withdraw(10) # withdraw 方法返回扣除后的金额
49 | 5
50 | >>> a.balance # 金额属性发生改变
51 | 5
52 | >>> a.withdraw(10)
53 | 'Insufficient funds'
54 | ```
55 |
56 | 如上所示,方法的行为可能取决于对象不断变化的属性,方法也可以改变对象的属性。具有相同参数的两次对 `withdraw` 的调用将返回不同的结果。
57 |
58 | ## 2.5.2 类的定义
59 |
60 | `class` 语句可以创建自定义类,类体里面又包含多条子语句。类语句定义类名,类体包含一组语句来定义类的属性。
61 |
62 | ```python
63 | class :
64 |
65 | ```
66 |
67 | 执行类语句,将创建一个新类,并在当前环境的第一帧中绑定 `` 。然后执行类体里面的语句。在 `class` 的 `` 中 `def` 或赋值语句中绑定的任何名称都会创建或修改类的属性。
68 |
69 | 类通常通过操作类属性来进行设计,这些属性是与该类的每个实例关联的名称 - 值对。类通过定义一个初始化对象的方法来指定特定对象的实例属性。例如,初始化 `Account` 类的对象的一部分是为它分配一个 0 的起始余额。
70 |
71 | `class` 语句中的 `` 包含 `def` 语句,`def` 语句为类的对象定义新方法。初始化对象的方法在 `Python` 中有一个特殊的名称 `__init__` (“init”的每一侧都有两个下划线),称为类的构造函数(constructor)。
72 |
73 | ```python
74 | >>> class Account:
75 | def __init__(self, account_holder):
76 | self.balance = 0
77 | self.holder = account_holder
78 | ```
79 |
80 | `Account` 的 `__init__` 方法有两个形式参数。第一个 `self` 绑定到新创建的 `Account` 对象。第二个参数 `account_holder` 绑定到调用类进行实例化时传递给类的参数。
81 |
82 | 构造函数将实例属性名称 `balance` 绑定到 0。它还将属性名称 `holder` 绑定到名称 `account_holder` 的值。形式参数 `account_holder` 是 `__init__` 方法中的本地名称。另一方面,通过最终赋值语句绑定的名称 `holder` 仍然存在,因为它使用点表达式存储为 `self` 的属性。
83 |
84 | 定义 `Account` 类后,我们可以实例化它。
85 |
86 | ```python
87 | >>> a = Account('Kirk')
88 | ```
89 |
90 | 上面的语句调用 `Account` 类创建一个新对象,这个对象是 `Account` 的一个实例,然后使用两个参数调用构造函数 `__init__` : 新创建的对象和字符串“Kirk” 。一般来说,我们使用参数名称 `self` 作为构造函数的第一个参数,它会自动绑定到正在实例化的对象。几乎所有的 Python 代码都遵守这个规定。
91 |
92 | 现在,我们可以使用符号点来访问对象的 `balance` 和 `holder` 。
93 |
94 | ```python
95 | >>> a.balance
96 | 0
97 | >>> a.holder
98 | 'Kirk'
99 | ```
100 |
101 | **身份标识**:每一个账号实例都有自己的余额属性,它的值是独立的。
102 |
103 | ```python
104 | >>> b = Account('Spock')
105 | >>> b.balance = 200
106 | >>> [acc.balance for acc in (a, b)]
107 | [0, 200]
108 | ```
109 |
110 | 为了强调这种独立性,每一个实例对象都具有唯一的身份标识。使用 `is` 和 `is not` 运算符可以比较对象的标识。
111 |
112 | ```python
113 | >>> a is a
114 | True
115 | >>> a is not b
116 | True
117 | ```
118 |
119 | 尽管是从相同的调用构造的,但绑定到 `a` 和 `b` 的对象并不相同。像前面的一样,使用赋值将对象绑定到新名称不会创建新对象。
120 |
121 | ```python
122 | >>> c = a
123 | >>> c is a
124 | True
125 | ```
126 |
127 | 仅当使用调用表达式语法实例化类(如 `Account` )时,才会创建具有用户定义类的新对象。
128 |
129 | **方法**:对象方法也由 `class` 语句内的 `def` 语句定义。下面, `deposit` 和 `withdraw` 都定义为 `Account` 类对象上的方法。
130 |
131 | ```python
132 | >>> class Account:
133 | def __init__(self, account_holder):
134 | self.balance = 0
135 | self.holder = account_holder
136 | def deposit(self, amount):
137 | self.balance = self.balance + amount
138 | return self.balance
139 | def withdraw(self, amount):
140 | if amount > self.balance:
141 | return 'Insufficient funds'
142 | self.balance = self.balance - amount
143 | return self.balance
144 | ```
145 |
146 | 虽然方法定义在声明方式上与函数定义没有区别,但方法定义在执行时确实具有不同的效果。由 `class` 语句中的 `def` 语句创建的函数值绑定到声明的名称,作为属性在类中本地绑定。该值可以使用类实例中的点表达式的方法调用。
147 |
148 | 每个方法都包含着一个特殊的首参 `self` ,该参数绑定调用该方法的对象。例如,假设在特定的 `Account` 对象上调用 `deposit` 并传递单个参数:存入的金额。对象本身就被绑定到 `self` ,而传入的参数绑定到 `amount` 。所有调用的方法都可以通过 `self` 参数来访问对象,因此它们都可以访问和操作对象的状态。
149 |
150 | 为了调用这些方法,我们再次使用点表达式,如下图所示。
151 |
152 | ```python
153 | >>> spock_account = Account('Spock')
154 | >>> spock_account.deposit(100)
155 | 100
156 | >>> spock_account.withdraw(90)
157 | 10
158 | >>> spock_account.withdraw(90)
159 | 'Insufficient funds'
160 | >>> spock_account.holder
161 | 'Spock'
162 | ```
163 |
164 | 当通过点表达式调用方法时,对象本身(在本例中绑定为 `spock_account` )扮演双重角色。首先,它确定名称 `withdraw` 的含义; `withdraw` 不是环境中的名称,而是 `Account` 类的本地名称。其次,当调用 `withdraw` 方法时,它绑定到第一个参数 `self` 。
165 |
166 | ## 2.5.3 消息传递和点表达式
167 |
168 | 在类中定义的方法和在构造函数中分配的实例属性是面向对象编程的基本元素。这两个概念在传递数据值的消息实现中复制了调度字典的大部分行为。对象使用点表达式获取消息,但这些消息不是任意字符串值键,而是类的本地名称。对象还具有命名的本地状态值(实例属性),但可以使用点表达式访问和操作该状态,而无需在实现中使用 `nonlocal` 语句。
169 |
170 | 消息传递的主要思想是,数据值应该通过响应与其表示的抽象类型相关的消息来具有行为。点表示式是 Python 的一个语法特征,它形式化了消息传递隐喻。将语言与内置对象系统一起使用的优点是,消息传递可以与其他语言功能(如赋值语句)无缝交互。我们不需要不同的消息来“获取”或“设置”与本地属性名称关联的值; 语言语法允许我们直接使用消息名称。
171 |
172 | **点表达式**:代码片段 `spock_account.deposit` 称为点表达式。点表达式由表达式、点和名称组成:
173 |
174 | ```python
175 | .
176 | ```
177 |
178 | `` 可以是任何有效的 Python 表达式,但 `` 必须是简单名称(而不是计算结果为名称的表达式)。点表达式的计算结果为作为 `` 值的对象的 `` 的属性值。
179 |
180 | 内置函数 `getattr` 也可以按名称返回对象的属性。它是点表示法的函数等效物。使用 `getattr` ,我们可以使用字符串查找属性,就像我们对调度字典所做的那样。
181 |
182 | ```python
183 | >>> getattr(spock_account, 'balance')
184 | 10
185 | ```
186 |
187 | 我们还可以使用 `hasattr`来测试对象是否具有指定的属性。
188 |
189 | ```python
190 | >>> hasattr(spock_account, 'deposit')
191 | True
192 | ```
193 |
194 | 对象的属性包括其所有实例属性,以及其类中定义的所有属性(包括方法)。方法是需要特殊处理的类的属性。
195 |
196 | **方法和函数**:在对象上调用方法时,该对象将作为第一个参数隐式传递给该方法。也就是说,点左侧的 `` 值的对象将自动作为第一个参数传递给点表达式右侧命名的方法。因此,对象绑定到参数 `self`。
197 |
198 | 为了实现自动 `self` 绑定,Python 区分了我们从文本开头就一直在创建的函数和绑定方法,它们将函数和将调用该方法的对象耦合在一起。绑定方法值已与其第一个参数(调用它的实例)相关联,在调用该方法时将命名为 `self`。
199 |
200 | 我们可以通过对点表达式的返回值调用 `type` 来查看交互式解释器的差异。作为类的属性,方法只是一个函数,但作为实例的属性,它是一个绑定方法:
201 |
202 | ```python
203 | >>> type(Account.deposit)
204 |
205 | >>> type(spock_account.deposit)
206 |
207 | ```
208 |
209 | 这两个结果的区别仅在于第一个是参数为 `self` 和 `amount` 的标准双参数函数。第二种是单参数方法,调用方法时,名称 `self` 将自动绑定到名为 `spock_account` 的对象,而参数 `amount` 将绑定到传递给方法的参数。这两个值(无论是函数值还是绑定方法值)都与相同的 `deposit` 函数体相关联。
210 |
211 | 我们可以通过两种方式调用 `deposit` :作为函数和作为绑定方法。在前一种情况下,我们必须显式地为 `self` 参数提供一个参数。在后一种情况下, `self` 参数会自动绑定。
212 |
213 | ```python
214 | >>> Account.deposit(spock_account, 1001) # 函数 deposit 接受两个参数
215 | 1011
216 | >>> spock_account.deposit(1000) # 方法 deposit 接受一个参数
217 | 2011
218 | ```
219 |
220 | 函数 `getattr` 的行为与点表示法完全相同:如果它的第一个参数是一个对象,但名称是类中定义的方法,则 `getattr` 返回一个绑定方法值。另一方面,如果第一个参数是一个类,则 `getattr` 直接返回属性值,这是一个普通函数。
221 |
222 | **命名约定**:类名通常使用 CapWords 约定(也称为 CamelCase,因为名称中间的大写字母看起来像驼峰)编写。方法名称遵循使用下划线分隔的小写单词命名函数的标准约定。
223 |
224 | 在某些情况下,有一些实例变量和方法与对象的维护和一致性相关,我们不希望对象的用户看到或使用。它们不是类定义的抽象的一部分,而是实现的一部分。Python 的约定规定,如果属性名称以下划线开头,则只能在类本身的方法中访问它,而不是用户访问。
225 |
226 | ## 2.5.4 类属性
227 |
228 | 某些属性值在给定类的所有对象之间共享。此类属性与类本身相关联,而不是与类的任何单个实例相关联。例如,假设银行以固定利率支付账户余额的利息。该利率可能会发生变化,但它是所有账户共享的单一价值。
229 |
230 | 类属性由 `class` 语句套件中的赋值语句创建,位于任何方法定义之外。在更广泛的开发人员社区中,类属性也可以称为类变量或静态变量。以下类语句为 `Account` 创建名称为 `interest` 的类属性。
231 |
232 | ```python
233 | >>> class Account:
234 | interest = 0.02 # 类属性
235 | def __init__(self, account_holder):
236 | self.balance = 0
237 | self.holder = account_holder
238 | # 在这里定义更多的方法
239 | ```
240 |
241 | 仍然可以从类的任何实例访问此属性。
242 |
243 | ```python
244 | >>> spock_account = Account('Spock')
245 | >>> kirk_account = Account('Kirk')
246 | >>> spock_account.interest
247 | 0.02
248 | >>> kirk_account.interest
249 | 0.02
250 | ```
251 |
252 | 但是,类属性的赋值会改变类的所有实例的属性值。
253 |
254 | ```python
255 | >>> Account.interest = 0.04
256 | >>> spock_account.interest
257 | 0.04
258 | >>> kirk_account.interest
259 | 0.04
260 | ```
261 |
262 | **属性名称**:我们已经在对象系统中引入了足够的复杂性,以至于我们必须指定如何将名称解析为特定属性。毕竟,我们可以很容易地拥有一个同名的类属性和一个实例属性。
263 |
264 | 正如我们所看到的,点表达式由表达式、点和名称组成:
265 |
266 | ```python
267 | .
268 | ```
269 |
270 | 计算点表达式:
271 |
272 | 1. 点表达式左侧的 `` ,生成点表达式的对象。
273 | 2. `` 与该对象的实例属性匹配;如果存在具有该名称的属性,则返回属性值。
274 | 3. 如果实例属性中没有 `` ,则在类中查找 ``,生成类属性。
275 | 4. 除非它是函数,否则返回属性值。如果是函数,则返回该名称绑定的方法。
276 |
277 | 在这个过程中,实例属性在类属性之前,就像本地名称在环境中优先于全局名称一样。在类中定义的方法与点表达式的对象相结合,以在此计算过程的第四步中形成绑定方法。在类中查找名称的过程具有其他细微差别,一旦我们引入类继承,很快就会出现这些细微差别。
278 |
279 | **属性赋值**:所有左侧包含点表达式的赋值语句都会影响该点表达式对象的属性。如果对象是实例,则赋值将设置实例属性。如果对象是类,则赋值将设置类属性。由于此规则,对对象的属性的赋值不会影响其类的属性。下面的示例说明了这种区别。
280 |
281 | 如果我们分配给帐户实例的命名属性 `interest`,我们将创建一个与现有类属性同名的新实例属性。
282 |
283 | ```python
284 | >>> kirk_account.interest = 0.08
285 | ```
286 |
287 | 并且该属性值将从点表达式返回。
288 |
289 | ```python
290 | >>> kirk_account.interest
291 | 0.08
292 | ```
293 |
294 | 但是,class 属性的 `interest` 仍保留其初始值,该值将针对其他账号(实例)返回。
295 |
296 | ```python
297 | >>> spock_account.interest
298 | 0.04
299 | ```
300 |
301 | 对类属性 `interest` 的更改将影响到 `spock_account` ,但 `kirk_account` 的实例属性将不受影响。
302 |
303 | ```python
304 | >>> Account.interest = 0.05 # 改变类属性
305 | >>> spock_account.interest # 实例属性发生变化(该实例中没有和类属性同名称的实例属性)
306 | 0.05
307 | >>> kirk_account.interest # 如果实例中存在和类属性同名的实例属性,则改变类属性,不会影响实例属性
308 | 0.08
309 | ```
310 |
311 | ## 2.5.5 继承
312 |
313 | 在面向对象编程范式中,我们经常会发现不同类型之间存在关联,尤其是在类的专业化程度上。即使两个类具有相似的属性,它们的特殊性也可能不同。
314 |
315 | 例如,我们可能需要实现一个支票账户,与标准账户不同,支票账户每次取款需额外收取 1 美元手续费,并且利率较低。下面我们展示了期望的行为。
316 |
317 | ```python
318 | >>> ch = CheckingAccount('Spock')
319 | >>> ch.interest # Lower interest rate for checking accounts
320 | 0.01
321 | >>> ch.deposit(20) # Deposits are the same
322 | 20
323 | >>> ch.withdraw(5) # withdrawals decrease balance by an extra charge
324 | 14
325 | ```
326 |
327 | `CheckingAccount` 是 `Account` 的特化。在 OOP 术语中,通用帐户将用作 `CheckingAccount` 的基类,而 `CheckingAccount` 将用作 `Account` 的子类。术语基类(base class)也常叫父类(parent class)和超类(superclass),而子类(subclass)也叫孩子类(child class)。
328 |
329 | 子类继承其父类的属性,但可以重写某些属性,包括某些方法。对于继承,我们只指定子类和父类之间的区别。我们在子类中未指定的任何内容都会被自动假定为与父类的行为一样。
330 |
331 | 继承在我们的对象隐喻中也起着作用,除了是一个有用的组织特征。继承旨在表示类之间的 is-a 关系,这与 has-a 关系形成对比。活期账户是一种特定类型的账户,因此从 `Account` 继承 `CheckingAccount` 是继承的适当使用。另一方面,银行有它管理的银行账户清单,所以任何一方都不应该从另一方继承。相反,帐户对象列表自然地表示为银行对象的实例属性。
332 |
333 | ## 2.5.6 使用继承
334 |
335 | 首先,我们给出了 `Account` 类的完整实现,其中包括该类及其方法的文档字符串。
336 |
337 | ```python
338 | >>> class Account:
339 | """一个余额非零的账户。"""
340 | interest = 0.02
341 | def __init__(self, account_holder):
342 | self.balance = 0
343 | self.holder = account_holder
344 | def deposit(self, amount):
345 | """存入账户 amount,并返回变化后的余额"""
346 | self.balance = self.balance + amount
347 | return self.balance
348 | def withdraw(self, amount):
349 | """从账号中取出 amount,并返回变化后的余额"""
350 | if amount > self.balance:
351 | return 'Insufficient funds'
352 | self.balance = self.balance - amount
353 | return self.balance
354 | ```
355 |
356 | 下面显示了 `CheckingAccount` 的完整实现。我们通过将计算结果为基类的表达式放在类名后面的括号中来指定继承。
357 |
358 | ```python
359 | >>> class CheckingAccount(Account):
360 | """从账号取钱会扣出手续费的账号"""
361 | withdraw_charge = 1
362 | interest = 0.01
363 | def withdraw(self, amount):
364 | return Account.withdraw(self, amount + self.withdraw_charge)
365 | ```
366 |
367 | 在这里,我们介绍一个 `CheckingAccount` 类的类属性 `withdraw_charge` 。我们为 `CheckingAccount` 的 `interest` 属性分配一个较低的值。我们还定义了一个新的 `withdraw` 方法来覆盖 `Account` 类中定义的行为。由于类套件中没有其他语句,所有其他行为都继承自基类 `Account` 。
368 |
369 | ```python
370 | >>> checking = CheckingAccount('Sam')
371 | >>> checking.deposit(10)
372 | 10
373 | >>> checking.withdraw(5)
374 | 4
375 | >>> checking.interest
376 | 0.01
377 | ```
378 |
379 | 表达式 `checking.deposit` 的计算结果是用于存款的绑定方法,该方法在 `Account` 类中定义。当 Python 解析点表达式中不是实例属性的名称时,它会在类中查找该名称。事实上,在类中“查找”名称的行为试图在原始对象的类的继承链中的每个父类中找到该名称。我们可以递归地定义此过程。
380 |
381 | 在类中查找名称。
382 |
383 | 1. 如果它命名在指定类中的属性,则返回属性值。
384 | 2. 否则,在该类的父类中查找该名称的属性。
385 |
386 | 在 `deposit` 的情况下,Python 将首先在实例上查找名称,然后在 `CheckingAccount` 类中查找名称。最后,它将在定义了 `deposit` 的 `Account` 类中查找。根据我们对点表达式的计算规则,由于 `deposit` 是在类中查找的 `checking` 实例的函数,因此点表达式的计算结果为绑定方法值。该方法使用参数 10 调用,该参数调用 deposit 方法,其中 `self` 绑定到 `checking` 对象, `amount` 绑定到 10。
387 |
388 | 对象的类始终保持不变。尽管在 `Account` 类中找到了 `deposit` 方法,但调用 `deposit` 时,`checking` 绑定到 `CheckingAccount` 的实例,而不是 `Account` 的实例。
389 |
390 | **调用父类**。重写的属性可以通过类对象来访问。例如,我们通过调用 `CheckingAccount` 中包含 `withdraw_charge` 参数的方法 `withdraw` 。该方法的实现是通过调用 `Account` 中的 `withdraw` 方法来实现的。
391 |
392 | 请注意,我们调用了 `self.withdraw_charge` 而不是等效的 `CheckingAccount.withdraw_charge` 。前者相对于后者的好处是,从 `CheckingAccount` 继承的类可能会覆盖 `withdraw_charge` 。如果是这种情况,我们希望我们的实现的 `withdraw` 找到新值而不是旧值。
393 |
394 | **接口**。在面向对象的程序中,不同类型的对象将共享相同的属性名称是极其常见的。对象接口是这些属性的属性和条件的集合。例如,所有帐户都必须具有采用数值参数的 `deposit` 和 `withdraw` 方法,以及 `balance` 属性。类 `Account` 和类 `CheckingAccount` 都实现此接口。继承(Inheritance)专门以这种方式促进名称共享。在某些编程语言(如 Java)中,必须显式声明接口实现。在其他对象(如 Python、Ruby 和 Go)中,任何具有适当名称的对象都实现了接口。
395 |
396 | 在使用对象(不是实现对象)的时候,我们只假设它们的属性,而不假设对象类型,则对将来的更改最可靠。例如耳朵,我们不先考虑它是猫的耳朵,还是狗的耳朵,我们只知道它是耳朵。我们只说耳朵是有形状的,它可以听见某种声音然后产生反馈的。等到以后,我们需要它是狗的耳朵时,我们在具体说它是漏斗状的,它的听力范围大概在 24 米左右。也就是说,他们使用对象抽象,而不是对其实现进行任何假设。
397 |
398 | 例如,假设我们运行彩票,我们希望将 5 美元存入每个帐户列表。以下实现不假定有关这些帐户类型的任何内容,因此同样适用于具有 `deposit` 方法的任何类型的对象:
399 |
400 | ```python
401 | >>> def deposit_all(winners, amount=5):
402 | for account in winners:
403 | account.deposit(amount) # 这里调用的是实例 account 的 deposit 方法
404 | # 对于不同实例来说,它们的 deposit 方法可能不同。这个例子相对于下面来讲,更加具有健壮性
405 | ```
406 |
407 | 上面的函数 `deposit_all` 仅假设每个 `account` 满足帐户对象抽象,因此它将与也实现此接口的任何其他帐户类一起使用。假设特定的帐户类将违反帐户对象抽象的抽象屏障。例如,以下实现不一定适用于新类型的帐户:
408 |
409 | ```python
410 | >>> def deposit_all(winners, amount=5):
411 | for account in winners:
412 | Account.deposit(account, amount) # 这里调用的是类 Account 中的 deposit 方法
413 | ```
414 |
415 | 我们将在本章后面更详细地讨论这个主题。
416 |
417 | ## 2.5.7 多继承
418 |
419 | Python 支持子类从多个基类继承属性的概念,这种语言功能称为多重继承(multiple inheritance)。
420 |
421 | 假设我们有一个从 `Account` 继承的 `SavingsAccount` ,但每次客户存款时都会向他们收取少量费用。
422 |
423 | ```python
424 | >>> class SavingsAccount(Account):
425 | deposit_charge = 2
426 | def deposit(self, amount):
427 | return Account.deposit(self, amount - self.deposit_charge)
428 | ```
429 |
430 | 然后,一位聪明的高管设想了一个具有 `CheckingAccount` 和 `SavingsAccount` 最佳功能的 `AsSeenOnTVAccount` 账户:提款费、存款费和低利率。它既是活期账户,又是储蓄账户!“如果我们建造它,”这位高管解释说,“有人会注册并支付所有这些费用。我们甚至会给他们一美元。
431 |
432 | ```python
433 | >>> class AsSeenOnTVAccount(CheckingAccount, SavingsAccount):
434 | def __init__(self, account_holder):
435 | self.holder = account_holder
436 | self.balance = 1 # 赠送的 1 $!
437 | ```
438 |
439 | 事实上,上面这段简短的代码已经实现了我们想要的功能了。取款和存款都将产生费用,分别使用 `CheckingAccount` 和 `SavingsAccount` 中的函数定义。
440 |
441 | ```python
442 | >>> such_a_deal = AsSeenOnTVAccount("John")
443 | >>> such_a_deal.balance
444 | 1
445 | >>> such_a_deal.deposit(20) # 调用 SavingsAccount 的 deposit 方法,会产生 2 $的存储费用
446 | 19
447 | >>> such_a_deal.withdraw(5) # 调用 CheckingAccount 的 withdraw 方法,产生 1 $的取款费用。
448 | 13
449 | ```
450 |
451 | 如果没有非歧义引用,就按预期正确解析:
452 |
453 | ```python
454 | >>> such_a_deal.deposit_charge
455 | 2
456 | >>> such_a_deal.withdraw_charge
457 | 1
458 | ```
459 |
460 | 但是,当引用不明确时,例如对 `Account` 和 `CheckingAccount` 中定义的 `withdraw` 方法的引用,该怎么办?下图描述了 `AsSeenOnTVAccount` 个类的继承图。每个箭头都指向从子类到基类。
461 |
462 | 
463 |
464 | 对于像这样的简单“菱形”形状,Python 会从左到右解析名称,然后向上解析名称。在此示例中,Python 按顺序检查以下类中的属性名称,直到找到具有该名称的属性:
465 |
466 | ```
467 | AsSeenOnTVAccount, CheckingAccount, SavingsAccount, Account, object
468 | ```
469 |
470 | 继承排序问题没有正确的解决方案,因为在某些情况下,我们可能更愿意将某些继承类置于其他类之上。但是,任何支持多重继承的编程语言都必须以一致的方式选择某些排序,以便该语言的用户可以预测其程序的行为。
471 |
472 | 进一步阅读。Python 使用称为 C3 方法解析排序的递归算法解析此名称。可以在所有类上使用 `mro` 方法查询任何类的方法解析顺序。
473 |
474 | ```python
475 | >>> [c.__name__ for c in AsSeenOnTVAccount.mro()]
476 | ['AsSeenOnTVAccount', 'CheckingAccount', 'SavingsAccount', 'Account', 'object']
477 | ```
478 |
479 | 找到方法解析顺序的具体算法不是本文的主题,但 Python 的主要作者已经提供了描述该算法的参考文献。
480 |
481 | ## 2.5.8 对象的作用
482 |
483 | Python 的对象系统旨在同时方便和灵活地实现数据抽象和消息传递。类、方法、继承和点表达式的特殊语法都使我们能够在程序中形式化对象的概念,从而提高我们组织大型程序的能力。换句话说,Python 的对象系统提供了一种方便而灵活的方法来创建和操作对象,使程序员能够更好地组织和管理复杂的程序。
484 |
485 | 特别是,我们希望我们的对象系统能够促进程序不同方面之间的关注点分离。程序中的每个对象封装和管理程序状态的某些部分,每个类语句定义实现程序整体逻辑的某些部分的函数。抽象障碍强制实施大型程序不同方面之间的边界。
486 |
487 | 面向对象编程非常适合用于模拟由独立但相互作用部分构成的系统。例如,不同用户在社交网络中进行交互,不同角色在游戏中进行交互,不同形状在物理模拟中进行交互。在表示这样的系统时,程序中的对象通常可以自然地映射到被建模系统中的对象,而类则代表它们的类型和关系。
488 |
489 | 另一方面,类可能不是实现某些抽象的最佳机制。函数式抽象提供了一个更自然的隐喻来表示输入和输出之间的关系。我们不应该觉得必须将程序中的每一点逻辑都塞进一个类中,尤其是在定义独立函数来操作数据更自然的情况下。函数还可以强制实现关注点的分离。换句话说,函数式编程提供了另一种有效地组织程序逻辑的方法,使得程序员能够更好地处理和维护程序。在某些情况下,使用函数式编程方法可能比使用面向对象编程更自然和有效。
490 |
491 | 多范式语言,如 Python,允许程序员将组织范式与适当的问题相匹配。学会识别何时引入新类,而不是新函数,以简化或模块化程序,是软件工程中一项重要的设计技能,值得认真关注。
492 |
--------------------------------------------------------------------------------
/sicp/2/6.md:
--------------------------------------------------------------------------------
1 | # 2.6 实现类和对象
2 |
3 | ::: details INFO
4 | 译者:[Jesper.Y](https://github.com/Jesper-Y)
5 |
6 | 来源:[2.6 Implementing Classes and Objects](https://www.composingprograms.com/pages/26-implementing-classes-and-objects.html)
7 |
8 | 对应:无
9 | :::
10 |
11 | 在面向对象编程范式(object-oriented programming paradigm)中工作时,我们使用对象的隐喻来指导程序的组织。大部分关于数据表示和操作的逻辑都通过类声明来表达。在本节中,我们将看到类和对象本身可以使用函数和字典来表示。通过以这种方式实现对象系统的目的是为了说明使用对象的隐喻并不需要特定的编程语言。即使在没有内置对象系统的编程语言中,程序也可以是面向对象的。
12 |
13 | 为了实现对象,我们将放弃点表示法(它需要内置语言支持),而是创建行为方式与内置对象系统的元素非常相似的调度字典。我们已经看到了如何通过调度字典实现消息传递的行为。为了完全实现对象系统,我们在实例、类和基类之间发送消息,它们都是包含属性的字典。
14 |
15 | 我们不会实现完整的 Python 对象系统,其中包括我们在本文中未涵盖的功能(例如元类(meta-class)和静态方法)。相反,我们将重点放在没有多重继承和内省行为(例如返回实例的类)的用户定义类上。我们的实现不打算严格遵循 Python 类型系统的规范。相反,它旨在实现支持对象隐喻的核心功能。
16 |
17 | ## 2.6.1 实例
18 |
19 | 我们从实例开始。实例拥有可以被设置并检索的具名属性,例如一个银行账户的实例 account 拥有具名属性 balance。我们使用调度字典实现一个实例,这个调度字典可以响应设置和获取("get" and "set")属性值的消息。属性本身存储在一个名为 **attributes** 的本地字典中。
20 |
21 | 正如我们在前面的章节所看到的,字典本身也是抽象的数据类型。我们使用函数实现数据对,使用数据对实现列表,然后使用列表实现字典。当我们使用字典实现一个对象系统时,牢记我们也可以仅仅只使用函数来实现对象。
22 |
23 | 在开始我们的实现之前,假定我们有一个类的实现,它可以查出任何不属于实例的名称。我们将一个类作为参数传递给 `make_instance` 的形参 `cls`。
24 |
25 | ```python
26 | >>> def make_instance(cls):
27 | """Return a new object instance, which is a dispatch dictionary."""
28 | def get_value(name):
29 | if name in attributes:
30 | return attributes[name]
31 | else:
32 | value = cls['get'](name)
33 | return bind_method(value, instance)
34 | def set_value(name, value):
35 | attributes[name] = value
36 | attributes = {}
37 | instance = {'get': get_value, 'set': set_value}
38 | return instance
39 | ```
40 |
41 | `instance` 是一个能够响应 `get` 和 `set` 消息的调度字典。`set` 消息和 Python 对象系统中的属性赋值操作相对应:所有已经赋值的属性直接存储在对象的本地属性字典中。在 `get` 消息中,如果 `name` 没有出现在本地 `attributes` 字典中,则会在类中查找。如果从类中查找返回的值是一个函数,则这个函数必须被绑定到实例。
42 |
43 | 对于绑定方法值。`make_instance` 中的 `get` 消息使用 `get_value` 函数在类中找到一个具名属性,然后调用 `bind_method` 函数。只有在这个具名属性是一个函数值时才会绑定一个方法,从函数值创建一个绑定方法值时,它会将 `instance` 作为第一个参数插入到函数值中,从而创建绑定方法值。
44 |
45 | ```python
46 | >>> def bind_method(value, instance):
47 | """Return a bound method if value is callable, or value otherwise."""
48 | if callable(value):
49 | def method(*args):
50 | return value(instance, *args)
51 | return method
52 | else:
53 | return value
54 | ```
55 |
56 | 在这个定义下,当方法被调用时,第一个参数 `self` 将会被绑定为 `instance` 的值。
57 |
58 | ## 2.6.2 类
59 |
60 | 不论是在 Python 的对象系统中还是在我们自己正在实现的对象系统中,都认为类也是一个对象。为了简单起见,我们可以说类这个对象并没有属于它的类类型(实际在 Python 中,类确实拥有它的类类型,几乎所有的类都共享同一个类类型,叫做 `type`)一个类可以响应 `get` 和 `set` 消息,以及 `new` 消息。
61 |
62 | ```python
63 | >>> def make_class(attributes, base_class=None):
64 | """Return a new class, which is a dispatch dictionary."""
65 | def get_value(name):
66 | if name in attributes:
67 | return attributes[name]
68 | elif base_class is not None:
69 | return base_class['get'](name)
70 | def set_value(name, value):
71 | attributes[name] = value
72 | def new(*args):
73 | return init_instance(cls, *args)
74 | cls = {'get': get_value, 'set': set_value, 'new': new}
75 | return cls
76 | ```
77 |
78 | 不像实例那样,类的 `get` 函数在没有找到属性时并不会查询它的类,而是查询它的基类(base_class)。类不需要进行方法绑定。
79 |
80 | 对于初始化,`make_class` 中的 `new` 函数会调用 `init_instance`,这个方法首先会创建一个新的实例,然后调用一个叫做 `__init__` 的方法。
81 |
82 | ```python
83 | >>> def init_instance(cls, *args):
84 | """Return a new object with type cls, initialized with args."""
85 | instance = make_instance(cls)
86 | init = cls['get']('__init__')
87 | if init:
88 | init(instance, *args)
89 | return instance
90 | ```
91 |
92 | 这个最终的函数完成了我们的对象系统。我们现在有了实例,它们会在本地 `set` 设置属性,但是在 `get` 获取属性时它们会转而回退到类中。实例从类中查询名称后,会将它自己绑定到函数值以此来创建方法。最后,类可以创建新的实例并且在创建后马上调用它们的 `__init__` 构造器函数。
93 |
94 | 在这个对象系统中,唯一应该被用户调用的函数是 `make_class`。所有其他的功能都是通过消息传递实现的。类似地,Python 的对象系统通过 `class` 语句调用,并且其他所有功能通过点表达式和对类的调用来实现。
95 |
96 | ## 2.6.3 使用已经实现的对象
97 |
98 | 我们现在重新使用前面的章节中的银行账户的例子。我们将使用我们自己实现的对象系统来创建一个 `Account` 类,一个 `CheckingAccount` 子类,以及为他们各自创建一个实例。
99 |
100 | `Account` 类通过 `make_account_class` 函数创建,这个函数和 Python 中的 `class` 语句有着相似结构的,但是最后调用了 `make_class`。
101 |
102 | ```python
103 | >>> def make_account_class():
104 | """Return the Account class, which has deposit and withdraw methods."""
105 | interest = 0.02
106 | def __init__(self, account_holder):
107 | self['set']('holder', account_holder)
108 | self['set']('balance', 0)
109 | def deposit(self, amount):
110 | """Increase the account balance by amount and return the new balance."""
111 | new_balance = self['get']('balance') + amount
112 | self['set']('balance', new_balance)
113 | return self['get']('balance')
114 | def withdraw(self, amount):
115 | """Decrease the account balance by amount and return the new balance."""
116 | balance = self['get']('balance')
117 | if amount > balance:
118 | return 'Insufficient funds'
119 | self['set']('balance', balance - amount)
120 | return self['get']('balance')
121 | return make_class(locals())
122 | ```
123 |
124 | 最后对 `locals` 的调用返回一个以字符串为 key 的字典,其中包含了当前局部帧的名称 - 值的绑定。
125 |
126 | `Account` 类最终通过赋值完成了实例化。
127 |
128 | ```python
129 | >>> Account = make_account_class()
130 | ```
131 |
132 | 然后一个账户实例通过 `new` 消息被创建,这要求一个与新创建的账户相关联的名称。
133 |
134 | ```python
135 | >>> kirk_account = Account['new']('kirk')
136 | ```
137 |
138 | 然后通过对 `kirk_account` 发送 `get` 消息就可以检索属性和方法。通过调用方法来更新账户的余额。
139 |
140 | ```python
141 | >>> kirk_account['get']('holder')
142 | 'kirk'
143 | >>> kirk_account['get']('interest')
144 | 0.02
145 | >>> kirk_account['get']('deposit')(20)
146 | 20
147 | >>> kirk_account['get']('withdraw')(5)
148 | 15
149 | ```
150 |
151 | 正如 Python 对象系统那样,设置一个实例的属性不会改变它的类中所对应的属性。
152 |
153 | ```python
154 | >>> kirk_account['set']('interest', 0.04)
155 | >>> Account['get']('interest')
156 | 0.02
157 | ```
158 |
159 | 对于继承,我们可以通过重载一部分类的属性来创建一个子类 `CheckingAccount`。在这种情况下,我们改变 `withdraw` 方法来收取费用,同时降低利率。
160 |
161 | ```python
162 | >>> def make_checking_account_class():
163 | """Return the CheckingAccount class, which imposes a $1 withdrawal fee."""
164 | interest = 0.01
165 | withdraw_fee = 1
166 | def withdraw(self, amount):
167 | fee = self['get']('withdraw_fee')
168 | return Account['get']('withdraw')(self, amount + fee)
169 | return make_class(locals(), Account)
170 | ```
171 |
172 | 在这个实现中,我们通过子类的 `withdraw` 函数调用了基类 `Account` 的 `withdraw` 函数。正如我们在 Python 的内置对象系统中会做的那样。我们可以像之前一样创建子类本身和一个实例。
173 |
174 | ```python
175 | >>> CheckingAccount = make_checking_account_class()
176 | >>> jack_acct = CheckingAccount['new']('Spock')
177 | ```
178 |
179 | 存款的行为与之前相同,构造函数也是如此。取款通过专门的 `withdraw` 方法收取了 1$ 的费用,并且 `interest` 通过 `CheckingAccount` 获得了新的较低的值。
180 |
181 | ```python
182 | >>> jack_acct['get']('interest')
183 | 0.01
184 | >>> jack_acct['get']('deposit')(20)
185 | 20
186 | >>> jack_acct['get']('withdraw')(5)
187 | 14
188 | ```
189 |
190 | 我们基于字典构建的对象系统在实现上与 Python 中的内置对象系统非常相似。在 Python 中,任何用户定义的类的实例都有一个特殊属性 `__dict__`,它将该对象的本地实例属性存储在一个字典中,就像我们的 `attribute` 字典一样。但 Python 与我们的系统不同之处在于,它区分了一些特殊方法,这些方法与内置函数交互,以确保这些函数对许多不同类型的参数都能正确运行。接下来的一节将详细讨论操作不同类型的函数的问题。
191 |
--------------------------------------------------------------------------------
/sicp/2/7.md:
--------------------------------------------------------------------------------
1 | # 2.7 对象抽象
2 |
3 | ::: details INFO
4 | 译者:[Jesper.Y](https://github.com/Jesper-Y)
5 |
6 | 来源:[2.7 Object Abstraction](https://www.composingprograms.com/pages/27-object-abstraction.html)
7 |
8 | 对应:Ants、Disc 09
9 | :::
10 |
11 | 对象系统允许程序员更高效地建立并使用抽象数据描述。其也设计为允许在同一个程序中存在多种抽象数据表现形式。
12 |
13 | 对象抽象的一个核心概念就是泛型函数,这种函数能够接受多种不同类型的值。我们将思考三种不同的用于实现泛型函数的技术:共享接口,类型派发和类型强制转换。在建立这些概念的过程中,我们也会发现一些 Python 对象系统的特性,这些特性支持泛型函数的创建。
14 |
15 | ## 2.7.1 字符串转换
16 |
17 | 为了高效地展示数据,一个对象值应该像它代表的数据一样进行行为,包括产生一个它自己的字符串表示。在像 Python 这样的交互式语言中,数据值的字符串表示是特别重要的,在交互式会话中,它可以自动地展示表达式的值的字符串形式。
18 |
19 | 字符串值为人们提供了一种相互传递信息的基本媒介。字符序列可以渲染在屏幕上、打印在纸上、大声阅读出来、转换成盲文或者以莫斯码广播。字符串也是编程的基础,因为他们可以表示 Python 表达式。
20 |
21 | Python 规定所有的对象都应该生成两个不同的字符串表示:一种是人类可读的文本,另一种是 Python 可解释的表示式。字符串的构造函数,即 `str`,返回一个人类可读的字符串。如果可能,`repr` 函数返回一个 Python 可解释的表达式,该表达式的求值结果与原对象相同。`repr` 的文档字符串(docstring)解释了这个特性:
22 |
23 | > _repr(object) -> string_
24 | >
25 | > _Return the canonical string representation of the object._
26 | >
27 | > _For most object types, eval(repr(object)) == object_
28 |
29 | > 返回对象的标准字符串表示。
30 | >
31 | > 对于大多数对象类型,`eval(repr(object)) == object`。
32 |
33 | 对于表达式的值调用 `repr` 的结果就是 Python 在交互式会话中所打印的内容。
34 |
35 | ```python
36 | >>> 12e12
37 | 12000000000000.0
38 | >>> print(repr(12e12))
39 | 12000000000000.0
40 | ```
41 |
42 | 在一些情况下,不存在对原始值的字符串表示时,Python 通常生成一个被尖括号包围的描述。
43 |
44 | ```python
45 | >>> repr(min)
46 | ''
47 | ```
48 |
49 | `str` 构造器通常与 `repr` 行为一致,但是在某些情况下它会提供一个更容易解释的文本表示。例如,我们可以看到 `str` 和 `repr` 对于日期对象的不同展示。
50 |
51 | ```python
52 | >>> from datetime import date
53 | >>> tues = date(2011, 9, 12)
54 | >>> repr(tues)
55 | 'datetime.date(2011, 9, 12)'
56 | >>> str(tues)
57 | '2011-09-12'
58 | ```
59 |
60 | 定义 `repr` 函数带来了一个新的挑战:我们想要它正确地应用于所有的数据类型,即使是那些实现 `repr` 时还不存在的类型。我们希望它是一个通用的或者多态(polymorphic)的函数,可以被应用于数据的多种(多)不同形式(态)。
61 |
62 | 在这情况下,对象系统提供了一种优雅的解决方案:`repr` 函数总是在其参数值上调用一个名为 `__repr__` 的方法。
63 |
64 | ```python
65 | >>> tues.__repr__()
66 | 'datetime.date(2011, 9, 12)'
67 | ```
68 |
69 | 通过在用户定义类中实现这个相同的方法,我们可以将 `repr` 函数的适用范围扩展到将来我们创建的任何类。这个例子突出了点表达式的另一个优势,那就是它们提供了一种机制,可以把现有的函数的作用域扩展到新的对象类型。
70 |
71 | `str` 构造器以类似的方式实现:它在其参数值上调用一个名为 `__str__` 的方法。
72 |
73 | ```python
74 | >>> tues.__str__()
75 | '2011-09-12'
76 | ```
77 |
78 | 这些能够应对多种类型的函数(多态函数)是一个更通用原则的例子:某些函数应该能够适用于多种数据类型。此外,创建这样一个函数的一种方式是使用在每个类中都有不同定义的共享属性名称,这就意味着这些函数在不同的类中会有不同的行为。
79 |
80 | ## 2.7.2 专用方法
81 |
82 | 在 Python 中,某些特殊名称会在特殊情况下被 Python 解释器调用。例如,类的 `__init__` 方法会在对象被创建时自动调用。`__str__` 方法会在打印时自动调用,`__repr__` 方法会在交互式环境显示其值的时候自动调用。
83 |
84 | 在 Python 中有一些为其他行为而准备的特殊名称。下面介绍其中某些常用的。
85 |
86 | 真值(True)和假值(False)。我们之前已经看到,数字类型在 Python 中拥有真值:更准确地说,0 是一个假值而其他所有数字都是真值。实际上 Python 中的所有对象都拥有真假值。默认情况下,用户定义类的对象被认为是真值,但是专门的 `__bool__` 方法可以用于覆盖这种行为。如果一个对象定义了 `__bool__` 方法,那么 Python 就会调用这个方法来确定它的真假值。
87 |
88 | 举一个例子,假设我们想让一个只有 0 存款的账号为假值。我们可以为 `Account` 添加一个 `__bool__` 方法来实现这种行为。
89 |
90 | ```python
91 | >>> Account.__bool__ = lambda self: self.balance != 0
92 | ```
93 |
94 | 我们可以调用 `bool` 构造器来看一个对象的真假值,同时我们也可以在布尔上下文中使用任何对象。
95 |
96 | ```python
97 | >>> bool(Account('Jack'))
98 | False
99 | >>> if not Account('Jack'):
100 | print('Jack has nothing')
101 | Jack has nothing
102 | ```
103 |
104 | 序列操作。我们已经知道使用 `len` 函数可以确定序列的长度。
105 |
106 | ```python
107 | >>> len('Go Bears!')
108 | 9
109 | ```
110 |
111 | `len` 函数调用了它的参数的 `__len__` 方法来确定其长度。所有的内置序列类型都实现了这个方法。
112 |
113 | ```python
114 | >>> 'Go Bears!'.__len__()
115 | 9
116 | ```
117 |
118 | 如果序列没有提供 `__bool__` 方法,那么 Python 会使用序列的长度来确定其真假值。空的序列是假值,而非空序列是真值。
119 |
120 | ```python
121 | >>> bool('')
122 | False
123 | >>> bool([])
124 | False
125 | >>> bool('Go Bears!')
126 | True
127 | ```
128 |
129 | `__getitem__` 方法由元素选择操作符调用,但也可以直接调用它。
130 |
131 | ```python
132 | >>> 'Go Bears!'[3]
133 | 'B'
134 | >>> 'Go Bears!'.__getitem__(3)
135 | 'B'
136 | ```
137 |
138 | 可调用对象。在 Python 中函数是一等对象,因此它们被作为数据进行传递,并且像其他对象那样拥有属性。Python 还允许我们定义像函数一样可以被“调用的对象”,只要在对象中包含一个 `__call__` 方法。通过这个方法,我们可以定义一个行为像高阶函数的类。
139 |
140 | 举个例子,思考下面这个高阶函数,它返回一个函数,这个函数将一个常量值加到其参数上。
141 |
142 | ```python
143 | >>> def make_adder(n):
144 | def adder(k):
145 | return n + k
146 | return adder
147 |
148 | >>> add_three = make_adder(3)
149 | >>> add_three(4)
150 | 7
151 | ```
152 |
153 | 我们可以创建一个 `Adder` 类,定义一个 `__call__` 方法来提供相同的功能。
154 |
155 | ```python
156 | >>> class Adder(object):
157 | def __init__(self, n):
158 | self.n = n
159 | def __call__(self, k):
160 | return self.n + k
161 | >>> add_three_obj = Adder(3)
162 | >>> add_three_obj(4)
163 | 7
164 | ```
165 |
166 | `Adder` 类表现的就像 `make_adder` 高阶函数,而 `add_three_obj` 表现得像 `add_three`。我们进一步模糊了数据和函数之间的界限。
167 |
168 | 算术运算。特定的方法也可以定义应用在用户定义的对象上的内置操作符的行为。为了提供这种通用性,Python 遵循特定的协议来应用每个操作符。例如,为了计算包含 `+` 操作符的表达式,Python 会检查操作符左右两侧的运算对象上是否有特定的方法。首先 Python 在左侧运算对象上检查其是否有 `__add__` 方法,然后在右侧运算对象上检查其是否有 `__radd__` 方法。如果找到了其中一个方法,就会将另一个运算对象的值作为它的参数来调用这个方法。下面的章节提供了一些示例。对于对其中进一步的细节感兴趣的读者,Python 文档详尽地描述了 [运算符的方法名称](http://docs.python.org/py3k/reference/datamodel.html#special-method-names)。《Dive into Python 3》有一个章节介绍了 [特殊方法名称](http://getpython3.com/diveintopython3/special-method-names.html),其中描述了这些特殊方法名称的使用方式。
169 |
170 | ## 2.7.3 多重表示
171 |
172 | 抽象障碍允许我们分离数据的使用和表示。然而在大型程序中,讨论数据类型的“底层表示”可能并不总是有意义。首先,一个数据对象可能由不止一种有用的表示,我们也许会想要设计能够处理多种表示形式的系统。
173 |
174 | 以一个简单的例子而言,复数可以用两种几乎相同的方式来表示:直角坐标系(实部和虚部)和极坐标系(幅度和角度)。有时直角坐标系更合适而有时极坐标系更合适。事实上,我们可以想象这样一个系统,复数在其中同时以两种形式表示,并且操作复数的函数可以处理任何一种表现形式。我们接下来实现这样是一个系统。需要注意的是,我们正在开发一个系统,这个系统使用通用操作符对复数进行数学运算,以此作为一个简单但不切实的示例程序。[复数类型](http://docs.python.org/py3k/library/stdtypes.html#typesnumeric) 已经内置在 Python 中,但为了示例我们将实现自己的复数类型。
175 |
176 | 允许数据多重表示的想法经常出现。大型的软件系统通常是由许多人长时间工作而设计的,同时受随时间变化的需求的影响。在这样的环境下,不可能让所有人都事先对于数据的表示达成一致的选择。除了使用数据抽象屏障将表示与使用隔离外,我们还需要抽象屏障来隔离不同的设计选择并允许不同的选择在同一个程序中共存。
177 |
178 | 我们将会在最高等级的抽象开始我们的实现,并逐步向具体的表现形式发展。复数是一个数值型(Number),数值可以相加或相乘。数值如何相加和相乘是通过名为 `add` 和 `mul` 的方法抽象出来的。
179 |
180 | ```python
181 | >>> class Number:
182 | def __add__(self, other):
183 | return self.add(other)
184 | def __mul__(self, other):
185 | return self.mul(other)
186 | ```
187 |
188 | 这个类要求数值型对象拥有 `add` 和 `mul` 方法,但是没有定义他们。此外,它并没有 `__init__` 方法。`Number` 的目的不是直接被初始化,而是作为一个不同特殊数值类的超类(superclass)提供服务。我们的下一个任务就是为复数类型恰当地定义 `add` 和 `mul`。
189 |
190 | 一个复数可以被认为是一个在二维空间中的点,这个空间有两个相互垂直的轴,即实轴和虚轴。基于这种观点,复数 `c = real + imag * i ( where i * i = -1 )` 可以被认为在一个平面上的点,它的水平坐标是 `real` 而垂直坐标是 `imag`。复数相加涉及到他们的 `real` 和 `imag` 各自相加。
191 |
192 | 当复数相乘时,把复数的表现形式认为是极坐标形式会更加自然,即表示为幅度(magnitude)和角度(angle)。两个复数相乘的结果是将一个复数按照另一个复数的长度拉伸,并将其旋转另一个复数的角度而得到的向量。
193 |
194 | `Complex` 类继承自 `Number` 类,并且给出了对复数的数学运算。
195 |
196 | ```python
197 | >>> class Complex(Number):
198 | def add(self, other):
199 | return ComplexRI(self.real + other.real, self.imag + other.imag)
200 | def mul(self, other):
201 | magnitude = self.magnitude * other.magnitude
202 | return ComplexMA(magnitude, self.angle + other.angle)
203 | ```
204 |
205 | 这个实现假定存在两个表示复数的类,分别对应他们的两种自然表示形式。
206 |
207 | - `ComplexRI` 使用实部和虚部构建一个复数。
208 | - `ComplexMA` 使用幅度和角度构建一个复数。
209 |
210 | 接口。对象属性是一种消息传递的形式,它允许不同的数据类型以不同的方式响应相同的信息。从不同的类中引发类似的行为的一组共享信息是一种强大的抽象方法。接口是一组共享的属性名称,以及对它们的行为的规范。对于复数来说,实现算术运算所需要的接口包括四个属性:`real`、`imam`、`magnitude` 和 `angle`。
211 |
212 | 为了使复数的算数运算正确,这些属性必须保持一致。也就是说,直角坐标(`real`,`imag`)和极坐标(`magnitude`,`angle`)在复平面上必须表示同一个点。`Complex` 类确定了如何使用这些属性来对复数进行 `add` 和 `mul` 操作,从而以这种方式隐式定义了这样一个接口。
213 |
214 | 属性。两个或多个属性相互之间保持一个固定关系的要求是一个新的问题。一种解决方案是只使用一种表现方式来存储属性值,并在需要时计算另一种表现方式。
215 |
216 | Python 有一种简单的计算属性的特性,可以通过零参数函数实时的计算属性。`@property` 修饰符允许函数在没有调用表达式语法(表达式后跟随圆括号)的情况下被调用。`Complex` 类存储了 `real` 和 `imag` 属性并在需要时计算 `magnitude` 和 `angle` 属性。
217 |
218 | ```python
219 | >>> from math import atan2
220 | >>> class ComplexRI(Complex):
221 | def __init__(self, real, imag):
222 | self.real = real
223 | self.imag = imag
224 | @property
225 | def magnitude(self):
226 | return (self.real ** 2 + self.imag ** 2) ** 0.5
227 | @property
228 | def angle(self):
229 | return atan2(self.imag, self.real)
230 | def __repr__(self):
231 | return 'ComplexRI({0:g}, {1:g})'.format(self.real, self.imag)
232 | ```
233 |
234 | 在这个实现下,所有四个属性复数算术运算所需要的属性都可以在不需要任何调用表达式的情况下被访问,并且对于 `real` 和 `imag` 的修改会反映到 `magnitude` 和 `angle` 中。
235 |
236 | ```python
237 | >>> ri = ComplexRI(5, 12)
238 | >>> ri.real
239 | 5
240 | >>> ri.magnitude
241 | 13.0
242 | >>> ri.real = 9
243 | >>> ri.real
244 | 9
245 | >>> ri.magnitude
246 | 15.0
247 | ```
248 |
249 | 类似的,`ComplexMA` 类存储了 `magnitude` 和 `angle` 属性,而在需要的时候计算 `real` 和 `imag`。
250 |
251 | ```python
252 | >>> from math import sin, cos, pi
253 | >>> class ComplexMA(Complex):
254 | def __init__(self, magnitude, angle):
255 | self.magnitude = magnitude
256 | self.angle = angle
257 | @property
258 | def real(self):
259 | return self.magnitude * cos(self.angle)
260 | @property
261 | def imag(self):
262 | return self.magnitude * sin(self.angle)
263 | def __repr__(self):
264 | return 'ComplexMA({0:g}, {1:g} * pi)'.format(self.magnitude, self.angle/pi)
265 | ```
266 |
267 | 对幅度和角度的改变同样也会立即反映到 `real` 和 `imag` 中。
268 |
269 | ```python
270 | >>> ma = ComplexMA(2, pi/2)
271 | >>> ma.imag
272 | 2.0
273 | >>> ma.angle = pi
274 | >>> ma.real
275 | -2.0
276 | ```
277 |
278 | 我们的复数实现现在已经完成了。在 `Complex` 类中,每一个实现复数的类都可以被用作任何一个算术运算函数的任何一个参数。
279 |
280 | ```python
281 | >>> from math import pi
282 | >>> ComplexRI(1, 2) + ComplexMA(2, pi/2)
283 | ComplexRI(1, 4)
284 | >>> ComplexRI(0, 1) * ComplexRI(0, 1)
285 | ComplexMA(1, 1 * pi)
286 | ```
287 |
288 | 使用接口来编码多重表示具有十分吸引人的特点。每一种表示形式的类都可以被单独开发;它们只需要就它们共享的属性名称和和对于这些属性的行为条件达成一致。接口还具有可添加性。如果程序员想要添加一个复数的第三方表现形式到同一个程序中,他们只需要创建一个拥有相同属性名称的类即可。
289 |
290 | 数据的多重表现形式和我们本章开始提到的数据抽象的概念联系密切。使用数据抽象,我们可以改变数据类型的实现而不需要改变程序的含义。通过接口和数据传递,我们可以在同一个程序中拥有多种不同的表现形式。在这两种情况下,一组名称和相应的行为条件定义的抽象使这种灵活性成为了可能。
291 |
292 | ## 2.7.4 泛型函数
293 |
294 | 泛型函数是适用于不同类型的参数的方法或函数。我们已经看过了许多例子。`Complex.add` 方法是泛型的,因为它可以接受 `ComplexRI` 或 `ComplexMA` 作为值。通过确保 `ComplexRI` 和 `ComplexMA` 共用同一个接口,我们已经获得了这种灵活性。使用接口和消息传递只是多种可以被用于实现泛型函数的方法中的一种。在本节我们将会考虑另外两个方法:类型派发和类型强制转换。
295 |
296 | 假设,除了我们的复数类,我们还实现了一个 `Rational` 类来精确地表示分数。`add` 和 `mul` 方法和之前的章节中出现的 `add_rational` 以及 `mul_rational` 都表示相同的运算。
297 |
298 | ```python
299 | >>> from fractions import gcd
300 | >>> class Rational(Number):
301 | def __init__(self, numer, denom):
302 | g = gcd(numer, denom)
303 | self.numer = numer // g
304 | self.denom = denom // g
305 | def __repr__(self):
306 | return 'Rational({0}, {1})'.format(self.numer, self.denom)
307 | def add(self, other):
308 | nx, dx = self.numer, self.denom
309 | ny, dy = other.numer, other.denom
310 | return Rational(nx * dy + ny * dx, dx * dy)
311 | def mul(self, other):
312 | numer = self.numer * other.numer
313 | denom = self.denom * other.denom
314 | return Rational(numer, denom)
315 | ```
316 |
317 | 我们已经通过包含 `add` 和 `mul` 方法实现了 `Number` 超类的接口。因此,我们可以使用熟悉的运算符来对分数进行相加或相乘。
318 |
319 | ```python
320 | >>> Rational(2, 5) + Rational(1, 10)
321 | Rational(1, 2)
322 | >>> Rational(1, 4) * Rational(2, 3)
323 | Rational(1, 6)
324 | ```
325 |
326 | 然而,我们还不能把一个分数加到复数上,即使这样的结合在数学上已经有了准确的定义。我们希望以某种精心受控制的方式引入这种跨类型的操作。以便在不严重违反我们的抽象屏障的情况下支持它。在我们期待的结果中存在着一种矛盾:我们希望能够将一个复数和一个分数相加,并且我们想要使用泛型的 `__add__` 方法来正确的处理所有的数值类型。同时我们希望尽可能地分离复数和分数的关注点,以维护一个模块化的程序。
327 |
328 | 类型派发。一种实现跨类型操作的方式是选择基于函数或方法的参数类型来选择相应的行为。类型派发的思想是写一个能够检查它所收到的参数的类型的函数,然后根据参数类型执行恰当的代码。
329 |
330 | 内置的函数 `isinstance` 接受一个对象或一个类。如果对象的类是所给的类或者继承自所给的类,它会返回一个真值。
331 |
332 | ```python
333 | >>> c = ComplexRI(1, 1)
334 | >>> isinstance(c, ComplexRI)
335 | True
336 | >>> isinstance(c, Complex)
337 | True
338 | >>> isinstance(c, ComplexMA)
339 | False
340 | ```
341 |
342 | 类型派发的一个简单例子是 `is_real` 函数,它对不同的复数类型使用不同的实现。
343 |
344 | ```python
345 | >>> def is_real(c):
346 | """Return whether c is a real number with no imaginary part."""
347 | if isinstance(c, ComplexRI):
348 | return c.imag == 0
349 | elif isinstance(c, ComplexMA):
350 | return c.angle % pi == 0
351 |
352 | >>> is_real(ComplexRI(1, 1))
353 | False
354 | >>> is_real(ComplexMA(2, pi))
355 | True
356 | ```
357 |
358 | 类型派发并不总是使用 `isinstance`,对于算术运算,我们会提供一个 `type_tag` 的属性给 `Rational` 和 `Complex` 实例,这个属性拥有一个字符串值。当两个值 `x` 和 `y` 有相同的 `type_tag` 时,我们可以直接使用 `x.add(y)` 来结合它们,否则我们需要跨类型操作。
359 |
360 | ```python
361 | >>> Rational.type_tag = 'rat'
362 | >>> Complex.type_tag = 'com'
363 | >>> Rational(2, 5).type_tag == Rational(1, 2).type_tag
364 | True
365 | >>> ComplexRI(1, 1).type_tag == ComplexMA(2, pi/2).type_tag
366 | True
367 | >>> Rational(2, 5).type_tag == ComplexRI(1, 1).type_tag
368 | False
369 | ```
370 |
371 | 为了结合复数和分数,我们写一个同时依赖于它们两个的表现形式的函数。接下来,我们依靠这样一个事实,即 `Rational` 能够被精确转换为 `float` 值,而这个值是一个实数。转换的结果可以和一个复数值相结合。
372 |
373 | ```python
374 | >>> def add_complex_and_rational(c, r):
375 | return ComplexRI(c.real + r.numer/r.denom, c.imag)
376 | ```
377 |
378 | 乘法涉及到相似的转换。在极坐标系中,复平面中的实数总是有一个正的幅度值。角度 0 象征一个正数。角度 `pi` 象征一个负数。
379 |
380 | ```python
381 | >>> def mul_complex_and_rational(c, r):
382 | r_magnitude, r_angle = r.numer/r.denom, 0
383 | if r_magnitude < 0:
384 | r_magnitude, r_angle = -r_magnitude, pi
385 | return ComplexMA(c.magnitude * r_magnitude, c.angle + r_angle)
386 | ```
387 |
388 | 加法和乘法都是可交换的,因此交换参数顺序可以使用相同的跨类型操作实现。
389 |
390 | ```python
391 | >>> def add_rational_and_complex(r, c):
392 | return add_complex_and_rational(c, r)
393 | >>> def mul_rational_and_complex(r, c):
394 | return mul_complex_and_rational(c, r)
395 | ```
396 |
397 | 类型派发的作用是保证这些跨类型的操作能在恰当的时候被使用。接下来,我们重写 `Number` 超类来为它的 `__add__` 和 `__mul__` 方法使用类型派发。
398 |
399 | 我们使用 `type_tag` 属性来区分参数的类型。也可以直接使用内置的 `isinstance` 方法,但是使用标签可以简化实现。使用类型标签也可以说明类型派发并不是必然和 Python 对象属性相联系的。而是一种在异构域上创建泛型函数的通用技术。
400 |
401 | `__add__` 方法考虑了两种情况。首先,如果两个参数具有相同的类型标签,那么它会假定第一个参数的 `add` 方法可以接受第二个参数作为其参数。否则,他会检查一个叫做 `adders` 的包含跨类型实现的字典,看其是否包含了可以对这些参数类型进行相加的函数。如果有这样一个函数,`cross_apply` 方法会找到并应用它。`__mul__` 方法有着相似的结构。
402 |
403 | ```python
404 | >>> class Number:
405 | def __add__(self, other):
406 | if self.type_tag == other.type_tag:
407 | return self.add(other)
408 | elif (self.type_tag, other.type_tag) in self.adders:
409 | return self.cross_apply(other, self.adders)
410 | def __mul__(self, other):
411 | if self.type_tag == other.type_tag:
412 | return self.mul(other)
413 | elif (self.type_tag, other.type_tag) in self.multipliers:
414 | return self.cross_apply(other, self.multipliers)
415 | def cross_apply(self, other, cross_fns):
416 | cross_fn = cross_fns[(self.type_tag, other.type_tag)]
417 | return cross_fn(self, other)
418 | adders = {("com", "rat"): add_complex_and_rational,
419 | ("rat", "com"): add_rational_and_complex}
420 | multipliers = {("com", "rat"): mul_complex_and_rational,
421 | ("rat", "com"): mul_rational_and_complex}
422 | ```
423 |
424 | 在这个 `Number` 类的新定义中,所有的跨类型实现都在 `adders` 和 `multipliers` 字典中使用类型标签对进行索引。
425 |
426 | 基于字典的类型派发是可扩展的。`Number` 的新子类可以通过声明新的类型标签并添加跨类型操作到 `Number.adders` 和 `Number.multipliers` 中来将自己安装到系统中。它们还可以在子类中定义自己的 `adders` 和 `multipliers`。
427 |
428 | 尽管我们已经引入了一些复杂的东西到系统中,但现在我们可以在加法和乘法表达式中混合不同类型。
429 |
430 | ```python
431 | >>> ComplexRI(1.5, 0) + Rational(3, 2)
432 | ComplexRI(3, 0)
433 | >>> Rational(-1, 2) * ComplexMA(4, pi/2)
434 | ComplexMA(2, 1.5 * pi)
435 | ```
436 |
437 | 强制转换。在完全不相关的操作和完全不相关的类型的一般情况下,实现显式的跨类型操作即是会有些繁琐,但仍然是我们所能期待的最好的结果。幸运的是,通过利用类型系统中可能潜在额外的结构,有时我们可以做得更好。通常不同的数据类型并不是完全不相关的,可能存在一些方法将一种类型视为另一种类型。这个过程被称为强制转换。例如,如果我们被要求使用算术方法结合一个分数和一个复数,则可以把分数看作一个虚部为零的复数。然后我们就可以使用 `Complex.add` 和 `Complex.mul` 来结合它们。
438 |
439 | 通常来说,我们可以通过设计一个强制转换函数实现这个想法,这个函数可以把一种类型的对象转换为另一种类型的等价对象。这里有一个典型的强制转换函数,它可以把一个分数转换为一个虚部为零的复数。
440 |
441 | ```python
442 | >>> def rational_to_complex(r):
443 | return ComplexRI(r.numer/r.denom, 0)
444 | ```
445 |
446 | `Number` 类的另一种定义通过尝试强制两种参数转换为相同类型来执行跨类型操作。`coercions` 字典通过类型标签对索引了所有可能的强制转换,指示相关值将第一个类型的值转换为第二个类型。
447 |
448 | 通常情况下不可能随意地将任何一种类型的数据对象转换为其他所有类型。例如,不可能将任意的复数类型转换为分数,因此在 `coercions` 字典中不会有这样的转换实现。
449 |
450 | `coerce` 方法返回两个具有相同类型标签的值。它会检查它的参数类型标签,将其和 `coercions` 字典中的条目进行比较,然后使用 `coerce_to` 将一个参数转换为另一个参数的类型。在 `coercions` 字典中只需要有一个条目就可以完成跨类型算术系统,从而取代了类型派发版本的 `Number` 中的四个跨类型函数。
451 |
452 | ```python
453 | >>> class Number:
454 | def __add__(self, other):
455 | x, y = self.coerce(other)
456 | return x.add(y)
457 | def __mul__(self, other):
458 | x, y = self.coerce(other)
459 | return x.mul(y)
460 | def coerce(self, other):
461 | if self.type_tag == other.type_tag:
462 | return self, other
463 | elif (self.type_tag, other.type_tag) in self.coercions:
464 | return (self.coerce_to(other.type_tag), other)
465 | elif (other.type_tag, self.type_tag) in self.coercions:
466 | return (self, other.coerce_to(self.type_tag))
467 | def coerce_to(self, other_tag):
468 | coercion_fn = self.coercions[(self.type_tag, other_tag)]
469 | return coercion_fn(self)
470 | coercions = {('rat', 'com'): rational_to_complex}
471 | ```
472 |
473 | 这个强制转换方案相比定义确定的跨类型操作具有一些优势。即使我们仍然需要写强制转换函数将各种类型联系起来,但对于每个类型对只需要一个函数而不是对每组类型和每个通用操作都编写不同函数。我们在这里依赖一个事实,即类型之间恰当地转换仅仅取决于类型自身,而与被应用到的特定操作无关。
474 |
475 | 扩展强制转换可以带来更多优势。一些更加精于设计的强制转换方案不仅仅只是尝试将一种类型转换到另一种,而是会尝试将两种不同的类型都转换为第三种通用类型。例如一个菱形和一个矩形:它们都不是对方的特殊情况,但是它们都可以被看作四边形。另一种强制转换的扩展是迭代强制转换,将一种数据类型通过中间类型转换到另一种类型。例如整数可以通过第一次转换成有理数,然后从有理数转换为实数。链式强制转换可以减少程序中所要求的强制转换的函数总量。
476 |
477 | 即使有很多优势,但是强制转换也有潜在的缺点。例如,强制转换函数可能在应用时丢失信息。在我们的例子中,分数是准确的表示,但是当它被转换成复数时则变成了近似值。
478 |
479 | 有些编程语言自带了自动类型强制转换的功能。事实上,Python 在早期版本中,对象上就有一个叫 `__coerce__` 的特殊方法用于实现这一功能。但是,随着时间的推移,人们发现这种内置的强制转换系统的实用价值并不能抵消它的复杂性,因此这个特性最终被移除了。取而代之的是,特定的运算符会根据需要对其参数进行强制转换。
480 |
--------------------------------------------------------------------------------
/sicp/2/8.md:
--------------------------------------------------------------------------------
1 | # 2.8 效率
2 |
3 | ::: details INFO
4 | 译者:[laziyu](https://github.com/lizzy-0323)
5 |
6 | 来源:[2.8 Efficiency](https://www.composingprograms.com/pages/28-efficiency.html)
7 |
8 | 对应:无
9 | :::
10 |
11 | 决定如何表示和处理数据通常受到替代方案效率的影响。效率指的是表示或处理所使用的计算资源,例如计算函数结果或表示对象所需的时间和内存量。这些数量可以根据实现的细节而大大不同。
12 |
13 | ## 2.8.1 测量效率
14 |
15 | 测量程序运行所需的时间或消耗的内存确切值是具有挑战性的,因为结果取决于计算机配置的许多细节。更可靠地表征程序的效率的方法是测量某些事件发生的次数,例如函数调用次数。
16 |
17 | 让我们回到我们第一个树递归函数,即用于计算斐波那契数列中的数字的 `fib` 函数。
18 |
19 | ```py
20 | >>> def fib(n):
21 | if n == 0:
22 | return 0
23 | elif n == 1:
24 | return 1
25 | else:
26 | return fib(n - 2) + fib(n - 1)
27 | >>> fib(5)
28 | 5
29 | ```
30 |
31 | 考虑计算 `fib(6)` 时的计算模式,如下所示,为了计算 `fib(5)`,我们计算 `fib(3)` 和 `fib(4)`。而要计算 `fib(3)`,我们需要计算 `fib(1)` 和 `fib(2)`。总体而言,这个演化过程看起来像一棵树。每个蓝色圆点表示在遍历这棵树时计算出的一个斐波那契数的完成计算。
32 |
33 | 
34 |
35 | 这个函数作为一个典型的树递归函数具有教学意义,但它是计算斐波那契数的一种极其低效的方式,因为它进行了大量的冗余计算。计算 `fib(3)` 的整个过程被重复执行。
36 |
37 | 我们可以测量这种低效性。这个高阶的 `count` 函数返回一个与其参数等效的函数,并且还维护一个 `call_count` 属性。通过这种方式,我们可以检查 `fib` 函数被调用的次数。
38 |
39 | ```py
40 | >>> def count(f):
41 | def counted(n):
42 | counted.call_count += 1
43 | return f(n)
44 | counted.call_count = 0
45 | return counted
46 | ```
47 |
48 | 通过计算对 `fib` 函数的调用次数,我们可以发现所需的调用次数增长速度比斐波那契数列本身还要快。这种调用的快速增长是树递归函数的特征。
49 |
50 | ```py
51 | >>> fib = count(fib)
52 | >>> fib(19)
53 | 4181
54 | >>> fib.call_count
55 | 10946
56 | ```
57 |
58 | **空间**。要了解函数的空间需求,我们通常必须指定在计算的环境模型中如何使用、保留和回收内存。在计算表达式时,解释器会保存所有活动的环境,以及这些环境引用的所有值和帧。我们称一个环境是活动的,如果它为正在计算的某个表达式提供了评估上下文。每当为其创建第一个帧的函数调用最终返回时,环境将变为非活动状态。
59 |
60 | 例如,在计算 `fib` 时,解释器按照之前显示的顺序计算每个值,遍历树的结构。为此,它只需要跟踪在计算的任何时刻位于当前节点上方的那些节点。用于计算其他分支的内存可以被回收,因为它不会影响未来的计算。总的来说,树递归函数所需的空间将与树的最大深度成比例。
61 |
62 | 以下的图表描述了计算 `fib(3)` 时所创建的环境。在计算初始应用 `fib` 的返回表达式时,表达式 `fib(n-2)` 被计算,得到一个值为 0。一旦这个值被计算出来,相应的环境帧(被标记为灰色)就不再需要:它不是一个活动环境的一部分。因此,一个良好设计的解释器可以回收用于存储该帧的内存。另一方面,如果解释器当前正在计算 `fib(n-1)` ,则通过这个 `fib` 应用(其中 n 为 2)所创建的环境是活动的。反过来,最初用于将 `fib` 应用于 3 的环境是活动的,因为其返回值尚未被计算出来。
63 |
64 |
65 |
66 | 高阶函数 `count_frames` 用于跟踪尚未返回的函数调用次数 `open_count` 。它通过在计算过程中记录当前活动的函数调用次数来实现。`max_count` 属性是 `open_count` 曾经达到的最大值,它对应于在计算过程中同时处于活动状态的最大帧数。
67 |
68 | ```py
69 | >>> def count_frames(f):
70 | def counted(n):
71 | counted.open_count += 1
72 | counted.max_count = max(counted.max_count, counted.open_count)
73 | result = f(n)
74 | counted.open_count -= 1
75 | return result
76 | counted.open_count = 0
77 | counted.max_count = 0
78 | return counted
79 | >>> fib = count_frames(fib)
80 | >>> fib(19)
81 | 4181
82 | >>> fib.open_count
83 | 0
84 | >>> fib.max_count
85 | 19
86 | >>> fib(24)
87 | 46368
88 | >>> fib.max_count
89 | 24
90 | ```
91 |
92 | 总结一下, `fib` 函数的空间要求(以活动帧数衡量)比输入小一个单位,这往往是较小的。而以递归调用次数衡量的时间要求比输出大,这往往是巨大的。
93 |
94 | ## 2.8.2 记忆化
95 |
96 | 树递归的计算过程通常可以通过记忆化(Memoization)来提高效率,这是一种增加递归函数效率的强大技术。记忆化函数会保存之前接收到的参数的返回值。如果第二次调用 `fib(25)` ,它不会再通过递归重新计算返回值,而是直接返回已经计算好的结果。
97 |
98 | 记忆化可以自然地表达为一个高阶函数,也可以用作装饰器。下面的定义创建了一个缓存(cache),用于存储先前计算过的结果,索引是它们计算所用的参数。使用字典作为缓存的数据结构要求被记忆化的函数的参数是不可变的。
99 |
100 | ```py
101 | >>> def memo(f):
102 | cache = {}
103 | def memorized(n):
104 | if n not in cache:
105 | cache[n] = f(n)
106 | return cache[n]
107 | return memorized
108 | ```
109 |
110 | 如果我们将 `memo` 应用于斐波那契数列的递归计算,将会产生一个新的计算模式,如下所示。
111 |
112 | 
113 |
114 | 在计算 `fib(5)` 时,在计算右侧分支的 `fib(4)` 时,已经计算过的 `fib(2)` 和 `fib(3)` 的结果被重复使用。因此,许多树递归计算根本不需要进行。
115 |
116 | 通过使用 `count` 函数,我们可以看到对于每个唯一的输入,`fib` 函数实际上只被调用一次。
117 |
118 | ```py
119 | >>> counted_fib = count(fib)
120 | >>> fib =memo(count_fib)
121 | >>> fib(19)
122 | 4181
123 | >>> counted_fib.call_count
124 | 20
125 | >>> fib(34)
126 | 5702887
127 | >>> counted_fib.call_count
128 | 35
129 | ```
130 |
131 | ## 2.8.3 增长阶数
132 |
133 | 计算过程在消耗计算资源(时间和空间)的速率上可能有很大的差异,正如之前的例子所展示的那样。然而,准确地确定调用函数时将使用多少空间或时间是一项非常困难的任务,这取决于许多因素。分析一个计算过程的有用方法是将其与一组具有类似需求的过程分类。一种有用的分类是该过程的增长阶(Orders of Growth),它以简单的术语表达了计算过程的资源需求随输入的函数增长。
134 |
135 | 为了介绍增长阶的概念,我们将分析下面的函数 `count_factors` ,它用于计算能够整除输入 $n$ 的整数的数量。该函数尝试将 $n$ 除以小于等于其平方根的每个整数。这个实现利用了以下事实:如果 $k$ 能够整除 $n$ ,且 $k < \sqrt{n}$ ,那么必然存在另一个因子 $j = n / k$,使得 $j > \sqrt{n}$。
136 |
137 |
138 |
139 | 计算 `count_factors` 所需的时间是多少?准确的答案会因不同的计算机而异,但我们可以对涉及的计算量做出一些有用的一般观察。这个过程执行 `while` 语句的次数是小于等于 $\sqrt{n}$ 的最大整数。在 `while` 语句之前和之后的语句分别执行一次。因此,总共执行的语句数为 $w*\sqrt{n}+v$ ,其中 $w$ 是 `while` 循环体中的语句数, $v$ 是 `while` 循环外的语句数。虽然这不是一个精确的公式,但它通常能够描述作为输入 $n$ 的函数而要计算的时间量。
140 |
141 | 要获得更精确的描述是很困难的。常量 $w$ 和 $v$ 实际上并不是常数,因为对因子的赋值语句有时会被执行,有时不会。增长阶分析使我们能够忽略这些细节,而是着重于增长的一般趋势。特别是, `count_factors` 的增长阶表达了以 $\sqrt{n}$ 速度计算 `count_factors(n)` 所需的时间,其中可能存在一些常量因子的误差范围。
142 |
143 | **Theta 符号(Theta Notation)** 是一种用于表示算法的渐进性能的数学符号。在 Theta 符号中,我们考虑一个参数 $n$ ,它衡量了某个计算过程的输入规模大小,并且用 $R(n)$ 表示该过程对于输入规模 $n$ 所需的某种资源量。在我们之前的例子中,我们将 $n$ 视为要计算给定函数的数值,但还有其他可能性。例如,如果我们的目标是计算一个数的平方根的近似值,我们可以将 $n$ 视为所需的有效位数。
144 |
145 | $R(n)$ 可以表示所使用的内存量、执行的基本机器步骤数量等等。在每次执行固定数量步骤的计算机中,计算表达式所需的时间将与在计算过程中执行的基本步骤数量成正比。
146 |
147 | 我们可以说如果存在正常数 $k_1$ 和 $k_2$ (与 $n$ 无关),使得对于任何大于某个最小值 $m$ 的 $n$ ,成立如下不等式:
148 |
149 | $$k1⋅f(n)≤R(n)≤k2⋅f(n)$$
150 |
151 | 我们称 $R(n)$ 的增长阶为 $Θ(f(n))$ ,用 $R(n)=Θ(f(n))$ 表示(读作“_theta of f(n)_”)。换句话说,对于大的 $n$ , $R(n)$ 的值总是夹在两个与 $f(n)$ 成比例的值之间:
152 |
153 | - 一个下界 $k1 ⋅ f(n)$ 和
154 | - 一个上界 $k2 ⋅ f(n)$
155 |
156 | 通过检查函数体,我们可以应用这个定义来展示计算 `count_factors(n)` 所需的步骤数量随着 $Θ(\sqrt{n})$ 增长
157 |
158 | 首先,我们选择 $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}$ 。
159 |
160 | 其次,我们可以验证上界。我们假设 `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}$ 。我们的论证到这里完成了。
161 |
162 | ## 2.8.4 示例:指数运算
163 |
164 | 考虑计算给定数的指数问题。我们希望有一个函数,它以底数 $b$ 和正整数指数 $n$ 作为参数,并计算 $b_n$ 。一种实现方法是通过递归定义:
165 |
166 | $$b^n = b⋅b^{(n-1)}$$
167 | $$b^0 = 1$$
168 |
169 | 这个递归定义可以很容易的转化为递归函数:
170 |
171 | ```py
172 | >>> def exp(b, n):
173 | if n == 0:
174 | return 1
175 | return b * exp(b, n - 1)
176 | ```
177 |
178 | 这是一个线性递归过程,它需要 $Θ(n)$ 步骤和 $Θ(n)$ 空间。就像阶乘一样,我们可以很容易地设计一个等效的线性迭代版本,它需要相似数量的步骤,但只需要常量空间。
179 |
180 | 这意味着递归的指数计算是一个线性过程,它的时间和空间复杂度都随着指数 $n$ 的增长而线性增加。与此相反,通过线性迭代,我们可以将计算复杂度优化为仅需常数级的空间,但步骤数量仍然是与指数 $n$ 成线性关系的。
181 |
182 | ```py
183 | >>> def exp_iter(b, n):
184 | result = 1
185 | for _ in range(n):
186 | result = result * b
187 | return result
188 | ```
189 |
190 | 我们可以通过使用连续平方法来以更少的步骤计算指数。例如,计算 b 的 8 次幂可以如下进行:
191 |
192 | $$b^8 = b * (b * (b * (b * (b * (b * (b * b))))))$$
193 |
194 | 而我们可以使用三次乘法来计算它:
195 |
196 | $$b^2 = b * b$$
197 | $$b^4= b^2 * b^2$$
198 | $$b^8= b^4 * b^4$$
199 |
200 | 这种方法对于指数是 2 的幂次的情况非常有效。我们也可以利用连续平方法来计算一般情况下的指数,如果我们使用如下递归规则:
201 |
202 | $$
203 | b^n =
204 | \begin{cases}
205 | \left(b^{\frac{n}{2}}\right)^2 & \text{如果 } n \text{ 是偶数} \\
206 | b \cdot b^{n-1} & \text{如果 } n \text{ 是奇数}
207 | \end{cases}
208 | $$
209 |
210 | 我们也可以用一个递归函数的方式来表达这个方法:
211 |
212 | ```py
213 | >>> def square(x):
214 | return x * x
215 |
216 | >>> def fast_exp(b, n):
217 | if n == 0:
218 | return 1
219 | if n % 2 == 0:
220 | return square(fast_exp(b, n // 2))
221 | else:
222 | return b * fast_exp(b, n - 1)
223 |
224 | >>> fast_exp(2, 100)
225 | 1267650600228229401496703205376
226 | ```
227 |
228 | 通过快速指数运算(`fast_exp`),计算过程在时间和空间上都以对数级别随 $n$ 增长。要理解这一点,观察一下使用 `fast_exp` 计算 $b$ 的 $2n$ 次幂仅仅比计算 $b$ 的 $n$ 次幂多进行一次乘法。因此,我们可以看到每一次允许的新乘法使得我们能够计算的指数大小翻倍(粗略估计)。因此,对于指数 $n$ ,所需的乘法次数大致以 2 为底数的 $n$ 的对数增长。这个过程具有 $Θ(log_{n})$ 的增长阶。
229 |
230 | 当 $n$ 变得很大时, $Θ(log_{n})$ 增长阶与 $Θ(n)$ 增长阶之间的差异会变得非常明显。例如,对于 $n$ 为 1000,使用 `fast_exp` 只需要 14 次乘法,而不是 1000 次。这显示了使用快速指数运算相对于普通指数运算在效率上的巨大优势。
231 |
232 | ## 2.8.5 增长类别
233 |
234 | 增长阶是为了简化计算过程的分析和比较而设计的。许多不同的计算过程可以具有等效的增长阶,这表示它们的增长方式相似。对于计算机科学家来说,了解和识别常见的增长阶并确定具有相同增长阶的过程是一项重要的技能。
235 |
236 | **常数项**:常数项不影响计算过程的增长阶。因此,例如, $Θ(n)$ 和 $Θ(500⋅n)$ 具有相同的增长阶。这个性质来自于 $Θ$ 符号的定义,它允许我们选择任意的常数 $k_1$ 和 $k_2$(比如 $\frac{1}{500}$ )作为上界和下界。为了简洁起见,增长阶中常数通常被忽略。
237 |
238 | **对数**:对数的底数不影响计算过程的增长阶。例如,$log2(n)$ 和 $log10(n)$ 具有相同的增长阶。改变对数的底数等价于乘以一个常数因子。
239 |
240 | **嵌套**:当内部的计算过程在外部过程的每一步中重复执行时,整个过程的增长阶是外部和内部过程的步骤数的乘积。
241 |
242 | 例如,下面的函数 `overlap` 计算列表 `a` 中与列表 `b` 中出现的元素数量。
243 |
244 | ```py
245 | >>> def overlap(a, b):
246 | count = 0
247 | for item in a:
248 | if item in b:
249 | count += 1
250 | return count
251 |
252 | >>> overlap([1, 3, 2, 2, 5, 1], [5, 4 ,2])
253 | 3
254 | ```
255 |
256 | 对于列表的 `in` 运算符,其时间复杂度为 $Θ(n)$ ,其中 $n$ 是列表 `b` 的长度。它被应用 $Θ(m)$ 次,其中 $m$ 是列表 `a` 的长度。表达式 `item in b` 是内部过程,而 `for item in a` 循环是外部过程。该函数的总增长阶是 $Θ(m⋅n)$ 。
257 |
258 | 低阶项。随着计算过程的输入增长,计算中增长最快的部分将主导总的资源使用。$Θ$ 符号捕捉了这种直觉。总的来说,除了增长最快的项外,其他项都可以忽略而不影响增长阶。
259 |
260 | 例如,考虑函数 `one_more`,它返回列表 `a` 中有多少个元素是另一个元素的值加 1。也就是说,在列表 `[3, 14, 15, 9]` 中,元素 15 比 14 大 1,所以 `one_more` 将返回 1。
261 |
262 | ```py
263 | >>> def one_more(a):
264 | return overlap(a, [x + 1 for x in a])
265 |
266 | >>> one_more([3, 14, 15, 9])
267 | 1
268 | ```
269 |
270 | 这个计算分为两个部分:列表推导和对 `overlap` 的调用。对于长度为 $n$ 的列表 a,列表推导需要 $Θ(n)$ 步,而对 `overlap` 的调用需要 $Θ(n^2)$ 步。这两部分的总步数是 $Θ(n + n^2)$ ,但这并不是表达增长阶最简单的方式。
271 |
272 | $Θ(n^2 + k*n)$ 和 $Θ(n^2)$ 对于任意常数 $k$ 来说是等价的,因为对于任何 $k$,$n^2$ 项在足够大的 $n$ 下将主导总和。这里的边界要求仅对于大于某个最小值 $m$ 的 $n$ 成立,从而确立了这种等价性。为简洁起见,增长阶中的低阶项通常被忽略,所以我们不会在 $theta$ 表达式中看到求和。
273 |
274 | **常见的类别。** 通过这些等价性,我们可以得到一小组常见的类别来描述大多数计算过程。最常见的类别如下,按从最慢到最快的增长顺序列出,并描述了随着输入增加而增长的情况。接下来将给出每个类别的例子。
275 |
276 | | 类别 | $Θ$ 表示 | 增长阶描述 | 例子 |
277 | | ---- | ------------ | ------------------------------------------------- | ---------- |
278 | | 常数 | $Θ(1)$ | 增长与输入无关 | `abs` |
279 | | 对数 | $Θ(log_{n})$ | 增长与输入的大小成正比 | `fast_exp` |
280 | | 线性 | $Θ(n)$ | 随着输入的递增,计算过程所需的资源会增加 n 个单位 | `exp` |
281 | | 乘方 | $Θ(n^2)$ | 随着输入的递增,计算过程所需的资源会增加 n 个单位 | `one_more` |
282 | | 指数 | $Θ(b^n)$ | 随着输入的递增,计算过程所需的资源会成倍增加 | `fib` |
283 |
284 | 除了这些之外,还有其他的增长阶类别,例如 `count_factors` 的 $Θ(\sqrt{n})$ 增长。然而,上述列举的类别是特别常见的。
285 |
286 | 指数增长描述了许多不同的增长阶,因为改变底数 $b$ 会影响增长阶。例如,我们的树递归斐波那契计算 `fib` 的步数随输入 $n$ 呈指数增长。特别地,我们可以证明第 $n$ 个斐波那契数是最接近于
287 |
288 | $$\frac{\phi^{n-2}}{\sqrt{5}}$$
289 |
290 | 的整数,其中 $ϕ$ 是黄金比例:
291 |
292 | $$\phi=\frac{1+\sqrt{5}}{2}≈1.6180$$
293 |
294 | 我们还提到步数与结果值成正比,所以树递归过程需要 $Θ(ϕ^n)$ 步,这是一个随着 $n$ 以指数级别增长的函数。
295 |
--------------------------------------------------------------------------------
/sicp/2/9.md:
--------------------------------------------------------------------------------
1 | # 2.9 递归对象
2 |
3 | ::: details INFO
4 | 译者:[Hhankyangg](https://github.com/Hhankyangg)
5 |
6 | 来源:[2.9 Recursive Objects](https://www.composingprograms.com/pages/29-recursive-objects.html)
7 |
8 | 对应:HW 05、Lab 08、Disc 08
9 | :::
10 |
11 | 对象可以以其他的对象作为自己的属性值。当这个类下的对象实例有一个属性的值还属于这个类时,这个对象就是一个递归对象。
12 |
13 | ## 2.9.1 类:链表
14 |
15 | 在之前的章节中我们已经提到过,链表 (Linked List) 由两个部分组成:第一个元素和链表剩下的部分。而剩下的这部分链表它本身就是个链表,这就是链表的递归定义法。其中,一个比较特殊的情况是空链表——他没有第一个元素和剩下的部分。
16 |
17 | 链表是一种序列 (sequence):它具有有限的长度并且支持通过索引选择元素。
18 |
19 | 现在我们可以实现具有相同行为的类。在现在这个版本中,我们将使用专用方法名来定义它的行为,专用方法允许我们的类可以使用 Python 内置的 `len` 函数和元素选择操作符(方括号或 `operator.getitem` )。这些内置函数将调用类的专用方法:长度由 `__len__` 计算,元素选择由 `__getitem__` 计算。空链表由一个长度为 0 且没有元素的空元组表示。
20 |
21 | ```py
22 | >>> class Link:
23 | """一个链表"""
24 | empty = ()
25 | def __init__(self, first, rest=()):
26 | assert rest == Link.empty or isinstance(rest, Link)
27 | self.first = first
28 | self.rest = rest
29 | def __getitem__(self, i):
30 | if i == 0:
31 | return self.first
32 | else:
33 | return self.rest[i-1]
34 | def __len__(self):
35 | return 1 + len(self.rest)
36 | >>> s = Link(3, Link(4, Link(5)))
37 | >>> len(s)
38 | 3
39 | >>> s[1]
40 | 4
41 | ```
42 |
43 | 以上的 `__len__` 和 `__getitem__` 的定义都是递归的。Python 的内置函数 `len` 在它接收属于用户定义的类的实例时调用了 `__len__` 方法。类似地,元素选择操作符则会调用 `__getitem__` 方法。因此,这两个方法定义的主体中都会间接的调用他们自己。对于 `__len__` 来说,基线条件 (base case) 就是当 `self.rest` 计算得到一个空元组,也就是 `Link.empty` 时,此时长度为 0。
44 |
45 | 内置的 `isinstance` 函数返回第一个参数的类型是否属于或者继承自第二个参数。`isinstance(rest, Link)` 在 `rest` 是 `Link` 的实例或 `Link` 的子类的实例时为 `True`。
46 |
47 | 我们对于链表的类定义已经很完备了,但是现在我们还无法直观地看到 `Link` 的实例。为了方便之后的调试工作,我们再定义一个函数去将一个 `Link` 实例转换为一个字符串表达式。
48 |
49 | ```py
50 | >>> def link_expression(s):
51 | """返回一个可以计算得到 s 的字符串表达式。"""
52 | if s.rest is Link.empty:
53 | rest = ''
54 | else:
55 | rest = ', ' + link_expression(s.rest)
56 | return 'Link({0}{1})'.format(s.first, rest)
57 | >>> link_expression(s)
58 | 'Link(3, Link(4, Link(5)))'
59 | ```
60 |
61 | 用这个方法去展示一个链表实在是太方便了,以至于我想在任何需要展示一个 `Link` 的实例的时候都用上它。为了达到这个美好愿景,我们可以将函数 `link_expression` 作为专用方法 `__repr__` 的值。Python 在展示一个用户定义的类的实例时,会调用它们的 `__repr__` 方法。
62 |
63 | ```py
64 | >>> Link.__repr__ = link_expression
65 | >>> s
66 | Link(3, Link(4, Link(5)))
67 | ```
68 |
69 | `Link` 类具有闭包性质 (closure property)。就像列表的元素可以是列表一样,一个 `Link` 实例的第一个元素也可以是一个 `Link` 实例。
70 |
71 | ```py
72 | >>> s_first = Link(s, Link(6))
73 | >>> s_first
74 | Link(Link(3, Link(4, Link(5))), Link(6))
75 | ```
76 |
77 | 链表 `s_first` 只有两个元素,但它的第一个元素是一个有着三个元素的链表。
78 |
79 | ```py
80 | >>> len(s_first)
81 | 2
82 | >>> len(s_first[0])
83 | 3
84 | >>> s_first[0][2]
85 | 5
86 | ```
87 |
88 | 递归函数特别适合操作链表。比如,递归函数 `extend_link` 建立了一个新的链表,这个新链表是由链表 `s` 和其后边跟着的链表 `t` 组成的。将这个函数作为 `Link` 类的方法 `__add__` 就可以仿真内置列表的加法运算。
89 |
90 | ```py
91 | >>> def extend_link(s, t):
92 | if s is Link.empty:
93 | return t
94 | else:
95 | return Link(s.first, extend_link(s.rest, t))
96 | >>> extend_link(s, s)
97 | Link(3, Link(4, Link(5, Link(3, Link(4, Link(5))))))
98 | >>> Link.__add__ = extend_link
99 | >>> s + s
100 | Link(3, Link(4, Link(5, Link(3, Link(4, Link(5))))))
101 | ```
102 |
103 | 想要实现列表推导式 (List Comprehensions),也就是从一个链表生成另一个链表,我们需要两个高阶函数:`map_link` 和 `filter_link`。其中 `map_link` 将函数 `f` 应用到链表 `s` 的每个元素,并将结果构造成一个新的链表。
104 |
105 | ```py
106 | >>> def map_link(f, s):
107 | if s is Link.empty:
108 | return s
109 | else:
110 | return Link(f(s.first), map_link(f, s.rest))
111 | >>> map_link(square, s)
112 | Link(9, Link(16, Link(25)))
113 | ```
114 |
115 | 函数 `filter_link` 返回了一个链表,这个链表过滤掉了原链表 `s` 中使函数 `f` 不返回真的元素,留下了其余的元素。通过组合使用 `map_link` 和 `filter_link`,我们可以达到和列表推导式相同的逻辑过程和结果。
116 |
117 | ```py
118 | >>> def filter_link(f, s):
119 | if s is Link.empty:
120 | return s
121 | else:
122 | filtered = filter_link(f, s.rest)
123 | if f(s.first):
124 | return Link(s.first, filtered)
125 | else:
126 | return filtered
127 | >>> odd = lambda x: x % 2 == 1
128 | >>> map_link(square, filter_link(odd, s))
129 | Link(9, Link(25))
130 | >>> [square(x) for x in [3, 4, 5] if odd(x)]
131 | [9, 25]
132 | ```
133 |
134 | 函数 `join_link` 递归的构造了一个 包含着所有在链表里的元素,并且这些元素被字符串 `separator` 分开 的字符串。这个结果相较于 `link_expression` 来说就更加简练。
135 |
136 | ```py
137 | >>> def join_link(s, separator):
138 | if s is Link.empty:
139 | return ""
140 | elif s.rest is Link.empty:
141 | return str(s.first)
142 | else:
143 | return str(s.first) + separator + join_link(s.rest, separator)
144 | >>> join_link(s, ", ")
145 | '3, 4, 5'
146 | ```
147 |
148 | **递归构造 (Recursive Construction).** 当以增量方式构造序列时,链表特别有用,这种情况在递归计算中经常出现
149 |
150 | 第一章我们介绍过的函数 `count_partitions` 通过树递归计算了使用大小最大为 `m` 的数对整数 `n` 进行分区的方法的个数。通过序列,我们还可以使用类似的过程显式枚举这些分区。
151 |
152 | 与计数方法个数的方法相同,我们利用相同的递归统计方法:将一个数 `n` 在最大数限制为 `m` 下分区包含两种情况:
153 |
154 | 1. 用 `m` 及以内的整数划分 `n-m`
155 | 2. 用 `m-1` 及以内的整数划分 `n`
156 |
157 | 对于基线情况,我们知道 0 只有一个分区方法,而划分一个负整数或使用小于 1 的数划分是不可能的。
158 |
159 | ```py
160 | >>> def partitions(n, m):
161 | """Return a linked list of partitions of n using parts of up to m.
162 | Each partition is represented as a linked list.
163 | """
164 | if n == 0:
165 | return Link(Link.empty) # A list containing the empty partition
166 | elif n < 0 or m == 0:
167 | return Link.empty
168 | else:
169 | using_m = partitions(n-m, m)
170 | with_m = map_link(lambda s: Link(m, s), using_m)
171 | without_m = partitions(n, m-1)
172 | return with_m + without_m
173 | ```
174 |
175 | 在递归情况下,我们构造两个分区子列表。第一个使用 `m`,因此我们将 `m` 添加到 `using_m` 的结果的每个元素中以形成 `with_m`。
176 |
177 | 分区的结果是高度嵌套的:是一个链表的链表。使用带有适当分隔符的 `join_link`,我们可以以易读的方式显示各个分区。
178 |
179 | ```py
180 | >>> def print_partitions(n, m):
181 | lists = partitions(n, m)
182 | strings = map_link(lambda s: join_link(s, " + "), lists)
183 | print(join_link(strings, "\n"))
184 | >>> print_partitions(6, 4)
185 | 4 + 2
186 | 4 + 1 + 1
187 | 3 + 3
188 | 3 + 2 + 1
189 | 3 + 1 + 1 + 1
190 | 2 + 2 + 2
191 | 2 + 2 + 1 + 1
192 | 2 + 1 + 1 + 1 + 1
193 | 1 + 1 + 1 + 1 + 1 + 1
194 | ```
195 |
196 | ## 2.9.2 类:树
197 |
198 | 树也可以用用户定义的类的实例来表示,而不是内置序列类型的嵌套实例。树是具有 **作为属性的分支序列** 的任何数据结构,同时,这些分支序列也是树。
199 |
200 | **内部值。** 以前,我们定义树的方式是假设所有的值都出现在叶子上。(译者:哪里讲过这个我怎么不清楚,读者若清楚可以提出 issue 或者提交 pr 修改此处。)然而在每个子树的根处定义具有内部值的树也很常见。内部值在树中称为 `label`。下面的 `Tree` 类就表示这样的树,其中每棵树都有一系列分支,这些分支也是树。
201 |
202 | ```py
203 | >>> class Tree:
204 | def __init__(self, label, branches=()):
205 | self.label = label
206 | for branch in branches:
207 | assert isinstance(branch, Tree)
208 | self.branches = branches
209 | def __repr__(self):
210 | if self.branches:
211 | return 'Tree({0}, {1})'.format(self.label, repr(self.branches))
212 | else:
213 | return 'Tree({0})'.format(repr(self.label))
214 | def is_leaf(self):
215 | return not self.branches
216 | ```
217 |
218 | 例如,`Tree` 类可以表示 **用于计算斐波那契数的函数 `fib`** 的递归表达式树 中计算的值。下面的函数 `fib_tree(n)` 返回一个 `Tree` 的实例,该实例以第 n 个斐波那契数作为其 `label`,并在其分支中跟踪所有先前计算的斐波那契数。
219 |
220 | ```py
221 | >>> def fib_tree(n):
222 | if n == 1:
223 | return Tree(0)
224 | elif n == 2:
225 | return Tree(1)
226 | else:
227 | left = fib_tree(n-2)
228 | right = fib_tree(n-1)
229 | return Tree(left.label + right.label, (left, right))
230 | >>> fib_tree(5)
231 | Tree(3, (Tree(1, (Tree(0), Tree(1))), Tree(2, (Tree(1), Tree(1, (Tree(0), Tree(1)))))))
232 | ```
233 |
234 | 并且,`Tree` 类的实例也可以用递归函数进行处理。例如,我们可以对树的 `label` 求和。
235 |
236 | ```py
237 | >>> def sum_labels(t):
238 | """对树的 label 求和,可能得到 None。"""
239 | return t.label + sum([sum_labels(b) for b in t.branches])
240 | >>> sum_labels(fib_tree(5))
241 | 10
242 | ```
243 |
244 | 我们也可以用 `memo` 去构造一个斐波那契树。有了它,重复的子树只会被记忆版本的 `fib_tree` 创建一次,然后在不同大小的树中被用作分支很多次。(译者:更多关于 `memo` 的内容可以看 [Python 中 的 memoize 和 memoized_memoize](https://blog.csdn.net/ztf312/article/details/82823336) 或者自行 Google 搜索)
245 |
246 | ```py
247 | >>> fib_tree = memo(fib_tree)
248 | >>> big_fib_tree = fib_tree(35)
249 | >>> big_fib_tree.label
250 | 5702887
251 | >>> big_fib_tree.branches[0] is big_fib_tree.branches[1].branches[1]
252 | True
253 | >>> sum_labels = memo(sum_labels)
254 | >>> sum_labels(big_fib_tree)
255 | 142587180
256 | ```
257 |
258 | 在这种情况下,通过记忆节省的计算时间和内存量是相当可观的。我们现在只创建 35 个实例,而不是创建 18,454,929 个不同的 `Tree` 实例。
259 |
260 | ## 2.9.3 集合
261 |
262 | 除了列表、元组、字典,Python 还有第四种内置的容器类型:集合(set),集合字面量遵循了它的数学表示法:用大括号括起来元素们。重复的元素会在创建集合时被移除。集合是无序的,这表明元素被打印的顺序可能和他们在字面量中的顺序不同。
263 |
264 | ```py
265 | >>> s = {3, 2, 1, 4, 4}
266 | >>> s
267 | {1, 2, 3, 4}
268 | ```
269 |
270 | Python 的集合支持多种操作:包括成员测试、长度计算以及并集和交集的标准集合操作。
271 |
272 | ```py
273 | >>> 3 in s
274 | True
275 | >>> len(s)
276 | 4
277 | >>> s.union({1, 5})
278 | {1, 2, 3, 4, 5}
279 | >>> s.intersection({6, 5, 4, 3})
280 | {3, 4}
281 | ```
282 |
283 | 除了 `union` 和 `intersection` 外,Python 还支持一些其他的集合方法:`isdisjoint`, `issubset`, 和 `issuperset` 提供了集合之间的比较。集合是可变的,可以用 `add`, `remove`, `discard` 和 `pop` 方法来一次改变一个元素。其他的一些方法提供了一次改变多个元素的功能,比如 `clear` 和 `update`。Python 的官方文档 [documentation for sets](http://docs.python.org/py3k/library/stdtypes.html#set) 在课应该足够容易理解,并为课程填写了更多细节。
284 |
285 | **实现集合。** 抽象地说,集合 (set) 是支持成员检验、并集、交集和合集的不同对象的集合 (collection)。支持将元素和集合连接在一起将返回一个新集合,该集合包含原集合的所有元素以及新元素 (如果新元素是不同的)。并集和交集返回分别出现在其中一个集合或同时出现在两个集合中的元素集合。与任何数据抽象一样,我们可以自由地在提供此 行为集合 (collection) 的集合的任何表示上实现任何函数。
286 |
287 | 在本节的其余部分中,我们将考虑实现集合的三种不同方法,它们的表示方式各不相同。我们将通过分析集合运算的增长顺序来表征这些不同表示的效率。我们将使用本节前面的 `Link` 和 `Tree` 类,它们可以为基本集合操作提供简单而优雅的递归解决方案。
288 |
289 | **作为无序序列的集合。** 表示集合的一种方法是将其表示为一个元素出现次数不超过一次的序列。空集合由空序列表示。成员测试将会递归地遍历列表。
290 |
291 | ```py
292 | >>> def empty(s):
293 | return s is Link.empty
294 | >>> def set_contains(s, v):
295 | """当且仅当 set s 包含 v 时返回 True。"""
296 | if empty(s):
297 | return False
298 | elif s.first == v:
299 | return True
300 | else:
301 | return set_contains(s.rest, v)
302 | >>> s = Link(4, Link(1, Link(5)))
303 | >>> set_contains(s, 2)
304 | False
305 | >>> set_contains(s, 5)
306 | True
307 | ```
308 |
309 | `set_contains` 的实现需要的时间复杂度是 $Θ(n)$ ,其中 n 是集合 s 的大小。使用这个线性时间函数来表示成员关系,我们可以在线性时间内将一个元素与一个集合相邻地组合到一起。
310 |
311 | ```py
312 | >>> def adjoin_set(s, v):
313 | """返回一个包含 s 的所有元素和元素 v 的所有元素的集合。"""
314 | if set_contains(s, v):
315 | return s
316 | else:
317 | return Link(v, s)
318 | >>> t = adjoin_set(s, 2)
319 | >>> t
320 | Link(2, Link(4, Link(1, Link(5))))
321 | ```
322 |
323 | 在设计时,我们应该考虑的问题之一是 **效率**。相交两个集合 `set1` 和 `set2` 也需要进行成员测试,但这次必须测试 `set1` 的每个元素在 `set2` 中的隶属性:这会从而导致步骤数的二次增长,即为 $Θ(n^2)$。
324 |
325 | ```py
326 | >>> def intersect_set(set1, set2):
327 | """返回一个集合,包含 set1 和 set2 中的公共元素。"""
328 | return keep_if_link(set1, lambda v: set_contains(set2, v))
329 | >>> intersect_set(t, apply_to_all_link(s, square))
330 | Link(4, Link(1))
331 | ```
332 |
333 | 在计算两个集合的并集时,我们必须小心不要重复包含任何元素。该 `union_set` 函数还需要线性的时间来进行成员资格测试,从而导致整个过程的时间复杂的为 $Θ(n^2)$。
334 |
335 | ```py
336 | >>> def union_set(set1, set2):
337 | """返回一个集合,包含 set1 和 set2 中的所有元素。"""
338 | set1_not_set2 = keep_if_link(set1, lambda v: not set_contains(set2, v))
339 | return extend_link(set1_not_set2, set2)
340 | >>> union_set(t, s)
341 | Link(2, Link(4, Link(1, Link(5))))
342 | ```
343 |
344 | **作为有序序列的集合。** 加快集合操作的一种方法是改变表示方法,使集合元素按递增顺序排列。要做到这一点,我们需要一些方法来比较两个对象,这样我们就可以知道哪个更大。在 Python 中,可以使用 < 和 > 操作符比较许多不同类型的对象,但在本例中我们将集中讨论数字。我们将通过按递增顺序列出其元素来表示一组数字。
345 |
346 | 排序的一个优点体现在 `set_contains` 中:在检查对象是否存在时,我们不再需要遍历整个集合。如果到达的集合元素比要查找的元素大,则知道该元素不在集合中:
347 |
348 | ```py
349 | >>> def set_contains(s, v):
350 | if empty(s) or s.first > v:
351 | return False
352 | elif s.first == v:
353 | return True
354 | else:
355 | return set_contains(s.rest, v)
356 | >>> u = Link(1, Link(4, Link(5)))
357 | >>> set_contains(u, 0)
358 | False
359 | >>> set_contains(u, 4)
360 | True
361 | ```
362 |
363 | 这样可以节省多少步骤?在最坏的情况下,我们正在寻找的元素可能是集合中最大的一个,因此步骤数与无序表示相同。另一方面,如果我们搜索许多不同大小的项,我们可以预期,有时我们可以在列表开始附近的某个点停止搜索,而其他时候我们仍然需要检查列表的大部分。平均而言,我们预计需要检查集合中大约一半的项目。因此,所需的平均步数约为 $\frac 1 2$ 。这仍然是 $Θ(n)$ 的增长,但它相较于之前的解决方法确实能帮我们节省一些时间。
364 |
365 | 通过重新实现 `intersect_set`,我们可以获得显著的加速。在无序表示中,此操作需要 $Θ(n^2)$ 的时间复杂度,因为我们对 `set1` 的每个元素执行了是否在 `set2` 的完整扫描。但是对于有序表示,我们可以使用更聪明的方法:同时遍历两个集合,跟踪 `set1` 中的元素 `e1` 和 `set2` 中的元素 `e2`。当 `e1` 和 `e2` 相等时,我们将该元素包含在交集中。
366 |
367 | 对于此算法我们做以下思考:假设 `e1 < e2`。由于 `e2` 小于 `set2` 的剩余元素,我们可以立即得出结论:`e1` 不可能出现在 `set2` 的剩余元素中,因此不在交集中。因此,我们不再需要考虑 `e1`。我们丢弃它并继续处理 `set1` 的下一个元素。当 `e2 < e1` 时,类似的逻辑查询 `set2` 的下一个元素。函数如下:
368 |
369 | ```py
370 | >>> def intersect_set(set1, set2):
371 | if empty(set1) or empty(set2):
372 | return Link.empty
373 | else:
374 | e1, e2 = set1.first, set2.first
375 | if e1 == e2:
376 | return Link(e1, intersect_set(set1.rest, set2.rest))
377 | elif e1 < e2:
378 | return intersect_set(set1.rest, set2)
379 | elif e2 < e1:
380 | return intersect_set(set1, set2.rest)
381 | >>> intersect_set(s, s.rest)
382 | Link(4, Link(5))
383 | ```
384 |
385 | 为了估计这个过程所需的步骤数,我们观察到在每一步中我们至少缩小一个集合的大小。因此,所需的步数最多是 `set1` 和 `set2` 的大小之和,而不是像无序表示那样是大小的乘积。这是 $Θ(n)$ 增长而不是 $Θ(n^2)$ 。这是相当大的时间节省,对于即使是中等规模的集合都十分有意义。例如,两个大小为 100 的集合的交集将需要大约 200 步,而无序表示则需要 10,000 步。
386 |
387 | 对于用有序序列表示的集合,也可以在线性时间内计算并集,添加元素等。这些实现将作为练习留下。
388 |
389 | **作为二叉搜索树的集合。**
390 |
391 | 我们可以通过将集合元素以 **恰好有两个分支的树的形式** 排列来做得比有序序列更好。
392 |
393 | - 树的根的 `entry` 保存集合的一个元素。
394 | - 左分支中的条目包括所有小于根节点的元素。
395 | - 右分支中的条目包括所有大于根节点的元素。
396 |
397 | 下图显示了代表集合 `{1,3,5,7,9,11}` 的一些树。同一个集合可以用树以许多不同的方式表示。在所有的二叉搜索树中,左分支中的所有元素都小于根节点,而右子树中的所有元素都大于根节点。
398 |
399 | 
400 |
401 | 二叉搜索树表示的优点是:假设我们想要检查一个值 `v` 是否包含在一个集合中。我们先比较 `v` 和 `entry`。如果 `v < entry`,我们知道我们只需要搜索左子树;如果 `v > entry`,我们只需要搜索右子树。现在,如果树是“平衡的”,即每个子树的大小将是原始树的一半左右。这样,我们一步就把搜索大小为 $n$ 的树的问题简化为搜索大小为 $\frac 1 2$ 的树的问题。由于树的大小在每一步中减半,我们应该期望搜索树所需的步数增长为 $Θ(log\ n)$。对于大型集合,这将比以前的表示有显著的加速。这个 `set_contains` 函数利用了树结构的集合的特点:
402 |
403 | ```py
404 | >>> def set_contains(s, v):
405 | if s is None:
406 | return False
407 | elif s.entry == v:
408 | return True
409 | elif s.entry < v:
410 | return set_contains(s.right, v)
411 | elif s.entry > v:
412 | return set_contains(s.left, v)
413 | ```
414 |
415 | 将元素加入到集合的实现与搜索类似,也需要 $Θ(log \ n)$ 。我们将 `v` 与 `entry` 进行比较,以确定 `v` 应该加到右分支还是左分支,并将 `v` 邻接到适当的分支后,将这个新构造的分支与原始 `entry` 和另一个分支拼接在一起。如果 `v` 等于这个元素,我们就返回这个节点。如果我们被要求将 `v` 与一棵空树相连,我们生成一棵树,它的 `entry` 是 `v`,左右分支都是空的。函数如下:
416 |
417 | ```py
418 | >>> def adjoin_set(s, v):
419 | if s is None:
420 | return Tree(v)
421 | elif s.entry == v:
422 | return s
423 | elif s.entry < v:
424 | return Tree(s.entry, s.left, adjoin_set(s.right, v))
425 | elif s.entry > v:
426 | return Tree(s.entry, adjoin_set(s.left, v), s.right)
427 | >>> adjoin_set(adjoin_set(adjoin_set(None, 2), 3), 1)
428 | Tree(2, Tree(1), Tree(3))
429 | ```
430 |
431 | 我们认为搜索树可以在对数步数中完成,但这是基于树是“平衡的”假设,即每棵树的左子树和右子树具有大约相同数量的元素,因此每个子树包含大约一半的父树元素。但是,我们怎么能确定我们建造的树木将是平衡的呢?即使我们从平衡树开始,使用 `adjoin_set` 添加元素也可能产生不平衡的结果。因为新附加元素的位置取决于该元素与元素列表中已有元素的比较,因此我们可以预期,如果我们“随机”添加元素,树将倾向于“平衡”。
432 |
433 | 但这并不是一定的。例如,如果我们从一个空集合开始,并按顺序连接数字 1 到 7,我们最终会得到一个高度不平衡的树,其中所有左侧子树都是空的,因此它与简单有序列表相比没有任何优势。解决此问题的一种方法是定义一个操作,将任意树转换为具有相同元素的平衡树。我们可以在每隔几次 `adjoin_set` 操作之后执行此转换,以保持集合的平衡。
434 |
435 | > 译者:可见 [浙江大学慕课:平衡二叉树](https://www.icourse163.org/learn/ZJU-93001?tid=360003#/learn/content?type=detail&id=702123&cid=748291)
436 |
437 | 交集和并集操作可以在线性时间内通过将树结构转换为有序列表和返回来执行。将留作练习。
438 |
439 | **Python 的集合实现方法**:Python 内置的 `set` 类型不使用任何这些表示。相反,Python 使用一种表示法,该表示法基于一种称为哈希(hashing)的技术提供恒定时间的成员关系测试和相邻操作,这是另一门课程的主题。内置的 Python 集合不能包含可变数据类型,如列表、字典或其他集合。为了允许嵌套集,Python 还有一个内置的不可变 `frozenset` 类,它与 `set` 类共享方法,但不包括改变方法和操作符。
440 |
--------------------------------------------------------------------------------
/sicp/3/1.md:
--------------------------------------------------------------------------------
1 | # 3.1 引言
2 |
3 | ::: details INFO
4 | 译者:[Tr4cck](https://github.com/Tr4cck),[Mancuoj](https://github.com/mancuoj)
5 |
6 | 来源:[3.1 Introduction](https://www.composingprograms.com/pages/31-introduction.html)
7 |
8 | 对应:Disc 10、Lab 10
9 | :::
10 |
11 | 第一章和第二章中描述了编程的两个基本要素:函数和数据之间的密切联系。我们知道了如何使用高阶函数将函数作为数据进行操作,如何使用消息传递和对象系统为数据定义行为。我们还研究了组织大型程序的技术,如函数抽象、数据抽象、类继承和泛型函数,这些核心概念为我们编写模块化、可维护和可扩展的程序打下了坚实的基础。
12 |
13 | 本章重点讨论编程的第三个基本要素:程序本身。Python 程序只是文本的集合,只有通过解释过程,我们才可以基于该文本执行有意义的计算。像 Python 这样的编程语言之所以很实用,是因为我们可以定义一个解释器,一个用于求解和执行 Python 程序的程序。毫不夸张地说,这就是编程中最基本的思想:解释器决定了编程语言中表达式的含义,但它只是另一个程序。
14 |
15 | 要理解这一点,就必须转变我们作为程序员的固有形象。我们要把自己视为语言的设计者,而不仅仅是别人设计的语言的使用者。
16 |
17 | ## 3.1.1 编程语言
18 |
19 | 各种程序设计语言在其语法结构、功能和应用领域方面有很大的不同。在通用编程语言中,函数定义和函数应用的结构是普遍存在的。但是,也有一些强大的语言不包括对象系统、高阶函数、赋值,甚至不包括控制结构,如 `while` 和 `for` 语句。我们将介绍 [Scheme 编程语言](),它是一门具有最小功能集的强大语言。本文介绍的 Scheme 子集不允许出现可变值。
20 |
21 | 在本章中,我们将研究解释器的设计以及它们在执行程序时产生的计算过程。为通用编程语言设计一个解释器可能令人望而生畏,毕竟,解释器是可以根据它们的输入执行任何可能的计算的程序。然而,许多解释器都有一个优雅的结构,即两个互递归函数:第一个函数求解环境中的表达式,第二个函数将函数应用于参数。
22 |
23 | 这些函数是递归的,因为它们是相互定义的:调用一个函数需要求解其函数体中的表达式,而求解一个表达式可能涉及调用一个或多个函数。
24 |
--------------------------------------------------------------------------------
/sicp/3/2.md:
--------------------------------------------------------------------------------
1 | # 3.2 函数式编程
2 |
3 | ::: details INFO
4 | 译者:[Mancuoj](https://github.com/mancuoj)
5 |
6 | 来源:[3.2 Functional Programming](https://www.composingprograms.com/pages/32-functional-programming.html)
7 |
8 | 对应:Disc 10、Lab 10
9 | :::
10 |
11 | 现代计算机上运行的软件是由各种编程语言编写的。其中,有些是物理级别的语言,例如专为特定计算机设计的机器语言。这种语言主要关注如何将数据和指令转化为计算机可以识别的二进制位。使用机器语言的程序员主要考虑如何最大限度地发挥硬件的性能,以高效执行计算任务。而高级语言则基于机器语言之上,它屏蔽了数据和程序的底层表示方式。这些高级语言提供了更多的组合和抽象手段,例如函数定义,使得大型软件系统的构建更为方便和直观。
12 |
13 | 在这个章节中,我们要介绍一种偏向函数式编程的高级语言。这种语言,实际上是 Scheme 语言的一个子集,其计算模型与 Python 很相似,但它特点是只使用表达式而不使用语句,特别适合符号计算,并且它处理的数据都是不可变的(immutable)。
14 |
15 | Scheme 是 [Lisp]() 的一个变种,而 Lisp 是继 [Fortran](http://en.wikipedia.org/wiki/Fortran) 之后仍然广受欢迎的第二古老的编程语言。Lisp 程序员社区几十年来持续蓬勃发展,[Clojure](http://en.wikipedia.org/wiki/Clojure) 等 Lisp 的新方言拥有现代编程语言中增长最快的开发人员社区。如果你想亲手试试本文中的例子,可以下载一个 [Scheme 的解释器](http://inst.eecs.berkeley.edu/~scheme/) 来操作。
16 |
17 | ## 3.2.1 表达式
18 |
19 | Scheme 程序主要是由各种表达式构成的,这些表达式可以是函数调用或一些特殊的结构。一个函数调用通常由一个操作符和其后面跟随的零个或多个操作数组成,这点和 Python 是相似的。不过在 Scheme 中,这些操作符和操作数都被放在一对括号里:
20 |
21 | ```scheme
22 | (quotient 10 2)
23 | ```
24 |
25 | Scheme 的语法一直采用前缀形式。也就是说,操作符像 `+` 和 `*` 都放在前面。函数调用可以互相嵌套,并且可能会写在多行上:
26 |
27 | ```scheme
28 | (+ (* 3 5) (- 10 6))
29 | ```
30 |
31 | ```scheme
32 | (+ (* 3
33 | (+ (* 2 4)
34 | (+ 3 5)))
35 | (+ (- 10 7)
36 | 6))
37 | ```
38 |
39 | 在 Scheme 中,表达式可以是基础类型,如数字,或是复合类型,如函数调用。数字字面量是基础类型,而函数调用则是可以包含任意子表达式一种复合形式。当求值一个函数调用时,Scheme 的处理方式和 Python 相似:首先对操作符和操作数进行求值,然后用操作符的结果(即函数)去处理操作数的结果(即参数)。
40 |
41 | 在 Scheme 里,`if` 表达式是一种特殊结构。尽管从语法上看,它似乎和常规的函数调用相似,但其求值方式却与众不同。一般来说,`if` 表达式的结构如下:
42 |
43 | ```scheme
44 | (if )
45 | ```
46 |
47 | 要理解 if 表达式是如何工作的,首先需要看它的 `` 部分,即条件判断。如果这个条件成立(即为真),解释器就会执行并返回 `` 的值;如果条件不成立,就会执行并返回 `` 的值。
48 |
49 | 我们可以用常见的比较操作符来比较数字,但在 Scheme 中,这些操作符仍然采用前缀的形式:
50 |
51 | ```scheme
52 | (>= 2 1)
53 | ```
54 |
55 | Scheme 中的布尔值有 `#t` 或者 `true` 代表真和 `#f` 或 `false` 代表假。你可以使用一些特定的布尔操作来组合它们,这些操作的执行逻辑和 Python 里的很相似。
56 |
57 | > - `(and ... )` 解释器会从左到右依次检查 `` 表达式。一旦有一个 `` 的结果是假,整个 `and` 表达式就直接返回假,并且剩下的 `` 表达式不再检查。只有当所有 `` 都是真时,`and` 表达式的返回值才是最后一个表达式的结果。
58 | > - `(or ... )` 解释器会从左到右依次检查 `` 表达式。一旦有一个 `` 的结果是真,`or` 表达式就直接返回那个真值,并且剩下的 `` 表达式不再检查。只有当所有 `` 都是假时,`or` 表达式才返回假。
59 | > - `(not )` 当 `` 表达式的结果是假时,`not` 表达式就返回真,否则返回假。
60 |
61 | ## 3.2.2 定义
62 |
63 | 你可以通过使用 `define` 这一特殊结构为某个值赋予一个名字:
64 |
65 | ```scheme
66 | (define pi 3.14)
67 | (* pi 2)
68 | ```
69 |
70 | 你可以用 `define` 的另一个版本来定义新的函数(在 Scheme 中,我们称之为“过程”)。例如,如果我们想定义一个求平方的函数,可以这样写:
71 |
72 | ```scheme
73 | (define (square x) (* x x))
74 | ```
75 |
76 | 定义一个过程(procedure)的标准格式如下:
77 |
78 | ```scheme
79 | (define ( ) )
80 | ```
81 |
82 | `` 是一个符号,用于表示与环境中的过程定义相关的内容。而 `` 是在过程主体内部用于指代过程对应参数的名称。在形式参数被替换为实际应用到过程的参数时,`` 将生成过程应用的值。 `` 和 `` 被括在括号内,就像实际调用所定义的过程一样。
83 |
84 | 一旦我们定义了 `square`,就可以在调用表达式中使用它了:
85 |
86 | ```scheme
87 | (square 21)
88 |
89 | (square (+ 2 5))
90 |
91 | (square (square 3))
92 | ```
93 |
94 | 用户自定义的函数可以接受多个参数,并且还可以包含特殊形式:
95 |
96 | ```scheme
97 | (define (average x y)
98 | (/ (+ x y) 2))
99 |
100 | (average 1 3)
101 | ```
102 |
103 | ```scheme
104 | (define (abs x)
105 | (if (< x 0)
106 | (- x)
107 | x))
108 | ```
109 |
110 | Scheme 支持与 Python 相同的词法作用域规则,允许进行局部定义。下面,我们使用嵌套定义和递归定义了一个用于计算平方根的迭代过程:
111 |
112 | ```scheme
113 | (define (sqrt x)
114 | (define (good-enough? guess)
115 | (< (abs (- (square guess) x)) 0.001))
116 | (define (improve guess)
117 | (average guess (/ x guess)))
118 | (define (sqrt-iter guess)
119 | (if (good-enough? guess)
120 | guess
121 | (sqrt-iter (improve guess))))
122 | (sqrt-iter 1.0))
123 | (sqrt 9)
124 | ```
125 |
126 | 匿名函数是通过 `lambda` 特殊形式创建的。`lambda` 用于创建过程,与 `define` 相似,但不需要为过程指定名称:
127 |
128 | ```scheme
129 | (lambda () )
130 | ```
131 |
132 | 生成的过程与使用 `define` 创建的过程一样,唯一的区别是它没有在环境中关联任何名称。实际上,以下表达式是等效的:
133 |
134 | ```scheme
135 | (define (plus4 x) (+ x 4))
136 | (define plus4 (lambda (x) (+ x 4)))
137 | ```
138 |
139 | 和任何返回过程的表达式一样,lambda 表达式可以在调用表达式中用作运算符:
140 |
141 | ```scheme
142 | ((lambda (x y z) (+ x y (square z))) 1 2 3)
143 | ```
144 |
145 | ## 3.2.3 复合类型
146 |
147 | 在 Scheme 语言中,`pair` 是内置的数据结构。出于历史原因,`pair` 是通过内置函数 `cons` 创建的,而元素则可以通过 `car` 和 `cdr` 进行访问:
148 |
149 | ```scheme
150 | (define x (cons 1 2))
151 |
152 | x
153 |
154 | (car x)
155 |
156 | (cdr x)
157 | ```
158 |
159 | Scheme 语言中也内置了递归列表,它们使用 `pair` 来构建。特殊的值 `nil` 或 `'()` 表示空列表。递归列表的值是通过将其元素放在括号内,用空格分隔开来表示的:
160 |
161 | ```scheme
162 | (cons 1
163 | (cons 2
164 | (cons 3
165 | (cons 4 nil))))
166 |
167 | (list 1 2 3 4)
168 |
169 | (define one-through-four (list 1 2 3 4))
170 |
171 | (car one-through-four)
172 |
173 | (cdr one-through-four)
174 |
175 | (car (cdr one-through-four))
176 |
177 | (cons 10 one-through-four)
178 |
179 | (cons 5 one-through-four)
180 | ```
181 |
182 | 要确定一个列表是否为空,可以使用内置的 `null?` 谓词。借助它,我们可以定义用于计算长度和选择元素的标准序列操作:
183 |
184 | ```scheme
185 | (define (length items)
186 | (if (null? items)
187 | 0
188 | (+ 1 (length (cdr items)))))
189 | (define (getitem items n)
190 | (if (= n 0)
191 | (car items)
192 | (getitem (cdr items) (- n 1))))
193 | (define squares (list 1 4 9 16 25))
194 |
195 | (length squares)
196 |
197 | (getitem squares 3)
198 | ```
199 |
200 | ## 3.2.4 符号数据
201 |
202 | 迄今为止,我们使用的所有复合数据对象最终都是由数字构建的。Scheme 的一个强大之处在于能够处理任意符号作为数据。
203 |
204 | 为了操作符号,我们需要语言中的一个新元素:引用数据对象的能力。假设我们想构建列表 `(a b)`。我们不能通过 `(list a b)` 来实现这一目标,因为这个表达式构建的是 `a` 和 `b` 的值,而不是它们自身的符号。在 Scheme 中,我们通过在它们前面加上一个单引号来引用符号 `a` 和 `b` 而不是它们的值:
205 |
206 | ```scheme
207 | (define a 1)
208 | (define b 2)
209 |
210 | (list a b)
211 |
212 | (list 'a 'b)
213 |
214 | (list 'a b)
215 | ```
216 |
217 | 在 Scheme 中,任何不被求值的表达式都被称为被引用。这种引用的概念源自一个经典的哲学区分,即一种事物(比如一只狗)会四处奔跑和吠叫,而“狗”这个词是一种语言构造,用来指代这种事物。当我们在引号中使用“狗”时,我们并不是指代某只特定的狗,而是指代一个词语。在语言中,引号允许我们讨论语言本身,而在 Scheme 中也是如此:
218 |
219 | ```scheme
220 | (list 'define 'list)
221 | ```
222 |
223 | 引用还使我们能够输入复合对象,使用传统的打印表示方式来表示列表:
224 |
225 | ```scheme
226 | (car '(a b c))
227 |
228 | (cdr '(a b c))
229 | ```
230 |
231 | 完整的 Scheme 语言包括更多功能,如变异操作(mutation operations)、向量(vectors)和映射(maps)。然而,到目前为止,我们介绍的这个子集已经提供了一个强大的函数式编程语言,可以实现我们在本文中讨论过的许多概念。
232 |
233 | ## 3.2.5 海龟图形
234 |
235 | 在这篇文章的 Scheme 版本中,加入了一个称为 Turtle 的图形功能(通常叫海龟),它源自 Logo 语言(Lisp 的另一种方言)中的一部分。这个“海龟”从一个画布的中心出发,根据特定的程序命令来移动和转向,并在它移动的过程中留下轨迹。尽管海龟图形最初是设计来吸引孩子学习编程的,但实际上,它对于高级程序员来说也是一个非常有趣的图形化编程工具。
236 |
237 | 当 Scheme 程序运行时,这只“海龟”会在画布上有一个特定的位置和方向。例如,使用 `forward` 和 `right` 这样的命令,可以改变“海龟”的位置和方向。为了方便,一些常用的程序命令有缩写形式,比如 `forward` 可以简写为 `fd`。在 Scheme 中,`begin` 这个特殊结构允许我们在一个表达式中串联多个子命令,这在执行多个操作时非常有用:
238 |
239 | ```scheme
240 | > (define (repeat k fn) (if (> k 0)
241 | (begin (fn) (repeat (- k 1) fn))
242 | nil))
243 | > (repeat 5
244 | (lambda () (fd 100)
245 | (repeat 5
246 | (lambda () (fd 20) (rt 144)))
247 | (rt 144)))
248 | nil
249 | ```
250 |
251 | 
252 |
253 | Python 也内置了完整的 Turtle 功能,作为一个 [turtle 库模块](http://docs.python.org/py3k/library/turtle.html)。
254 |
255 | 再举一个例子,Scheme 可以用其 Turtle 图形功能以非常简洁的方式来展现递归的图形。谢尔宾斯基三角形是一种分形结构,它将大的三角形分解成三个小的三角形,小三角形的顶点位于大三角形边的中点。我们可以用下面的 Scheme 程序来实现这个到一定递归深度的绘制:
256 |
257 | ```
258 | > (define (repeat k fn)
259 | (if (> k 0)
260 | (begin (fn) (repeat (- k 1) fn))
261 | nil))
262 |
263 | > (define (tri fn)
264 | (repeat 3 (lambda () (fn) (lt 120))))
265 |
266 | > (define (sier d k)
267 | (tri (lambda ()
268 | (if (= k 1) (fd d) (leg d k)))))
269 |
270 | > (define (leg d k)
271 | (sier (/ d 2) (- k 1))
272 | (penup)
273 | (fd d)
274 | (pendown))
275 | ```
276 |
277 | `triangle` 函数是一个通用方法,它可以将某个绘图操作重复三次,每次操作后,都会让乌龟左转。`sier` 函数接受两个参数:一是长度 `d`,另一个是递归的深度 `k`。如果深度为 1,它就画一个简单的三角形;否则,它会调用 `leg` 函数来构建一个由三个小三角形组成的大三角形。`leg` 函数的功能是画一个谢尔宾斯基三角形的边,它首先递归调用 `sier` 函数来画出边的上半部分,然后移动乌龟到下一个顶点。而 `penup` 和 `pendown` 函数的作用是控制乌龟的画笔,使其在移动时可以选择是否画线。通过 `sier` 和 `leg` 的相互递归调用,我们可以得到以下效果:
278 |
279 | ```scheme
280 | > (sier 400 6)
281 | ```
282 |
283 | 
284 |
--------------------------------------------------------------------------------
/sicp/3/3.md:
--------------------------------------------------------------------------------
1 | # 3.3 异常
2 |
3 | ::: details INFO
4 | 译者:[Bryan Zhang](https://github.com/billycrapediem)
5 |
6 | 来源:[3.3 Exceptions](https://www.composingprograms.com/pages/33-exceptions.html)
7 |
8 | 对应:无
9 | :::
10 |
11 | 程序员必须时刻注意程序中可能出现的错误。举例来说:一个函数没有收到它所预期的参数,必要的信息可能缺失,或者网络连接可能中断。在设计程序时,必须预见可能发生的异常,并采取措施。
12 |
13 | 处理程序中的错误没有唯一正确的方法。比如,对于用于提供持续服务的程序(如网络服务器),他们需要具备鲁棒性,将错误日志记录下来以供作考虑,同时尽可能继续为新的请求提供服务。另一方面,Python 解释器处理错误时会立即终止程序并打印错误消息,这样程序员可以及时解决问题。无论哪种情况,程序员都必须在如何处理异常时做出明智的选择。
14 |
15 | **异常**(Exceptions)是本节的主题。在程序当中,异常通过添加错误处理的逻辑提供了一种通用的机制。抛出异常(raising an exception)是一种中断程序正常执行流程的技术,它表示发生一些异常情况,并直接返回到程序中预定处理该情况的部分。Python 解释器在检测到表达式或语句中出现错误时会抛出异常。用户也可以使用 `raise` 和 `assert` 语句来抛出异常。
16 |
17 | **抛出异常**:异常是一个对象实例,其类(class)直接或间接继承自 `BaseException` 类。在第一章中引入的 `assert` 语句会抛出一个类为 `AssertionError` 的异常。通常情况下,可以使用 `raise` 语句来抛出任何异常实例。[Python 文档](https://www.composingprograms.com/pages/33-exceptions.html) 中描述了 `raise` 语句的一般形式。最常见的用法是构造一个异常实例并将其抛出。
18 |
19 | ```python
20 | >>> raise Exception(' An error occurred')
21 | Traceback (most recent call last):
22 | File "", line 1, in
23 | Exception: an error occurred
24 | ```
25 |
26 | 当抛出异常时,当前代码块中的后续语句将不会执行。除非异常被处理(如下所述),否则解释器将直接返回到交互式的读取 - 求值 - 打印(read-eval-print-loop)循环,或者在 Python 是通过文件参数启动时会完全终止。此外,解释器将打印一个堆栈回溯(stack backtrace),它是一个结构化的文本块,描述了在异常被抛出的执行分支中活动的嵌套函数调用集合。在上述示例中,文件名 `` 表示该异常是由用户在交互会话中引发的,而不是来自文件中的代码。
27 |
28 | **处理异常**(handling exceptions)。异常可以由封闭的 `try` 语句来处理。`try` 语句由多个子句组成;第一个以 `try` 开头,其余的以 `except` 开头:
29 |
30 | ```python
31 | try:
32 |
33 | except as :
34 |
35 | ```
36 |
37 | 在执行 try 语句时,`` 总是立即执行。只有在执行 `` 过程中发生异常时,except 子句的内容才会执行。每个 except 子句指定了要处理的特定异常类。例如,如果 `` 是 `AssertionError`,那么在执行 `` 过程中引发的任何继承自 `AssertionError` 类的实例都将由随后的 `` 处理。在 `` 内部,标识符 `` 绑定到被引发的异常对象,但此绑定不会在 `` 之外存在。
38 |
39 | 例如,我们可以使用 `try` 语句处理 `ZeroDivisionError` 异常,当异常被引发时,将名称 `x` 绑定到 $0$。
40 |
41 | ```python
42 | >>> try:
43 | x = 1 / 0:
44 | except ZeroDivisionError as e:
45 | print('handling a', type(e))
46 | x = 0
47 | handling a
48 | >>> x
49 | 0
50 | ```
51 |
52 | `try` 语句将处理 `` 中发生的异常(包括 应用在 `` 的函数,无论是直接还是间接应用)。当引发异常时,控制权会直接跳转到处理该类型异常的最近一次 `try` 语句的 `` 中。
53 |
54 | ```python
55 | >>> def invert(x):
56 | result = 1/x # 抛出一个异常(ZeroDivisionError) 如果 x 为 0
57 | print('Never printed if x is 0')
58 | return result
59 | >>> def invert_safe(x):
60 | try:
61 | return invert(x)
62 | except ZeroDivisionError as e:
63 | return str(e)
64 |
65 | >>> invert_safe(2)
66 | Never printed if x is 0
67 | 0.5
68 | >>> invert_safe(0)
69 | 'division by zero'
70 | ```
71 |
72 | 这个例子说明了在 `invert` 中的 `print` 表达式永远不会被评估,而是转移到 `invert_safe` 的 `except` 子句的内容中。将 `ZeroDivisionError` `e` 强制转换为字符串会得到可解释字符串:“division by zero”。
73 |
74 | ## 3.3.1 异常对象(Exception Object)
75 |
76 | 异常对象本身可以具有属性,例如在 `assert` 语句中的错误消息,以及关于异常在执行过程中被引发的位置的信息。用户自定义的异常类可以具有额外的属性(attributes)。
77 |
78 | 在第一章中,我们实现了牛顿法来寻找任意函数的零点。下面的示例定义了一个异常类,每当出现数值错误(ValueError)时,它返回在迭代改进过程中发现的最佳猜测。当将 `sqrt` 函数应用于负数时,会引发数学域错误(ValueError 的一种类型)。通过引发一个 `IterImproveError` 来处理此异常,并将牛顿法中最近的猜测存储为属性。
79 |
80 | 首先,我们定义一个新的类,它继承自 `Exception` 。
81 |
82 | ```python
83 | >>> class IterImproveError(Exception):
84 | def __init__(self, last_guess):
85 | self.last_guess = last_guess
86 | ```
87 |
88 | 接下来,我们定义 `improve` 方法,这是我们通用的迭代改进算法。这个版本通过引发一个 `IterImproveError` 来处理任何数值异常(ValueError),并将最近的猜测存储起来。与之前一样, `improve` 接受两个函数作为参数,每个函数都接受一个数值参数。`update` 函数返回新的猜测,而 `done` 函数返回一个布尔值,表示更新的值是否正确。
89 |
90 | ```python
91 | >>> def improve(update, done, guess=1, max_updates=1000):
92 | k = 0
93 | try:
94 | while not done(guess) and k < max_updates:
95 | guess = update(guess)
96 | k = k + 1
97 | return guess
98 | except ValueError:
99 | raise IterImproveError(guess)
100 | ```
101 |
102 | 最后,我们定义 `find_zero` 函数,它用于返回 `improve` 函数的结果(`update` 函数为 `newton_update`)。牛顿更新函数(newton_update)在第一章中定义,并且对于这个例子不需要进行任何更改。这个版本的 `find_zero1` 通过返回最后的猜测来处理 `IterImproveError` 异常。
103 |
104 | ```python
105 | >>> def find_zero(f, guess=1):
106 | def done(x):
107 | return f(x) == 0
108 | try:
109 | return improve(newton_update(f), done, guess)
110 | except IterImproveError as e:
111 | return e.last_guess
112 | ```
113 |
114 | 考虑通过 `find_zero` 函数来寻找 $2x^{2}+\sqrt{ x }$ 的零点。该函数在 $0$ 处有一个零点,但在任何负数上评估它都会引发 `ValueError` 异常。我们第一章中实现的牛顿法将引发该错误,并且无法返回任何零点的猜测。而我们修订后的实现会在错误发生前返回最后的猜测。
115 |
116 | ```python
117 | >>> from math import sqrt
118 | >>> find_zero(lambda x: 2*x*x + sqrt(x))
119 | -0.030211203830201594
120 | ```
121 |
122 | 尽管这个近似值仍然远离正确的答案 $0$,但某些应用程序更倾向于粗略的近似值,而不是数值异常(ValueError)。
123 |
124 | 异常是另一种帮助我们将程序的关注点模块化的技术。在这个例子中,Python 的异常机制允许我们将迭代改进的逻辑(在 try 子句中内容中保持不变)与处理错误的逻辑(在 except 子句中出现)分开。我们还会发现,在 Python 中实现解释器时,异常也是一个有用的功能。
125 |
--------------------------------------------------------------------------------
/sicp/3/4.md:
--------------------------------------------------------------------------------
1 | # 3.4 组合语言的解释器
2 |
3 | ::: details INFO
4 | 译者: [silver](https://github.com/silver-ymz)
5 |
6 | 来源:[3.4 Interpreters for Languages with Combination](https://www.composingprograms.com/pages/34-interpreters-for-languages-with-combination.html)
7 |
8 | 对应:HW 06
9 | :::
10 |
11 | 我们现在开始一场技术之旅,通过这种技术,编程语言可以建立在其它语言之上。元语言抽象——建立新的语言——其在工程设计的各个分支中都发挥着重要作用。这对计算机编程尤为重要,因为在编程中,我们不仅可以制定新的语言,还可以通过构造解释器来实现这些语言。编程语言的解释器是一种函数,当应用于该语言的表达式时,它执行计算该表达式所需的操作。
12 |
13 | 我们将首先定义一种语言的解释器,它是 Scheme 的一个有限子集,称为计算器语言。然后,我们将为 Scheme 整体开发一个简略的解释器。我们创建的解释器将是完整的,因为它允许我们用 Scheme 编写完全通用的程序。为此,它将实现我们在第 1 章为 Python 程序引入的相同的运算模型。
14 |
15 | 由于本节中的许多示例过于复杂,无法自然融入本文的格式中。因此,它们都将包含在配套的 [Scheme-Syntax Calculator](https://www.composingprograms.com/examples/scalc/scalc.html) 网站中。
16 |
17 | ## 3.4.1 基于 Scheme 语法的计算器
18 |
19 | 基于 Scheme 语法的计算器(或简称计算器语言)是一种用于加法、减法、乘法和除法运算的表达式语言。计算器语言使用 Scheme 的调用表达式语法和运算符行为。加法($+$)和乘法($*$)运算都可以传入任意数量的参数:
20 |
21 | ```scheme
22 | > (+ 1 2 3 4)
23 | 10
24 | > (+)
25 | 0
26 | > (* 1 2 3 4)
27 | 24
28 | > (*)
29 | 1
30 | ```
31 |
32 | 减法($-$)具有两种行为。只传入一个参数时,它会对该值取相反数。传入至少两个参数时,它会用第一个参数减去之后的所有参数。除法($/$)也有类似的两种行为:计算单个参数的倒数,或用第一个参数除以之后的所有参数:
33 |
34 | ```scheme
35 | > (- 10 1 2 3)
36 | 4
37 | > (- 3)
38 | -3
39 | > (/ 15 12)
40 | 1.25
41 | > (/ 30 5 2)
42 | 3
43 | > (/ 10)
44 | 0.1
45 | ```
46 |
47 | 一个调用表达式的求值过程是先对所有子表达式求值,然后将运算符应用于所得结果:
48 |
49 | ```scheme
50 | > (- 100 (* 7 (+ 8 (/ -12 -3))))
51 | 16.0
52 | ```
53 |
54 | 我们将在 Python 中实现计算器语言的解释器。也就是说,我们将编写一个 Python 程序,输入字符串,并返回该字符串作为计算器语言表达式的求值结果。如果计算器语言表达式不完整,我们的解释器将抛出相应的异常。
55 |
56 | ## 3.4.2 表达式树
57 |
58 | 到目前为止,我们在描述评估过程时引入的表达式树,还只是一个概念。我们从未显式将表达式树表示程序中的数据。为了编写解释器,我们必须将表达式作为数据进行操作。
59 |
60 | 计算器语言中的基元表达式只是数字或字符串:可能是 int,float 或运算符。所有组合表达式都是调用表达式。调用表达式是一个 Scheme 列表,第一个元素(运算符)后面有零个或多个运算表达式。
61 |
62 | **Scheme 对**:在 Scheme 中,列表一定是嵌套对,但并非所有对都是列表。为了在 Python 中表示 Scheme 对和列表,我们将定义一个类似于本章前面的 `Rlist` 类的 `Pair` 类。实现代码可以在 [scheme_reader](https://www.composingprograms.com/examples/scalc/scheme_reader.py.html) 查看。
63 |
64 | 空列表由一个名为 `nil` 的对象表示,它是类 `nil` 的一个实例。我们假设运行时只有一个 `nil` 实例被创建。
65 |
66 | `Pair` 类和 `nil` 对象是用 Python 表示的 Scheme 值。它们有代表 Python 表达式的 `repr` 字符串和代表 Scheme 表达式的 `str` 字符串。
67 |
68 | ```py
69 | >>> s = Pair(1, Pair(2, nil))
70 | >>> s
71 | Pair(1, Pair(2, nil))
72 | >>> print(s)
73 | (1 2)
74 | ```
75 |
76 | 它们实现了长度和元素选择的基本 Python 接口,以及返回 Scheme 列表的 `map` 方法。
77 |
78 | ```py
79 | >>> len(s)
80 | 2
81 | >>> s[1]
82 | 2
83 | >>> print(s.map(lambda x: x+4))
84 | (5 6)
85 | ```
86 |
87 | **嵌套列表**。嵌套对可以表示列表,并且列表的元素本身也可以是列表。因此,嵌套对足以代表 Scheme 表达式,而后者实际上就是嵌套列表。
88 |
89 | ```py
90 | >>> expr = Pair('+', Pair(Pair('*', Pair(3, Pair(4, nil))), Pair(5, nil)))
91 | >>> print(expr)
92 | (+ (* 3 4) 5)
93 | >>> print(expr.second.first)
94 | (* 3 4)
95 | >>> expr.second.first.second.first
96 | 3
97 | ```
98 |
99 | 本例说明所有计算器语言表达式都是嵌套的 Scheme 列表。我们的计算器语言解释器将读入嵌套的 Scheme 列表,将其转换为表示为 `Pair` 实例的表达式树(参见下面的解析表达式章节),然后对表达式树进行求值(参见下面的计算器语言求值章节)。
100 |
101 | ## 3.4.3 解析表达式
102 |
103 | 解析是根据原始文本输入生成表达式树的过程。解析器由两个组件组成:词法分析器(lexical analyzer)和语法分析器(syntactic analyzer)。首先,词法分析器将输入字符串划分为 标记(token)。标记表示语言的最小语法单元,比如名称和符号。然后,语法分析器根据这个标记序列构建一个表达式树。词法分析器生成的标记序列就被语法分析器所消耗。
104 |
105 | **词法分析**。将字符串解释为标记序列的组件称为分词器(tokenizer)或词法分析器。在我们的实现中,分词器是 [scheme_tokens](https://www.composingprograms.com/examples/scalc/scheme_tokens.py.html) 中的函数 `tokenize_line`。Scheme 标记被空格、括号、点或单引号所分隔。分隔符同运算符和数字一样,也是标记。分词器会逐个字符地解析一行,并检验运算符和数字的格式。
106 |
107 | 对格式良好的计算器语言表达式进行分词,不仅可以分离所有运算符和分隔符,还可以识别多字符数字(例如 2.3),并将其转换为数字类型。
108 |
109 | ```py
110 | >>> tokenize_line('(+ 1 (* 2.3 45))')
111 | ['(', '+', 1, '(', '*', 2.3, 45, ')', ')']
112 | ```
113 |
114 | 词法分析是一个迭代过程,可以单独作用于输入程序的每一行。
115 |
116 | **语法分析**。将标记序列解析为表达式树的组件称为语法分析器。语法分析是一个树递归过程,它必须考虑可能跨越多行的整个表达式。
117 |
118 | 语法分析由 [scheme_reader](https://www.composingprograms.com/examples/scalc/scheme_reader.py.html) 中的 `scheme_read` 函数实现。它是树递归的,因为分析一个标记序列往往涉及分析这些标记的子表达式,而子表达式本身又可能是更大的表达式树的一个分支(比如操作数)。递归产生了运算器所使用的层次结构。
119 |
120 | `scheme_read` 函数希望它的输入 `src` 是一个 `Buffer` 实例,其可以访问一系列标记。`Buffer` 在 [buffer](https://www.composingprograms.com/examples/scalc/buffer.py.html) 模块中定义,它将跨多行的标记收集到一个可以进行语法分析的对象中。
121 |
122 | ```py
123 | >>> lines = ['(+ 1', ' (* 2.3 45))']
124 | >>> expression = scheme_read(Buffer(tokenize_lines(lines)))
125 | >>> expression
126 | Pair('+', Pair(1, Pair(Pair('*', Pair(2.3, Pair(45, nil))), nil)))
127 | >>> print(expression)
128 | (+ 1 (* 2.3 45))
129 | ```
130 |
131 | `scheme_read` 函数首先检查各种基本情况,包括空输入(这会引发文件结束异常,在 Python 中称为 `EOFError` )和基元表达式。每当 `(` 标记在列表的开头时,就会调用 `read_tail` 来递归解析。
132 |
133 | `scheme_read` 的实现可以读取格式良好的 `Scheme` 列表,而这正是计算器语言所需要的。关于点列表和引号形式的解析留作练习。
134 |
135 | 信息丰富的语法错误可以极大地提高解释器的可用性。由此引发的 `SyntaxError` 异常包含对所遇问题的描述。
136 |
137 | ## 3.4.4 计算器语言求值
138 |
139 | [scalc](https://www.composingprograms.com/examples/scalc/scalc.py.html) 模块为计算器语言实现了一个求值器。`calc_eval` 函数将表达式作为参数并返回其值。辅助函数 `simplify`、`reduce` 和 `as_scheme_list` 的定义同样出现在模型中,并将在下文中使用。对于计算器语言,表达式仅有的两种合法语法形式是数字和调用表达式,它们都是 `Pair` 实例,并表示格式良好的 `Scheme` 列表。数字是自求值的,可以直接从 `calc_eval` 返回。调用表达式则需要调用函数。
140 |
141 | ```py
142 | >>> def calc_eval(exp):
143 | """Evaluate a Calculator expression."""
144 | if type(exp) in (int, float):
145 | return simplify(exp)
146 | elif isinstance(exp, Pair):
147 | arguments = exp.second.map(calc_eval)
148 | return simplify(calc_apply(exp.first, arguments))
149 | else:
150 | raise TypeError(exp + ' is not a number or call expression')
151 | ```
152 |
153 | 调用表达式的求值方法是,首先将 `calc_eval` 递归地作用在操作数列表上,从而计算出参数列表。然后,调用另一个函数 `calc_apply` 来将运算符作用于这些参数。
154 |
155 | ```py
156 | >>> def calc_apply(operator, args):
157 | """Apply the named operator to a list of args."""
158 | if not isinstance(operator, str):
159 | raise TypeError(str(operator) + ' is not a symbol')
160 | if operator == '+':
161 | return reduce(add, args, 0)
162 | elif operator == '-':
163 | if len(args) == 0:
164 | raise TypeError(operator + ' requires at least 1 argument')
165 | elif len(args) == 1:
166 | return -args.first
167 | else:
168 | return reduce(sub, args.second, args.first)
169 | elif operator == '*':
170 | return reduce(mul, args, 1)
171 | elif operator == '/':
172 | if len(args) == 0:
173 | raise TypeError(operator + ' requires at least 1 argument')
174 | elif len(args) == 1:
175 | return 1/args.first
176 | else:
177 | return reduce(truediv, args.second, args.first)
178 | else:
179 | raise TypeError(operator + ' is an unknown operator')
180 | ```
181 |
182 | 上面,每个组件都计算不同运算符的结果,或者在传入错误数量的参数时引发适当的 `TypeError`。`calc_apply` 函数可以直接调用,但必须传入值列表作为参数而不是操作数表达式列表。
183 |
184 | ```py
185 | >>> calc_apply('+', as_scheme_list(1, 2, 3))
186 | 6
187 | >>> calc_apply('-', as_scheme_list(10, 1, 2, 3))
188 | 4
189 | >>> calc_apply('*', nil)
190 | 1
191 | >>> calc_apply('*', as_scheme_list(1, 2, 3, 4, 5))
192 | 120
193 | >>> calc_apply('/', as_scheme_list(40, 5))
194 | 8.0
195 | ```
196 |
197 | `calc_eval` 的作用是通过首先计算操作数子表达式的值,然后将它们作为参数传递给 `calc_apply`,从而对 `calc_apply` 进行正确的调用。因此,`calc_eval` 可以接受嵌套表达式。
198 |
199 | ```py
200 | >>> print(exp)
201 | (+ (* 3 4) 5)
202 | >>> calc_eval(exp)
203 | 17
204 | ```
205 |
206 | `calc_eval` 的结构是对类型(表达式的形式)进行调度的一个示例。表达式的第一种形式是数字,它不需要额外的求值步骤。通常,不需要额外求值步骤的基元表达式称为自求值。在我们的计算器语言中,唯一的自求值表达式是数字,但通用编程语言也可能包括字符串、布尔值等。
207 |
208 | **读取 - 求值 - 打印循环**:与解释器交互的一种典型方式是读取 - 求值 - 打印循环(Read-eval-print loops),或称 REPL。这是一种读取表达式、求值并为用户打印结果的交互模式。Python 交互会话就是这种循环的一个例子。
209 |
210 | REPL 的实现可以在很大程度上独立于它所使用的解释器。下面的函数 `read_eval_print_loop` 缓冲用户输入,使用语言特定的 `scheme_read` 函数构造表达式,然后打印对该表达式应用 `calc_eval` 的结果。
211 |
212 | ```py
213 | >>> def read_eval_print_loop():
214 | """Run a read-eval-print loop for calculator."""
215 | while True:
216 | src = buffer_input()
217 | while src.more_on_line:
218 | expression = scheme_read(src)
219 | print(calc_eval(expression))
220 | ```
221 |
222 | 该版本的 `read_eval_print_loop` 包含交互式界面的所有基本组件。会话示例如下:
223 |
224 | ```scheme
225 | > (* 1 2 3)
226 | 6
227 | > (+)
228 | 0
229 | > (+ 2 (/ 4 8))
230 | 2.5
231 | > (+ 2 2) (* 3 3)
232 | 4
233 | 9
234 | > (+ 1
235 | (- 23)
236 | (* 4 2.5))
237 | -12
238 | ```
239 |
240 | 该循环实现没有终止或错误处理机制。我们可以通过向用户报告错误来改进界面。我们还可以让用户通过键盘中断信号(UNIX 上为 Control-C)或文件结束异常(UNIX 上为 Control-D)来退出循环。为了实现这些改进,我们将原来的 `while` 语句放在 `try` 语句中。第一个 `except` 子句处理 `scheme_read` 引发的语法错误(`SyntaxError`)和值错误(`ValueError`)异常,以及 `calc_eval` 引发的类型错误(`TypeError`)和除零错误(`ZeroDivisionError`)异常。
241 |
242 | ```py
243 | >>> def read_eval_print_loop():
244 | """Run a read-eval-print loop for calculator."""
245 | while True:
246 | try:
247 | src = buffer_input()
248 | while src.more_on_line:
249 | expression = scheme_read(src)
250 | print(calc_eval(expression))
251 | except (SyntaxError, TypeError, ValueError, ZeroDivisionError) as err:
252 | print(type(err).__name__ + ':', err)
253 | except (KeyboardInterrupt, EOFError): # -D, etc.
254 | print('Calculation completed.')
255 | return
256 | ```
257 |
258 | 这种循环执行方式会在不退出循环的情况下报告错误。用户可以在收到错误信息后重新启动循环,而不是在出错时退出程序,从而修改自己的表达式。在导入 `readline` 模块后,用户甚至可以使用向上箭头或 Control-P 回顾之前的输入。最终的结果是提供了一个信息丰富的错误报告界面:
259 |
260 | ```scheme
261 | > )
262 | SyntaxError: unexpected token: )
263 | > 2.3.4
264 | ValueError: invalid numeral: 2.3.4
265 | > +
266 | TypeError: + is not a number or call expression
267 | > (/ 5)
268 | TypeError: / requires exactly 2 arguments
269 | > (/ 1 0)
270 | ZeroDivisionError: division by zero
271 | ```
272 |
273 | 当我们将解释器推广到计算器语言以外的新语言时,我们将看到 `read_eval_print_loop` 是由解析函数、求值函数和 try 语句处理的异常类型参数化的。除了这些变化,所有 REPL 都可以使用相同的结构来实现。
274 |
--------------------------------------------------------------------------------
/sicp/3/5.md:
--------------------------------------------------------------------------------
1 | # 3.5 抽象语言的解释器
2 |
3 | ::: details INFO
4 | 译者:[silver](https://github.com/silver-ymz)
5 |
6 | 来源:[3.5 Interpreters for Languages with Abstraction](https://www.composingprograms.com/pages/35-interpreters-for-languages-with-abstraction.html)
7 |
8 | 对应:Disc 11、Disc 12、Disc 13、Disc 14、HW 07、HW 08、HW 09、Lab 11、Lab 12、Lab 13、Lab 14、Scheme、Scheme Challenge、Scheme Contest
9 | :::
10 |
11 | 计算器语言提供了一种方法,来组合嵌套的调用表达式。但是,它无法定义新的运算符、为值命名或表达通用的计算方法。计算器语言不支持任何方式的抽象。因此,它不是一种特别强大或通用的编程语言。现在,我们的任务是定义一种通用编程语言,通过将名称绑定到数值和定义新操作来支持抽象。
12 |
13 | 上一章以 Python 源代码的形式提供了一个完整的解释器。与此不同,本章将采用描述性的方法。配套项目要求你构建一个功能齐全的 Scheme 解释器来实现这里提出的想法。
14 |
15 | ## 3.5.1 结构
16 |
17 | 本节介绍 Scheme 解释器的一般结构。完成该项目即可产生本文所述解释器的可用实现。
18 |
19 | Scheme 解释器与计算器语言解释器的结构基本相同。解析器生成表达式,表达式由求值函数解释。求值函数检查表达式的形式,对于调用表达式,它调用一个函数对某些参数进行应用。求值器的大部分差异都与特殊形式、用户自定义函数和计算环境模型的实现相关。
20 |
21 | **解析**:计算器语言解释器中的 [scheme_reader](https://www.composingprograms.com/examples/scalc/scheme_reader.py.html) 和 [scheme_tokens](https://www.composingprograms.com/examples/scalc/scheme_tokens.py.html) 模块几乎足以解析任何有效的 Scheme 表达式。不过,它还不支持引号或点列表。完整的 Scheme 解释器应该能够解析以下输入表达式。
22 |
23 | ```py
24 | >>> read_line("(car '(1 . 2))")
25 | Pair('car', Pair(Pair('quote', Pair(Pair(1, 2), nil)), nil))
26 | ```
27 |
28 | 实现 Scheme 解释器的第一个任务是扩展 [scheme_reader](https://www.composingprograms.com/examples/scalc/scheme_reader.py.html) 以正确解析点列表和引号。
29 |
30 | **求值**(Evaluation)。Scheme 一次计算一个表达式。求值器的框架实现在配套项目的 `scheme.py` 中定义。`scheme_read` 返回的每个表达式都传递给 `scheme_eval` 函数,该函数计算当前环境 `env` 中的表达式 `expr`。
31 |
32 | `scheme_eval` 函数用于对 Scheme 中不同形式的表达式进行求值,包括基元、特殊形式和调用表达式。在 Scheme 中,组合形式可以通过检查其第一个元素来确定。每种特殊形式都有自己的求值规则。下面是 `scheme_eval` 的简化实现。为了便于讨论,我们删除了一些错误检查和特殊形式处理。完整的实现见配套项目。
33 |
34 | ```py
35 | >>> def scheme_eval(expr, env):
36 | """Evaluate Scheme expression expr in environment env."""
37 | if scheme_symbolp(expr):
38 | return env[expr]
39 | elif scheme_atomp(expr):
40 | return expr
41 | first, rest = expr.first, expr.second
42 | if first == "lambda":
43 | return do_lambda_form(rest, env)
44 | elif first == "define":
45 | do_define_form(rest, env)
46 | return None
47 | else:
48 | procedure = scheme_eval(first, env)
49 | args = rest.map(lambda operand: scheme_eval(operand, env))
50 | return scheme_apply(procedure, args, env)
51 | ```
52 |
53 | **函数应用**(Procedure application)。上面的最后一种情况调用了第二个函数,即由函数 `scheme_apply` 实现的函数应用。与计算器语言中的 `calc_apply` 函数相比,Scheme 中的函数应用流程要通用得多。它作用于两种参数:`PrimitiveProcedure` 或 `LambdaProcedure`。`PrimitiveProcedure` 是由 Python 实现的;它包含一个绑定到 Python 函数的实例属性 `fn`。此外,它可能需要也可能不需要访问当前环境。每当应用该函数时,都会调用该 Python 函数。
54 |
55 | `LambdaProcedure` 是用 Scheme 实现的。它有一个 `body` 属性,该属性是一个 Scheme 表达式,每当该函数被应用时都会对其进行求值。要将函数应用于参数列表,需要在一个新环境中对主体表达式进行求值。为了构建这个环境,需要在环境中添加一个新的帧。在这帧中,该函数的形式参数将与实际参数相绑定。然后,主体表达式将使用 `scheme_eval` 进行求值。
56 |
57 | **求值/应用递归**。实现求值整个流程的函数 `scheme_eval` 和 `scheme_apply` 是相互递归的。每当遇到调用表达式时,求值函数都将调用应用函数。而应用函数使用求值函数,将操作数表达式求值为参数,或者对用户定义的函数进行求值。这种相互递归的结构在解释器中非常普遍:求值通过应用来定义,应用又通过求值来定义。
58 |
59 | 这种递归循环以语言基元结束。求值函数有一个基本情况,即对一个基元表达式求值。一些特殊形式也构成了没有递归调用的基本情况。同样地,应用函数也有一个基本情况,即应用于一个基元函数。处理表达式的求值函数与处理函数及其参数的应用函数相互调用,这种相互递归的结构,构成了求值流程的本质。
60 |
61 | ## 3.5.2 环境
62 |
63 | 现在我们已经描述了 Scheme 解释器的结构,接下来我们来实现构成环境的 `Frame` 类。每个 `Frame` 实例代表一个环境,在这个环境中,符号与值绑定。一个帧有一个保存绑定(`bindings`)的字典,以及一个父(`parent`)帧。对于全局帧而言,父帧为 `None`。
64 |
65 | 绑定不能直接访问,而是通过两种 `Frame` 方法:`lookup` 和 `define`。第一个方法实现了第一章中描述的计算环境模型的查找流程。符号与当前帧的绑定相匹配。如果找到它,则返回它绑定到的值。如果没有找到,则继续在父帧中查找。另一方面,`define` 方法用来将符号绑定到当前帧中的值。
66 |
67 | `lookup` 的实现和 `define` 的使用留作练习。为了说明它们的用途,请看以下 Scheme 程序示例:
68 |
69 | ```scheme
70 | (define (factorial n)
71 | (if (= n 0) 1 (* n (factorial (- n 1)))))
72 |
73 | (factorial 5)
74 | 120
75 | ```
76 |
77 | 第一个输入表达式是一个 `define` 形式,将由 Python 函数 `do_define_form` 求值。定义一个函数有如下步骤:
78 |
79 | 1. 检查表达式的格式,确保它是一个格式良好的 Scheme 列表,在关键字 `define` 后面至少有两个元素。
80 | 2. 分析第一个元素(这里是一个 `Pair`),找出函数名称 `factorial` 和形式参数表 `(n)`。
81 | 3. 使用提供的形式参数、函数主体和父环境创建 `LambdaProcedure`。
82 | 4. 在当前环境的第一帧中,将 `factorial` 符号与此函数绑定。在示例中,环境只包括全局帧。
83 |
84 | 第二个输入是调用表达式。传递给 `scheme_apply` 的 `procedure` 是刚刚创建并绑定到符号 `factorial` 的 `LambdaProcedure`。传入的 `args` 是一个单元素 Scheme 列表 `(5)`。为了应用该函数,我们将创建一个新帧来扩展全局帧(`factorial` 函数的父环境)。在这帧中,符号 `n` 被绑定为数值 5。然后,我们将在该环境中对 `factorial` 函数主体进行求值,并返回其值。
85 |
86 | ## 3.5.3 数据即程序
87 |
88 | 在思考对 Scheme 表达式进行求值的程序时,一个类比可能会有所帮助。关于程序的含义,一种操作观点认为,程序是对抽象机器的描述。例如,再看一下这个计算阶乘的程序:
89 |
90 | ```scheme
91 | (define (factorial n)
92 | (if (= n 0) 1 (* n (factorial (- n 1)))))
93 | ```
94 |
95 | 我们也可以使用条件表达式在 Python 中表达一个等价的程序。
96 |
97 | ```py
98 | >>> def factorial(n):
99 | return 1 if n == 1 else n * factorial(n - 1)
100 | ```
101 |
102 | 我们可以把这个程序看作是对一台机器的描述,这台机器包含减法、乘法和相等检验等部分,还有一个双位开关和另一台阶乘机。(阶乘机是无限的,因为它包含了另一个阶乘机)。下图是阶乘运算机的流程图,显示了各部分的连接方式。
103 |
104 | 
105 |
106 | 类似地,我们可以把 Scheme 解释器看作是一个非常特殊的机器,它接受对机器的描述作为输入。有了这个输入,解释器就会配置自己,以模拟所描述的机器。例如,如果我们向我们的求值器输入阶乘的定义,求值器就能计算阶乘。
107 |
108 | 从这个角度看,我们的 Scheme 解释器是一种通用机器。当其他机器被描述为 Scheme 程序时,它就会模仿这些机器。它是编程语言操作的数据对象与编程语言本身之间的桥梁。想象一下,用户在我们运行的 Scheme 解释器中键入一个 Scheme 表达式。从用户的角度来看,诸如 (+ 2 2) 这样的输入表达式是编程语言中的表达式,解释器应该对其进行求值。然而,从 Scheme 解释器的角度来看,该表达式只是一个由单词组成的句子,需要根据一组定义明确的规则进行处理。
109 |
110 | 用户的程序就是解释器的数据,这不一定会引起混淆。事实上,有时忽略这种区别,让用户明确地将数据对象作为表达式来求值,也是很方便的。在 Scheme 中,每当使用 `run` 函数时,我们都会使用此功能。在 Python 中也有类似的函数:`eval` 函数可以对 Python 表达式求值,`exec` 函数将执行 Python 语句。因此,
111 |
112 | ```py
113 | >>> eval('2+2')
114 | 4
115 | ```
116 |
117 | 和
118 |
119 | ```py
120 | >>> 2+2
121 | 4
122 | ```
123 |
124 | 两者都返回相同的结果。在动态编程语言中,对执行过程中构建的表达式进行求值是一个常见而强大的功能。虽然很少有语言能把这种实践同 Scheme 一样普遍,但在程序执行过程中构建和求值表达式的能力对于任何程序员来说都是非常有价值的工具。
125 |
--------------------------------------------------------------------------------
/sicp/4/1.md:
--------------------------------------------------------------------------------
1 | # 4.1 引言
2 |
3 | ::: details INFO
4 | 译者:[Mancuoj](https://github.com/mancuoj)
5 |
6 | 来源:[4.1 Introduction](https://www.composingprograms.com/pages/41-introduction.html)
7 | :::
8 |
9 | 现代计算机可以处理大量的关于世界各个方面的数据。通过这些大数据集,我们可以以前所未有的方式了解人类的行为:语言的使用方式、拍摄的照片、讨论的主题以及人们如何与周围环境互动。为了高效处理大型数据集,程序被组织成一系列对顺序数据流进行操作的管道(pipelines)。在本章中,我们将思考一套技术来有效地处理和操作连续的数据流。
10 |
11 | 在第二章中,我们介绍了一种序列接口(sequence interface),由内置的 Python 数据类型如 `list` 和 `range` 实现。在本章中,我们会将顺序数据的概念扩展到包括无界(unbounded)甚至无限大小的集合。无限序列的两个数学例子是正整数和斐波那契数列。无限长度的连续数据集也会出现在其他计算域中。例如,通过手机信号塔发送的电话呼叫序列、计算机用户的鼠标移动序列以及飞机传感器测量的加速度序列都会随着世界的演变而不断增长。
12 |
--------------------------------------------------------------------------------
/sicp/4/4.md:
--------------------------------------------------------------------------------
1 | # 4.4 Logic 语言编程
2 |
3 | ::: details INFO
4 | 译者:[kam1usec](https://github.com/kam1usec)
5 |
6 | 来源:[4.4 Logic Programming](https://www.composingprograms.com/pages/44-logic-programming.html)
7 |
8 | 对应:无
9 | :::
10 |
11 | 在本节中,我们将介绍一种专门为本节内容设计的声明式查询语言 logic。这是一种基于 [Prolog](http://en.wikipedia.org/wiki/Prolog) 和 [计算机程序结构与解释](http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-29.html#%_sec_4.4.1) 的声明式语言。[logic](https://www.composingprograms.com/examples/logic/logic..html) 解释器是基于前面章节的模式项目的一个完全实现。其中,数据记录被表示为模式列表,查询语句被表示为模式值。
12 |
13 | ## 4.4.1 事实和查询
14 |
15 | 数据库存储着代表系统中事实的记录。而查询解释器的目的是直接从数据库记录中检索事实集合,并使用逻辑推理从数据库中推导出新的事实。logic 中的事实语句由一个或多个跟随关键字 "fact" 的列表组成。简单事实是一个单独的列表。例如,一个对美国总统感兴趣的犬舍老板可以使用 logic 记录她家狗狗的家谱,如下所示:
16 |
17 | > 译者注:abraham(亚伯拉罕)、barack(巴拉克)、clinton(克林顿)、eisenhower(艾森豪威尔)等等为总统名;在 logic 语言中,数据存储(数据库)指的是一个集合,其中包含了一些已知事实或关系,这些事实或关系可以被查询解释器引用并用于推理和判断过程中。
18 |
19 | ```
20 | (fact (parent abraham barack))
21 | (fact (parent abraham clinton))
22 | (fact (parent delano herbert))
23 | (fact (parent fillmore abraham))
24 | (fact (parent fillmore delano))
25 | (fact (parent fillmore grover))
26 | (fact (parent eisenhower fillmore))
27 | ```
28 |
29 | 在以逻辑为基础的系统中,关系通常不像函数或过程一样被应用,而是与查询相匹配。这句话的意思是,当向数据库发出查询请求时,系统会尝试将查询语句与现有的关系进行匹配,以便找到相关信息。查询指定正在寻找的信息(例如,“谁是巴拉克的父母?”),系统会尝试找到与查询匹配的关系(例如,“狗 - 亚伯拉罕是巴拉克的父亲”)。一旦找到一个匹配的关系,系统就可以使用逻辑推理来推导新的信息或根据数据库中已有的事实得出结论。这种逻辑表示和推理方法可以更灵活地推理实体之间的复杂关系。
30 |
31 | 一个查询包括一个或多个列表,以关键词“query”开头。查询可能包含变量,这些变量是以问号开头的字符串。查询解释器会将这些变量与事实进行匹配:
32 |
33 | ```
34 | (query (parent abraham ?child))
35 | Success!
36 | child: barack
37 | child: clinton
38 | ```
39 |
40 | 查询解释器会返回 `Success!` 以表示该查询与数据库中的一些事实相匹配,并且下面列出了匹配变量 `?child` 的替换结果。
41 |
42 | **复合事实**:事实可能包含变量以及多个子表达式。一个多表达式的事实以结论开头,后跟假设。为了使结论成立,所有的假设都必须被满足:
43 |
44 | ```
45 | (fact ... )
46 | ```
47 |
48 | > `` 代表结论,` ... ` 代表前提条件或假设,可以包含变量和多个子表达式。
49 |
50 | 例如,可以根据数据库中已有的关于父母的事实来声明有关孩子的事实。
51 |
52 | ```
53 | (fact (child ?c ?p) (parent ?p ?c))
54 | ```
55 |
56 | 以上的事实可以理解为:如果 `?p` 是 `?c` 的父母,则 `?c` 是 `?p` 的孩子。现在,这个查询可以引用这个事实。
57 |
58 | ```
59 | (query (child ?child fillmore))
60 | Success!
61 | child: abraham
62 | child: delano
63 | child: grover
64 | ```
65 |
66 | 上述查询需要查询解释器将定义孩子的事实与 `Fillmore` 作为父母的事实相结合。用户不需要知道如何组合这些信息,只需要知道结果具有特定的形式。查询解释器需要根据可用的事实证明 `(child abraham fillmore)` 为真。
67 |
68 | 查询不一定需要包含变量。它可以简单地验证一个事实,如果为真,并返回一个 `Success!`。
69 |
70 | ```
71 | (query (child herbert delano))
72 | Success!
73 | ```
74 |
75 | 如果查询没有匹配到事实,那么解释器会返回一个 `Failed`。
76 |
77 | ```
78 | (query (child eisenhower ?parent))
79 | Failed.
80 | ```
81 |
82 | **否定**。我们可以使用特殊关键词 `not` 来检查查询是否不与任何事实匹配。
83 |
84 | ```
85 | (query (not ))
86 | ```
87 |
88 | 这个查询会在 `` 匹配失败时返回 `Success!`,而在 `` 匹配成功时返回 `Failed`。这个方法被称为否定即失败。
89 |
90 | ```
91 | (query (not (parent abraham clinton)))
92 | Failed.
93 | (query (not (parent abraham barack)))
94 | Failed.
95 | ```
96 |
97 | 使用(not)关键词时,若是查询不与事实匹配,结果为真,查询与事实匹配,结果反而为假。我们可以思考以下查询的结果:
98 |
99 | ```
100 | (query (not (parent abraham ?who)))
101 | ```
102 |
103 | 为什么这个查询会返回 `Failed`?有许多值可以绑定到 `?who`,使得这个查询匹配成功,如果按照 `not` 查询的步骤,那么我们首先要检查关系 `(parent abraham ?who)`。因为 `?who` 可以绑定任意值,即 `barack` 或 `clinton`,所以这个关系为真。因此这个关系的否定查询会返回 `Failed`。
104 |
105 | ## 4.4.2 递归事实
106 |
107 | logic 语言允许递归事实。也就是说,一个事实的结论可能取决于包含相同符号的假设。例如,血缘关系是用两个事实定义的。如果某个 `?a` 是 `?y` 的祖先,那么它既可以是 `?y` 的父母,也可以是 `?y` 的祖先的父母:
108 |
109 | ```
110 | (fact (ancestor ?a ?y) (parent ?a ?y))
111 | (fact (ancestor ?a ?y) (parent ?a ?z) (ancestor ?z ?y))
112 | ```
113 |
114 | A single query can then list all ancestors of herbert:
115 | 因此一个查询就可以列出 herbert 的所有祖先。
116 |
117 | ```
118 | (query (ancestor ?a herbert))
119 | Success!
120 | a: delano
121 | a: fillmore
122 | a: eisenhower
123 | ```
124 |
125 | **复合查询**。查询可以包含多个子表达式,所有这些子表达式由相同的符号变量组成。如果一个变量在查询中出现多次,则它在每个上下文中必须取相同的值。使用以下查询查找 Herbert 和 Barack 的祖先:
126 |
127 | ```
128 | (query (ancestor ?a barack) (ancestor ?a herbert))
129 | Success!
130 | a: fillmore
131 | a: eisenhower
132 | ```
133 |
134 | 递归事实可能需要很长的推理链才能将查询与数据库中的已有事实相匹配,例如要证明 `(ancestor fillmore herbert)` 的事实,我们必须连续证明以下每个事实:
135 |
136 | ```
137 | (parent delano herbert) ; (1), a simple fact
138 | (ancestor delano herbert) ; (2), from (1) and the 1st ancestor fact
139 | (parent fillmore delano) ; (3), a simple fact
140 | (ancestor fillmore herbert) ; (4), from (2), (3), & the 2nd ancestor fact
141 | ```
142 |
143 | 这样,只要查询解释器能够发现它们,这个事实意味着大量附加事实甚至无限多的事实。
144 |
145 | **层级事实**。到目前为止,每个事实和查询表达式都是一个符号列表。事实和查询列表可以包含列表,并提供一种表示分层数据的方法。接下来,我们把每只狗的颜色与作为附加记录的名称一起存储:
146 |
147 | ```
148 | (fact (dog (name abraham) (color white)))
149 | (fact (dog (name barack) (color tan)))
150 | (fact (dog (name clinton) (color white)))
151 | (fact (dog (name delano) (color white)))
152 | (fact (dog (name eisenhower) (color tan)))
153 | (fact (dog (name fillmore) (color brown)))
154 | (fact (dog (name grover) (color tan)))
155 | (fact (dog (name herbert) (color brown)))
156 | ```
157 |
158 | 查询可以表达层级事实的完整结构,也可以将变量与整个列表匹配。
159 |
160 | ```
161 | (query (dog (name clinton) (color ?color)))
162 | Success!
163 | color: white
164 | (query (dog (name clinton) ?info))
165 | Success!
166 | info: (color white)
167 | ```
168 |
169 | 数据库的许多能力在于查询解释器能够在单个查询中结合多种类型的事实。以下查询查找所有颜色相同且其中一只是另一只祖先的狗对:
170 |
171 | ```
172 | (query (dog (name ?name) (color ?color))
173 | (ancestor ?ancestor ?name)
174 | (dog (name ?ancestor) (color ?color)))
175 | Success!
176 | name: barack color: tan ancestor: eisenhower
177 | name: clinton color: white ancestor: abraham
178 | name: grover color: tan ancestor: eisenhower
179 | name: herbert color: brown ancestor: fillmore
180 | ```
181 |
182 | 变量可以引用分层记录中的列表,也可以使用点符号。跟在点符号后面的变量匹配事实列表的其余部分。点分列表可以出现在事实或查询中。下面的例子通过列出犬类祖先的血统链条来构建狗的家谱。(年轻的巴拉克沿袭了一条历史悠久的总统狗的血统。)
183 |
184 | ```
185 | (fact (pedigree ?name) (dog (name ?name) . ?details))
186 | (fact (pedigree ?child ?parent . ?rest)
187 | (parent ?parent ?child)
188 | (pedigree ?parent . ?rest))
189 | (query (pedigree barack . ?lineage))
190 | Success!
191 | lineage: ()
192 | lineage: (abraham)
193 | lineage: (abraham fillmore)
194 | lineage: (abraham fillmore eisenhower)
195 | ```
196 |
197 | 声明式或逻辑编程以非常高效的方式表达事实之间的关系。例如,如果我们希望两个列表可以连接成一个更长的列表,这个长列表包含第一个列表的元素,后跟第二个列表的元素。我们可以制定两个规则:首先,规定一个基本操作声明,将一个空列表添加到任何列表都会得到该列表;
198 |
199 | ```
200 | (fact (append-to-form () ?x ?x))
201 | ```
202 |
203 | 第二,在一个递归事实中,一个以第一个元素为 `?a`,剩余部分为 `?r` 的列表与另一个列表 `?y` 拼接起来形成一个新列表,这个新列表以 `?a` 为第一个元素,而一些附加的剩余部分为 `?z`。为了使这种关系成立,必须确保 `?r` 和 `?y` 被拼接起来形成 `?z`。
204 |
205 | ```
206 | (fact (append-to-form (?a . ?r) ?y (?a . ?z)) (append-to-form ?r ?y ?z))
207 | ```
208 |
209 | 查询解释器可以通过这两个事实,计算附加在一起的任意两个列表。
210 |
211 | ```
212 | (query (append-to-form (a b c) (d e) ?result))
213 | Success!
214 | result: (a b c d e)
215 | ```
216 |
217 | 此外,它可以计算 `?left` 和 `?right` 所有可能的列表对,并且将它们拼接起来形成列表 `(a b c d e)`:
218 |
219 | ```
220 | (query (append-to-form ?left ?right (a b c d e)))
221 | Success!
222 | left: () right: (a b c d e)
223 | left: (a) right: (b c d e)
224 | left: (a b) right: (c d e)
225 | left: (a b c) right: (d e)
226 | left: (a b c d) right: (e)
227 | left: (a b c d e) right: ()
228 | ```
229 |
230 | 虽然我们的查询解释器可能显得相当智能,但是我们会看到它是通过多次重复一个简单操作来找到这些组合的:在数据库中匹配包含上述变量的两个列表。
231 |
--------------------------------------------------------------------------------
/sicp/4/5.md:
--------------------------------------------------------------------------------
1 | # 4.5 合一
2 |
3 | ::: details INFO
4 | 译者:[silver](https://github.com/silver-ymz)
5 |
6 | 来源:[4.5 Unification](https://www.composingprograms.com/pages/45-unification.html)
7 |
8 | 对应:无
9 | :::
10 |
11 | 在本节中,我们将介绍用 logic 语言进行推理的查询解释器的实现。解释器是一种通用的问题求解器,但在求解问题的规模和类型上有很大的局限性。目前已有更复杂的逻辑编程语言,但构建高效的推理程序仍是计算机科学领域一个活跃的研究课题。
12 |
13 | 查询解释器执行的基本操作称为合一。合一是将查询与事实(每个事实都可能包含变量)进行匹配的一般方法。查询解释器反复执行这一操作,首先将原始查询与事实的结论相匹配,然后将事实的假设与数据库中的其他结论相匹配。在此过程中,查询解释器会搜索与查询相关的所有事实。如果它找到了支持查询的变量赋值方法,就会将赋值作为成功结果返回。
14 |
15 | ## 4.5.1 模式匹配
16 |
17 | 为了返回与查询匹配的简单事实,解释器必须将包含变量的查询与不包含变量的事实相匹配。例如,查询 `(query (parent abraham ?child))` 与事实 `(fact (parent abraham barack))` 在变量 `?child` 值为 `barack` 时相匹配。
18 |
19 | 一般而言,如果有变量名绑定到值,使得将这些值替换到模式中产生表达式,那么模式就与某个表达式(可能是嵌套的 Scheme 列表)匹配。
20 |
21 | 例如,表达式 `((a b) c (a b))` 与模式 `(?x c ?x)` 匹配,其中变量 `?x` 绑定到值 `(a b)`。同样的表达式也与模式 `((a ?y) ?z (a b))` 匹配,其中变量 `?y` 绑定到 `b`,`?z` 绑定到 `c`。
22 |
23 | ## 4.5.2 表示事实和查询
24 |
25 | 通过导入所提供的逻辑示例程序,可以复制以下示例。
26 |
27 | ```py
28 | >>> from logic import *
29 | ```
30 |
31 | 在逻辑语言中,查询和事实都以 Scheme 列表的形式表示,使用前一章中的相同 `Pair` 类和 `nil` 对象。例如,查询表达式 `(?x c ?x)` 表示为嵌套的 `Pair` 实例。
32 |
33 | ```py
34 | >>> read_line("(?x c ?x)")
35 | Pair('?x', Pair('c', Pair('?x', nil)))
36 | ```
37 |
38 | 与 Scheme 项目一样,将符号绑定到值的环境由 `Frame` 类的一个实例表示,该实例具有一个名为 `bindings` 的属性。
39 |
40 | 在 logic 语言中执行模式匹配的函数称为 `unify`。它接受两个输入,`e` 和 `f`,以及一个记录变量与值绑定的环境 `env`。
41 |
42 | ```py
43 | >>> e = read_line("((a b) c (a b))")
44 | >>> f = read_line("(?x c ?x)")
45 | >>> env = Frame(None)
46 | >>> unify(e, f, env)
47 | True
48 | >>> env.bindings
49 | {'?x': Pair('a', Pair('b', nil))}
50 | >>> print(env.lookup('?x'))
51 | (a b)
52 | ```
53 |
54 | 上面,`unify` 的返回值为 `True`,表示模式 `f` 能够匹配表达式 `e`。合一的结果记录在 `env` 中,将 `?x` 绑定到 `(a b)`。
55 |
56 | ## 4.5.3 合一算法
57 |
58 | 合一是模式匹配的一种推广,它试图在两个可能都包含变量的表达式之间找到映射关系。`unify` 函数通过递归过程实现合一,它对两个表达式的相应部分执行合一,直到出现矛盾或者可以建立与所有变量的可行绑定。
59 |
60 | 让我们从一个例子开始。模式 `(?x ?x)` 可以匹配模式 `((a ?y c) (a b ?z))`,因为有一个不含变量的表达式可以同时匹配这两个模式:`((a b c) (a b c))`。合一通过以下步骤确定这一解决方案:
61 |
62 | 1. 匹配每个模式的第一个元素,变量 `?x` 与表达式 `(a ?y c)` 绑定。
63 | 2. 匹配每个模式的第二个元素,首先将变量 `?x` 替换为其值。然后,通过将 `?y` 绑定到 `b`,将 `?z` 绑定到 `c`,`(a ?y c)` 与 `(a b ?z)` 匹配。
64 |
65 | 因此,传递给 `unify` 的环境绑定将包含 `?x`、 `?y` 和 `?z`:
66 |
67 | ```py
68 | >>> e = read_line("(?x ?x)")
69 | >>> f = read_line(" ((a ?y c) (a b ?z))")
70 | >>> env = Frame(None)
71 | >>> unify(e, f, env)
72 | True
73 | >>> env.bindings
74 | {'?z': 'c', '?y': 'b', '?x': Pair('a', Pair('?y', Pair('c', nil)))}
75 | ```
76 |
77 | 合一的结果可能会将变量绑定到一个同样包含变量的表达式中,就像我们上面看到的将 `?x` 绑定到 `(a ?y c)`。`bind` 函数会递归、重复地将表达式中的所有变量与其值绑定,直到没有绑定变量为止。
78 |
79 | ```py
80 | >>> print(bind(e, env))
81 | ((a b c) (a b c))
82 | ```
83 |
84 | 一般来说,合一是通过检查几个条件来进行的。合一的实现直接遵循下面的描述。
85 |
86 | 1. 如果输入 `e` 和 `f` 是变量,则用它们的值替换。
87 | 2. 如果 `e` 和 `f` 相等,则合一成功。
88 | 3. 如果 `e` 是变量,则合一成功,并将 `e` 绑定到 `f`。
89 | 4. 如果 `f` 是变量,则合一成功,并将 `f` 绑定到 `e`。
90 | 5. 如果两者都不是变量,也不是列表,并且不相等,那么 `e` 和 `f` 就不能合一,因此合一失败。
91 | 6. 如果这些情况都不成立,那么 `e` 和 `f` 都是列表,因此要对它们的第一个和第二个对应元素分别进行合一。
92 |
93 | ```py
94 | >>> def unify(e, f, env):
95 | """Destructively extend ENV so as to unify (make equal) e and f, returning
96 | True if this succeeds and False otherwise. ENV may be modified in either
97 | case (its existing bindings are never changed)."""
98 | e = lookup(e, env)
99 | f = lookup(f, env)
100 | if e == f:
101 | return True
102 | elif isvar(e):
103 | env.define(e, f)
104 | return True
105 | elif isvar(f):
106 | env.define(f, e)
107 | return True
108 | elif scheme_atomp(e) or scheme_atomp(f):
109 | return False
110 | else:
111 | return unify(e.first, f.first, env) and unify(e.second, f.second, env)
112 | ```
113 |
114 | ## 4.5.4 证明
115 |
116 | 对于 logic 语言,一种思考方式是将其视为形式系统中断言的证明器。每个陈述的事实在形式系统中建立了一个公理,而每个查询都必须由查询解释器根据这些公理建立。换句话说,每个查询断言存在某种变量赋值,使其所有子表达式都同时符合系统的事实。查询解释器的作用是验证这一点是否成立。
117 |
118 | 例如,根据有关狗的事实集合,我们可以断言存在克林顿狗和棕褐色狗的共同祖先。查询解释器只有在能够确定这一断言为真的情况下,才会输出 `Success!`。作为副产品,它还会告知我们该共同祖先和棕褐色狗的名称:
119 |
120 | ```
121 | (query (ancestor ?a clinton)
122 | (ancestor ?a ?brown-dog)
123 | (dog (name ?brown-dog) (color brown)))
124 |
125 | Success!
126 | a: fillmore brown-dog: herbert
127 | a: eisenhower brown-dog: fillmore
128 | a: eisenhower brown-dog: herbert
129 | ```
130 |
131 | 结果中显示的三个赋值中的每一个都是查询在给定事实情况下为真的更大证明的一部分。完整的证明将包括使用的所有事实,例如包括 `(parent abraham clinton)` 和 `(parent fillmore abraham)`。
132 |
133 | ## 4.5.5 搜索
134 |
135 | 为了从系统中已经建立的事实中建立查询,查询解释器在所有可能的事实中进行搜索。合一是对两个表达式进行模式匹配的基本操作。查询解释器中的搜索程序会选择要合一的表达式,以便找到一组事实,将它们串联起来,从而建立查询。
136 |
137 | 递归的 `search` 函数实现了 logic 语言的搜索过程。它的输入包括表示查询子句的 Scheme 列表 `clauses`、包含当前符号与值绑定(初始为空)的环境 `env`,以及已经串联起来的规则链的深度 `depth`。
138 |
139 | ```py
140 | >>> def search(clauses, env, depth):
141 | """Search for an application of rules to establish all the CLAUSES,
142 | non-destructively extending the unifier ENV. Limit the search to
143 | the nested application of DEPTH rules."""
144 | if clauses is nil:
145 | yield env
146 | elif DEPTH_LIMIT is None or depth <= DEPTH_LIMIT:
147 | if clauses.first.first in ('not', '~'):
148 | clause = ground(clauses.first.second, env)
149 | try:
150 | next(search(clause, glob, 0))
151 | except StopIteration:
152 | env_head = Frame(env)
153 | for result in search(clauses.second, env_head, depth+1):
154 | yield result
155 | else:
156 | for fact in facts:
157 | fact = rename_variables(fact, get_unique_id())
158 | env_head = Frame(env)
159 | if unify(fact.first, clauses.first, env_head):
160 | for env_rule in search(fact.second, env_head, depth+1):
161 | for result in search(clauses.second, env_rule, depth+1):
162 | yield result
163 | ```
164 |
165 | 同时满足所有子句的搜索从第一个子句开始。在第一个子句是否定的特殊情况下,我们不再尝试用事实来合一查询的第一个子句,而是通过递归调用 `search` 来检查是否存在这种合一的可能性。如果递归调用一无所获,我们就继续搜索其余子句。如果可以合一,我们就立即失败。
166 |
167 | 如果我们的第一个子句不是否定,那么对于数据库中的每个事实,`search` 都会尝试将事实的第一个子句与查询的第一个子句合一起来。合一在一个新的环境 `env_head` 中进行。作为合一的副作用,变量会绑定到 `env_head` 中的值。
168 |
169 | 如果合一成功,则子句与当前规则的结论相匹配。下面的 for 语句试图建立规则的假设,以便建立结论。在这里,递归规则的假设将递归传递给 `search`,从而建立结论。
170 |
171 | 最后,每次对 `fact.second` 的成功搜索,生成的环境都会绑定到 `env_rule`。在给定这些值对变量的绑定之后,最终的 for 语句进行搜索,以建立初始查询中的其余子句。任何成功的结果都会通过内部的 yield 语句返回。
172 |
173 | **唯一名称**:合一假设 `e` 和 `f` 之间不共享任何变量。然而,我们经常在 logic 语言的事实和查询中重复使用变量名。我们不希望混淆一个事实中的 `?x` 和另一个事实中的 `?x`;这些变量是不相关的。为了确保变量名不会混淆,在将一个事实传入 `unify` 之前,我们会使用 `rename_variables` 将其变量名替换为唯一的变量名,并为该事实添加一个唯一的整数。
174 |
175 | ```py
176 | >>> def rename_variables(expr, n):
177 | """Rename all variables in EXPR with an identifier N."""
178 | if isvar(expr):
179 | return expr + '_' + str(n)
180 | elif scheme_pairp(expr):
181 | return Pair(rename_variables(expr.first, n),
182 | rename_variables(expr.second, n))
183 | else:
184 | return expr
185 | ```
186 |
187 | 其余的细节,包括 logic 语言的用户界面和各种辅助函数的定义,都展示在 [logic](http://composingprograms.com/examples/logic/logic.py.html) 示例中。
188 |
--------------------------------------------------------------------------------
/sicp/4/6.md:
--------------------------------------------------------------------------------
1 | # 4.6 分布式计算
2 |
3 | ::: details INFO
4 | 译者:[Bryan Zhang](https://github.com/billycrapediem)
5 |
6 | 来源:[4.6 Distributed Computing](https://www.composingprograms.com/pages/46-distributed-computing.html)
7 |
8 | 对应:无
9 | :::
10 |
11 | 大规模数据处理应用程序通常在多台计算机之间协调工作。分布式计算(Distributed Computing)是指多台相互连接但独立的计算机协调合作,执行计算任务。
12 |
13 | 不同的计算机是独立的,意味着它们不直接共享内存。但是,它们通过消息(Message)进行通信,将信息通过网络从一台计算机传输到另一台计算机。
14 |
15 | ## 4.6.1 消息
16 |
17 | 在计算机之间发送的消息是字节序列(sequences of bits)。消息的目的各不相同;消息可以请求数据、发送数据,或者指示另一台计算机执行一个调用。在所有情况下,两台互相通信的计算机会使用相互绑定的编码和解码方式。为了实现这一点,计算机采用了一种消息协议,为字节序列赋予了含义。
18 |
19 | 消息协议(message protocols)是一组用于编码和解释消息的规则。发送和接收计算机都必须就消息的编码解码方式达成一致,以实现成功的通信。许多消息协议规定,消息必须符合特定的格式。比如,固定位置的某些字节(bits)指示了固定的条件。其他协议使用特定的字节或字节序列来界定消息的各个部分,就像标点符号在编程语言的语法中界定子表达式一样。
20 |
21 | 消息协议并不是特定的程序或软件库。相反,它们是一种规则,可以被各种程序应用,甚至可以用不同的编程语言编写。因此,具有完全不同软件系统的计算机可以通过遵守消息协议来参与同一个分布式系统。
22 |
23 | **TCP/IP 协议**:在互联网上,消息通过 [互联网协议](https://en.wikipedia.org/wiki/Internet_Protocol) 从一台机器传输到另一台机器,该协议规定了如何在不同网络之间传输数据包,以实现全球范围的互联网通信。IP 协议是基于网络在任何地方都不可靠且结构动态的假设设计的。此外,它不假设存在任何中央的通信跟踪或监控。每个数据包都包含一个头部,其中包含目标 IP 地址以及其他信息。所有数据包都根据最大努力原则(best-effort basis)在网络中进行转发,最终会抵达设定上的目的地。
24 |
25 | 这种设计对通信施加了一些限制。使用现代 IP(IPv4 和 IPv6)实现传输的数据包最大大小为 $65,535$ 字节。更大的数据值必须分割成多个数据包。IP 协议不保证数据包将按照发送的顺序接收。有些数据包可能会丢失,有些数据包可能会传输多次。
26 |
27 | [传输控制协议 Transmission Control Protocol](http://en.wikipedia.org/wiki/Transmission_Control_Protocol) 是在 IP 协议的基础上提供了对任意大小字节流可靠有序的传输。该协议通过正确地对 IP 传输的数据包进行排序,去除重复数据包,并请求重新传输丢失的数据包来实现可靠的传输。这种提高的可靠性是以延迟为代价的,即从一个点发送消息到另一个点所需的时间。
28 |
29 | TCP(Transmission Control Protocol)将数据流分割成 TCP 段(TCP segments),每个段包含一部分数据,其前面是一个包含序列和状态信息的头部,以支持数据的可靠、有序传输。有些 TCP 段根本不包含数据,而是用于在两台计算机之间建立或终止连接。
30 |
31 | 在两台计算机 A 和 B 之间建立连接的过程分为三个步骤:
32 |
33 | 1. A 向 B 的一个端口发送请求,以建立 TCP 连接,并提供一个端口号,以便发送响应。
34 | 2. B 向 A 指定的端口发送响应,并等待其响应得到确认。
35 | 3. A 发送确认响应,确认数据可以在双向传输。
36 |
37 | 经过这三步的 "握手",TCP 连接建立完成,A 和 B 可以相互发送数据。终止 TCP 连接则按照一系列步骤进行,其中客户端和服务器都请求并确认连接的结束。
38 |
39 | ## 4.6.2 客户端/服务器体系架构
40 |
41 | 客户端/服务器体系结构是一种从中央源提供服务的方式。服务器提供服务,多个客户端与服务器通信以获取该服务。在这种体系结构中,客户端和服务器有不同的角色。服务器的角色是响应客户端的服务请求,而客户端的角色是发出请求,并利用服务器的响应来执行某些任务。下面的图示说明了这种体系结构。
42 |
43 | 
44 |
45 | 这种模型最具影响力的应用是万维网(World Wide Web)。当一个网络浏览器显示网页内容时,运行在独立计算机上的几个程序通过客户端/服务器体系结构进行交互。本节描述了请求网页的过程,以阐述客户端/服务器分布式系统的核心思想。
46 |
47 | **角色(Role)**:Web 用户计算机上的 Web 浏览器应用在请求网页时扮演客户端的角色。当从互联网上的域名(例如 www.nytimes.com)请求内容时,它必须与至少两个不同的服务器进行通信。
48 |
49 | 客户端首先从域名服务器(Domain Name Server DNS)请求该名称所对应的计算机的互联网协议(IP)地址。DNS 提供将域名映射到 IP 地址的服务,这些 IP 地址是互联网上计算机的数字标识符。Python 可以使用 socket 模块直接进行此类请求。
50 |
51 | ```python
52 | >>> from socket import gethostbyname
53 | >>> gethostbyname('www.nytimes.com')
54 | '170.149.172.130'
55 | ```
56 |
57 | 接下来,客户端从位于该 IP 地址的 Web 服务器请求网页内容。在这种情况下,响应是一个 [HTML ](http://en.wikipedia.org/wiki/HTML) 文档,其中包含当天的新闻标题和文章摘录,以及表达式,指示 Web 浏览器客户端如何在用户的屏幕上布局内容。Python 可以使用 `urllib.request` 模块进行所需的两个请求以检索此内容。
58 |
59 | ```python
60 | >>> from urllib.request import urlopen
61 | >>> response = urlopen('http://www.nytimes.com').read()
62 | >>> response
63 | b''
64 | ```
65 |
66 | 在接收到此响应后,浏览器会为图像、视频和页面的其他辅助组件发出额外的请求。这些请求源于原始 HTML 中额外内容的地址以及它们如何嵌入到页面中的描述。
67 |
68 | **HTTP 请求**:超文本传输协议(Hypertext Transfer Protocol HTTP)是一种使用 TCP 实现的协议,用于管理万维网(World Wide Web WWW)的通信。它在 Web 浏览器和 Web 服务器之间假设了一个客户端/服务器体系结构。HTTP 规定了浏览器和服务器之间交换的消息格式。所有的 Web 浏览器都使用 HTTP 格式从 Web 服务器请求页面,而所有的 Web 服务器都使用 HTTP 格式发送它们的响应。
69 |
70 | HTTP 请求有几种类型,其中最常见的是针对特定网页的 `GET` 请求。`GET` 请求指定一个位置。例如,将地址 http://en.wikipedia.org/wiki/UC_Berkeley 输入到 Web 浏览器中,将向 en.wikipedia.org 的 80 端口发出 HTTP `GET` 请求,请求位于 `/wiki/UC_Berkeley` 的内容。服务器返回一个 HTTP 响应:
71 |
72 | ```HTTP
73 | HTTP/1.1 200 OK
74 | Date: Mon, 23 May 2011 22:38:34 GMT
75 | Server: Apache/1.3.3.7 (Unix) (Red-Hat/Linux)
76 | Last-Modified: Wed, 08 Jan 2011 23:11:55 GMT
77 | Content-Type: text/html; charset=UTF-8
78 |
79 | ... web page content ...
80 | ```
81 |
82 | 在第一行中,文本 `200 OK` 表示在响应请求时没有出现错误。头部的后续行提供有关服务器、日期和正在发送的内容类型的信息。
83 |
84 | 如果您输入了错误的网址,或者点击了一个错误的链接,您可能会看到类似以下错误的消息:
85 |
86 | ```HTTP
87 | 404 Error File Not Found
88 | ```
89 |
90 | 这意味着服务器发送回的 HTTP 头部以以下方式开始:
91 |
92 | ```HTTP
93 | HTTP/1.1 404 Not Found
94 | ```
95 |
96 | 数字 $200$ 和 $404$ 是 HTTP 响应代码。一组固定的响应代码是消息协议的常见特征。协议的设计者试图通过预测协议发送的常见消息,并分配固定的代码来减小传输大小并建立常见的消息语义。在 HTTP 协议中,响应代码 200 表示成功,而 404 表示找不到资源的错误。HTTP 1.1 标准中还存在各种其他 [响应代码](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes)。
97 |
98 | **模块化(Modularity)**:客户端和服务器是一个强大的概念。服务器提供服务,可能同时为多个客户端提供服务,而客户端则获取该服务。客户端不需要知道服务是如何提供的,也不需要知道他们接收的数据是如何存储或计算的,而服务器也不需要知道服务如何被客户端使用。
99 |
100 | 在网络上,我们通常认为客户端和服务器位于不同的计算机上,但即使在单台计算机上的系统也可以具有客户端/服务器体系结构。例如,计算机上的输入设备信号需要对计算机上运行的程序普遍可用。操作系统的设备驱动程序是服务器,接收物理信号并将它们作为可用的消息提供给客户端。而这些程序是客户端,从服务器中获取鼠标和键盘输入数据。此外,中央处理单元(CPU)和专用图形处理单元(GPU)通常以客户端/服务器体系结构参与,其中 CPU 作为客户端,GPU 作为图像的服务器。
101 |
102 | 客户端/服务器系统的一个缺点是服务器是单点故障。服务器是唯一具有提供服务能力的组件。可以有任意数量的客户端,这些客户端是可以互换的,并且可以根据需要随时进入或退出。
103 |
104 | 客户端/服务器系统的另一个缺点是,如果有太多的客户端,计算资源会变得稀缺。客户端增加了对系统的需求,而不贡献任何计算资源。
105 |
106 | ## 4.6.4 对等式系统(peer-to-peer System)
107 |
108 | 客户端/服务器模型适用于面向服务的情况。然而,对于其他计算目的来说,更平等的分工可能是更好的选择。在 "对等"(peer-to-peer)分布式系统中,运算工作分配给系统的所有组件。所有计算机都发送和接收数据,并且它们都贡献处理能力和内存。随着分布式系统的规模增加,它的计算资源能力也增加。在对等系统中,系统的所有组件都为分布式计算贡献处理能力和内存。
109 |
110 | 在所有参与者之间分工合作是对等系统的特征标志。这意味着参与者需要可靠地相互通信方法。为了确保消息能够到达预定目的地,对等系统需要具备有完备的网络结构。在这些系统中,各个组件合作维护关于其他组件位置的信息,以便将消息发送到预定目的地。
111 |
112 | 在某些对等系统中,维护网络的健康状态的任务由一组专门的组件承担。这样的系统并不是纯粹的对等系统,因为它们具有不同类型的组件,这些组件具有不同的功能。支持对等网络的组件起到了脚手架的作用。比如,它们帮助网络保持连接,维护有关不同计算机位置的信息,并帮助新加入的成员在其邻域内找到位置。
113 |
114 | Skype 是一个具有对等体架构的数据传输应用的示例。当不同计算机上的两个人进行 Skype 会话时,他们的通信通过一个对等网络传输。这个网络由运行 Skype 应用程序的其他计算机组成。每台计算机都知道其邻域内几台其他计算机的位置。一台计算机通过将数据包传递给一个邻居来帮助将其发送到目的地,邻居再将其传递给另一个邻居,以此类推,直到数据包达到预定的目的地。Skype 并不是一个纯粹的对等系统。一个超级节点的脚手架网络负责登录和注销用户,维护有关其计算机位置的信息,并在用户进入和退出时修改网络结构。
115 |
--------------------------------------------------------------------------------
/sicp/4/7.md:
--------------------------------------------------------------------------------
1 | # 4.7 分布式数据处理
2 |
3 | ::: details INFO
4 | 译者:[Asandstar](https://github.com/asandstar)
5 |
6 | 来源:[4.7 Distributed Data Processing](https://www.composingprograms.com/pages/47-distributed-data-processing.html)
7 |
8 | 对应:无
9 | :::
10 |
11 | 分布式系统通常用于收集、访问和处理大型数据集。例如,本章前面介绍的数据库系统可以对存储在多台机器上的数据集进行操作。任何一台机器都可能不包含响应查询所需的数据,因此需要进行通信来处理请求。
12 |
13 | 本节将探讨一种典型的大数据处理场景,即数据集过大,无法由单台机器处理,而是分布在多台机器上,每台机器处理数据集的一部分。处理结果通常必须跨机器汇总,以便将一台机器的计算结果与其他机器的计算结果结合起来。为了协调这种分布式数据处理,我们将讨论一种名为[MapReduce](https://en.wikipedia.org/wiki/MapReduce)的编程框架。
14 |
15 | 使用 MapReduce 创建分布式数据处理应用程序结合了本文中介绍的许多思想。应用程序用纯函数来表示,这些函数用于映射(map)大型数据集,然后将映射的值序列还原(reduce)成最终结果。
16 |
17 | 函数式编程中的熟悉概念在 MapReduce 程序中得到了最大程度的应用。MapReduce 要求用于映射和还原数据的函数必须是纯函数。一般来说,仅用纯函数表示的程序在执行方式上具有相当大的灵活性。子表达式可以按照任意顺序并行计算,而不会影响最终结果。MapReduce 应用程序会并行评估许多纯函数,重新安排计算顺序,以便在分布式系统中高效执行。
18 |
19 | MapReduce 的主要优势在于,它能在分布式数据处理应用程序的两个部分之间实现关注点分离:
20 |
21 | 1. 处理数据和合并结果的映射和还原函数。
22 | 2. 机器之间的通信和协调。
23 |
24 | 协调机制可以处理分布式计算中出现的许多问题,如机器故障、网络故障和进度监控。虽然管理这些问题会给 MapReduce 应用程序带来一些复杂性,但这些复杂性都不会暴露给应用程序开发人员。相反,构建 MapReduce 应用程序只需指定上述(1)中的映射和还原函数即可;分布式计算的挑战通过抽象被隐藏起来。
25 |
26 | ## 4.7.1 MapReduce
27 |
28 | MapReduce 框架假定输入是任意类型的大量无序输入值流。例如,每个输入可能是某个庞大语料库中的一行文本。计算分三步进行。
29 |
30 | 1. 对每个输入应用映射函数,输出零个或多个任意类型的中间键值对。
31 | 2. 所有中间键值对都按键分组,因此键值相同的键值对可以被还原在一起。
32 | 3. 还原函数合并给定键 `k` 的值;它输出零个或多个值,每个值在最终输出中都与 `k` 相关联。
33 |
34 | 为了执行这一计算,MapReduce 框架创建了在计算中扮演不同角色的任务(可能在不同的机器上)。映射任务(map task)将映射函数应用于输入数据的某些子集,并输出中间键值对。还原(reduce)任务按键对键值进行排序和分组,然后对每个键的值应用还原函数。映射任务和还原任务之间的所有通信都由框架处理,按键对中间键值对进行分组的任务也是如此。
35 |
36 | 为了在 MapReduce 应用程序中利用多台机器,多个映射器在映射阶段(map phase)并行运行,多个还原器在还原阶段(reduce phase)并行运行。在这两个阶段之间,排序阶段(sort phase)通过排序将键值对组合在一起,从而使所有具有相同键值的键值对都相邻。
37 |
38 | 考虑一下计算文本语料库中元音的问题。我们可以使用 MapReduce 框架并适当选择 map 和 reduce 函数来解决这个问题。map 函数将一行文本作为输入,并输出键值对,其中键是 vowel,值是 count。输出中省略了零计数:
39 |
40 | ```python
41 | def count_vowels(line):
42 | """A map function that counts the vowels in a line."""
43 | for vowel in 'aeiou':
44 | count = line.count(vowel)
45 | if count > 0:
46 | emit(vowel, count)
47 | ```
48 |
49 | reduce 函数是 Python 内置的求和函数,它的输入是值的迭代器(给定键的所有值),并返回它们的和。
50 |
51 | ## 4.7.2 本地实现
52 |
53 | 要指定 MapReduce 应用程序,我们需要一个 MapReduce 框架的实现,我们可以在其中插入 map 和 reduce 函数。在下一节中,我们将使用开源的[Hadoop](https://en.wikipedia.org/wiki/Apache_Hadoop)实现。在本节中,我们将使用 Unix 操作系统的内置工具开发一个最小的实现。
54 |
55 | Unix 操作系统在用户程序和计算机底层硬件之间建立了一个抽象屏障。它为程序之间的通信提供了一种机制,特别是允许一个程序使用另一个程序的输出作为输入。在他们关于 Unix 编程的开创性文章中,Kernigham 和 Pike 断言:""系统的力量更多来自于程序之间的关系,而不是程序本身"。
56 |
57 | 在 Python 源文件的第一行添加注释,说明程序应使用 Python 3 解释器执行,就可以将 Python 源文件转换为 Unix 程序。Unix 程序的输入是一个可迭代对象,称为标准输入,访问方式为 `sys.stdin`。对这个对象进行迭代会产生字符串值的文本行。Unix 程序的输出称为标准输出(standard output),访问方式为 `sys.stdout`。内置的 `print` 函数将一行文本写入标准输出。下面的 Unix 程序将其输入的每一行反向输出:
58 |
59 | ```python
60 | #!/usr/bin/env python3
61 |
62 | import sys
63 |
64 | for line in sys.stdin:
65 | print(line.strip('\n')[::-1])
66 | ```
67 |
68 | 如果我们将该程序保存到名为 `rev.py` 的文件中,就可以将其作为 Unix 程序执行。首先,我们需要告诉操作系统,我们创建了一个可执行程序:
69 |
70 | ```shell
71 | $ chmod u+x rev.py
72 | ```
73 |
74 | 接下来,我们可以向这个程序传递输入信息。一个程序的输入可以来自另一个程序。使用 | 符号(称为 "管道")就可以达到这种效果,它将管道前程序的输出导入管道后程序。程序 `nslookup` 输出 IP 地址的主机名(本例中为《纽约时报》):
75 |
76 | ```shell
77 | $ nslookup 170.149.172.130 | ./rev.py
78 | moc.semityn.www
79 | ```
80 |
81 | cat 程序会输出文件内容。因此,`rev.py` 程序可以用来反转 `rev.py` 文件的内容:
82 |
83 | ```shell
84 | $ cat rev.py | ./rev.py
85 | 3nohtyp vne/nib/rsu/!#
86 |
87 | sys tropmi
88 |
89 | :nidts.sys ni enil rof
90 | )]1-::[)'n\'(pirts.enil(tnirp
91 | ```
92 |
93 | 这些工具足以让我们实现一个基本的 MapReduce 框架。这个版本只有单个 map 任务和单个 reduce 任务,它们都是用 Python 实现的 Unix 程序。我们使用以下命令运行整个 MapReduce 应用程序:
94 |
95 | ```shell
96 | $ cat input | ./mapper.py | sort | ./reducer.py
97 | ```
98 |
99 | `mapper.py` 和 `reducer.py` 程序必须实现 map 函数和 reduce 函数,以及一些简单的输入和输出行为。例如,为了实现上述元音计数应用程序,我们将编写以下程序 `count_vowels_mapper.py` 程序:
100 |
101 | ```python
102 | #!/usr/bin/env python3
103 |
104 | import sys
105 | from mr import emit
106 |
107 | def count_vowels(line):
108 | """A map function that counts the vowels in a line."""
109 | for vowel in 'aeiou':
110 | count = line.count(vowel)
111 | if count > 0:
112 | emit(vowel, count)
113 |
114 | for line in sys.stdin:
115 | count_vowels(line)
116 | ```
117 |
118 | 此外,我们还要编写以下 `sum_reducer.py` 程序:
119 |
120 | ```python
121 | #!/usr/bin/env python3
122 |
123 | import sys
124 | from mr import values_by_key, emit
125 |
126 | for key, value_iterator in values_by_key(sys.stdin):
127 | emit(key, sum(value_iterator))
128 | ```
129 |
130 | [mr module](https://www.composingprograms.com/examples/mapreduce/mr.py)是本文的配套模块,它提供了 `emit` 函数和 `group_values_by_key` 函数,前者用于发射键值对,后者用于将具有相同键值的值分组。该模块还包含 MapReduce 的 Hadoop 分布式实现的接口。
131 |
132 | 最后,假设我们有以下名为 `haiku.txt` 的输入文件:
133 |
134 | ```shell
135 | Google MapReduce
136 | Is a Big Data framework
137 | For batch processing
138 | ```
139 |
140 | 通过使用 Unix 管道进行本地执行,我们可以计算出 haiku 中每个元音的数量:
141 |
142 | ```shell
143 | $ cat haiku.txt | ./count_vowels_mapper.py | sort | ./sum_reducer.py
144 | 'a' 6
145 | 'e' 5
146 | 'i' 2
147 | 'o' 5
148 | 'u' 1
149 | ```
150 |
151 | ## 4.7.3 分布式实现
152 |
153 | [Hadoop](https://en.wikipedia.org/wiki/Apache_Hadoop) 是 MapReduce 框架开源实现的名称,可在机器集群上执行 MapReduce 应用程序,为高效并行处理分配输入数据和计算。它的流接口允许任意的 Unix 程序定义 map 和 reduce 函数。事实上,我们的 `count_vowels_mapper.py` 和 `sum_reducer.py` 可直接用于 Hadoop 安装,以计算大型文本语料库中的元音数量。
154 |
155 | 与我们简单的本地 MapReduce 实现相比,Hadoop 有几个优势。首先是速度:在不同的机器上同时运行不同的任务,并行应用 map 和 reduce 函数。第二是容错:当一个任务因故失败时,其结果可由另一个任务重新计算,以完成整体计算。第三是监控:该框架提供了一个用户界面,用于跟踪 MapReduce 应用程序的进度。
156 |
157 | 要使用提供的 `mapreduce.py` 模块运行元音计数应用程序,请安装 Hadoop,将 `HADOOP` 的赋值语句更改为本地安装的根目录,将文本文件集复制到 Hadoop 分布式文件系统中,然后运行:
158 |
159 | ```shell
160 | $ python3 mr.py run count_vowels_mapper.py sum_reducer.py [input] [output]
161 | ```
162 |
163 | 其中,`[input]` 和 `[output]` 是 Hadoop 文件系统中的目录。
164 |
165 | 有关 Hadoop 流接口和系统使用的更多信息,请查阅 [Hadoop Streaming Documentation](https://hadoop.apache.org/docs/stable/hadoop-streaming/HadoopStreaming.html)。
166 |
--------------------------------------------------------------------------------
/sicp/4/8.md:
--------------------------------------------------------------------------------
1 | # 4.8 并行计算
2 |
3 | ::: details INFO
4 | 译者:[sumingfirst](https://github.com/sumingfirst)
5 |
6 | 来源:[4.8 Parallel Computing](https://www.composingprograms.com/pages/48-parallel-computing.html)
7 |
8 | 对应:无
9 | :::
10 |
11 | 从 1970 年代到 2000 年代中期,单个处理器内核的速度呈指数级增长。这种速度的提高大部分是通过增加时钟频率(处理器执行基本操作的速率)来实现的。然而,在 2000 年代中期,由于功率和发热限制,这种指数增长突然结束,从那时起,单个处理器内核的速度增长要慢得多。相反,CPU 制造商开始在单个处理器中放置多个核心,从而可以同时执行更多操作。
12 |
13 | 并行性并不是一个新概念。大型并行机已经使用了几十年,主要用于科学计算和数据分析。即使在具有单个处理器内核的个人计算机中,操作系统和解释器也提供了并发的抽象。这是通过上下文切换或在不同任务之间快速切换来完成的,而无需等待它们完成。因此,即使一台机器上只有一个处理核心,多个程序可以在其同时运行。
14 |
15 | 鉴于当前处理器内核数量增加的趋势,为了更快的运行,各个应用程序现在必须利用并行性。在单个程序内部,必须安排计算,使尽可能多的工作可以并行完成。但是,并行性在编写正确代码方面引入了新的挑战,尤其是在存在共享的可变状态的情况下。
16 |
17 | 对于可以在函数模型中有效解决的问题,没有共享的可变状态,并行性几乎不会带来任何问题。纯函数提供引用透明性,这意味着表达式可以替换为其值,反之亦然,而不会影响程序的行为。这样就可以并行计算不相互依赖的表达式。正如前一节所讨论的,MapReduce 框架允许使用最小的编程工作量来指定并行运行的功能性程序。
18 |
19 | 不幸的是,并非所有问题都可以使用函数式编程有效地解决。伯克利视图项目已经确定了科学和工程中的 [十三种常见计算模式](https://view.eecs.berkeley.edu/wiki/Dwarf_Mine),其中只有一种是 MapReduce。其余模式需要共享状态。
20 |
21 | 在本节的其余部分,我们将看到可变共享状态如何将错误引入并行程序,以及许多防止此类错误的方法。我们将在两个应用程序的上下文中研究这些技术,包括一个 [网络爬虫](https://www.composingprograms.com/examples/parallel/crawler.py.html) 和一个 [粒子模拟器](https://www.composingprograms.com/examples/parallel/particle.py.html)。
22 |
23 | ## 4.8.1 Python 中的并行性
24 |
25 | 在我们深入研究并行性的细节之前,让我们先探讨一下 Python 对并行计算的支持。Python 提供了两种并行执行方式:线程和多进程。
26 |
27 | **线程**:在 _线程_ 中,单个解释器中存在多个执行“线程”。每个线程独立于其他线程执行代码,尽管它们共享相同的数据。然而,CPython 解释器是 Python 的主要实现,一次只解释一个线程中的代码,在它们之间切换以提供并行的错觉。另一方面,解释器外部的操作(例如写入文件或访问网络)可以并行运行。
28 |
29 | `threading` 模块包含允许创建和同步线程的类。下面是一个多线程程序的简单示例:
30 |
31 | ```python
32 | >>> import threading
33 | >>> def thread_hello():
34 | other = threading.Thread(target=thread_say_hello, args=())
35 | other.start()
36 | thread_say_hello()
37 |
38 | >>> def thread_say_hello():
39 | print('hello from', threading.current_thread().name)
40 |
41 | >>> thread_hello()
42 | hello from Thread-1
43 | hello from MainThread
44 | ```
45 |
46 | `Thread` 构造函数创建一个新线程。它需要新线程运行的目标函数,以及该函数的参数。在 `Thread` 对象上调用 `start` 标志着它准备运行。该 `current_thread` 函数返回与当前执行线程关联的 `Thread ` 对象。
47 |
48 | 在此示例中,打印可以按任何顺序进行,因为我们尚未以任何方式同步它们。
49 |
50 | **多进程**:Python 还支持多进程,这允许程序生成多个解释器或进程,每个解释器或进程都可以独立运行代码。这些进程通常不共享数据,因此必须在进程之间通信任何共享状态。另一方面,进程根据底层操作系统和硬件提供的并行级别并行执行。因此,如果 CPU 有多个处理器核心,Python 进程可以真正并发运行。
51 |
52 | `multiprocessing` 模块包含用于创建和同步进程的类。以下是使用进程的 hello 示例:
53 |
54 | ```python
55 | >>> import multiprocessing
56 | >>> def process_hello():
57 | other = multiprocessing.Process(target=process_say_hello, args=())
58 | other.start()
59 | process_say_hello()
60 |
61 | >>> def process_say_hello():
62 | print('hello from', multiprocessing.current_process().name)
63 |
64 | >>> process_hello()
65 | hello from MainProcess
66 | >>> hello from Process-1
67 | ```
68 |
69 | 如本示例所示,`多进程` 中的许多类和函数类似于 `线程` 中的类和函数。此示例还演示了缺少同步如何影响共享状态,因为显示可以被视为共享状态。在这里,来自交互式进程的解释器的提示出现在另一个进程的打印输出之前。
70 |
71 | ## 4.8.2 共享状态的问题
72 |
73 | 为了进一步说明共享状态的问题,让我们看一个在两个线程之间共享的计数器的简单示例:
74 |
75 | ```python
76 | import threading
77 | from time import sleep
78 |
79 | counter = [0]
80 |
81 | def increment():
82 | count = counter[0]
83 | sleep(0) # try to force a switch to the other thread
84 | counter[0] = count + 1
85 |
86 | other = threading.Thread(target=increment, args=())
87 | other.start()
88 | increment()
89 | print('count is now: ', counter[0])
90 | ```
91 |
92 | 在此程序中,两个线程尝试递增同一计数器。CPython 解释器几乎可以随时在线程之间切换。只有最基本的操作是原子的,这意味着它们似乎是即时发生的,在评估或执行期间不可能切换。递增计数器需要多个基本操作:读取旧值、向其添加一个值和写入新值。解释器可以在这些操作中的任何一个之间切换线程。
93 |
94 | 为了显示当解释器在错误的时间切换线程时会发生什么,我们尝试通过休眠 0 秒来强制切换。运行此代码时,解释器通常会在调用时 `sleep` 切换线程。这可能会导致以下操作序列:
95 |
96 | ```python
97 | Thread 0 Thread 1
98 | read counter[0]: 0
99 | read counter[0]: 0
100 | calculate 0 + 1: 1
101 | write 1 -> counter[0]
102 | calculate 0 + 1: 1
103 | write 1 -> counter[0]
104 | ```
105 |
106 | 最终结果是计数器的值为 1,即使它增加了两次!更糟糕的是,解释器可能很少在错误的时间切换,这使得调试变得困难。即使有调用 `sleep` ,此程序有时会生成正确的计数 2,有时生成不正确的计数 1。
107 |
108 | 仅当存在共享数据时,才会出现此问题,共享数据可能被一个线程更改,而另一个线程访问它。这种冲突称为**争用条件**,它是仅存在于平行世界中的 bug 示例。
109 |
110 | 为了避免争用条件,必须防止可能被多个线程更改和访问的共享数据。例如,如果我们可以确保线程 1 仅在线程 0 完成访问后访问计数器,反之亦然,我们可以保证计算出正确的结果。我们说共享数据如果受到并发访问保护,则共享数据是**同步**的。在接下来的几个小节中,我们将看到多种提供同步的机制。
111 |
112 | ## 4.8.3 不需要同步时
113 |
114 | 在某些情况下,如果并发访问不会导致不正确的行为,则不需要同步对共享数据的访问。最简单的示例是只读数据。由于此类数据永远不会发生突变,因此所有线程将始终读取相同的值,无论它们何时访问数据。
115 |
116 | 在极少数情况下,发生突变的共享数据可能不需要同步。但是,要了解何时适用这种情况,需要深入了解解释器以及底层软件和硬件的工作原理。考虑以下示例:
117 |
118 | ```python
119 | items = []
120 | flag = []
121 |
122 | def consume():
123 | while not flag:
124 | pass
125 | print('items is', items)
126 |
127 | def produce():
128 | consumer = threading.Thread(target=consume, args=())
129 | consumer.start()
130 | for i in range(10):
131 | items.append(i)
132 | flag.append('go')
133 |
134 | produce()
135 | ```
136 |
137 | 在这里,生产者线程将 items 添加到 `items` ,而消费者等到 `flag` 为非空。当生产者完成添加 items 时,它会向 `flag` 添加一个元素,允许消费者使用。
138 |
139 | 在大多数 Python 实现中,此示例将正常工作。但是,在其他编译器和解释器甚至硬件本身上,一个常见的优化是重新排序在单个线程内不依赖于彼此数据的操作。在这样的系统中,语句 `flag.append ('go')` 可能会被移到循环之前,因为它们之间都不依赖于对方的数据。通常,除非确定底层系统不会对相关操作重新排序,应避免使用此类代码。
140 |
141 | ## 4.8.4 同步数据结构
142 |
143 | 同步共享数据的最简单方法是使用提供同步操作的数据结构。`queue` 模块包含一个 `Queue` 类,该类提供同步的先进先出(FIFO)访问数据。`Put` 方法将 items 添加到 `Queue` , `get` 方法检索 items。`Queue` 类本身确保这些方法同步,因此无论线程操作如何交错,items 都不会丢失。下面是一个生产者/消费者示例,它使用 `Queue` :
144 |
145 | ```python
146 | from queue import Queue
147 |
148 | queue = Queue()
149 |
150 | def synchronized_consume():
151 | while True:
152 | print('got an item:', queue.get())
153 | queue.task_done()
154 |
155 | def synchronized_produce():
156 | consumer = threading.Thread(target=synchronized_consume, args=())
157 | consumer.daemon = True
158 | consumer.start()
159 | for i in range(10):
160 | queue.put(i)
161 | queue.join()
162 |
163 | synchronized_produce()
164 | ```
165 |
166 | 除了使用 `Queue`、`get` 和 `put` 函数,此代码还进行了一些更改。我们已将消费者线程标记为 _守护进程_,这意味着程序退出之前不会等待该线程完成。这允许我们在消费者中使用无限循环。但是,我们确实需要确保主线程退出,但只有在从队列中消耗了所有 items 之后才退出。消费者调用 `task_done` 方法来通知 Queue 它已经完成了一个项目的处理,主线程调用 `join` 方法,直到所有项目都被处理完,确保程序只有在处理完之后才退出。
167 |
168 | 使用一个 `Queue` 更复杂的示例是 [并行网络爬虫](https://www.composingprograms.com/examples/parallel/crawler.py.html),它搜索网站上的死链接。这个爬虫跟踪同一站点托管的所有链接,因此它必须处理多个 URL,不断向 `Queue` 添加新的 URL 并删除要进行处理的 URL。通过使用同步 `Queue`,多个线程可以安全地同时添加和删除数据结构。
169 |
170 | ## 4.8.5 锁
171 |
172 | 当特定数据结构的同步版本不可用时,我们必须提供自己的同步。锁是执行此操作的基本机制。它最多可以由一个线程获取,之后没有其他线程可以获取它,直到它被先前获取它的线程释放。
173 |
174 | 在 Python 中,`threading` 模块包含一个提供锁定 `Lock` 的类。 `Lock` 具有 `acquire` 和 `release` 方法来获取和释放锁,并且该类保证一次只有一个线程可以获取它。当锁已经被持有时,所有其他试图获取锁的线程都被迫等待,直到锁被释放。
175 |
176 | 为了让锁保护特定的一组数据,所有的线程都需要遵循一个规则:除非持有那个特定的锁,否则任何线程都不能访问共享数据。实际上,所有的线程都需要将他们对共享数据的操作 "包装" 在获取和释放锁的调用中。
177 |
178 | 在 [并行网络爬虫](https://www.composingprograms.com/examples/parallel/crawler.py.html) 中,使用一个集合跟踪任何线程遇到的所有 URL,以避免多次处理特定 URL(并可能陷入一个循环)。但是,Python 不提供同步集合,因此我们必须使用锁来保护对普通集合的访问:
179 |
180 | ```python
181 | seen = set()
182 | seen_lock = threading.Lock()
183 |
184 | def already_seen(item):
185 | seen_lock.acquire()
186 | result = True
187 | if item not in seen:
188 | seen.add(item)
189 | result = False
190 | seen_lock.release()
191 | return result
192 | ```
193 |
194 | 这里需要一个锁,以防止在该线程检查该 URL 是否在集合中和将其添加到集合之间,另一个线程将其添加到集合中。此外,向集合添加不是原子的,因此并发地尝试向集合添加可能会破坏其内部数据。
195 |
196 | 在此代码中,我们必须小心,直到我们释放锁后才返回。通常,我们必须确保在不再需要时释放锁。这可能非常容易出错,尤其是在存在异常的情况下,因此 Python 提供了一个 with 复合语句来处理为我们获取和释放锁:
197 |
198 | ```python
199 | def already_seen(item):
200 | with seen_lock:
201 | if item not in seen:
202 | seen.add(item)
203 | return False
204 | return True
205 | ```
206 |
207 | `with` 语句确保在执行其套件之前获取 `seen_lock`,并且在由于任何原因退出套件时释放它。(with 语句实际上可以用于锁定以外的操作,不过我们在这里不讨论其他用途。)
208 |
209 | 必须相互同步的操作必须使用相同的锁。但是,必须只与同一操作集中的操作同步的两个不相交的操作集,应该使用两个不同的锁对象,以避免过度同步。
210 |
211 | ## 4.8.6 屏障
212 |
213 | 避免共享数据访问冲突的另一种方法是将程序分为多个阶段,确保共享数据在没有其他线程访问它的阶段中发生变化。屏障通过要求所有线程先到达该程序才能继续执行,将程序划分为多个阶段。在屏障之后执行的代码不能与屏障之前执行的代码并发。
214 |
215 | 在 Python 中,`threading` 模块以 `Barrier` 实例 `wait` 方法的形式提供了一个屏障:
216 |
217 | ```python
218 | counters = [0, 0]
219 | barrier = threading.Barrier(2)
220 |
221 | def count(thread_num, steps):
222 | for i in range(steps):
223 | other = counters[1 - thread_num]
224 | barrier.wait() # wait for reads to complete
225 | counters[thread_num] = other + 1
226 | barrier.wait() # wait for writes to complete
227 |
228 | def threaded_count(steps):
229 | other = threading.Thread(target=count, args=(1, steps))
230 | other.start()
231 | count(0, steps)
232 | print('counters:', counters)
233 |
234 | threaded_count(10)
235 | ```
236 |
237 | 在此示例中,读取和写入共享数据发生在不同的阶段,由屏障隔开。写入发生在同一阶段,但它们是不相交的; 这种不相交对于避免在同一阶段并发写入相同数据是必要的。由于此代码已正确同步,因此两个计数器在末尾将始终为 10。
238 |
239 | [多线程粒子模拟器](https://www.composingprograms.com/examples/parallel/particle.py.html) 以类似的方式使用屏障来同步对共享数据的访问。在模拟中,每个线程都拥有许多粒子,所有这些粒子在许多离散的时间步长过程中相互作用。粒子具有位置、速度和加速度,并且根据其他粒子的位置在每个时间步长中计算新的加速度。粒子的速度必须相应地更新,其位置也必须根据其速度进行更新。
240 |
241 | 与上面的简单示例一样,有一个读取阶段,其中所有粒子的位置都由所有线程读取。每个线程在此阶段更新其自身粒子的加速,但由于这些是不相交的写入,因此不需要同步它们。在写入阶段,每个线程都会更新其自身粒子的速度和位置。同样,这些是不相交的写入,它们通过屏障免受读取阶段的影响。
242 |
243 | ## 4.8.7 消息传递
244 |
245 | 最后一种避免共享数据不当变化的机制是完全避免对相同数据的并发访问。在 Python 中,使用多进程而不是线程自然会导致这种情况,因为进程在单独的解释器中运行,具有自己的数据。多个进程所需的任何状态都可以通过在进程之间传递消息来传达。
246 |
247 | `multiprocessing` 模块中的 `Pipe` 类提供了进程之间的通信通道。默认情况下,它是双工的,即双向通道,但传入参数 `False` 将导致单向通道。`Send` 方法通过通道发送一个对象,而 `recv` 方法接收一个对象。后者是阻塞的,意味着调用 `recv` 的进程将等待,直到接收到对象。
248 |
249 | 以下是使用进程和管道的生产者/消费者示例:
250 |
251 | ```python
252 | def process_consume(in_pipe):
253 | while True:
254 | item = in_pipe.recv()
255 | if item is None:
256 | return
257 | print('got an item:', item)
258 |
259 | def process_produce():
260 | pipe = multiprocessing.Pipe(False)
261 | consumer = multiprocessing.Process(target=process_consume, args=(pipe[0],))
262 | consumer.start()
263 | for i in range(10):
264 | pipe[1].send(i)
265 | pipe[1].send(None) # done signal
266 |
267 | process_produce()
268 | ```
269 |
270 | 在此示例中,我们使用消息 `None` 来表示通信结束。我们还在创建消费者进程时,将管道的一端作为参数传递给目标函数。这是必要的,因为状态必须在进程之间显式共享。
271 |
272 | [粒子模拟器](https://www.composingprograms.com/examples/parallel/particle.py.html) 的多进程版本使用管道在每个时间步中的进程之间传递粒子位置。事实上,它使用管道在进程之间建立整个循环管道,以最小化通信。每个过程都将自己的粒子位置注入其管道阶段,最终通过管道的完整旋转。在每次旋转的步骤中,过程都会将当前处于其自身管道阶段的位置的力施加到其自身的粒子上,因此在完全旋转后,所有力都已施加到其粒子上。
273 |
274 | `Multiprocessing` 模块为进程提供了其他同步机制,包括同步队列、锁,以及从 Python 3.3 开始的 barrier。例如,可以使用锁或屏障将打印同步到屏幕,从而避免我们前面看到的不正确的显示输出。
275 |
276 | ## 4.8.8 同步陷阱
277 |
278 | 虽然同步方法对于保护共享状态很有效,但它们也可能被错误地使用,无法完成正确的同步、过度同步或导致程序由于死锁而挂起。
279 |
280 | **欠同步**。并行计算的一个常见缺陷是忽略正确同步共享访问。在 set 示例中,我们需要将成员资格检查和插入同步在一起,以便另一个线程无法在这两个操作之间执行插入。即使这两个操作是单独同步的,但未能将两个操作同步在一起也是错误的。
281 |
282 | **过度同步**。另一个常见错误是过度同步程序,使得不冲突的操作不能并发发生。举个简单的例子,我们可以通过在线程启动时获取主锁并仅在线程完成时释放它来避免对共享数据的所有冲突访问。这将序列化我们的整个代码,因此没有并行运行。在某些情况下,这甚至可能导致我们的程序无限期挂起。例如,考虑一个消费者/生产者程序,其中消费者获得锁并且从不释放它。这可以防止生产者生产任何物品,这反过来又阻止了消费者做任何事情,因为它没有什么可消费的东西。
283 |
284 | 虽然这个例子很简单,但在实践中,程序员经常在某种程度上过度同步他们的代码,从而阻止他们的代码充分利用可用的并行性。
285 |
286 | **死锁**。由于同步机制会导致线程或进程相互等待,因此很容易出现死锁,即两个或多个线程或进程卡住,等待彼此完成的情况。我们刚刚看到了忽略释放锁如何导致线程无限期地卡住。但是,即使线程或进程正确释放了锁,程序仍可能达到死锁。
287 |
288 | 死锁的来源是循环等待,如下图所示的进程。任何进程都无法继续,因为它正在等待等待它完成的其他进程。
289 |
290 | 
291 |
292 | 例如,我们将设置一个包含两个进程的死锁。假设它们共享一个双工管道,并尝试相互通信,如下所示:
293 |
294 | ```python
295 | def deadlock(in_pipe, out_pipe):
296 | item = in_pipe.recv()
297 | print('got an item:', item)
298 | out_pipe.send(item + 1)
299 |
300 | def create_deadlock():
301 | pipe = multiprocessing.Pipe()
302 | other = multiprocessing.Process(target=deadlock, args=(pipe[0], pipe[1]))
303 | other.start()
304 | deadlock(pipe[1], pipe[0])
305 |
306 | create_deadlock()
307 | ```
308 |
309 | 两个进程都尝试首先接收数据。回想一下, `recv` 方法会阻塞直到某个项可用。由于两个进程都没有发送任何内容,因此两个进程都将无限期地等待另一个进程向其发送数据,从而导致死锁。
310 |
311 | 同步操作必须正确对齐以避免死锁。这可能需要在接收之前发送管道,以相同的顺序获取多个锁,并确保所有线程在正确的时间到达正确的屏障。
312 |
313 | ## 4.8.9 结论
314 |
315 | 正如我们所看到的,并行性为编写正确和高效的代码带来了新的挑战。随着硬件层面上并行性的增加趋势在可预见的未来将持续下去,并行计算在应用程序编程中将变得越来越重要。目前有一个非常活跃的研究领域致力于让程序员更轻松地实现并行性并减少出错的可能性。我们在这里的讨论仅作为对这一计算机科学的关键领域的基本介绍。
316 |
--------------------------------------------------------------------------------
/sicp/index.md:
--------------------------------------------------------------------------------
1 | # COMPOSING PROGRAMS
2 |
3 | 这是一个翻译项目,原书为伯克利 CS61A 的配套教材 [Composing Programs](https://www.composingprograms.com/),也是计算机一大圣经 [SICP](https://book.douban.com/subject/1148282/) 的 Python 版本。
4 |
5 | 项目现在为维护状态,如果您发现了翻译的错漏或含混之处,可以提交 issue 或者 PR,我会抽时间 review 或者修改。同时感谢所有参与翻译的同学,也感谢每一个给项目 star 的同学。
6 |
7 | ## 贡献者列表
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/sicp/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/sicp/public/sicp/call_expression.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/call_expression.png
--------------------------------------------------------------------------------
/sicp/public/sicp/celsius_fahrenheit_constraint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/celsius_fahrenheit_constraint.png
--------------------------------------------------------------------------------
/sicp/public/sicp/constraints.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/constraints.png
--------------------------------------------------------------------------------
/sicp/public/sicp/curves.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/curves.png
--------------------------------------------------------------------------------
/sicp/public/sicp/deadlock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/deadlock.png
--------------------------------------------------------------------------------
/sicp/public/sicp/distributed_system.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/distributed_system.png
--------------------------------------------------------------------------------
/sicp/public/sicp/expression_tree.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/expression_tree.png
--------------------------------------------------------------------------------
/sicp/public/sicp/factorial_machine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/factorial_machine.png
--------------------------------------------------------------------------------
/sicp/public/sicp/fib.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/fib.png
--------------------------------------------------------------------------------
/sicp/public/sicp/fib_memo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/fib_memo.png
--------------------------------------------------------------------------------
/sicp/public/sicp/function_abs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/function_abs.png
--------------------------------------------------------------------------------
/sicp/public/sicp/function_print.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/function_print.png
--------------------------------------------------------------------------------
/sicp/public/sicp/multiple_inheritance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/multiple_inheritance.png
--------------------------------------------------------------------------------
/sicp/public/sicp/newton.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/newton.png
--------------------------------------------------------------------------------
/sicp/public/sicp/pi_sum.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/pi_sum.png
--------------------------------------------------------------------------------
/sicp/public/sicp/set_trees.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/set_trees.png
--------------------------------------------------------------------------------
/sicp/public/sicp/sier.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/sier.png
--------------------------------------------------------------------------------
/sicp/public/sicp/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csfive/composing-programs-zh/4e90231aad267d2b2d4d758decaff51d45d0b854/sicp/public/sicp/star.png
--------------------------------------------------------------------------------