├── .gitignore
├── README-S2.md
├── README.md
├── assets
├── china-cellphone.png
├── comic-abstract.png
├── conemu-powershell-admin.png
├── conemu-powershell.png
├── conemu-settings.png
├── conemu.png
├── crlf-in-vsc.png
├── csv-data-in-vscode.png
├── flights.db
├── fsm-1.png
├── fsm-2.png
├── fsm-3.png
├── fsm-4.png
├── git-branches-1.png
├── git-branches-2.png
├── git-branches-3.png
├── git-branches-4.png
├── git-branches-5.png
├── git-branches-6.png
├── git-commit-message.png
├── git-diff.png
├── git-local-remote-repos.png
├── git-log.png
├── git-pull-push.png
├── git-states.png
├── git-time-machine.png
├── moma-artists.csv
├── mysql-data-import.png
├── mysql-workbench.png
├── myutil.py
├── python-repl.png
├── recursion-factorial.png
├── regex-visualization.svg
├── sample.txt
├── siri.png
├── tree-graph.png
└── vscode-install-cli.png
├── p1-1-understanding-programming-languages.ipynb
├── p1-2-structure-1.ipynb
├── p1-3-structure-2.ipynb
├── p1-4-structure-3.ipynb
├── p1-5-structure-4.ipynb
├── p1-6-structure-5.ipynb
├── p1-7-oo-1.ipynb
├── p1-8-oo-2.ipynb
├── p1-9-oo-3.ipynb
├── p1-a-string.ipynb
├── p1-b-final.ipynb
├── p2-1-function-def.ipynb
├── p2-2-docstrings.ipynb
├── p2-3-modules.ipynb
├── p2-4-recursion.ipynb
├── p2-5-functional-1.ipynb
├── p2-6-string-data.ipynb
├── p2-7-iterable-iterator.ipynb
├── p2-8-list.ipynb
├── p2-9-tuple-set-dict.ipynb
├── p2-a-tree.ipynb
├── p2-b-fsm.ipynb
├── p2-c-database.ipynb
├── p2-d-functional-2.ipynb
├── x1-setup.md
├── x2-students-book.md
├── x3-git-github.ipynb
├── x4-regex.ipynb
├── x5-mysql-setup.ipynb
└── x6-redis-setup.ipynb
/.gitignore:
--------------------------------------------------------------------------------
1 | .python-version
2 |
3 | .DS_Store
4 | .ipynb_checkpoints/
5 | __pycache__
6 | **/.DS_Store
7 | **/.ipynb_chechpoints
8 | **/__pycache__
9 | log.txt
10 | **/log.txt
--------------------------------------------------------------------------------
/README-S2.md:
--------------------------------------------------------------------------------
1 | # 进入编程世界 S2
2 |
3 | 相信你已注意到,我们已经更新了第二阶段(“进阶篇”)[课程](https://github.com/neolee/pilot),相应的[学习用书](https://github.com/neolee/pilot-student)也已同步更新。
4 |
5 | 在第一阶段(“基础篇”)你已经学习了:
6 | * 编程语言是怎么回事:在编译器和解释器的帮助下,我们可以写出计算机可以遵照执行的程序指令;
7 | * 程序的基本结构:值与变量,操作符与函数,逻辑判断与分支,循环等;
8 | * 运用抽象思维编写模块化的程序:编程的基本困难在于控制复杂度,通过面向对象等抽象方法,我们可以从小到大一点一点的构建起复杂的系统。
9 |
10 | 我们通过大量 Python 代码实例体验了这些要素,现在应该能够读懂简单的代码,也能依葫芦画瓢的写出一些简单的代码。如果能够独立完成第一阶段最后的课程练习,基本上就具备了继续学习的基础。
11 |
12 | 在第二阶段我们将学习编程中的进阶知识点,这些知识点都和两个东西有关:**函数**(*function*)和**数据**(*data*)。
13 |
14 | 我们之前讲过,写程序和讲故事很像,数据就是故事里的人物,而函数就是发生在人物身上的事情,他们说的话,他们做的事,以及他们彼此之前的互动。
15 |
16 | 写程序来解决一个问题,基本上就是两件事:
17 | * 找到一个合适的方法来表述这个问题,这叫**模型**(*model*),或者叫**数据结构**(*data structure*);
18 | * 写出一组函数来对这些数据进行处理,这叫**算法**(*algorithm*)。
19 |
20 | 这二者结合起来,就能得到问题的“解决方案”。
21 |
22 | 数据结构和算法是紧密联系、不可分割的,一般来说的思路是从理解问题入手,找到本质上最适合表达问题的数据结构,然后在实现算法的过程中不断优化调整。
23 |
24 | 优秀的编程语言会提供强大的语法工具来帮助我们编写各种各样的**函数**,也会提供大量内置**数据**类型,覆盖我们常用的数据结构,即使没覆盖到的,我们借助现成的函数和数据结构也可以自行实现。Python 在这两方面都做得很好。
25 |
26 | 在新的“进阶篇”中,你会先更深入地学习函数,然后循序渐进地学习一系列最重要的数据结构及其应用场景。在这个过程中自然离不开动手编程的实践,和对“编程思想”更深入的理解。
27 |
28 | 如果你还没有完成“基础篇”的学习,或者现在刚开始准备进入编程之门,也没有问题,无论之前的讲解、教材、学习用书还是支线课程,都可继续学习,遇到问题随时可在主仓库 [Issues](https://github.com/neolee/pilot/issues) 系统中查询和/或提出。
29 |
30 | 编程和里面蕴含的思维方法,是可以学习一辈子的本事。无论进度如何,只要持续学下去,就一定能到达编程世界的自由境界,*keep exploring and thinking.*
31 |
32 | > #### 重要提示
33 | > 之前已经 fork & clone [学习用书](https://github.com/neolee/pilot-student) 的话,需要按照 [GitHub 官方指南](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/syncing-a-fork) 来完成你的 repo 和新版学习用书之间的同步。
34 | >
35 | > 这个过程虽然不难,但也有点麻烦,如果实在弄不好,其实把之前你用的本地学习用书备份一下,然后删掉原本的 fork,再重新 fork & clone 新的学习用书就可以了。反正“描红本”描完就扔也没问题。
36 | >
37 | > **更新**:感谢 [zhipingyang120](https://github.com/zhipingyang120) 同学写了一篇中文的 step-by-step 的 [学习用书更新同步指南](https://github.com/neolee/pilot/issues/651),可以参考。
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 进入编程世界的第一课
2 |
3 | ## 更新
4 |
5 | 第二阶段课程(S2)已发布, 请阅读 [对 S2 课程的说明](README-S2.md)。
6 |
7 | ## 前言
8 |
9 | 这个仓库(*repo*)里保存的是**教材**,也就是需要你阅读和学习的内容。我们另外还有个 *repo* 是你的**学习用书**,我们会在下面告诉你如何使用这两个仓库。
10 |
11 | 编程(*programming*)并不算是一门**科学**,更像是一种**手艺**,里面有科学,有理论有思想,也有经验和体会。学习编程最有效的方法是——赶紧动手开始写程序,如果不自己动手,听再多、再牛的课程讲解也没用。
12 |
13 | 在动手写程序的前提下,优秀的课程才能发挥作用——那就是缩短你走弯路的时间,就好像学踢足球或者打高尔夫球,一开始就掌握**正确的姿势**很重要,之后就能够**事半功倍**。
14 |
15 | 这门课程就能够发挥这样的作用——但必须你亲手开始编程。
16 |
17 | 为了帮助你做到这一点,我们设计了一个学习方案,你的每个学习单元都由 **听课**、**自学** 和 **提问** 三个环节组成。
18 |
19 | #### 听课
20 |
21 | 这部分以讲解原理和思维方法为主,重点在“学会学习的方法(*learning to learn*)”和“问题解决的方法(*problem solving*)”。
22 |
23 | 课程的讲解大致上每周一次,在课程讲解的最后会给大家布置自学任务,就是下面这个环节。
24 |
25 | #### 自学
26 |
27 | 你要通过我们提供的教材和学习用书完成 **自学任务**。方法很简单:
28 | * 使用下面的课程大纲打开指定章节进行阅读;
29 | * 一边读一边在 **学习用书** 中完成代码的编写和运行。
30 |
31 | 我们的教材和学习用书都是用一种叫做 **Jupyter Notebook** 的格式编写的,这种格式编写的 *notebook* 中除了文字,还有**可运行的程序代码**(!)。
32 |
33 | 学习用书和教材的区别只有一个:在学习用书中,所有写着程序代码的格子(*cell*)都被清空了,等着你自己动手填进去。
34 |
35 | 所以学习用书就好像我们小时候学写字时用的“描红本”,你可以对着教材“描”和“抄”。不要小看了这个“描”和“抄”的过程,无数实践证明,自己输入一遍和光看就是不一样;而且,程序和描字不同,很多时候你可以自己修改一些地方,改的越多你就掌握的越多。
36 |
37 | 为了使用 **学习用书**,你首先需要配置好一个(你可以用一辈子的)编程环境,然后从我们的共享代码仓库获取学习用书并运行,具体来说按照下面的两个指引操作即可:
38 |
39 | 1. [编程环境配置指南](x1-setup.md)
40 | 2. [如何使用配套学习用书](x2-students-book.md)
41 |
42 | 自学过程中遇到问题是正常的,所以下面这个环节很重要。
43 |
44 | #### 提问
45 |
46 | 我们鼓励大家提问,在学习过程中遇到问题就及时提出来,及时解决。
47 |
48 | 万事开头难,一开始快速清障前进,后面就会越来越顺。
49 |
50 | 可以使用 GitHub 提供的 Issues 功能来提问,只要访问 [这个页面](https://github.com/neolee/pilot/issues),点击右上的 `New issue` 绿色按钮即可提出问题。
51 |
52 | 提问是有一些技巧的,经过思考的问题更容易得到靠谱答案。
53 |
54 | 关于这个问题,有个近乎标准的答案在网上已经存在很久了,那就是大牛 Eric S. Raymond 2001 年发表在 BBS 上的 [How To Ask Questions The Smart Way](http://www.catb.org/~esr/faqs/smart-questions.html),这篇文章从发表之后一直在不断更新修订,内容清晰详实,附有丰富的“好问题”和“蠢问题”样例,一看就明白;另有质量很不错的 [中文译文](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md)(也是开源的)。请务必仔细阅读,尽量按照里面的建议来思考和组织你的问题。
55 |
56 | ## 课程大纲
57 |
58 | #### 第一部分 基础篇
59 |
60 | * [第一章 理解编程语言](p1-1-understanding-programming-languages.ipynb)
61 | * [第二章 程序的基本结构(一):值与变量](p1-2-structure-1.ipynb)
62 | * [第三章 程序的基本结构(二):操作符与函数](p1-3-structure-2.ipynb)
63 | * [第四章 程序的基本结构(三):逻辑判断与分支](p1-4-structure-3.ipynb)
64 | * [第五章 程序的基本结构(四):循环](p1-5-structure-4.ipynb)
65 | * [第六章 程序的基本结构(五):异常处理](p1-6-structure-5.ipynb)
66 | * [第七章 理解对象与类:起源篇](p1-7-oo-1.ipynb)
67 | * [第八章 理解对象与类:概念篇](p1-8-oo-2.ipynb)
68 | * [第九章 理解对象与类:Python 篇](p1-9-oo-3.ipynb)
69 | * [第十章 字符与字符串](p1-a-string.ipynb)
70 | * [最十一章 课程练习](p1-b-final.ipynb)
71 |
72 | #### 第二部分 进阶篇
73 |
74 | * [第一章 函数定义再探](p2-1-function-def.ipynb)
75 | * [第二章 程序中的文档](p2-2-docstrings.ipynb)
76 | * [第三章 模块](p2-3-modules.ipynb)
77 | * [第四章 递归](p2-4-recursion.ipynb)
78 | * [第五章 函数也是数据:初级篇](p2-5-functional-1.ipynb)
79 | * [第六章 字符串数据](p2-6-string-data.ipynb)
80 | * [第七章 Iterable 与 Iterator](p2-7-iterable-iterator.ipynb)
81 | * [第八章 列表](p2-8-list.ipynb)
82 | * [第九章 元组,集合,字典](p2-9-tuple-set-dict.ipynb)
83 | * [第十章 树](p2-a-tree.ipynb)
84 | * [第十一章 有限状态机](p2-b-fsm.ipynb)
85 | * [第十二章 数据和数据库](p2-c-database.ipynb)
86 | * [第十三章 函数也是数据:进阶篇](p2-d-functional-2.ipynb)
87 |
88 | #### 附录
89 |
90 | 1. [编程环境配置指南](x1-setup.md)
91 | 2. [如何使用配套学习用书](x2-students-book.md)
92 | 3. [Git 与 GitHub 入门](x3-git-github.ipynb)
93 | 4. [正则表达式入门](x4-regex.ipynb)
94 | 5. [MySQL 配置指南](x5-mysql-setup.ipynb)
95 | 6. [Redis 配置指南](x6-redis-setup.ipynb)
96 |
97 | #### 重要的附注
98 |
99 | 由于 GitHub 的某些 bug,点击上面的这些链接打开 *notebook* 时可能会出现 `Sorry, something went wrong. Reload?` 的错误,这时可打开下面的链接,改用 Jupyter Notebook 官方提供的阅读器:
100 |
101 | > https://nbviewer.jupyter.org/github/neolee/pilot/tree/master/
102 |
103 | `ipynb` 后缀的文件就是 *notebook* 文件,文件名最开始的数字对应第几章(`a` 对应第十章,`b` 对应最终章,`x` 开头的则是附录)。
104 |
105 | 比较推荐的学习方式是,用一个浏览器窗口打开上面列出的课程内容,同时在你自己机器的学生用书目录下运行 `jupyter lab`,并在学生用书的对应 *notebook* 下自己输入代码尝试。
106 |
107 | 具体可参考这个 [视频指引](https://www.bilibili.com/video/av71399509/)。
108 |
109 | ## 问题与反馈
110 |
111 | 如果在学习过程中遇到问题或者发现教材中的错误,可以通过 GitHub 的 Issues 系统提出,这个系统基本上就像一个问答论坛,但它集成了一些功能,让它目的性更强、更容易跟踪问题解决的进度状态。
112 |
113 | 访问我们课程教材的 Issues 页面:
114 |
115 | > [https://github.com/neolee/pilot/issues](https://github.com/neolee/pilot/issues)
116 |
117 | 点击右上的 `New issue` 绿色按钮来提出问题或者反馈。
118 |
119 | 遇到问题的时候其实也可以到这个 Issues 页面去搜索一下,看看是不是有人提过,得到了怎样的答案;如果没人提过,那就正好可以由你来提出,所有人都会从中获益。
120 |
121 | 有些常见或者特别有价值的问题我们会整理出来放到课程项目的 [Wiki](https://github.com/neolee/pilot/wiki) 中,方便大家查阅。
--------------------------------------------------------------------------------
/assets/china-cellphone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/china-cellphone.png
--------------------------------------------------------------------------------
/assets/comic-abstract.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/comic-abstract.png
--------------------------------------------------------------------------------
/assets/conemu-powershell-admin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/conemu-powershell-admin.png
--------------------------------------------------------------------------------
/assets/conemu-powershell.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/conemu-powershell.png
--------------------------------------------------------------------------------
/assets/conemu-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/conemu-settings.png
--------------------------------------------------------------------------------
/assets/conemu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/conemu.png
--------------------------------------------------------------------------------
/assets/crlf-in-vsc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/crlf-in-vsc.png
--------------------------------------------------------------------------------
/assets/csv-data-in-vscode.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/csv-data-in-vscode.png
--------------------------------------------------------------------------------
/assets/flights.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/flights.db
--------------------------------------------------------------------------------
/assets/fsm-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/fsm-1.png
--------------------------------------------------------------------------------
/assets/fsm-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/fsm-2.png
--------------------------------------------------------------------------------
/assets/fsm-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/fsm-3.png
--------------------------------------------------------------------------------
/assets/fsm-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/fsm-4.png
--------------------------------------------------------------------------------
/assets/git-branches-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/git-branches-1.png
--------------------------------------------------------------------------------
/assets/git-branches-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/git-branches-2.png
--------------------------------------------------------------------------------
/assets/git-branches-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/git-branches-3.png
--------------------------------------------------------------------------------
/assets/git-branches-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/git-branches-4.png
--------------------------------------------------------------------------------
/assets/git-branches-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/git-branches-5.png
--------------------------------------------------------------------------------
/assets/git-branches-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/git-branches-6.png
--------------------------------------------------------------------------------
/assets/git-commit-message.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/git-commit-message.png
--------------------------------------------------------------------------------
/assets/git-diff.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/git-diff.png
--------------------------------------------------------------------------------
/assets/git-local-remote-repos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/git-local-remote-repos.png
--------------------------------------------------------------------------------
/assets/git-log.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/git-log.png
--------------------------------------------------------------------------------
/assets/git-pull-push.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/git-pull-push.png
--------------------------------------------------------------------------------
/assets/git-states.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/git-states.png
--------------------------------------------------------------------------------
/assets/git-time-machine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/git-time-machine.png
--------------------------------------------------------------------------------
/assets/mysql-data-import.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/mysql-data-import.png
--------------------------------------------------------------------------------
/assets/mysql-workbench.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/mysql-workbench.png
--------------------------------------------------------------------------------
/assets/myutil.py:
--------------------------------------------------------------------------------
1 | from math import sqrt
2 |
3 | def is_prime(n):
4 | """Return a boolean value based upon whether the argument n is a prime number."""
5 | if n < 2:
6 | return False
7 |
8 | if n in (2, 3):
9 | return True
10 |
11 | for i in range(2, int(sqrt(n)) + 1):
12 | if n % i == 0:
13 | return False
14 |
15 | return True
16 |
17 | def say_hi(*names, greeting='Hi', capitalized=False):
18 | """
19 | Print a string, with a greeting to everyone.
20 | :param *names: tuple of names to be greeted.
21 | :param greeting: 'Hello' as default.
22 | :param capitalized: Whether name should be converted to capitalized before print. False as default.
23 | :returns: None
24 | """
25 | for name in names:
26 | if capitalized:
27 | name = name.capitalize()
28 | print(f'{greeting}, {name}!')
--------------------------------------------------------------------------------
/assets/python-repl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/python-repl.png
--------------------------------------------------------------------------------
/assets/recursion-factorial.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/recursion-factorial.png
--------------------------------------------------------------------------------
/assets/sample.txt:
--------------------------------------------------------------------------------
1 |
2 | begin began begun begins beginning
3 | google gooogle goooogle goooooogle
4 | coloured color coloring colouring colored
5 | never ever verb however everest
6 | 520 52000 5200000 520000000 520000000000
7 | error wonderer achroiocythaemia achroiocythemia
8 | The white dog wears a black hat.
9 | Handel, Händel, Haendel
10 |
11 | (843) 542-4256
(431) 270-9664
12 | 3336741162
3454953965
13 |
14 | - peoplesr@live.com
- jaxweb@hotmail.com
15 | - dhwon@comcast.net
- krueger@me.com
16 |
17 | URLs
18 | https://docs.python.org/3/howto/regex.html
19 | https://docs.python.org/3/library/re.html
20 | passwords
21 | Pasw0rd~
22 | i*Eh,GF67E
23 | a$4Bh9XE&E
24 | duplicate words
25 | It's very very big.
26 | Keep it simple, simple, simple!
--------------------------------------------------------------------------------
/assets/siri.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/siri.png
--------------------------------------------------------------------------------
/assets/tree-graph.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/tree-graph.png
--------------------------------------------------------------------------------
/assets/vscode-install-cli.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/neolee/pilot/c89bab64df97045a02d4a63b875c0ab3a6f51762/assets/vscode-install-cli.png
--------------------------------------------------------------------------------
/p1-1-understanding-programming-languages.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# 理解编程语言"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "简单地说,程序就是人类书写交由机器去执行的一系列指令,而编程语言是书写时遵照的语法规则。\n",
15 | "\n",
16 | "编程很像教孩子,计算机就像一个孩子,一开始什么都不懂,也不会自己思考,但非常听话,你教TA怎样就怎样,一旦学会的就不会忘,可以不走样地做很多次不出错。但是如果教的不对,TA也会不折不扣照做。\n",
17 | "\n",
18 | "对我们来说,和孩子的教育一样,关键在于理解对方的沟通语言,以及理解对方会如何理解你的表达。"
19 | ]
20 | },
21 | {
22 | "cell_type": "markdown",
23 | "metadata": {},
24 | "source": [
25 | "## 计算机系统和 CPU"
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "metadata": {},
31 | "source": [
32 | "计算机本身并不复杂,大致上就是三部分:\n",
33 | "* 负责执行指令的**中央处理器(CPU)**;\n",
34 | "* 负责存放 CPU 执行所需要数据的**内存**;\n",
35 | "* 各种**外设**,像硬盘、键盘、鼠标、显示设备、网卡等等这些都是。\n",
36 | "\n",
37 | "这里面 CPU 是核心的核心,因为它相当于人的大脑,我们教给计算机的指令实际上就是 CPU 在一条一条地执行,执行过程中会读写内存里的数据,会调用各种外设的接口(通过一种叫做“设备驱动”的软件模块)来完成数据的输入输出(I/O)操作。\n",
38 | "\n",
39 | "CPU 是一种计算机芯片,芯片可以执行的指令是在设计时就定好的, 不同芯片有不同的指令集,固化在硬件里,一般无法改变。因为大小、发热和功耗等问题,芯片的指令集不能面面俱到,而是有所侧重,比如一般计算机里 CPU 芯片比较擅长整数的运算,而 GPU(图形处理芯片,也就是显卡)比较擅长浮点数的运算,计算机图形处理尤其是 3D 图形处理会涉及大量的浮点运算,所以通常交给 GPU 去做,又快又省电;后来人们发现除了 3D 图形处理,人工智能等领域的一些科学计算也多是浮点运算,所以 GPU 也被拿来做这些计算工作。而我们的手机里的芯片,是把 CPU/GPU 还有其他一些做特殊工作的芯片集成在一块硅晶片上,这样可以节省空间,也能节约功耗,这种高度集成的芯片就叫 SoC(*System on Chip*)。"
40 | ]
41 | },
42 | {
43 | "cell_type": "markdown",
44 | "metadata": {},
45 | "source": [
46 | "## 汇编和编译器"
47 | ]
48 | },
49 | {
50 | "cell_type": "markdown",
51 | "metadata": {},
52 | "source": [
53 | "那么 CPU 执行的代码长啥样呢,我们可以瞄一眼,大致长这样:\n",
54 | "\n",
55 | "```nasm\n",
56 | "_add: ## @add\n",
57 | "push rbp\n",
58 | "mov rbp, rsp\n",
59 | "mov dword ptr [rbp - 4], edi\n",
60 | "mov dword ptr [rbp - 8], esi\n",
61 | "mov esi, dword ptr [rbp - 4]\n",
62 | "add esi, dword ptr [rbp - 8]\n",
63 | "mov eax, esi\n",
64 | "pop rbp\n",
65 | "ret\n",
66 | "\n",
67 | "_main: ## @main\n",
68 | "push rbp\n",
69 | "mov rbp, rsp\n",
70 | "sub rsp, 16\n",
71 | "mov dword ptr [rbp - 4], 0\n",
72 | "mov edi, 27\n",
73 | "mov esi, 15\n",
74 | "call _add\n",
75 | "add rsp, 16\n",
76 | "pop rbp\n",
77 | "ret\n",
78 | "```\n",
79 | "\n",
80 | "这叫汇编指令(*assembly*),和 CPU 实际执行的“机器指令(*machine code*)”几乎一样了,只是机器语言是二进制的,全是 01011001 这样,而汇编将其翻译成了我们稍微看得懂的一些指令和名字。机器可以直接执行的指令其实不多,在 Intel 官方的 [x64 汇编文档](https://software.intel.com/en-us/articles/introduction-to-x64-assembly) 里列出的常用指令码只有 20 多个,上图中的 `mov` `push` `pop` `add` `sub` `call` `ret` 等都是。我们目前并不需要搞懂这些都是干啥的,不过可以简单说一下,`mov` 就是把一个地方保存的数写到另一个地方,`call` 是调用另外一个代码段,`ret` 是返回 `call` 的地方继续执行。\n",
81 | "\n",
82 | "所以其实计算机执行的指令大致就是:\n",
83 | "* 把数放到某地方,可能在 CPU 内部也可能在内存里;\n",
84 | "* 对某地方的数进行特定运算(通常是加减乘除);\n",
85 | "* 跳转到另一个地方执行一段指令;\n",
86 | "* 返回原来的位置继续执行。\n",
87 | "如此这般。\n",
88 | "\n",
89 | "可以想象,如果要完成复杂的任务,涉及复杂的逻辑流程,用汇编代码写起来可要费好大的劲儿了。但这就是机器的语言,CPU 实际上就只会这种语言,别的它都听不懂,怎么办?\n",
90 | "\n",
91 | "想来想去,是不是有可能我们用某种方式写下我们解决问题的方法,然后让计算机把这些方法翻译成上面那种计算机懂的指令呢?这种翻译的程序肯定不好写,但是一旦写好了以后所有人都可以用更接近人话的方式表达,计算机也能自己翻译给自己并且照做了,所谓辛苦一时享受一世,岂不美哉。程序员的典型思维就是这样:如果有个对人来说很麻烦的事情,就要试试看是不是可以让计算机来做。\n",
92 | "\n",
93 | "这个想法早已被证明完全可行,这样的翻译程序叫做“编译器(*compiler*)”。现在存在于世的编程语言有数百种,每一种都对应一个编译器,它可以读懂你用这语言写的程序,经过词法分析(*tokenizing*)、语法分析(*parsing*)、语义分析(*semantic analysis*)、优化(*optimization*)和代码生成(*code emission*)等环节,最终输出像上面那样机器可以看懂和执行的指令。\n",
94 | "\n",
95 | "编译理论和实现架构已经发展了很多年,已经非常成熟,并在不断优化中。只要愿意学习,任何人都可以设计自己的编程语言并为之实现一个编译器。\n",
96 | "\n",
97 | "顺便,上面的汇编代码是如下的 C 语言代码通过 LLVM/Clang 编译器的处理生成的:\n",
98 | "\n",
99 | "```c\n",
100 | "int add(int a, int b) {\n",
101 | " return a + b;\n",
102 | "}\n",
103 | "\n",
104 | "int main() {\n",
105 | " return add(27, 15);\n",
106 | "}\n",
107 | "```\n",
108 | "\n",
109 | "C 语言被公认是最接近机器语言的“高级编程语言”,因为 C 语言对内存数据的管理方式几乎和机器语言是一样的“手工操作”,需要编程者非常了解 CPU 对内存的管理模式。但尽管如此,C 语言还是高级编程语言,很接近我们人类的语言,基本上一看就懂。\n",
110 | "\n",
111 | "各种各样的高级编程语言,总有一样适合你,这些语言极大地降低了计算机编程的门槛,让几乎所有人都有机会编程,而这一切都是因为有编译器这样的“自动翻译”存在,建立了人类和机器之间畅通的桥梁。"
112 | ]
113 | },
114 | {
115 | "cell_type": "markdown",
116 | "metadata": {},
117 | "source": [
118 | "## 解释器和解释运行"
119 | ]
120 | },
121 | {
122 | "cell_type": "markdown",
123 | "metadata": {},
124 | "source": [
125 | "前面我们介绍了编程语言编译为机器代码的原理,我们写好的程序可以被编译器翻译为机器代码,然后被机器运行。编译就好像把我们写好的文章整篇读完然后翻译为另一种语言,拿给会那个语言的人看。\n",
126 | "\n",
127 | "还有另外一种运行程序的方法,不需要整篇文章写完,可以一句一句的翻译,写一句翻一句执行一句,做这件事的也是一个程序,叫解释器(*interpreter*)。解释器和编译器的主要区别就在于,它读入源代码是一行一行处理的,读一行执行一行。\n",
128 | "\n",
129 | "解释器的好处是,可以实现一种交互式的编程,启动解释器之后,你说一句解释器就回你一句,有来有回,很亲切,出了问题也可以马上知道。现代人几乎不写信了,打电话和微信是主要的沟通模式,可能也是类似的道理吧。\n",
130 | "\n",
131 | "Python 就是一种解释型语言,在命令行界面运行 `python` 主程序,就会进入一个交互式的界面,输入一行程序立刻可以得到反馈,差不多就是这个样子:"
132 | ]
133 | },
134 | {
135 | "cell_type": "markdown",
136 | "metadata": {},
137 | "source": [
138 | "
"
139 | ]
140 | },
141 | {
142 | "cell_type": "markdown",
143 | "metadata": {},
144 | "source": [
145 | "这就是 Python 的解释器界面,这种输入一行执行一行的界面有个通用的名字叫做 *REPL*(*read–eval–print loop* 的缩写),意思是这个程序可以读取(*read*)你的输入、计算(*evaluate*)、然后打印(*print*)结果,循环往复,直到你退出——在上图的界面中,输入 `exit()` 回车,就可以退出 Python 的 *REPL*。\n",
146 | "\n",
147 | "我们还可以把 Python 程序源代码(*source code*)存进一个文件里,然后让解释器直接运行这个文件。打开命令行界面(我们假定你已经[设置好了你的编程环境](x1-setup.md)),执行下面的操作:\n",
148 | "\n",
149 | "```shell\n",
150 | "cd ~/Code\n",
151 | "touch hello.py\n",
152 | "code hello.py\n",
153 | "```\n",
154 | "\n",
155 | "在打开的 VSCode 中输入下面的代码:\n",
156 | "\n",
157 | "```python\n",
158 | "print(f'4 + 9 = {4 + 9}')\n",
159 | "print('Hello world!')\n",
160 | "```\n",
161 | "\n",
162 | "保存,回到命令行界面执行:\n",
163 | "\n",
164 | "```python\n",
165 | "python hello.py\n",
166 | "```\n",
167 | "\n",
168 | "解释器会读取 `hello.py` 里面的代码,一行一行的执行并输出对应的结果。\n",
169 | "\n",
170 | "> 在有的系统中,`python` 不存在或者指向较老的 Python,可以通过 `python -V` 命令来查看其版本,如果返回 2.x 的话,可以将上述命令改为 `python3 hello.py`。"
171 | ]
172 | },
173 | {
174 | "cell_type": "markdown",
175 | "metadata": {},
176 | "source": [
177 | "解释器的实现方式有好几种:\n",
178 | "* 分析源代码,直接执行(做某些解释器已经定义好的事情),或者\n",
179 | "* 把源代码翻译成某种特定的中间代码,然后交给一个专门运行这种中间代码的程序执行,这种程序通常叫虚拟机(*virtual machine*),或者\n",
180 | "* 把源代码编译为机器代码然后交给硬件执行。\n",
181 | "\n",
182 | "无论哪种方式,其实也会有词法分析、句法分析、语义分析、代码优化和生成等过程,和编译器的基本架构是相似的,事实上由于实现上的复杂性,有时候编译和解释之间并没有那么硬的界限。\n",
183 | "\n",
184 | "Python 官方解释器的工作原理是上列的第二种,即生成中间代码——叫做“字节码(*bytecode*)”——和虚拟机的方式;而另外有种 Python 的实现([PyPy](http://pypy.org/))采用的是第三种方式,即预编译为机器码的方式。"
185 | ]
186 | },
187 | {
188 | "cell_type": "markdown",
189 | "metadata": {},
190 | "source": [
191 | "在这本书里,绝大部分代码运行用的是 Jupyter Lab,它的目标是把 *REPL* 做的更好用,让我们可以在浏览器中使用,有更好的语法高亮显示,最棒的是,可以把我们交互编程的过程保存下来,与人共享。不过它背后还是 Python 解释器,和命令行界面下运行 `python` 没有本质区别(但用起来的愉快程度和效率还是有很大区别的)。\n",
192 | "\n",
193 | "> Jupyter Lab 通过一个可扩展的架构正在支持越来越多的文档类型和编程语言,以后学习很多别的语言也可以用它。"
194 | ]
195 | },
196 | {
197 | "cell_type": "markdown",
198 | "metadata": {},
199 | "source": [
200 | "要使用 Jupyter Lab 需要先用 Python 的包管理工具 `pip` 来安装:\n",
201 | "\n",
202 | "```shell\n",
203 | "pip install jupyterlab\n",
204 | "```\n",
205 | "\n",
206 | "然后就可以用 `jupyter lab` 来运行 Jupyter Lab,在里面打开 *notebook* 进行交互式编程的尝试了,最好的办法是[使用我们的学习用书](x2-students-book.md)。"
207 | ]
208 | },
209 | {
210 | "cell_type": "markdown",
211 | "metadata": {},
212 | "source": [
213 | "## 小结"
214 | ]
215 | },
216 | {
217 | "cell_type": "markdown",
218 | "metadata": {},
219 | "source": [
220 | "* 程序是人类书写交由机器去执行的一系列指令,而编程语言是书写时遵照的语法规则;\n",
221 | "* 计算机 CPU 只能理解非常基础的指令,人要直接写出机器指令是困难和低效的;\n",
222 | "* 编译器和解释器是一些特殊的程序,能够把我们用高级编程语言编写的程序翻译成机器指令,然后交给计算机去执行;\n",
223 | "* 可以通过命令行 REPL、命令行加源文件和 Jupyter Lab 这样的可视化环境来运行 Python 程序,背后都是 Python 解释器。"
224 | ]
225 | }
226 | ],
227 | "metadata": {
228 | "kernelspec": {
229 | "display_name": "Python 3",
230 | "language": "python",
231 | "name": "python3"
232 | },
233 | "language_info": {
234 | "codemirror_mode": {
235 | "name": "ipython",
236 | "version": 3
237 | },
238 | "file_extension": ".py",
239 | "mimetype": "text/x-python",
240 | "name": "python",
241 | "nbconvert_exporter": "python",
242 | "pygments_lexer": "ipython3",
243 | "version": "3.7.4"
244 | }
245 | },
246 | "nbformat": 4,
247 | "nbformat_minor": 4
248 | }
249 |
--------------------------------------------------------------------------------
/p1-2-structure-1.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# 程序的基本结构"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "我们前面说过,编程就像教孩子,更具体地说,就像给孩子讲故事。编故事和讲故事是有套路的,最基本的离不开“角色”和“情节”,角色要生动独特,情节要跌宕起伏一波三折,就比较吸引人,最后完成了一个主旨的表述。\n",
15 | "\n",
16 | "以计算机为目标对象写故事,就要用编程语言的语法规则,在语法规则的体系内写清楚角色和情节,写出来的故事就是源代码(*source code*),然后通过编程语言的编译器或解释器让计算机运行源代码,得到我们想要的结果。\n",
17 | "\n",
18 | "理解程序的结构,就是理解编程语言提供给我们的“表达方式”有哪些,我们可以怎么去讲我们的故事。好的编程语言有很强的表达能力,用适合自己的编程语言书写代码经常会产生愉悦感,就来源于编程语言的表达力。\n",
19 | "\n",
20 | "编程语言这个东西,在程序员的世界里就好像时尚品牌,有喜欢这个牌子也有喜欢那个牌子的,程序员之间的闲聊多半都会导向“哪个编程语言最好”的话题,当然一般是得不出结果的。我个人认为好的编程语言应该具备几个特征:\n",
21 | "* 强表达能力:能够用简练的语法完成对各种逻辑的表达;\n",
22 | "* 有助于抽象:能够帮助程序员更好地进行问题的抽象;\n",
23 | "* 易于理解和学习:语法自然和流畅,容易为人所理解;\n",
24 | "* 有独特性:至少在某个特定领域有独特的优势,而不是面面俱到但处处平淡。\n",
25 | "\n",
26 | "随着学习越来越深入,大家也会有自己的感受,到时候我们也可以来谈谈,什么是我们心目中最好的编程语言。\n",
27 | "\n",
28 | "对我们目前阶段来说,Python 是一种非常好的教学语言,基本上满足上面列出的特征,大家可以在学习过程中自行体会。下面我们就以 Python 为例来理解程序的一般结构,也就是程序的表达方式。"
29 | ]
30 | },
31 | {
32 | "cell_type": "markdown",
33 | "metadata": {},
34 | "source": [
35 | "# 程序的基本结构(一):值与变量"
36 | ]
37 | },
38 | {
39 | "cell_type": "markdown",
40 | "metadata": {},
41 | "source": [
42 | "如果源代码是我们写出来的故事,那么“值和变量”就是故事的角色,是源代码中的“主语”和“宾语”。\n",
43 | "\n",
44 | "在程序的世界里出现的角色无非是各种数据,最基本的数据包括:\n",
45 | "* 逻辑上的真和假,大名叫布尔值(*boolean*);\n",
46 | "* 各种数字,包括整数和小数(在计算机里叫浮点数 *float*);\n",
47 | "* 表示文本的字符和字符串(*string*)。\n",
48 | "\n",
49 | "除了这些最基本的数据,还有几类“高级”一点的:\n",
50 | "* 对象:是我们可以自由定义的数据类型,一个对象可以有各种各样的属性(*attribute*)和方法(*method*);在这一部分的后面会专门介绍对象;\n",
51 | "* 数据容器:可以容纳数据的数据,比如一列数字;我们会在第四部分专门学习数据容器;\n",
52 | "* 函数:函数是完成特定工作的一段源代码,函数是程序本身的组成部分,但函数也是数据,我们会在第四部分专门学习函数。"
53 | ]
54 | },
55 | {
56 | "cell_type": "markdown",
57 | "metadata": {},
58 | "source": [
59 | "数据在程序里的存在有两种形式,一种是“值(*value*)”,一种是“变量(*variable*)”。下面是一些值的例子:\n",
60 | "\n",
61 | "```python\n",
62 | "42\n",
63 | "3.14\n",
64 | "'a'\n",
65 | "'abracadabra'\n",
66 | "True\n",
67 | "False\n",
68 | "```\n",
69 | "\n",
70 | "值是有类型的,Python 提供了一个函数叫做 `type()`,可以告诉我们某个值是什么类型,我们可以试一下:"
71 | ]
72 | },
73 | {
74 | "cell_type": "code",
75 | "execution_count": 1,
76 | "metadata": {},
77 | "outputs": [
78 | {
79 | "data": {
80 | "text/plain": [
81 | "int"
82 | ]
83 | },
84 | "execution_count": 1,
85 | "metadata": {},
86 | "output_type": "execute_result"
87 | }
88 | ],
89 | "source": [
90 | "type(42)"
91 | ]
92 | },
93 | {
94 | "cell_type": "code",
95 | "execution_count": 2,
96 | "metadata": {},
97 | "outputs": [
98 | {
99 | "data": {
100 | "text/plain": [
101 | "float"
102 | ]
103 | },
104 | "execution_count": 2,
105 | "metadata": {},
106 | "output_type": "execute_result"
107 | }
108 | ],
109 | "source": [
110 | "type(3.14)"
111 | ]
112 | },
113 | {
114 | "cell_type": "code",
115 | "execution_count": 3,
116 | "metadata": {},
117 | "outputs": [
118 | {
119 | "data": {
120 | "text/plain": [
121 | "str"
122 | ]
123 | },
124 | "execution_count": 3,
125 | "metadata": {},
126 | "output_type": "execute_result"
127 | }
128 | ],
129 | "source": [
130 | "type('abracadabra')"
131 | ]
132 | },
133 | {
134 | "cell_type": "code",
135 | "execution_count": 4,
136 | "metadata": {},
137 | "outputs": [
138 | {
139 | "data": {
140 | "text/plain": [
141 | "bool"
142 | ]
143 | },
144 | "execution_count": 4,
145 | "metadata": {},
146 | "output_type": "execute_result"
147 | }
148 | ],
149 | "source": [
150 | "type(False)"
151 | ]
152 | },
153 | {
154 | "cell_type": "markdown",
155 | "metadata": {},
156 | "source": [
157 | "上面的运行结果告诉我们:\n",
158 | "* `42` 类型是 `int`,这是 *integer* 的简写,即**整数**;\n",
159 | "* `3.14` 类型是 `float`,即**浮点数**,我们可以理解为就是**小数**;\n",
160 | "* `'abracadabra'` 类型是 `str`,这是 *string* 的简写,即**字符串**;\n",
161 | "* `False` 类型是 `bool`,这是 *boolean* 的简写,即**布尔值**,布尔值在计算机中代表逻辑上的真和假,只有两个布尔值,`True` 和 `False`。\n",
162 | "\n",
163 | "一般来说,同样类型的数据之间是可以发生一些“运算”的,比如数字之间可以加减乘除,字符串之间可以串接起来,布尔值之间可以进行逻辑运算,而不同类型的数据之间就好像不同物种一样,没法交互。所以搞清楚数据的类型是很重要的事情。"
164 | ]
165 | },
166 | {
167 | "cell_type": "markdown",
168 | "metadata": {},
169 | "source": [
170 | "有了值,我们就可以做计算,但是写不出真正的程序,为什么呢?因为程序写出来是希望能被很多人反复使用的——这些人叫做**用户**(*user*)——这样才比较划算,每次使用,用户输入一些数据,程序进行处理,然后给出用户想要的结果,每次的输入可能是同一类型,但具体的值是多少在我们写程序时是不知道的,所以我们需要一种东西能够来代替值写在程序中,而在程序运行时这些东西才根据用户的输入变成实际的“值”,这些“东西”就是**变量**。\n",
171 | "\n",
172 | "变量使得我们可以编写一个程序来处理一类值,而不管具体值是多少。我们学习代数的时候用 *a + b = b + a* 就表达了“加法交换律”这样高度抽象的普适规律,而不管 *a* 和 *b* 到底是多少,程序里的变量也一样,让我们能够进行“数据的抽象”,这个概念很重要,后面我们还会不断深化它。\n",
173 | "\n",
174 | "所有编程语言都支持值和变量,也支持把特定的值赋予某个变量,这样的操作叫做“**赋值**(*assignment*)”,下面是一些例子:"
175 | ]
176 | },
177 | {
178 | "cell_type": "code",
179 | "execution_count": 5,
180 | "metadata": {},
181 | "outputs": [],
182 | "source": [
183 | "a = 12\n",
184 | "b = 30\n",
185 | "f = 3.14\n",
186 | "s = 'abracadabra'\n",
187 | "l = [1, 2, 3]\n",
188 | "t = True\n",
189 | "f = False"
190 | ]
191 | },
192 | {
193 | "cell_type": "markdown",
194 | "metadata": {},
195 | "source": [
196 | "赋值之后变量就具有了相应的值,**同时也具有了该值对应的类型**,所以变量一经赋值就有了类型:"
197 | ]
198 | },
199 | {
200 | "cell_type": "code",
201 | "execution_count": 6,
202 | "metadata": {},
203 | "outputs": [
204 | {
205 | "data": {
206 | "text/plain": [
207 | "int"
208 | ]
209 | },
210 | "execution_count": 6,
211 | "metadata": {},
212 | "output_type": "execute_result"
213 | }
214 | ],
215 | "source": [
216 | "type(a)"
217 | ]
218 | },
219 | {
220 | "cell_type": "code",
221 | "execution_count": 7,
222 | "metadata": {},
223 | "outputs": [
224 | {
225 | "data": {
226 | "text/plain": [
227 | "str"
228 | ]
229 | },
230 | "execution_count": 7,
231 | "metadata": {},
232 | "output_type": "execute_result"
233 | }
234 | ],
235 | "source": [
236 | "type(s)"
237 | ]
238 | },
239 | {
240 | "cell_type": "code",
241 | "execution_count": 8,
242 | "metadata": {},
243 | "outputs": [
244 | {
245 | "data": {
246 | "text/plain": [
247 | "bool"
248 | ]
249 | },
250 | "execution_count": 8,
251 | "metadata": {},
252 | "output_type": "execute_result"
253 | }
254 | ],
255 | "source": [
256 | "type(t)"
257 | ]
258 | },
259 | {
260 | "cell_type": "code",
261 | "execution_count": 9,
262 | "metadata": {},
263 | "outputs": [
264 | {
265 | "data": {
266 | "text/plain": [
267 | "list"
268 | ]
269 | },
270 | "execution_count": 9,
271 | "metadata": {},
272 | "output_type": "execute_result"
273 | }
274 | ],
275 | "source": [
276 | "type(l)"
277 | ]
278 | },
279 | {
280 | "cell_type": "markdown",
281 | "metadata": {},
282 | "source": [
283 | "最后的这个 `l` 是新东西,叫做**列表**(*list*),是一种数据容器,我们以后会详细介绍。"
284 | ]
285 | },
286 | {
287 | "cell_type": "markdown",
288 | "metadata": {},
289 | "source": [
290 | "如果我们想消灭一个变量不再使用,可以用 `del` 命令:"
291 | ]
292 | },
293 | {
294 | "cell_type": "code",
295 | "execution_count": 10,
296 | "metadata": {},
297 | "outputs": [],
298 | "source": [
299 | "del b"
300 | ]
301 | },
302 | {
303 | "cell_type": "markdown",
304 | "metadata": {},
305 | "source": [
306 | "这之后变量 `b` 就变成没有赋值的状态了,既没有值也没有类型,实际上无法使用——在哪儿用都会报错,不信你可以试试。"
307 | ]
308 | },
309 | {
310 | "cell_type": "markdown",
311 | "metadata": {},
312 | "source": [
313 | "Python 还支持“多重赋值”,就是一次给多个变量赋值,比如这样:"
314 | ]
315 | },
316 | {
317 | "cell_type": "code",
318 | "execution_count": 11,
319 | "metadata": {},
320 | "outputs": [],
321 | "source": [
322 | "a, b = 12, 30"
323 | ]
324 | },
325 | {
326 | "cell_type": "markdown",
327 | "metadata": {},
328 | "source": [
329 | "之后 `a` 的值就是 `12`,而 `b` 的值就是 `30`。"
330 | ]
331 | },
332 | {
333 | "cell_type": "markdown",
334 | "metadata": {},
335 | "source": [
336 | "赋值语句的右边不仅可以是值,也可以是变量,当一个变量写在赋值语句右边时,可以把它看做是“值的名字”,Python 解释器会把它替换成它的值(这个过程叫“**求值**”),比如:"
337 | ]
338 | },
339 | {
340 | "cell_type": "code",
341 | "execution_count": 12,
342 | "metadata": {},
343 | "outputs": [],
344 | "source": [
345 | "g = f"
346 | ]
347 | },
348 | {
349 | "cell_type": "markdown",
350 | "metadata": {},
351 | "source": [
352 | "先把 `f` 替换为它的值,也就是 `False`,上面的语句就变成了 `g = False`,这样变量 `g` 就得到 `False` 这个值,以及 `bool` 类型。"
353 | ]
354 | },
355 | {
356 | "cell_type": "markdown",
357 | "metadata": {},
358 | "source": [
359 | "## 小结"
360 | ]
361 | },
362 | {
363 | "cell_type": "markdown",
364 | "metadata": {},
365 | "source": [
366 | "* 值和变量是程序的基本组成,是程序里操作的对象,就像故事中的主角配角;\n",
367 | "* 值具有类型,了解值的类型很重要;\n",
368 | "* 变量是值的抽象,可以帮助我们处理用户输入的任何值;\n",
369 | "* 赋值语句是值与变量、变量与变量之间的桥梁。"
370 | ]
371 | }
372 | ],
373 | "metadata": {
374 | "kernelspec": {
375 | "display_name": "Python 3",
376 | "language": "python",
377 | "name": "python3"
378 | },
379 | "language_info": {
380 | "codemirror_mode": {
381 | "name": "ipython",
382 | "version": 3
383 | },
384 | "file_extension": ".py",
385 | "mimetype": "text/x-python",
386 | "name": "python",
387 | "nbconvert_exporter": "python",
388 | "pygments_lexer": "ipython3",
389 | "version": "3.7.4"
390 | }
391 | },
392 | "nbformat": 4,
393 | "nbformat_minor": 4
394 | }
395 |
--------------------------------------------------------------------------------
/p1-4-structure-3.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# 程序的基本结构(三):逻辑判断与分支"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "如果源代码是我们写出来的故事,那么“逻辑判断与分支”就是故事中的情节编排,是场景之间的关联、排列和衔接——这一点程序和小说、影视剧不那么相近,倒是更像电子游戏。游戏是互动性最强的艺术形式,可以根据玩家的行为走向不同的情节,发生不同的事件和冲突,这种分支多样性极大地增加了表现力和趣味性。\n",
15 | "\n",
16 | "程序也一样,如果一个程序只能顺序一条一条指令执行,能表达的东西就太少了。我们需要根据输入的不同执行不同的指令,最终给出不一样的结果,这样程序才有价值。所以所有的编程语言都会提供逻辑判断和分支执行的能力。"
17 | ]
18 | },
19 | {
20 | "cell_type": "markdown",
21 | "metadata": {},
22 | "source": [
23 | "## if...else 语句"
24 | ]
25 | },
26 | {
27 | "cell_type": "markdown",
28 | "metadata": {},
29 | "source": [
30 | "所谓分支,其实也很简单,就是“如果这样就 A 否则就 B”,通过这个句式的组合可以实现无穷无尽的变化,这个句式翻译成 Python 的语法就是:\n",
31 | "\n",
32 | "```python\n",
33 | "if X:\n",
34 | " A\n",
35 | "else:\n",
36 | " B\n",
37 | "```\n",
38 | "\n",
39 | "X 是一个“逻辑判断”,其结果要么是真(*True*)要么是假(*False*);A 和 B 是两个代码段(*code block*),分别缩进以表示从属于 `if` 和 `else`。上面的代码意思是:如果 X 是真就执行代码段 A,否则就执行代码段 B。\n",
40 | "\n",
41 | "Python 还可以连着写好几个 `if`,比如:\n",
42 | "\n",
43 | "```python\n",
44 | "if X:\n",
45 | " A\n",
46 | "elif Y:\n",
47 | " B\n",
48 | "else:\n",
49 | " C\n",
50 | "```\n",
51 | "\n",
52 | "这里的 `elif` 是 *else if* 的简化写法,整个意思是:如果 X 是真就执行 A(不管 Y 如何),否则继续判断 Y——如果 Y 是真就执行 B,否则就执行 C。\n",
53 | "\n",
54 | "下面我们重点看看 X、Y 这些所谓“逻辑判断”可以是什么东西。"
55 | ]
56 | },
57 | {
58 | "cell_type": "markdown",
59 | "metadata": {},
60 | "source": [
61 | "## 逻辑表达式"
62 | ]
63 | },
64 | {
65 | "cell_type": "markdown",
66 | "metadata": {},
67 | "source": [
68 | "只要最终给出一个逻辑真值或假值的东西都可以算作“逻辑判断”,我们大致分分类可以有这么些:\n",
69 | "* 布尔类型的变量或者值,要么是 `True` 要么是 `False`;\n",
70 | "* 上一章介绍的**大小比较操作符**的运算结果,例如 `a <= 6` `a + b == c` 这类;\n",
71 | "* 返回布尔值的函数,例如我们上一章介绍的 `isinstance()`;\n",
72 | "* 上面这些东西通过上一章介绍的**逻辑运算操作符**组合起来,例如 `(a > 1) and (a <= 6)` `isinstance(x, int) or isinstance(x, float)`。\n",
73 | "\n",
74 | "这些东西通称“逻辑表达式”,因为其结果最终都是一个逻辑真值或者假值,根据其真假 `if...else` 语句就知道到底应该执行哪一个分支。我们来看例子。"
75 | ]
76 | },
77 | {
78 | "cell_type": "code",
79 | "execution_count": 1,
80 | "metadata": {},
81 | "outputs": [
82 | {
83 | "name": "stdout",
84 | "output_type": "stream",
85 | "text": [
86 | "94 是偶数\n"
87 | ]
88 | }
89 | ],
90 | "source": [
91 | "from random import randrange\n",
92 | "n = randrange(1, 100)\n",
93 | "if n % 2 == 0:\n",
94 | " print(n, '是偶数')\n",
95 | "else:\n",
96 | " print(n, '是奇数')"
97 | ]
98 | },
99 | {
100 | "cell_type": "markdown",
101 | "metadata": {},
102 | "source": [
103 | "上面的代码首先引入 `random` 模块里的一个函数 `randrange()`,然后调用这个函数来生成一个 1~100 之间的随机数并赋给 n——我们先不去细究这里面的东西,知道这个结果就好,关键是下面的 `if...else` 语句:如果 n 除以 2 的余数是 0(还记得上一章我们介绍的整除操作符 `//` 和 `%` 吧),就打印 ‘n 是偶数’,否则打印 ‘n 是奇数’。由于 n 是随机生成的一个数,所以你可以反复多次运行上面这段代码(运行的方法是选择上面这个 *cell*,按 ⌃+回车),看看不同的结果。"
104 | ]
105 | },
106 | {
107 | "cell_type": "markdown",
108 | "metadata": {},
109 | "source": [
110 | "有了逻辑判断和条件分支,我们可以做好多事情了,比如我们可以实现一个算绝对值的函数:"
111 | ]
112 | },
113 | {
114 | "cell_type": "code",
115 | "execution_count": 2,
116 | "metadata": {},
117 | "outputs": [],
118 | "source": [
119 | "def abs(x):\n",
120 | " if x >= 0:\n",
121 | " return x\n",
122 | " else:\n",
123 | " return -x"
124 | ]
125 | },
126 | {
127 | "cell_type": "markdown",
128 | "metadata": {},
129 | "source": [
130 | "这个函数非常简单,如果是大于等于零的数就直接返回这个数,否则返回它的相反数,我们可以测试下:"
131 | ]
132 | },
133 | {
134 | "cell_type": "code",
135 | "execution_count": 3,
136 | "metadata": {},
137 | "outputs": [
138 | {
139 | "data": {
140 | "text/plain": [
141 | "42"
142 | ]
143 | },
144 | "execution_count": 3,
145 | "metadata": {},
146 | "output_type": "execute_result"
147 | }
148 | ],
149 | "source": [
150 | "abs(42)"
151 | ]
152 | },
153 | {
154 | "cell_type": "code",
155 | "execution_count": 4,
156 | "metadata": {},
157 | "outputs": [
158 | {
159 | "data": {
160 | "text/plain": [
161 | "3.14"
162 | ]
163 | },
164 | "execution_count": 4,
165 | "metadata": {},
166 | "output_type": "execute_result"
167 | }
168 | ],
169 | "source": [
170 | "abs(-3.14)"
171 | ]
172 | },
173 | {
174 | "cell_type": "markdown",
175 | "metadata": {},
176 | "source": [
177 | "我们还可以实现一个我们自己的 `type()` 函数,和官方的 `type()` 功能也差不多,即返回一个变量或者值的数据类型:"
178 | ]
179 | },
180 | {
181 | "cell_type": "code",
182 | "execution_count": 5,
183 | "metadata": {},
184 | "outputs": [],
185 | "source": [
186 | "def type_0(x):\n",
187 | " if isinstance(x, bool):\n",
188 | " return 'bool'\n",
189 | " elif isinstance(x, int):\n",
190 | " return 'int'\n",
191 | " elif isinstance(x, float):\n",
192 | " return 'float'\n",
193 | " elif isinstance(x, str):\n",
194 | " return 'str'\n",
195 | " else:\n",
196 | " return 'unknown'"
197 | ]
198 | },
199 | {
200 | "cell_type": "code",
201 | "execution_count": 6,
202 | "metadata": {},
203 | "outputs": [
204 | {
205 | "data": {
206 | "text/plain": [
207 | "'int'"
208 | ]
209 | },
210 | "execution_count": 6,
211 | "metadata": {},
212 | "output_type": "execute_result"
213 | }
214 | ],
215 | "source": [
216 | "type_0(42)"
217 | ]
218 | },
219 | {
220 | "cell_type": "code",
221 | "execution_count": 7,
222 | "metadata": {},
223 | "outputs": [
224 | {
225 | "data": {
226 | "text/plain": [
227 | "'str'"
228 | ]
229 | },
230 | "execution_count": 7,
231 | "metadata": {},
232 | "output_type": "execute_result"
233 | }
234 | ],
235 | "source": [
236 | "type_0('abracadabra')"
237 | ]
238 | },
239 | {
240 | "cell_type": "code",
241 | "execution_count": 8,
242 | "metadata": {},
243 | "outputs": [
244 | {
245 | "data": {
246 | "text/plain": [
247 | "'bool'"
248 | ]
249 | },
250 | "execution_count": 8,
251 | "metadata": {},
252 | "output_type": "execute_result"
253 | }
254 | ],
255 | "source": [
256 | "type_0(False)"
257 | ]
258 | },
259 | {
260 | "cell_type": "code",
261 | "execution_count": 9,
262 | "metadata": {},
263 | "outputs": [
264 | {
265 | "data": {
266 | "text/plain": [
267 | "'unknown'"
268 | ]
269 | },
270 | "execution_count": 9,
271 | "metadata": {},
272 | "output_type": "execute_result"
273 | }
274 | ],
275 | "source": [
276 | "type_0([1, 2, 3])"
277 | ]
278 | },
279 | {
280 | "cell_type": "markdown",
281 | "metadata": {},
282 | "source": [
283 | "最后一个例子显示出我们的 `type_0` 实现和系统的 `type` 还是有点差距,不过没关系,我们才刚开始嘛。"
284 | ]
285 | },
286 | {
287 | "cell_type": "markdown",
288 | "metadata": {},
289 | "source": [
290 | "## 万物皆为布尔值"
291 | ]
292 | },
293 | {
294 | "cell_type": "markdown",
295 | "metadata": {},
296 | "source": [
297 | "我们上一节列出了几类逻辑表达式,它们都可以放在 `if` 后面做逻辑判断,但可以放在 `if` 后面的远不止这些,事实上**几乎任何东西**都可以。因为 Python 提供了一组规则来判断一个值“相当于”逻辑真还是假,这种定义是在所谓“合理类比”和方便性的基础上做出的,比如:\n",
298 | "* 数字 0 “相当于”假,而其他数字都相当于真;\n",
299 | "* 空字符串“相当于”假,非空的字符串“相当于”真。\n",
300 | "\n",
301 | "其他很多情形也类似,一般来说 0 啊、空啊之类的都“相当于”假,其他就算真了。如果我们搞不清楚某个东西相当于真还是假,可以借助于内置函数 `bool()`,这个函数可以把任何东西变成布尔值(`True` 或者 `False`),下面是一些例子:"
302 | ]
303 | },
304 | {
305 | "cell_type": "code",
306 | "execution_count": 10,
307 | "metadata": {},
308 | "outputs": [
309 | {
310 | "data": {
311 | "text/plain": [
312 | "True"
313 | ]
314 | },
315 | "execution_count": 10,
316 | "metadata": {},
317 | "output_type": "execute_result"
318 | }
319 | ],
320 | "source": [
321 | "bool(42)"
322 | ]
323 | },
324 | {
325 | "cell_type": "code",
326 | "execution_count": 11,
327 | "metadata": {},
328 | "outputs": [
329 | {
330 | "data": {
331 | "text/plain": [
332 | "False"
333 | ]
334 | },
335 | "execution_count": 11,
336 | "metadata": {},
337 | "output_type": "execute_result"
338 | }
339 | ],
340 | "source": [
341 | "bool(0)"
342 | ]
343 | },
344 | {
345 | "cell_type": "code",
346 | "execution_count": 12,
347 | "metadata": {},
348 | "outputs": [
349 | {
350 | "data": {
351 | "text/plain": [
352 | "False"
353 | ]
354 | },
355 | "execution_count": 12,
356 | "metadata": {},
357 | "output_type": "execute_result"
358 | }
359 | ],
360 | "source": [
361 | "bool(0.0)"
362 | ]
363 | },
364 | {
365 | "cell_type": "code",
366 | "execution_count": 13,
367 | "metadata": {},
368 | "outputs": [
369 | {
370 | "data": {
371 | "text/plain": [
372 | "False"
373 | ]
374 | },
375 | "execution_count": 13,
376 | "metadata": {},
377 | "output_type": "execute_result"
378 | }
379 | ],
380 | "source": [
381 | "bool('')"
382 | ]
383 | },
384 | {
385 | "cell_type": "code",
386 | "execution_count": 14,
387 | "metadata": {},
388 | "outputs": [
389 | {
390 | "data": {
391 | "text/plain": [
392 | "True"
393 | ]
394 | },
395 | "execution_count": 14,
396 | "metadata": {},
397 | "output_type": "execute_result"
398 | }
399 | ],
400 | "source": [
401 | "bool('abracadabra')"
402 | ]
403 | },
404 | {
405 | "cell_type": "code",
406 | "execution_count": 15,
407 | "metadata": {},
408 | "outputs": [
409 | {
410 | "data": {
411 | "text/plain": [
412 | "True"
413 | ]
414 | },
415 | "execution_count": 15,
416 | "metadata": {},
417 | "output_type": "execute_result"
418 | }
419 | ],
420 | "source": [
421 | "bool('0') # 这是一个非空字符串,不要和 bool(0) 搞混哦~"
422 | ]
423 | },
424 | {
425 | "cell_type": "code",
426 | "execution_count": 16,
427 | "metadata": {},
428 | "outputs": [
429 | {
430 | "data": {
431 | "text/plain": [
432 | "True"
433 | ]
434 | },
435 | "execution_count": 16,
436 | "metadata": {},
437 | "output_type": "execute_result"
438 | }
439 | ],
440 | "source": [
441 | "bool([1, 2, 3]) # 和字符串类似,非空列表相当于真,空列表相当于假"
442 | ]
443 | },
444 | {
445 | "cell_type": "code",
446 | "execution_count": 17,
447 | "metadata": {},
448 | "outputs": [
449 | {
450 | "data": {
451 | "text/plain": [
452 | "False"
453 | ]
454 | },
455 | "execution_count": 17,
456 | "metadata": {},
457 | "output_type": "execute_result"
458 | }
459 | ],
460 | "source": [
461 | "bool([])"
462 | ]
463 | },
464 | {
465 | "cell_type": "markdown",
466 | "metadata": {},
467 | "source": [
468 | "这种“相当于”的逻辑,可以帮助我们写出更简洁的代码,比如下面两段代码是完全等价的:"
469 | ]
470 | },
471 | {
472 | "cell_type": "code",
473 | "execution_count": 18,
474 | "metadata": {},
475 | "outputs": [],
476 | "source": [
477 | "a = 42\n",
478 | "if n != 0:\n",
479 | " a = a / n"
480 | ]
481 | },
482 | {
483 | "cell_type": "code",
484 | "execution_count": 19,
485 | "metadata": {},
486 | "outputs": [],
487 | "source": [
488 | "a = 42\n",
489 | "if n:\n",
490 | " a = a / n"
491 | ]
492 | },
493 | {
494 | "cell_type": "markdown",
495 | "metadata": {},
496 | "source": [
497 | "下面两段也完全等价:"
498 | ]
499 | },
500 | {
501 | "cell_type": "code",
502 | "execution_count": 20,
503 | "metadata": {},
504 | "outputs": [
505 | {
506 | "name": "stdin",
507 | "output_type": "stream",
508 | "text": [
509 | "请输入您的姓名 \n"
510 | ]
511 | },
512 | {
513 | "name": "stdout",
514 | "output_type": "stream",
515 | "text": [
516 | "姓名不可为空,请重新输入\n"
517 | ]
518 | }
519 | ],
520 | "source": [
521 | "s = input('请输入您的姓名')\n",
522 | "if s == '':\n",
523 | " print('姓名不可为空,请重新输入')"
524 | ]
525 | },
526 | {
527 | "cell_type": "code",
528 | "execution_count": 21,
529 | "metadata": {},
530 | "outputs": [
531 | {
532 | "name": "stdin",
533 | "output_type": "stream",
534 | "text": [
535 | "请输入您的姓名 Neo\n"
536 | ]
537 | }
538 | ],
539 | "source": [
540 | "s = input('请输入您的姓名')\n",
541 | "if not s:\n",
542 | " print('姓名不可为空,请重新输入')"
543 | ]
544 | },
545 | {
546 | "cell_type": "markdown",
547 | "metadata": {},
548 | "source": [
549 | "最后这个例子里的 `input()` 也是 Python 内置函数,它的作用是提示用户输入点什么,并把用户输入的东西作为函数值返回,我们会经常在例子中用到这个东西。"
550 | ]
551 | },
552 | {
553 | "cell_type": "markdown",
554 | "metadata": {},
555 | "source": [
556 | "## 小结"
557 | ]
558 | },
559 | {
560 | "cell_type": "markdown",
561 | "metadata": {},
562 | "source": [
563 | "* 根据一个逻辑判断做分支执行的能力使得计算机程序真正开始变得“智能”,程序中的角色(变量与值)和交互(操作符与函数)通过它才最终编排成有用的东西;\n",
564 | "* `if...else` 的语法简单直观,但通过各种逻辑表达式组合可以实现丰富的逻辑选择;\n",
565 | "* Python 中的几乎任何值都能转换为布尔值 `True` 或 `False`,`bool()` 函数可以告诉我们特定值对应的是逻辑真还是假。"
566 | ]
567 | }
568 | ],
569 | "metadata": {
570 | "kernelspec": {
571 | "display_name": "Python 3",
572 | "language": "python",
573 | "name": "python3"
574 | },
575 | "language_info": {
576 | "codemirror_mode": {
577 | "name": "ipython",
578 | "version": 3
579 | },
580 | "file_extension": ".py",
581 | "mimetype": "text/x-python",
582 | "name": "python",
583 | "nbconvert_exporter": "python",
584 | "pygments_lexer": "ipython3",
585 | "version": "3.7.4"
586 | }
587 | },
588 | "nbformat": 4,
589 | "nbformat_minor": 4
590 | }
591 |
--------------------------------------------------------------------------------
/p1-5-structure-4.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# 程序的基本结构(四):循环"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "循环也是一种常见结构,它代表了一种常见模式:批处理。我们在现实生活中也经常面对“批处理”这种模式:\n",
15 | "* 我们乘坐地铁,要坐十站去到目的地,那么就是把“乘坐一站地铁”循环十遍;\n",
16 | "* 我们要洗一大串葡萄,就是把“洗一颗葡萄”循环 N 遍;\n",
17 | "* 我们要学完这个部分,就是不断循环“学习下一章”直到“本部分没有更多章”。\n",
18 | "\n",
19 | "我们写程序也是如此,计算机特别擅长的就是重复劳动,不知疲倦而且不走样,我们的程序经常是一次处理一组数据,对其中的每个数据做一些操作,然后下一个,然后下一个……直到这组数据都处理完,这就是循环的基本概念了。\n",
20 | "\n",
21 | "循环可以用下面的逻辑来表述:\n",
22 | "1. 从一组数据 S 中取出下一个数据 x => x 是一个变量,这对应一个赋值语句;\n",
23 | "2. 对 x 执行若干操作 => 这对应一个代码段,里面可以有各种操作符和函数;\n",
24 | "3. 如果 S 中还有未取出的数据,则回到第1步继续 => 这对应一个逻辑判断 `if...else`,但这个“回到第1步”是什么呢?\n",
25 | "4. 结束。\n",
26 | "\n",
27 | "我们前面介绍的值与变量、操作符与函数、逻辑判断与 `if...else` 分支,已经构成了相当完整的程序“写作工具”,可以从理论上证明这套写作工具能写出任何我们想要的程序(只要是能用计算机解答的问题),不过有些逻辑表述起来会有些麻烦,或者不那么易懂,所以一般的编程语言会针对这些问题再提供一些附加的“写作工具”,这一章的循环和下一章的异常处理都是这种性质。上面第3步中的“回到第1步”就是循环实现的关键操作,这个要怎么做呢?\n",
28 | "\n",
29 | "最早的一批编程语言对这个操作的实现真的就是按照“回到第1步”的字面意思做的,就是使用 *goto* 指令的**跳转语句**,这个语句让计算机跳到指定的一行执行,这样我们就可以复用上面的第1和第2步,不断的循环执行它们。不过后来 *goto* 语句闯了祸,被人类封印了,说白了就是 *goto* 太自由太强大了,可以在程序中任意的指定下一个执行的语句,当程序变大变复杂时导致程序的执行顺序非常难以预测,程序就很容易出错。这段历史非常深刻的改变了编程世界的方方面面,我们后面介绍结构化编程和面向对象时还会讲到这个故事。\n",
30 | "\n",
31 | "人们转而使用一种“有限制的跳转”来实现循环,Python 支持两种形式的循环语句,分别是 **for 循环** 和 **while 循环**,我们下面分别来介绍。"
32 | ]
33 | },
34 | {
35 | "cell_type": "markdown",
36 | "metadata": {},
37 | "source": [
38 | "## for 循环"
39 | ]
40 | },
41 | {
42 | "cell_type": "markdown",
43 | "metadata": {},
44 | "source": [
45 | "Python 的 `for` 循环可以对一组数据做循环,比如给出一个列表,可以针对列表里每个元素按顺序循环一遍。下面是例子:"
46 | ]
47 | },
48 | {
49 | "cell_type": "code",
50 | "execution_count": 1,
51 | "metadata": {},
52 | "outputs": [
53 | {
54 | "name": "stdout",
55 | "output_type": "stream",
56 | "text": [
57 | "2\n",
58 | "3\n",
59 | "5\n",
60 | "7\n"
61 | ]
62 | }
63 | ],
64 | "source": [
65 | "primes = [2, 3, 5, 7]\n",
66 | "for prime in primes:\n",
67 | " print(prime)"
68 | ]
69 | },
70 | {
71 | "cell_type": "markdown",
72 | "metadata": {},
73 | "source": [
74 | "列表我们还没正式介绍过,但已经零零星星出现过几次,这个东西非常重要,而且很多事情和它有关,我们在第四部分介绍数据容器时会重点介绍列表,在此之前我们只要知道:列表就是方括号括起来的一串数据,里面每个数据可以是任何类型,并且是有序排列的。就像上面例子里的 `primes`,方括号括起来的四个素数。\n",
75 | "\n",
76 | "而 `for` 循环的语法就像上面展示的:\n",
77 | "1. `for prime in primes` 的意思是,从 `primes` 中取出下一个元素,将其赋值给 `prime`(这个变量叫做“**循环变量**”),然后运行下面缩进的代码段;\n",
78 | "2. 在 `for` 语句最后冒号下面缩进的代码段,叫做“**循环体**”,是循环中反复执行的片段,这里我们简单地调用 `print()` 函数打印循环变量 prime 的值;\n",
79 | "3. 循环体执行完毕就回到第1步执行 `for` 那一行,直到 `primes` 中取不出下一个元素,即列表循环完毕,整个循环结束。"
80 | ]
81 | },
82 | {
83 | "cell_type": "markdown",
84 | "metadata": {},
85 | "source": [
86 | "Python 提供有一个内置函数 `range()` 可以构造任何整数等差数列,经常拿来和 `for` 循环一起用,在官方手册中 `range()` 函数的参数是[这么定义](https://docs.python.org/3.7/library/functions.html#func-range)的:\n",
87 | "\n",
88 | "```python\n",
89 | "range(stop)\n",
90 | "range(start, stop[, step])\n",
91 | "```\n",
92 | "\n",
93 | "上面的文档说明 `range()` 这个函数有两个版本:第一个接受一个参数;第二个接受三个参数,其中最后一个有缺省值,所以可以不提供(方括号括起来的部分表示有缺省值、可提供可不提供的参数,这个方括号和上面表示列表的不是一回事哦):\n",
94 | "* 第一个版本中,唯一的参数是 `range()` 要构造的数列的上限,`range(stop)` 会输出从 `0` 到 `stop-1` 的 整数列;\n",
95 | "* 第二个版本中,前两个参数分别是 `range()` 要构造的数列的下限和上限,`range(start, stop)` 会输出从 `start` 到 `stop-1` 的 整数列;如果还提供了第三个参数 `step`,这是等差数列的**公差**,`range(start, stop, step)` 会输出 `[start, start+step, start+step*2,...]` 这样一列整数,最大不超过 `stop-1`。\n",
96 | "\n",
97 | "下面是一些例子:"
98 | ]
99 | },
100 | {
101 | "cell_type": "code",
102 | "execution_count": 2,
103 | "metadata": {},
104 | "outputs": [
105 | {
106 | "data": {
107 | "text/plain": [
108 | "[0, 1, 2, 3, 4, 5]"
109 | ]
110 | },
111 | "execution_count": 2,
112 | "metadata": {},
113 | "output_type": "execute_result"
114 | }
115 | ],
116 | "source": [
117 | "list(range(6))"
118 | ]
119 | },
120 | {
121 | "cell_type": "markdown",
122 | "metadata": {},
123 | "source": [
124 | "注意我们在 `range()` 函数外面套了一个函数 `list()` 来把 `range()` 的输出结果转换为一个列表,这样方便我们看。\n",
125 | "\n",
126 | "> 可能有善于思考的你会问:`range()` 的输出还要用 `list()` 来“转换为列表”,那 `range()` 输出的是什么呢?这是个好问题,`range()` 输出的是一个“**迭代器**(*iterator*)”,是 Python 非常有特色也非常强大的工具,可惜目前我们还不容易搞清楚这个东西,我们把它放在第四部分中,学完迭代器顺便还能知道 `for` 循环的本质是什么。"
127 | ]
128 | },
129 | {
130 | "cell_type": "code",
131 | "execution_count": 3,
132 | "metadata": {},
133 | "outputs": [
134 | {
135 | "data": {
136 | "text/plain": [
137 | "[2, 3, 4, 5, 6, 7, 8, 9]"
138 | ]
139 | },
140 | "execution_count": 3,
141 | "metadata": {},
142 | "output_type": "execute_result"
143 | }
144 | ],
145 | "source": [
146 | "list(range(2, 10)) # 注意 range() 输出是从 start 开始,到 stop-1 结束,也就是说,不包含后一个参数本身"
147 | ]
148 | },
149 | {
150 | "cell_type": "code",
151 | "execution_count": 4,
152 | "metadata": {},
153 | "outputs": [
154 | {
155 | "data": {
156 | "text/plain": [
157 | "[2, 3, 4, 5, 6, 7, 8, 9]"
158 | ]
159 | },
160 | "execution_count": 4,
161 | "metadata": {},
162 | "output_type": "execute_result"
163 | }
164 | ],
165 | "source": [
166 | "list(range(2, 10, 1)) # step 的缺省值就是 1,所以这句和上一句完全等价"
167 | ]
168 | },
169 | {
170 | "cell_type": "code",
171 | "execution_count": 5,
172 | "metadata": {},
173 | "outputs": [
174 | {
175 | "data": {
176 | "text/plain": [
177 | "[2, 5, 8]"
178 | ]
179 | },
180 | "execution_count": 5,
181 | "metadata": {},
182 | "output_type": "execute_result"
183 | }
184 | ],
185 | "source": [
186 | "list(range(2, 10, 3)) # 这次指定了 3 作为公差,"
187 | ]
188 | },
189 | {
190 | "cell_type": "markdown",
191 | "metadata": {},
192 | "source": [
193 | "`for` 循环用的那个列表不一定是数字,别的类型也都可以,比如在 Python 中字符串可以看作其中每个字符组成的列表,所以下面的代码会打印出字符串 s 中的每个字符:"
194 | ]
195 | },
196 | {
197 | "cell_type": "code",
198 | "execution_count": 6,
199 | "metadata": {},
200 | "outputs": [
201 | {
202 | "name": "stdout",
203 | "output_type": "stream",
204 | "text": [
205 | "a\n",
206 | "b\n",
207 | "r\n",
208 | "a\n",
209 | "c\n",
210 | "a\n",
211 | "d\n",
212 | "a\n",
213 | "b\n",
214 | "r\n",
215 | "a\n"
216 | ]
217 | }
218 | ],
219 | "source": [
220 | "s = 'abracadabra'\n",
221 | "for c in s:\n",
222 | " print(c)"
223 | ]
224 | },
225 | {
226 | "cell_type": "markdown",
227 | "metadata": {},
228 | "source": [
229 | "其实 `for` 可以循环的东西远不止列表,我们在后面介绍迭代器的时候会再回来讨论 `for` 循环。"
230 | ]
231 | },
232 | {
233 | "cell_type": "markdown",
234 | "metadata": {},
235 | "source": [
236 | "## while 循环"
237 | ]
238 | },
239 | {
240 | "cell_type": "markdown",
241 | "metadata": {},
242 | "source": [
243 | "`while` 循环是更一般化的一种循环结构,基本上就是我们在本章开头描述的“只要某条件成立就继续循环,否则结束循环”的逻辑,那个使得循环继续的条件叫做“**循环条件**”。我们还是用例子来说明:"
244 | ]
245 | },
246 | {
247 | "cell_type": "code",
248 | "execution_count": 7,
249 | "metadata": {},
250 | "outputs": [
251 | {
252 | "name": "stdout",
253 | "output_type": "stream",
254 | "text": [
255 | "0\n",
256 | "1\n",
257 | "2\n",
258 | "3\n",
259 | "4\n",
260 | "Loop ends.\n"
261 | ]
262 | }
263 | ],
264 | "source": [
265 | "count = 0\n",
266 | "while count < 5:\n",
267 | " print(count)\n",
268 | " count += 1\n",
269 | " \n",
270 | "print('Loop ends.')"
271 | ]
272 | },
273 | {
274 | "cell_type": "markdown",
275 | "metadata": {},
276 | "source": [
277 | "上面的代码一开始变量 `count` 值为 0,然后开始一个 `while` 循环,循环条件是一个逻辑表达式 `count < 5`,只要这个条件成立就会反复执行它下面缩进的代码段,每执行完一次都会重新检查一下循环条件,如果循环条件不成立(逻辑表达式的值为 *False*)则循环不会继续,直接跳到循环体的后面执行了。\n",
278 | "\n",
279 | "在这个例子中,循环体先打印 `count` 的值,然后对 `count` 执行 +1 的操作,这样循环若干次之后,`count` 的值会达到 5,从而使得循环条件 `count < 5` 不再成立,于是循环就结束了,跳到循环体的后面,打印出 *Loop ends*。"
280 | ]
281 | },
282 | {
283 | "cell_type": "markdown",
284 | "metadata": {},
285 | "source": [
286 | "和 `for` 循环不一样,`while` 的循环条件非常自由,完全由我们的代码来控制,这样的好处是可以处理各种情况的循环,坏处是,有可能玩砸掉。试想上面的循环,如果我们在循环体里没有给 `count` 执行 +1,那么 `count` 的值就永远也不会达到和超过 5,这个循环就会无限进行下去,直到这个页面挂掉。所以在写 `while` 循环时要特别小心的检查,确保循环条件是会终止的,避免出现无限循环的情况。"
287 | ]
288 | },
289 | {
290 | "cell_type": "markdown",
291 | "metadata": {},
292 | "source": [
293 | "## break 和 continue"
294 | ]
295 | },
296 | {
297 | "cell_type": "markdown",
298 | "metadata": {},
299 | "source": [
300 | "在 for 和 while 的循环体里可以执行 `break` 和 `continue` 两个命令:\n",
301 | "* `break` 直接终止整个循环,跳到循环体之后的代码执行;\n",
302 | "* `continue` 跳过本次循环余下的代码,直接回到 `for`(取下一个元素)或者 `while`(进行循环条件检查)。\n",
303 | "\n",
304 | "这两个命令一般用于特定边界情况的处理。我们来看几个例子。"
305 | ]
306 | },
307 | {
308 | "cell_type": "code",
309 | "execution_count": 8,
310 | "metadata": {},
311 | "outputs": [
312 | {
313 | "name": "stdout",
314 | "output_type": "stream",
315 | "text": [
316 | "1\n",
317 | "3\n",
318 | "5\n",
319 | "7\n",
320 | "9\n"
321 | ]
322 | }
323 | ],
324 | "source": [
325 | "for x in range(10):\n",
326 | " if x % 2 == 0:\n",
327 | " continue\n",
328 | " print(x)"
329 | ]
330 | },
331 | {
332 | "cell_type": "markdown",
333 | "metadata": {},
334 | "source": [
335 | "上面的代码在 0~9 的数列中循环,如果循环变量 `x` 是偶数就跳过循环体剩下的部分(即 `print(x)` 这一句),继续下一个,所以最后只会打印出所有奇数(偶数都被 `continue` 跳过了)。"
336 | ]
337 | },
338 | {
339 | "cell_type": "code",
340 | "execution_count": 9,
341 | "metadata": {},
342 | "outputs": [],
343 | "source": [
344 | "from random import randrange\n",
345 | "\n",
346 | "while True:\n",
347 | " n = randrange(-2, 5) # randrange 返回一个给定范围的随机整数\n",
348 | " if n < 0:\n",
349 | " break\n",
350 | " elif n % 2 == 0:\n",
351 | " print(n, 'is even')\n",
352 | " else:\n",
353 | " print(n, 'is odd')"
354 | ]
355 | },
356 | {
357 | "cell_type": "markdown",
358 | "metadata": {},
359 | "source": [
360 | "还是使用 `randrange()` 函数来生成一个随机整数。注意上面 `while` 的循环条件是固定真值,所以后面的循环体会一直循环执行,但不用担心,因为遇到 `break` 循环就会终止。在循环体中我们用 `randrange()` 来生成一个 -2 到 4 之间的整数,如果是负数就执行 `break` 终止循环,否则看取出来的是奇数还是偶数,相应的输出一句话。试着自己运行下这个程序看看。"
361 | ]
362 | },
363 | {
364 | "cell_type": "markdown",
365 | "metadata": {},
366 | "source": [
367 | "## 小结"
368 | ]
369 | },
370 | {
371 | "cell_type": "markdown",
372 | "metadata": {},
373 | "source": [
374 | "* 循环就是反复执行一组特定操作,也是程序中常见的结构;\n",
375 | "* Python 中的 `for` 循环可以针对一组数据进行循环,而 `while` 则是根据循环条件成立与否来决定循环是否继续;\n",
376 | "* 可以使用 `continue` 和 `break` 语句来改变循环的执行流程。"
377 | ]
378 | }
379 | ],
380 | "metadata": {
381 | "kernelspec": {
382 | "display_name": "Python 3",
383 | "language": "python",
384 | "name": "python3"
385 | },
386 | "language_info": {
387 | "codemirror_mode": {
388 | "name": "ipython",
389 | "version": 3
390 | },
391 | "file_extension": ".py",
392 | "mimetype": "text/x-python",
393 | "name": "python",
394 | "nbconvert_exporter": "python",
395 | "pygments_lexer": "ipython3",
396 | "version": "3.7.4"
397 | }
398 | },
399 | "nbformat": 4,
400 | "nbformat_minor": 4
401 | }
402 |
--------------------------------------------------------------------------------
/p1-6-structure-5.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# 程序的基本结构(五):异常处理"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "异常处理是个很重要,也有点难度的概念。我们先尝试理解基本的概念,后面再不断通过实践来加深掌握。"
15 | ]
16 | },
17 | {
18 | "cell_type": "markdown",
19 | "metadata": {},
20 | "source": [
21 | "我们前面说过,大多数程序的工作都是:接受输入,对输入进行处理,然后输出结果。这里有个很重要的理念,是所有资深程序员都习惯成自然的认知,那就是“输入是不可控的”,输入可能是用户通过键盘鼠标触控屏输入的,也可能是读取某个设备上一个程序的输出,简言之,都存在不可期的情况。\n",
22 | "\n",
23 | "假定我们期待用户或者某个程序给我们一个不为 0 的整数,我们要拿来做除数算出一个值,如果来的不是整数是个小数怎么办?或者就是个 0 怎么办?或者干脆就不是个数怎么办?\n",
24 | "\n",
25 | "还有依赖于外部设备的各种异常状况:如果我们从某个程序读一个数,但是那个程序死掉了,一直没给我们数,我们一直等着卡死在这里?或者我们要向打印机输出一个报表,但是打印机被人踢掉了电源线,一直不响应怎么办?"
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "metadata": {},
31 | "source": [
32 | "所有这些错误都在程序运行时才会出现,程序写出来是没有错的,运行时出现奇怪的异常状况,程序又没有好好处理的话,就会出现灾难,这些运行时出现的异常状况就叫**运行时错误**(*runtime error*)或者**运行时异常**(*runtime exception*)。现代编程语言一般都提供标准的异常处理方案,让我们可以写程序来处理这类异常。\n",
33 | "\n",
34 | "Python 提供的异常处理机制可以用下面的模板来说明:\n",
35 | "\n",
36 | "```python\n",
37 | "try:\n",
38 | " # 把有可能出现异常的代码放在 try 后面\n",
39 | " # 当出现异常时解释器会捕获异常\n",
40 | " # 并根据异常的类型执行后面的对应代码块\n",
41 | " do_something_nasty()\n",
42 | "except ValueError:\n",
43 | " # 如果发生 ValueError 类型的异常则执行这个代码块\n",
44 | " pass\n",
45 | "except (TypeError, ZeroDivisionError):\n",
46 | " # 可以一次指定几个不同类型的异常在一起处理exceptions\n",
47 | " # 如果出现 TypeError 或者 ZeroDivisionError 则执行这个代码块\n",
48 | " pass\n",
49 | "except:\n",
50 | " # 所有上面没有专门处理的类型的异常会在这里处理\n",
51 | " pass\n",
52 | "else:\n",
53 | " # 当且仅当 try 代码块里无异常发生时这个代码块会被执行\n",
54 | " pass\n",
55 | "finally:\n",
56 | " # 无论发生了什么这个代码块都会被执行\n",
57 | " # 通常这里是清理性的代码,比如我们在 try 里面打开一个文件进行处理\n",
58 | " # 无论过程中有没有异常出现最后都应该关闭文件释放资源\n",
59 | " # 这样的操作就适合在这里执行\n",
60 | "```\n",
61 | "\n",
62 | "上面出现的关键字 `pass` 的意思是“什么也不做”,Python 语法需要有点什么,但是我们暂时什么都不想做的时候放上一个 `pass` 就可以了。"
63 | ]
64 | },
65 | {
66 | "cell_type": "markdown",
67 | "metadata": {},
68 | "source": [
69 | "以 `try` 开始的异常处理结构可以包含所有这些模块,但并不是都必须有,但至少应该有一个 `except` 或者 `finally`。\n",
70 | "\n",
71 | "Python 有很多[内置的运行时错误类型](https://docs.python.org/3/library/exceptions.html#bltin-exceptions)可以直接使用,比如我们上面看到的:\n",
72 | "* `TypeError`:当一个操作或者函数收到的参数类型不对,或者一个类型的对象不支持某个被请求的操作时抛出这个异常;\n",
73 | "* `ValueError`:当一个操作或者函数收到的参数类型对但是值不合法时抛出这个异常;\n",
74 | "* `ZeroDivisionError`:出现 0 作除数的情况时抛出这个异常。\n",
75 | "\n",
76 | "我们来看几个官方指引中的例子并稍作解释。"
77 | ]
78 | },
79 | {
80 | "cell_type": "code",
81 | "execution_count": 1,
82 | "metadata": {},
83 | "outputs": [
84 | {
85 | "name": "stdin",
86 | "output_type": "stream",
87 | "text": [
88 | "Please enter a number: 123g\n"
89 | ]
90 | },
91 | {
92 | "name": "stdout",
93 | "output_type": "stream",
94 | "text": [
95 | "Not a valid number. Try again...\n"
96 | ]
97 | },
98 | {
99 | "name": "stdin",
100 | "output_type": "stream",
101 | "text": [
102 | "Please enter a number: 42\n"
103 | ]
104 | },
105 | {
106 | "name": "stdout",
107 | "output_type": "stream",
108 | "text": [
109 | "Your number is: 42\n"
110 | ]
111 | }
112 | ],
113 | "source": [
114 | "while True:\n",
115 | " try:\n",
116 | " x = int(input('Please enter a number: '))\n",
117 | " break\n",
118 | " except ValueError:\n",
119 | " print('Not a valid number. Try again...')\n",
120 | "\n",
121 | "print('Your number is:', x)"
122 | ]
123 | },
124 | {
125 | "cell_type": "markdown",
126 | "metadata": {},
127 | "source": [
128 | "这里用一个无限循环中的 `input()` 方法来获取用户输入,然后把输入的字符串用 `int()` 方法转换为一个整数;如果用户没有输入一个整数,`int()` 方法会抛出一个 `ValueError`,于是执行 `except ValueError` 后面的代码块,打印一个提示,然后继续 `while True`,再次提示用户输入;如果 `try` 代码段里第一句执行成功(用户输入成功转换为整数并赋值给 x 变量),那就继续执行后面一句 `break` 终止循环,继续执行其他代码(这时候 x 里面已经有了用户输入的整数)。\n",
129 | "\n",
130 | "在这里异常处理确保用户输入可以转换为整数且赋值给 x,否则就不会继续执行下去,经过这样的处理,在这段代码之后我们可以相当有把握的说:x 里面有个合法的、用户输入的整数值;同时用户不管怎么乱输入也不会对程序构成致命影响,我们预期到可能出现的问题,并做了合理处理。这就是异常处理的意义所在。"
131 | ]
132 | },
133 | {
134 | "cell_type": "code",
135 | "execution_count": 2,
136 | "metadata": {},
137 | "outputs": [
138 | {
139 | "name": "stdout",
140 | "output_type": "stream",
141 | "text": [
142 | "Handling run-time error: division by zero\n"
143 | ]
144 | }
145 | ],
146 | "source": [
147 | "def this_fails():\n",
148 | " x = 1/0\n",
149 | "\n",
150 | "try:\n",
151 | " this_fails()\n",
152 | "except ZeroDivisionError as err:\n",
153 | " print('Handling run-time error:', err)"
154 | ]
155 | },
156 | {
157 | "cell_type": "markdown",
158 | "metadata": {},
159 | "source": [
160 | "这个例子展示了用 `except ZeroDivisionError as err` 这样的语法来取得一个 `err` 对象,这个对象是系统定义的 `Exception` 类型或者子类,里面存放着发生异常时的具体上下文信息,可以打印出来也可以做别的处理。\n",
161 | "\n",
162 | "我们还可以从 `Exception` 派生出我们自己的异常类型,并使用 `raise` 关键字来在出现某种情况时抛出我们定义的异常,并在文档中做出清晰的说明。这样使用我们代码的其他程序员就知道什么情况是我们程序处理不了的,会抛出什么样的异常,并在调用端用捕获异常进行处理。\n",
163 | "\n",
164 | "建议学习 [关于 Python 异常处理的官方教程](https://docs.python.org/3/tutorial/errors.html) 来了解更多。"
165 | ]
166 | },
167 | {
168 | "cell_type": "markdown",
169 | "metadata": {},
170 | "source": [
171 | "## 小结"
172 | ]
173 | },
174 | {
175 | "cell_type": "markdown",
176 | "metadata": {},
177 | "source": [
178 | "* 程序处理用户或其他系统提供的输入时可能出现预期之外的异常状况,可以使用异常处理来捕获异常并进行应急处置;\n",
179 | "* 理解 Python 异常处理的模板含义;\n",
180 | "* 通过例子初步了解异常处理可能的应用场景。"
181 | ]
182 | }
183 | ],
184 | "metadata": {
185 | "kernelspec": {
186 | "display_name": "Python 3",
187 | "language": "python",
188 | "name": "python3"
189 | },
190 | "language_info": {
191 | "codemirror_mode": {
192 | "name": "ipython",
193 | "version": 3
194 | },
195 | "file_extension": ".py",
196 | "mimetype": "text/x-python",
197 | "name": "python",
198 | "nbconvert_exporter": "python",
199 | "pygments_lexer": "ipython3",
200 | "version": "3.7.4"
201 | }
202 | },
203 | "nbformat": 4,
204 | "nbformat_minor": 4
205 | }
206 |
--------------------------------------------------------------------------------
/p1-7-oo-1.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# 理解对象与类:起源篇"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "在现代编程语言中,对象和类是极其常见的概念,绝大部分现代编程语言都或多或少的支持对象和类的概念,Python 也不例外。\n",
15 | "\n",
16 | "对象和类出自于“**面向对象**(*object-oriented, OO*)”这一经典的抽象模型,后面我们还会介绍近年流行起来(其实渊源比面向对象更久远)的“**函数式**(*functional*)”抽象,这些都是软件编程里的思维方法和设计方法,也就是说,并不是一些特定语言或技术,而是一类泛用的**方法论**(*methodology*),他们有个共同的高大上名字叫“**范型**(*paradigm*)”,为什么会有这些东西?如何学习和运用才能从中受益呢?要回答这些问题,就要从软件开发的根本困难说起。\n",
17 | "\n",
18 | "> 也有人称之为“范式”,但一般来说范式对应的英文是“normal form”,比如关系型数据库的那一组范式 1NF/2NF/3NF/BCNF,还有用来定义编程语言语法的巴科斯范式(BNF,Backus Normal Form)。"
19 | ]
20 | },
21 | {
22 | "cell_type": "markdown",
23 | "metadata": {},
24 | "source": [
25 | "## 软件开发的根本困难"
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "metadata": {},
31 | "source": [
32 | "先给结论:**软件开发的根本困难在于管理软件系统的复杂度。**\n",
33 | "\n",
34 | "软件系统的复杂度包括物理上的规模,比如包含多少个源文件,总共多少行源代码;也包含逻辑上的规模,比如有多少子系统,多少模块,多少个功能点,多少个用户界面等;还包含开发和维护系统的人的规模,一个人开发和维护的系统,比一百个人开发和维护的系统要简单多了。\n",
35 | "\n",
36 | "**计算机软件的本质是人类教计算机干活的一系列指令**,如果这些指令错了计算机肯定干不对,有时候甚至会干出可怕的后果(比如波音 737 MAX 型客机连续出现空难的根源就是其自动控制系统中存在软件缺陷)。软件系统的复杂度上去之后,就会带来一系列问题,核心就是软件开发者不能简单清晰的知道到底软件代码是如何在计算机中执行的,那么也就更加无法保证软件在各种情况下的正确性了。\n",
37 | "\n",
38 | "如果我们的软件只是打印一句“Hello world”,那什么也不用讲究,因为它的复杂度几乎为零,不过现实中真正有用的软件往往有着不低的复杂度,动辄成千上万行源代码,要很多人很多年才能做出来。对初学者甚至不需要那么大规模,有一百行源代码就晕了。\n",
39 | "\n",
40 | "> 其实让计算机在显示器上打印出一句话的复杂度是很高的,只不过大量的工作被操作系统和编程语言工具(比如编译器)做了,如果我们界定那些软件都是可靠的,那么就可以说 Hello World 本身复杂度很低。这恰恰是我们下面会讲的,软件开发的核心理念之一“化整为零、责任分离”的例子。"
41 | ]
42 | },
43 | {
44 | "cell_type": "markdown",
45 | "metadata": {},
46 | "source": [
47 | "## 软件危机"
48 | ]
49 | },
50 | {
51 | "cell_type": "markdown",
52 | "metadata": {},
53 | "source": [
54 | "我们多次说过,**计算机软件的本质是人类教计算机干活的一系列指令**,在人类有计算机的初期,软件真的就是一大堆给计算机的指令的列表,比如这样的:\n",
55 | "1. 从某个地方读一个数 a;\n",
56 | "2. 从另一个地方读一个数 b;\n",
57 | "3. 把 a 和 b 加起来;\n",
58 | "4. 在终端上打印结果;\n",
59 | "5. 结束。\n",
60 | "\n",
61 | "但这样平铺直叙只能做很简单的事情,稍微复杂的事情就需要借助“条件判断”才能实现,比如这样:\n",
62 | "1. 从某个地方读一个数 a;\n",
63 | "2. 从另一个地方读一个数 b;\n",
64 | "3. 如果 b 是 0 或者 正数,执行第 4 步,否则执行第 5 步;\n",
65 | "4. 计算 a + b;跳到第 6 步;\n",
66 | "5. 计算 a - b;\n",
67 | "6. 在终端上打印结果;\n",
68 | "7. 结束。\n",
69 | "\n",
70 | "这里就有了条件判断、分支和跳转,注意这里的分支跳转是根据输入的数决定的,而这些数是在软件运行时才确定的,开发的时候程序员并不确定程序的实际执行顺序。这还只是一个非常简单的例子,现实世界的软件是由成千上万这样的代码叠加构成的,其结构和执行路径简直就是“一大锅意大利面”。在那个年代,随着计算机硬件不断提升,软件做的事越来越复杂,于是出现了大批一直完不成的软件项目(一直出错甚至一直没法完整运转起来),软件开发的先行者们发明了一个词“*软件危机*(*software crisis*)”来形容这种窘境。\n",
71 | "\n",
72 | "> “软件危机”这个看上去像好莱坞大片或者游戏大作的词来自北约组织,想想也不意外,那个时候只有政府和军事机构用得起计算机。\n",
73 | "\n",
74 | "软件危机促进了软件开发的第一次革命,人们把工程化方法应用在软件开发领域,并建立了一系列重要的方法论体系来应对软件危机,这其中最重要的两个分别是“**软件质量管理**(*software quality management, SQM*)”和“**结构化编程**(*procedural programming*)”。前者把文档化、软件测试等质量管理方法引入软件开发的完整生命周期中(从需求产生到开发、上线、维护直至被废弃);而后者则引入了软件开发“**模块化**(*modularity*)”的重要概念,这个概念现在也未过时(可预见的将来也不会),其后新的思想和方法都可以看做是它的发扬光大。"
75 | ]
76 | },
77 | {
78 | "cell_type": "markdown",
79 | "metadata": {},
80 | "source": [
81 | "## 模块化"
82 | ]
83 | },
84 | {
85 | "cell_type": "markdown",
86 | "metadata": {},
87 | "source": [
88 | "模块化是结构化编程的核心理念,其实很简单,就是“化整为零、分而治之”的古老智慧的编程版本。既然大型软件系统的复杂度不可避免,那我们就通过“分解”和“组装”来简化:\n",
89 | "* 分解:大型系统分解成中型,中型分解成小型,小型系统分解成一个个子系统和模块,模块分解成若干代码段,最后这些代码段都足够简单,对给定输入给出可预期的输出,易于描述、易于实现、易于测试,这样的代码段通称**过程**(*procedure*)或者“**函数**(*function*)”,也有各种其他称谓,本质相同;\n",
90 | "* 组装:通过调用简单函数来完成更复合、更复杂的任务,不断重复这个堆积木的函数,只要小模块都是正确的,那么组合而成的系统也应该是正确的。\n",
91 | "\n",
92 | "比如前面的例子就变成了:\n",
93 | "* 定义函数 `add`,输入是两个数,输出是两个数相加之和;\n",
94 | "* 定义函数 `sub`,输入是两个数,输出是第一个数减去第二个数的差;\n",
95 | "* 定义函数 `print`,输入是一个数,`print` 函数将其显示在缺省终端上;\n",
96 | "* 定义函数 `main`,完成下述流程:\n",
97 | " 1. 从某个地方读一个数 `a`;\n",
98 | " 2. 从另一个地方读一个数 `b`;\n",
99 | " 3. 如果 `b >= 0` 则定义 `c = add(a, b)`,否则 `c = sub(a, b)`;\n",
100 | " 4. 调用 `print(c)`;\n",
101 | " 5. 结束。\n",
102 | "\n",
103 | "这样整件事被分解成了 `add`、`sub`、`print` 和 `main` 四个函数,每个函数都只做很简单的、易于验证的事情,每个函数可以被不同的人编写和测试,复杂度被分解和降低了;而这些函数可以用严格定义的程序流程(条件分支、循环等)组合起来,形成更大的函数,如此我们就可以从非常简单的积木出发,最终构建起宏伟的城堡。\n",
104 | "\n",
105 | "这种“分而治之”的思想,有个专门的术语来表达,叫做“**责任分离**(*separation of concern, SoC*)”,这里面除了“分”,还隐含着“黑盒”的理念,也就是每个函数搞定自己的任务,调用你的函数不用管你怎么做到的,也不用担心你会做错,各司其职,互不干预,只要输入输出的格式不发生变化,整个系统就能正常运作。这也给了每个函数的实现者最大的自由,可以互不影响地不断优化迭代(比如修正错误、提升性能等)。\n",
106 | "\n",
107 | "模块化另一个显而易见的好处就是更好的“**复用性**(*re-usability*)”,一个模块如果解决了一个普遍性问题,而且实现又正确又高效又健壮(软件的健壮性是个很有意思的专门话题,以后我们会专门讨论),就可以用在很多地方,不需要多次实现。"
108 | ]
109 | },
110 | {
111 | "cell_type": "markdown",
112 | "metadata": {},
113 | "source": [
114 | "## 软件设计范型"
115 | ]
116 | },
117 | {
118 | "cell_type": "markdown",
119 | "metadata": {},
120 | "source": [
121 | "模块化带给我们更可控的复杂度(通过责任分离)和更好的代码复用,但是面对一个现实世界的复杂问题时,要怎么才能设计和构建出一个模块化良好的软件系统呢?先驱们在软件工程实践中发展出了各种方法论体系,也就是我们今天看到的面向对象、函数式等编程范型,它们除了提供模块化的特性以外,还力求做到:\n",
122 | "* 容易学习和掌握;\n",
123 | "* 统一的术语和编程模式;\n",
124 | "* 对经常遇到的问题有开箱即用的解决方案。\n",
125 | "\n",
126 | "这些方法论体系一般是在特定软件系统和特定开发团队中萌芽并逐步发展起来的,所以必然有各自侧重的领域。目前主流的编程语言都是**多范型**(*multi-paradigm*)的,即融合了多种编程范型的特性和优势。我们开始学习编程范型,一般应该这样:\n",
127 | "* 在具体编程语言和场景中学习,而不是为学而学;\n",
128 | "* 理解一种范型的核心价值,它最擅长解决的是什么问题,是通过什么独特的思想和工具解决的;\n",
129 | "* 牢记软件开发的根本困难和模块化等本质性思想,不断用之来检验具体方案。"
130 | ]
131 | },
132 | {
133 | "cell_type": "markdown",
134 | "metadata": {},
135 | "source": [
136 | "## 预告"
137 | ]
138 | },
139 | {
140 | "cell_type": "markdown",
141 | "metadata": {},
142 | "source": [
143 | "好,讲完了历史,下一章我们介绍面向对象的基本概念。"
144 | ]
145 | }
146 | ],
147 | "metadata": {
148 | "kernelspec": {
149 | "display_name": "Python 3",
150 | "language": "python",
151 | "name": "python3"
152 | },
153 | "language_info": {
154 | "codemirror_mode": {
155 | "name": "ipython",
156 | "version": 3
157 | },
158 | "file_extension": ".py",
159 | "mimetype": "text/x-python",
160 | "name": "python",
161 | "nbconvert_exporter": "python",
162 | "pygments_lexer": "ipython3",
163 | "version": "3.7.4"
164 | }
165 | },
166 | "nbformat": 4,
167 | "nbformat_minor": 4
168 | }
169 |
--------------------------------------------------------------------------------
/p1-8-oo-2.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# 理解对象与类:概念篇"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "面向对象是最广为人知的编程范型,有大量的书籍、专著阐述面向对象,大量编程语言和工具以面向对象作为核心特征。编程世界里有个规律,越是流行和广泛应用的东西,越是争议多,OO 也不例外,在 OO 的吐槽者里不乏重量级大牛,他们的吐槽自有他们的道理。"
15 | ]
16 | },
17 | {
18 | "cell_type": "markdown",
19 | "metadata": {},
20 | "source": [
21 | "## 争议"
22 | ]
23 | },
24 | {
25 | "cell_type": "markdown",
26 | "metadata": {},
27 | "source": [
28 | "比如 Erlang 的发明者,[Joe Armstrong](https://en.wikipedia.org/wiki/Joe_Armstrong_(programmer)) 就专门写过一篇文章 [Why OO Sucks](http://harmful.cat-v.org/software/OO_programming/why_oo_sucks) (为什么面向对象逊毙了),这篇文章基本上可以算所有反面向对象人士必引用的檄文,而且非常到位,并不容易反击,其实 Joe Armstrong 很认同面向对象方法里一些核心理念,他反对的是一些冗余和不必要的限制。有一次 Joe 参加一个技术论坛[接受采访时提到](https://www.infoq.com/interviews/johnson-armstrong-oop/),面向对象方法有三点很有价值,分别是**消息、隔离和多态**(*messaging, isolating and polymorphism*),他自己发明的 Erlang 语言以实时并发处理为主要目标,对这三个特性都有很好的设计和实现。他还打过一个令人忍俊不禁的比方:\n",
29 | "\n",
30 | "> 支持 OOP 的语言的问题在于,它们总是随身携带着一堆并不明确的环境——你明明只不过想要个香蕉,可你所获得的是一个大猩猩手里拿着香蕉…… 以及那大猩猩身后的整个丛林!\n",
31 | "> \n",
32 | "> -[Coders at Work](http://www.codersatwork.com)"
33 | ]
34 | },
35 | {
36 | "cell_type": "markdown",
37 | "metadata": {},
38 | "source": [
39 | "另一位大牛,[Rob Pike](https://en.wikipedia.org/wiki/Rob_Pike) 早年是贝尔实验室(Bell Lab)UNIX 组的成员(这个组发明了 UNIX 和 C 语言,分别是现代操作系统和高级编程语言的基石),一直是 C 语言的拥趸,经常讽刺面向对象效率低下,曾经在一个讨论帖里直接把 OOP 比作“[Roman numerals of computing](https://groups.google.com/forum/#!topic/comp.os.plan9/VUUznNK2t4Q%5B151-175%5D)”。后来,他在 Google 和以前贝尔实验室的前辈 [Ken Thompson](https://en.wikipedia.org/wiki/Ken_Thompson) 还有 Google 的软件工程师 [Robert Griesemer](https://github.com/griesemer) 一起发明了 Go 语言,很多人认为这是 C 语言的精神继承者,也是一种多范型编程语言。"
40 | ]
41 | },
42 | {
43 | "cell_type": "markdown",
44 | "metadata": {},
45 | "source": [
46 | "还有一位 [Paul Graham](https://en.wikipedia.org/wiki/Paul_Graham_(programmer)),著名创投基金 YCombinator 的创始人,他干投资前也干开发,YC 有个挺有名的论坛叫 [Hacker News](https://news.ycombinator.com/),用自己发明的函数式编程语言 Arc 写的,因为这语言太少人用,所以作为老板的他亲自维护了好久(前几年好像终于交出去了),基金名字里的 *combinator* 也是个函数式编程术语(所以你知道这位老兄大概是什么的粉丝了)。他在 [Why Arc isn't Especially Object-Oriented](http://www.paulgraham.com/noop.html) 中说他认为 OOP 之所以流行,就是因为平庸程序员(*mediocre programers*)太多,大公司用这种编程范型去阻止那帮家伙,让他们捅不出太大的娄子。以我个人的经验,他这个观点完全对,而且在大公司里这还真是件重要的事情!\n",
47 | "\n",
48 | "> Arc 是最古老的函数式语言 Lisp 的一个变种,在 Lisp 的世界里通称“方言(*dialect*)”。"
49 | ]
50 | },
51 | {
52 | "cell_type": "markdown",
53 | "metadata": {},
54 | "source": [
55 | "所以可以看出来,这些大牛主要是觉得 OO 的体系过于复杂、有很多不必要的限制,因为他们都是顶级聪明的人,顶级聪明人对效率有着近乎偏执的追求,所以他们难以容忍一些东西,而这些东西对这个行业里大部分不那么聪明的人来说可能还真有些用。\n",
56 | "\n",
57 | "对我们来说比较简单,争议归争议,应用归应用——就好像英语的弊端不见得比其他语言少,可就是最流行,那怎么办呢?用呗——虽然该抱怨的时候也得抱怨抱怨,在软件这一行待久了,就知道其实抱怨是创新的动力。\n",
58 | "\n",
59 | "前面提过,编程范型会通过精心选择的概念、术语和语言特性来让自己更容易学习、使用,并对常规问题提供标准的解决方案。面向对象是非常独特的一个编程范型,因为它的核心概念有两层,一层是真正的核心概念,解决了前面提到的模块化思想以及软件工程实践中需要解决的大量实际问题,而另一层是一个“与人们熟悉的现实世界对应的隐喻”,后面这一层显著提升了面向对象方法的亲和力,降低了学习难度,这很可能是面向对象如此流行的重要原因。我们先介绍这个现实世界的隐喻层,再来了解面向对象最本质的那些特性。"
60 | ]
61 | },
62 | {
63 | "cell_type": "markdown",
64 | "metadata": {},
65 | "source": [
66 | "## 类和对象"
67 | ]
68 | },
69 | {
70 | "cell_type": "markdown",
71 | "metadata": {},
72 | "source": [
73 | "顾名思义,面向对象的方法把一切事物都看作“**对象**(*object*)”,而把类似事物的共性特征抽象出来称为“**类**(*class*)”。\n",
74 | "\n",
75 | "反过来说,*class* 定义一类事物的特征,就像一个模板,需要时可以按照这个模板创造(或者描述)一个具体的 *object* 出来。\n",
76 | "\n",
77 | "> **Class vs. Object** \n",
78 | "> 在面向对象的编程语言里,程序具体操作的都是 *object*,而 *class* 只是描述一类 *object* 共性的模板。\n",
79 | "\n",
80 | "举例来说,我的工作台上有个台灯,这个台灯是一个对象,它拥有亮度、色温、电压等属性,还有一个操作界面——开关,操作一次就打开了,再操作一次就关了。\n",
81 | "\n",
82 | "经过仔细思考,我们发现所有的灯基本都有这些属性和操作界面,所以我们可以抽象出一个“灯”的类(*class*)来,有亮度、色温、电压这些属性,再提供一个接口叫开关,不管什么灯,其属性和接口都叫一样的名字,操作方法都一样。使用灯的我们(或者其他程序,现在不是有很多计算机控制的灯吗),只需要与“开关”这个接口打交道,而不必关心灯泡内部的设计和原理——说实话,这是个很伟大的设计思想,不仅实现了模块化,而且用现实世界做参照,一下子就能理解和学会。\n",
83 | "\n",
84 | "在程序设计过程中,我们常常需要对标现实世界里的事物做**抽象**(*abstract*),抽象是为了更高效地描述现实世界而进行的“取舍”,只要抓准核心特征,其他的都可以省略,从而省下好多工作量。\n",
85 | "\n",
86 | "这个手段,漫画家们最常用。为什么你看到下面的图片觉得它们俩看起来像是人?尤其是在你明明知道那肯定不是人的情况下,却已然接受那是两个漫画小人的形象?"
87 | ]
88 | },
89 | {
90 | "cell_type": "markdown",
91 | "metadata": {},
92 | "source": [
93 | "
"
94 | ]
95 | },
96 | {
97 | "cell_type": "markdown",
98 | "metadata": {},
99 | "source": [
100 | "这种描绘方式,就是抽象,很多“没必要”的细节都被去掉了(或者反过来说,没有被采用),留下的两个特征,一个是头,一个是双眼——连那双“眼睛”都抽象到只剩下一个黑点了。我们在前面关于灯的例子里,也忽略了灯的形状、尺寸、颜色、质地等特征,因为大部分时候我们用不到。\n",
101 | "\n",
102 | "> 当然,这种“必要性”和具体问题有关,如果你要处理的是室内设计问题,那可能大小和颜色就很重要了,也需要加入到抽象出来的 *class* 里去。\n",
103 | "\n",
104 | "这种被选出来的“必要的特征”,叫做对象的“**属性**(*attributes*)”,进而,这些抽象的对象,实际上也能做一些抽象过后被保留下来的“必要的行为”,比如,说话,哭笑,这些叫做对象的“**方法**(*methods*)”。\n",
105 | "\n",
106 | "从面向对象的编程语言角度去看世界,要定义一类事物,就建立一个 *class*,*class* 定义两类东西:\n",
107 | "\n",
108 | "* **属性**:用自然语言描述,通常是名词,表示这类事物拥有的共性特征;\n",
109 | "* **方法**:用自然语言描述,通常是动词,表示我们可以对这类事物做什么,或者请求它做什么。\n",
110 | "\n",
111 | "面向对象的编程语言会在需要时用这个 *class* 做模板,创建出一个具体 *object*,让我们很方便的操作这个对象,就像操作一件真实的物品。\n",
112 | "\n",
113 | "这种思维模式非常经济,而且易于理解:基于现实世界参照物,去掉不必要的东西,只留下对我们有用的抽象模型。可以这么说,如果没有这个思维方法,今天从事软件开发的人大概会少一半。"
114 | ]
115 | },
116 | {
117 | "cell_type": "markdown",
118 | "metadata": {},
119 | "source": [
120 | "## 访问控制"
121 | ]
122 | },
123 | {
124 | "cell_type": "markdown",
125 | "metadata": {},
126 | "source": [
127 | "面向对象的语言允许设置类的属性和方法是否对外可见,外部可以访问的叫公共的(*public*),不可见的叫私有的(*private*)。\n",
128 | "\n",
129 | "这种设计是为了贯彻“责任分离”的原则,如果类里面有些数据和过程只被类自己内部使用,那么就不应该被外部看到,从而留下最大限度的修改灵活性;而所有 *public* 属性和方法,是会被其他程序使用的,其输入输出的规格需要尽可能稳定,否则一改就有一大堆用到的地方要跟着改,非常麻烦而且容易出错。\n",
130 | "\n",
131 | "简言之,类的 *public* 部分就像店铺的门面招牌,里面可以随便折腾,但招牌轻易不能动。\n",
132 | "\n",
133 | "*Public* 属性和方法因为具有这样特殊又重要的定位,赢得了一个特定名词叫“**接口**(*interface*)”,接口的意义重大,有的面向对象编程语言干脆把 `interface` 单独拿出来,作为和 `class` 一样的的关键字。"
134 | ]
135 | },
136 | {
137 | "cell_type": "markdown",
138 | "metadata": {},
139 | "source": [
140 | "## 抽象层次"
141 | ]
142 | },
143 | {
144 | "cell_type": "markdown",
145 | "metadata": {},
146 | "source": [
147 | "前面可以看到面向对象的方法通过现实世界的事物抽象为 *class* 来实现“分而治之”,下面我们来看看怎么通过多层次抽象来加强这种“责任分离”,同时带来更清晰和优雅的“复用”。还是用灯的例子开始。\n",
148 | "\n",
149 | "我们上面已经把一般的灯处理的不错了,现在我们碰到了一种新式的灯,除了拥有一般灯的特征,还可以多档调节亮度,研究一番之后我们发现这种新式灯拥有一般灯的所有属性和方法,再加一个“亮度范围”的 *attribute*,一个“调节亮度”的 *method*,就能描述好了。那么我们是不是要把“灯”这个类照抄一遍,再加上这些新东西呢?在编程的世界里长得(几乎)一模一样的两个东西永远是不好的,因为它们有相当大部分是可以复用的,如果各起炉灶从头做,既重复劳动又不好维护(如果有问题你就要改两个地方),而面向对象的方法提供了现成的解决方案:**继承**和**子类**。\n",
150 | "\n",
151 | "面向对象的编程语言允许我们为“灯”这个类创建一个“**子类**(*sub-class*)”,这个子类拥有父类的一切(不需要再写一遍),然后还可以拥有自己添加的任何东西,这叫做“**继承**(*inheritance*)”,这个术语又是对现实世界的隐喻,而且真像那么回事。\n",
152 | "\n",
153 | "“灯”这个类处理了电压、亮度、色温和开关,“可调亮度灯”这个类继承了这一切,再处理了调节亮度的问题,非常完美的做到了责任分离。最妙的是,这个操作可以一直做,从“灯”开始,你可以派生出各种各样的灯,会变色的、分档调亮和无级调亮的等等。"
154 | ]
155 | },
156 | {
157 | "cell_type": "markdown",
158 | "metadata": {},
159 | "source": [
160 | "与继承相反,还有一种反向操作,就是抽象出更一般性的类,这叫“泛化(*generalization*)”。比如我们发现除了灯以外,还有别的东西也是带一个开关的,比如水龙头、电扇、电视等等,我们可以给所有这些有开关的事物抽象出一个类“可开关设备”,里面就只有一个方法叫“开关”,并把“灯”里关于开关的处理代码挪到“可开关设备”里,然后让“灯”继承“可开关设备”,这样“灯”仍然保持以前的属性和方法,但是我们在创建比如“电视”类时,就可以继承“可开关设备”,直接得到“开关”这个方法的所有逻辑和代码,“分而治之”和“复用”进一步得到了提升。\n",
161 | "\n",
162 | "*Generalization* 是一种重要的抽象思维方法,如果目前理解起来还有点困难,后面结合例子会更清楚。"
163 | ]
164 | },
165 | {
166 | "cell_type": "markdown",
167 | "metadata": {},
168 | "source": [
169 | "面向对象的思维方法和编程语言,提供了强有力的抽象和建模工具。软件开发人员分析现实世界的问题和概念,然后建立对应的 *class* 来描述不同概念的属性和方法,把更一般性的共性用“泛化”抽象成公共的父类,根据需要“派生”出特化的子类,就能建立一个现实世界在计算机里的模型,而当现实世界的事物发生变化,只要找出变化部分对应的 *class* 进行相应修改就好了。\n",
170 | "\n",
171 | "另外补充一下,现实世界映射是面向对象方法中常用的手段,但也有很多 *class* 并不来源于现实世界的事物,而只是为了满足我们的抽象需要,比如上面那个“可开关设备”,就是一个抽象的共性特征而已。"
172 | ]
173 | },
174 | {
175 | "cell_type": "markdown",
176 | "metadata": {},
177 | "source": [
178 | "## 多态"
179 | ]
180 | },
181 | {
182 | "cell_type": "markdown",
183 | "metadata": {},
184 | "source": [
185 | "**多态**(*polymorphism*)是理解面向对象方法的一个分水岭,前面都是和现实世界有类比的比较直观好懂的概念,从这里开始,抽象程度一下子就上去了,但这是非常非常重要也避不开的概念,很多专家甚至认为多态是比继承更本质的概念,因为继承有替代方案(以后我们会讲到),而多态没有。\n",
186 | "\n",
187 | "> 因为多态是一个计算机程序范畴的概念,所以这一段的描述不可避免的要涉及具体的代码例子,我们这里用到的代码以 Python 书写,但其概念是适用于任何面向对象编程语言的。\n",
188 | "\n",
189 | "我们还是回到灯的例子。上面其实留了个小问题,那就是“可调亮度灯”的“亮度”这个属性,其实和不可调亮度的灯有点差别,它到底是指最低亮度、最高亮度还是当前亮度呢?我们选择的方案是用来指当前亮度,而增加一个亮度范围来描述可调的最低最高亮度。这种情况在实际软件开发实践中很常见,就是子类对父类定义过的属性或者方法会有一定的修改或者特化,在面向对象的术语中,这种修改和特化叫“**覆盖**(*override*)”。\n",
190 | "\n",
191 | "我们实现了一个获取灯的当前亮度的功能,这显然对所有灯都适用,所以应该在父类“灯”里添加这个“获取亮度”的方法,这样“灯”和它所有子类都有了这个功能。当然,在“可调亮度灯”类里,这个获取亮度的方法会有区别,可以 *override* 这个方法的实现,但接口遵循父类的定义没变,还是返回亮度数值,这种父类和子类“接口一致但实现各异”的做法叫做**面向接口编程**(*interface-based programming*),是实现多态的一个关键前提。\n",
192 | "\n",
193 | "假定我们需要开发一个家里所有灯统一的中央控制系统,这个系统能显示所有灯的亮度,假如家里有十盏灯,我们就有十个灯的对象,有的是“灯”类的,有的是“可调亮度灯”类的,有的是别的不知道什么类的,这些灯的对象在安装的时候就创建好放在中央控制系统中,用一个列表 `lights_in_house` 保存着。\n",
194 | "\n",
195 | "这个列表里的对象要么是“灯”类对象,要么是“灯”的某个子类对象,所以都有“获取亮度”这个方法,所以显示所有灯亮度的方法可以这么写:\n",
196 | "\n",
197 | "```\n",
198 | "for light in lights_in_house:\n",
199 | " display(light.brightness)\n",
200 | "```\n",
201 | "\n",
202 | "这两行代码的意思是:依次取出 `lights_in_house` 里的每一个元素(将其赋值给循环变量 `light`),然后执行:\n",
203 | "1. 获取 `light` 的 `brightness` 属性的数值;\n",
204 | "2. 将其作为输入参数调用 `display` 函数(简单起见我们没写出这个函数实现,但它做的事情容易理解,就是把亮度数值显示出来)。\n",
205 | "\n",
206 | "注意获取灯的亮度是用 `light.brightness` 这一段实现的,奇妙的是,这个 `light` 可能是不同类的对象,程序在运行时才知道实际上是什么类,程序运行中会根据它的类型自动运行那个类对应的代码来获取亮度,不管它是一般的还是可调亮度的灯。\n",
207 | "\n",
208 | "这就意味着,我们在编写代码的时候可以不管一个对象是什么类的对象,只要它支持某个属性或者方法,就可以直接使用,编程语言(编译器、运行环境或者解释器)会在运行时自动根据其实际类型执行正确版本的代码。简单地说,这就是面向对象语言的“**多态**(*polymorphism*)”特性。\n",
209 | "\n",
210 | "这是一种强大的”**责任分离**(*separation of concern*)”工具,上面的例子中,为了显示亮度,完全不需要知道“灯”类有哪些子类,只要知道 **“灯”和它的子类都有 brightness 这个属性** 就行了,只要这一点不变,上面那段代码一直成立,我们不管以后买了什么奇奇怪怪的灯,给“灯”类扩展了多少子类,都不影响上面那段代码。多么优雅而又方便!\n",
211 | "\n",
212 | "> **Override and Polymorphism** \n",
213 | "> *Override* 和 *polymorphism*,让我们可以分别定义父类和子类对相同接口的不同实现,而在运行时系统会根据调用时对象的实际类型自动选择正确版本来运行,从而将类的定义和使用充分解耦,给出了大量实际场景下“责任分离”的优雅方案。"
214 | ]
215 | },
216 | {
217 | "cell_type": "markdown",
218 | "metadata": {},
219 | "source": [
220 | "## 面向对象编程的分支"
221 | ]
222 | },
223 | {
224 | "cell_type": "markdown",
225 | "metadata": {},
226 | "source": [
227 | "客观的说,面向对象编程的一些术语和机制确实存在不必要的复杂、重叠甚至冲突,这和面向对象方法及技术的发展历史有关,这里简单提一下。\n",
228 | "\n",
229 | "历史上面向对象的发展其实有两个分支,一个以 Smalltalk 语言为代表,另一个以 C++ 语言为代表,他们都有 *class* 和 *object* 的概念,但也有非常大的差异,最后 C++ 依靠 C 的兼容性赢下了标准之争,成为实际上面向对象的“正宗”,而 Smalltalk 只能作为一个小众语言存在,虽然在很多专家心目中 Smalltallk 才是更优秀/先进的那个,这也是计算机行业屡见不鲜的事了。\n",
230 | "\n",
231 | "> 不过 Smalltalk 有个大弟子叫做 Objective-C,是 Steve Jobs 离开 Apple 创办 NeXT 的时候选择的系统语言,后来被带回苹果,成为苹果生态下 OS X 和 iOS 的唯一开发语言,直到 Swift 语言出现。顺便说一句,Objective-C 也是兼容 C 语言的,在 Web 成为一大主流之前 C 语言简直是编程世界的主宰。\n",
232 | "\n",
233 | "这两个派系有些东西是各自独有的,有些东西虽然两边都有,但是叫法和实现思路迥异,最大的一个差异就是前面提过的 Joe Armstrong 喜欢的 “messaging”,Smalltalk 的这个概念用了一个独特的隐喻,不强调对象的属性和方法,而代之以“消息”,当要使用某个对象时唯一的操作就是向这个对象发送一条消息,这个消息说明了发送方想要什么(获取信息或请求对象执行某些操作),接受消息的对象响应这些消息返回相应数据或者执行相应操作,这个隐喻也很直观,而且从这个概念发展开去,Smalltalk 建立了一套简洁灵活的面向对象编程工具集。这种体系的具体优势和劣势在哪里,涉及到更深入的软件设计思想和实践,我们就不在这里展开了。"
234 | ]
235 | },
236 | {
237 | "cell_type": "markdown",
238 | "metadata": {},
239 | "source": [
240 | "## 预告"
241 | ]
242 | },
243 | {
244 | "cell_type": "markdown",
245 | "metadata": {},
246 | "source": [
247 | "所有这些概念看着可能有些抽象和枯燥,但必须有个地方把这些都说清楚,如果有些东西没搞明白也没关系,在下面的实例学习中可以随时对照着来回看。"
248 | ]
249 | }
250 | ],
251 | "metadata": {
252 | "kernelspec": {
253 | "display_name": "Python 3",
254 | "language": "python",
255 | "name": "python3"
256 | },
257 | "language_info": {
258 | "codemirror_mode": {
259 | "name": "ipython",
260 | "version": 3
261 | },
262 | "file_extension": ".py",
263 | "mimetype": "text/x-python",
264 | "name": "python",
265 | "nbconvert_exporter": "python",
266 | "pygments_lexer": "ipython3",
267 | "version": "3.7.4"
268 | }
269 | },
270 | "nbformat": 4,
271 | "nbformat_minor": 4
272 | }
273 |
--------------------------------------------------------------------------------
/p1-9-oo-3.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# 理解对象与类:Python 篇"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "在前面大套的历史背景和理论概念解说之后,我们来看看 Python 中对面向对象概念的具体实现,主要结合一些代码例子进行说明。"
15 | ]
16 | },
17 | {
18 | "cell_type": "markdown",
19 | "metadata": {},
20 | "source": [
21 | "## 类定义与对象创建"
22 | ]
23 | },
24 | {
25 | "cell_type": "markdown",
26 | "metadata": {},
27 | "source": [
28 | "首先我们来看看类的定义和对应对象的创建:"
29 | ]
30 | },
31 | {
32 | "cell_type": "code",
33 | "execution_count": 1,
34 | "metadata": {},
35 | "outputs": [],
36 | "source": [
37 | "class Dog:\n",
38 | "\n",
39 | " kind = ''\n",
40 | "\n",
41 | " def __init__(self, name):\n",
42 | " self.name = name\n",
43 | " \n",
44 | " def bark(self):\n",
45 | " return 'woof-woof'\n",
46 | "\n",
47 | "a = Dog('Fido')\n",
48 | "b = Dog('Buddy')"
49 | ]
50 | },
51 | {
52 | "cell_type": "markdown",
53 | "metadata": {},
54 | "source": [
55 | "上面这段代码逐行解释如下:\n",
56 | "* 第一行是关键字 `class` 打头,代表类定义的开始,后面是类的名字,然后是一个冒号,表示下面缩进的代码段是 `Dog` 类的定义;在 Python 中类本身也是个对象,叫**类对象**(*class object*);\n",
57 | " * 你现在能感觉到,Python 的冒号通常都是这个作用,前面看到过的 `if` `else` `for` `while` `def` 都类似;\n",
58 | "* 下面是一个在类定义里直接出现的变量 `kind`,这样定义的变量称为**类变量**(*class variable*),这种变量是整个类共有的,所有该类的对象实例共享;\n",
59 | "* 下面是一个函数 `__init__()` 的定义,因为这个函数定义在类里,所以通常我们称之为**方法**(*method*):\n",
60 | " * 方法的第一参数必定是 `self`,这是个特殊的变量,代表从这个类实例化出来的对象自己;注意这里是类的定义,还没有真正创建出对象,这个 `self` 相当于对未来产生对象的“提前引用”;类定义中的方法在被调用时解释器都会自动传递这个 `self` 进去;\n",
61 | " * 作为一种约定俗成的规矩,类似 `__init__()` 用**双下划线**开头和结尾的方法,属于**特殊方法**(*special method*),是 Python 解释器特别定义和使用的;\n",
62 | " * `__init__()` 是解释器在实例化一个对象之后自动调用的方法,用来对新创建的对象实例做初始化;\n",
63 | " * 方法可以在 `self` 之后带任意的输入参数,这些输入参数由方法自行使用;\n",
64 | " * `self.name` 定义了一个**实例变量**(*instance variable*),前面说了 `self` 代表未来被实例化的对象自身,`.` 表示属于对象的,`name` 则是这个实例变量的名字,这个方法用传进来的参数 `name` 来初始化实例变量 `self.name`,要注意这两个 `name` 是不一样的;\n",
65 | "* 下面是函数 `bark()` 的定义,这是我们自定义的方法,没有自定义的输入参数(例行的 `self` 参数不算),然后返回 *dog* 的叫声;\n",
66 | "* 缩进的类定义部分结束,下面是创建这个类的对象实例的方法,`a = Dog('Fido')`:\n",
67 | " * 一个类的实例化,就是用这个类作为模板创建一个实际存在的对象,Python 的语法是把类对象 `Dog` 当做一个函数来使用,`Dog('Fido')` 看上去就是个函数,它的意思是:创建一个 `Dog` 类的**对象实例**(*instance object*),并用以参数 `'Fido'` 调用类的 `__init__()` 方法;\n",
68 | " * 解释器会在内存中创建这个对象实例,然后用参数 `'Fido'` 调用类的 `__init__(self, name)` 方法,第一个参数 `self` 解释器会自动放进去(就是刚刚创建好的对象实例),而 `name` 参数就是 `'Fido'`,`__init__()` 方法运行完毕就会把这个对象的 `self.name` 实例变量设为 `'Fido'`;\n",
69 | " * 实例化执行完毕,将创建的对象赋给 `a` 变量。\n",
70 | "* 创建第二个对象实例 `b`。\n",
71 | "\n",
72 | "创建出来的两个对象实例就可以使用了:"
73 | ]
74 | },
75 | {
76 | "cell_type": "code",
77 | "execution_count": 2,
78 | "metadata": {},
79 | "outputs": [
80 | {
81 | "data": {
82 | "text/plain": [
83 | "'canidae'"
84 | ]
85 | },
86 | "execution_count": 2,
87 | "metadata": {},
88 | "output_type": "execute_result"
89 | }
90 | ],
91 | "source": [
92 | "# 类变量可以通过 class object 来访问,并在所有实例变量之间共享\n",
93 | "Dog.kind = 'canidae'\n",
94 | "a.kind"
95 | ]
96 | },
97 | {
98 | "cell_type": "code",
99 | "execution_count": 3,
100 | "metadata": {},
101 | "outputs": [
102 | {
103 | "data": {
104 | "text/plain": [
105 | "'canidae'"
106 | ]
107 | },
108 | "execution_count": 3,
109 | "metadata": {},
110 | "output_type": "execute_result"
111 | }
112 | ],
113 | "source": [
114 | "b.kind"
115 | ]
116 | },
117 | {
118 | "cell_type": "code",
119 | "execution_count": 4,
120 | "metadata": {},
121 | "outputs": [
122 | {
123 | "data": {
124 | "text/plain": [
125 | "'Fido'"
126 | ]
127 | },
128 | "execution_count": 4,
129 | "metadata": {},
130 | "output_type": "execute_result"
131 | }
132 | ],
133 | "source": [
134 | "# 而实例变量是各个对象实例自己拥有,互不影响\n",
135 | "a.name"
136 | ]
137 | },
138 | {
139 | "cell_type": "code",
140 | "execution_count": 5,
141 | "metadata": {},
142 | "outputs": [
143 | {
144 | "data": {
145 | "text/plain": [
146 | "'Buddy'"
147 | ]
148 | },
149 | "execution_count": 5,
150 | "metadata": {},
151 | "output_type": "execute_result"
152 | }
153 | ],
154 | "source": [
155 | "b.name"
156 | ]
157 | },
158 | {
159 | "cell_type": "code",
160 | "execution_count": 6,
161 | "metadata": {},
162 | "outputs": [
163 | {
164 | "data": {
165 | "text/plain": [
166 | "'woof-woof'"
167 | ]
168 | },
169 | "execution_count": 6,
170 | "metadata": {},
171 | "output_type": "execute_result"
172 | }
173 | ],
174 | "source": [
175 | "# 当然我们也可以使用我们定义的方法\n",
176 | "a.bark()"
177 | ]
178 | },
179 | {
180 | "cell_type": "markdown",
181 | "metadata": {},
182 | "source": [
183 | "如果前面的看上去有点复杂,可以先努力理解和记住下面的几个要点:\n",
184 | "* 可以用 `class MyClass:` 的语法来定义类;\n",
185 | "* 类定义中直接定义的变量是类变量(*class variable*),为所有对象实例所共享,可用 `MyClass.xxx` 的语法访问;\n",
186 | "* 类定义中出现的 `self.xxx` 是实例变量(*instance variable*),是各个对象实例各自的属性(*attribute*),互不影响,对象实例创建后可以用 `obj.xxx` 的语法访问;\n",
187 | "* 类定义中通常会定义初始化方法 `__init__(self, param1, param2, ...)`,该方法会在对象实例创建后自动被调用,其参数中 `self` 是固定的,而后面如果有其他参数,需要在创建对象实例时传给类对象,类似这样:`obj = MyClass(param1, param2, ...)`,这两个地方的参数表是对应上的;\n",
188 | "* 类定义中定义的函数是对象实例的方法(*method*),可以用 `obj.method()` 的语法调用;所有方法的第一个参数固定为 `self`,其他参数的使用方法与前述 `__init__` 方法一样。"
189 | ]
190 | },
191 | {
192 | "cell_type": "markdown",
193 | "metadata": {},
194 | "source": [
195 | "## 第二个类"
196 | ]
197 | },
198 | {
199 | "cell_type": "markdown",
200 | "metadata": {},
201 | "source": [
202 | "有狗就有猫,我们现在可以依葫芦画瓢的定义一个猫类出来:"
203 | ]
204 | },
205 | {
206 | "cell_type": "code",
207 | "execution_count": 7,
208 | "metadata": {},
209 | "outputs": [],
210 | "source": [
211 | "class Cat:\n",
212 | "\n",
213 | " kind = 'felidae'\n",
214 | "\n",
215 | " def __init__(self, name):\n",
216 | " self.name = name\n",
217 | " \n",
218 | " def mew(self):\n",
219 | " return 'meow-meow'\n",
220 | "\n",
221 | "c = Cat('Garfield')"
222 | ]
223 | },
224 | {
225 | "cell_type": "code",
226 | "execution_count": 8,
227 | "metadata": {},
228 | "outputs": [
229 | {
230 | "data": {
231 | "text/plain": [
232 | "'felidae'"
233 | ]
234 | },
235 | "execution_count": 8,
236 | "metadata": {},
237 | "output_type": "execute_result"
238 | }
239 | ],
240 | "source": [
241 | "Cat.kind"
242 | ]
243 | },
244 | {
245 | "cell_type": "code",
246 | "execution_count": 9,
247 | "metadata": {},
248 | "outputs": [
249 | {
250 | "data": {
251 | "text/plain": [
252 | "'Garfield'"
253 | ]
254 | },
255 | "execution_count": 9,
256 | "metadata": {},
257 | "output_type": "execute_result"
258 | }
259 | ],
260 | "source": [
261 | "c.name"
262 | ]
263 | },
264 | {
265 | "cell_type": "code",
266 | "execution_count": 10,
267 | "metadata": {},
268 | "outputs": [
269 | {
270 | "data": {
271 | "text/plain": [
272 | "'meow-meow'"
273 | ]
274 | },
275 | "execution_count": 10,
276 | "metadata": {},
277 | "output_type": "execute_result"
278 | }
279 | ],
280 | "source": [
281 | "c.mew()"
282 | ]
283 | },
284 | {
285 | "cell_type": "markdown",
286 | "metadata": {},
287 | "source": [
288 | "和前面的狗类好像没有太大区别,因为从特征上基本一致:无论是猫还是狗,都有一个类变量 `kind`(动物的分类),以及一个实例变量 `name`(动物的名字),然后有个方法来发出叫声。这时候我们可以想到的是,是不是可以将这两个类的共性特征抽取出来,用一个公共的父类来实现呢?"
289 | ]
290 | },
291 | {
292 | "cell_type": "markdown",
293 | "metadata": {},
294 | "source": [
295 | "## 父类、子类、继承与多态"
296 | ]
297 | },
298 | {
299 | "cell_type": "markdown",
300 | "metadata": {},
301 | "source": [
302 | "首先我们定义一个比猫和狗都更加抽象的类,准备用作猫和狗的共通父类,这个类把上面说的共性特征都表达出来,想一下应该怎么写,然后看下面的代码:"
303 | ]
304 | },
305 | {
306 | "cell_type": "code",
307 | "execution_count": 11,
308 | "metadata": {},
309 | "outputs": [],
310 | "source": [
311 | "class Animal:\n",
312 | "\n",
313 | " kind = ''\n",
314 | "\n",
315 | " def __init__(self, name):\n",
316 | " self.name = name\n",
317 | " \n",
318 | " def voice(self):\n",
319 | " return '...'"
320 | ]
321 | },
322 | {
323 | "cell_type": "markdown",
324 | "metadata": {},
325 | "source": [
326 | "我们可以看到,`Animal` 类具有类变量 `kind`、实例变量 `self.name` 和一个方法 `voice()`,但是没有给出具体的分类名和叫声是什么。\n",
327 | "\n",
328 | "下面我们看看怎么继承这个父类来定义子类,在 Python 中要继承一个已存在的类,使用下面这样的语法:"
329 | ]
330 | },
331 | {
332 | "cell_type": "code",
333 | "execution_count": 12,
334 | "metadata": {},
335 | "outputs": [],
336 | "source": [
337 | "class Dog(Animal):\n",
338 | " \n",
339 | " kind = 'canidae'\n",
340 | " \n",
341 | " def voice(self):\n",
342 | " return 'woof-woof'\n",
343 | " \n",
344 | " bark = voice"
345 | ]
346 | },
347 | {
348 | "cell_type": "markdown",
349 | "metadata": {},
350 | "source": [
351 | "`class` 语句中类名后面可以紧跟一个括号,里面是要继承的父类,这样定义出来的子类就直接拥有了父类的一切,但可以修改,上面的定义就修改了类变量 `kind` 的值,给出了 `voice()` 方法的一个狗类实现(“汪汪汪”),并定义了一个自己独有的方法 `bark`(作为 `voice` 方法的一个别名)。下面的猫类也类似:"
352 | ]
353 | },
354 | {
355 | "cell_type": "code",
356 | "execution_count": 13,
357 | "metadata": {},
358 | "outputs": [],
359 | "source": [
360 | "class Cat(Animal):\n",
361 | " \n",
362 | " kind = 'felidae'\n",
363 | " \n",
364 | " def voice(self):\n",
365 | " return 'meow-meow'\n",
366 | " \n",
367 | " mew = voice"
368 | ]
369 | },
370 | {
371 | "cell_type": "markdown",
372 | "metadata": {},
373 | "source": [
374 | "注意这两个子类都没写构造方法 `__init__()`,因为父类的就好用,不需要改。现在我们可以使用这两个子类来实例化一些对象出来:"
375 | ]
376 | },
377 | {
378 | "cell_type": "code",
379 | "execution_count": 14,
380 | "metadata": {},
381 | "outputs": [],
382 | "source": [
383 | "a = Animal('キュゥべえ')\n",
384 | "c = Cat('Garfield')\n",
385 | "d = Dog('Snoopy')"
386 | ]
387 | },
388 | {
389 | "cell_type": "code",
390 | "execution_count": 15,
391 | "metadata": {},
392 | "outputs": [
393 | {
394 | "name": "stdout",
395 | "output_type": "stream",
396 | "text": [
397 | "キュゥべえ : ...\n",
398 | "Garfield : meow-meow\n",
399 | "Snoopy : woof-woof\n"
400 | ]
401 | }
402 | ],
403 | "source": [
404 | "for animal in [a, c, d]:\n",
405 | " print(animal.name, ':', animal.voice())"
406 | ]
407 | },
408 | {
409 | "cell_type": "markdown",
410 | "metadata": {},
411 | "source": [
412 | "仔细看上面的代码,`a` `c` `d` 虽然是不同类的对象实例,但是因为它们都是 `Animal` 或其子类的对象实例,所以拥有 `Animal` 类所定义的标准接口(**继承**),从而可以在一个循环中一视同仁的处理,而且不同类的对象实例自动调用了各自类定义的 `voice()` 方法实现,产生各自正确的输出(**多态**)。\n",
413 | "\n",
414 | "所以其实也不复杂吧?"
415 | ]
416 | },
417 | {
418 | "cell_type": "markdown",
419 | "metadata": {},
420 | "source": [
421 | "## 公有和私有?"
422 | ]
423 | },
424 | {
425 | "cell_type": "markdown",
426 | "metadata": {},
427 | "source": [
428 | "和 C++、Java 等面向对象编程语言不一样,Python 并没有私有成员一说,从语法上讲,所有类定义中出现的类变量、对象变量、方法都是公开的。\n",
429 | "\n",
430 | "但是 Python 同时给出了一个相对宽松的“弱约束”,就是作为一种约定俗成的代码风格,将类定义中所有下划线 `_` 开头的变量和方法作为“内部”变量和方法来看待,也就是说这些变量和方法属于类的内部实现的一部分,随时可能改变,外部最好不要依赖于这些变量和方法。\n",
431 | "\n",
432 | "所以 Python 虽然没有提供强制受限的“私有成员”,但还是遵循了“接口与实现分离”的思维模式,我们在定义一个类的时候,如果有不希望外部直接使用的变量和方法,可以在其名字前加一个下划线 `_`。"
433 | ]
434 | },
435 | {
436 | "cell_type": "markdown",
437 | "metadata": {},
438 | "source": [
439 | "## 小结"
440 | ]
441 | },
442 | {
443 | "cell_type": "markdown",
444 | "metadata": {},
445 | "source": [
446 | "* 了解在 Python 中如何定义类和创建类的对象实例;\n",
447 | "* 了解在 Python 中的继承和多态的实现方法。\n",
448 | "\n",
449 | "现在可以再读一遍前一章的概念,应该会有不同的感受。"
450 | ]
451 | }
452 | ],
453 | "metadata": {
454 | "kernelspec": {
455 | "display_name": "Python 3",
456 | "language": "python",
457 | "name": "python3"
458 | },
459 | "language_info": {
460 | "codemirror_mode": {
461 | "name": "ipython",
462 | "version": 3
463 | },
464 | "file_extension": ".py",
465 | "mimetype": "text/x-python",
466 | "name": "python",
467 | "nbconvert_exporter": "python",
468 | "pygments_lexer": "ipython3",
469 | "version": "3.7.4"
470 | }
471 | },
472 | "nbformat": 4,
473 | "nbformat_minor": 4
474 | }
475 |
--------------------------------------------------------------------------------
/p2-1-function-def.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# 函数定义再探"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "我们已经见过不少函数,也自己写过一些函数。我们已经理解函数的概念来自代数:从**输入参数**出发,**计算**出函数的**返回值**;我们也知道可以用 `def foo():` 来定义函数。其实函数的定义非常复杂,我们不太能够在第一次介绍时就讲清楚,所以之前我们就采取“先引入用起来”的方法,这也是一种知识上的“提前引用”。\n",
15 | "\n",
16 | "这一章我们就围绕函数定义深入看看。"
17 | ]
18 | },
19 | {
20 | "cell_type": "markdown",
21 | "metadata": {},
22 | "source": [
23 | "## 为函数命名"
24 | ]
25 | },
26 | {
27 | "cell_type": "markdown",
28 | "metadata": {},
29 | "source": [
30 | "哪怕一个函数内部什么都不干,它也得有个名字,然后名字后面要加上圆括号 `()`,以明示它是个函数,而不是某个变量。"
31 | ]
32 | },
33 | {
34 | "cell_type": "code",
35 | "execution_count": 1,
36 | "metadata": {},
37 | "outputs": [],
38 | "source": [
39 | "def do_nothing():\n",
40 | " pass\n",
41 | "\n",
42 | "do_nothing()"
43 | ]
44 | },
45 | {
46 | "cell_type": "markdown",
47 | "metadata": {},
48 | "source": [
49 | "这就是个“什么也不干”的函数,关键字 `pass` 就是什么也不干的意思。"
50 | ]
51 | },
52 | {
53 | "cell_type": "markdown",
54 | "metadata": {},
55 | "source": [
56 | "给函数命名(给变量命名也一样)需要遵循的一些规则如下:\n",
57 | "* 首先,名称不能以数字开头,能用在名称开头的只有大小写字母和下划线 `_`;\n",
58 | "* 其次,名称中不能有空格,如果一个名字里有好几个词汇,可以用下划线来分割(`do_nothing`),也可以用所谓 *Camel Case* 风格(*doNothing*),习惯上更推荐使用下划线;\n",
59 | "* 最后,绝对不能与 Python 语言的**关键字**(*keyword*)重复。\n",
60 | "\n",
61 | "最后这一条,关键字也叫**保留字**(*reserved*),是编程语言保护起来内部使用的,如果程序用这些词儿做变量或者函数或者类型名字,编译器或者解释器就无法正确工作了。Python 提供了一个模块叫 `keyword` 来帮助我们了解语言有哪些关键字:"
62 | ]
63 | },
64 | {
65 | "cell_type": "code",
66 | "execution_count": 2,
67 | "metadata": {},
68 | "outputs": [
69 | {
70 | "data": {
71 | "text/plain": [
72 | "['False',\n",
73 | " 'None',\n",
74 | " 'True',\n",
75 | " 'and',\n",
76 | " 'as',\n",
77 | " 'assert',\n",
78 | " 'async',\n",
79 | " 'await',\n",
80 | " 'break',\n",
81 | " 'class',\n",
82 | " 'continue',\n",
83 | " 'def',\n",
84 | " 'del',\n",
85 | " 'elif',\n",
86 | " 'else',\n",
87 | " 'except',\n",
88 | " 'finally',\n",
89 | " 'for',\n",
90 | " 'from',\n",
91 | " 'global',\n",
92 | " 'if',\n",
93 | " 'import',\n",
94 | " 'in',\n",
95 | " 'is',\n",
96 | " 'lambda',\n",
97 | " 'nonlocal',\n",
98 | " 'not',\n",
99 | " 'or',\n",
100 | " 'pass',\n",
101 | " 'raise',\n",
102 | " 'return',\n",
103 | " 'try',\n",
104 | " 'while',\n",
105 | " 'with',\n",
106 | " 'yield']"
107 | ]
108 | },
109 | "execution_count": 2,
110 | "metadata": {},
111 | "output_type": "execute_result"
112 | }
113 | ],
114 | "source": [
115 | "import keyword\n",
116 | "keyword.kwlist"
117 | ]
118 | },
119 | {
120 | "cell_type": "markdown",
121 | "metadata": {},
122 | "source": [
123 | "`keyword.kwlist` 就是当前你使用的 Python 解释器中不可使用的关键字列表,如果我们不记得这个列表,可以随时用 `keyword.iskeyword('xxx')` 来查询某个词是不是关键字。"
124 | ]
125 | },
126 | {
127 | "cell_type": "markdown",
128 | "metadata": {},
129 | "source": [
130 | "在程序里给变量、函数命名是个挺重要的事情,影响到程序的可读性,就像小说的语言,最好能有一种流畅清晰、又始终一致的**风格**(*style*)。为了让全世界的 Python 程序员都有相对一致的风格,Python 社区有专门的一套建议规范,放在专门维护 Python 语言特性的社区 [PEP](https://www.python.org/dev/peps/) 上:\n",
131 | "\n",
132 | "* [PEP 8 -- Style Guide for Python Code: Naming Conventions](https://www.python.org/dev/peps/pep-0008/#naming-conventions)\n",
133 | "\n",
134 | "> PEP,是 *Python enhancement proposal* 的缩写,每当有重要的语言特性新需求新想法,就放在这里,经过广大 Python 用户和开发者的讨论完善,在某个版本放进 Python 中。很多 PEP 早已从 *proposal* 毕业变成官方特性,但也还在这里保留着。PEP 8 就是一个古老的 *proposal*,现在已为大多数 Python 用户采纳。"
135 | ]
136 | },
137 | {
138 | "cell_type": "markdown",
139 | "metadata": {},
140 | "source": [
141 | "## 没有、一个和多个参数"
142 | ]
143 | },
144 | {
145 | "cell_type": "markdown",
146 | "metadata": {},
147 | "source": [
148 | "函数可以没有参数,也可以有一个或者多个参数。\n",
149 | "\n",
150 | "没有参数就意味着,这个函数执行不依赖于输入,比如我们定义一个函数来在程序结束时打印一句退出提示:"
151 | ]
152 | },
153 | {
154 | "cell_type": "code",
155 | "execution_count": 3,
156 | "metadata": {},
157 | "outputs": [
158 | {
159 | "name": "stdout",
160 | "output_type": "stream",
161 | "text": [
162 | "Program exits. Bye.\n"
163 | ]
164 | }
165 | ],
166 | "source": [
167 | "def exit_info():\n",
168 | " print('Program exits. Bye.')\n",
169 | " \n",
170 | "exit_info()"
171 | ]
172 | },
173 | {
174 | "cell_type": "markdown",
175 | "metadata": {},
176 | "source": [
177 | "注意即使没有参数,无论定义还是调用时,函数名后面的括号都是不可省略的,这是函数身份的标志。"
178 | ]
179 | },
180 | {
181 | "cell_type": "markdown",
182 | "metadata": {},
183 | "source": [
184 | "函数也可以有多个参数,调用时输入参数的值是严格按照参数的顺序去匹配的。比如我们写一个函数输出某年到某年之间的所有闰年:"
185 | ]
186 | },
187 | {
188 | "cell_type": "code",
189 | "execution_count": 4,
190 | "metadata": {},
191 | "outputs": [
192 | {
193 | "name": "stdout",
194 | "output_type": "stream",
195 | "text": [
196 | "2000\n",
197 | "2004\n",
198 | "2008\n",
199 | "2012\n",
200 | "2016\n"
201 | ]
202 | }
203 | ],
204 | "source": [
205 | "def leap_years(begin, end):\n",
206 | " year = begin\n",
207 | " while year < end:\n",
208 | " if (year % 4 == 0 and year % 100 != 0) or year % 400 == 0:\n",
209 | " print(year)\n",
210 | " year += 1\n",
211 | " \n",
212 | "leap_years(2000, 2020)"
213 | ]
214 | },
215 | {
216 | "cell_type": "markdown",
217 | "metadata": {},
218 | "source": [
219 | "当我们调用 `leap_years(2000, 2020)` 时,输入两个参数值 2000 和 2020,按照顺序匹配函数定义 `leap_years(begin, end)`,于是 `begin = 2000` `end = 2020`。所以参数的顺序是不能搞错的,有些函数参数很多,要是开发过程中还调整过顺序的话,那简直就是灾难,所以一般情况下还是保持函数参数不要乱动为好。\n",
220 | "\n",
221 | "顺便说一句,判断闰年的算法虽然不难,但要写的简洁也不容易。建议你可以先自己思考和实现一遍,然后尝试搞清楚为啥上面代码里的那行 `if` 是对的。实际上闰年的判断有很多正确的写法,你应该尝试写出自己的版本并确认它的正确性。"
222 | ]
223 | },
224 | {
225 | "cell_type": "markdown",
226 | "metadata": {},
227 | "source": [
228 | "## 没有、一个和多个返回值"
229 | ]
230 | },
231 | {
232 | "cell_type": "markdown",
233 | "metadata": {},
234 | "source": [
235 | "和参数一样,Python 的函数可以没有返回值,也可以有一个或者多个返回值。\n",
236 | "\n",
237 | "上面的 `exit_info` 和 `leap_year` 也是没有返回值的例子,它们的效果都通过 `print` 函数来体现。实际上没有返回语句的函数,等价于在其最后有一句 `return None`,表示函数返回了一个空值 `None`,`None` 在 Python 中是一个合法的值,表示什么都没有,它在逻辑上等价于 `False`:"
238 | ]
239 | },
240 | {
241 | "cell_type": "code",
242 | "execution_count": 5,
243 | "metadata": {},
244 | "outputs": [
245 | {
246 | "data": {
247 | "text/plain": [
248 | "False"
249 | ]
250 | },
251 | "execution_count": 5,
252 | "metadata": {},
253 | "output_type": "execute_result"
254 | }
255 | ],
256 | "source": [
257 | "bool(None)"
258 | ]
259 | },
260 | {
261 | "cell_type": "markdown",
262 | "metadata": {},
263 | "source": [
264 | "所以即使没有返回值的函数,也可以用在 `if` 后面做逻辑表达式,不过我们并不推荐这么做,因为可读性很差。"
265 | ]
266 | },
267 | {
268 | "cell_type": "markdown",
269 | "metadata": {},
270 | "source": [
271 | "大部分情况下函数是有返回值的,因为绝大部分情况下函数的作用都是做“数据处理”,从输入出发得到输出。\n",
272 | "\n",
273 | "一般情况下函数都只有一个返回值,我们已经见过不少例子;但 Python 也允许多返回值,比如我们想用一个函数来计算两个整数相除的商和余数,可以这么写:"
274 | ]
275 | },
276 | {
277 | "cell_type": "code",
278 | "execution_count": 6,
279 | "metadata": {},
280 | "outputs": [
281 | {
282 | "name": "stdout",
283 | "output_type": "stream",
284 | "text": [
285 | "8 2\n"
286 | ]
287 | }
288 | ],
289 | "source": [
290 | "def idiv(a, b):\n",
291 | " quotient = a // b\n",
292 | " remainder = a % b\n",
293 | " return quotient, remainder\n",
294 | "\n",
295 | "q, r = idiv(50, 6)\n",
296 | "print(q, r)"
297 | ]
298 | },
299 | {
300 | "cell_type": "markdown",
301 | "metadata": {},
302 | "source": [
303 | "和多参数的情况类似,多返回值的情况下,赋值也是按照顺序匹配的,上面的代码中赋值语句左边的 `q` 匹配到第一个返回值,`r` 匹配第二个。"
304 | ]
305 | },
306 | {
307 | "cell_type": "markdown",
308 | "metadata": {},
309 | "source": [
310 | "## 函数内与函数外:变量的作用域"
311 | ]
312 | },
313 | {
314 | "cell_type": "markdown",
315 | "metadata": {},
316 | "source": [
317 | "下面的代码经常会把人搞晕:"
318 | ]
319 | },
320 | {
321 | "cell_type": "code",
322 | "execution_count": 7,
323 | "metadata": {},
324 | "outputs": [
325 | {
326 | "name": "stdout",
327 | "output_type": "stream",
328 | "text": [
329 | "2\n",
330 | "1\n"
331 | ]
332 | }
333 | ],
334 | "source": [
335 | "def increase_one(n):\n",
336 | " n += 1\n",
337 | " return n\n",
338 | "\n",
339 | "n = 1\n",
340 | "print(increase_one(n))\n",
341 | "print(n)"
342 | ]
343 | },
344 | {
345 | "cell_type": "markdown",
346 | "metadata": {},
347 | "source": [
348 | "请你思考一下,为什么这段代码里的两个 `print` 函数输出分别是 2 和 1。\n",
349 | "\n",
350 | "这个问题就涉及到变量的**作用域**(*scope*)问题,也就是说在不同地方出现的同名变量和函数,可能是完全不同的两个东西:\n",
351 | "* 函数定义体中的变量的作用域是该函数内,程序的其他部分不知道其存在,这种变量叫**局部变量**(*local variable*);函数的输入参数也是局部变量,也只在函数定义体中有效;\n",
352 | "* 不在任何函数、类定义体中的变量的作用域是全局的,在任何地方都可以访问,这种变量称为**全局变量**(*global variable*);\n",
353 | "* 如果局部变量和全局变量同名,函数定义体内会优先局部变量,不会把它当做全局变量。\n",
354 | "\n",
355 | "这样我们就能理解上面代码输出的 2 和 1 了:\n",
356 | "* 第一个 `print()` 打印的是函数调用 `increase_one(n)` 的返回值,这个语句不在任何函数定义体中,所以它里面用到的变量都是全局变量:\n",
357 | " * 在调用 `increase_one()` 时参数 `n`,按照作用域原理,是全局变量 `n` 当时的值,也就是 1;\n",
358 | " * 在 `increase_one()` 函数定义内,参数 `n` 是输入参数即局部变量,带着传进来的值 1,经过加一之后返回,返回值是 2;\n",
359 | " * `print` 打印这个返回值,输出 2;\n",
360 | " * 这个过程中处理的都是局部变量,完全不影响全局变量 `n` 的值;\n",
361 | "* 第二个 `print()` 打印的是全局变量 `n` 的值,输出出 1。"
362 | ]
363 | },
364 | {
365 | "cell_type": "markdown",
366 | "metadata": {},
367 | "source": [
368 | "以上的文字,可能需要反复阅读若干遍;几遍下来,消除了疑惑,以后就彻底没问题了;若是这个疑惑并未消除,或者关键点并未消化,以后则会反复被这个疑惑所坑害,浪费无数时间。\n",
369 | "\n",
370 | "顺便说一句,上面这个例子用来说明作用域的概念很有用,但是平时写程序最好别这么写,减少重名的变量可以提升代码的清晰度和可读性。"
371 | ]
372 | },
373 | {
374 | "cell_type": "markdown",
375 | "metadata": {},
376 | "source": [
377 | "与此相关的,我们在介绍列表等数据容器时,会为上面的规则作出重要的补充,这里先留一个伏笔。"
378 | ]
379 | },
380 | {
381 | "cell_type": "markdown",
382 | "metadata": {},
383 | "source": [
384 | "## 带缺省值的参数"
385 | ]
386 | },
387 | {
388 | "cell_type": "markdown",
389 | "metadata": {},
390 | "source": [
391 | "我们其实已经见过带缺省值的参数(*argument with default value*),这里我们更细致的看看这个特性。\n",
392 | "\n",
393 | "在函数定义中可以在某个参数后面用等号 `=` 给它一个缺省值,调用时可以省略传入这个参数的值,直接采用缺省值;当然也可以在调用时传入这个参数的值来覆盖掉缺省值。这种特性相当于给了这个函数两个版本,一个带某个参数,一个不带,不带的版本就当该参数是某个缺省值。看看下面的例子:"
394 | ]
395 | },
396 | {
397 | "cell_type": "code",
398 | "execution_count": 8,
399 | "metadata": {},
400 | "outputs": [],
401 | "source": [
402 | "def greeting(name, msg='Hi'):\n",
403 | " print(f'{msg}, {name}!')"
404 | ]
405 | },
406 | {
407 | "cell_type": "code",
408 | "execution_count": 9,
409 | "metadata": {},
410 | "outputs": [
411 | {
412 | "name": "stdout",
413 | "output_type": "stream",
414 | "text": [
415 | "Hi, Neo!\n"
416 | ]
417 | }
418 | ],
419 | "source": [
420 | "greeting('Neo')"
421 | ]
422 | },
423 | {
424 | "cell_type": "code",
425 | "execution_count": 10,
426 | "metadata": {},
427 | "outputs": [
428 | {
429 | "name": "stdout",
430 | "output_type": "stream",
431 | "text": [
432 | "Good morning, Neo!\n"
433 | ]
434 | }
435 | ],
436 | "source": [
437 | "greeting('Neo', 'Good morning')"
438 | ]
439 | },
440 | {
441 | "cell_type": "markdown",
442 | "metadata": {},
443 | "source": [
444 | "一个函数可以有多个带缺省值的参数,但有一个限制:所有这些带缺省值的参数只能堆在参数表的最后,也就是说你定义的参数表里,出现一个带缺省值的参数,则它后面的都必须带缺省值。如果把上面的 `greeting()` 函数的两个参数调换一下,会扔出一个 `SyntaxError: non-default argument follows default argument` 的异常。"
445 | ]
446 | },
447 | {
448 | "cell_type": "markdown",
449 | "metadata": {},
450 | "source": [
451 | "## 指定参数名来调用函数"
452 | ]
453 | },
454 | {
455 | "cell_type": "markdown",
456 | "metadata": {},
457 | "source": [
458 | "我们前面说过,调用函数时传入的参数值会严格按照顺序去匹配参数变量,第一个输入值赋给第一个参数变量,第二个值赋给第二个参数变量,依此类推。因为有了上面说的带缺省值参数,这个规则出现了变通的可能。\n",
459 | "\n",
460 | "如果一个函数有多个带缺省值的参数,我们想忽略掉某几个参数(就用其缺省值),但指定后面某一个参数的值(覆盖缺省值),例如下面这个函数:"
461 | ]
462 | },
463 | {
464 | "cell_type": "code",
465 | "execution_count": 11,
466 | "metadata": {},
467 | "outputs": [],
468 | "source": [
469 | "def greeting(name, msg='Hi', punc='!'):\n",
470 | " print(f'{msg}, {name}{punc}')"
471 | ]
472 | },
473 | {
474 | "cell_type": "markdown",
475 | "metadata": {},
476 | "source": [
477 | "在这个版本的 `greeting()` 函数中,包含一个普通参数 `name` 和两个带缺省值的参数 `msg` `punc`,如果我们想跳过 `msg` 只传入 `name`(这个是必须的,因为没有缺省值)和 `punc` 的值,那么就可用下面的语法:"
478 | ]
479 | },
480 | {
481 | "cell_type": "code",
482 | "execution_count": 12,
483 | "metadata": {},
484 | "outputs": [
485 | {
486 | "name": "stdout",
487 | "output_type": "stream",
488 | "text": [
489 | "Hi, Neo.\n"
490 | ]
491 | }
492 | ],
493 | "source": [
494 | "greeting('Neo', punc='.')"
495 | ]
496 | },
497 | {
498 | "cell_type": "markdown",
499 | "metadata": {},
500 | "source": [
501 | "这里第一个值按照顺序位置匹配到参数变量 `name`,这叫 *positional argument*(即“按照位置顺序匹配的参数”),而按照位置下一个是 `msg`,是我们想跳过的,所以要注明参数变量名,说明下一个传入的值 `'.'` 是给 `punc` 参数变量的,这叫 *keyword argument*(即“按照参数名匹配的参数”)。\n",
502 | "\n",
503 | "由于所有带缺省值的参数都在普通参数的后面,所以我们只要记住:\n",
504 | "* 调用函数时先传入所有不带缺省值的参数的值,严格按照函数定义的位置顺序(*positional*);\n",
505 | "* 然后想指定哪些带缺省值参数的值,就用 `变量名=值` 这样的格式在后面列出(*keyword*),未列出的就还用缺省值了。\n",
506 | "\n",
507 | "在后半部分,顺序就无所谓了,可以和定义时不一样,反正是用名字指定的(*keyword*),比如我们完全可以这么干:"
508 | ]
509 | },
510 | {
511 | "cell_type": "code",
512 | "execution_count": 13,
513 | "metadata": {},
514 | "outputs": [
515 | {
516 | "name": "stdout",
517 | "output_type": "stream",
518 | "text": [
519 | "Good nite, Neo.\n"
520 | ]
521 | }
522 | ],
523 | "source": [
524 | "greeting('Neo', punc='.', msg='Good nite')"
525 | ]
526 | },
527 | {
528 | "cell_type": "markdown",
529 | "metadata": {},
530 | "source": [
531 | "## 变长参数"
532 | ]
533 | },
534 | {
535 | "cell_type": "markdown",
536 | "metadata": {},
537 | "source": [
538 | "到目前为止,Python 的函数定义还是很简单清晰的,无论参数还是返回值,都没什么难懂的。下面开始就要进入比较混沌的领域了。\n",
539 | "\n",
540 | "所谓变长参数就是函数定义时名字前面带个星号 `*` 的参数变量,这表示这个变量其实是一组值,多少个都可以。我们先来看个简单的例子:"
541 | ]
542 | },
543 | {
544 | "cell_type": "code",
545 | "execution_count": 14,
546 | "metadata": {},
547 | "outputs": [],
548 | "source": [
549 | "def say_hi(*names):\n",
550 | " for name in names:\n",
551 | " print('Hi,', name)"
552 | ]
553 | },
554 | {
555 | "cell_type": "code",
556 | "execution_count": 15,
557 | "metadata": {},
558 | "outputs": [
559 | {
560 | "name": "stdout",
561 | "output_type": "stream",
562 | "text": [
563 | "Hi, Neo\n"
564 | ]
565 | }
566 | ],
567 | "source": [
568 | "say_hi('Neo')"
569 | ]
570 | },
571 | {
572 | "cell_type": "code",
573 | "execution_count": 16,
574 | "metadata": {},
575 | "outputs": [
576 | {
577 | "name": "stdout",
578 | "output_type": "stream",
579 | "text": [
580 | "Hi, Neo\n",
581 | "Hi, Trinity\n"
582 | ]
583 | }
584 | ],
585 | "source": [
586 | "say_hi('Neo', 'Trinity')"
587 | ]
588 | },
589 | {
590 | "cell_type": "code",
591 | "execution_count": 17,
592 | "metadata": {},
593 | "outputs": [
594 | {
595 | "name": "stdout",
596 | "output_type": "stream",
597 | "text": [
598 | "Hi, Neo\n",
599 | "Hi, Trinity\n",
600 | "Hi, Morpheus\n"
601 | ]
602 | }
603 | ],
604 | "source": [
605 | "say_hi('Neo', 'Trinity', 'Morpheus')"
606 | ]
607 | },
608 | {
609 | "cell_type": "markdown",
610 | "metadata": {},
611 | "source": [
612 | "在这个例子里,`*names` 是一个变长参数(*arbitrary argument*),调用时可以传入一个或者多个值,函数会把这些值看做一个列表,赋给局部变量 `names`——后面我们会知道,其实不是**列表**(*list*),而是一个**元组**(*tuple*)——然后我们在函数体中可以用 `for...in` 来对这个 `names` 做循环。\n",
613 | "\n",
614 | "> 有些中文书籍把 *arbitrary arguments* 翻译成“可变参数”或者“任意参数”。事实上,在这样的地方,无论怎样的中文翻译都是很难准确表达原意的。这还算好的,甚至还见过翻译成“武断的参数”的——这样的翻译肯定会使读者产生说不明道不白的疑惑。\n",
615 | ">\n",
616 | "> 所以,**入门之后就尽量只用英文**是个好策略。虽然刚开始有点吃力,但后面会很省心,很长寿——是呀,少浪费时间、少浪费生命,其实就相当于更长寿了呀!"
617 | ]
618 | },
619 | {
620 | "cell_type": "markdown",
621 | "metadata": {},
622 | "source": [
623 | "在使用 *arbitrary argument* 的场合,有几点需要注意:\n",
624 | "* 参数变量名最好用复数单词,一看就知道是一组数据;这个变量在函数里通常都会被 `for...in` 循环处理,用复数名词在写类似 `for name in names` 的循环语句时会很舒服、很地道(*idiomatic*),是的,写程序和学外语一样,不写则已,写就要尽量写得“地道”;\n",
625 | "* 这种参数变量只能有一个,因为从它开始后面的输入值都会被当做它的一部分,多了就不知道怎么分了,显然,如果有这种参数,必须放在参数表的最后。\n",
626 | "\n",
627 | "上面的第二点,有一个不太常见的例外,那就是一个函数既有 *arbitrary arguments* 又有 *arguments with default values* 的情况,那么可以有两个 *arbitrary arguments*,其中第二个必须带缺省值,然后参数表排列成这样:\n",
628 | "\n",
629 | "`def monstrosity(*normal arguments*, *normal arbitrary argument*, *arguments with defaults*, *arbitrary argument with default*)`\n",
630 | "\n",
631 | "这样是完全符合语法要求的,调用时传入参数值还是按照前面讲的规则,先按照位置顺序匹配前两部分,多出来的都归 *normal arbitrary argument*;然后按照参数变量名指定对应值,没指定的都用缺省值。不过这实在是太麻烦了,不知道什么情况下才必须用这么可怕的函数,还是祈祷我们不会碰到这样的场景吧!\n",
632 | "\n",
633 | "当然,只有上面列出的前三个部分的情况还是有的,比如下面的例子:"
634 | ]
635 | },
636 | {
637 | "cell_type": "code",
638 | "execution_count": 18,
639 | "metadata": {},
640 | "outputs": [],
641 | "source": [
642 | "def say_hi(*names, msg='Hi', punc='!'):\n",
643 | " for name in names:\n",
644 | " print(f'{msg}, {name}{punc}')"
645 | ]
646 | },
647 | {
648 | "cell_type": "code",
649 | "execution_count": 19,
650 | "metadata": {},
651 | "outputs": [
652 | {
653 | "name": "stdout",
654 | "output_type": "stream",
655 | "text": [
656 | "Hi, Neo.\n",
657 | "Hi, Trinity.\n"
658 | ]
659 | }
660 | ],
661 | "source": [
662 | "say_hi('Neo', 'Trinity', punc='.')"
663 | ]
664 | },
665 | {
666 | "cell_type": "markdown",
667 | "metadata": {},
668 | "source": [
669 | "## 小结"
670 | ]
671 | },
672 | {
673 | "cell_type": "markdown",
674 | "metadata": {},
675 | "source": [
676 | "* 函数定义四要素:函数名、参数表、函数体和返回值,本章对每一个部分都进行了更深入的说明,尤其是一些特殊的用法;\n",
677 | "* 函数定义内外是两个不同的“**作用域**(*scope*)”,区分出全局变量和局部变量,需要充分理解其运作原理;\n",
678 | "* 参数表可以分为四段(正常情况下最多只会用到前三段),需要充分理解每一段的特点,如何定义和使用,以及为什么。"
679 | ]
680 | }
681 | ],
682 | "metadata": {
683 | "kernelspec": {
684 | "display_name": "Python 3",
685 | "language": "python",
686 | "name": "python3"
687 | },
688 | "language_info": {
689 | "codemirror_mode": {
690 | "name": "ipython",
691 | "version": 3
692 | },
693 | "file_extension": ".py",
694 | "mimetype": "text/x-python",
695 | "name": "python",
696 | "nbconvert_exporter": "python",
697 | "pygments_lexer": "ipython3",
698 | "version": "3.7.4"
699 | }
700 | },
701 | "nbformat": 4,
702 | "nbformat_minor": 4
703 | }
704 |
--------------------------------------------------------------------------------
/p2-2-docstrings.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# 程序中的文档"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "你在调用函数的时候,你像是函数这个产品的用户。\n",
15 | "\n",
16 | "而你写一个函数,像是做一个产品,这个产品将来可能会被很多用户使用——包括你自己。\n",
17 | "\n",
18 | "产品,就应该有产品说明书,别人用得着,你自己也用得着——很久之后的你,很可能把当初的各种来龙去脉忘得一干二净,所以也同样需要产品说明书,别看那产品曾经是你自己设计的。\n",
19 | "\n",
20 | "产品说明书最好能说明这个函数的输入是什么,输出是什么,以及需要外部了解的实现细节(如果有的话,比如基于什么论文的什么算法,可能有什么局限等等)。\n",
21 | "\n",
22 | "但是程序员往往都很懒,写程序已经很烧脑和费时间了,还要写说明书,麻烦;而且还有个最大的问题是程序和说明书的同步,程序和家电不一样,经常会改,一旦改了就要同步说明书,这真是有点强人所难了。\n",
23 | "\n",
24 | "Python 在这方面很用功,把函数的“产品说明书”当作语言内部的功能,也就是你在写代码的时候给每个函数写一点注释,只要这些注释按照某种格式去写,调用函数的人就可以方便的看到这些注释,还能用专门的工具自动生成说明书文档,甚至发布到网上去给你的用户(其他程序员)看。这并不是 Python 发明的方法,但 Python 绝对是主流语言里做的最好的之一,Python 社区提供了不是一个,而是好几个这类文档工具,比较流行的有 [Sphinx](http://www.sphinx-doc.org/en/master/index.html)、[pdoc](https://github.com/BurntSushi/pdoc)、[pydoctor](https://github.com/twisted/pydoctor) 和 [Doxygen](http://www.doxygen.nl/) 几个。\n",
25 | "\n",
26 | "这些工具各有优劣,一般来说对小型项目 [pdoc](https://github.com/BurntSushi/pdoc) 是最好最快的解决方案,而对比较复杂的项目 [Sphinx](http://www.sphinx-doc.org/en/master/index.html) 是公认的强者。"
27 | ]
28 | },
29 | {
30 | "cell_type": "markdown",
31 | "metadata": {},
32 | "source": [
33 | "## Docstring 简介"
34 | ]
35 | },
36 | {
37 | "cell_type": "markdown",
38 | "metadata": {},
39 | "source": [
40 | "*Docstrings* 就是前面我们提到的“有一定格式要求的注释”,我们可以在我们书写的函数体最开始书写这些文档注释,然后就可以使用内置的 `help()` 函数,或者 *function* 类对象的 `__doc__` 这个属性去查到这段注释。\n",
41 | "\n",
42 | "我们看看下面这个例子,这是判断给定参数是否素数的一个函数:\n",
43 | "\n",
44 | "> 你会发现我们的例子经常换,似乎没啥必要的情况下也会用一些没写过的东西做例子,其实这都是小练习,我们强烈的鼓励你对每一个这样的例子,除了理解我们正在讲的东西(比如这一章讲函数文档),也要顺便搞清楚这个例子里其他东西,比如怎么判断一个数是不是素数之类的。编程是一门手艺,越练越精。"
45 | ]
46 | },
47 | {
48 | "cell_type": "code",
49 | "execution_count": 1,
50 | "metadata": {},
51 | "outputs": [],
52 | "source": [
53 | "from math import sqrt\n",
54 | "\n",
55 | "def is_prime(n):\n",
56 | " \"\"\"Return a boolean value based upon whether the argument n is a prime number.\"\"\"\n",
57 | " if n < 2:\n",
58 | " return False\n",
59 | " \n",
60 | " if n in (2, 3):\n",
61 | " return True\n",
62 | "\n",
63 | " for i in range(2, int(sqrt(n)) + 1):\n",
64 | " if n % i == 0:\n",
65 | " return False\n",
66 | " \n",
67 | " return True"
68 | ]
69 | },
70 | {
71 | "cell_type": "code",
72 | "execution_count": 2,
73 | "metadata": {},
74 | "outputs": [
75 | {
76 | "data": {
77 | "text/plain": [
78 | "True"
79 | ]
80 | },
81 | "execution_count": 2,
82 | "metadata": {},
83 | "output_type": "execute_result"
84 | }
85 | ],
86 | "source": [
87 | "is_prime(23)"
88 | ]
89 | },
90 | {
91 | "cell_type": "code",
92 | "execution_count": 3,
93 | "metadata": {},
94 | "outputs": [
95 | {
96 | "name": "stdout",
97 | "output_type": "stream",
98 | "text": [
99 | "Help on function is_prime in module __main__:\n",
100 | "\n",
101 | "is_prime(n)\n",
102 | " Return a boolean value based upon whether the argument n is a prime number.\n",
103 | "\n"
104 | ]
105 | }
106 | ],
107 | "source": [
108 | "help(is_prime)"
109 | ]
110 | },
111 | {
112 | "cell_type": "markdown",
113 | "metadata": {},
114 | "source": [
115 | "如上所示,*docstring* 是紧接着 `def` 语句,写在函数体第一行的一个字符串(写在函数体其他地方无效),用三个单引号或者双引号括起来,和函数体其他部分一样缩进,可以是一行也可以写成多行。只要存在这样的字符串,用函数 `help()` 就可以提取其内容显示出来,这样调用函数的人就可以不用查看你的源代码就读到你写的“产品手册”了。"
116 | ]
117 | },
118 | {
119 | "cell_type": "code",
120 | "execution_count": 4,
121 | "metadata": {},
122 | "outputs": [
123 | {
124 | "data": {
125 | "text/plain": [
126 | "'Return a boolean value based upon whether the argument n is a prime number.'"
127 | ]
128 | },
129 | "execution_count": 4,
130 | "metadata": {},
131 | "output_type": "execute_result"
132 | }
133 | ],
134 | "source": [
135 | "is_prime.__doc__"
136 | ]
137 | },
138 | {
139 | "cell_type": "markdown",
140 | "metadata": {},
141 | "source": [
142 | "后面我们会学到,函数也是一种对象(其实 Python 中几乎所有东西都是对象),它们也有一些属性,比如 `__doc__` 这个属性就会输出函数的 *docstring*。"
143 | ]
144 | },
145 | {
146 | "cell_type": "markdown",
147 | "metadata": {},
148 | "source": [
149 | "## 书写 Docstring 的基本原则"
150 | ]
151 | },
152 | {
153 | "cell_type": "markdown",
154 | "metadata": {},
155 | "source": [
156 | "规范,虽然是人们最好遵守的,但其实通常是很多人并不遵守的东西。\n",
157 | "\n",
158 | "既然学,就要**像样**——这真的很重要。所以,非常有必要认真阅读 Python [PEP 257](https://www.python.org/dev/peps/pep-0257/),也就是 Python 社区关于 *docstring* 的规范。\n",
159 | "\n",
160 | "简要总结一下 PEP 257 中所强调的部分要点:\n",
161 | "\n",
162 | "* 无论是单行还是多行的 *docstring*,一概使用三个双引号括起来;\n",
163 | "* 在 *docstring* 前后都不要有空行;\n",
164 | "* 多行 *docstring*,第一行是概要,随后空一行,再写其它部分;\n",
165 | "* 书写良好的 *docstring* 应概括描述以下内容:参数、返回值、可能触发的错误类型、可能的副作用,以及函数的使用限制等。\n",
166 | "\n",
167 | "类定义也有相应的文档规范建议,你可以阅读 [PEP 257](https://www.python.org/dev/peps/pep-0257/) 文档作为起步,并参考 [Sphinx](https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html)、[numpy/scipy](https://numpydoc.readthedocs.io/en/latest/format.html)(这是非常著名的 Python 第三方库,我们以后会讲)和 [Google](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html) 的文档来学习大而规范的项目文档通常是怎么要求的。\n",
168 | "\n",
169 | "而关于 *docstring* 的内容,需要**格外注意**的是:\n",
170 | "\n",
171 | "> *Docstring* 是**写给人看的**,所以,在复杂代码的 *docstring* 中,写清楚 **why** 要远比写 *what* 更重要,因为 *what* 往往可以通过阅读代码来了解,而 *why* 就要难很多。你先记住这点,以后的体会自然会不断加深。"
172 | ]
173 | },
174 | {
175 | "cell_type": "markdown",
176 | "metadata": {},
177 | "source": [
178 | "## 文档生成工具简介"
179 | ]
180 | },
181 | {
182 | "cell_type": "markdown",
183 | "metadata": {},
184 | "source": [
185 | "前面提到过,除了 `help` 和 `__doc__` 这样的内置函数和属性可以方便我们使用 *docstring*,还有一系列文档工具可以从源代码中的注释自动生成在线文档。\n",
186 | "\n",
187 | "这些工具通常需要在一个项目中做好约定才能顺利使用起来,而且有自己一套对 *docstring* 的内容与格式要求,比如 [Sphinx](http://www.sphinx-doc.org/en/master/index.html) 标准的 *docstring* 写出来大致是这个样子的:\n",
188 | "\n",
189 | "```python\n",
190 | "class Vehicle(object):\n",
191 | " '''\n",
192 | " The Vehicle object contains lots of vehicles\n",
193 | " :param arg: The arg is used for ...\n",
194 | " :type arg: str\n",
195 | " :param `*args`: The variable arguments are used for ...\n",
196 | " :param `**kwargs`: The keyword arguments are used for ...\n",
197 | " :ivar arg: This is where we store arg\n",
198 | " :vartype arg: str\n",
199 | " '''\n",
200 | "\n",
201 | "\n",
202 | " def __init__(self, arg, *args, **kwargs):\n",
203 | " self.arg = arg\n",
204 | "\n",
205 | " def cars(self, distance, destination):\n",
206 | " '''We can't travel a certain distance in vehicles without fuels, so here's the fuels\n",
207 | "\n",
208 | " :param distance: The amount of distance traveled\n",
209 | " :type amount: int\n",
210 | " :param bool destinationReached: Should the fuels be refilled to cover required distance?\n",
211 | " :raises: :class:`RuntimeError`: Out of fuel\n",
212 | "\n",
213 | " :returns: A Car mileage\n",
214 | " :rtype: Cars\n",
215 | " ''' \n",
216 | " pass\n",
217 | "```\n",
218 | "\n",
219 | "这种格式叫做 *reStructureText*,通过 Sphinx 的工具处理之后可以生成在线文档,差不多是[这个样子](https://simpleble.readthedocs.io/en/latest/simpleble.html#the-simplebledevice-class)。\n",
220 | "\n",
221 | "通过插件 Sphinx 也能处理前面提过的 Numpy 和 Google 的格式文档。"
222 | ]
223 | },
224 | {
225 | "cell_type": "markdown",
226 | "metadata": {},
227 | "source": [
228 | "## 小结"
229 | ]
230 | },
231 | {
232 | "cell_type": "markdown",
233 | "metadata": {},
234 | "source": [
235 | "* Python 提供内置的文档工具来书写和阅读程序文档;\n",
236 | "* 对自己写的每个函数和类写一段简明扼要的 *docstring* 是培养好习惯的开始;\n",
237 | "* 通过扩展阅读初步了解 Python 社区对文档格式的要求。"
238 | ]
239 | }
240 | ],
241 | "metadata": {
242 | "kernelspec": {
243 | "display_name": "Python 3",
244 | "language": "python",
245 | "name": "python3"
246 | },
247 | "language_info": {
248 | "codemirror_mode": {
249 | "name": "ipython",
250 | "version": 3
251 | },
252 | "file_extension": ".py",
253 | "mimetype": "text/x-python",
254 | "name": "python",
255 | "nbconvert_exporter": "python",
256 | "pygments_lexer": "ipython3",
257 | "version": "3.7.4"
258 | }
259 | },
260 | "nbformat": 4,
261 | "nbformat_minor": 4
262 | }
263 |
--------------------------------------------------------------------------------
/p2-4-recursion.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# 递归"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "递归是个很有趣也很有用的概念。所谓“**递归**(*recursion*,*recursive*)”,就是“用自己定义自己”,或者“自己包含自己”。举个例子,大家都听过的这个故事就是递归的经典例子:\n",
15 | "\n",
16 | "```\n",
17 | "从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是这样的:从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是这样的:从前有座山,……\n",
18 | "```\n",
19 | "\n",
20 | "如果我们把这个故事叫做 A,那么 A 的定义可以写作这样:\n",
21 | "\n",
22 | "```\n",
23 | "A ::= 从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是这样的:A\n",
24 | "```\n",
25 | "\n",
26 | "上面这个定义里的 `::=` 的意思是“定义为”,这种语法叫 [巴科斯范式(BNF)](https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form)。这个定义里最有意思的地方是,在 A 的定义中用到了 A 本身,如果我们把 A 的定义代入 `::=` 右边出现的 A,就会出现无限循环下去的情况,这恰恰是原来那个故事的 point 所在。"
27 | ]
28 | },
29 | {
30 | "cell_type": "markdown",
31 | "metadata": {},
32 | "source": [
33 | "如果把这个故事写成 Python 代码,大致是这样子的\n",
34 | "\n",
35 | "```python\n",
36 | "def a_monk_telling_story():\n",
37 | " print('从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是这样的:')\n",
38 | " return a_monk_telling_story()\n",
39 | "```\n",
40 | "\n",
41 | "这个简短的程序体现了递归的本质:一个函数的定义中调用了自己。不过我们可不能真写这样的程序,因为它会无限的自我调用下去,形成“无限循环”,或者更难听的词儿“死循环”——循环到死。"
42 | ]
43 | },
44 | {
45 | "cell_type": "markdown",
46 | "metadata": {},
47 | "source": [
48 | "## 递归的基本概念"
49 | ]
50 | },
51 | {
52 | "cell_type": "markdown",
53 | "metadata": {},
54 | "source": [
55 | "递归在编程中很有用,要了解这一点,我们可以来了解递归的来源:又是数学。人们很早就意识到,数学上有一类东西,很不好写出一个公式来计算,但用自己来定义自己反而会很清晰和严谨,比如大名鼎鼎的斐波那契(*Fibonacci*)数列,这个数列不仅有很高的理论价值,而且有很多数学和其他学科里的直接应用,它的定义是:"
56 | ]
57 | },
58 | {
59 | "cell_type": "markdown",
60 | "metadata": {},
61 | "source": [
62 | "\\begin{equation*}\n",
63 | " f(n) = \\begin{cases}\n",
64 | " 0 & n = 0\\\\\n",
65 | " 1 & n = 1\\\\\n",
66 | " f(n-1) + f(n-2) & \\text{otherwise}\n",
67 | " \\end{cases}\n",
68 | "\\end{equation*}"
69 | ]
70 | },
71 | {
72 | "cell_type": "markdown",
73 | "metadata": {},
74 | "source": [
75 | "也就是说,开头两项是 0 和 1,之后每一项都是前两项之和,这个定义来自数学家 Leonardo Fibonacci 对兔子生长问题的研究:\n",
76 | "* 第一个月初有一对刚诞生的兔子;\n",
77 | "* 第二个月之后(第三个月初)它们可以生育;\n",
78 | "* 每月每对可生育的兔子会诞生下一对新兔子;\n",
79 | "* 兔子永不死去。\n",
80 | "\n",
81 | "在这一模型下,每个月兔子的数量就可以简单直观地用上面的递归表达式来表示。而要写出斐波那契(Fibonacci)数列的通项公式(即通过 n 的有限次运算求出第 n 项的值)就没那么容易了,当然这个公式目前不算什么了,长这样:"
82 | ]
83 | },
84 | {
85 | "cell_type": "markdown",
86 | "metadata": {},
87 | "source": [
88 | "\\begin{equation*}\n",
89 | " f(n) = \\frac{1}{\\sqrt{5}}((\\frac{1+\\sqrt{5}}{2})^{n} - (\\frac{1-\\sqrt{5}}{2})^{n})\n",
90 | "\\end{equation*}"
91 | ]
92 | },
93 | {
94 | "cell_type": "markdown",
95 | "metadata": {},
96 | "source": [
97 | "所以可以看出,递归表达式和通项表达式很不一样,有时候递归表达式代表了问题的本质,从递归表达式出发就可以一个一个计算出值来,而通项表达式更方便快速计算出特定某一项来,却完全看不出和最初的问题有什么关联。"
98 | ]
99 | },
100 | {
101 | "cell_type": "markdown",
102 | "metadata": {},
103 | "source": [
104 | "另一个经典的递归例子是阶乘的定义,n 的阶乘 $n!$ 就是从 1 开始一直乘到 n,递归定义很简单:"
105 | ]
106 | },
107 | {
108 | "cell_type": "markdown",
109 | "metadata": {},
110 | "source": [
111 | "\\begin{equation*}\n",
112 | "n! = (n-1)! \\times n\n",
113 | "\\end{equation*}"
114 | ]
115 | },
116 | {
117 | "cell_type": "markdown",
118 | "metadata": {},
119 | "source": [
120 | "可要写出它的通项,怎么写呢?当然我们可以写 `n! = 1x2x3x...xn`,但是 `...` 并不是在数学上严谨的一个东西啊。"
121 | ]
122 | },
123 | {
124 | "cell_type": "markdown",
125 | "metadata": {},
126 | "source": [
127 | "为了用 Python 计算阶乘的值,可以有两种方法,循环,和递归。先来看循环的方法:"
128 | ]
129 | },
130 | {
131 | "cell_type": "code",
132 | "execution_count": null,
133 | "metadata": {},
134 | "outputs": [],
135 | "source": [
136 | "result = 1\n",
137 | "for i in range(5):\n",
138 | " result = result * (i+1)\n",
139 | "print(result)"
140 | ]
141 | },
142 | {
143 | "cell_type": "markdown",
144 | "metadata": {},
145 | "source": [
146 | "就是把 1~n 的整数都乘起来,下面看看递归的方法:"
147 | ]
148 | },
149 | {
150 | "cell_type": "code",
151 | "execution_count": null,
152 | "metadata": {},
153 | "outputs": [],
154 | "source": [
155 | "def f(n):\n",
156 | " if n == 1:\n",
157 | " return 1\n",
158 | " else:\n",
159 | " return n * f(n-1)\n",
160 | " \n",
161 | "f(5)"
162 | ]
163 | },
164 | {
165 | "cell_type": "markdown",
166 | "metadata": {},
167 | "source": [
168 | "和前面的版本结果一样。这个实现基本就是阶乘数学定义的翻译,很容易明白,但我们需要格外关注的是在函数里调用自己这件事。我们可以一层一层的分析一下在函数调用 `f(5)` 之后到底发生了什么。"
169 | ]
170 | },
171 | {
172 | "cell_type": "markdown",
173 | "metadata": {},
174 | "source": [
175 | "
"
176 | ]
177 | },
178 | {
179 | "cell_type": "markdown",
180 | "metadata": {},
181 | "source": [
182 | "如上图所示,当 f(5) 被调用之后,函数开始运行……\n",
183 | "* 因为 `5 > 1`,所以,在计算 `n * f(n-1)` 的时候要再次调用自己 `f(4)`;所以必须等待 `f(4)` 的值返回;\n",
184 | "* 因为 `4 > 1`,所以,在计算 `n * f(n-1)` 的时候要再次调用自己 `f(3)`;所以必须等待 `f(3)` 的值返回;\n",
185 | "* 因为 `3 > 1`,所以,在计算 `n * f(n-1)` 的时候要再次调用自己 `f(2)`;所以必须等待 `f(2)` 的值返回;\n",
186 | "* 因为 `2 > 1`,所以,在计算 `n * f(n-1)` 的时候要再次调用自己 `f(1)`;所以必须等待 `f(1)` 的值返回;\n",
187 | "* 因为 `1 == 1`,所以,这时候不会再次调用 `f()` 了,`f(1)` 返回值是 `1`;\n",
188 | "* 下一步,`f(2)` 返回值是 `2 * 1 = 2`;\n",
189 | "* 下一步,`f(3)` 返回值是`3 * 2 = 6`;\n",
190 | "* 下一步,`f(4)` 返回值是`4 * 6 = 24`;\n",
191 | "* 下一步,`f(5)` 返回值是`5 * 24 = 120`。\n",
192 | "\n",
193 | "至此,函数调用 `f(5)` 才执行完,最终返回值是 `120`。这个调用中“**递归**(*recursively*)”调用了四次 `f()` 自己。\n",
194 | "\n",
195 | "> *Recursive* 本来就是“反复、重复”的意思。\n",
196 | "\n",
197 | "试着在自己脑子里把这个过程走通,有点烧脑,不过搞清楚了也很有意思。"
198 | ]
199 | },
200 | {
201 | "cell_type": "markdown",
202 | "metadata": {},
203 | "source": [
204 | "## 递归的终止"
205 | ]
206 | },
207 | {
208 | "cell_type": "markdown",
209 | "metadata": {},
210 | "source": [
211 | "从上面的例子,我们可以发现,最终 f(1) 不再需要递归,可以直接返回一个确定的值,这一步是递归的转折点,特别重要,如果没有这个,递归就会无穷无尽的重复下去,永远得不到结果,就像最开始我们写的那个老和尚讲故事的例子一样。\n",
212 | "\n",
213 | "把我们计算阶乘的例子中的 `if n == 1:` 条件去掉,直接写成这样:\n",
214 | "\n",
215 | "```python\n",
216 | "def f(n):\n",
217 | " return n * f(n-1)\n",
218 | "```\n",
219 | "\n",
220 | "如果运行这个版本的 `f(5)`,会得到一个运行时异常 `RecursionError: maximum recursion depth exceeded`,因为 Python 对递归次数是有限制的,达到一定次数还没返回的递归会抛出这个异常。\n",
221 | "\n",
222 | "我们写递归函数的时候,要特别小心的确认递归的终止条件,就和循环中的退出条件一样,一定不能出现无限递归或者死循环的情况。"
223 | ]
224 | },
225 | {
226 | "cell_type": "markdown",
227 | "metadata": {},
228 | "source": [
229 | "## 递归的好处与代价"
230 | ]
231 | },
232 | {
233 | "cell_type": "markdown",
234 | "metadata": {},
235 | "source": [
236 | "从理论上可以证明,所有递归(*recursion*)都可以改写为循环(*iteration*),反之所有循环也都可以用递归改写。那么我们写递归这样的算法好处是什么呢?\n",
237 | "\n",
238 | "> Alonzo Church 和 Alan Turing 两位现代计算机科学的奠基者的[经典论文](https://en.wikipedia.org/wiki/Church%E2%80%93Turing_thesis)提供了证明,前提是内存管够。\n",
239 | "\n",
240 | "最大的好处是清晰易懂。递归通常用于本质上就带有递归特性的问题(如前面所见的例子),递归算法几乎原样展现了问题的本质,容易理解也容易编写。\n",
241 | "\n",
242 | "另外的好处是,对某些问题的算法,递归是最优化的,比如树型结构的遍历。\n",
243 | "\n",
244 | "凡事有得必有失。递归的代价是什么呢?\n",
245 | "\n",
246 | "主要代价有二:递归一般会使用更多的内存,因为每次调用自身都需要建立新函数调用所需要的环境,占用相应的资源,一直迭代到最深层开始返回才会释放这些资源,对此有些编程语言会在编译器和运行时进行优化,比如“**尾递归优化**(*tail-recursion optimization, TRO*)”,能够将这种消耗免除,但 Python 并不支持(以后也不会支持),所以 Python 会限制递归的层次,超过就扔出 `RecursiveError`。\n",
247 | "\n",
248 | "另一个代价是,有的时候递归算法不是最高效的,这涉及到算法分析,我们目前还不用深入,知道有这些代价就好。"
249 | ]
250 | },
251 | {
252 | "cell_type": "markdown",
253 | "metadata": {},
254 | "source": [
255 | "对于我们来说,了解这种思考问题的方法,在碰到适合的问题时能多一个思路,就是相当不错的收获了。"
256 | ]
257 | },
258 | {
259 | "cell_type": "markdown",
260 | "metadata": {},
261 | "source": [
262 | "## 小结"
263 | ]
264 | },
265 | {
266 | "cell_type": "markdown",
267 | "metadata": {},
268 | "source": [
269 | "* 递归函数在函数体定义中包含对自己调用的一种函数,通常用于实现某些天然具有递归性质的算法;\n",
270 | "* 递归函数中必须正确设定递归终止的条件,避免出现无限递归的情况;\n",
271 | "* 初步了解递归的优势和代价,在未来的学习中持续加深理解。"
272 | ]
273 | }
274 | ],
275 | "metadata": {
276 | "kernelspec": {
277 | "display_name": "Python 3",
278 | "language": "python",
279 | "name": "python3"
280 | },
281 | "language_info": {
282 | "codemirror_mode": {
283 | "name": "ipython",
284 | "version": 3
285 | },
286 | "file_extension": ".py",
287 | "mimetype": "text/x-python",
288 | "name": "python",
289 | "nbconvert_exporter": "python",
290 | "pygments_lexer": "ipython3",
291 | "version": "3.7.4"
292 | }
293 | },
294 | "nbformat": 4,
295 | "nbformat_minor": 4
296 | }
297 |
--------------------------------------------------------------------------------
/p2-5-functional-1.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# 函数也是数据:初级篇"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "前面我们已经提过一次,**函数也是一种对象**。\n",
15 | "\n",
16 | "或者说:**函数也是数据**(*function is data*),是差不多的。\n",
17 | "\n",
18 | "这是个重要的时刻,从现在开始我们对函数的理解将有一个新的飞跃。函数虽然是操作数据的工具,但它自己也是个数据对象,我们可以对它进行各种各样的操作,甚至在运行时动态的构造出一个函数来,这些都是 Python(以及很多流行的现代化编程语言)的亮点。我们在这一部分的最后还会就这个话题进行展开,从而引入一系列非常有用的“**函数式**(*functional*)”编程工具,在目前这一章我们先稍微品尝一下这个概念,来看看函数别名和匿名函数。"
19 | ]
20 | },
21 | {
22 | "cell_type": "markdown",
23 | "metadata": {},
24 | "source": [
25 | "## 函数别名"
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "metadata": {},
31 | "source": [
32 | "在 Python 中,所有函数也是对象,证据就是它们都有对象 id。Python 会为创建的每一个对象(不管基本数据类型,还是某个 *class* 的实例)指定一个唯一的 id,可以用内置函数 `id()` 来查看,比如:"
33 | ]
34 | },
35 | {
36 | "cell_type": "code",
37 | "execution_count": 1,
38 | "metadata": {},
39 | "outputs": [
40 | {
41 | "data": {
42 | "text/plain": [
43 | "4356419792"
44 | ]
45 | },
46 | "execution_count": 1,
47 | "metadata": {},
48 | "output_type": "execute_result"
49 | }
50 | ],
51 | "source": [
52 | "n = 42\n",
53 | "id(n)"
54 | ]
55 | },
56 | {
57 | "cell_type": "markdown",
58 | "metadata": {},
59 | "source": [
60 | "函数也有这个 id,比如:"
61 | ]
62 | },
63 | {
64 | "cell_type": "code",
65 | "execution_count": 2,
66 | "metadata": {},
67 | "outputs": [],
68 | "source": [
69 | "def _is_leap(year):\n",
70 | " return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0"
71 | ]
72 | },
73 | {
74 | "cell_type": "code",
75 | "execution_count": 3,
76 | "metadata": {},
77 | "outputs": [
78 | {
79 | "data": {
80 | "text/plain": [
81 | "4394147424"
82 | ]
83 | },
84 | "execution_count": 3,
85 | "metadata": {},
86 | "output_type": "execute_result"
87 | }
88 | ],
89 | "source": [
90 | "id(_is_leap)"
91 | ]
92 | },
93 | {
94 | "cell_type": "markdown",
95 | "metadata": {},
96 | "source": [
97 | "既然函数有 id,是个对象,那是什么类型的对象呢?可以用内置函数 `type` 来看:"
98 | ]
99 | },
100 | {
101 | "cell_type": "code",
102 | "execution_count": 4,
103 | "metadata": {},
104 | "outputs": [
105 | {
106 | "data": {
107 | "text/plain": [
108 | "function"
109 | ]
110 | },
111 | "execution_count": 4,
112 | "metadata": {},
113 | "output_type": "execute_result"
114 | }
115 | ],
116 | "source": [
117 | "type(_is_leap)"
118 | ]
119 | },
120 | {
121 | "cell_type": "markdown",
122 | "metadata": {},
123 | "source": [
124 | "所以函数是个 `function` 类型的对象。\n",
125 | "\n",
126 | "既然是个对象,我们就可以用赋值语句来创建函数的**别名**(*alias*):"
127 | ]
128 | },
129 | {
130 | "cell_type": "code",
131 | "execution_count": 5,
132 | "metadata": {},
133 | "outputs": [],
134 | "source": [
135 | "is_leap = _is_leap"
136 | ]
137 | },
138 | {
139 | "cell_type": "code",
140 | "execution_count": 6,
141 | "metadata": {},
142 | "outputs": [
143 | {
144 | "data": {
145 | "text/plain": [
146 | "4394147424"
147 | ]
148 | },
149 | "execution_count": 6,
150 | "metadata": {},
151 | "output_type": "execute_result"
152 | }
153 | ],
154 | "source": [
155 | "id(is_leap)"
156 | ]
157 | },
158 | {
159 | "cell_type": "markdown",
160 | "metadata": {},
161 | "source": [
162 | "可以看到,这两个函数的 id 完全一样,是同一个对象的两个名字而已。我们可以用这两个名字来调用这个函数,完全没区别:"
163 | ]
164 | },
165 | {
166 | "cell_type": "code",
167 | "execution_count": 7,
168 | "metadata": {},
169 | "outputs": [
170 | {
171 | "data": {
172 | "text/plain": [
173 | "False"
174 | ]
175 | },
176 | "execution_count": 7,
177 | "metadata": {},
178 | "output_type": "execute_result"
179 | }
180 | ],
181 | "source": [
182 | "_is_leap(2018)"
183 | ]
184 | },
185 | {
186 | "cell_type": "code",
187 | "execution_count": 8,
188 | "metadata": {},
189 | "outputs": [
190 | {
191 | "data": {
192 | "text/plain": [
193 | "False"
194 | ]
195 | },
196 | "execution_count": 8,
197 | "metadata": {},
198 | "output_type": "execute_result"
199 | }
200 | ],
201 | "source": [
202 | "is_leap(2018)"
203 | ]
204 | },
205 | {
206 | "cell_type": "markdown",
207 | "metadata": {},
208 | "source": [
209 | "那么,我们为什么需要给函数取别名呢?\n",
210 | "\n",
211 | "很多时候是为了提供更好的代码可读性,比如在特定上下文让某个函数的作用更显而易见,比如以前的例子里,我们曾经在 `Cat` 类里给父类 `Animal` 的 `voice()` 方法定义别名叫 `meow()`。\n",
212 | "\n",
213 | "还有一种情况是一个函数需要在运行时动态指向不同的实现版本。这里只简单描述一个典型场景:假定我们要渲染一段视频,如果系统里有兼容的显卡(GPU),就调用显卡来渲染,会更快更省电,如果没有则用 CPU 来渲染,会慢一点和更耗电一点,于是我们把用 GPU 渲染的算法写成函数 `_render_by_gpu()`,用 CPU 渲染的算法写成函数 `_render_by_cpu()`,而检测是否存在可用 GPU 的算法写成函数 `is_gpu_available()`,然后可以用下面的方法来定义一个函数 `render`:\n",
214 | "\n",
215 | "```python\n",
216 | "if is_gpu_available():\n",
217 | " render = _render_by_gpu\n",
218 | "else:\n",
219 | " render = _render_by_cpu\n",
220 | "```\n",
221 | "\n",
222 | "这样 `render()` 就成为一个当前系统中最优化的渲染函数,在程序的其他地方就不用管细节,直接用这个函数就好。这就是动态函数别名的价值。\n",
223 | "\n",
224 | "顺便说一句,在任何一个工程里,为函数或者变量取名都是**很简单却不容易**的事情——因为可能会重名(虽然已经尽量用变量的作用域隔离了),可能会因取名含混而令后来者费解,所以,仅仅为了少敲几下键盘而给一个函数取个更短的别名,实际上并不是好主意,更不是好习惯。尤其现在的编辑器都支持自动补全和多光标编辑的功能,变量名长点不是什么大问题。"
225 | ]
226 | },
227 | {
228 | "cell_type": "markdown",
229 | "metadata": {},
230 | "source": [
231 | "## 匿名函数"
232 | ]
233 | },
234 | {
235 | "cell_type": "markdown",
236 | "metadata": {},
237 | "source": [
238 | "有的函数需要两个甚至更多名字,有的函数却一个也不要,人生就是这么丰富多彩啊!\n",
239 | "\n",
240 | "所谓匿名函数,就是有时候我们需要一个函数,但就在一个地方,用完就扔,再也不会用了,Python 对这种情况提供了一个方便的语法,不需要 `def` 那套严肃完整的语法,一行就可以写完一个函数,这个语法使用关键字 `lambda`。`lambda` 是希腊字母 `λ` 的英语音译,在计算机领域是个来头不小的词儿,代表了一系列高深的理论,[和阿伦佐·丘奇(Alonzo Church)的理论有关](https://en.wikipedia.org/wiki/Lambda_calculus),有兴趣的话可以自行研究。\n",
241 | "\n",
242 | "不过目前我们不需要管那么多,只要了解怎么快速创建“用过即扔”的小函数就好了。"
243 | ]
244 | },
245 | {
246 | "cell_type": "markdown",
247 | "metadata": {},
248 | "source": [
249 | "比如下面这个很简单的函数:"
250 | ]
251 | },
252 | {
253 | "cell_type": "code",
254 | "execution_count": 9,
255 | "metadata": {},
256 | "outputs": [
257 | {
258 | "data": {
259 | "text/plain": [
260 | "8"
261 | ]
262 | },
263 | "execution_count": 9,
264 | "metadata": {},
265 | "output_type": "execute_result"
266 | }
267 | ],
268 | "source": [
269 | "def add(x, y):\n",
270 | " return x + y\n",
271 | "add(3, 5)"
272 | ]
273 | },
274 | {
275 | "cell_type": "markdown",
276 | "metadata": {},
277 | "source": [
278 | "我们可以用 `lambda` 来改写:"
279 | ]
280 | },
281 | {
282 | "cell_type": "code",
283 | "execution_count": 10,
284 | "metadata": {},
285 | "outputs": [
286 | {
287 | "data": {
288 | "text/plain": [
289 | "8"
290 | ]
291 | },
292 | "execution_count": 10,
293 | "metadata": {},
294 | "output_type": "execute_result"
295 | }
296 | ],
297 | "source": [
298 | "add = lambda x, y: x + y\n",
299 | "add(3, 5)"
300 | ]
301 | },
302 | {
303 | "cell_type": "markdown",
304 | "metadata": {},
305 | "source": [
306 | "甚至更简单一点,名字也不要了:"
307 | ]
308 | },
309 | {
310 | "cell_type": "code",
311 | "execution_count": 11,
312 | "metadata": {},
313 | "outputs": [
314 | {
315 | "data": {
316 | "text/plain": [
317 | "8"
318 | ]
319 | },
320 | "execution_count": 11,
321 | "metadata": {},
322 | "output_type": "execute_result"
323 | }
324 | ],
325 | "source": [
326 | "(lambda x, y: x + y)(3, 5)"
327 | ]
328 | },
329 | {
330 | "cell_type": "markdown",
331 | "metadata": {},
332 | "source": [
333 | "最后这种形式,就是典型的匿名函数了。简单地说,`lambda` 可以生成一个函数对象,出现在所有需要一个函数的地方,可以将其赋给一个变量(如上面的 `add`),这个变量就称为函数变量(别名),可以当函数用;也可以直接把 `lambda` 语句用括号括起来当一个函数用(上面后一种形式)。\n",
334 | "\n",
335 | "在 Python 官方文档中,`lambda` 语句的语法定义是这样的:\n",
336 | "\n",
337 | "`lambda_expr ::= \"lambda\" [parameter_list] \":\" expression`\n",
338 | "\n",
339 | "这个语法定义采用的是 [巴克斯范式(BNF)](https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form)标注,现在不明白没关系(虽然对照上面的例子也能猜出个大概吧),以后我们会专门介绍。\n",
340 | "\n",
341 | "其实也很简单,就是这个样子:\n",
342 | "\n",
343 | "```python\n",
344 | "lambda x, y: x + y\n",
345 | "```\n",
346 | "\n",
347 | "先写上 `lambda` 关键字,其后分为两个部分,`:` 之前是参数表,之后是表达式,这个表达式的值,就是这个函数的返回值。**注意**:`lambda` 语句中,`:` 之后有且只能有一个表达式,所以它搞不出很复杂的函数,比较适合一句话的函数。\n",
348 | "\n",
349 | "而这个函数呢,没有名字,所以被称为 “匿名函数”。\n",
350 | "\n",
351 | "`add = lambda x, y: x + y`\n",
352 | "\n",
353 | "就相当于是给一个没有名字的函数取了个名字。"
354 | ]
355 | },
356 | {
357 | "cell_type": "markdown",
358 | "metadata": {},
359 | "source": [
360 | "我们在后面的学习中会遇到很多 `lambda` 的例子,因为有很多地方需要这种就用一次而且一句话的小函数,目前大家只要对这个概念和语法有所理解并且记住就可以了。"
361 | ]
362 | },
363 | {
364 | "cell_type": "markdown",
365 | "metadata": {},
366 | "source": [
367 | "## 小结"
368 | ]
369 | },
370 | {
371 | "cell_type": "markdown",
372 | "metadata": {},
373 | "source": [
374 | "* 函数也是对象,有 *id*,是 `function` 类型;\n",
375 | "* 所以函数也可以被赋值给一个变量,把那个变量变成自己的别名(*alias*);\n",
376 | "* 可以用 `lambda` 来创建一次性、一句话的小函数,在很多场景下都很有用。"
377 | ]
378 | }
379 | ],
380 | "metadata": {
381 | "kernelspec": {
382 | "display_name": "Python 3",
383 | "language": "python",
384 | "name": "python3"
385 | },
386 | "language_info": {
387 | "codemirror_mode": {
388 | "name": "ipython",
389 | "version": 3
390 | },
391 | "file_extension": ".py",
392 | "mimetype": "text/x-python",
393 | "name": "python",
394 | "nbconvert_exporter": "python",
395 | "pygments_lexer": "ipython3",
396 | "version": "3.7.4"
397 | }
398 | },
399 | "nbformat": 4,
400 | "nbformat_minor": 4
401 | }
402 |
--------------------------------------------------------------------------------
/p2-7-iterable-iterator.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Iterable 与 Iterator"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "在我们开始学习数据容器之前,先来学习 *iterable* 和 *iterator* 的概念。\n",
15 | "\n",
16 | "理解这两个概念就能理解 `for...in` 循环的本质,同时 *iterable* 也是所有数据容器的共性特征。"
17 | ]
18 | },
19 | {
20 | "cell_type": "markdown",
21 | "metadata": {},
22 | "source": [
23 | "## `for...in` 循环的本质"
24 | ]
25 | },
26 | {
27 | "cell_type": "markdown",
28 | "metadata": {},
29 | "source": [
30 | "*Iterable* 和 *iterator* 这两个词,来源于动词 *iterate*,*iterate* 与其名词形式 *iteration* 的含义就是“重复”,重复做一件事或者重复一句话;在计算机领域通常翻译为“迭代”,对一组数据逐个执行特定操作都可以称为 *iteration*,比如你已经熟悉的 `for...in` 循环就是例子:"
31 | ]
32 | },
33 | {
34 | "cell_type": "code",
35 | "execution_count": 1,
36 | "metadata": {},
37 | "outputs": [
38 | {
39 | "name": "stdout",
40 | "output_type": "stream",
41 | "text": [
42 | "1\n",
43 | "2\n",
44 | "3\n"
45 | ]
46 | }
47 | ],
48 | "source": [
49 | "for i in [1, 2, 3]:\n",
50 | " print(i)"
51 | ]
52 | },
53 | {
54 | "cell_type": "markdown",
55 | "metadata": {},
56 | "source": [
57 | "上面的代码本质是这样:\n",
58 | "1. 取列表 `[1, 2, 3]` 中的第一个元素(`1`),令 `i = 1`;\n",
59 | "2. 执行 `print(i)`,打印出 `i` 的值;\n",
60 | "3. 取列表 `[1, 2, 3]` 中的“下一个”元素,令 `i` 的值等于该元素;\n",
61 | "4. 重复步骤 2 至 3,直到列表中没有元素(即“下一个”元素为空值)。"
62 | ]
63 | },
64 | {
65 | "cell_type": "markdown",
66 | "metadata": {},
67 | "source": [
68 | "这个概念抽象一下,迭代的本质是对一组数据执行特定操作,确保所有数据都被操作正好一次,不重不漏。这是非常常用的一种场景,这里面有两个要素:\n",
69 | "* 被迭代处理的数据集 `X`,其中包含若干元素,可以通过“给我下一个元素”这样的指令逐个取出这些元素,确保不重不漏;\n",
70 | "* 需要对 X 的每个元素执行的操作 `f()`。\n",
71 | "\n",
72 | "只要具备这两个要素,我们就可以通过下面这样的代码来遍历操作 X 中的所有元素:\n",
73 | "```python\n",
74 | "for x in X:\n",
75 | " f(x)\n",
76 | "```"
77 | ]
78 | },
79 | {
80 | "cell_type": "markdown",
81 | "metadata": {},
82 | "source": [
83 | "Python 中的 *iterable* 和 *iterator* 就是从上面描述的抽象模型里发展而来的设计,给出了在 Python 中做迭代的标准模式。\n",
84 | "\n",
85 | "* 一个对象 X 是“**可迭代的**(*iterable*)”,就是说这个对象支持一个名为 `__iter__()` 的方法,这个方法返回一种叫“**迭代器**(*iterator*)”的对象;\n",
86 | "* 一个**迭代器**(*ietrator*)必须支持一个名为 `__next__()` 的方法,这个方法每次返回 X 里的下一个元素,直到所有元素被遍历正好一次。"
87 | ]
88 | },
89 | {
90 | "cell_type": "markdown",
91 | "metadata": {},
92 | "source": [
93 | "而我们用了好多次的 `for...in` 循环实际上是这么实现的:\n",
94 | "1. 获得 X 的迭代器 `iter = X.__iter__()`;\n",
95 | "2. 获得 X 中下一个元素 `x = iter.__next__()`;\n",
96 | "3. 如果 x 不为空,则执行 `f(x)` 然后重复步骤 2~3;否则结束。"
97 | ]
98 | },
99 | {
100 | "cell_type": "markdown",
101 | "metadata": {},
102 | "source": [
103 | "这下我们终于可以揭晓了:**凡是 *iterable* 的东西,都可以放在 `for...in` 循环中进行迭代操作**。\n",
104 | "\n",
105 | "我们后面要介绍的数据容器(*list*、*tuple*、*dictionary*、*set*)都是 *iterable*,所以都可以用 `for-in` 来遍历。\n",
106 | "\n",
107 | "另外,X 可以同时支持 `__iter__()` 和 `__next__()`,这样 X 既是 *iterable* 也是自己的 *iterator*,可以省略取迭代器的步骤,直接用 `__next__()` 开始,上面列出的几种内置数据容器都是这么做的。\n",
108 | "\n",
109 | "下面我们创建一些自己的 *iterable* 试试。"
110 | ]
111 | },
112 | {
113 | "cell_type": "markdown",
114 | "metadata": {},
115 | "source": [
116 | "## 迭代器例一:平方数序列"
117 | ]
118 | },
119 | {
120 | "cell_type": "markdown",
121 | "metadata": {},
122 | "source": [
123 | "第一个例子我们返回自然数的平方数序列,也就是 `1, 4, 9, 16, 25, ...`\n",
124 | "\n",
125 | "*Iterator* 是一种对象,所以首先我们要使用 `class` 关键字来定义一个类:"
126 | ]
127 | },
128 | {
129 | "cell_type": "code",
130 | "execution_count": 2,
131 | "metadata": {},
132 | "outputs": [
133 | {
134 | "data": {
135 | "text/plain": [
136 | "1"
137 | ]
138 | },
139 | "execution_count": 2,
140 | "metadata": {},
141 | "output_type": "execute_result"
142 | }
143 | ],
144 | "source": [
145 | "class squares:\n",
146 | " # 实例变量 self.base 用来记录当前平方数的基数,这个基数应该从 1 开始逐渐递增。\n",
147 | " # 然后定义了三个标准方法:\n",
148 | " # __init__() 把 self.base 设置为初始值 0;\n",
149 | " # __iter__() 返回迭代器,这里就是自己,我们直接返回 self;\n",
150 | " # __next__() 是迭代器主方法,这里我们先把 self.base 加一,然后返回它的平方,由于初始为 0,第一次调用返回的是 1 的平方。\n",
151 | " # 最后定义 next 作为 __next__ 的别名,方便调用(也与 Python 2.x 兼容)。\n",
152 | " def __init__(self):\n",
153 | " self.base = 0\n",
154 | " \n",
155 | " def __iter__(self):\n",
156 | " return self\n",
157 | "\n",
158 | " def __next__(self):\n",
159 | " self.base += 1\n",
160 | " return self.base * self.base\n",
161 | "\n",
162 | " next = __next__\n",
163 | "\n",
164 | "# 创建 squares 类的实例对象 iter,然后就可以调用其 next() 方法来迭代\n",
165 | "iter = squares()\n",
166 | "iter.next()"
167 | ]
168 | },
169 | {
170 | "cell_type": "code",
171 | "execution_count": 3,
172 | "metadata": {},
173 | "outputs": [
174 | {
175 | "data": {
176 | "text/plain": [
177 | "4"
178 | ]
179 | },
180 | "execution_count": 3,
181 | "metadata": {},
182 | "output_type": "execute_result"
183 | }
184 | ],
185 | "source": [
186 | "iter.next()"
187 | ]
188 | },
189 | {
190 | "cell_type": "code",
191 | "execution_count": 4,
192 | "metadata": {},
193 | "outputs": [
194 | {
195 | "data": {
196 | "text/plain": [
197 | "9"
198 | ]
199 | },
200 | "execution_count": 4,
201 | "metadata": {},
202 | "output_type": "execute_result"
203 | }
204 | ],
205 | "source": [
206 | "iter.next()"
207 | ]
208 | },
209 | {
210 | "cell_type": "markdown",
211 | "metadata": {},
212 | "source": [
213 | "注意到我们上面没有任何约束条件,这个迭代可以无限进行下去,如果我们把这个迭代器用在 `for...in` 循环中的话就会出现无限循环(直到计算机耗尽内存资源),所以通常我们会希望有个约束条件让迭代到一定程度就停下来,我们可以改写一下上面的代码:"
214 | ]
215 | },
216 | {
217 | "cell_type": "code",
218 | "execution_count": 5,
219 | "metadata": {},
220 | "outputs": [],
221 | "source": [
222 | "class squares:\n",
223 | " # 这次我们的初始化方法增加了一个参数 high\n",
224 | " # 初始化时将其保存在一个新增实例变量 self.high 中,限制迭代时 self.base 不能超过这个数\n",
225 | " def __init__(self, high):\n",
226 | " self.base = 0\n",
227 | " self.high = high\n",
228 | " \n",
229 | " def __iter__(self):\n",
230 | " return self\n",
231 | "\n",
232 | " # 在迭代器主方法里加入判断,如果 self.base 超过了 self.high 就扔出一个 StopIteration 异常\n",
233 | " def __next__(self):\n",
234 | " self.base += 1\n",
235 | " if self.base > self.high:\n",
236 | " raise StopIteration\n",
237 | " else:\n",
238 | " return self.base * self.base\n",
239 | "\n",
240 | " next = __next__\n",
241 | "\n",
242 | "# 这次我们实例化时要传入一个参数来指定 self.high 的值\n",
243 | "# 这将使得我们调用三次 next(),输出 1,4,9 之后再调用就不会有返回值\n",
244 | "iter = squares(3)"
245 | ]
246 | },
247 | {
248 | "cell_type": "markdown",
249 | "metadata": {},
250 | "source": [
251 | "这次我们的迭代器有“边界”了,就可以将其直接用在 `for...in` 循环中,`for...in` 循环会在碰到 `StopIteration` 时自动终止:"
252 | ]
253 | },
254 | {
255 | "cell_type": "code",
256 | "execution_count": 6,
257 | "metadata": {},
258 | "outputs": [
259 | {
260 | "name": "stdout",
261 | "output_type": "stream",
262 | "text": [
263 | "1\n",
264 | "4\n",
265 | "9\n"
266 | ]
267 | }
268 | ],
269 | "source": [
270 | "for n in squares(3):\n",
271 | " print(n)"
272 | ]
273 | },
274 | {
275 | "cell_type": "markdown",
276 | "metadata": {},
277 | "source": [
278 | "## 迭代器例二:斐波那契数列"
279 | ]
280 | },
281 | {
282 | "cell_type": "markdown",
283 | "metadata": {},
284 | "source": [
285 | "第二个例子我们来实现一个**斐波那契**(*Fibonacci*)数列,这个数列之前已经见过,其特点是每一个元素它前面两个元素之和,写下来大致是这个样子的:`1, 1, 2, 3, 5, 8, 13, 21, ...`\n",
286 | "\n",
287 | "基本的迭代器实现和例一没啥区别,如果前面的内容没有什么问题,这个也就很好理解:"
288 | ]
289 | },
290 | {
291 | "cell_type": "code",
292 | "execution_count": 7,
293 | "metadata": {},
294 | "outputs": [],
295 | "source": [
296 | "class fib:\n",
297 | " def __init__(self):\n",
298 | " self.prev = 0\n",
299 | " self.curr = 1\n",
300 | "\n",
301 | " def __iter__(self):\n",
302 | " return self\n",
303 | "\n",
304 | " def __next__(self):\n",
305 | " self.prev, self.curr = self.curr, self.prev + self.curr\n",
306 | " return self.prev"
307 | ]
308 | },
309 | {
310 | "cell_type": "markdown",
311 | "metadata": {},
312 | "source": [
313 | "可以看到我们这个实现也是可以无限迭代的,如果我们需要限制迭代次数,可以像例一那样改写,但是大多数情况下不需要,因为这种操作太常见了,早就有了标准化的通用解决方案——这也是学编程的一个窍门:凡是看上去很“常规”和“常见”的需求,一般都会有优秀的通用解决方案,我们找出来用比自己写好,这叫“**不重复发明轮子**(*Don't Reinvent The Wheel*)”原理。这些标准解决方案在 `itertools` 模块中,用于对无限迭代器进行切片的方法叫做 `islice()`。\n",
314 | "\n",
315 | "`islice()` 做的事情相当于从迭代器产生的无限序列中切出一段来,它返回的也是一个迭代器,返回的迭代器输出的就是有头有尾、有限的序列了。\n",
316 | "\n",
317 | "`islice()` 接受三个参数:一个迭代器实例,切片的起始和结束序号(序号从 0 开始算);切的时候和 `range()` 函数的算法一样,包含起始序号那一项,但不包括结束序号的一项。比如 `islice(f, 0, 10)` 就是从迭代器 `f` 生成的序列中切出第 1 到第 10 项,返回这 10 项对应的有限迭代器。\n",
318 | "\n",
319 | "其实 `islice()` 还可以有第四个参数:步长 `step`,缺省值为 1,就是起始到结束范围内每一项都保留;如果指定了别的 `step` 值,那么就是每隔 `step-1` 项输出一项。基本逻辑和 `range()` 函数一样。\n",
320 | "\n",
321 | "因为 `islice()` 返回的还是迭代器,所以可以直接用于 `for...in` 循环中。"
322 | ]
323 | },
324 | {
325 | "cell_type": "code",
326 | "execution_count": 8,
327 | "metadata": {},
328 | "outputs": [
329 | {
330 | "name": "stdout",
331 | "output_type": "stream",
332 | "text": [
333 | "1\n",
334 | "1\n",
335 | "2\n",
336 | "3\n",
337 | "5\n",
338 | "8\n",
339 | "13\n",
340 | "21\n",
341 | "34\n",
342 | "55\n"
343 | ]
344 | }
345 | ],
346 | "source": [
347 | "from itertools import islice\n",
348 | "\n",
349 | "f = fib()\n",
350 | "for n in islice(f, 0, 10):\n",
351 | " print(n)"
352 | ]
353 | },
354 | {
355 | "cell_type": "markdown",
356 | "metadata": {},
357 | "source": [
358 | "`islice()` 帮我们做了限界这样的事情,我们在写迭代器的时候就不用考虑了,只要把迭代的算法写清楚,要截取一段来用的话用 `islice()` 就好了。`itertools` 里还有不少很有用的东西,比如 `cycle()` 可以循环一个有限的迭代器得到一个无限版本:"
359 | ]
360 | },
361 | {
362 | "cell_type": "code",
363 | "execution_count": 9,
364 | "metadata": {},
365 | "outputs": [
366 | {
367 | "data": {
368 | "text/plain": [
369 | "['red', 'yellow', 'green', 'red', 'yellow', 'green', 'red']"
370 | ]
371 | },
372 | "execution_count": 9,
373 | "metadata": {},
374 | "output_type": "execute_result"
375 | }
376 | ],
377 | "source": [
378 | "from itertools import cycle\n",
379 | "# 数组既是 iterable 也是 iterator,cycle() 从输入的有限数组循环创建一个无限的 iterator\n",
380 | "colors = cycle(['red', 'yellow', 'green'])\n",
381 | "# 用 islice() 截取这个无限迭代器的前 10 个,然后用 list() 把输出的迭代器转换成列表\n",
382 | "list(islice(colors, 0, 7))"
383 | ]
384 | },
385 | {
386 | "cell_type": "markdown",
387 | "metadata": {},
388 | "source": [
389 | "更多关于 `itertools` 库的说明可以参考 [官方文档](https://docs.python.org/3.7/library/itertools.html)。"
390 | ]
391 | },
392 | {
393 | "cell_type": "markdown",
394 | "metadata": {},
395 | "source": [
396 | "## One More Thing..."
397 | ]
398 | },
399 | {
400 | "cell_type": "markdown",
401 | "metadata": {},
402 | "source": [
403 | "不知道你有没有在书写上面的代码时觉得每次要定义个 `class` 还有 `__iter__()` `__next__()` 这样的“八股”很啰嗦?恭喜你,很有优秀程序员的潜质,事实上上面代码里真正有价值的就是 `__next__()` 的实现部分,所以 Python 提供了一种特殊的迭代器(*iterator* )写法,叫做“**生成器**(*generator*)”,更紧凑更精简。比如上面的 Fibonacci 数列,用生成器来写就是这样的:"
404 | ]
405 | },
406 | {
407 | "cell_type": "code",
408 | "execution_count": 10,
409 | "metadata": {},
410 | "outputs": [
411 | {
412 | "data": {
413 | "text/plain": [
414 | "[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]"
415 | ]
416 | },
417 | "execution_count": 10,
418 | "metadata": {},
419 | "output_type": "execute_result"
420 | }
421 | ],
422 | "source": [
423 | "def fib():\n",
424 | " prev, curr = 0, 1\n",
425 | " while True:\n",
426 | " # 这个 yield 是一切魔法的核心,下面解释\n",
427 | " yield curr\n",
428 | " prev, curr = curr, prev + curr\n",
429 | "\n",
430 | "f = fib()\n",
431 | "list(islice(f, 0, 10))"
432 | ]
433 | },
434 | {
435 | "cell_type": "code",
436 | "execution_count": 11,
437 | "metadata": {},
438 | "outputs": [
439 | {
440 | "data": {
441 | "text/plain": [
442 | "generator"
443 | ]
444 | },
445 | "execution_count": 11,
446 | "metadata": {},
447 | "output_type": "execute_result"
448 | }
449 | ],
450 | "source": [
451 | "type(fib())"
452 | ]
453 | },
454 | {
455 | "cell_type": "markdown",
456 | "metadata": {},
457 | "source": [
458 | "是不是清爽漂亮了很多?但是这个魔法的原理是啥呢?首先我们来看看 `fib()` 这个定义:\n",
459 | "* 一个 *generator* 定义就是一个函数的定义,但这是个特殊的函数,函数体里没有 `return`,取而代之的是特殊的关键字 `yield`;\n",
460 | "* 解释器会自动把任何包含 `yield` 的函数包装成 `generator`(一种特殊的 *iterator*)类型的对象,并将此对象作为函数的返回值,所以上面的 `f` 变量其实还是个 *iterator*。\n",
461 | "\n",
462 | "然后就是 `yield` 这个魔法的关键了。`yield` 和 `return` 相当,也会从函数中返回一些值,但不一样的是:`return` 返回值的同时彻底终止了函数,而 `yield` 则是“暂停”了函数执行,保存了函数当时的所有状态(比如所有局部变量的值),以后再返回到 `yield` 的下一行继续执行——这个“以后”到底是什么时候呢?聪明的你一定可以猜到,就是我们下一次调用 `__next__()` 的时候!\n",
463 | "\n",
464 | "小结一下,上面的代码定义了一个函数 `fib()`,调用这个函数会返回一个生成器(一种特殊的迭代器):\n",
465 | "* 当我们第一次调用这个生成器的 `__next__()` 的时候,会执行 `fib()` 函数的函数体直到碰到 `yield` 语句,`yield` 的返回值就是这次调用 `__next__()` 的结果;同时解释器暂停函数 `fib()` 的运行并把状态保存下来;\n",
466 | "* 当我们再次调用生成器的 `__next__()` 的时候,解释器重新唤醒函数 `fib()`,恢复保存的状态,并从上次 `yield` 的下一行继续执行,直到再碰到 `yield`,`yield` 的返回值就是这次调用 `__next__()` 的结果;\n",
467 | "* 每次调用生成器的 `__next__()` 时重复上述操作。\n",
468 | "\n",
469 | "是不是略有点烧脑?在这里你一定要烧清楚。这次清楚了后面就简单,否则这一类的逻辑还会有很多,始终是要迈过的坎,而且生成器很有用,很多地方都会碰到。"
470 | ]
471 | },
472 | {
473 | "cell_type": "markdown",
474 | "metadata": {},
475 | "source": [
476 | "## 练习"
477 | ]
478 | },
479 | {
480 | "cell_type": "markdown",
481 | "metadata": {},
482 | "source": [
483 | "尝试定义可以生成素数的迭代器和生成器,并利用之输出前 10 个素数。"
484 | ]
485 | },
486 | {
487 | "cell_type": "markdown",
488 | "metadata": {},
489 | "source": [
490 | "### 参考答案"
491 | ]
492 | },
493 | {
494 | "cell_type": "markdown",
495 | "metadata": {},
496 | "source": [
497 | "我们先来看迭代器的写法,用到了我们之前写过的 `is_prime()` 算法:"
498 | ]
499 | },
500 | {
501 | "cell_type": "code",
502 | "execution_count": 12,
503 | "metadata": {},
504 | "outputs": [
505 | {
506 | "data": {
507 | "text/plain": [
508 | "[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]"
509 | ]
510 | },
511 | "execution_count": 12,
512 | "metadata": {},
513 | "output_type": "execute_result"
514 | }
515 | ],
516 | "source": [
517 | "from math import sqrt\n",
518 | "from itertools import islice\n",
519 | "\n",
520 | "class primes:\n",
521 | " def __init__(self):\n",
522 | " self.next = 2\n",
523 | "\n",
524 | " def __iter__(self):\n",
525 | " return self\n",
526 | "\n",
527 | " def _is_prime(self, n):\n",
528 | " if n < 2:\n",
529 | " return False\n",
530 | "\n",
531 | " if n in (2, 3):\n",
532 | " return True\n",
533 | "\n",
534 | " for i in range(2, int(sqrt(n)) + 1):\n",
535 | " if n % i == 0:\n",
536 | " return False\n",
537 | "\n",
538 | " return True\n",
539 | "\n",
540 | " def __next__(self):\n",
541 | " n = self.next\n",
542 | " while not self._is_prime(n):\n",
543 | " n += 1\n",
544 | "\n",
545 | " self.next = n + 1\n",
546 | " return n\n",
547 | " \n",
548 | "list(islice(primes(), 0, 10))"
549 | ]
550 | },
551 | {
552 | "cell_type": "markdown",
553 | "metadata": {},
554 | "source": [
555 | "参照前面的例子,将上面这个 `iterator` 类改写为 `generator` 应该也不难,我们把这个作业留给你。\n",
556 | "\n",
557 | "下面额外给出一种 *generator* 的写法,这个写法用到了著名的**埃拉托斯特尼筛法**(*Sieve Of Eratosthenes*),是快速计算素数的经典算法,关于筛法的基本介绍可以参考[维基百科](https://zh.wikipedia.org/zh-cn/%E5%9F%83%E6%8B%89%E6%89%98%E6%96%AF%E7%89%B9%E5%B0%BC%E7%AD%9B%E6%B3%95)。\n",
558 | "\n",
559 | "我们在代码的每一步都写了简要注释,主要戏法在函数体里定义的那个 `D`,这是一个 `dictionary` 类型的数据容器,如果看不明白也没关系,在学完后面几种数据容器之后再来看,就应该容易一些了。"
560 | ]
561 | },
562 | {
563 | "cell_type": "code",
564 | "execution_count": 13,
565 | "metadata": {},
566 | "outputs": [],
567 | "source": [
568 | "def primes():\n",
569 | " \"\"\"Generator which yields the sequence of prime numbers via the Sieve of Eratosthenes.\"\"\"\n",
570 | " D = {} # map composite integers to primes witnessing their compositeness\n",
571 | " q = 2 # first integer to test for primality\n",
572 | " while 1:\n",
573 | " if q not in D:\n",
574 | " yield q # not marked composite, must be prime\n",
575 | " D[q*q] = [q] # first multiple of q not already marked\n",
576 | " else:\n",
577 | " for p in D[q]: # move each witness to its next multiple\n",
578 | " D.setdefault(p+q,[]).append(p)\n",
579 | " del D[q] # no longer need D[q], free memory\n",
580 | " q += 1"
581 | ]
582 | },
583 | {
584 | "cell_type": "code",
585 | "execution_count": 14,
586 | "metadata": {},
587 | "outputs": [
588 | {
589 | "data": {
590 | "text/plain": [
591 | "[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]"
592 | ]
593 | },
594 | "execution_count": 14,
595 | "metadata": {},
596 | "output_type": "execute_result"
597 | }
598 | ],
599 | "source": [
600 | "from itertools import islice\n",
601 | "list(islice(primes(), 0, 20))"
602 | ]
603 | }
604 | ],
605 | "metadata": {
606 | "kernelspec": {
607 | "display_name": "Python 3",
608 | "language": "python",
609 | "name": "python3"
610 | },
611 | "language_info": {
612 | "codemirror_mode": {
613 | "name": "ipython",
614 | "version": 3
615 | },
616 | "file_extension": ".py",
617 | "mimetype": "text/x-python",
618 | "name": "python",
619 | "nbconvert_exporter": "python",
620 | "pygments_lexer": "ipython3",
621 | "version": "3.7.4"
622 | }
623 | },
624 | "nbformat": 4,
625 | "nbformat_minor": 4
626 | }
627 |
--------------------------------------------------------------------------------
/p2-a-tree.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# 树"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "## 定义问题"
15 | ]
16 | },
17 | {
18 | "cell_type": "markdown",
19 | "metadata": {},
20 | "source": [
21 | "当我们说“树形结构”的时候,那是从自然界的大树上得到的启发,从根开始不断展开的枝丫,代表了从单一到复杂的一层层展开,在现实世界充满了这样的事物:国家的行政区划(以及任何组织的架构)、电商网站的商品分类、论坛里的板块等等。"
22 | ]
23 | },
24 | {
25 | "cell_type": "markdown",
26 | "metadata": {},
27 | "source": [
28 | "
"
29 | ]
30 | },
31 | {
32 | "cell_type": "markdown",
33 | "metadata": {},
34 | "source": [
35 | "> 虽然自然界里树是根在下面而向上分支,但是我们在处理树形结构时,如图这样的从上向下分支更容易画也更容易看。"
36 | ]
37 | },
38 | {
39 | "cell_type": "markdown",
40 | "metadata": {},
41 | "source": [
42 | "解决问题的第一步是先要清晰明确地定义问题,我们来尝试把我们对树的直觉印象书写成尽可能清晰明确的定义和表述。在一个树形结构里:\n",
43 | "* 树(*tree*)由节点(*node*)和连接节点的边(*edge*)组成;\n",
44 | "* 总有一个节点是分支的起点,它分出了所有其他的节点,这个节点叫根节点(*root node*);\n",
45 | "* 一个节点分支出来的节点叫它的“子节点(*child nodes*)”,它是其子节点的“父节点(*parent node*)”;上图中节点 A 的父节点是根节点,子节点是 B 和 C,而节点 C 的父节点是 A,子节点是 D 和 E;节点 D 的父节点是 C,而它没有子节点;\n",
46 | "* 拥有共同父节点的两个节点互为“兄弟姐妹(*sibling nodes*)”;比如上图中 B 和 C,D 和 E;\n",
47 | "* 没有子节点的节点也叫“叶子节点(*leaf node*)”;比如上图中的 D 和 E;\n",
48 | "* 一个节点的父节点,以及父节点的父节点…直至根节点,都是这个节点的“祖先节点(*ancestor nodes*)”;比如上图中 E 的祖先节点包括 C、A 和 根节点;\n",
49 | "* 一个节点的所有子节点,以及所有子节点的子节点…直至叶子节点,都是这个节点的“后代节点(*descendant nodes*)”;\n",
50 | "* 一个节点 X 和它所有后代节点、以及这些节点之间的边,也组成一棵树,叫做原数的一棵(以 X 为根的)“子树(*sub-tree*)”;\n",
51 | "* 根节点没有父节点也没有祖先节点;\n",
52 | "* 除了根节点以外的任何节点,有且只有 1 个父节点,有至少 1 个祖先节点;\n",
53 | "* 任何节点都可以有 0、1 或者多个子节点,可以有 0、1 或者多个后代节点;\n",
54 | "* 一个节点和它的任一祖先或者后代节点之间,一定存在一条由边首尾连接组成的路径(*path*),比如上图中根节点是 E 的祖先,它们之间的路径是 `root -> A -> C -> E`;这个路径就像是两个节点之间的亲属关系链;\n",
55 | "* 两个节点之间 *path* 的长度,就是它由几条边组成,决定了这两个节点之间隔了多少“代”,也叫节点之间的“距离(*distance*)”;\n",
56 | "* 一个节点和根节点之间的 *distance* 经常有特别的含义,相当于该节点在树的“第几层级(*tier*)”;比如在行政区划里哪些是一级节点(省、直辖市、自治区)哪些是二级节点等;\n",
57 | "* 有些树里节点的子节点是有顺序的,叫做“有序树(*ordered tree*)”;如果我们不特别指出,那就是不考虑这种顺序概念。\n",
58 | "\n",
59 | "上面这种“把直觉规则化”的过程,对后面设计解决方案是至关重要的,大家可以自己尝试,多多体会。\n",
60 | "\n",
61 | "> 事实上在数学图论里有对树的[更严谨更数学化的定义](https://en.wikipedia.org/wiki/Tree_(graph_theory)),不过上面的表述对目前的我们来说基本够用了。"
62 | ]
63 | },
64 | {
65 | "cell_type": "markdown",
66 | "metadata": {},
67 | "source": [
68 | "## 分析操作场景"
69 | ]
70 | },
71 | {
72 | "cell_type": "markdown",
73 | "metadata": {},
74 | "source": [
75 | "可以看到树和我们前面讲的所有数据结构都不一样,没办法用一种“线性(*linear*)”结构来表达,如何在计算机中实现一个树形数据结构?面对这样的问题,我们应该如何思考呢?\n",
76 | "\n",
77 | "一个优秀的起点是问题解决的一半。设计数据结构的起点是:**思考我们会怎么操作和使用这个数据结构**。\n",
78 | "\n",
79 | "假定我们已经有一个树形数据结构,我们可能会有这些操作场景(可以尝试用行政区划或者论坛版块等熟悉的实例来帮助思考):\n",
80 | "* 查找和遍历:\n",
81 | " * 给定一个 *node* 找出它的 *parent* ✪\n",
82 | " * 给定一个 *node* 找出它所有的 *children* ✪\n",
83 | " * 给定一个 *node* 找出它某个特定 *child*\n",
84 | " * 给定一个 *node* 找出它所有的 *siblings*\n",
85 | " * 给定一个 *node* 遍历其 *sub-tree* 即找出它的所有 *descendants* ✪\n",
86 | " * 给定 *node* *A* 和 *B*,找到 *A* 和 *B* 之间的 *path* 和 *distance*\n",
87 | " * 给定一个 *node* 确定它的 *tier*\n",
88 | "* 编辑\n",
89 | " * 给一个 *node* 增加一个 *child* ✪\n",
90 | " * 删除一个 *node* ✪ -> 新一步思考:这意味着什么?删除整个子树还是?\n",
91 | " * 修改一个 *node* 的 *parent* ✪\n",
92 | "\n",
93 | "差不多就这些,其中标记了 ✪ 号的是感觉特别重要和基础的操作。\n",
94 | "\n",
95 | "通过形象化的图示、对基本概念的定义和表述、对可能操作场景的罗列,已经增加了很多我们对问题理解的深度和全面度,下来我们可以尝试做出一些初步设计判断了。"
96 | ]
97 | },
98 | {
99 | "cell_type": "markdown",
100 | "metadata": {},
101 | "source": [
102 | "## 设计初步方案"
103 | ]
104 | },
105 | {
106 | "cell_type": "markdown",
107 | "metadata": {},
108 | "source": [
109 | "从前面的分析,我们可以有这么一些收获:\n",
110 | "* 树中节点之间的父子关系是核心和本质的内容;\n",
111 | "* 表达这种父子关系的关键是节点,节点应保存父节点和子节点相关信息;\n",
112 | "* 树的结构上带有显著的“递归”特点,可以有助于我们的设计。\n",
113 | "\n",
114 | "如果你不记得“递归(*recursion*,*recursive*)”是怎么回事了,可以温习[递归函数](p2-4-recursion.ipynb)一章。\n",
115 | "\n",
116 | "树也是递归的一个典型例子,因为一棵树可以看做由**根节点、根节点的子节点和以这些子节点为根的子树**合起来组成的,如果我们想实现对树进行操作的函数 `f()`,那么我们可以让这个函数接受一个树的根节点作为输入参数,这个函数大致上会是这个样子:\n",
117 | "\n",
118 | "```python\n",
119 | "def f(root):\n",
120 | " # 对 root 做一些操作\n",
121 | " # 然后取出 root 的所有子节点,对其中每个子节点调用 f() 本身\n",
122 | " for node in root.children:\n",
123 | " f(node)\n",
124 | "```\n",
125 | "\n",
126 | "也就是说,我们只要对某个节点做操作,然后获取这个节点的所有子节点,递归调用自己,就能对整个树做操作了。\n",
127 | "\n",
128 | "在树形数据结构中,“获取一个节点的所有子节点”是至关重要的操作。\n",
129 | "\n",
130 | "据此我们可以做出这么一个初步的设计:"
131 | ]
132 | },
133 | {
134 | "cell_type": "code",
135 | "execution_count": 1,
136 | "metadata": {},
137 | "outputs": [],
138 | "source": [
139 | "class TreeNode:\n",
140 | " def __init__(self, name='root', data=None, parent=None, children=None):\n",
141 | " self.name = name\n",
142 | " self.data = data\n",
143 | " \n",
144 | " if parent:\n",
145 | " # 确认 parent 参数是 TreeNode 类型\n",
146 | " assert isinstance(parent, TreeNode)\n",
147 | " parent.add_child(self)\n",
148 | " self.parent = parent\n",
149 | " \n",
150 | " self.children = []\n",
151 | " if children:\n",
152 | " for child in children:\n",
153 | " self.add_child(child)\n",
154 | "\n",
155 | " def add_child(self, node):\n",
156 | " # 1. 确认 node 参数是 TreeNode 类型\n",
157 | " # 2. 将要加入的子节点的 parent 属性设为自己\n",
158 | " # 3. 然后将其加入 children 列表\n",
159 | " assert isinstance(node, TreeNode)\n",
160 | " node.parent = self\n",
161 | " self.children.append(node)"
162 | ]
163 | },
164 | {
165 | "cell_type": "markdown",
166 | "metadata": {},
167 | "source": [
168 | "我们设计的核心数据结构是表示树节点的自定义类型 TreeNode,这个类型的对象有四个实例变量(属性):\n",
169 | "* *name*:节点的名字,最好能唯一标识出一个节点\n",
170 | "* *data*:节点相关的任何数据,可以是任何数据类型\n",
171 | "* *parent*:节点的父节点,如果没有父节点就是 None\n",
172 | "* *children*:节点所有子节点组成的一个列表\n",
173 | "\n",
174 | "这里面 *parent* 和 *children* 里的元素都必须是 TreeNode 类型的对象,我们在处理这两个属性时要先确认这一点,在上面的代码中我们用 Python 的 `assert` 语句和 `isinstance()` 函数来实现:\n",
175 | "* `assert` 关键字后面的表达式必须返回 True,否则程序将抛出 `AssertionError` 异常后终止;\n",
176 | "* `isinstance(obj, type)` 之前我们就介绍过,它接受两个参数,第一个参数是一个对象,而第二个参数是一个类型,函数判断第一个参数是不是第二个参数指明的类型,如果是返回 `True`,否则返回 `False`。\n",
177 | "\n",
178 | "如上定义的 `TreeNode` 类,它实例化出来的对象 `node` 具备如下的能力:\n",
179 | "* 很容易取得其父节点 *parent*;\n",
180 | "* 很容易取得其所有子节点 *children*;\n",
181 | "* 已经实现了增加子节点的操作。\n",
182 | "\n",
183 | "最有意思的是,TreeNode 类型实际上也代表了树本身,因为一个节点加上它所有子节点,本来就是一棵树嘛!\n",
184 | "\n",
185 | "在这个类型定义的基础上,我们可以实现前一节列出的所有操作,你可以把这作为练习动手试一试。\n",
186 | "\n",
187 | "> 在实际项目中,我们并不需要定义树的数据结构,因为有优秀的第三方实现可用,比如 Python 非常棒的第三方库 [anytree](https://anytree.readthedocs.io/en/latest/#),你可以试试,看和你自己实现的有什么区别。"
188 | ]
189 | },
190 | {
191 | "cell_type": "markdown",
192 | "metadata": {},
193 | "source": [
194 | "## 小结"
195 | ]
196 | },
197 | {
198 | "cell_type": "markdown",
199 | "metadata": {},
200 | "source": [
201 | "这一章介绍非常常见的树形逻辑怎么实现。没有很多代码,主要讲的是思维方法,请你读完一遍之后尝试自己从头思考和建立一个基本的树的数据结构,如果遇到问题就再读一遍。"
202 | ]
203 | }
204 | ],
205 | "metadata": {
206 | "kernelspec": {
207 | "display_name": "Python 3",
208 | "language": "python",
209 | "name": "python3"
210 | },
211 | "language_info": {
212 | "codemirror_mode": {
213 | "name": "ipython",
214 | "version": 3
215 | },
216 | "file_extension": ".py",
217 | "mimetype": "text/x-python",
218 | "name": "python",
219 | "nbconvert_exporter": "python",
220 | "pygments_lexer": "ipython3",
221 | "version": "3.7.4"
222 | }
223 | },
224 | "nbformat": 4,
225 | "nbformat_minor": 4
226 | }
227 |
--------------------------------------------------------------------------------
/p2-b-fsm.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# 有限状态机"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "有限状态机(*finite state machine*,简称 *FSM*),有时也被称为 *finite state automation*,有时就简单地叫 *state machine*,不属于一看就知道大概是什么的概念(这一点和前面我们讲过的都不一样)。有限状态机有相当深刻的理论背景,算是比较高级的东西了,很多程序员别说学校里,工作十年可能都没碰过这东西,但其实真的不难理解,而且学会了就爱不释手,因为它解决某些问题真是太好用了。"
15 | ]
16 | },
17 | {
18 | "cell_type": "markdown",
19 | "metadata": {},
20 | "source": [
21 | "## 什么是有限状态机"
22 | ]
23 | },
24 | {
25 | "cell_type": "markdown",
26 | "metadata": {},
27 | "source": [
28 | "其实我们身边到处都是“有限状态机”的例子,最简单的一个是灯:灯有两种状态:“亮”和“熄”,灯可以从一种状态变成另一种,“亮”的状态下接收到“关”的指令就会变成“熄”,“熄”的状态下接收到“开”的指令就会变成“亮”,就像下图这样:"
29 | ]
30 | },
31 | {
32 | "cell_type": "markdown",
33 | "metadata": {},
34 | "source": [
35 | "
"
36 | ]
37 | },
38 | {
39 | "cell_type": "markdown",
40 | "metadata": {},
41 | "source": [
42 | "在这个图里,圆圈表示“状态(*state*)”,箭头代表状态间可以发生的“转换(*transition*)”,而箭头上标注的文字代表触发状态转换的“输入(*input*)”。这基本上就是状态机的三大要件了。\n",
43 | "\n",
44 | "灯只有两个状态,不算很有意思,我们可以再看一个常见的例子:红绿灯,我们熟知的红绿灯的颜色按照“绿 -> 黄 -> 红 -> 绿”这样的顺序循环变化——嗯,我知道有的还有“绿灯闪烁”之类的状态,不过我们这里简化一下,用有限状态机来描述大致如下图:"
45 | ]
46 | },
47 | {
48 | "cell_type": "markdown",
49 | "metadata": {},
50 | "source": [
51 | "
"
52 | ]
53 | },
54 | {
55 | "cell_type": "markdown",
56 | "metadata": {},
57 | "source": [
58 | "这里:\n",
59 | "* 有三种状态:绿,黄,红;\n",
60 | "* 状态转换是受限的,绿只能转黄,黄只能转红,红只能转绿;诸如黄转绿这样的状态转换是不允许的;\n",
61 | "* 状态转换的输入条件很简单,接收到 1 就转换到下一个状态。"
62 | ]
63 | },
64 | {
65 | "cell_type": "markdown",
66 | "metadata": {},
67 | "source": [
68 | "所以简单来说,有限状态机就是一台包含了预先定义好的一组状态的机器,当机器接收到一个指令,就根据指令内容查一张预先定义好的表:\n",
69 | "1. 检查当前状态是否接受这个指令;\n",
70 | "2. 如果不接受,那就当无事发生;\n",
71 | "3. 如果接受,再检查表中“当前状态x该指令”对应的目标状态是什么,然后把机器状态转换为目标状态。\n",
72 | "\n",
73 | "至于何时发送指令给状态机,是由外部系统决定的,比如红绿灯的例子里,外部系统是几个定时器,时间到了就发信号给有限状态机切换状态。\n",
74 | "\n",
75 | "有了现实生活中的例子打底,我们现在可以来看看抽象的“有限状态机(*FSM*)”是怎么定义的了。"
76 | ]
77 | },
78 | {
79 | "cell_type": "markdown",
80 | "metadata": {},
81 | "source": [
82 | "
"
83 | ]
84 | },
85 | {
86 | "cell_type": "markdown",
87 | "metadata": {},
88 | "source": [
89 | "如上图所示,每一个 *FSM* 都包含五个要素:\n",
90 | "* *Q* 包含了有限个状态(*states*)的集合;\n",
91 | "* *Σ* 包含了有限个、非空的有效输入(*input*)的集合;\n",
92 | "* *δ* 一系列转换函数(*transition functions*),定义了什么样的当前状态结合什么输入可以变成什么目标状态,通常可以定义为一张二维表(见上图);\n",
93 | "* *q0* 起始状态,并不是所有 *FSM* 都关心起始状态,比如红绿灯就无所谓起始状态;\n",
94 | "* *F* 包含了所有“结束状态(*final states*)”的集合,这个名字容易误解,它的作用和有限状态机的具体类型及面对的问题有关,我们可以简单理解为“标记出来有特别含义的状态的集合”就可以了,注意这个集合可以是空的。"
95 | ]
96 | },
97 | {
98 | "cell_type": "markdown",
99 | "metadata": {},
100 | "source": [
101 | "以上图所示的 *FSM* 为例,\n",
102 | "* 这个 *FSM* 有四个有效状态,*Q = { A, B C, D}*;\n",
103 | "* 这个 *FSM* 只接受两个合法输入,*Σ = { 0, 1 }*;\n",
104 | "* 当这个 *FSM* 接收到输入时,不在 *Σ = { 0, 1 }* 中的输入会被丢弃;如果输入在 *Σ* 中(是 *0* 或者 *1*),就查 *δ* 表,看看当前状态对应行和输入对应列的交叉点是什么状态,比如当前状态是 *A*,输入是 *1*,那么对应状态为 *C*,也就是说应该转换到状态 *C*。\n",
105 | "* 起始状态 *q0 = A* 和结束状态集 *F = { D }* 这两个对某些 *FSM* 来说很重要,比如正则表达式。\n",
106 | "\n",
107 | "> 正则表达式对应一大类有限状态机,主要用来解决“在一系列输入之后是什么状态”的问题,通过回答这个问题来判断输入序列是不是我们想要的,或者输入序列属于什么分类,这种状态机有很深刻的理论背景,有兴趣的话可以读一下计算理论(*computation theory*)的经典教材,比如[这本](https://www.amazon.com/Introduction-Theory-Computation-Michael-Sipser/dp/113318779X);这类状态机还被广泛应用于人工智能(比如图像识别算法)中。"
108 | ]
109 | },
110 | {
111 | "cell_type": "markdown",
112 | "metadata": {},
113 | "source": [
114 | "在计算机编程领域 *FSM* 最广泛的应用之一是流程和行为控制(*flow and behavior control*),简单说就是管理:\n",
115 | "* 在某个状态下什么能做什么不能做;\n",
116 | "* 做了什么会变成另外的什么状态。\n",
117 | "\n",
118 | "游戏里[玩家行为控制](https://gameprogrammingpatterns.com/state.html)、[NPC(*non-player character*,非玩家角色)的 AI](https://gamedevelopment.tutsplus.com/tutorials/finite-state-machines-theory-and-implementation--gamedev-11867)、剧情任务流程等都是用 *FSM* 来实现的;所有的[工作流系统](http://b.xfreeservice.com/redir/clickGate.php?u=8otB939m&m=12&p=3b121G4eNI&t=33&splash=0&s=&q=state%20machine%20workflow&url=https%3A%2F%2Fdocs.microsoft.com%2Fen-us%2Fdotnet%2Fframework%2Fwindows-workflow-foundation%2Fstate-machine-workflows)都包含 *FSM*;还有电商核心系统之一的“订单系统”(*order system*)。\n",
119 | "\n",
120 | "我们用过淘宝都知道,一个订单从创建开始要经历好几个状态,中间也有不同的操作可以进行,下面是一个比较典型的流程设计,经过一定简化,并以“状态”的主视角来描绘:"
121 | ]
122 | },
123 | {
124 | "cell_type": "markdown",
125 | "metadata": {},
126 | "source": [
127 | "
"
128 | ]
129 | },
130 | {
131 | "cell_type": "markdown",
132 | "metadata": {},
133 | "source": [
134 | "图中圆边的矩形代表状态,最上面一排是“正常”的状态和流程;第二排的矩形则表示一些“逆向”子流程,通常是由用户或客户发起的特殊操作,这些操作会带来其他一些订单状态,为了简单起见没有在这里展开。"
135 | ]
136 | },
137 | {
138 | "cell_type": "markdown",
139 | "metadata": {},
140 | "source": [
141 | "流程说明:\n",
142 | "* 当买家点击下单时订单生成,处于“已创建”状态;\n",
143 | "* 这个状态下的正常操作是“支付”,如果输入“支付成功”会进入下一个状态“已支付”,“支付失败”或者没有任何操作则停在本状态;\n",
144 | "* 这个状态下其他可选操作包括“修改”、“取消”等,分别会去到订单修改和订单取消子流程(这里略去细节);\n",
145 | "* 支付成功后进入处于“已支付”状态;\n",
146 | "* 这个状态下需要等待商家发货,商家输入“已发货”会进入下一个状态“配送中”;\n",
147 | "* 这个状态下不能修改订单了,但仍然可以取消订单;\n",
148 | "* 商家发货后进入“配送中”状态;\n",
149 | "* 当配送到货,买家签收成功输入则进入下一个状态“已签收”;如果配送失败(买家不在家之类的情况)则留在“配送中”状态(另外择时重新送货);\n",
150 | "* 这个状态下已不能修改和取消订单,但是可以发起退货申请,进入退货子流程(这里略去细节);\n",
151 | "* 买家签收后进入“已签收”状态;\n",
152 | "* 买家满意,确认订单完成则进入最后状态“已完成”,订单生命周期结束;\n",
153 | "* 否则买家可以发起退货进入退货子流程(略)。\n",
154 | "\n",
155 | "从这里我们可以看到,实际业务系统中状态和转换的规则相当复杂(我们这还是大大简化的版本),每个状态下允许的操作和可能转换的下一个状态都是严格受控的,现在我们思考一下,我们可以如何用程序来实现这样的流程呢?"
156 | ]
157 | },
158 | {
159 | "cell_type": "markdown",
160 | "metadata": {},
161 | "source": [
162 | "## 利用有限状态机编写易于维护的代码"
163 | ]
164 | },
165 | {
166 | "cell_type": "markdown",
167 | "metadata": {},
168 | "source": [
169 | "回忆我们之前提到的,流程和行为控制(*flow and behavior control*)的关键是管理:\n",
170 | "* 在某个状态下什么能做什么不能做;\n",
171 | "* 做了什么会变成另外的什么状态。\n",
172 | "\n",
173 | "最简单直接的办法就是书写一堆 `if...else` 的判断规则,大致会是这个样子:"
174 | ]
175 | },
176 | {
177 | "cell_type": "code",
178 | "execution_count": 1,
179 | "metadata": {},
180 | "outputs": [],
181 | "source": [
182 | "class Order:\n",
183 | " STATES = {'created', 'paid', 'delivering', 'received', 'done', 'cancelling', 'returning', 'closed'}\n",
184 | " state = 'created'\n",
185 | " \n",
186 | " def can_pay(self):\n",
187 | " return state == 'created'\n",
188 | " \n",
189 | " def can_deliver(self):\n",
190 | " return state == 'paid'\n",
191 | " \n",
192 | " def can_cancel(self):\n",
193 | " return state == 'created' or state == 'paid'\n",
194 | " \n",
195 | " def can_receive(self):\n",
196 | " return state == 'delivering'\n",
197 | " \n",
198 | " # 还有一大堆类似这样的规则,不写了\n",
199 | " \n",
200 | " def payment_service(self):\n",
201 | " # 调用远程接口完成实际支付\n",
202 | " pass\n",
203 | " \n",
204 | " # 然后是关键操作的实现,比如支付\n",
205 | " def pay(self):\n",
206 | " if self.can_pay(self):\n",
207 | " result = payment_service(self)\n",
208 | " if result.succeeded:\n",
209 | " state = 'paid'\n",
210 | " return True\n",
211 | " else:\n",
212 | " return False\n",
213 | " else:\n",
214 | " raise ValueError()\n",
215 | " \n",
216 | " def cancel(self):\n",
217 | " if self.can_cancel(self):\n",
218 | " self.state = 'cancelling'\n",
219 | " # 取消订单,申请审批和清理数据,如果顺利成功再——\n",
220 | " self.state = 'closed'\n",
221 | " else:\n",
222 | " raise ValueError()\n",
223 | " \n",
224 | " # 还有一大堆操作的函数,每一个里面都要判断状态是不是可以做想做操作\n",
225 | " # 然后根据执行的情况修改 self.state 为对应的新状态"
226 | ]
227 | },
228 | {
229 | "cell_type": "markdown",
230 | "metadata": {},
231 | "source": [
232 | "这样的代码非常冗长和重复,难以维护且难以修改,设想一下,假设在上面的基础上再增加一个状态,要连带修改不确定几处地方,做完这样的修改还需要相应修改所有的测试用例,累就不说了,关键是容易出错。\n",
233 | "\n",
234 | "有限状态机实际上是这些“八股”的通用实现,然后提供一个非常简洁的接口供我们使用。有兴趣的话可以自己尝试用 Python 写一个 *FSM* 的实现出来,只做最基本功能的话也不是很难,但我们实际上没必要自己写,Python 有不少 *FSM* 的第三方实现,比如 [transitions](https://github.com/pytransitions/transitions) 这个库,我们下面就用它来演示一下上面的流程如何用 *FSM* 实现。\n",
235 | "\n",
236 | "运行下面的例子之前需要在命令行界面运行 `pip install transitions` 来安装这个库。也可以在 *notebook* 里打开一个新的 *cell* 直接输入 `!pip install transitions`,和在命令行运行的效果是一样的。"
237 | ]
238 | },
239 | {
240 | "cell_type": "code",
241 | "execution_count": 2,
242 | "metadata": {},
243 | "outputs": [
244 | {
245 | "name": "stdout",
246 | "output_type": "stream",
247 | "text": [
248 | "支付服务申请中……\n",
249 | "已通知用户:商品配送中\n"
250 | ]
251 | },
252 | {
253 | "data": {
254 | "text/plain": [
255 | "'done'"
256 | ]
257 | },
258 | "execution_count": 2,
259 | "metadata": {},
260 | "output_type": "execute_result"
261 | }
262 | ],
263 | "source": [
264 | "# 引入 transitions 库里的核心类\n",
265 | "from transitions import Machine\n",
266 | "\n",
267 | "class Order:\n",
268 | " # 定义状态集\n",
269 | " states = ['created', 'paid', 'delivering', 'received', 'done', 'cancelling', 'returning', 'closed']\n",
270 | " \n",
271 | " def __init__(self, order_id):\n",
272 | " self.order_id = order_id\n",
273 | " \n",
274 | " # 创建 FSM\n",
275 | " self.machine = Machine(model=self, states=Order.states, initial='created')\n",
276 | " \n",
277 | " # 定义状态转换函数\n",
278 | " # 基本的语法很好懂,trigger 参数是输入函数名,source 和 dest 分别是当前和转换后的状态\n",
279 | " # before 参数表示进行这个状态转换之前要调用的函数,如果该函数运行时出现异常,状态转换会中止\n",
280 | " self.machine.add_transition(trigger='t_pay', source='created', dest='paid', before='payment_service')\n",
281 | " # after 参数表示当这个状态转换完成后调用的函数,我们用这个函数来通知用户已经发货在途了\n",
282 | " self.machine.add_transition(trigger='t_deliver', source='paid', dest='delivering', after='notify_delivering')\n",
283 | " self.machine.add_transition(trigger='t_receive', source='delivering', dest='received')\n",
284 | " self.machine.add_transition(trigger='t_confirm', source='received', dest='done')\n",
285 | " # 可以一次定义多个状态向同一个状态的装换\n",
286 | " self.machine.add_transition(trigger='t_cancel', source=['created', 'paid'], dest='cancelling')\n",
287 | " self.machine.add_transition(trigger='t_return', source=['delivering', 'received'], dest='returning')\n",
288 | " self.machine.add_transition(trigger='t_close', source=['cancelling', 'returning'], dest='closed')\n",
289 | " \n",
290 | " def payment_service(self):\n",
291 | " print('支付服务申请中……')\n",
292 | " # 调用远程接口完成实际支付,如果失败可抛出异常,对应的状态转换会中止(即,不会转换到 'paid' 状态)\n",
293 | " return\n",
294 | " \n",
295 | " def notify_delivering(self):\n",
296 | " # 通知用户已发货在途\n",
297 | " print('已通知用户:商品配送中')\n",
298 | " return\n",
299 | "\n",
300 | "# 然后就可以测试一下了\n",
301 | "order1 = Order(1)\n",
302 | "order1.state # => 'created'\n",
303 | "# order1.t_receive() # => 如果运行这一句会抛出 MachineError 异常,因为当前状态与此 trigger(输入)不匹配,转换不被允许\n",
304 | "order1.t_pay() # => 会先调用 payment_service(),成功的话返回 True\n",
305 | "order1.state # => 'paid'\n",
306 | "order1.t_deliver() # => 成功后调用 notify_delivering() 通知用户\n",
307 | "order1.t_receive()\n",
308 | "order1.t_confirm()\n",
309 | "order1.state # => 'done'"
310 | ]
311 | },
312 | {
313 | "cell_type": "markdown",
314 | "metadata": {},
315 | "source": [
316 | "除了具体业务执行的代码,上面基本上完整实现了流程控制的部分,值得注意的是,借助 *FSM* 的实现,不仅简洁易懂,而且易于维护,假定我们需要对流程规则进行修改,或者在某些状态转换的前后添加一些操作,我们通常都只需要修改一处代码,而不用到处找哪里还要改。\n",
317 | "\n",
318 | "顺便说一下, [transitions](https://github.com/pytransitions/transitions) 这个库还有不少强大的功能,有兴趣可以自行发掘下。"
319 | ]
320 | },
321 | {
322 | "cell_type": "markdown",
323 | "metadata": {},
324 | "source": [
325 | "## 小结"
326 | ]
327 | },
328 | {
329 | "cell_type": "markdown",
330 | "metadata": {},
331 | "source": [
332 | "本章介绍了重要的数据模型“有限状态机(*FSM*)”,需要理解其背后的现实世界模型、具体应用及其带来的好处。\n",
333 | "* *FSM* 是对程序中一组的状态进行管理的工具;\n",
334 | "* *FSM* 能够精简程序里的逻辑判断,我们只需要陈述规则,*FSM* 自动管理什么可以什么不可以;\n",
335 | "* 尝试体会和理解 *FSM* 背后的抽象思维方式,如何从特定问题中抽象出可以普遍应用的通用工具。"
336 | ]
337 | }
338 | ],
339 | "metadata": {
340 | "kernelspec": {
341 | "display_name": "Python 3",
342 | "language": "python",
343 | "name": "python3"
344 | },
345 | "language_info": {
346 | "codemirror_mode": {
347 | "name": "ipython",
348 | "version": 3
349 | },
350 | "file_extension": ".py",
351 | "mimetype": "text/x-python",
352 | "name": "python",
353 | "nbconvert_exporter": "python",
354 | "pygments_lexer": "ipython3",
355 | "version": "3.7.4"
356 | }
357 | },
358 | "nbformat": 4,
359 | "nbformat_minor": 4
360 | }
361 |
--------------------------------------------------------------------------------
/x1-setup.md:
--------------------------------------------------------------------------------
1 | # 编程环境配置指南
2 |
3 | > “工欲善其事必先利其器。”
4 | >
5 | > ― 《论语·卫灵公》
6 |
7 | 这份 step-by-step 的指南将帮助你配置好一个可以用一辈子的编程环境。如果你使用的是 PC 电脑请看“Windows 篇”;如果使用的是 Mac 电脑请看“macOS 篇”。
8 |
9 | ## 符号说明
10 |
11 | * ⌃ - Control 键,⇧ - Shift 键,⌥ - Alt/Option 键,⌘ - Command 键
12 | * ↩︎ - 回车键,⇥ - Tab 键
13 |
14 | ## Windows 篇
15 |
16 | * [视频指引](https://www.bilibili.com/video/av73160593/)
17 |
18 | > 重要提示:具体操作请以下面的文字说明为准,视频指引仅供参考。
19 |
20 | ### 确认操作系统环境
21 |
22 | 我们推荐使用 Windows 10 操作系统,更老的 Windows 也许可以,但会增加不必要的麻烦。
23 |
24 | **确认系统架构**
25 |
26 | * 点击桌面左下角的搜索按钮,输入 `cmd` 运行命令行界面(Command Prompt);
27 | * 在命令行界面输入 `wmic CPU get DataWidth ↩︎`,返回的是 CPU 的架构,64 或 32 位;
28 | * 在命令行界面输入 `wmic OS get OSArchitecture ↩︎`,返回的是 Windows 操作系统架构,64 或 32 位。
29 |
30 | **确认 PowerShell 版本**
31 |
32 | PowerShell 是 Windows 下的增强命令行环境,也是我们以后要用的主要命令行界面。以下操作继续在上面打开的命令行界面进行:
33 |
34 | * 在命令行界面输入 `powershell ↩︎`,注意到命令行界面的行首提示信息出现了 `PS` 字样;
35 | * 在命令行界面输入 `$PSVersionTable.PSVersion.Major ↩︎`。
36 |
37 | 上面的命令返回为 5 或者以上就没问题,否则需要下载并安装:
38 | 1. [.NET Framework 4.5 or later](https://www.microsoft.com/net/download)
39 | 2. [Windows Management Framework 5.x](https://aka.ms/wmf5download)
40 |
41 | 以上确认完毕可以在命令行界面输入 `exit ↩︎` 退出。
42 |
43 | ### 安装命令行界面 ConEmu
44 |
45 | 我们刚才已经用了 Windows 自带的命令行界面 `cmd.exe`(大名叫 Command Prompt),为啥还要另外装一个呢?因为这个更方便好用,可以减少不少以后的麻烦。
46 |
47 | * 进入 [ConEmu 首页](https://conemu.github.io/),点击 Download 按钮,选择下载页面中最新的 “Installer (32-bit, 64-bit)” 安装器版本(一般是 Alpha 版本,有时候没有新的 Alpha 版本那就是 Preview 版本);
48 | * 运行下载好的 ConEmu 安装程序(通常叫 `ConEmuSetup.xxxxx.exe`),如果前面检查的 Windows 版本为 64 位就选择安装 x64(64位)版本,否则选择 x86(32位)版本;安装时有的防病毒软件可能会报出病毒警告,请放心继续安装,这是[误报](https://conemu.github.io/en/FalseAlarms.html)。
49 |
50 | 安装完毕运行 ConEmu,应该可以看到下面这样的界面:
51 |
52 | 
53 |
54 | * 点击 ConEmu 右上角最右边的按钮,从弹出菜单中选择 Settings,在打开的设置窗口将下图所示的选项改为 {Shells::PowerShell},点击 Save settings 来保存修改:
55 |
56 | 
57 |
58 | * 现在退出 ConEmu 然后重新运行它,这次应该进入一个 PowerShell 环境,注意每行开始的提示符变成了 PS:
59 |
60 | 
61 |
62 | 以后当我们说“打开命令行界面运行 xxxx 命令”的时候,就是指在上图这个 PowerShell 界面下输入 `xxxx ↩︎`。
63 |
64 | 有时候我们需要以管理员的权限执行一些命令行命令,那么就需要启动 ConEmu 然后打开一个管理员权限的 PowerShell 界面,方法是选右上角的绿色加号然后按下图选择:
65 |
66 | 
67 |
68 | 现在我们先不要关闭 ConEmu,继续下一步。
69 |
70 | ### 安装软件包管理工具 Scoop
71 |
72 | 在我们的编程生涯中会用到很多命令行软件,安装、卸载、更新和管理这些软件最简单的办法就是使用一个好用的软件包管理工具,Windows 下最好的命令行软件包管理工具就是 [Scoop](https://scoop.sh/)。
73 |
74 | 下面的操作就在上一步打开的 ConEmu 的 PowerShell 命令行界面下运行。
75 |
76 | 首先,安装 Scoop 需要一定的权限,通过下面两行命令来查看目前我们的权限,以及获取我们需要的权限:
77 |
78 | * 输入 `Get-ExecutionPolicy ↩︎` 如果系统返回 `RemoteSigned` 则可以略过下面一步直接继续,否则——
79 | * 输入 `Set-ExecutionPolicy RemoteSigned -scope CurrentUser ↩︎`
80 |
81 | 如上,将 *execution policy* 设置为 `RemoteSigned` 之后,就可以运行 Scoop 的安装命令了:
82 |
83 | * 输入 `iwr -useb get.scoop.sh | iex ↩︎`
84 |
85 | 这个命令会下载 Scoop 的安装脚本并执行,过程中会从几个不同的服务器下载安装一些软件,并对你的系统进行一系列配置。等待上述安装脚本执行完毕,如果中间有报错可以把错误提示截屏或者拷贝保存下。
86 |
87 | 如果运行无误,Scoop 就安装好了,我们接着运行下面这几个命令:
88 | ```powershell
89 | scoop list
90 | scoop install git
91 | scoop update
92 | ```
93 |
94 | 第一个命令会列出已安装的软件包。
95 |
96 | 第二个命令会安装非常重要的 *git* 软件包,这是用于文件版本管理和协同的重要工具,大名鼎鼎的“程序员交友社区” [GitHub.com](https://github.com/) 就是建立在 *git* 基础之上的。
97 |
98 | 第三个命令会更新 Scoop 的本地数据库。
99 |
100 | > 把 GitHub.com 叫“交友社区”是个梗,其实 GitHub 是用于分享和协同开发的在线服务。
101 |
102 | 如果运行这三个命令出现问题(通常红字代表有错误发生),可以参考后面的 **Scoop 相关排错** 一节。
103 |
104 | ### 安装 Python
105 |
106 | 如果一切无误,我们就可以着手安装 Python 了,运行:
107 |
108 | ```powershell
109 | scoop bucket add versions
110 | scoop update
111 | scoop install python37@3.7.4
112 | ```
113 |
114 | > 目前 Python 3.8.0 已经正式发布,Scoop 上最新的 python 包就是 3.8.x 版本,但因为太新,一些我们会用到的第三方程序没有完全兼容,所以目前仍需使用 3.7.x(这里推荐使用课程本身开发时的 3.7.4 版本)。
115 |
116 | 上述安装命令运行完毕之后可以再次运行 `scoop list`,应会列出已经安装好的几个软件包。我们还可以输入 `python -V` 来查看安装的 Python 的版本(应该是 3.7.4)。
117 |
118 | 另外有个常用的工具包叫 `busybox`,里面包含了大量 Unix、Linux、macOS 系统里常用的命令,让我们在 Windows 下也能使用这些命令,推荐安装:
119 |
120 | ```powershell
121 | scoop install busybox
122 | ```
123 |
124 | ### Scoop 相关疑难
125 |
126 | 这个过程中如果遇到问题,可以运行 `scoop checkup`,这个命令会让 Scoop 进行自检,给出自己发现的问题和建议解决方案,一般按它说的做就没错,比如在某些系统上它会建议你执行两个命令:
127 |
128 | ```powershell
129 | Set-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1
130 | scoop install innounp dark
131 | ```
132 |
133 | 尤其是第一行命令,在一些系统上是很多问题的根源,遇到问题可以试试先运行它。
134 |
135 | 另外可以运行 `scoop list` 来检查你已经安装过的软件包,其中如果有标记 `*failed*` 的,就是安装失败的软件包,可以用下面的命令来重装(`xxxx` 是有问题的软件包的完整名字):
136 |
137 | ```powershell
138 | scoop uninstall xxxx
139 | scoop install xxxx
140 | ```
141 |
142 | ### 安装 Visual Studio Code
143 |
144 | Visual Studio Code 是微软开发并开源的程序源代码编辑器(以下简称 VSCode),VSCode 集成了对各种编程语言和工具的支持,我们写程序代码和文档都可以用它。
145 |
146 | * 访问 [Visual Studio Code](https://code.visualstudio.com/) 主页并点击下载按钮,下载时注意看清楚是和自己的操作系统一致(Windows)的版本;
147 | * 运行下载好的 VSCode 安装程序(通常叫 `VSCodeUserSetup-xxx-1.xx.x.exe`)。
148 |
149 | 安装好后即可从 Windows 开始菜单运行 VSCode,也可以从命令行运行,回到 ConEmu 窗口,在命令行界面操作:
150 |
151 | * 输入 `mkdir Code ↩︎` 在用户主目录下创建一个叫 `Code` 的子目录,以后我们写的代码都可以放在这里;
152 | * 输入 `cd Code ↩︎` 进入 Code 子目录;
153 | * 输入 `code . ↩︎` 这里 `code` 是运行 VSCode 的命令行命令,后面跟的参数是命令 VSCode 打开的文件或者文件夹,这里我们用一个点 `.` 代表“当前目录”,所以此命令会运行 VSCode 并打开 `Code` 这个目录;
154 |
155 | 现在 `Code` 目录下什么都没有,可以试着创建一个新文件:
156 |
157 | * 在 VSCode 中按 ⌃+N 打开一个新文件,输入 `print('Hello world!')`,然后按 ⌃+S 保存,文件名为 `hello.py`;
158 |
159 | 回到 ConEmu 窗口,然后:
160 |
161 | * 输入 `ls ↩︎`,应该可以看到刚才新建的文件 `hello.py`;
162 | * 输入 `python hello.py ↩︎` 应该可以看到运行 `hello.py` 程序的结果。
163 |
164 | 恭喜,你的 Windows 系统已经是 *programming ready* 状态,可以继续[下一步](x2-students-book.md)了。
165 |
166 | ## macOS 篇
167 |
168 | * [视频指引](https://www.bilibili.com/video/av62687425/)
169 |
170 | > 重要提示:具体操作请以下面的文字说明为准,视频指引仅供参考。
171 |
172 | ### 命令行界面
173 |
174 | macOS 本质上是 Unix,所以命令行界面是自带现成的,在 macOS 按 ⌘+空格 键调出系统搜索框(大名叫 Spotlight),输入 `terminal ↩︎`,打开的窗口叫 Terminal,这就是 macOS 的命令行环境。
175 |
176 | ### 安装软件包管理工具 Homebrew
177 |
178 | 在我们的编程生涯中会用到很多命令行软件,安装、卸载、更新和管理这些软件最简单的办法就是使用一个好用的软件包管理工具,macOS 下最好的命令行软件包管理工具就是 [Homebrew](https://brew.sh/)。
179 |
180 | 下面的操作就在上一步打开的 Terminal 命令行界面下运行:
181 |
182 | * 输入:`xcode-select --install ↩︎` 这个命令会安装 Apple 开发工具包,是 Homebrew 需要的
183 | * 输入:`/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" ↩︎`
184 |
185 | 这个命令将下载并执行 Homebrew 的安装脚本,自动安装 Homebrew 到 /usr/local/ 目录下;注意提示 `Password:` 的时候输入你的登录密码并回车。
186 |
187 | * 运行 Homebrew 自更新命令:`brew update ↩︎`
188 | * 运行 Homebrew 自检命令:`brew doctor ↩︎`
189 |
190 | > 如果曾经从 App Store 下载安装过完整的 Xcode,需要你启动 Xcode 并且完成初始化(安装命令行工具、接受用户授权协议等),看到 Xcode 的欢迎窗口就可以了。
191 |
192 | ### 安装 Python
193 |
194 | 如果前面的操作没有错误,Homebrew 就安装好了,我们建议立刻安装两个软件包,运行:
195 |
196 | ```shell
197 | brew install git python ↩︎
198 | ```
199 |
200 | * *git* 是用于文件版本管理和协同的重要工具,大名鼎鼎的“程序员交友社区” GitHub.com 就是建立在 *git* 基础之上的;
201 | * *python* 这个包会安装 Python 完整的运行环境。
202 |
203 | > 把 GitHub.com 叫“交友社区”是个梗,其实 GitHub 是用于分享和协同开发的在线服务。
204 |
205 | > 目前 Python 3.8.0 已经正式发布,但 Homebrew 上最新的 python 包仍是 3.7 版本;因为 3.8 太新,还有不少第三方程序没有完全兼容,我们学习仍需要使用 3.7 版本,所以指定 Homebrew 安装 python 包。以后 Homebrew 的 python 包应该会升级到 3.8.x,那时我们也会随之更新这个指引。
206 |
207 | 上述安装命令运行完毕之后可以运行 `brew list`,应会列出已经安装好的这两个软件包以及所有自动安装的依赖包。我们还可以输入 `python3 -V` 来查看新安装的 Python 的版本。
208 |
209 | > 在某些 macOS 系统中会内置一个老的 Python 2.7 的环境,而 `python` 命令会指向这个老的 Python 环境,这会给我们以后的操作带来不少麻烦,可以执行下面的命令来强制让 `python` 指向我们安装的 Python 3 的环境:
210 |
211 | ```shell
212 | rm /usr/local/bin/python
213 | ln -s /usr/local/bin/python3 /usr/local/bin/python
214 | ```
215 |
216 | ### 安装 Visual Studio Code
217 |
218 | Visual Studio Code 是微软开发并开源的给程序员用的文本编辑器(以下简称 VSCode),VSCode 集成了对各种编程语言和工具的支持,我们写程序代码和文档都可以用它。
219 |
220 | * 访问 [Visual Studio Code](https://code.visualstudio.com/) 主页并点击下载按钮,下载时注意看清楚是和自己的操作系统一致(macOS)的版本;
221 | * 解压下载文件(通常叫 `VSCode-darwin-stable.zip`)会得到 VSCode 应用程序,将其移动(拖到) Applications 目录即可。
222 |
223 | 按 ⌘+空格 键调出 Spotlight,输入 `code` 应该可以看到 Visual Studio Code 出现在第一选项,回车即可运行 VSCode。
224 |
225 | * 在 VSCode 中按 ⌘⇧+P 打开命令窗口,输入 install,从弹出的命令中选择 Shell Command: Install 'code' command in PATH,这样以后我们可以在命令行里用 `code` 命令来运行 VSCode 并打开指定文件或者文件夹:
226 |
227 | 
228 |
229 | 现在回到 Terminal 窗口,在命令行界面操作:
230 |
231 | * 输入 `mkdir Code ↩︎` 在用户主目录下创建一个叫 `Code` 的子目录,以后我们写的代码都可以放在这里;
232 | * 输入 `cd Code ↩︎` 进入 Code 子目录;
233 | * 输入 `code . ↩︎` 这里 `code` 是运行 VSCode 的命令行命令,后面跟的参数是命令 VSCode 打开的文件或者文件夹,这里我们用一个点 `.` 代表“当前目录”,所以此命令会运行 VSCode 并打开 `Code` 这个目录;
234 |
235 | 现在 `Code` 目录下什么都没有,可以试着创建一个新文件:
236 |
237 | * 在 VSCode 中按 ⌘+N 打开一个新文件,输入 `print('Hello world!')`,然后按 ⌘+S 保存,文件名为 `hello.py`;
238 |
239 | 回到 Terminal 窗口,然后:
240 |
241 | * 输入 `ls ↩︎`,应该可以看到刚才新建的文件 `hello.py`;
242 | * 输入 `python3 hello.py` 应该可以看到运行 `hello.py` 程序的结果。
243 |
244 | 恭喜,你的 macOS 系统已经是 *programming ready* 状态,可以继续[下一步](x2-students-book.md)了。
245 |
--------------------------------------------------------------------------------
/x2-students-book.md:
--------------------------------------------------------------------------------
1 | # 如何使用配套学习用书
2 |
3 | > “A dream written down with a date becomes a goal. A goal broken down into steps becomes a plan.”
4 | >
5 | > ― Greg Reid
6 |
7 | 使用“学习用书”不仅可以帮助边学边练,还是你的学习跟踪记录,你的 *proof of work*。要使用学习用书,按照下面的步骤顺序操作:
8 |
9 | ### 登录 GitHub
10 |
11 | 无论如何你都必须有 GitHub 账户,GitHub 是个神奇的地方,不仅可以帮助程序员,还可以帮助很多非程序员实现奇妙的社会化大合作。
12 |
13 | 当然,这很简单,访问 [github.com](https://github.com),注册一个 GitHub 帐号(如果还没有的话)并登录。
14 |
15 | ### 获取你的学习用书
16 |
17 | 第一步,访问本书配套学习用书的共享代码仓库(*repo*),
18 |
19 | > [https://github.com/neolee/pilot-student](https://github.com/neolee/pilot-student)
20 |
21 | 点击右上角的 Fork 按钮,这是 git 的重要操作,它会从我们的 *repo* 分叉出一个一模一样的 *repo* 并加入到你的账号中,这个新的 *repo* 继承了原 *repo* 的历史和现状,但它的未来由你来决定。
22 |
23 | 在这个新的 *repo* 的首页上有个绿色的 *Clone or download* 按钮,点击它会打开一个小的下拉显示,里面有个文本框写有这个 *repo* 的访问地址,点击它右边的小按钮将其拷贝到系统剪贴板(后面会用)。
24 |
25 | 第二步,现在要把属于你的这个分叉 *repo* 克隆到你自己的机器本地来,由于你顺利完成了[环境准备](x1-setup.md),你的机器上已经有完善的命令行界面和软件包管理工具,还装好了 *git*,现在可以打开命令行界面进行如下操作:
26 | * 输入 `cd Code ↩︎` 进入我们之前创建的子目录(如果还没有建立,可以用 `mkdir Code ↩︎` 来创建);
27 | * 输入 `git clone `,在最后有个空格,在空格后粘贴你前面拷贝的,你 *fork* 的 *repo* 的地址,然后输入回车 `↩︎`;
28 | * 输入 `cd pilot-student ↩︎` 进入克隆好的目录中。
29 |
30 | 上面的命令会在 `<你的用户根目录>/Code` 目录下创建一个子目录,然后把学习用书从 GitHub 服务器上克隆到这个目录下。
31 |
32 | ### 安装和运行 Jupyter Lab
33 |
34 | 因为学习用书都是用 Jupyter Notebook 写就的,要使用就需要安装 Jupyter Lab 环境,在我们已经装好 Python 的前提下这很容易,只要在命令行界面运行下面的命令就可以了:
35 |
36 | ```shell
37 | python -m pip install --upgrade pip
38 | pip install jupyterlab
39 | ```
40 |
41 | `pip` 是 Python 自己的软件包管理工具,它负责安装、删除和管理 Python 浩若烟海的第三方代码库,我们以后会经常用到。上面第一行是更新 `pip` 自己,因为我们刚装好 Python,通常需要更新一下 `pip` 自己;第二行则是让 `pip` 安装 Jupyter Lab 以及所有依赖支持包。
42 |
43 | > 如果在运行上面第二个命令时报错说找不到、不认识 `pip` 命令,可尝试将 `pip` 换成 `pip3`,即运行 `pip3 install jupyterlab`。
44 |
45 | 某些环境下运行 Jupyter Lab 需要 nodejs,所以建议也安装好。如果是 Winidows 系统,执行:
46 |
47 | ```powershell
48 | scoop install nodejs
49 | ```
50 |
51 | macOS 系统则执行:
52 |
53 | ```shell
54 | brew install node
55 | ```
56 |
57 | 上述操作都成功后 Jupyter Lab 就准备就绪了,在你克隆好的学习用书目录里运行 `jupyter lab ↩︎` 来启动 Jupyter Lab 的服务程序,并打开一个浏览器页面,里面列出了学习用书里的所有 *notebook*(.ipynb 后缀名的文件),双击就能打开了。
58 |
59 | > 注意,运行 `jupyter lab` 的命令行窗口必须保持着,你才能继续在浏览器里使用 Jupyter Lab;如果你用完了,需要退出,在这个命令行窗口按 Control+C 组合键,就会停止 Jupyter Lab 服务,回到命令行交互界面。一般来说不要在 `jupyter lab` 运行时关闭那个窗口。
60 | >
61 | > 如果在 `jupyter lab` 运行着的时候你需要命令行界面执行一些任务,只要在 ConEmu 里打开一个新的 tab 就可以了,不用动之前的那个。
62 |
63 | > **关于浏览器的说明**:Jupyter Lab 不支持 Internet Explorer 11 以及之前的所有版本(简称 IE),这些浏览器是 Windows 的古老遗产,也是所有前端开发者的噩梦。如果你的 Windows 以 IE 作为缺省浏览器,那么执行 `jupyter lab ↩︎` 打开的页面会是一片空白。解决方案就是换用其他更新一些的浏览器,比如 Windows 自带的 Edge,或者程序员首选的 Chrome 和 Firefox 的最新版本,具体操作可以从下面两方法中任选其一:
64 | > 1. 在 Windows 的系统设置中找到缺省应用设置(Default apps),修改 Web 浏览器的设置;
65 | > 2. 手动打开 Chrome 一类的浏览器,然后把命令行界面里 `http://localhost:8888/?token=...` 那一整行地址拷贝进去打开。
66 |
67 | ### 使用学习用书
68 |
69 | 现在你可以在 Jupyter Lab 中打开 *notebook*,在代码 *cell* 中输入 Python 代码并运行,每学完一章,对应的学习用书中的 *notebook* 也应该经你亲手补完,你当然还可以在 *notebook* 中加入自己的想法,测试自己想到的程序代码等等,总之只要你想做的都可以做,做完之后保存 *notebook*,你的成果就被记录下来了,然后你可以:
70 |
71 | * 使用 `git commit` 来把你修改的内容提交到本地仓库(*local repo*);
72 | * 使用 `git push` 来把你本地仓库中新的变化同步到 GitHub 上的远程仓库(*remote repo*)——这就是你的学习“工作证明(PoW)”。
73 |
74 | 在学习用书的代码仓库中还有一个特殊的目录 `vor`,意思是 *Voice of Readers*,用来收集像你一样的学习者的反馈,比如你可以写一篇《我的自学之路》,放在这个目录里,然后 *commit* 和 *push* 到你自己的学习用书 *repo* 中,甚至还可以用一种特定流程(叫做 *pull request*)提交回我们的原 *repo* 中,如果被我们接受,就会成为原 *repo* 的内容,被所有读者 *fork* 和阅读。
75 |
76 | 当然了,上面说的这些都需要你多学一样反正你早晚都必须学会的东西——*git*,我们在附录中有一篇 [Git 与 GitHub 入门教程](x3-git-github.ipynb) 可以参考。
77 |
78 | 学习需要投入时间,而时间我们没办法糊弄它。
79 |
80 | 有了 *git* 这样的工具之后,我们在什么时候做了什么样的工作,是很容易证明的,而当我们不小心犯了错误还能回溯到历史版本,相当于我们从某种程度上获得了一些对时间的控制力——这对我们来说真是天大的好事。
81 |
82 | 未来,我们会专门设置一个 *repo*,通过自动扫描学习用书所有 *forks* 来跟进本书的学习记录——这种记录在过往的书籍中是不可能存在的,然而现在却可以了。将来这种记录的作用甚至有可能比“学历”还要重要。
83 |
84 | 关于学习环境的准备,还可参考这个 [视频指引](https://www.bilibili.com/video/av71399509/)。
85 |
86 | ### 问题与反馈
87 |
88 | 如果在学习过程中遇到问题或者发现教材中的错误,可以通过 GitHub 的 Issues 系统提出,这个系统基本上就像一个问答论坛,但它集成了编程相关的能力,让它目的性更强、更容易跟踪问题解决的进度状态。
89 |
90 | 访问我们课程教材的 Issues 页面:
91 |
92 | > [https://github.com/neolee/pilot/issues](https://github.com/neolee/pilot/issues)
93 |
94 | 点击右上的 `New` 按钮来提出问题或者反馈。
95 |
96 | 遇到问题的时候其实也可以到这个页面去搜索一下,看看是不是有人提过,得到了怎样的答案;如果没人提过,那就正好可以由你来提出,所有人也都会从中获益。
97 |
98 | 有些常见或者特别有价值的问题我们也会整理出来放到课程项目的 [Wiki](https://github.com/neolee/pilot/wiki) 中,方便大家查阅。
99 |
--------------------------------------------------------------------------------
/x5-mysql-setup.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# MySQL 环境准备"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "## 安装"
15 | ]
16 | },
17 | {
18 | "cell_type": "markdown",
19 | "metadata": {},
20 | "source": [
21 | "最新的 MySQL 服务程序可以在[官网下载](https://dev.mysql.com/downloads/installer/),官网也提供了针对不同操作系统的[安装指引](https://dev.mysql.com/doc/refman/8.0/en/installing.html)。\n",
22 | "\n",
23 | "如果已经按照我们提供的[指南](x1-setup.md)配置好自己机器的编程环境的话,就可以用 [Homebrew](https://brew.sh/)(macOS 下)或 [Scoop](https://scoop.sh/)(Windows 下)来安装。比较推荐这个办法。"
24 | ]
25 | },
26 | {
27 | "cell_type": "markdown",
28 | "metadata": {},
29 | "source": [
30 | "### Windows"
31 | ]
32 | },
33 | {
34 | "cell_type": "markdown",
35 | "metadata": {},
36 | "source": [
37 | "1. 在 ConEMU 中打开一个 PowerShell (Admin) 会话窗口(即管理员权限的命令行界面,如果有安全提示选择允许运行),然后执行下面的命令:\n",
38 | "\n",
39 | "```powershell\n",
40 | "scoop bucket add versions\n",
41 | "scoop install mysql57\n",
42 | "```\n",
43 | "\n",
44 | "2. 打开 `C:\\Users\\trust\\apps\\mysql57\\current\\my.ini` 文件(如果不存在就创建一个),文件内容应该是这样的:\n",
45 | "\n",
46 | "```ini\n",
47 | "[client]\n",
48 | "# Which port the client should use as a default.\n",
49 | "# this means if you're connecting to the local machine, then you can omit \n",
50 | "# the --port parameter\n",
51 | "port=3306\n",
52 | "\n",
53 | "[mysqld]\n",
54 | "# Which port you're using. You'll need to write \n",
55 | "# mysql --port=3360 if you're specifying the port\n",
56 | "port=3306\n",
57 | "# These two have to do with size of data allowed in the system\n",
58 | "key_buffer_size=16M\n",
59 | "max_allowed_packet=8M\n",
60 | "\n",
61 | "[mysqldump]\n",
62 | "# The style of mysqldump to use as default\n",
63 | "quick\n",
64 | "```\n",
65 | "\n",
66 | "3. 回到前面的 PowerShell (Admin) 窗口,运行:\n",
67 | "\n",
68 | "```powershell\n",
69 | "mysqld --install\n",
70 | "```\n",
71 | "\n",
72 | "4. 在 Windows 搜索栏输入 Services 找到并运行 Services 管理程序,如果上一步没有问题,打开的列表中应该有 MySQL 一项,右键选择启动(Start)即可。\n",
73 | "\n",
74 | "> 在较老的 Windows 上没有 Services 管理程序,可以打开 Control Panel(控制面板),找到 Administration Tools(管理工具)里面的 Services(服务)。"
75 | ]
76 | },
77 | {
78 | "cell_type": "markdown",
79 | "metadata": {},
80 | "source": [
81 | "### macOS"
82 | ]
83 | },
84 | {
85 | "cell_type": "markdown",
86 | "metadata": {},
87 | "source": [
88 | "在命令行界面运行下面的命令即可:\n",
89 | "\n",
90 | "```shell\n",
91 | "brew install mysql@5.7\n",
92 | "brew services start mysql@5.7\n",
93 | "```"
94 | ]
95 | },
96 | {
97 | "cell_type": "markdown",
98 | "metadata": {},
99 | "source": [
100 | "## 初始配置"
101 | ]
102 | },
103 | {
104 | "cell_type": "markdown",
105 | "metadata": {},
106 | "source": [
107 | "当 MySQL Server 运行起来之后,就可以通过命令行工具 `mysql` 来操作,下面的操作无论在 Windows 还是 macOS 下都是一样的。\n",
108 | "\n",
109 | "在命令行下输入 `mysql -uroot` 进入 MySQL 的 REPL:\n",
110 | "\n",
111 | "```shell\n",
112 | "Welcome to the MySQL monitor. Commands end with ; or \\g.\n",
113 | "Your MySQL connection id is 32\n",
114 | "Server version: 5.7.26 Homebrew\n",
115 | "\n",
116 | "Copyright (c) 2000, 2019, Oracle ...\n",
117 | "\n",
118 | "Type 'help;' or '\\h' for help. Type '\\c' to clear the current input statement.\n",
119 | "\n",
120 | "mysql>\n",
121 | "```\n",
122 | "\n",
123 | "在 `mysql>` 提示符之后可以输入任何 MySQL 支持的 SQL 语句(用半角分号 `;` 结尾)。"
124 | ]
125 | },
126 | {
127 | "cell_type": "markdown",
128 | "metadata": {},
129 | "source": [
130 | "### 修改 root 密码"
131 | ]
132 | },
133 | {
134 | "cell_type": "markdown",
135 | "metadata": {},
136 | "source": [
137 | "注意我们目前是以 `root` 用户的身份登录到 MySQL 服务(命令行里的 `-u` 就是指定登录用户的参数),这是最大权限的用户,可以做任何事情,而且初始没有密码。这是个很不安全的状态,我们首先要改变这个状态。\n",
138 | "\n",
139 | "在 `mysql>` 提示符之后输入(注意将 `mypass` 改为你选择的 `root` 用户密码):\n",
140 | "\n",
141 | "```sql\n",
142 | "SET PASSWORD FOR root@localhost = PASSWORD('mypass');\n",
143 | "```\n",
144 | "\n",
145 | "回车运行,`root` 用户的密码已经改成上面设置的,然后可以输入 `EXIT` 退出 MySQL 的 REPL,再次运行 `mysql -uroot` 将无法进入,会有一个 `Access denied` 的错误。\n",
146 | "\n",
147 | "在命令行运行(`-p` 参数表示用户有密码,希望在提示后输入):\n",
148 | "\n",
149 | "```shell\n",
150 | "> mysql -uroot -p\n",
151 | "Enter password: *******\n",
152 | "```\n",
153 | "\n",
154 | "输入密码后成功进入 REPL。"
155 | ]
156 | },
157 | {
158 | "cell_type": "markdown",
159 | "metadata": {},
160 | "source": [
161 | "### 创建数据库和对应用户"
162 | ]
163 | },
164 | {
165 | "cell_type": "markdown",
166 | "metadata": {},
167 | "source": [
168 | "以下操作都以 `root` 用户身份在 `mysql` 的提示符后输入执行。\n",
169 | "\n",
170 | "```sql\n",
171 | "mysql> CREATE DATABASE demo;\n",
172 | "Query OK, 1 row affected (0.01 sec)\n",
173 | "\n",
174 | "mysql> SHOW DATABASES;\n",
175 | "+--------------------+\n",
176 | "| Database |\n",
177 | "+--------------------+\n",
178 | "| information_schema |\n",
179 | "| demo |\n",
180 | "| mysql |\n",
181 | "| performance_schema |\n",
182 | "| sys |\n",
183 | "+--------------------+\n",
184 | "5 rows in set (0.02 sec)\n",
185 | "\n",
186 | "mysql> USE demo\n",
187 | "Database changed\n",
188 | "mysql> SHOW TABLES;\n",
189 | "Empty set (0.01 sec)\n",
190 | "```\n",
191 | "\n",
192 | "如上所示,创建了一个新的数据库 `demo`,其中还没有任何表。下面来创建一个用户,并赋予该用户对 `demo` 数据库的完整操作权限(但不能访问 MySQL 服务中的其他数据)。\n",
193 | "\n",
194 | "```sql\n",
195 | "mysql> CREATE USER learn@localhost IDENTIFIED BY 'demo';\n",
196 | "Query OK, 0 rows affected (0.02 sec)\n",
197 | "\n",
198 | "mysql> GRANT ALL ON demo.* TO learn@localhost;\n",
199 | "Query OK, 0 rows affected (0.00 sec)\n",
200 | "\n",
201 | "mysql> FLUSH PRIVILEGES;\n",
202 | "Query OK, 0 rows affected (0.01 sec)\n",
203 | "```\n",
204 | "\n",
205 | "上面第一条命令创建了一个叫 `learn` 的用户,限制其只能从本地(`localhost`)登录,并设置其密码为 `demo`;第二条命令则赋予该用户对 `demo` 数据库的完整权限(用户缺省是什么权限都没有的);最后一条命令是要求服务器刷新权限库,令前面的设置生效。\n",
206 | "\n",
207 | "现在可以退出 REPL,然后尝试用 `mysql -ulearn -p` 来以 `learn` 身份登录,输入密码后可以进入 REPL 界面,然后可以试试看我们能做什么。\n",
208 | "\n",
209 | "```sql\n",
210 | "mysql> SHOW DATABASES;\n",
211 | "+--------------------+\n",
212 | "| Database |\n",
213 | "+--------------------+\n",
214 | "| information_schema |\n",
215 | "| demo |\n",
216 | "+--------------------+\n",
217 | "2 rows in set (0.00 sec)\n",
218 | "\n",
219 | "mysql> use demo\n",
220 | "Database changed\n",
221 | "mysql> SHOW TABLES;\n",
222 | "Empty set (0.00 sec)\n",
223 | " \n",
224 | "mysql> CREATE TABLE users (id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, username VARCHAR(255), firstname VARCHAR(255), lastname VARCHAR(255)); \n",
225 | "Query OK, 0 rows affected (0.04 sec) \n",
226 | " \n",
227 | "mysql> SHOW TABLES; \n",
228 | "+----------------+ \n",
229 | "| Tables_in_demo | \n",
230 | "+----------------+ \n",
231 | "| users | \n",
232 | "+----------------+ \n",
233 | "1 row in set (0.00 sec) \n",
234 | " \n",
235 | "mysql> DESC users; \n",
236 | "+-----------+--------------+------+-----+---------+----------------+ \n",
237 | "| Field | Type | Null | Key | Default | Extra | \n",
238 | "+-----------+--------------+------+-----+---------+----------------+ \n",
239 | "| id | int(11) | NO | PRI | NULL | auto_increment | \n",
240 | "| username | varchar(255) | YES | | NULL | | \n",
241 | "| firstname | varchar(255) | YES | | NULL | | \n",
242 | "| lastname | varchar(255) | YES | | NULL | | \n",
243 | "+-----------+--------------+------+-----+---------+----------------+ \n",
244 | "4 rows in set (0.01 sec)\n",
245 | "\n",
246 | "mysql> DROP TABLE users;\n",
247 | "Query OK, 0 rows affected (0.01 sec) \n",
248 | "```\n",
249 | "\n",
250 | "可以看到,`learn` 只能访问自己有权限的数据库,并且在 `demo` 数据库内拥有创建和删除表的权限。\n",
251 | "\n",
252 | "至此 MySQL 环境配置完毕。"
253 | ]
254 | },
255 | {
256 | "cell_type": "markdown",
257 | "metadata": {},
258 | "source": [
259 | "## 准备学习用数据库"
260 | ]
261 | },
262 | {
263 | "cell_type": "markdown",
264 | "metadata": {},
265 | "source": [
266 | "最后我们来准备下在学习中我们会用到的数据库。这部分是可选的,如果没有完成,完全可以用自己创建的简单数据来测试,不过完成下面的操作也是对环境熟悉的过程,你完全可以试试,很多编程的经验都是折腾这些环境和数据得到的。"
267 | ]
268 | },
269 | {
270 | "cell_type": "markdown",
271 | "metadata": {},
272 | "source": [
273 | "### 准备数据文件"
274 | ]
275 | },
276 | {
277 | "cell_type": "markdown",
278 | "metadata": {},
279 | "source": [
280 | "首先从 [Kagger](https://www.kaggle.com/karangadiya/fifa19) 下载数据,下载回来是一个名为 `fifa19.zip` 的压缩包,解压之后是一个名为 `data.csv` 的 *CSV* 格式数据文件,将其改名为 `fifa19.csv`。\n",
281 | "\n",
282 | "因为数据文件 `fifa19.csv` 的编码格式和我们下面用的工具不兼容,所以需要做一点处理,用 Visual Studio Code 打开这个文件,点击右下角的编码栏 `UTF-8 with BOM`:"
283 | ]
284 | },
285 | {
286 | "cell_type": "markdown",
287 | "metadata": {},
288 | "source": [
289 | "
"
290 | ]
291 | },
292 | {
293 | "cell_type": "markdown",
294 | "metadata": {},
295 | "source": [
296 | "点击上图右下角红框标出的栏,窗口上方会弹出菜单,依次选择 *Save with Encoding* 和 *UTF-8*,然后可以关闭 Visual Studio Code。"
297 | ]
298 | },
299 | {
300 | "cell_type": "markdown",
301 | "metadata": {},
302 | "source": [
303 | "### 数据导入"
304 | ]
305 | },
306 | {
307 | "cell_type": "markdown",
308 | "metadata": {},
309 | "source": [
310 | "我们使用官方的 GUI 工具来完成这个导入。\n",
311 | "\n",
312 | "首先从 MySQL 官方网站下载 [MySQL Workbench](https://dev.mysql.com/downloads/workbench/) 并运行下载好的安装包(名字一般是 `mysql-workbench-community-x.x.xx-winx64.msi`)。\n",
313 | "\n",
314 | "打开安装好的 MySQL Workbench,如果前面 MySQL 服务安装和运行无误,这里 Workbench 会检测到本地运行的服务,并显示在启动界面,点击之就可以连接开始管理本地的数据库:"
315 | ]
316 | },
317 | {
318 | "cell_type": "markdown",
319 | "metadata": {},
320 | "source": [
321 | "
"
322 | ]
323 | },
324 | {
325 | "cell_type": "markdown",
326 | "metadata": {},
327 | "source": [
328 | "如果前面的初始配置无误,在打开的界面左侧选择 *Schemas* 就可以看到 `demo` 数据库,右键选择 `demo` 数据库然后选择 *Table Data Import Wizard* 打开数据导入向导:"
329 | ]
330 | },
331 | {
332 | "cell_type": "markdown",
333 | "metadata": {},
334 | "source": [
335 | "
"
336 | ]
337 | },
338 | {
339 | "cell_type": "markdown",
340 | "metadata": {},
341 | "source": [
342 | "按照向导一步步操作:\n",
343 | "1. 浏览并选择刚才我们解压并修改过的 `fifa19.csv`,点击 *Next >*;\n",
344 | "2. 选择 *Create new table*,下拉框里选择数据库 `demo`,表名输入 `players`,点击 *Next >*;\n",
345 | "3. 在 *Columns* 下面去掉第一行 `MyUnknownColumn` 那一行的选择框,点击 *Next >*;\n",
346 | "4. 点击 *Next >* 开始导入数据,这要一会儿。\n",
347 | "\n",
348 | "完成后在主界面 `demo` 数据库下面可以看到新建的 `players` 表,如果没有可以右键点击 `demo` 选择 *Refresh All*。\n",
349 | "\n",
350 | "右键点击 `players` 表选择 *Select Rows - Limit 1000* 可以查询显示表的前 1000 行,从而确认导入成功。"
351 | ]
352 | }
353 | ],
354 | "metadata": {
355 | "kernelspec": {
356 | "display_name": "Python 3",
357 | "language": "python",
358 | "name": "python3"
359 | },
360 | "language_info": {
361 | "codemirror_mode": {
362 | "name": "ipython",
363 | "version": 3
364 | },
365 | "file_extension": ".py",
366 | "mimetype": "text/x-python",
367 | "name": "python",
368 | "nbconvert_exporter": "python",
369 | "pygments_lexer": "ipython3",
370 | "version": "3.7.4"
371 | }
372 | },
373 | "nbformat": 4,
374 | "nbformat_minor": 4
375 | }
376 |
--------------------------------------------------------------------------------
/x6-redis-setup.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Redis 环境准备"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "## macOS"
15 | ]
16 | },
17 | {
18 | "cell_type": "markdown",
19 | "metadata": {},
20 | "source": [
21 | "在命令行界面运行下列命令即可:\n",
22 | "\n",
23 | "```shell\n",
24 | "brew install redis\n",
25 | "brew services start redis\n",
26 | "```"
27 | ]
28 | },
29 | {
30 | "cell_type": "markdown",
31 | "metadata": {},
32 | "source": [
33 | "## Windows"
34 | ]
35 | },
36 | {
37 | "cell_type": "markdown",
38 | "metadata": {},
39 | "source": [
40 | "### 安装"
41 | ]
42 | },
43 | {
44 | "cell_type": "markdown",
45 | "metadata": {},
46 | "source": [
47 | "在命令行界面运行下列命令:\n",
48 | "\n",
49 | "```shell\n",
50 | "scoop install redis\n",
51 | "```"
52 | ]
53 | },
54 | {
55 | "cell_type": "markdown",
56 | "metadata": {},
57 | "source": [
58 | "### 作为 Windows 系统服务运行"
59 | ]
60 | },
61 | {
62 | "cell_type": "markdown",
63 | "metadata": {},
64 | "source": [
65 | "1. 将 Redis 安装目录加入系统 Path 路径\n",
66 | "\n",
67 | "* 在文件浏览器中找到 This PC(这台电脑)右键点击之,选择 Properties(属性设置);\n",
68 | "* 在弹出的窗口中选择 Advanced system settings(高级系统设置);\n",
69 | "* 在弹出的窗口中选择下面的 Environment Variables(环境变量);\n",
70 | "* 在弹出的窗口中上面一个列表框里找到 Path 一项,双击打开编辑;\n",
71 | "* 在编辑窗口选择 New(新建),然后选择 Browse...(浏览),找到 `C:\\Users\\本地用户名\\scoop\\apps\\redis\\current`,确认并关闭编辑窗口。\n",
72 | "\n",
73 | "> 上面最后一步中,如果 Windows 版本较老,可能打开的编辑窗口里没有新建按钮,只有一个直接编辑 Path 变量值的文本输入框,那么在输入框的最后加入`;`,然后将 redis 安装路径 `C:\\Users\\本地用户名\\scoop\\apps\\redis\\current` 粘贴在最后,点击确定直至关闭系统设置窗口。\n",
74 | "\n",
75 | "2. 在 ConEMU 中打开一个 PowerShell (Admin) 会话窗口(即管理员权限的命令行界面,如果有安全提示选择允许运行),然后执行下面的命令:\n",
76 | "\n",
77 | "```powershell\n",
78 | "cd C:\\Users\\trust\\scoop\\apps\\redis\\current\n",
79 | "redis-server.exe --service-install redis.windows-service.conf --loglevel verbose\n",
80 | "```\n",
81 | "\n",
82 | "3. 在 Windows 搜索栏输入 Services 找到并运行 Services 管理程序,如果上一步没有问题,打开的列表中应该有 Redis 一项,右键选择启动(Start)即可。\n",
83 | "\n",
84 | "> 在较老的 Windows 上没有 Services 管理程序,可以打开 Control Panel(控制面板),找到 Administration Tools(管理工具)里面的 Services(服务)。"
85 | ]
86 | },
87 | {
88 | "cell_type": "markdown",
89 | "metadata": {},
90 | "source": [
91 | "## 验证"
92 | ]
93 | },
94 | {
95 | "cell_type": "markdown",
96 | "metadata": {},
97 | "source": [
98 | "在命令行界面运行 `redis-cli` 命令,如果前面 Redis 服务安装运行成功可以看到 Redis 环境的提示符:\n",
99 | "\n",
100 | "```shell\n",
101 | "127.0.0.1:6379>\n",
102 | "```"
103 | ]
104 | }
105 | ],
106 | "metadata": {
107 | "kernelspec": {
108 | "display_name": "Python 3",
109 | "language": "python",
110 | "name": "python3"
111 | },
112 | "language_info": {
113 | "codemirror_mode": {
114 | "name": "ipython",
115 | "version": 3
116 | },
117 | "file_extension": ".py",
118 | "mimetype": "text/x-python",
119 | "name": "python",
120 | "nbconvert_exporter": "python",
121 | "pygments_lexer": "ipython3",
122 | "version": "3.7.4"
123 | }
124 | },
125 | "nbformat": 4,
126 | "nbformat_minor": 4
127 | }
128 |
--------------------------------------------------------------------------------