LookMinds
去登录

《自学是门手艺》(第 1 部分 · 第 1–22 章)

《自学是门手艺》,共 46 章,本页第 1–22 章。想写一本关于自学能力的书,还真的不是一天两天的事,所以肯定不是心血来潮。等我快把初稿框架搭完,跟霍炬说起我正在写的内容时,霍炬说:不过,这次真的写了。写出来的其实并不是,或者说,并不仅仅是 “一本编程书”。

Source: https://github.com/xiaolai/the-craft-of-selfteaching

《自学是门手艺》(第 1 部分 · 第 1–22 章)

> 作者:李笑来 · 本人在 GitHub 公开的书稿 · [GitHub 仓库](https://github.com/xiaolai/the-craft-of-selfteaching) · 本馆按 2026-05-21 的版本收录,共 46 章,本页为第 1–22 章。图片以链接形式给出。

前言

想写一本关于自学能力的书,还真的不是一天两天的事,所以肯定不是心血来潮。

等我快把初稿框架搭完,跟霍炬说起我正在写的内容时,霍炬说:

> 你还记得吗,你第一次背个包来我家的时候,咱们聊的就是咋写本有意思的编程书……

我说:

> 真是呢!十三年就这么过去了……

不过,这次真的写了。写出来的其实并不是,或者说,并不仅仅是 “一本编程书”。

这本 “书” 是近些年我一直在做却没做完整的事情,讲清楚 “学习学习再学习”:

> 学会学习之后再去学习……

只不过,这一次我阐述地更具体 —— 不是 “学会学习”,而是 “学会自学” —— 这一点点的变化,让十多年前没写顺的东西,终于在这一次迎刃而解,自成体系。

> 以前,我在写作课里讲,写好的前提就是 “Narrow down your topic” —— 把话题范围缩小缩小再缩小…… 这次算是给读者一个活生生的实例了罢。

自学能力,对每个个体来说,是这个变化频率和变化幅度都在不断加大的时代里最具价值的能力。具备这个能力,不一定能直接增加一个人的幸福感(虽然实际上常常确实能),但它一定会缓解甚至消除一个人的焦虑 —— 若是在一个以肉眼可见的方式变化着的环境里生存,却心知肚明自己已然原地踏步许久,自己正在被这个时代甩在身后,谁能不焦虑呢?

实际上,这些年来我写的书,都是关于学习的,无论是《[把时间当作朋友](https://github.com/xiaolai/time-as-a-friend)》,还是《通往财富自由之路》,甚至《韭菜的自我修养》,你去看就知道,背后都是同样的目标:学习、进步 —— 甚至**进化**。

这一次的《自学是门手艺》,首先,可以看作是之前内容的 “实践版”:

> 完成这本书的内容,起码会习得一个新技能:编程。

更为重要的是,可以把《自学是门手艺》当作之前内容的 “升级版”:

> 自学能力,是持续学习持续成长的发动机……

仔细观察整个人群,你就会发现一个惊人且惊悚的事实:

> **至少有 99% 的人终生都没有掌握自学能力!**

其实这个数字根本不夸张。根据 2017 年的统计数据,从 1977 年到 2017 年,40 年间全国大学录取人数总计为 1.15 亿左右(11518.2 万),占全国人口数量的 10% 不到,另外,这其中一半以上是专科生…… 你觉得那些 4% 左右的本科毕业生中,带着自学能力走入社会的比例是多少?不夸张地讲,我觉得 1% 都是很高的比例了 —— 所以,前面提到的 99% 都是很客气的说法。

绝大多数人,终其一生都没有自学过什么。他们也不是没学过,也不是没辛苦过,但事实却是:他们在有人教、有人带、有人逼的情况下都没真学明白那些基础知识…… 更可怕的是,他们学的那些东西中,绝大多数终其一生只有一个用处:考试。于是,考试过后,那些东西就 “考过即弃” 了…… 不得不承认,应试教育的确是磨灭自学能力的最有效方法。

在随后的生活里,尽管能意识到自己应该学点什么,常有 “要是我也会这个东西就好了” 的想法,但基本上百分之百以无奈结束 —— 再也没有人教、再也没有人带、再也没有人逼…… 于是,每次 “决心重新做人” 都默默地改成 “继续做人” 而后逢年过节再次许愿 “重新做人”……

这是有趣而又尴尬的真相:

> 没有不学习的人。

你仔细观察就知道了,就算被你认为不学无术的人,其实也在学习,只不过,他们的选择不同,他们想学的是投机取巧,并天天琢磨怎样才能更好地投机取巧……

但他们不是最倒霉的人。最倒霉的人是那种人,也 “认真学了”,可总是最终落得个越来越焦虑的下场……
经常有一些人指责另外一些人 “贩卖焦虑” —— 根据我的观察,这种指责的肤浅在于,焦虑不是被卖方贩卖的产品,焦虑其实是买方长期自行积累的结果。

**别人无法贩卖给你焦虑,是你自己焦虑** —— 是你自己在为自己不断积累越来越多的焦虑……

然而,又有谁不想解决掉焦虑呢?又有谁不想马上解决掉焦虑呢?

于是,你焦虑,你就要找解决方案。而焦虑的你找到的解决方案,就是花个钱买本书,报个班,找个老师,上个课…… 这能说是别人贩卖焦虑给你吗?

自学能力强的人,并非不花钱,甚至他们花的钱可能更多。他们也花钱买书,而且买更多的书;他们也可能花钱上课,而且要上就上最好的课、最好的班;他们更经常费尽周折找到恰当的人咨询、求教、探讨 —— 所以,事实上,他们更可能花了更多的钱……

但自学能力强的人不焦虑,起码他们不会因为学习以及学习过程而焦虑 —— 这是重大差别。

而焦虑的大多数,并不是因为别人贩卖焦虑给他们,他们才 “拥有” 那些焦虑 —— 他们一直在焦虑,并且越来越焦虑……

为什么呢?总也学不会、学不好,换做是你,你不焦虑吗?!

生活质量就是这样一点一点被消磨掉的 —— 最消耗生活质量的东西,就是焦虑。

我相信,若是《自学是门手艺》这本书真的有用,它的重要用处之一就是能够缓解你的焦虑,让你明白,首先焦虑没用,其次,有办法也有途径让你摆脱过往一事无成的状况,逐步产生积累,并且逐步体会到那积累的作用,甚至最后还能感觉到更多积累带来的加速度…… 到那时候,焦虑就是 “别人的事情” 了。

自学没有什么 “秘诀”。**它是一门手艺**,并且,严格意义上来讲,它**只是**一门手艺。

手艺的特点就是**无需天分**。手艺的特点就是**熟练程度决定一切**。从这一点上来看,自学这门手艺和擀饺子皮没什么区别 —— 就那点事,刚开始谁都笨手笨脚,但熟练了之后,就那么回事…… 而已。

做什么事都有技巧,这不可否认。

自学当然也有技巧…… 不过,请做好思想准备:

> 这儿的空间,**没什么新鲜**……

—— 这是崔健一首歌里的歌词片段,但放在这里竟然非常恰当到位。

**一切与自学相关的技巧都是老生常谈**。

中国人说,熟能生巧;老外说,Practice makes perfect —— 你看,与自学相关的技巧,干脆不分国界……

—— 因为这事人类从起点开始就没变过 —— 每代人都有足够多的人在自学这件事上挣扎…… 有成的有不成的;成的之中有大成有小成…… 可有一件事同样不变:留下的文字、留下的信息,都是大成或者小成之人留下的,不成的人不声不响就销声匿迹。

并且,从各国历史上来看,自学技巧这个话题从未涉及到政治,无论是在东方还是西方都是如此。结果就是,在自学能力这个小领域中,留下并流传下来的信息,几乎从未被审查,从未被摧毁,从未被侵犯,从未被扭曲 —— 真的是个特别罕见的 “纯净的领域” —— 这真的是整个人类不可想像之意外好运。

这就是为什么一切的自学技巧到最后肯定是老生常谈的原因。

大部分年轻人讨厌老生常谈。

但这还真的是被误导的结果。年轻人被什么误导了呢?

每一代人都是新鲜出生,每一代人出生时都在同一水准…… 随着时间的推移,总是庸者占绝大多数,这个 “绝大多数” 不是 51%,不是 70%,而是 99%!—— 年轻人吃亏就吃在没把这个现象考虑进来。

也就是说,虽然有用的道理在不断地传播,可终究还是 99% 的人做不到做不好,于是:

> 讲大道理的更可能是庸者、失败者,而不是成功者。

人类有很多天赋。就好像我反复提到的那样,“就算不懂也会用” 是人类的特长。同样的道理,人类在这方面同样擅长:

> 无论自己什么样,在 “判断别人到底是不是真的很成功” 上,基本上有 99% 的把握……

所以,十岁不到的时候,绝大多数小朋友就 “看穿” 了父母,后来再 “看穿” 了老师…… 发现他们整天说的都是他们自己做不到的事情…… 于是误以为自己 “看穿” 了整个世界。

那时候小朋友们还没学、或者没学好概率这个重要知识,于是,他们并不知道那只不过是 99% 的情况,而且更不知道 “**因素的重要性与它所占的比例常常全无正相关**”,所以当然不知道那自己尚未见到的 `1%` 才可能是最重要的……

于是,99% 的小朋友们一不小心就把自己 “搭了进去”:

> 不仅讨厌老生常谈,而且偏要对着干,干着干着就把自己变成了另外一个属于那 99% 的另外一个老生……

这是 99% 的人终其一生的生动写照。

做 `1%` 很难吗?真的很简单,有时仅仅一条就可能奏效:

> **在自学这件事上,重视一切老生常谈……**

很难吗?不难,只不过是一个 “开关” 而已。

当我动手写这本 “书” 的时候,是 47 岁那年(2019)的春节前 —— 显然,这个时候我也早就是一位 “老生” 了…… 并且,这些道理我已经前后讲了二十年!算是 “常谈” 甚至 “长谈” 了罢……

开始在新东方教书那年,我 28 岁;用之前那一点三脚猫的编程能力辅助着去写《TOEFL 核心词汇 21 天突破》是 2003 年;后来写《[把时间当作朋友](https://github.com/xiaolai/time-as-a-friend)》是 2007 年,这本书的印刷版出版发行是在 2009 年;再后来陆续写了很多内容,包括没有纸质版发行只有在线版的《人人都能用英语》(2013);以及因为在罗振宇的得到 App 上开专栏,把之前写过的《学习学习再学习》重构且扩充而出版的《通往财富自由之路》(2017);甚至连《韭菜的自我修养》(2018)都是讲思考、学习、和认知升级的……

说来说去,就那些事 —— **没什么新鲜**。

这中间也有很多写了却没写完,或者因为写得自己不满意扔在柜子里的东西,比如《人人都是工程师》(2016)—— 哈!我就是这么坚韧,有了目标就会死不放弃…… 3 年后的今天,我终于用那个时候完全想不到的方式完成了当时的目标,并且,做到了很多 3 年前自己都完全想象不到的事情。

在写当前这本《自学是门手艺》的过程中,我从一开始就干脆没想给读者带来什么 “新鲜” 的或者 “前所未见” 的自学技巧 —— 因为真的就没有,根本就没有什么新鲜的自学技巧…… 没有,真的没有 —— 至少,我自己这么久了还是真的没见识过。

然而,我算是最终能做到的人。知道、得到、做到之间,均各不相同。

二十年前,在拥挤的课堂里坐在台下听我讲课的小朋友们,绝大多数在当时应该没有想到他们遇到了这样一个人 —— 二十年后,刚认识我的人也不会自动知道我是这样的人。

但是,这些年里,看到我在一点一点进步、从未原地踏步的人很多很多…… 我猜,所谓的 “榜样”,也不过如此了罢。

不夸张地讲,这可能是当前世界上**最硬核的鸡汤书**了 —— 因为,虽然它就是鸡汤(李笑来自认就是个鸡汤作者),但它不是 “只是拿话糊弄你” 那种,也不是 “只不过是善意的鼓励” 那种,它是那种 “教会你人生最重要的技能” 的鸡汤,并且还不仅仅只有一种技能,起码两个:“自学能力” 和 “编程能力”…… 而这两个能力中的无论哪一种,都是能确定地提高读者未来收入的技能,对,就是 `100%` 地确定 —— 有个会计专业的人求职的时候说 “我还会编程” 且还能拿出作品,你看看他可不可能找不到工作?你看看他是不是能薪水更高?

`#!` —— 这是个程序员能看懂的梗。

关键在于,这个老生不是说说而已的老生,他是能够**做到**的人:

> - 一个末流大学的会计专业毕业的人不得已去做了销售;
> - 这个销售后来去国内最大的课外辅导机构当了 7 年 TOEFL/GRE/GMAT 老师;
> - 这个英语老师后来同时成了很多畅销书、长销书的作者;
> - 这个作者后来居然成了著名天使投资人;
> - 这个投资人后来竟然写了本关于编程入门的书籍;
> - 这本 “书” 最终竟然还是一个完整的产品,不仅仅是 “一本书”……

然而呢?

—— 然而,即便是这样的老生,也讲不出什么新鲜道理。

因为啊,历史上留下来的所有关于自学的技巧,都是人类史上最聪明的人留下来的 —— 你我这样的人,照做就可以了…… 现在你明白怎么回事了吧?

**记住罢** ——

> **千万不要一不小心就把自己搭进去……**

<p style="text-align: right"><strong>李笑来</strong></p>
<p style="text-align: right">初稿完成于 <em>2019</em> 年 <em>2</em> 月 <em>27</em> 日</p>

如何证明你真的读过这本书?

積ん読

日语里有个很好玩的词,“**積ん読**”([tsundoku](https://en.wikipedia.org/wiki/Tsundoku)):

> 指那些买回来堆在那里还没读过的(甚至后来干脆不看了的)书……

细想想,每个人都有很多很多 “積ん読”。小时候我们拿回家的教科书中就有相当一部分,其实就是 “積ん読”,虽然那时候掏钱买书的是父母,不仔细看、或者干脆不看的时候,也知道自己在偷懒…… 再后来就是 “主动犯罪” 了 —— 比如,很多人买到手里的英语词汇书是根本就没有翻到过第二个列表的,乃至于过去我常常开玩笑说,中国学生都认识一个单词,*abandon*,不是吗?这个单词是很多很多人 “决心重新做人” 而后 “就这样罢” 的铁板钉钉的见证者。

在没有电子书的时代,印刷版书籍多少还有一点 “装饰品” 功用,可是到了电子书时代,谁知道你的设备里有多少付费书籍呢?攒下那么多,其实并没有炫耀的地方,给谁看呢?据说,Kindle 的后台数据里可以看到清楚的 “打开率”,大抵上也是在 &frac14; ~ &frac13; 之间,也就是说,差不多有 &frac23; ~ &frac34; 的电子书籍被购买下载之后,从来就没有被打开过。

如此看来,付费之后并不阅读,只能欺骗一个对象了:自己。跟心理学家们之前想象的不同,我认为人们通常是不会欺骗自己的,至少很难 “故意欺骗自己”。所以,对于 “买了之后坚决不读” 这个现象,我不认为 “给自己虚妄的满足感” 是最好的解释。

更朴素一点,更接近真相的解释是:

> 那百分之七八十的人,其实是想着给自己一个希望……

—— 等我有空了一定看。嗯。

说来好笑,其实每个人共同拥有的目标之一是这样的:

> 成为前百分之二十的少数人……

然而,PK 掉百分之七八十的人的方法真的很简单很简单啊:

> 把买来的书都真真切切地认真读过就可以了。

这实在是太简单了罢?!可是…… 我知道你刚刚那个没出息的闪念:

> 那我少买书甚至不买书不就可以了吗?

你自己都知道这是荒谬的,却忍不住为你的小聪明得意 —— 其实吧,幸亏有你们在,否则我们怎么混进前百分之二十呢?

PoW

比特币这个地球上第一个真正被证明为可行的区块链应用中有一个特别重要的概念,叫做 “**工作证明**”(Proof of Work)—— 你干活了就是干活了,你没干活就是没干活,你的工作是可被证明的……

借用这个思路,我设计了个方法,让你有办法证明自己就是看过这本书,就是读完了这本书 —— 你能向自己也向别人证明自己曾经的工作…… 是不是挺好?

证明的方法是使用 [github.com](https://github.com) 这个网站以及版本控制工具 **git**。

具体步骤

请按照以下步骤操作:

> 1. 注册 [github.com](https://github.com) 帐号 —— 无论如何你都必须有 github 账户;
> 2. 使用浏览器访问 [https://github.com/selfteaching/the-craft-of-selfteaching](https://github.com/selfteaching/the-craft-of-selfteaching);
> 3. 在页面右上部找到 “Fork” 按钮,将该仓库 Fork 到你自己的账户中;
> 4. 使用 `git clone` 命令或者使用 [Desktop for Github](https://desktop.github.com/) 将 [the craft of selfteaching](https://github.com/selfteaching/the-craft-of-selfteaching) 这个你 Fork 过来的仓库克隆到本地;
> 5. 按照 [Jupyterlab 的安装与配置](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/T-appendix.jupyter-installation-and-setup.md) 的说明在本地搭建好 Jupyterlab —— 如果在 Jupyterlab 中浏览本书的话,其中的所有代码都是可以 “当场执行” 的,并且,你还可以直接改着玩……
> 6. 在阅读过程中,可以不断通过修改文章中的代码作为练习 —— 这样做的结果就是已阅读过的文件会发生变化…… 每读完一章,甚至时时刻刻,你都可以通过 `git commit` 命令向你自己 Fork 过来的仓库提交变化 —— 这就是你的阅读工作证明;
> 7. 仓库里有一个目录,`my-notes`,你可以把你在学习过程中写的笔记放在那里;
> 8. 仓库里还有另外一个目录,`from-readers`;那是用来收集读者反馈的 —— 将来你可以写一篇《我的自学之路》,放在这个目录里,单独创建一个分支,而后提交 `pull request`,接受其他读者投票,若是达到一定的赞同率,那么你的文章就会被收录到主仓库中被更多人看到,激励更多的人像你一样走上自学之路……

当然,为了这么做,你还要多学一样反正你早晚都必须学会的东西,Git —— 请参阅附录《[Git 入门](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/T-appendix.git-introduction.md)》。

时间就是这样,我们没办法糊弄它。而有了 git 这样的工具之后,我们在什么时候做了什么样的工作,是很容易证明的 —— 这对我们来说真是天大的好事。

如何使用 Pull Request 为这本书校对

另外,在你阅读的过程中,发现有错别字啊、代码错误啊,甚至有 “更好的表述” 等等,都可以通过 `pull request` 来帮我改进 —— 这也是一种 “工作证明”。

(1) 使用浏览器访问 https://github.com/selfteaching/the-craft-of-selfteaching

(2) 点击右上角的 “Fork 按钮”,将该仓库 Fork 到你的 Github 账户中

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/github-fork.png?raw=true)

(3) 创建一个新分支,可以取名为 `from-<your_username>`,比如,`by git.basic.tutorial`;之后点击 Create Branch 建立新分支。

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/github-new-branch.png?raw=true)

(4) 在新分支下进行修改某个文件,而后提交 —— 提交前不要嫌麻烦,一定要在 Comment 中写清楚修改说明:

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/github-commit.png?raw=true)

以上示例图片中是修改了 README.md 文件 —— 事实上,你应该提交的是的确有必要的校对。

另外,**请注意**:在创建分支之前,要将你的 Fork 更新到最新版。具体操作方法见下一节《如何在 Github 网站上将自己的 Fork 与原仓库同步》。

(5) 在页面顶部选择 Pull request 标签:

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/github-pull-request.png?raw=true)

而后点击 `Compare & pull request` 按钮 —— 如果看不到这个按钮,那就点击下面刚刚修改文件的链接,如上图中的 “Update README.md”(这是你刚刚提交修改时所填写的标题)。

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/github-open-pull-request.png?raw=true)

确认无误之后,点击 `Create pull request` 按钮。

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/github-create-pull-request.png?raw=true)

(6) 随后,Github 用户 [@xiaolai](https://github.com/xiaolai) —— 就是我,即,the-craft-of-selfteaching 这个仓库的所有者,会被通知有人提交了 Pull request,我会看到:

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/github-confirm-merge.png?raw=true)

在我确认这个 Pull request 修改是正确的、可接受的之后,我就会按 `Merge pull request` 按钮 —— 如此这般,一个修正就由你我共同完成了。

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/github-merged-pull-request.png?raw=true)

**注意**

提交 Pull request 的时候,最佳策略如下:

> * 提交 Pull request 之前,必须先将你的 Fork 的 master 与原仓库同步到最新;
> * 从 master 创建 **新的 branch** 进行增补、修改等操作;
> * 尽量每次只提交一个小修改;
> * 提交时尽量简短且清楚地说明修改原因;
> * 耐心等待回复。

当自己的 Fork 过来的仓库已经被你在本地 “玩残” 了的时候,它千万不能被当作用来提交 Pull request 的版本。自己本地怎么玩都无所谓,但需要向别人提交 Pull request 的时候,必须重新弄一个当前最新版本到本地,而后再在其基础上修改。

如何在 Github 网站上将自己的 Fork 与原仓库同步

(1) 在你的 Fork 页面中如下图所示,点击 `Compare` 链接:

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/sync-fork-1.png?raw=true)

(2) 将 `base repository` 更改成当前自己的 Fork,在图示中即为 `gitbasictutorial/the-craft-of-selfteaching`:

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/sync-fork-2.png?raw=true)

(3) 这时候,页面会显示 `There isn't anything to compare.`,因为你在比较 “自己” 和 “自己”。点击 `compare across forks` 链接:

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/sync-fork-3.png?raw=true)

(4) 将 `head repository` 更改成 Upstream Repository(即,上游仓库),在图示中即为 `selfteaching/the-craft-of-selfteaching`:

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/sync-fork-4.png?raw=true)

(5) 稍等片刻,你会看到比较结果;而后你可以创建一个 Pull request —— 这是一个由你自己向你自己的 Fork 仓库提交的 Pull request:

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/sync-fork-5.png?raw=true)

(6) 而后你在 `Pull requests` 标签页里会看到你刚刚提交的 Pull request:

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/sync-fork-6.png?raw=true)

(7) 同意并合并之后的结果是,你的 Fork 与上游仓库同步完成了:

[〔图〕](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/images/sync-fork-7.png?raw=true)

当然,有时会出现一些你无法解决的问题,那么,还有一个最后的方法:

> 将你的 Fork 删除,而后重新到 https://github.com/selfteaching/the-craft-of-selfteaching 页面按一次 `Fork` 按钮……

如何使用 github 记录自己的学习过程

你可以在本地建立一个分支(branch),例如,取名为 `study`:

```bash
git branch study
git checkout study
```

如此这般之后,你在本地工作目录中所做的任何修改,都可以提交到 `study` 这个分支之中。

你每次在 Jupyterlab 中浏览 `ipynb` 文件,按 `^ + Enter` 执行 code cell 中的代码的时候,该文件都会发生一些变化;你也可以随意修改文件中的任何地方,比如,添加一个 code cell,将某段代码从头至尾 “敲” 一遍;也可以修改某个 code cell 中的代码看看执行结果有什么不同;还可以添加或者修改任何 markdown cell —— 就当自己做笔记了……

总而言之,当你阅读完某一章节并如上所说那样做了一些改动之后,那个 `ipynb` 文件就发生了一些变化。于是,你就可以执行以下命令:

```bash
git add .
git commit -am 'my study result'
git push
```

如此这般,在 `study` 这个分支中就记录着你的学习轨迹。

当然,如果在这过程中,你发现本书自身有需要校对的地方,那么,你需要切换到 `master` 分支,执行以下命令:

```bash
git checkout master
git pull
```

而后再修改,进而按照上一节的方法提交 Pull request。

未来,在 [https://github.com/selfteaching](https://github.com/selfteaching) 下我会专门设置一个 repo,用来自动扫描 github 上本书的学习记录 —— 这种记录在过往的书籍当中是不可能存在的,然而,现在却可以了。在我看来,将来这种记录的作用甚至有可能比 “学历” 还要重要。

为什么一定要掌握自学能力?

一句话解释清楚:

> 没有自学能力的人没有**未来**。

有两个因素需要深入考虑:

> * 未来的日子还很长
> * 这世界进步得太快

我有个观察:

> 很多人都会不由自主地去复刻父母的人生时刻表。

比如,你也可能观察到了,父母晚婚的人自己晚婚的概率更高,父母晚育的人自己晚育的概率也更高……

再比如,绝大多数人的内心深处,会不由自主地因为自己的父母在五十五岁的时候退休了,所以就默认自己也会在五十五岁前后退休…… 于是,到了四十岁前后的时候就开始认真考虑退休,在不知不觉中就彻底丧失了斗志,早早就活得跟已经老了很多岁似的。

但是,这很危险,因为很多人完全没有意识到自己所面临的人生,与父母所面临的人生可能完全不一样 —— 各个方面都不一样。单举一个方面的例子,也是比较容易令人震惊的方面:

> 全球范围内都一样,在过去的五十年里,人们的平均寿命预期增长得非常惊人……

拿中国地区做例子,根据世界银行的数据统计,中国人在出生时的寿命预期,从 1960 年的 _43.73_ 岁,增长到了 2016 年的 _76.25_ 岁,56 年间的增幅竟然有 **74.39%** 之多!

```python
import matplotlib.pyplot as plt
import numpy as np

data = np.genfromtxt('life-expectancy-china-1960-2016.txt',
delimiter=',',
names=['x', 'y'])
da1960 = data[0][1]
da2016 = data[-1][1]
increase = (da2016 - da1960) / da1960
note = 'from {:.2f} in 1960 to {:.2f} in 2016, increased {:.2%}'\
.format(da1960, da2016, increase)

plt.figure(figsize=(10, 5))
plt.plot(data['x'], data['y'])
plt.ylabel('Life Expectancy from Birth')
plt.tick_params(axis='x', rotation=70)
plt.title('CHINA\n' + note)

# plt.savefig('life-expectancy-china-1960-2016.png', transparent=True)
plt.show()

# data from:
# https://databank.worldbank.org/data/reports.aspx?source=2&series=SP.DYN.LE00.IN
```

[〔图:png〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/Part.1.A.better.teachyourself_2_0.png?raw=true)

如此发展下去,虽然人类不大可能永生不死,但平均寿命依然在持续延长是个不争的事实。与上一代不同,现在的千禧一代,需要面对的是百岁人生 —— 毫无疑问,不容置疑。

这么长的人生,比默认的想象中可能要多出近一倍的人生,再叠加上另外一个因素 —— 这是个变化越来越快的世界 —— 会是什么样子?

我是 1972 年出生的。从交通工具来看,我经历过出门只能靠步行,大街上都是牛车马车,机动车顶多见过拖拉机,到有自行车,到见过摩托车,到坐小汽车,到自己开车,到开有自动辅助驾驶功能的电动车…… 从阅读来看,我经历过只有新华书店,到有网络上的文字,到可以在当当上在线买到纸质书,到有了国际信用卡后可以在 Amazon 上第一时间阅读新书的电子版、听它的有声版,到现在可以很方便地获取最新知识的互动版,并直接参与讨论…… 从技能上来看,我经历过认为不识字是文盲,到不懂英语是文盲,到不懂计算机是文盲,到现在,不懂数据分析的基本与文盲无异……

我也见识过很多当年很有用很赚钱很令人羡慕的技能 “突然” 变成几乎毫无价值的东西,最明显的例子是驾驶。也就是二十多年前,的哥还是很多人羡慕的职业呢!我本科的时候学的是会计专业,那时候我们还要专门练习打算盘呢!三十年之后的今天,就算有人打算盘打得再快,有什么具体用处嘛?我上中学的时候,有个人靠出版字帖赚了大钱 —— 那时候据说只要写字漂亮就能找到好工作;可今天,写字漂亮与否还是决定工作好坏的决定性因素吗?打印机很便宜啊!

这两个因素叠加在一起的结果就是,这世界对很多人来说,其实是越来越残忍的。

我见过太多的同龄人,早早就停止了进步,早早就被时代甩在身后,早早就因此茫然不知所措 —— 早早晚晚,你也会遇到越来越多这样的人。他们的共同特征只有一个:

> 没有自学能力

有一个统计指数,叫做人类发展指数(Human Development Index),它的曲线画出来,怎么看都有即将成为指数级上升的趋势。

```python
import matplotlib.pyplot as plt
import numpy as np
plt.figure(figsize=(10, 5))

lebdata = np.genfromtxt('life-expectancy-china-1960-2016.txt',
delimiter=',',
names=['x', 'y'])

hdidata = np.genfromtxt('hdi-china-1870-2015.txt',
delimiter=',',
names=['x', 'y'])

plt.plot(hdidata['x'], hdidata['y'], label='Human Development Index')
plt.tick_params(axis='x', rotation=70)
plt.title('China: 1870 - 2015')

plt.plot(lebdata['x'], lebdata['y'] * 0.005, label='Life Expectancy from Birth')
plt.plot(secondary_y=True)

plt.legend()

# plt.savefig('human-development-index-china-1870-2015.png', transparent=True)
plt.show()

# link:
# https://ourworldindata.org/human-development-index

# data from:
# blob:https://ourworldindata.org/44b6da71-f79e-42ab-ab37-871e4bd256e9
```

[〔图:png〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/Part.1.A.better.teachyourself_4_0.png?raw=true)

社会发展越来越快,你要面对的人生越来越长,在那一段与你的直觉猜想并不相同的漫漫人生路上,你居然没有磨练过自学能力,竟然只能眼睁睁地看着自己被甩下且无能为力,难道接下来要在那么长的时间里 “苦中作乐” 吗?

没有未来的日子,怎么过呢?

我本科学的是会计,研究生跑到国外读宏观经济学没读完,跑回国内做计算机硬件批发,再后来去新东方应聘讲授托福课程,离开新东方之后创业,再后来做投资,这期间不断地写书…… 可事实上,我的经历在这个时代并不特殊。有多少人在后来的职业生涯中所做的事情与当年大学里所学的专业相符呢?

纽约联邦储蓄银行在 2012 年做过一个调查,发现人们的职业与自己大学所学专业相符的比例连 _30%_ 都不到。而且,我猜,这个比例会持续下降的 —— 因为这世界变化快,因为大多数教育机构与世界发展脱钩的程度只能越来越严重……

```python
import matplotlib.pyplot as plt

labels = ['Major Match', '']
sizes = [273, 727]
colors = ['#E2E2E2', '#6392BF']
explode = (0, 0.08)
plt.figure(figsize=(7, 7))
plt.pie(sizes,
labels=labels,
explode=explode,
autopct='%1.1f%%',
colors=colors,
startangle=270,
shadow=True)
# plt.savefig('major-match-job.png', transparent=True)
plt.show()

# data from:
# https://libertystreeteconomics.newyorkfed.org/2013/05/do-big-cities-help-college-graduates-find-better-jobs.html
```

[〔图:png〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/Part.1.A.better.teachyourself_6_0.png?raw=true)

绝大多数人终生都饱受**时间幻觉**的拖累。

小时候觉得时间太长,那是幻觉;长大了觉得时间越来越快,那还是幻觉 —— 时间从来都是匀速的。最大的幻觉在于,总是以为 “时间不够了” —— 这个幻觉最坑人。许多年前,有一次我开导我老婆。她说,“啊?得学五年才行啊?!太长了!” 我说,

> “你回头看看呗,想想呗,五年前你在做什么?是不是回头一看的时候,五年前就好像是昨天?道理是一样的,五年之后的某一天你回头想今天,也是 ‘一转眼五年就过去’ 了…… 只不过,你今天觉得需要时间太多,所以不肯学 —— 但是,不管你学还是不学,五年还是会 ‘一转眼就过去’ 的…… 到时候再回头,想起这事的时候,没学的你,一定会后悔 —— 事实上,你已经有很多次后悔过 ‘之前要是学了就好了’,不是吗?”

现在回头看,开导是非常成功的。十多年后的今天,她已经真的可以被称为 “自学专家” —— 各种运动在她那儿都不是事。健身,可以拿个北京市亚军登上健与美杂志封面;羽毛球,可以参加专业比赛;潜水,潜遍全球所有潜水胜地,到最后拿到的各种教练证比她遇到的各地教练的都多、更高级;帆船,可以组队横跨大西洋;爬山,登上喜马拉雅……

都说,人要有一技之长。那这一技究竟应该是什么呢?

> 自学能力是唯一值得被不断磨练的长技。

磨练出自学能力的好处在于,无论这世界需要我们学什么的时候,我们都可以主动去学,并且还是马上开始 —— 不需要等别人教、等别人带。

哪怕有很强的自学能力的意思也并不是说,什么都能马上学会、什么都能马上学好,到最后无所不精无所不通…… 因为这里有个时间问题。无论学什么,都需要耗费时间和精力,与此同时更难的事情在于不断填补耐心以防它过早耗尽。另外,在极端的情况下,多少也面临天分问题。比如身高可能影响打篮球的表现,比如长相可能影响表演的效果,比如唱歌跑调貌似是很难修复的,比如有些人的粗心大意其实是基因决定的,等等。不过,以我的观察,无论是什么,哪怕只是学会一点点,都比不会强。哪怕只是中等水平,就足够应付生活、工作、养家糊口的需求。

我在大学里学的是会计专业,毕业后找不到对口工作,只好去做销售 —— 没人教啊!怎么办?自学。也有自学不怎么样的时候,比如当年研究生课程我就读不完。后来想去新东方教书 —— 因为听说那里赚钱多 —— 可英语不怎么样啊!怎么办?自学。离开新东方去创业,时代早就变了,怎么办?自学,学的不怎么样,怎么办?硬挺。虽然创业这事后来也没怎么大成,但竟然在投资领域开花结果 —— 可赚了钱就一切平安如意了吗?并不是,要面对之前从来没可能遇到的一些险恶与困境,怎么办?自学。除了困境之外,更痛苦的发现在于对投资这件事来说,并没有受过任何有意义的训练,怎么办?自学。觉得自己理解的差不多了,一出手就失败,怎么办?接着学。

我出身一般,父母是穷教师。出生在边疆小镇,儿时受到的教育也一般,也是太淘气 —— 后来也没考上什么好大学。说实话,我自认天资也一般,我就是那种被基因决定了经常马虎大意的人。岁数都这么大了,情商也都不是一般的差 —— 还是跟年轻的时候一样,经常莫名其妙就把什么人给得罪透了……

但我过得一直不算差。

靠什么呢?人么,一个都靠不上。到最后,我觉得只有一样东西真正可靠 —— **自学能力**。于是,经年累月,我磨练出了一套属于我自己的本领:只要我觉得有必要,我什么都肯学,学什么都能学会到够用的程度…… 编程,我不是靠上课学会的;英语,不是哪个老师教我的;写作,也不是谁能教会我的;教书,没有上过师范课程;投资,更没人能教我 —— 我猜,也没人愿意教我…… 自己用的东西自己琢磨,挺好。

关键在于,自学这事并不难,也不复杂,挺简单的,因为它所需要的一切都很朴素。

于是,从某个层面上来看,我每天都过的很开心。为什么?因为我有未来。凭什么那么确信?因为我知道我自己有自学能力。

**—— 我希望你也有。**

准确地讲,希望你有个更好的未来。

而现在我猜,此刻,你心中也是默默如此作想的罢。

为什么把编程当作自学的入口?

很多人误以为 “编程” 是很难的事情。

…… 实则不然 —— 这恰恰是我们选择 “编程” 作为自学的第一个 “执行项目” 的原因。

一本关于自学能力的书,若是真的能够起到作用,那么它就必须让读者在读之前和读之后不一样 —— 比如,之前可能没有自学能力,或者自学能力很差,之后就有了一定的自学能力……

然而,这很难。不但对读者来说很难,对作者来说更难 —— 我当过那么多年被学生高度评价的老师,出版过若干本畅销且长销的书籍,所以更是清楚地知道例子的重要性。

道理当然很重要;可是,在传递道理的时候,例子相对来看好像更重要。

同样的道理,例子不准,人就可能会理解错;例子不精彩,人就可能听不进去;例子居然可以令人震惊,那就可以做到让听众、让读者 “永生不忘”。

许多年前,有位后来在美国读书已经博士毕业了的学生来信,大意是说:

> 好多年前,我在新东方上课,听您讲,人学习就好像是动物进化一样…… 很多人很早就开始停止了进化,本质上跟猴子没啥区别。
>
> 那段类比好长,我记不太清楚细节了…… 可是,当时我是出了一身汗的,因为我忽然觉得自己是一只猴子。可是,突然之间,我不想继续做猴子,更不想一直做猴子!
>
> 从那之后,我好像变了一个人似的…… 现在我已经博士毕业了,觉得应该写封信告诉您,我不再是猴子了,最起码是大猩猩,而且我保证,我会一直进化。
>
> ……

所以啊,在我看来,写书讲课之前,最重要的工作,也是做得最多的事情,其实就是 “找到好例子” —— 那即意味着说,先要找到很多很多恰当合适的例子,而后再通过反复比较试验,挑出那个效果最好的例子。了解了这一点,将来你准备任何演讲,都会不由自主地多花一点时间在这方面,效果肯定比 “把幻灯片做得更花哨一些” 要好太多了罢?

后来,我选中了一个例子:“**自学编程**” —— “*尽量只通过阅读学会编程*”。

(一)

选择它的理由,首先就在于:

> 事实证明,**它就是无论是谁都能学会的** —— 千万别不信。

它老少皆宜 —— 也就是说,“只要你愿意”,根本没有年龄差异。十二岁的孩子可以学;十八岁的大学生可以学;在职工作人员可以学…… 就算你已经退休了,想学就能学,谁也拦不住你。

它也不分性别,男性可以学,女性同样可以学,性别差异在这里完全不存在。

它也不分国界,更没有区域差异 —— 互联网的恩惠在于,你在北京、纽约也好,老头沟、门头沟也罢,在这个领域里同样完全没有任何具体差异。

尤其是在中国。现状是,中国的人口密度极高,优质教育资源的确就是稀缺…… 但在计算机科学领域,所有的所谓 “优质教育资源” 事实上完全没有任何独特的竞争力 —— 编程领域,实际上是当今世上极为罕见的 “**教育机会公平之地**”。又不仅在中国如此,事实上,在全球范围内也都是如此。

(二)

编程作为 “讲解如何习得自学能力的例子”,实在是太好了。

首先,编程这个东西反正要自学 —— 不信你问问计算机专业的人,他们会如实告诉你的,学校里确实也教,但说实话都教得不太好……

其次,编程这个东西最适合 “仅靠阅读自学” —— 这个领域发展很快,到最后,新东西出来的时候,没有老师存在,任由你是谁,都只能去阅读 “官方文档”,只此一条路。

然后,也是最重要的一条,别管是不是很多人觉得编程是很难的东西,事实上它就是每个人都应该具备的技能。

许多年前,不识字,被称为文盲……

后来,人们反应过来了,不识英文,也是文盲,因为科学文献的主导语言是英文,读不懂英文,什么都吃不上热乎的;等菜好不容易端上来了吧,早就凉了不说,味道都常常会变……

再后来,不懂基本计算机操作技能的,也算是文盲,因为他们无论做什么事情,效率都太低下了,明明可以用快捷键一下子完成的事情,却非要手动大量重复……

到了最近,不懂数据分析的,也开始算作文盲了。许多年前人们惊呼信息时代来了的时候,其实暂时体会不到什么太多的不同。然而,许多年过去,互联网上的格式化数据越来越多,不仅如此,实时产出的格式化数据也越来越多,于是,数据分析不仅成了必备的能力,而且早就开始直接影响一个人的薪资水平。

你作为一个个体,每天都在产生各种各样的数据,然后时时刻刻都在被别人使用着、分析着…… 然而你自己却全然没有数据分析能力,甚至不知道这事很重要,是不是感觉很可怕?你看看周边那么多人,有多大的比例想过这事?反正那些天天看机器算法生成的信息流的人好像就是全然不在意自己正在被支配……

怎么办?学呗,学点编程罢 —— 巧了,*这还真是个正常人都能学会的技能*。

(三)

编程作为 “讲解如何习得自学能力的例子” 最好的地方在于,这个领域的知识结构,最接近每个人所面对的人生中的知识结构。

这是什么意思呢?

编程入门的门槛之所以高,有个比较特殊的原因:

> **它的知识点结构不是线性的**。

我们在中小学里所遇到的教科书,其中每个章节所涉及到的知识点之间,全都是线性关联。第一章学好了,就有基础学第二章;在第二章的概念不会出现在第一章之中……

很遗憾,编程所涉及到的知识点没办法这样组织 —— 就是不行。编程教材之所以难以读懂,就是因为它的各章中的知识点结构不是线性排列的。你经常在某一章读到不知道后面第几章才可能讲解清楚的概念。

比如,几乎所有的 Python 编程书籍上来就举个例子:

```python
print('Hello, world!')
```

姑且不管这个例子是否意义非凡或者意义莫名,关键在于,`print()` 是个函数,而函数这个概念,不可能一上来就讲清楚,只能在后面若干章之后才开始讲解……

> 想要理解当前的知识点,需要依赖对以后才能开始学习的某个甚至多个知识点的深入了解……

这种现象,可以借用一个专门的英文概念,叫做 “**Forward References**” —— 原本是计算机领域里的一个[术语](https://en.wikipedia.org/wiki/Forward_declaration)。为了配合当前的语境,姑且把它翻译为 “**过早引用**” 罢,或者 “**前置引用**” 也行。

学校里的课本,都很严谨 —— 任何概念,未经声明就禁止使用。所以,学完一章,就能学下一章;跳到某一章遇到不熟悉的概念,往前翻肯定能找到……

在学校里习惯了这种知识体系的人,离开学校之后马上抓瞎 —— **社会的知识结构不仅不是这样的,而且*几乎全都不是*这样的**。工作中、生活里,充满了各式各样的 “*过早引用*”。为什么总是要到多年以后你才明白父母曾经说过的话那么有道理?为什么总要到孩子已经长大之后才反应过来当初自己对孩子做错过很多事情?为什么在自己成为领导之前总是以为他们只不过是在忽悠你?为什么那么多人创业失败了之后才反应过来当初投资人提醒的一些观念其实是千真万确的?—— 因为很多概念很多观念是 “过早引用”,在当时就是非常难以理解……

自学编程在这方面的好处在于,在自学的过程中,其实你相当于过了一遍 “模拟人生” —— 于是,面对同样的 “*过早引用*”,你不会觉得那么莫名其妙,你有一套你早已在 “模拟人生” 中练就的方法论去应对。

(四)

另外一个把编程作为 “讲解如何习得自学能力的例子” 最好的地方在于,你在这个过程中将不得不习得英语 —— 起码是英文阅读能力,它能让你在不知不觉中 “脱盲”。

学编程中最重要的活动就是 “阅读官方文档”。学 Python 更是如此。Python 有很多非常优秀的地方,其中一个令人无法忽视的优点就是它的文档完善程度极好。它甚至有专门的文档生成工具,[Sphinx](http://www.sphinx-doc.org/en/master/):

> Sphinx is a tool that makes it easy to create intelligent and beautiful documentation, written by Georg Brandl and licensed under the BSD license.
>
> It was originally created for [the Python documentation](https://docs.python.org/), and it has excellent facilities for the documentation of software projects in a range of languages. Of course, this site is also created from reStructuredText sources using Sphinx!

最好的 Python 教程,是官方网站上的 [The Python Tutorial](https://docs.python.org/3/tutorial/index.html),读它就够了。我个人完全没兴趣从头到尾写一本 Python 编程教材,不仅因为人家写得真好,而且它就放在那里。

虽然你在官方网站上就是很难找到它的中文版,虽然就不告诉你到底在哪里也显得很不厚道,但是,我建议你就只看英文版 —— 因为离开了这个教程之后,还是要面对绝大多数都是英文的现实。

为了照顾那些也想读完本书,但因为种种原因想着读中文可以快一些的人,链接还是放在这里:

> * https://docs.python.org/zh-cn/3/tutorial/index.html (for v.3.7.2)
> * http://www.pythondoc.com/pythontutorial3/ (for v.3.6.3)

我曾经专门写过一本书发布在网上,叫《[人人都能用英语](https://github.com/xiaolai/everyone-can-use-english)》。其中的观点就是,大多数人之所以在英语这事上很矬,是因为他们花无数的时间去 _“学”_,但就是 _“不用”_。学以致用,用以促学。可就是不用,无论如何就是不用,那英语学了那么多年能学好吗?

自学编程的一个 “副作用” 就是,**你不得不用英语**。而且还是天天用,不停地用。

当年我上大学的时候,最初英语当然也不好。不过,因为想读当时还是禁书的《动物庄园》([Animal Farm](https://www.marxists.org/subject/art/literature/children/texts/orwell/animal-farm/index.htm)),就只好看原版(当时好不容易搞到的是本英法对照版)…… 然后英语阅读就基本过关了。

这原理大抵上是这样,刚开始,英语就好像一层毛玻璃,隔在你和你很想要了解的内容之间。然而,由于你对那内容的兴趣和需求是如此强烈,乃至于即便隔着毛玻璃你也会挣扎着去看清楚…… 挣扎久了(其实没两天就不一样),你的 “视力” 就进化了,毛玻璃还在那里,但你好像可以穿透它看清一切……

自学编程,也算是一举两得了!

(五)

当然,把编程作为 “讲解如何习得自学能力的例子”,实在是太好了的最重要原因在于,自学编程对任何人来说都绝对是:

> * 现实的(Practical)
> * 可行动的(Actionable)
> * 并且还是真正是可达成的(Achievable)

最重要的就是最后这个 “可达成的”。虽然对读者和作者来说,一个做到没那么容易,另一个讲清楚也非常难,但是,既然是所有人都 “可达成的”,总得试试吧?但是,请相信我,这事比减肥容易多了 —— 毕竟,你不是在跟基因作斗争。

这只是个起点。

尽量只靠阅读学会编程,哪怕仅仅是入门,这个经历和经验都是极为宝贵的。

自学是门手艺。只不过它并不像卖油翁的手艺那样很容易被别人看到,也不是很容易拿它出来炫耀 —— 因为别人看不到么!然而,经年累月,就不一样了,那好处管他别人知不知道,自己却清楚得很!

你身边总有些人能把别人做不好的事做得极好,你一定很羡慕。可他们为什么能做到那样呢?很简单啊,他们的自学能力强,所以他们能学会大多数自学能力差的人终生学不到的东西。而且他们的自学能力会越来越强,每学会一样新东西,他们就积累了更多自学经验,难以对外言表的经验,再遇到什么新东西,相对没那么吃力。

另外,自学者最大的感受就是万物相通。他们经常说的话有这么一句:“…… **到最后,都是一样的呢**。”

(六)

最后一个好处,一句话就能说清楚,并且,随着时间的推移,你对此的感触会越来越深:

> 在这个领域里,自学的人最多……

没有什么比这句话更令人舒心的了:**相信我,你并不孤独**。

只靠阅读习得新技能

习得自学能力的终极目标就是:

> 有能力**只靠阅读**就能习得新技能。

退而求其次,是 “尽量只靠阅读就习得新技能” —— 当然,刚开始的时候可能需要有人陪伴,一起学,一起讨论,一起克服困难…… 但就是要摆脱 “没人教,没人带,没人逼,就彻底没戏” 的状态。

小时候总是听大人说:

> 不是什么东西都可以从书本里学到的……

一度,我觉得他们说的有道理。再后来,隐约感觉这话哪儿有毛病,但竟然又感觉无力反驳……

那时,真被他们忽悠到了;后来,也差点被他们彻底忽悠到!

幸亏后来我渐渐明白,且越来越相信:

> 自己生活工作学习上遇到的所有疑问,书本里应该都有答案 —— 起码有所参考。

“不是什么东西都可以从书本里学到的……” 这话听起来那么有道理,只不过是因为自己读书**不够多**、**不够对**而已。

过了 25 岁,我放弃了读小说,虚构类作品,我只选择看电影;而非虚构类作品,我选择尽量只读英文书,虽然那时候买起来很贵也很费劲,但我觉得值 —— 英文世界和中文世界的文化风格略有不同。在英文世界里,你看到的正常作者好像更多地把 “通俗易懂”、“逻辑严谨” 当作最基本的素养;而在中文世界里,好像 “故弄玄虚”、“偷梁换柱” 更常见一些;在英文世界里,遇到读不懂的东西可以很平静地接受自己暂时的愚笨,心平气和地继续努力就好;在中文世界里,遇到装神弄鬼欺世盗名的,弄不好最初根本没认出来,到最后跟 “认贼作父” 一样令人羞辱难当不堪回首。

说实话,我真觉得这事跟崇洋媚外没什么关系。我是朝鲜族,去过韩国,真觉得韩国的书更没法看(虽然明显是个人看法)…… 2015 年年底,我的律师告诉我,美国移民就快帮我办下来了,可那时候我开始觉得美国政府也各种乱七八糟,于是决定停止办理。我是个很宅的人,除了餐馆基本上哪儿都不去,陪家人朋友出去所谓旅游的时候,我只不过是换个房间继续宅着…… 可这些都不是重点,重点在于:

> **知识原本就应该无国界**…… 不是吗?不是吗!

再说,这些年我其实还读了不少中国人写的英文书呢,比如,张纯如的书很值得一看;郑念的 Life and Death in Shanghai,真的很好很好。我也读了不少老外写的关于中国的书 —— 这些年我一直推荐费正清的剑桥中国史(The Cambridge History of China),当然有中文版的,不过,能读英文版的话感受很不一样。

当然,英文书里同样烂书也很多,烂作者也同样一大堆,胡说八道欺世盗名的一大串…… 但总体上来看,非小说类著作质量的确更高一点。

还有,英语在科学研究领域早已成为 “主导语言”(Dominant Language)也是不争的事实。不过,英语成为主导语言的结果,就是英语本身被不断 “强奸”,外来语越来越多,“Long time no see” 被辞典收录就是很好的例子。事实上,英语本身就是个大杂烩……

[〔图:Joseph M. Williams’ research visualized (top 10,000 words).〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/Joseph-M-Williams-research-visualized.png?raw=true)<a href='https://medium.com/@andreas_simons/the-english-language-is-a-lot-more-french-than-we-thought-heres-why-4db2db3542b3'></a>

读书越多越明白读书少会被忽悠…… 很多人真的会大头捣蒜一般地认同 “不是什么东西都可以从书本里学到的……”

另外,很多人在如此教导小朋友的时候,往往是因为 “人心叵测” 啊,“江湖险恶” 啊,所以害怕小朋友吃亏。可事实上,如若说,人间那些勾心斗角的事貌似从书本里学不来的话,其实也不过还是历史书看少了 —— 勾心斗角的套路历史上全都被反复用过了。倒是有本中文书值得吐血推荐,民国时代的作者连阔如先生写的《江湖丛谈》,粗略扫过你就知道了,江湖那点事,也早就有人给你里里外外翻了个遍…… 只不过这书不太容易买到就是了。

我也遇到过这样的反驳:

> 书本能教会你做生意吗?!

说实话,去回驳这个反驳还真挺难,因为反驳者是脑容量特别有限才能说出这种话 —— 世界上有那么多商学院都是干嘛的?搞得它们好像不存在一样。首先,它们的存在说明,商业这事是有迹可循的,是可学习的;其次,商业类书籍非常多,是非虚构类书籍中的一大品类;更为重要的是,做生意这事,看谁做 —— 有本事(即,比别人拥有更多技能)的人做生意和没本事的人做生意,用同样的商业技巧,能有一样的效果吗?最后啊,这世界在这方面从来没有变过:一技傍身的人,总是不愁生活……

更为重要的是,这才几年啊,互联网本身已经成了一本大书 —— 关于全世界的一整本大书。仅仅是 10 多年前,大约在 2008 年前后,经过几年发展的 Wikipedia 被众多西方大学教授们群起而攻,指责它错误百出…… 可现在呢?Wikipedia 好像有天生的自我修复基因,它变得越来越值得信赖,越来越好用。

七零后八零后长大的过程中,还经常被父母无故呵斥:“怎么就你事这么多!” 或者无奈敷衍:“等你长大了就明白了……” 九零后、零零后呢?他们很少有什么疑问需要向父母提问,直接问搜索引擎,效果就是父母们天天被惊到甚至吓到。最近两年更不一样了,我有朋友在旧金山生活,他的孩子整天跟 Google 说话,有点什么问题,就直接 “Hey Google...”

我长大的那个年代,一句 “通过阅读了解世界” 好像还是很抽象甚至很不现实的话,现在呢?现在,除了阅读之外,你还能想出什么更有效的方法吗?反正我想不出。

有个很有趣的现象:

> 人么,只要识字,就忍不住阅读……

只不过,人们阅读的选择很不同而已。有自学能力的人和没有自学能力的人,在这一点上很容易分辨:

> * 有自学能力的人,选择阅读 “有繁殖能力” 的内容;
> * 没有自学能力的人,阅读只是为了消磨时光……

我把那些能给你带来新视野,能让你改变思考模式,甚至能让你拥有一项新技能的内容称之为 “有繁殖能力的内容”。

人都一样,拥有什么样的能力之后,就会忍不住去用,甚至总是连下意识中也要用。

那些靠阅读机器算法推送的内容而杀时间的人,恰恰就是因为他们有阅读能力才去不断地读,读啊读,像是那只被打了兴奋剂后来死在滚轮上的小白鼠。如果这些人哪怕有一点点自学能力,那么他们很快就会分辨出自己正在阅读的东西不会刺激自己的产出,只会消磨自己的时间;那么,他们就会主动放弃阅读那些杀时间的内容,把那时间和精力自然而然地用在筛选有繁殖能力的内容,让自己进步,让自己习得更多技能上去了。

所以,只要你有一次 “**只靠阅读习得一项新技能**” 的经验,你就变成另外一个人了。你会不由自主、哪怕下意识里都会去运用你新习得的能力…… 从这个角度看,自学很上瘾!能上瘾,却不仅无害,还好处无穷,这样的好事,恐怕也就这一个了罢。

我有过只靠阅读学会游泳的经历…… 听起来不像真的吧?更邪门的是,罗永浩同学的蛙泳,是我站在游泳池边,仅靠言语讲解,就让他从入水就扑腾开始三十分钟之内可以开始蛙泳的 —— 虽然当天他第一次蛙泳,一个来回五十米都坚持不下来。

仅靠阅读学会新技能不仅是可能的,并且,你随后会发现的真相是:

> 绝大多数情况下,没人能教你,也不一定有人愿意教你…… 到最后,你想学会或你必须学会什么东西的时候,**你只能靠阅读!** —— 因为其实你谁都靠不上……

我有很多偶像,英国数学家乔治・布尔就是其中一个 —— 因为他就是个基本上只靠阅读自学成才的人。十八、九岁,就自学了微积分 —— 那是将近两百年前,没有 Google,没有 Wikipedia…… 然后他还自己创办了学校,给自己打工…… 从来没有上过大学,后来却被皇家学院聘请为该学院第一个数学教授。然后,人家发明的布尔代数,在百年之后引发了信息革命…… 达芬奇也是这样的人 —— 要说惨,他比所有人都惨…… 因为几乎从一开始就貌似没有谁有资格有能力教他。

这些例子都太遥远了。给你讲个我身边的人,我亲自打过很长时间交道的人 —— 此人姓邱,人称邱老板。

邱老板所写的区块链交易所引擎,在 Github 上用的是个很霸气的名字,“[貔貅](https://github.com/peatio/peatio)”(英文用了一个生造的词,Peatio)—— 这个 Repo 至 2019 年春节的时候,总计有 2,913 个 Star,有 2,150 个 Fork…… 绝对是全球这个领域中最受关注的开源项目。2017 年 9 月,云币应有关部门要求关闭之前,是全球排名前三的区块链交易所。

邱老板当年上学上到几年级呢?初中未读完,所以,跟他聊天随口说成语是很有负担的,因为他真的可能听不懂…… 然而,他的编程、他的英语,全是自学的…… 学到什么地步呢?学到可以创造极有价值的商业项目的地步。他什么学习班都没上过,全靠阅读 —— 基本上只读互联网这本大书。

讲真,你没有选择,只靠阅读习得新技能,这是你唯一的出路。

开始阅读前的一些准备

内容概要

关于 Python 编程的第一部分总计 7 章,主要内容概括为:

> 1. 以布尔值为入口开始理解程序本质
> 1. 了解值的分类和运算方法
> 1. 简要了解流程控制的原理
> 1. 简要了解函数的基本构成
> 1. 相对完整地了解字符串的操作
> 1. 了解各种容器的基础操作
> 1. 简要了解文件的读写操作

阅读策略

首先,不要试图一下子就全部搞懂。这不仅很难,**在最初的时候也完全没必要**。

因为这部分的知识结构中,充满了 “过早引用”。请在第一遍粗略完成第 1 部分中的 E1 ~ E7 之后,再去阅读《[如何从容应对 “过早引用”?](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.F.deal-with-forward-references.md)》。

其次,这一部分,注定要**反复阅读若干遍**。

在开始之前,要明确这一部分的阅读目标。

这一部分的目标,不是让你读完之后就可以开始写程序;也不是让你读完之后就对编程或者 Python 编程有了完整的了解,甚至不是让你真的学会什么…… 这一部分的目标,只是让你 “**脱盲**”。

不要以为脱盲是很容易的事情。你看,所有人出生的时候,都天然是 “文盲”;人们要上好多年的学,才能够真正 “脱盲” —— 仔细想想吧,小学毕业的时候,所有人就真的彻底脱盲了吗?

以中文脱盲为例,学字的同时,还要学习笔划;为了学更多的字,要学拼音,要学如何使用《新华字典》……

学会了一些基础字之后,要学更多的词,而后在练习了那么多造词造句之后,依然会经常用错…… 你看,脱盲,和阅读能力强之间距离很长呢;不仅如此,阅读能力强和写作能力强之间的距离更长……

反复阅读这一部分的结果是:

> * 你对基本概念有了一定的了解
> * 你开始有能力相对轻松地阅读部分官方文档
> * 你可以读懂一些简单的代码

仅此而已。

心理建设

当我们开始学习一项新技能的时候,我们的大脑会不由自主地紧张。可这只不过是多年之间在学校里不断受挫的积累效应 —— 学校里别的地方不一定行,可有个地方特别行:给学生制造全方位、无死角、层层递进的挫败感。

可是,你要永远记住两个字:

> 别怕!

用四个字也行:

> 啥也别怕!

六个字也可以:

> 没什么可怕的!

我遇到最多的孱弱之语大抵是这样的:

> 我一个文科生……

哈哈,从某个层面望过去,其实吧,编程既不是文科也不是理科…… 它更像是 “手工课”。你越学就越清楚这个事实,它就好像是你做木工一样,学会使用一个工具,再学会使用另外一个工具,其实总共就没多少工具。然后,你更多做的是各种拼接的工作,至于能做出什么东西,最后完全靠你的想象力……

十来岁的孩子都可以学会的东西,你怕什么?

**别怕**,无论说给自己,还是讲给别人,都是一样的,它可能是人生中最重要的鼓励词。

关于这一部分内容中的代码

所有的代码,都可以在选中代码单元格(Code Cell)之后,按快捷键 `⇧ ⏎` 或 `^ ⏎` 执行,查看结果。

少量执行结果太长的代码,其输出被设置成了 “Scrolled”,是可以通过触摸板或鼠标滑轮上下滑动的。

为了避免大量使用 `print()` 才能看到输出结果,在很多的代码单元格中,开头插入了以下代码:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
```
你可以暂时忽略它们的意义和工作原理。注意:有时,你需要在执行第二次的时候,才能看到全部输出结果。

另外,有少量代码示例,为了让读者每次执行的时候看到不同的结果,使用了随机函数,为其中的变量赋值,比如:

```python
import random
r = random.randrange(1, 1000)
```

同样,你可以暂时忽略它们的意义和工作原理;只需要知道因为有它们在,所以每次执行那个单元格中的代码会有不同的结果就可以了。

如果你不是直接在网站上浏览这本 “书”、或者不是在阅读印刷版,而是在本地自己搭建 Jupyterlab 环境使用,那么请参阅附录《[Jupyterlab 的安装与配置](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/T-appendix.jupyter-installation-and-setup.md)》。

> **注意**:尤其需要仔细看看《[Jupyterlab 的安装与配置](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/T-appendix.jupyter-installation-and-setup.md)》的《关于 Jupyter lab themes》这一小节 —— 否则,阅读体验会有很大差别。

另外,如果你使用的是 [nteract](https://nteract.io) 桌面版 App 浏览 `.ipynb` 文件,那么有些使用了 `input()` 函数的代码是无法在 nteract 中执行的。

入口

“速成”,对绝大多数人<a href='#fn1' name='fn1b'><sup>[1]</sup></a>来说,在绝大多数情况下,是不大可能的。

编程如此,自学编程更是如此。有时,遇到复杂度高一点的知识,连快速入门都不一定是很容易的事情。

所以,这一章的名称,特意从 “*入门*” 改成了 “**入口**” —— 它的作用是给你 “指一个入口”,至于你能否从那个入口进去,是你自己的事了……

不过,有一点不一样的地方,我给你指出的入口,跟别的编程入门书籍不一样 —— 它们几乎无一例外都是从一个 “Hello World!” 程序开始的…… 而我们呢?

让我们从认识一个人开始罢……

乔治・布尔

1833 年,一个 18 岁的英国小伙脑子里闪过一个念头:

> **逻辑关系**应该能用**符号**表示。

这个小伙子叫乔治・布尔([George Boole](https://en.wikipedia.org/wiki/George_Boole),其实之前就提到过我的这位偶像),于 1815 年出生于距离伦敦北部 120 英里之外的一个小镇,林肯。父亲是位对科学和数学有着浓厚兴趣的鞋匠。乔治・布尔在父亲的影响下,靠阅读自学成才。14 岁的时候就在林肯小镇名声大噪,因为他翻译了一首希腊语的诗歌并发表在本地的报纸上。

到了 16 岁的时候,他被本地一所学校聘为教师,那时候他已经在阅读微积分书籍。19 岁的时候布尔创业了 —— 他办了一所小学,自任校长兼教师。23 岁,他开始发表数学方面的论文。他发明了 “操作演算”,即,通过操作符号来研究微积分。他曾经考虑过去剑桥读大学,但后来放弃了,因为为了入学他必须放下自己的研究,还得去参加标准本科生课程。这对一个长期只靠自学成长的人来说,实在是太无法忍受了。

1847 年,乔治 32 岁,出版了他人生的第一本书籍,[THE MATHEMATICAL ANALYSIS OF LOGIC](http://www.gutenberg.org/ebooks/36884) —— 18 岁那年的闪念终于成型。这本书很短,只有 86 页,但最终它竟然成了人类的瑰宝。在书里,乔治・布尔很好地解释了如何使用代数形式表达逻辑思想。

1849 年,乔治・布尔 34 岁,被当年刚刚成立的女皇学院(Queen's College)聘请为第一位数学教授。随后他开始写那本最著名的书,[AN INVESTIGATION OF THE LAWS OF THOUGHT](http://www.gutenberg.org/ebooks/15114)。他在前言里写到:

> “The design of the following treatise is to investigate the fundamental laws of those operations of the mind by which reasoning is performed; to give expression to them in the symbolical language of a Calculus, and upon this foundation to establish the science of Logic and construct its method; …”
>
> “本书论述的是,探索心智推理的基本规律;用微积分的符号语言进行表达,并在此基础上建立逻辑和构建方法的科学……”

在大学任职期间,乔治・布尔写了两本教科书,一本讲微分方程,另外一本讲差分方程,而前者,[A TREATISE ON DIFFERENTIAL EQUATIONS](https://archive.org/details/atreatiseondiff06boolgoog/page/n7),直到今天,依然难以超越。

[〔图:george-boole-1864〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/george-boole-in-1864.png?raw=true)

乔治・布尔于 1864 年因肺炎去世。

乔治・布尔在世的时候,人们并未对他的布尔代数产生什么兴趣。直到 70 年后,克劳德・香农([Claude Elwood Shannon](https://en.wikipedia.org/wiki/Claude_Shannon))发表那篇著名论文,[A SYMBOLIC ANALYSIS OF RELAY AND SWITCHING CIRCUITS](https://www.cs.virginia.edu/~evans/greatworks/shannon38.pdf) 之后,布尔代数才算是开始被大规模应用到实处。

有本书可以闲暇时间翻翻,[The Logician and the Engineer: How George Boole and Claude Shannon Created the Information Age](https://www.amazon.com/gp/product/B0091XBUTM/ref=dbs_a_def_rwt_hsch_vapi_tkin_p1_i4)。可以说,没有乔治・布尔的**布尔代数**,没有克劳德・香农的**逻辑电路**,就没有后来的计算机,就没有后来的互联网,就没有今天的信息时代 —— 世界将会怎样?

2015 年,乔治・布尔诞辰 200 周年,Google 设计了[专门的 Logo](https://www.google.com/doodles/george-booles-200th-birthday) 纪念这位为人类作出巨大贡献的自学奇才。

[〔图:george-boole-google-doodle-2015〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/george-boole-google-doodle-2015.gif?raw=true)

Google Doodle 的寄语是这样的:

> A very happy **11001000**_th_ birthday to genius George Boole!

布尔运算

从定义上来看,所谓**程序**(Programs)其实一点都不神秘。

因为程序这个东西,不过是按照一定*顺序*完成任务的**流程**(Procedures)。根据定义,日常生活中你做盘蛋炒饭给自己吃,也是完成了一个 “做蛋炒饭” 的程序 —— 你按部就班完成了一系列的步骤,最终做好了一碗蛋炒饭给自己吃 —— 从这个角度望过去,所有的菜谱都是程序……

只不过,菜谱这种程序,编写者是人,执行者还是人;而我们即将要学会写的程序,编写者是人,执行者是计算机 —— 当然,菜谱用自然语言编写,计算机程序由程序员用编程语言编写。

然而,这些都不是最重要的差异 —— 最重要的差异在于计算机能做**布尔运算**(Boolean Operations)。

于是,一旦代码编写好之后,计算机在执行的过程中,除了可以 “*按照顺序执行任务*” 之外,还可以 “*根据不同情况执行不同的任务*”,比如,“*如果条件尚未满足则重复执行某一任务*”。

计算器和计算机都是电子设备,但计算机更为强大的原因,用通俗的说法就是它 “**可编程**”(Programable)—— 而所谓可编程的核心就是*布尔运算*及其相应的**流程控制**(Control Flow);没有布尔运算能力就没有办法做*流程控制*;没有流程控制就只能 “按顺序执行”,那就显得 “很不智能”……

布尔值

在 Python 语言中,**布尔值**(Boolean Value)用 `True` 和 `False` 来表示。

**注意**:请小心区分大小写 —— 因为 Python 解释器是对大小写敏感的,对它来说,`True` 和 `true` 不是一回事。

任何一个**逻辑表达式**都会返回一个*布尔值*。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
# 请暂时忽略以上两行……

1 == 2
1 != 2
```

False
True

`1 == 2`,用自然语言描述就是 “_1 等于 2 吗?_” —— 它的布尔值当然是 `False`。

`1 != 2`,用自然语言描述就是 “_1 不等于 2 吗?_” —— 它的布尔值当然是 `True`。

**注意**:自然语言中的 “*等于*”,在 Python 编程语言中,使用的符号是 `==`,**不是一个等号!**

**请再次注意**:单个等号 `=`,有其他的用处。初学者最不适应的就是,在编程语言里所使用的操作符,与他们之前在其他地方已经习惯了的使用方法并不相同 —— 不过,适应一段时间就好了。

逻辑操作符

Python 语言中的**逻辑操作符**(Logical Operators)如下表所示 —— 为了理解方便,也可以将其称为 “*比较操作符*”。

| 比较操作符 | 意义 | 示例 | 布尔值 |
| ---------- | -------- | ---------------- | ------- |
| `==` | 等于 | `1 == 2` | `False` |
| `!=` | 不等于 | `1 != 2` | `True` |
| `>` | 大于 | `1 > 2` | `False` |
| `>=` | 大于等于 | `1 >= 1` | `True` |
| `<` | 小于 | `1 < 2` | `True` |
| `<=` | 小于等于 | `1 <= 2` | `True` |
| `in` | 属于 | `'a' in 'basic'` | `True` |

除了等于、大于、小于之外,Python 还有一个逻辑操作符,`in`:

这个表达式 `'a' in 'basic'` 用自然语言描述就是:

> “`'a'` 存在于 `'basic'` 这个字符串之中吗?”(属于关系)

布尔运算操作符

以上的例子中,逻辑操作符的**运算对象**(Operands)是数字值和字符串值。

而针对布尔值进行运算的操作符很简单,只有三种:与、或、非:

> 分别用 `and`、`or`、`not` 表示

**注意**:它们全部是小写。因为布尔值只有两个,所以布尔运算结果只有几种而已,如下图所示:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/boolean-operators.png?raw=true)

先别管以下代码中 `print()` 这个函数的工作原理,现在只需要关注其中布尔运算的结果:

```python
print('(True and False) yields:', True and False)
print('(True and True) yields:', True and True)
print('(False and True) yields:', False and True)
print('(True or False) yields:', True or False)
print('(False or True) yields:', False or True)
print('(False or False) yields:', False or False)
print('(not True) yields:', not True)
print('(not False) yields:', not False)
```

(True and False) yields: False
(True and True) yields: True
(False and True) yields: False
(True or False) yields: True
(False or True) yields: True
(False or False) yields: False
(not True) yields: False
(not False) yields: True

千万不要误以为布尔运算是*理科生*才必须会、才能用得上的东西…… 文理艺分科是中国的特殊分类方式,真挺害人的。比如,设计师们在计算机上创作图像的时候,也要频繁使用*或与非*的布尔运算操作才能完成各种图案的拼接…… 抽空看看这个网页:[Boolean Operations used by Sketch App](https://sketchapp.com/docs/shapes/boolean-operations/) —— 这类设计软件,到最后是每个人都用得上的东西呢。另,难道艺术生不需要学习文科或者理科?—— 事实上,他们也有文化课……

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/[email protected]?raw=true)

流程控制

有了布尔运算能力之后,才有*根据情况决定流程*的所谓**流程控制**(Control Flow)的能力。

```python
import random
r = random.randrange(1, 1000)
# 请暂时忽略以上两句的原理,只需要了解其结果:
# 引入随机数,而后,每次执行的时候,r 的值不同

if r % 2 == 0:
print(r, 'is even.')
else:
print(r, 'is odd.')
```

693 is odd.

你可以多执行几次以上程序,看看每次不同的执行结果。执行方法是,选中上面的 Cell 之后按快捷键 `shift + enter`。

现在看代码,先忽略其它的部分,只看关键部分:

```python
...
if r % 2 == 0:
...
else:
...
```

这个 `if/else` 语句,完成了流程的**分支**功能。`%` 是计算余数的符号,如果 `r` 除以 `2` 的余数等于 `0`,那么它就是偶数,否则,它就是奇数 —— 写成布尔表达式,就是 `r % 2 == 0`。

这一次,你看到了单个等号 `=`:`r = random.randrange(1, 1000)`。

这个符号在绝大多数编程语言中都是 “**赋值**”(Assignment)的含义。

在 `r = 2` 之中,`r` 是一个名称为 `r` 的**变量**(Variable)—— 现在只需要将变量理解为程序*保存数值的地方*;而 `=` 是赋值符号,`2` 是一个整数**常量**(Literal)。

语句 `r = 2` 用自然语言描述就是:

> “把 `2` 这个值保存到名称为 `r` 的变量之中”。

现在先别在意头两行代码的工作原理,只关注它的工作结果:`random.randrange(1, 1000)` 这部分代码的作用是返回一个 _1 到 1000 之间_(含左侧 `1` 但不含右侧 `1000`)的随机整数。每次执行以上的程序,它就生成一个新的随机整数,然后因为 `=` 的存在,这个数就被保存到 `r` 这个变量之中。

计算机程序的所谓 “智能”(起码相对于计算器),首先是因为它能做*布尔运算*。计算机的另外一个好处是 “不知疲倦”(反正它也不用自己交电费),所以,它最擅长处理的就是 “重复”,这个词在程序语言中,术语是**循环**(Loop)。以下程序会打印出 `10` 以内的所有奇数:

```python
for i in range(10):
if i % 2 != 0:
print(i)
```

1
3
5
7
9

其中 `range(10)` 的返回值,是 `0~9` 的整数序列(默认起始值是 `0`;含左侧 `0`,不含右侧 `10`)。

用自然语言描述以上的程序,大概是这样的 —— 自然语言写在 `#` 之后:

```python
for i in range(10): # 对于 0~9 中的所有数字都带入 i 这个变量,执行一遍以下任务:
if i % 2 != 0: # 如果 i 除以 2 的余数不等于零的话,执行下面的语句:
print(i) # 向屏幕输出 i 这个变量中所保存的值
```

就算你让它打印出一百亿以内的奇数,它也毫不含糊 —— 你只需要在 `range()` 这个函数的括号里写上一个那么大的整数就行……

让它干一点稍微复杂的事吧,比如,我们想要打印出 `100` 以内所有的*质数*(Primes)。

根据质数的定义,它大于等于 `2`,且只有在被它自身或者 `1` 做为除数时余数为 `0`。判断一个数字是否是质数的**算法**是这样的:

> * 设 `n` 为整数,`n >= 2`;
> * 若 `n == 2`,`n` 是质数;
> * 若 `n > 2`,就把 `n` 作为被除数,从 `2` 开始一直到 `n - 1` 都作为除数,*逐一*计算看看余数是否等于 `0`?
>> * 如果是,那就不用接着算了,它不是质数;
>> * 如果全部都试过了,余数都不是 `0`,那么它是质数。

于是,你需要两个**嵌套**的循环,第一个是负责让被除数 `n` 从 `2` **遍历**(就是依次经历一次)到 `99`(题目是 `100` 以内,所以不包含 `100`)的循环,第二个是在前者内部负责让除数 `i` 从 `2` 遍历到 `n - 1` 的循环:

```python
for n in range(2, 100): #range(2,100)表示含左侧 2,不含右侧 100,是不是第三次看到这个说法了?
if n == 2:
print(n)
continue
for i in range(2, n):
if (n % i) == 0:
break
else: # 这里目前你可能看不懂…… 但先关注结果吧。
print(n)
```

2
3
5
7
11
13
17
19
23
29
31
37
41
43
47
53
59
61
67
71
73
79
83
89
97

所谓算法

以上的**算法**可以改进(程序员们经常用的词汇是 “*优化*”)<a id='plusone'></a>:

> 从 `2` 作为除数开始试,试到 <sup>2</sup>&radic;<span style="text-decoration: overline">n</span> 之后的一个整数就可以了……

```python
for n in range(2, 100):
if n == 2:
print(n)
continue
for i in range(2, int(n ** 0.5)+1): #为什么要 +1 以后再说…… n 的 1/2 次方,相当于根号 n。
if (n % i) == 0:
break
else:
print(n)
```

2
3
5
7
11
13
17
19
23
29
31
37
41
43
47
53
59
61
67
71
73
79
83
89
97

你看,寻找更有效的算法,或者说,不断优化程序,提高效率,最终是程序员的工作,不是编程语言本身的工作。关于判断质数最快的算法,[可以看 Stackoverflow 上的讨论](https://stackoverflow.com/questions/1801391/what-is-the-best-algorithm-for-checking-if-a-number-is-prime),有更多时间也可以翻翻 [Wikipedia](https://en.wikipedia.org/wiki/Generating_primes)。

到最后,**所有的工具都一样,效用取决于使用它的人**。所以,学会使用工具固然重要,更为重要的是与此同时自己的能力必须不断提高。

虽然写代码这事刚开始学起来好像门槛很高,那只不过是幻觉,其实门槛比它更高的多的去了。到最后,它就是个最基础的工具,还是得靠思考能力 —— 这就好像识字其实挺难的 —— 小学初中高中加起来十来年,我们才掌握了基本的阅读能力;可最终,即便是本科毕业、研究生毕业,真的能写出一手好文章的人还是少之又少一样 —— 因为用文字值得写出来的是思想,用代码值得写出来的是创造,或者起码是有意义的问题的有效解决方案。有思想,能解决问题,是另外一门手艺 —— 需要终生精进的手艺。

所谓函数

我们已经反复见过 `print()` 这个**函数**(Functions)了。它的作用很简单,就是把传递给它的值输出到屏幕上 —— 当然,事实上它的使用细节也很多,以后慢慢讲。

现在,最重要的是初步理解一个函数的基本构成。关于**函数**,相关的概念有:*函数名*(Function Name)、*参数*(Parameters)、*返回值*(Return Value)、*调用*(Call)。

拿一个更为简单的函数作为例子,`abs()`。它的作用很简单:接收一个数字作为参数,经过运算,返回该数字的绝对值。

```python
a = abs(-3.1415926)
a
```

3.1415926

在以上的代码的第 1 行中,

> * 我们*调用*了一个*函数名*为 `abs` 的函数;写法是 `abs(-3.1415926)`;
> * 这么写,就相当于向它*传递*了一个*参数*,其值为:`-3.1415926`;
> * 该函数接收到这个参数之后,根据这个参数的*值*在函数内部进行了*运算*;
> * 而后该函数返回了一个值,*返回值*为之前接收到的参数的值的绝对值 `3.1415926`;
> * 而后这个*值*被保存到*变量* `a` 之中。

从结构上来看,每个函数都是一个完整的程序,因为一个程序,核心构成部分就是*输入*、*处理*、*输出*:

> * 它有输入 —— 即,它能接收外部通过参数传递的值;
> * 它有处理 —— 即,内部有能够完成某一特定任务的代码;尤其是,它可以根据 “输入” 得到 “输出”;
> * 它有输出 —— 即,它能向外部输送返回值……

被调用的函数,也可以被理解为**子程序**(Sub-Program)—— 主程序执行到函数调用时,就开始执行实现函数的那些代码,而后再返回主程序……

我们可以把判断一个数字是否是质数的过程,写成函数,以便将来在多处用得着的时候,随时可以调用它:

```python
def is_prime(n): # 定义 is_prime(),接收一个参数
if n < 2: # 开始使用接收到的那个参数(值)开始计算……
return False # 不再是返回给人,而是返回给调用它的代码……
if n == 2:
return True
for m in range(2, int(n**0.5)+1):
if (n % m) == 0:
return False
else:
return True

for i in range(80, 110):
if is_prime(i): # 调用 is_prime() 函数,
print(i) # 如果返回值为 True,则向屏幕输出 i
```

83
89
97
101
103
107
109

细节补充

语句

一个完整的程序,由一个或者多个**语句**(Statements)构成。通常情况下,建议每一行只写一条语句。

```python
for i in range(10):
if i % 2 != 0:
print(i)
```

1
3
5
7
9

语句块

在 Python 语言中,**行首空白**(Leading whitespace,由空格 `' '` 或者 Tab `⇥` 构成)有着特殊的含义。

如果有行首空白存在,那么,Python 将认为这一行与其他邻近有着相同行首空白的语句同属于一个**语句块** —— 而一个语句块必然由一个行末带有冒号 `:` 的语句起始。同属于一个语句块中的语句,行首空白数量应该相等。这看起来很麻烦,可实际上,程序员一般都使用专门的文本编辑器,比如 [Visual Studio Code](https://code.visualstudio.com),其中有很多的辅助工具,可以让你很方便地输入具备一致性的行首空白。

以上程序,一共三个语句,两个语句块,一个 `for` 循环*语句块*中包含着一个 `if` 条件*语句块*。注意第一行和第二行末尾的冒号 `:`。

在很多其他的语言中,比如,JavaScript,用大括号 `{}` 作为语句块标示 —— 这是 Python 比较特殊的地方,它组织语句块的方式如下图所示:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/python-leading-space.png?raw=true)

>**注意**:在同一个文件里,不建议混合使用 Tab 和 Space;要么全用空格,要么全用制表符。

注释

在 Python 程序中可以用 `#` 符号标示**注释**语句。

所谓的注释语句,就是程序文件里写给人看而不是写给计算机看的部分。本节中的代码里就带着很多的注释。

人写的 Python 语言代码,要被 Python **解释器**翻译成机器语言,而后才能让计算机 “读懂”,随后计算机才可以按照指令执行。解释器在编译程序的过程中,遇到 `#` 符号,就会忽略其后的部分(包括这个注释符号)。

操作符

在本节,我们见到的比较操作符可以比较它左右的值,而后返回一个布尔值。

我们也见过两个整数被**操作符** `%` 连接,左侧作为被除数,右侧作为除数,`11 % 3` 这个表达式的值是 `2`。对于数字,我们可用的操作符有 `+`、`-`、`*`、`/`、`//`、`%`、`**` —— 它们分别代表加、减、乘、除、商、余、幂。

赋值符号与操作符的连用

你已经知道变量是什么了,也已经知道赋值是什么了。于是,你看到 `x = 1` 就明白了,这是为 `x` 赋值,把 `1` 这个值保存到变量 `x` 之中去。

但是,若是你看到 ` x += 1`,就迷惑了,这是什么意思呢?

这只是编程语言中的一种惯用法。它相当于 `x = x + 1`。

看到 `x = x + 1` 依然会困惑…… 之所以困惑,是因为你还没有习惯把单等号 `=` 当作赋值符号,把双等号 `==` 当作逻辑判断的 “等于”。

`x = x + 1` 的意思是说,把表达式 `x + 1` 的值保存到变量 `x` 中去 —— 如此这般之后,`x` 这个变量中所保存的就不再是原来的值了……

```python
x = 0
x += 1
print(x)
```

1

其实不难理解,只要习惯了就好。理论上,加减乘除商余幂这些操作符,都可以与赋值符号并用。

```python
x = 11
x %= 3 # x = x % 3
print(x)
```

2

总结

以下是这一章中所提到的重要概念。了解它们以及它们之间的关系,是进行下一步的基础。

> * 数据:整数、布尔值;操作符;变量、赋值;表达式
> * 函数、子程序、参数、返回值、调用
> * 流程控制、分支、循环
> * 算法、优化
> * 程序:语句、注释、语句块
> * 输入、处理、输出
> * 解释器

你可能已经注意到了,这一章的小节名称罗列出来的话,看起来像是一本编程书籍的目录 —— 只不过是概念讲解顺序不同而已。事实上还真的就是那么回事。

这些概念,基本上都是**独立于**某一种编程语言的(Language Independent),无论将来你学习哪一种编程语言,不管是 C++,还是 JavaScript,抑或是 Golang,这些概念都在那里。

学会一门编程语言之后,再学其它的就会容易很多 —— 而且,当你学会了其中一个之后,早晚你会顺手学其它的,为了更高效使用微软办公套件,你可能会花上一两天时间研究一下 VBA;为了给自己做个网页什么的,你会顺手学会 JavaScript;为了修改某个编辑器插件,你发现人家是用 Ruby 写的,大致读读官方文档,你就可以下手用 Ruby 语言了;为了搞搞数据可视化,你会发现不学会 R 语言有点不方便……

你把这些概念装在脑子里,而后就会发现几乎所有的编程入门教学书籍结构都差不多是由这些概念构成的。因为,所有的编程语言基础都一样,所有的编程语言都是我们指挥计算机的工具。无论怎样,反正都需要输入输出,无论什么语言,不可能没有布尔运算,不可能没有流程控制,不可能没有函数,只要是高级语言,就都需要编译器…… 所以,掌握这些基本概念,是将来持续学习的基础。

-----
**脚注**

<a name='fn1'>[1]</a>:对于自学能力强、有很多自学经验的人来说,速成往往真的是可能、可行的。因为他们已经积累的知识与经验会在习得新技能时发挥巨大的作用,乃至于他们看起来相对别人花极少的时间就能完成整个自学任务。也就是说,将来的那个已经习得自学能力、且自学能力已经磨练得很强的你,常常真的可以做到在别人眼里 “速成”。

<a href='#fn1b'><small>↑Back to Content↑</small></a>

值及其相应的运算

从结构上来看,一切的计算机程序,都由且只由两个最基本的成分构成:

> * **运算**(Evaluation)
> * **流程控制**(Control Flow)

没有流程控制的是计算器而已;有流程控制的才是可编程设备。

看看之前我们见过的计算质数的程序:(按一下 `⎋`,即 `ESC`,确保已经进入命令模式,`⇧ L` 可以切换是否显示代码行号)

```python
def is_prime(n): # 定义 is_prime(),接收一个参数
if n < 2: # 开始使用接收到的那个参数(值)开始计算……
return False # 不再是返回给人,而是返回给调用它的代码……
if n == 2:
return True
for m in range(2, int(n**0.5)+1):
if (n % m) == 0:
return False
else:
return True

for i in range(80, 110):
if is_prime(i): # 调用 is_prime() 函数,
print(i) # 如果返回值为 True,则向屏幕输出 i
```

83
89
97
101
103
107
109

`if...`,`for...` 在控制流程:在什么情况下运算什么,在什么情况下重复运算什么;

第 13 行 `is_prime()` 这个函数的调用,也是在控制流程 —— 所以我们可以**把函数看作是 “子程序”**;

一旦这个函数被调用,流程就转向开始执行在第 1 行中定义的 `is_prime()` 函数内部的代码,而这段代码内部还是*计算*和*流程控制*,决定一个返回值 —— 返回值是布尔值;再回到第 13 行,将返回值交给 `if` 判断,决定是否执行第 14 行……

而计算机这种可编程设备之所以可以做流程控制,是因为它可以做**布尔运算**,即,它可以*对布尔值*进行*操作*,而后将布尔值交给*分支*和*循环*语句,构成了程序中的流程控制。

值

从本质上看,程序里的绝大多数语句包含着**运算**(Evaluation),即,在对某个值进行**评价**。这里的 “评价”,不是 “判断某人某事的好坏”,而是 “*计算出某个值究竟是什么*” —— 所以,我们用中文的 “**运算**” 翻译这个 “_Evaluation_” 可能表达得更准确一些。

在程序中,被运算的可分为**常量**(Literals)和**变量**(Variables)。

```python
a = 1 + 2 * 3
a += 1
print(a)
```

在以上代码中,

`1`、`2`、`3`,都是**常量**。_Literal_ 的意思是 “字面的”,顾名思义,常量的*值*就是它字面上的值。`1` 的值,就是 `1`。

`a` 是**变量**。顾名思义,它的值将来是可变的。比如,在第 2 句中,这个变量的*值*发生了改变,之前是 `7`,之后变成了 `8`。

第 1 句中的 `+`、`*`,是**操作符**(Operators),它用来对其左右的值进行相应的*运算*而后得到一个值。先是由操作符 `*` 对 `2` 和 `3` 进行运算,
生成一个值,`6`;然后再由操作符 `+` 对 `1` 和 `6` 进行运算,生成一个值 `7`。先算乘除后算加减,这是操作符的**优先级**决定的。

`=` 是赋值符号,它的作用是将它右边的值保存到左边的变量中。

*值*是程序的基础成分(Building blocks),它就好像盖房子用的砖块一样,无论什么样的房子,到最后都主要是由砖块构成。

*常量*,当然有个*值* —— 就是它们字面所表达的值。

*变量*必须先*赋值*才能使用,也就是说,要先把一个*值*保存到变量中,它才能在其后被运算。

在 Python 中每个函数都有*返回值*,即便你在定义一个函数的时候没有设定返回值,它也会加上默认的返回值 `None`……(请注意 `None` 的大小写!)

```python
def f():
pass
print(f()) # 输出 f() 这个函数被调用后的返回值,None
print(print(f())) # 这一行最外围的 print() 调用了一次 print(f()),所以输出一个 None,
# 而后再输出这次调用的返回值,所以又输出一次 None
```

None
None
None

当我们调用一个函数的时候,本质上来看,就相当于:

> 我们把一个值交给某个函数,请函数根据它内部的运算和流程控制对其进行操作而后返回另外一个值。

比如,`abs()` 函数,就会返回传递给它的*值*的*绝对值*;`int()` 函数,会将传递给它的值的小数部分砍掉;`float()` 接到整数参数之后,会返回这个整数的浮点数形式:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

abs(-3.14159)
int(abs(-3.14159))
float(int(abs(-3.14159)))
```

3.14159
3
3.0

值的类型

在编程语言中,总是包含最基本的三种数据类型:

> * 布尔值(Boolean Value)
> * 数字(Numbers):整数(Int)、浮点数(Float)、复数(Complex Numbers)
> * 字符串(Strings)

既然有不同类型的数据,它们就分别对应着不同类型的值。

运算的一个默认法则就是,通常情况下应该是*相同类型的值才能相互运算*。

显然,数字与数字之间的运算是合理的,但你让 `+` 这个操作符对一个字符串和一个数字进行运算就不行:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

11 + 10 - 9 * 8 / 7 // 6 % 5
'3.14' + 3 # 这一句会报错
```

20.0

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

<ipython-input-18-e922b7565e53> in <module>
3
4 11 + 10 - 9 * 8 / 7 // 6 % 5
----> 5 '3.14' + 3 # 这一句会报错

TypeError: can only concatenate str (not "int") to str

所以,在不得不对不同类型的值进行运算之前,总是要事先做 **Type Casting**(类型转换)。比如,

> * 将字符串转换为数字用 `int()`、`float()`;
> * 将数字转换成字符串用 `str()`;

另外,即便是在数字之间进行计算的时候,有时也需要将整数转换成浮点数字,或者反之:
> * 将整数转换成浮点数字用 `float()`;
> * 将浮点数字转换成整数用 `int()`;

有个函数,`type()`,可以用来查看某个值属于什么类型:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

type(3)
type(3.0)
type('3.14')
type(True)
type(range(10))
type([1,2,3])
type((1,2,3))
type({1,2,3})
type({'a':1, 'b':2, 'c':3})
```

int
float
str
bool
range
list
tuple
set
dict

操作符

针对不同类型的数据,有各自专用的**操作符**。

数值操作符

针对数字进行计算的操作符有加减乘除商余幂:`+`、`-`、`*`、`/`、`//`、`%`、`**`。

其中 `+` 和 `-` 可以对单个值进行操作,`-3`;其它的操作符需要有两个值才能操作。

从优先级来看,这些操作符中:

> * 对两个值进行操作的 `+`、`-` 的优先级最低;
> * 稍高的是 `*`、`/`、`//`、`%`;
> * 更高的是对单个值进行操作的 `+`、`-`;
> * 优先级最高的是 `**`。

完整的操作符优先级列表,参见官方文档:

> https://docs.python.org/3/reference/expressions.html#operator-precedence

布尔值操作符

针对布尔值,操作符有`与`、`或`、`非`:`and`、`or`、`not`。

它们之中,优先级最低的是或 `or`,然后是与 `and`, 优先级最高的是非 `not`:

```python
True and False or not True
```

False

最先操作的是 `not`,因为它优先级最高。所以,上面的表达式相当于 `True and False or (not True)`,即相当于 `True and False or False`;

然后是 `and`,所以,`True and False or False` 相当于是 `(True and False) or False`,即相当于 `False or False`;

于是,最终的值是 `False`。

逻辑操作符

数值之间还可以使用逻辑操作符,`1 > 2` 返回布尔值 `False`。逻辑操作符有:`<`(小于)、`<=`(小于等于)、`>`(大于)、`>=`(大于等于)、`!=`(不等于)、`==`(等于)。

逻辑操作符的优先级,高于布尔值的操作符,低于数值计算的操作符。
即:数值计算的操作符优先级最高,其次是逻辑操作符,布尔值的操作符优先级最低。

```python
n = -95
n < 0 and (n + 1) % 2 == 0
```

True

字符串操作符

针对字符串,有三种操作:

> * 拼接:`+` 和 `' '`(后者是空格)
> * 拷贝:`*`
> * 逻辑运算:`in`、`not in`;以及,`<`、`<=`、`>`、`>=`、`!=`、`==`

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

'Awesome' + 'Python'
'Awesome' 'Python'
'Python, ' + 'Awesome! ' * 3
'o' in 'Awesome' and 'o' not in 'Python'
```

'AwesomePython'
'AwesomePython'
'Python, Awesome! Awesome! Awesome! '
False

字符之间,字符串之间,除了 `==` 和 `!=` 之外,也都可以被逻辑操作符 `<`、`<=`、`>`、`>=` 运算:

```python
'a' < 'b'
```

True

这是因为字符对应着 Unicode 码,字符在被比较的时候,被比较的是对应的 Unicode 码。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

'A' > 'a'
ord('A')
ord('a')
```

False
65
97

当字符串被比较的时候,将从两个字符串各自的第一个字符开始逐个比较,“一旦决出胜负马上停止”:

```python
'PYTHON' > 'Python 3'
```

False

列表的操作符

数字和字符串(由字符构成的序列)是最基本的数据类型,而我们往往需要批量处理数字和字符串,这样的时候,我们需要**数组**(Array)。不过,在 Python 语言中,它提供了一个**容器**(Container)的概念,用来容纳批量的数据。

Python 的容器有很多种 —— 字符串,其实也是容器的一种,它的里面容纳着批量的字符。

我们先简单接触一下另外一种容器:**列表**(List)。

列表的标示,用方括号 `[]`;举例来说,`[1, 2, 3, 4, 5]` 和 `['ann', 'bob', 'cindy', 'dude', 'eric']`,或者 `['a', 2, 'b', 32, 22, 12]` 都是一个列表。

因为列表和字符串一样,都是*有序容器*(容器还有另外一种是无序容器),所以,它们可用的操作符其实相同:

> * 拼接:`+` 和 `' '`(后者是空格)
> * 拷贝:`*`
> * 逻辑运算:`in`、`not in`;以及,`<`、`<=`、`>`、`>=`、`!=`、`==`

两个列表在比较时(前提是两个列表中的数据元素类型相同),遵循的还是跟字符串比较相同的规则:“一旦决出胜负马上停止”。但实际上,由于列表中可以包含不同类型的元素,所以,通常情况下没有实际需求对他们进行 “大于、小于” 的比较。(比较时,类型不同会引发 `TypeError`……)

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

a_list = [1, 2, 3, 4, 5]
b_list = [1, 2, 3, 5]
c_list = ['ann', 'bob', 'cindy', 'dude', 'eric']
a_list > b_list
10 not in a_list
'ann' in c_list
```

False
True
True

更复杂的运算

对于数字进行加、减、乘、除、商、余、幂的操作,对于字符串进行拼接、拷贝、属于的操作,对布尔值进行或、与、非的操作,这些都是相对简单的运算。

更为复杂一点的,我们要通过调用函数来完成 —— 因为在函数内部,我们可以用比 “单个表达式” 更为复杂的程序针对传递进来的参数进行运算。换言之,函数就相当于各种事先写好的子程序,给它传递一个值,它会对其进行运算,而后返回一个值(最起码返回一个 `None`)。

以下是 Python 语言所有的内建函数([Built-in Functions](https://docs.python.org/3/library/functions.html)):

| ( | Python | Built-in | Functions | ) |
| --- | --- | --- | --- | --- |
| [abs()](https://docs.python.org/3/library/functions.html#abs) | [delattr()](https://docs.python.org/3/library/functions.html#delattr) | [hash()](https://docs.python.org/3/library/functions.html#hash) | [memoryview()](https://docs.python.org/3/library/functions.html#func-memoryview) | [set()](https://docs.python.org/3/library/functions.html#func-set) |
| [all()](https://docs.python.org/3/library/functions.html#all) | [dict()](https://docs.python.org/3/library/functions.html#func-dict) | [help()](https://docs.python.org/3/library/functions.html#help) | [min()](https://docs.python.org/3/library/functions.html#min) | [setattr()](https://docs.python.org/3/library/functions.html#setattr) |
| [any()](https://docs.python.org/3/library/functions.html#any) | [dir()](https://docs.python.org/3/library/functions.html#dir) | [hex()](https://docs.python.org/3/library/functions.html#hex) | [next()](https://docs.python.org/3/library/functions.html#next) | [slice()](https://docs.python.org/3/library/functions.html#slice) |
| [ascii()](https://docs.python.org/3/library/functions.html#ascii) | [divmod()](https://docs.python.org/3/library/functions.html#divmod) | [id()](https://docs.python.org/3/library/functions.html#id) | [object()](https://docs.python.org/3/library/functions.html#object) | [sorted()](https://docs.python.org/3/library/functions.html#sorted) |
| [bin()](https://docs.python.org/3/library/functions.html#bin) | [enumerate()](https://docs.python.org/3/library/functions.html#enumerate) | [input()](https://docs.python.org/3/library/functions.html#input) | [oct()](https://docs.python.org/3/library/functions.html#oct) | [staticmethod()](https://docs.python.org/3/library/functions.html#staticmethod) |
| [bool()](https://docs.python.org/3/library/functions.html#bool) | [eval()](https://docs.python.org/3/library/functions.html#eval) | [int()](https://docs.python.org/3/library/functions.html#int) | [open()](https://docs.python.org/3/library/functions.html#open) | [str()](https://docs.python.org/3/library/functions.html#func-str) |
| [breakpoint()](https://docs.python.org/3/library/functions.html#breakpoint) | [exec()](https://docs.python.org/3/library/functions.html#exec) | [isinstance()](https://docs.python.org/3/library/functions.html#isinstance) | [ord()](https://docs.python.org/3/library/functions.html#ord) | [sum()](https://docs.python.org/3/library/functions.html#sum) |
| [bytearray()](https://docs.python.org/3/library/functions.html#func-bytearray) | [filter()](https://docs.python.org/3/library/functions.html#filter) | [issubclass()](https://docs.python.org/3/library/functions.html#issubclass) | [pow()](https://docs.python.org/3/library/functions.html#pow) | [super()](https://docs.python.org/3/library/functions.html#super) |
| [bytes()](https://docs.python.org/3/library/functions.html#func-bytes) | [float()](https://docs.python.org/3/library/functions.html#float) | [iter()](https://docs.python.org/3/library/functions.html#iter) | [print()](https://docs.python.org/3/library/functions.html#print) | [tuple()](https://docs.python.org/3/library/functions.html#func-tuple) |
| [callable()](https://docs.python.org/3/library/functions.html#callable) | [format()](https://docs.python.org/3/library/functions.html#format) | [len()](https://docs.python.org/3/library/functions.html#len) | [property()](https://docs.python.org/3/library/functions.html#property) | [type()](https://docs.python.org/3/library/functions.html#type) |
| [chr()](https://docs.python.org/3/library/functions.html#chr) | [frozenset()](https://docs.python.org/3/library/functions.html#func-frozenset) | [list()](https://docs.python.org/3/library/functions.html#func-list) | [range()](https://docs.python.org/3/library/functions.html#func-range) | [vars()](https://docs.python.org/3/library/functions.html#vars) |
| [classmethod()](https://docs.python.org/3/library/functions.html#classmethod) | [getattr()](https://docs.python.org/3/library/functions.html#getattr) | [locals()](https://docs.python.org/3/library/functions.html#locals) | [repr()](https://docs.python.org/3/library/functions.html#repr) | [zip()](https://docs.python.org/3/library/functions.html#zip) |
| [compile()](https://docs.python.org/3/library/functions.html#compile) | [globals()](https://docs.python.org/3/library/functions.html#globals) | [map()](https://docs.python.org/3/library/functions.html#map) | [reversed()](https://docs.python.org/3/library/functions.html#reversed) | [__import__()](https://docs.python.org/3/library/functions.html#__import__) |
| [complex()](https://docs.python.org/3/library/functions.html#complex) | [hasattr()](https://docs.python.org/3/library/functions.html#hasattr) | [max()](https://docs.python.org/3/library/functions.html#max) | [round()](https://docs.python.org/3/library/functions.html#round) | |

现在倒不用着急一下子全部了解它们 —— 反正早晚都会的。

这其中,针对数字,有计算绝对值的函数 `abs()`,有计算商余的函数 `divmod()` 等等。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

abs(-3.1415926)
divmod(11, 3)
```

3.1415926
(3, 2)

这些内建函数也依然只能完成 “基本操作”,比如,对于数字,我们想计算三角函数的话,内建函数就帮不上忙了,于是,我们需要调用标准库(Standard Library)中的 math 模块(Module):

```python
import math
math.sin(5)
```

-0.9589242746631385

代码 `math.sin(5)` 这里的 `.`,也可以被理解为 “操作符”,它的作用是:

> 从其它模块中调用函数。

代码 `math.sin(5)` 的作用是:

> 把 `5` 这个值,传递给 `math` 这个模块里的 `sin()` 函数,让 `sin()` 根据它内部的代码对这个值进行运算,而后返回一个值(即,计算结果)。

类(Class)中定义的函数,也可以这样被调用 —— 虽然你还不明白类(Class)究竟是什么,但从结构上很容易理解,它实际上也是保存在其他文件中的一段代码,于是,那段代码内部定义的函数,也可以这样调用。

比如,数字,其实属于一个类,所以,我们可以调用那个类里所定义的函数,比如,`float.as_integer_ratio()`,它将返回两个值,第一个值除以第二个值,恰好等于传递给它的那个浮点数字参数:

```python
3.1415926.as_integer_ratio()
```

(3537118815677477, 1125899906842624)

关于布尔值的补充

当你看到以下这样的表达式,而后再看看它的结果,你可能会多少有点迷惑:

```python
True or 'Python'
```

True

这是因为 Python 将 `True` 定义为:

> By default, an object is considered true unless its class defines either a \_\_bool\_\_() method that returns `False` or a \_\_len\_\_() method that returns zero, when called with the object.
>
> https://docs.python.org/3/library/stdtypes.html#truth-value-testing

这一段文字,初学者是看不懂的。但下一段就好理解了:

> Here are most of the built-in objects considered `False`:
>
> > * constants defined to be false: `None` and `False`.
> > * zero of any numeric type: `0`, `0.0`, `0j`, `Decimal(0)`, `Fraction(0, 1)`
> > * empty sequences and collections: `''`, `()`, `[]`, `{}`, `set()`, `range(0)`

所以,`'Python'` 是个非空的字符串,即,不属于是 `empty sequences`,所以它不被认为是 `False`,即,它的布尔值是 `True`

于是,这么理解就轻松了:

> 每个变量或者常量,除了它们的值之外,同时还相当于有一个对应的布尔值。

关于值的类型的补充

除了数字、布尔值、字符串,以及上一小节介绍的列表之外,还有若干数据类型,比如 `range()`(等差数列)、`tuple`(元组)、`set`(集合)、`dictionary`(字典),再比如 `Date Type`(日期)等等。

它们都是基础数据类型的各种组合 —— 现实生活中,更多需要的是把基础类型组合起来构成的数据。比如,一个通讯簿,里面是一系列字符串分别对应着若干字符串和数字。

``` python
entry[3662] = {
'first_name': 'Michael',
'last_name': 'Willington',
'birth_day': '12/07/1992',
'mobile': {
'+714612234',
'+716253923'
}
'id': 3662,
...
}
```

针对不同的类型,都有相对应的操作符,可以对其进行运算。

这些类型之间有时也有不得不相互运算的需求,于是,在相互运算之前同样要 _Type Casting_,比如将 List 转换为 Set,或者反之:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

a = [1, 2, 3, 4, 5, 6, 7]
b = set(a)
c = list(b)
a
b
c
```

[1, 2, 3, 4, 5, 6, 7]
{1, 2, 3, 4, 5, 6, 7}
[1, 2, 3, 4, 5, 6, 7]

总结

回到最开始:从结构上来看,一切的计算机程序,都由且只由两个最基本的成分构成:

> * **运算**(Evaluation)
> * **流程控制**(Control Flow)

这一章主要介绍了基础数据类型的运算细节。而除了基础数据类型,我们需要由它们组合起来的更多复杂数据类型。但无论数据的类型是什么,被操作符操作的总是该数据的**值**。所以,虽然绝大多数编程书籍按照惯例会讲解 “数据类型”,但为了究其本质,我们在这里关注的是 “值的类型”。虽然只是关注焦点上的一点点转换,但实践证明,这一点点的不同,对初学者更清楚地把握知识点有巨大的帮助。

针对每一种*值*的类型,无论简单复杂,都有相应的操作方式:

> * **操作符**
> * 值运算
> * 逻辑运算
> * **函数**
> * 内建函数
> * 其他模块里的函数
> * 其本身所属类之中所定义的函数

所以,接下来要学习的,无非就是熟悉各种*数据类型*,及其相应的操作,包括能对它们的*值*进行操作的操作符和函数;无论是操作符还是函数,最终都会返回一个相应的**值**,及其相应的*布尔值* —— 这么看来,编程知识结构没多复杂。因为换句话讲,

> 接下来你要学习的无非是各种*数据类型*的*运算*而已。

另外,虽然现在尚未来得及对**函数**进行深入讲解,但最终你会发现它跟操作符一样,在程序里无所不在。

备注

另外,以下几个链接先放在这里,未来你会返回来参考它们,还是不断地参考它们:

> * 关于表达式:https://docs.python.org/3/reference/expressions.html
> * 关于所有操作的优先级:https://docs.python.org/3/reference/expressions.html#operator-precedence
> * 上一条链接不懂 BNF 的话根本读不懂:https://en.wikipedia.org/wiki/Backus-Naur_form
> * Python 的内建函数:https://docs.python.org/3/library/functions.html
> * Python 的标准数据类型:https://docs.python.org/3/library/stdtypes.html

另外,其实所有的操作符,在 Python 内部也是调用函数完成的……

> https://docs.python.org/3.7/library/operator.html

流程控制

在相对深入了解了值的基本操作之后,我们需要再返回来对流程控制做更深入的了解。

之前我们看过这个寻找质数的程序:

```python
for n in range(2, 100):
if n == 2:
print(n)
continue
for i in range(2, n):
if (n % i) == 0:
break
else:
print(n)
```

这其中,包含了*分支*与*循环* —— 无论多复杂的流程控制用这两个东西就够了,就好像无论多复杂的电路最终都是由通路和开路仅仅两个状态构成的一样。

> 今天的人们觉得这是 “天经地义” 的事情,可实际上并非如此。这是 1966 年的一篇论文所带来的巨大改变 —— *Flow diagrams, turing machines and languages with only two formation rules* by Böhm and Jacopini (1966)。实际上,直到上个世纪末,`GOTO` 语句才从各种语言里近乎 “灭绝”……
>
> 任何进步,无论大小,其实都相当不容易,都非常耗时费力 —— 在哪儿都一样。有兴趣、有时间,可以去浏览 Wikipedia 上的简要说明 —— [Wikipedia: Minimal structured control flow](https://en.wikipedia.org/wiki/Control_flow#Goto)。

if 语句

`if` 语句的最简单构成是这样 —— 注意第 1 行末尾的冒号 `:` 和第 2 行的缩进:

```python
if expression:
statements
```

如果表达式 `expression` 返回值为真,执行 `if` 语句块内部的 `statements`,否则,什么都不做,执行 `if` 之后的下一个语句。

```python
import random
r = random.randrange(1, 1000)

if r % 2 == 0:
print(f'{r} is even.')
```

372 is even.

如果,表达式 `expression` 返回值无论真假,我们都需要做一点相应的事情,那么我们这么写:

```python
if expression:
statements_for_True
else:
statements_for_False
```

如果表达式 `expression` 返回值为真,执行 `if` 语句块内部的 `statements_for_True`,否则,就执行 `else` 语句块内部的 `statements_for_False`

```python
import random
r = random.randrange(1, 1000)

if r % 2 == 0:
print(f'{r} is even.')
else:
print(f'{r} is odd.')
```

945 is odd.

有时,表达式 `<expression>` 返回的值有多种情况,并且针对不同的情况我们都要做相应的事情,那么可以这么写:

```python
if expression_1:
statements_for_expression_1_True

elif expression_2:
statements_for_expression_2_True

elif expression_3:
statements_for_expression_3_True

elif expression_...:
statements_for_expression_..._True
```

Python 用 `elif` 处理这种多情况分支,相当于其它编程语言中使用 `switch` 或者 `case`……

`elif` 是 `else if` 的缩写,作用相同。

以下程序模拟投两个骰子的结果 —— 两个骰子数字加起来,等于 `7` 算平,大于 `7` 算大,小于 `7` 算小:

```python
import random
r = random.randrange(2, 13)

if r == 7:
print('Draw!')
elif r < 7:
print('Small!')
elif r > 7:
print('Big!')
```

Big!

当然你还可以模拟投飞了的情况,即,最终的骰子数是 `0` 或者 `1`,即,`< 2`:

```python
import random
r = random.randrange(0, 13) # 生成的随机数应该从 0 开始了;

if r == 7:
print('Draw!')
elif r >= 2 and r < 7: # 如果这里直接写 elif r < 7:,那么,else: 那一部分永远不会被执行……
print('Small!')
elif r > 7:
print('Big!')
else:
print('Not valid!')
```

Small!

for 循环

Python 语言中,`for` 循环不使用其它语言中那样的计数器,取而代之的是 `range()` 这个我称其为 “整数等差数列生成器” 的函数。

用 C 语言写循环是这样的:

```C
for( a = 0; a < 10; a = a + 1 ){
printf("value of a: %d\n", a);
}
```

用 Python 写同样的东西,是这样的:

```python
for a in range(10):
print(f'value of a: {a}') #每次 a 的值都不同,从 0 递增至 9
```

value of a: 0
value of a: 1
value of a: 2
value of a: 3
value of a: 4
value of a: 5
value of a: 6
value of a: 7
value of a: 8
value of a: 9

range() 函数

`range()` 是个内建函数,[它的文档](https://docs.python.org/3/library/functions.html#func-range)是这样写的:

> **range**(_stop_)
>
> **range**(_start, stop[, step]_)

只有一个参数的时候,这个参数被理解为 `stop`,生成一个从 `0` 开始,到 `stop - 1` 的整数数列。

这就解释了为什么有的时候我们会在 `for ... in range(...):` 这种循环内的语句块里进行计算的时候,经常会在变量之后写上 `+ 1`,因为我们 range(n) 的返回数列中不包含 `n`,但我们有时候却需要 `n`。[点击这里返回看看第一章里提到的例子:所谓算法那一小节](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.E.1.entrance.ipynb#plusone)。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

range(10)
list(range(10)) # 将 range(10) 转换成 list,以便清楚看到其内容。
```

range(0, 10)

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

`start` 参数的默认值是 `0`。如需指定起点,那么得给 `range()` 传递两个参数,比如,`range(2, 13)`……

```python
list(range(2, 13))
```

[2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

第三个参数可选;`step`,步长,就相当于是 “等差数列” 当中的 “差”,默认值是 `1`。例如,`range(1, 10, 2)` 生成的是这样一个数列 `[1, 3, 5, 7, 9]`。所以,打印 `0 ~ 10` 之间的所有奇数,可以这样写:

```python
for i in range(1, 10, 2):
print(i)
```

1
3
5
7
9

我们也可以生成负数的数列:

```python
list(range(0, -10, -1))
```

[0, -1, -2, -3, -4, -5, -6, -7, -8, -9]

Continue、Break 和 Pass

在循环的过程中,还可以用 `continue` 和 `break` 控制流程走向,通常是在某条件判断发生的情况下 —— 正如你早就见过的那样:

```python
for n in range(2, 100):
if n == 2:
print(n)
continue
for i in range(2, n):
if (n % i) == 0:
break
else:
print(n)
```

`continue` 语句将忽略其后的语句开始下次循环,而 `break` 语句将从此结束当前循环,开始执行循环之后的语句:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/continue-break.png?raw=true)

`for` 语句块还可以附加一个 `else` —— 这是 Python 的一个比较有个性的地方。附加在 `for` 结尾的 `else` 语句块,_在没有 `break` 发生的情况下会运行_。

```python
for n in range(2, 100):
if n == 2:
print(n)
continue
for i in range(2, n):
if (n % i) == 0:
break
else: # 下一行的 print(n) 事实上属于语句块 for i in range(2, n):
print(n) # 整个循环结束,都没有发生 break 的情况下,才执行一次 print(n)
```

2
3
5
7
11
13
17
19
23
29
31
37
41
43
47
53
59
61
67
71
73
79
83
89
97

试比较以下两段代码:

```python
for n in range(2, 100):
if n == 2:
print(n)
continue
for i in range(2, n):
if (n % i) == 0:
break
print(n) # 事实上相当于针对 range(2, 100) 中每个 n 都执行了一次 print(n)
# 这个 print(n) 属于语句块 for n in range(2, 100):
```

```python
for n in range(2, 100):
if n == 2:
print(n)
continue
for i in range(2, n):
if (n % i) == 0:
break
print(n) # 事实上相当于针对 range(2, n) 中每个 i 都执行了一次 print(n)
```

2
3
5
5
5
7
7
7
7
7
9
11
11
11
...
97
97
97
97
99

`pass` 语句什么都不干:

再比如,
```python
def someFunction():
pass
```

又或者:

```python
for i in range(100):
pass
if i % 2 == 0:
pass
```

换个角度去理解的话可能更清楚:`pass` 这个语句更多是给写程序的人用的。当你写程序的时候,你可以用 `pass` 占位,而后先写别的部分,过后再回来补充本来应该写在 `pass` 所在位置的那一段代码。

写嵌套的判断语句或循环语句的时候,最常用 `pass`,因为写嵌套挺费脑子的,一不小心就弄乱了。所以,经常需要先用 `pass` 占位,而后逐一突破。

while 循环

今天,在绝大多数编程语言中,都提供两种循环结构:

> * Collection-controlled loops(以集合为基础的循环)
> * Condition-controlled loops(以条件为基础的循环)

之前的 `for ... in ...` 就是 Collection-controlled loops;而在 Python 中提供的 Condition-controlled loops 是 `while` 循环。

`while` 循环的格式如下:

```python
while expression:
statements
```

输出 1000 以内的斐波那契数列的程序如下:

```python
n = 1000
a, b = 0, 1
while a < n:
print(a, end=' ')
a, b = b, a+b
print()
```

0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

`for` 和 `while` 的区别在哪里?什么时候应该用哪个?

`for` 更适合处理序列类型的数据(Sequence Type)的迭代,比如处理字符串中的每一个字符,比如把 `range()` 返回的数列当作某种序列类型的索引。

`while` 更为灵活,因为它后面只需要接上一个逻辑表达式即可。

一个投骰子赌大小的游戏

虽然还不可能随心所欲写程序,但是,你现在具备了起码的 “阅读能力”。有了以上大概的介绍,你也许可以读懂一些代码了 —— 它们在你眼里再也不是天书了……

以下是一个让用户和程序玩掷骰子赌大小的程序。规则如下:

>* 每次计算机随机生成一个 `2... 12` 之间的整数,用来模拟机器人投两个骰子的情况;
* 机器人和用户的起始资金都是 10 个硬币
* 要求用户猜大小:
* 用户输入 `b` 代表 “大”;
* 用户输入 `s` 代表 “小”;
* 用户输入 `q` 代表 “退出”;
* 用户的输入和随机产生的数字比较有以下几种情况:
* 随机数小于 `7`,用户猜小,用户赢;
* 随机数小于 `7`,用户猜大,用户输;
* 随机数等于 `7`,用户无论猜大还是猜小,结局平,不输不赢;
* 随机数大于 `7`,用户猜小,用户输;
* 随机数大于 `7`,用户猜大,用户赢;
* 游戏结束条件:
* 机器人和用户,若任意一方硬币数量为 `0`,则游戏结束;
* 用户输入了 `q` 主动终止游戏。

```python
from random import randrange

coin_user, coin_bot = 10, 10 # 可以用一个赋值符号分别为多个变量赋值
rounds_of_game = 0

def bet(dice, wager): # 接收两个参数,一个是骰子点数,另一个用户的输入
if dice == 7:
print(f'The dice is {dice};\nDRAW!\n') # \n 是换行符号
return 0
elif dice < 7:
if wager == 's':
print(f'The dice is {dice};\nYou WIN!\n')
return 1
else:
print(f'The dice is {dice};\nYou LOST!\n')
return -1
elif dice > 7:
if wager == 's':
print(f'The dice is {dice};\nYou LOST!\n')
return -1
else:
print(f'The dice is {dice};\nYou WIN!\n')
return 1

while True: # 除 for 之外的另外一个循环语句
print(f'You: {coin_user}\t Bot: {coin_bot}')
dice = randrange(2, 13) # 生成一个 2 到 12 的随机数
wager = input("What's your bet? ")
if wager == 'q':
break
elif wager in 'bs': # 只有当用户输入的是 b 或者 s 得时候,才 “掷骰子”……
result = bet(dice, wager)
coin_user += result # coin_user += result 相当于 coin_user = coin_user + result
coin_bot -= result
rounds_of_game += 1
if coin_user == 0:
print("Woops, you've LOST ALL, and game over!")
break
elif coin_bot == 0:
print("Woops, the robot's LOST ALL, and game over!")
break

print(f"You've played {rounds_of_game} rounds.\n")
print(f"You have {coin_user} coins now.\nBye!")
```

总结

有控制流,才能算得上是程序。

> * 只处理一种情况,用 `if ... `
> * 处理 `True`/`False` 两种情况,用 `if ... else ...`
> * 处理多种情况,用 `if ... elif ... elif ... else ...`
> * 迭代有序数据类型,用 `for ... in ...`,如果需要处理没有 `break` 发生的情况,用 `for ... else ...`
> * 其它循环,用 `while ...`
> * 与循环相关的语句还有 `continue`、`break`、`pass`
> * 函数从控制流角度去看其实就是子程序

函数

函数,实际上是可被调用的完整的程序。它具备输入、处理、输出的功能。又因为它经常在主程序里被调用,所以它总是更像是个子程序。

了解一个函数,无非是要了解它的两个方面:

> * 它的**输入**是怎么构成的(都有哪些参数?如何指定?);
> * 以及它的**输出**是什么(返回值究竟是什么?)……

从这个角度看,牛,对人类来说就是个函数,它吃的是*草*,挤出来的是*奶*…… 开玩笑了。

在我们使用函数的过程中,我们常常*有意忽略*它的内部如何完成从输入到输出之间的*处理过程* —— 这就好像我们平日里用灯泡一样,大多数情况下,我们只要知道开关的使用方法就够了 —— 至于为什么按到这个方向上灯会亮,为什么按到另外一个方向上灯会灭,并不是我们作为用户必须关心的事情……

当然,如果你是设计开关的人就不一样了,你必须知道其中的运作原理;但是,最终,你还是希望你的用户用最简单方便的操作界面,而不是必须搞懂所有原理才能够使用你所设计的产品……

当我们用 Python 编程的时候,更多的情况下,我们只不过是在使用别人已经写好的函数,或者用更专业一点的词藻,叫做 “已完好封装的函数”。而我们所需要做的事情(所谓的 “学习使用函数”),其实只不过是 “通过阅读产品说明书了解如何使用产品” 而已,真的没多神秘……

**注意**

> 这一章的核心目的,不是让你学会如何写函数;而是通过一些例子,让你大抵上学会 “*如何阅读官方文档中关于函数的使用说明*”。也请注意之前的那个词:“*大抵上*”,所以**千万别怕自己最初的时候理解不全面**。

另外,这一章中用来举例的函数,全部来自于同一个官方文档页面,[Built-in Functions](https://docs.python.org/3/library/functions.html):

> https://docs.python.org/3/library/functions.html

示例 print()

基本的使用方法

`print()` 是初学者最常遇到的函数 —— 姑且不说是不是最常用到的。

它最基本的作用就是把传递给它的值输出到屏幕上,如果不给它任何参数,那么它就输出一个空行:

```python
print('line 1st')
print('line 2nd')
print()
print('line 4th')
```

line 1st
line 2nd

line 4th

你也可以向它传递多个参数,参数之间用 `,` 分开,它就会把那些值逐个输出到屏幕,每个值之间默认用空格分开。

```python
print('Hello,', 'jack', 'mike', '...', 'and all you guys!')
```

Hello, jack mike ... and all you guys!

当我们想把变量或者表达式的值插入字符串中的时候,可以用 f-string:

```python
name = 'Ann'
age = '22'
print(f'{name} is {age} years old.')
```

Ann is 22 years old.

但这并不是 `print()` 这个函数的功能,这实际上是 [`f-string`](https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting) 的功能,`f-string` 中用花括号 `{}` 括起来的部分是表达式,最终转换成字符串的时候,那些表达式的值(而不是变量或者表达式本身)会被插入相应的位置……

```python
name = 'Ann'
age = '22'
f'{name} is {age} years old.'
```

'Ann is 22 years old.'

所以,`print(f'{name} is {age} years old.')` 这一句中,函数 `print()` 完成的还是它最基本的功能:给它什么,它就把什么输出到屏幕上。

print() 的官方文档说明

以下,是 print() 这个函数的[官方文档](https://docs.python.org/3/library/functions.html#print):

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/doc-print.png?raw=true)

最必须读懂的部分,就是这一行:

> `print(*object, sep=' ', end='\n', file=sys.stdout, flush=False)` <a href='#fn1' name='fn1b'><sup>[1]</sup></a>

先只注意那些有着 `=` 的参数,`sep=' '`、`end='\n'`、`file=sys.stdout`,和 `flush=False`。

这其中,先关注这三个 `sep=' '`、`end='\n'`、`file=sys.stdout`:

> * `sep=' '`:接收多个参数之后,输出时,分隔符号默认为空格,`' '`;
> * `end='\n'`:输出行的末尾默认是换行符号 `'\n'`;
> * `file=sys.stdout`:默认的输出对象是 `sys.stdout`(即,用户正在使用的屏幕)……

也就是说,这个函数中有若干个具有默认值的参数,即便我们在调用这个函数的时候,就算没有指定它们,它们也存在于此。

即,当我们调用 `print('Hello', 'world!')` 的时候,相当于我们调用的是 `print('Hello', 'world!', sep=' ', end='\n', file=sys.stdout, flush=False)`

```python
import sys # 如果没有这一行,代码会报错

print('Hello', 'world!') # 下一行的输出和这一行相同
print('Hello', 'world!', sep=' ', end='\n', file=sys.stdout, flush=False)
print('Hello', 'world!', sep='-', end='\t')
print('Hello', 'world!', sep='~') # 上一行的末尾是 \t,所以,这一行并没有换行显示
print('Hello', 'world!', sep='\n') # 参数之间用换行 \n 分隔
```

Hello world!
Hello world!
Hello-world! Hello~world!
Hello
world!

很多人只看各种教材、教程,却从来不去翻阅官方文档 —— 到最后非常吃亏。只不过是多花一点点的功夫而已,看过之后,就会知道:原来 `print()` 这个函数是可以往文件里写数据的,只要指定 `file` 这个参数为一个已经打开的文件对象就可以了(真的有很多人完全不知道)……

另外,现在可以说清楚了:

> `print()` 这个函数的返回值是 `None` —— 注意,它向屏幕输出的内容,与 `print()` 这个函数的返回值不是一回事。

做为例子,看看 `print(print(1))` 这个语句 —— `print()` 这个函数被调用了两次,第一次是 `print(1)`,它向屏幕输出了一次,完整的输出值实际上是 `str(1) + '\n'`,而后返回一个值,`None`;而第二次调用 print(),这相当于是向屏幕输出这个 `None`:

```python
print(print(1))
```

1
None

“**看说明书**” 就是这样,全都看了,真不一定全部看懂,但看总是比不看强,因为总是有能看懂的部分……

关键字参数

在 Python 中,函数的参数,有两种:
> * **位置参数**(Positional Arguments,在官方文档里常被缩写为 *arg*)
> * **关键字参数**(Keyword Arguments,在官方文档里常被缩写为 *kwarg*)

在函数定义中,带有 `=` 的,即,已为其设定了默认值的参数,叫做 Keyword Arguments,其它的是 Positional Arguments。

在调用有 Keyword Arguments 的函数之时,如若不提供这些参数,那么参数在执行时,启用的是它在定义的时候为那些 Keyword Arguments 所设定的默认值;如若提供了这些参数的值,那么参数在执行的时候,启用的是接收到的相应值。

比如,`sorted()` 函数,它的定义如下:

> `sorted(iterable, *, key=None, reverse=False)`

现在先只关注它的 Keyword Arguments,`reverse`:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

sorted('abdc')
sorted('abdc', reverse=True)
```

['a', 'b', 'c', 'd']
['d', 'c', 'b', 'a']

位置参数

位置参数,顾名思义,是 “由位置决定其值的参数”。拿 `divmod()` 为例,它的[官方文档](https://docs.python.org/3/library/functions.html#divmod)是这样写的:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/doc-divmod.png?raw=true)

它接收且必须接收两个参数。

> * 当你调用这个函数的时候,括号里写的第一个参数,是被除数,第二个参数是除数 —— 此为该函数的输入;
> * 而它的返回值,是一个元组(Tuple,至于这是什么东西,后面讲清楚),其中包括两个值,第一个是商,第二个是余 —— 此为该函数的输出。

作为 “这个函数的用户”,你不能(事实上也没必要)调换这两个参数的意义。因为,根据定义,被传递的值的意义就是由参数的位置决定的。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

divmod(11, 3)
a, b = divmod(11, 3)
a
b

divmod(3, 11)
a, b = divmod(3, 11)
a
b
```

(3, 2)
3
2
(0, 3)
0
3

可选位置参数

有些函数,如 `pow()`,有**可选的位置参数**(Optional Positional Arguments)。

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/doc-pow.png?raw=true)

于是,`pow()` 有两种用法,各有不同的结果:

> * `pow(x, y)` —— 返回值是 `x ** y`
> * `pow(x, y, z)` —— 返回值是 `x ** y % z`

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

pow(2, 3)
pow(2, 3, 4)
```

8
0

注意 `pow()` 函数定义部分中,圆括号内的方括号 `[, z]` —— 这是非常严谨的标注,如果没有 `z`,那么那个逗号 `,` 就是没必要的。

看看 `exec()` 的官方文档(先别管这个函数干嘛用的),注意函数定义中的两个嵌套的方括号:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/doc-exec.png?raw=true)

这些方括号的意思是说:

> * 没在方括号里的 `object` 是不可或缺的参数,调用时必须提供;
> * 可以有第二个参数,第二个参数会被接收为 `globals`;
> * 在有第二个参数的情况下,第三个参数会被接收为 `locals`;
> * *但是,你没办法在不指定 `globals` 这个位置参数的情况下指定 `locals`……*

可接收很多值的位置参数

再回头看看 `print()`,它的第一个位置参数,`object` 前面是有个星号的:`*object, ...`。

对函数的用户来说,这说明,这个位置可以接收很多个参数(或者说,这个位置可以接收一个列表或者元组)。

再仔细看看 `print()`,它只有一个位置参数:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/doc-print-def.png?raw=true)

因为位置决定了值的定义,一般来说,一个函数里最多只有一个这种可以接收很多值的位置参数 —— 否则如何获知谁是谁呢?

如果与此同时,还有若干个位置参数,那么,*能够接收很多值的位置参数*只能放置最后,就好像 `max()` 函数那样:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/doc-max.png?raw=true)

Class 也是函数

虽然你现在还不一定知道 Class 究竟是什么,但在阅读官方文档的时候,遇到一些内建函数前面写着 Class,比如 `Class bool([x])`,千万别奇怪,因为 Class 本质上来看就是一种特殊类型的函数,也就是说,它也是函数:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/doc-class.png?raw=true)

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

bool()
bool(3.1415926)
bool(-3.1415926)
bool(1 == 2)
bool(None)
```

False
True
True
False
False

总结

本章需要(大致)了解的重点如下,其实很简单:

> * 你可以把函数当作一个产品,而你自己是这个产品的用户;
> * 既然你是产品的用户,你要养成好习惯,一定要亲自阅读产品说明书;
> * 调用函数的时候,注意*可选位置参数的使用方法*和*关键字参数的默认值*;
> * 函数定义部分,注意两个符号就行了,`[]` 和 `=`;
> * 所有的函数都有返回值,即便它内部不指定返回值,也有一个默认返回值:`None`;
> * 另外,一定要耐心阅读该函数在使用的时候需要注意什么 —— 产品说明书的主要作用就在这里……

知道这些就很好了!

这就好像你拿着一张地图,不可能一下子掌握其中所有的细节,但花几分钟搞清楚 “图例”(Legend)部分总是可以的,知道什么样的线标示的是公交车,什么样的线标示的是地铁,什么样的线标示的是桥梁,然后知道上北下南左西右东 —— 这之后,就可以开始慢慢研究地图了……

为了学会使用 Python,你以后最常访问的页面一定是这个:

> * https://docs.python.org/3/library/index.html
>
> 而最早反复阅读查询的页面肯定是其中的这两个:
> * https://docs.python.org/3/library/functions.html
> * https://docs.python.org/3/library/stdtypes.html

对了,还有就是,在这一章之后,你已经基本上 “**精通**” 了 `print()` 这个函数的用法。

-----
**脚注**

(2019.02.14)<a name='fn1'>[1]</a>:`print()` 函数的[官方文档](https://docs.python.org/3/library/functions.html#print)里,`sep=''` 肯定是 `sep=' '` 的笔误 —— 可以用以下代码验证:

```python
print('a', 'b', sep='')
print('a', 'b')
```
(2019.03.16)有读者提醒:https://github.com/selfteaching/the-craft-of-selfteaching/issues/111

> 而现在(2019.03.16)复制粘贴文档中的 `sep=' '`,会发现是有空格的。
>
> 这是改了么?

我回去查看了一下 2019.02.13 我提交的 bug track:[https://bugs.python.org/issue35986](https://bugs.python.org/issue35986),结论是 “人家没问题,是我自己的浏览器字体设置有问题”……

然而,我决定将这段文字保留在此书里,以便人们看到 “平日里软件维护是什么样的” —— 作为一个实例放在这里,很好。

<a href='#fn1b'><small>↑Back to Content↑</small></a>

字符串

在任何一本编程书籍之中,关于字符串的内容总是很长 —— 就好像每本英语语法书中,关于动词的内容总是占全部内容的至少三分之二。这也没什么办法,因为处理字符串是计算机程序中最普遍的需求 —— 因为程序的主要功能就是完成人机交互,人们所用的就是字符串而不是二进制数字。

在计算机里,所有的东西最终都要被转换成数值。又由于计算机靠的是电路,所以,最终只能处理 `1` 和 `0`,于是,最基本的数值是二进制;于是,连整数、浮点数字,都要最终转换成二进制数值。这就是为什么在所有编程语言中 `1.1 + 2.2` 并不是你所想象的 `3.3` 的原因。

```python
1.1 + 2.2
```

3.3000000000000003

因为最终所有的值都要转换成二进制 —— 这时候,小数的精度就有损耗,多次浮点数字转换成二进制相互运算之后再从二进制转换为十进制之后返回的结果,精度损耗就更大了。因此,在计算机上,浮点数字的精度总有极限。有兴趣进一步可以看看[关于 decimal 模块的文档](https://docs.python.org/3/library/decimal.html)。

字符串也一样。一个字符串由 0 个字符或者多个字符构成,它最终也要被转换成数值,再进一步被转换成二进制数值。空字符串的值是 `None`,即便是这个 `None` —— 也最终还是要被转换成二进制的 `0`。

字符码表的转换

很久以前,计算机的中央处理器最多只能够处理 8 位二进制数值,所以,那时候的计算机只能处理 256 个字符,即,2<sup>8</sup> 个字符。那个时候计算机所使用的码表叫 ASCII。现在计算机的中央处理器,大多是 64 位的,所以可以使用 2<sup>64</sup> 容量的码表,叫做 [Unicode](https://zh.wikipedia.org/wiki/Unicode)。随着多年的收集,2018 年 6 月 5 日公布的 `11.0.0` 版本已经包含了 13 万个字符 —— 突破 10 万字符是在 2005 年<a href='#fn1' name='fn1b'><sup>[1]</sup></a>。

把单个字符转换成码值的函数是 `ord()`,它只接收单个字符,否则会报错;它返回该字符的 unicode 编码。与 `ord()` 相对的函数是 `chr()`,它接收且只接收一个整数作为参数,而后返回相应的字符。`ord()` 接收多个字符的话会报错。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

ord('a')
chr(122)

ord('氅') # 完了,汉字也有你不认识的吧?
chr(25354) # 这个字估计你也不认识……

# ord('Python') # 这一句会报错
```

97
'z'
27653
'挊'

字符串的标示

标示一个字符串,有 4 种方式,用单引号、用双引号,用三个单引号或者三个双引号:

```python
'Simple is better than complex.' # 用单引号
```

'Simple is better than complex.'

```python
"Simple is better than complex." # 用双引号
```

'Simple is better than complex.'

```python
# 用三个单引号。注意输出结果中的 \n
# 这个字符串,看起来是两行,保存在内存或者变量之中的时候,
# 是一整串,其中的换行是用 \n 表示的。
'''
Simple is better than complex.
Complex is better than complicated.
'''
```

'\nSimple is better than complex.\nComplex is better than complicated.\n'

```python
#用三个双引号。注意输出结果中的 \n
"""
Simple is better than complex.
Complex is better than complicated.
"""
```

'\nSimple is better than complex.\nComplex is better than complicated.\n'

```python
print(
"""
Simple is better than complex.
Complex is better than complicated.
"""
)
# 用 print() 输出的时候,\n 就是不可见字符,字符串本身如下:
# '\nSimple is better than complex.\nComplex is better than complicated.\n'
# 其中的 \n 被打印出来的时候显示成换行
```

Simple is better than complex.
Complex is better than complicated.

字符串与数值之间的转换

由数字构成的字符串,可以被转换成数值,转换整数用 `int()`,转换浮点数字用 `float()`。

与之相对,用 `str()`,可以将数值转换成字符串类型。

注意,int() 在接收字符串为参数的时候,只能做整数转换。下面代码最后一行会报错:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

int('3')
float('3')
str(3.1415926)
# int('3.1415926') # 这一行会报错
```

3
3.0
'3.1415926'

`input()` 这个内建函数的功能是接收用户的键盘输入,而后将其作为字符串返回。它可以接收一个字符串作为参数,在接收用户键盘输入之前,会把这个参数输出到屏幕,作为给用户的提示语。这个参数是可选参数,直接写 `input()`,即,没有提供参数,那么它在要求用户输入的时候,就没有提示语。

以下代码会报错,因为 `age < 18` 不是合法的逻辑表达式,因为 `age` 是由 `input()` 传递过来的字符串;于是,它不是数字,那么它不可以与数字比较……

```python
age = input('Please tell me your age: ')
if age < 18:
print('I can not sell you drinks...')
else:
print('Have a nice drink!')
```

Please tell me your age: 19

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

<ipython-input-9-0573fe379e83> in <module>
1 age = input('Please tell me your age: ')
----> 2 if age < 18:
3 print('I can not sell you drinks...')
4 else:
5 print('Have a nice drink!')

TypeError: '<' not supported between instances of 'str' and 'int'

要改成这样才可能行:
为什么是可能行而不是一定行?如果用户 `input` 键盘输入的是 `eighteen` 或者 ` 十八 ` 等,依然会导致 `int()` 失败并得到 `ValueError` 的报错。用户输入的不可控,可能会导致千奇百怪的报错。但在这里,我们先简化处理,在引导语中加入一个正确的示例并默认用户会按引导语正确输入。

```python
age = int(input('''Please tell me your age:
an int number , e.g: 22
'''))
if age < 18:
print('I can not sell you drinks...')
else:
print('Have a nice drink!')
```

Please tell me your age: 19
Have a nice drink!

**注意**:如果你用来浏览当前 `.ipynb` 文件的是那个桌面 App [Nteract](https://nteract.io/),它目前不支持 input() 这个函数的调用……

转义符

有一个重要的字符,叫做 “转义符”,`\`,也有的地方把它称为 “脱字符”,因为它的英文原文是 _Escaping Character_。它本身不被当作字符,你要想在字符串里含有这个字符,得这样写 `\\`:

```python
'\\'
```

'\\'

```python
'\'
```

File "<ipython-input-10-d44a383620ab>", line 1
'\'
^
SyntaxError: EOL while scanning string literal

上面这一行报错信息是 `SyntaxError: EOL while scanning string literal`。这是因为 `\'` 表示的是单引号字符 `'`(Literal)—— 是可被输出到屏幕的 `'`,而不是用来标示字符串的那个 `'` —— 别急,无论哪个初学者第一次读到前面的句子都觉得有点莫名其妙…… —— 于是,Python 编译器扫描这个 “字符串” 的时候,还没找到标示字符串末尾的另外一个 `'` 的时候就读到了 `EOL`(End Of Line)。

如果你想输出这么个字符串,`He said, it's fine.`,如果用双引号扩起来 `"` 倒没啥问题,但是如果用单引号扩起来就麻烦了,因为编译器会把 `it` 后面的那个单引号 `'` 当作字符串结尾。

```python
'He said, it's fine.'
```

File "<ipython-input-11-2bcf2ca6dd95>", line 1
'He said, it's fine.'
^
SyntaxError: invalid syntax

于是你就得用转义符 `\`:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

# 要么你这么写:
'He said, it\'s fine.'
# 要么你这么写:
"He said, it's fine."
# 要么,不管用单引号还是双引号标示字符串,都习惯于用 \' 和 \" 书写属于字符串内部的引号……
"He said, it\'s fine."
```

"He said, it's fine."
"He said, it's fine."
"He said, it's fine."

转义符号 `\` 的另外两个常用形式是和 `t`、`n` 连起来用,`\t` 代表制表符(就是用 TAB `⇥` 键敲出来的东西),`\n` 代表换行符(就是用 Enter `⏎` 敲出来的东西)。

由于历史原因,Linux/Mac/Windows 操作系统中,换行符号的使用各不相同。Unix 类操作系统(包括现在的 MacOS),用的是 `\n`;Windows 用的是 `\r\n`,早期苹果公司的 Macintosh 用的是 `\r`(参见 [Wikipedia: Newline](https://en.wikipedia.org/wiki/Newline))。

所以,一个字符串,有两种形式,**raw** 和 **presentation**,在后者中,`\t` 被转换成制表符,`\n` 被转换成换行。

在写程序的过程中,我们在代码中写的是 _raw_,而例如当我们调用 `print()` 将字符串输出到屏幕上时,是 _presentation_:

```python
s = "He said, it\'s fine." # raw
print(s) # presentation
```

He said, it's fine.

以后有时间去看看这两个内建函数,能了解更多细节:
> * **ascii**(_object_) https://docs.python.org/3/library/functions.html#ascii
> * **repr**(_object_) https://docs.python.org/3/library/functions.html#repr

字符串的操作符

字符串可以用空格 `' '` 或者 `+` 拼接:

```python
'Hey!' + ' ' + 'You!' # 使用操作符 +
```

'Hey! You!'

```python
'Hey!' 'You!' # 空格与 + 的作用是相同的。
```

'Hey!You!'

字符串还可以与整数倍操作符 `*` 操作,`'Ha' * 3` 的意思是说,把字符串 `'Ha'` 复制三遍:

```python
'Ha' * 3
```

'HaHaHa'

```python
'3.14' * 3
```

'3.143.143.14'

字符串还可以用 `in` 和 `not in` 操作符 —— 看看某个字符或者字符串是否被包含在某个字符串中,返回的是布尔值:

```python
'o' in 'Hey, You!'
```

True

字符串的索引

字符串是由一系列的字符构成的。在 Python 当中,有一个容器(Container)的概念,这个概念前面提到过,后面还会深入讲解。现在需要知道的是,字符串是容器的一种;容器可分为两种,有序的和无序的 —— 字符串属于**有序容器**。

字符串里的每个字符,对应着一个从 `0` 开始的索引。比较有趣的是,索引可以是负数:

| 0 | 1 | 2 | 3 | 4 | 5 |
| ---- | ---- | ---- | ---- | ---- | ---- |
| P | y | t | h | o | n |
| -6 | -5 | -4 | -3 | -2 | -1 |

```python
s = 'Python'
for char in s:
print(s.index(char), char)
```

0 P
1 y
2 t
3 h
4 o
5 n

对于有序容器中的元素 —— 字符串就是字符的有序容器 —— 由于它们是有索引的,所以我们可以根据索引提取容器中的值,你可以把 `[]` 当作是有序容器的操作符之一,我们姑且将其称为 “*索引操作符*”。注意以下代码第 3 行中,`s` 后面的 `[]`,以及里面的变量 `i`:

```python
s = 'Python'
for i in range(len(s)):
print(s[i])

#上面的代码仅是为了演示索引操作符的使用,更简洁的写法是:
for i in s:
print(i)
```

P
y
t
h
o
n

我们可以使用*索引操作符*根据*索引***提取**字符串这个*有序容器*中的*一个或多个元素*,即,其中的字符或字符串。这个 “提取” 的动作有个专门的术语,叫做 “Slicing”(切片)。索引操作符 `[]` 中可以有一个、两个或者三个整数参数,如果有两个参数,需要用 `:` 隔开。它最终可以写成以下 4 种形式:

> * `s[index]` —— 返回索引值为 `index` 的那个字符
> * `s[start:]` —— 返回从索引值为 `start` 开始一直到字符串末尾的所有字符
> * `s[start:stop]` —— 返回从索引值为 `start` 开始一直到索引值为 `stop` 的那个字符*之前*的所有字符
> * `s[:stop]` —— 返回从字符串开头一直到索引值为 `stop` 的那个字符*之前*的所有字符
> * `s[start:stop:step]` —— 返回从索引值为 `start` 开始一直到索引值为 `stop` 的那个字符*之前*的,以 `step` 为步长提取的所有字符

提醒:无论是 `range(1,2)`,或者 `random.randrange(100, 1000)` 又或者 `s[start:stop]` 都有一个相似的规律,包含左侧的 `1`, `100`, `start`,不包含右侧的 `2`, `1000`, `stop`。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

s = 'Python'
s[1]
s[2:]
s[2:5]
s[:5]
s[1:5:2]
```

'y'
'thon'
'tho'
'Pytho'
'yh'

处理字符串的内建函数

[Python 内建函数](https://docs.python.org/3/library/functions.html#slice)中,把字符串当做处理对象的有:`ord()`、`input()`、`int()`、`float()`、`len()`、`print()`。再次注意,`ord()` 只接收单个字符为参数。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

ord('\n')
ord('\t')
ord('\r')
chr(65) # 与 ord() 相对的函数
s = input('请照抄一遍这个数字 3.14: ')
int('3')
# int(s) 这一句会报错…… 所以暂时注释掉了
float(s) * 9
len(s)
print(s*3)
```

10
9
13
'A'
请照抄一遍这个数字 3.14: 3.14
3
28.26
4
3.143.143.14

处理字符串的 Method

在 Python 中,字符串是一个**对象** —— 更准确地讲,是 str 类(`Class str`)的对象。

第一部分尚未读完的你,暂时不用了解对象究竟是什么;只需要知道的是,一个对象的内部有很多函数…… 这些写在对象内部的函数,有个专门的名称,类的**方法**(Method)。问题在于,在讲解编程的内容里,“方法” 这个词(比如,处理数值的*方法*是……)随处可见;所以,**为了避免歧义**,以后的文字里,提到 “类的方法” 的时候,直接用 **Method** 这个英文单词……

字符串有很多可以调用的 Methods。以下介绍的 `str` Methods,在官方文档 "[Text Sequence Type](https://docs.python.org/3/library/stdtypes.html#text-sequence-type-str)" 中都可以找到。

调用 str 类的 Methods 是使用 `.` 这个符号,比如:

```python
'Python'.upper()
```

大小写转换

转换字符串大小写的是 `str.upper()`、`str.lower()` 和 `str.swapcase()`,以及 `str.casefold()`;另外,还有专门针对行首字母大写的 `str.capitalize()` 和针对每个词的首字母大写的 `str.title()`:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

'Now is better than never.'.upper()

# 在 Python 命令行工具之中,单个下划线,是个特殊变量;
# 保存着最近的语句或者表达式的结果
# 上一个 Cell 执行过后,_ 中保存着 'NOW IS BETTER THAN NEVER.'

_.lower()
```

'NOW IS BETTER THAN NEVER.'

'now is better than never.'

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

casefold() 也是转换成小写,但它能处理更多欧洲语言字符

'ß'.casefold() # 德语字符中,大写 ß 的小写形式是 ss
len('ß'.casefold())
'ß'.lower() # lower() 对这类字符无能为力……
len('ß'.lower())
# casefold
'\u0132' # IJ 这个字符的 Unicode 编码
'\u0132'.casefold()
'\u0132'.lower() # 对这个字符来说,lower() 和 casefold 的效果一样
len('\u0132'.casefold())

# 这是一篇有用的文章:
# Truths programmers should know about case
# https://www.b-list.org/weblog/2018/nov/26/case/
```

'ss'
2
'ß'
1
'IJ'
'ij'
'ij'
1

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

s = 'Now is better than never.'
s.capitalize() # 句首字母大写
s.title() # 每个单词首字母大写
```

'Now is better than never.'
'Now Is Better Than Never.'

```python
s = 'Now is better than never.'
s.swapcase() # 逐个字符更替大小写
s.title()
s.title().swapcase()
```

'nOW IS BETTER THAN NEVER.'
'Now Is Better Than Never.'
'nOW iS bETTER tHAN nEVER.'

另外,还有个 `str.encode()` 在处理非英文字符串(比如中文)的时候,经常会用到:

```python
# str.encode(encoding="utf-8", errors="strict")
# 关于更多可能的 encoding list, 请参阅:
# https://docs.python.org/3/library/codecs.html#standard-encodings
s = '简单优于复杂。'
s.encode()
```

b'\xe7\xae\x80\xe5\x8d\x95\xe4\xbc\x98\xe4\xba\x8e\xe5\xa4\x8d\xe6\x9d\x82\xe3\x80\x82'

搜索与替换

让我们从 `str.count()` 这个搜寻子字符串出现次数的 Method(即,`str` 这个 `Class` 中定义的函数)开始。

官方文档是这么写的:

> `str.count(sub[,start[,end]])`

下面的函数说明加了默认值,以便初次阅读更容易理解:

> `str.count(sub [,start=0[, end=len(str)]])`

这里的方括号 `[]` 表示该参数可选;方括号里再次嵌套了一个方括号,这个意思是说,在这个可选参数 `start` 出现的情况下,还可以再有一个可选参数 `end`;

而 `=` 表示该参数有个默认值。上述这段说明如果你感到熟悉的话,说明前面的内容确实阅读到位了…… 与大量 “前置引用” 相伴随的是知识点的重复出现。

> * 只给定 `sub` 一个参数的话,于是从第一个字符开始搜索到字符串结束;
> * 如果,随后给定了一个可选参数的话,那么它是 `start`,于是从 `start` 开始,搜索到字符串结束;
> * 如果 `start` 之后还有参数的话,那么它是 `end`;于是从 `start` 开始,搜索到 `end - 1` 结束(即不包含索引值为 `end` 的那个字符)。
>
> 返回值为字符串中 `sub` 出现的次数。

注意:字符串中第一个字符的索引值是 `0`。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

s = """Simple is better than complex.
Complex is better than complicated."""
s.lower().count('mp')
s.lower().count('mp', 10)
s.lower().count('mp', 10, 30)
```

4
3
1

以下是 `str` 的搜索与替换的 Methods:`str.find()`, `str.rfind()`, `str.index()` 的示例:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

s = """Simple is better than complex.
Complex is better than complicated."""

print('Example of str.find():')
# str.find(sub[, start[, end]])
s.lower().find('mpl')
s.lower().find('mpl', 10)
s.lower().find('mpl', 10, 20) # 没有找到就返回 -1
print()

print('Example of str.rfind():')
# str.rfind(sub[, start[, end]])
# rfind() 返回最后 sub 出现的那次的位置;find()是最早的那次
s.lower().rfind('mpl')
s.lower().rfind('mpl', 10)
s.lower().rfind('mpl', 10, 20) # 没有找到就返回 -1
print()

print('Example of str.index():')
# str.index(sub[, start[, end]])
# 作用与 find() 相同,但如果没找到的话,会触发 ValueError 异常
# https://docs.python.org/3/library/exceptions.html#ValueError
s.lower().index('mpl')
# str.rindex(sub[, start[, end]])
# 作用与 rfind() 相同,但如果没找到的话,会触发 ValueError 异常
s.lower().rindex('mpl')
print()
```

Example of str.find():

2
24
-1
Example of str.rfind():
56
56
-1
Example of str.index():
2
56

`str.startswith()` 和 `str.endswith()` 是用来判断一个*字符串*是否以某个*子字符串*起始或者结束的:

```python
s = """Simple is better than complex.
Complex is better than complicated."""

# str.startswith(prefix[, start[, end]])
print("s.lower().startswith('S'):", \
s.lower().startswith('S'))
print("s.lower().startswith('b', 10):", \
s.lower().startswith('b', 10))
print("s.lower().startswith('e', 11, 20):", \
s.lower().startswith('e', 11, 20))

# str.endswith(suffix[, start[, end]])
print("s.lower().endswith('.'):", \
s.lower().endswith('.'))
print("s.lower().endswith('.', 10):", \
s.lower().endswith('.', 10))
print("s.lower().endswith('.', 10, 20):", \
s.lower().endswith('.', 10, 20))

# 好玩:中国人一不小心就会把这两个函数写成或者记成
# startwith() 和 endwith() —— 少写一个 s!
```

s.lower().startswith('S'): False
s.lower().startswith('b', 10): True
s.lower().startswith('e', 11, 20): True
s.lower().endswith('.'): True
s.lower().endswith('.', 10): True
s.lower().endswith('.', 10, 20): False

为了找到位置而进行搜索之前,你可能经常需要事先确认需要寻找的字符串在寻找对象中是否存在,这个时候,可以用 `in` 操作符:

```python
s = """Simple is better than complex.
Complex is better than complicated."""
# 如果你只想知道 “有没有”,而无需知道 “在哪里”,那么可以用:
print('mpl' in s)
```

True

能搜索,就应该能替换 —— `str.replace()`,它的函数说明是这样的:

> `str.replace(old, new[, count])`

用 `new` 替换 `old`,替换 `count` 个实例(实例:example,每次处理的对象就是实例,即具体的操作对象),其中,`count` 这个参数是可选的。

```python
s = """Simple is better than complex.
Complex is better than complicated."""

# str.startswith(prefix[, start[, end]])
print("s.lower().replace('mp', '[ ]', 2):\n")
print(s.lower().replace('mp', '[ ]', 2))
```

s.lower().replace('mp', '[ ]', 2):

si[ ]le is better than co[ ]lex.
complex is better than complicated.

另外,还有个专门替换 TAB(`\t`)的 Method,

> `str.expandtabs( tabsize=8)`

它的作用非常简单,就是把字符串中的 TAB(`\t`)替换成空格,默认是替换成 `8` 个空格 —— 当然你也可以指定究竟替换成几个空格

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

# str.expandtabs(tabsize=8)
s = "Special\tcases\taren't\tspecial\tenough\tto\tbreak\tthe\trules."
s.expandtabs()
s.expandtabs(2)
```

"Special cases aren't special enough to break the rules."
"Special cases aren't special enough to break the rules."

去除子字符

> `str.strip([chars])`

它最常用的场景是去除一个字符串首尾的所有空白,包括空格、TAB、换行符等等。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

s = "\r \t Simple is better than complex. \t \n"
s
s.strip()
```

'\r \t Simple is better than complex. \t \n'

'Simple is better than complex.'

但是,如果给定了一个字符串作为参数,那么参数字符串中的所有字母都会被当做需要从首尾剔除的对象,直到新的首尾字母不包含在参数中,就会停止剔除:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

s = "Simple is better than complex."
s
s.strip('Six.p') # p 全部处理完之后,p 并不在首尾,所以原字符串中的 p 字母不受影响;
s.strip('pSix.mle') # 这一次,首尾的 p 被处理了…… 参数中的字符顺序对结果没有影响,换成 Sipx.mle 也一样……
```

'Simple is better than complex.'
'mple is better than comple'
' is better than co'

还可以只对左侧处理,`str.lstrip()` 或者只对右侧处理,`str.rstrip()`

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

# str.lstrip([chars])
s = "Simple is better than complex."
s
s.lstrip('Six.p') # p 全部处理完之后,p 并不在首部,所以原字符串中的 p 字母不受影响;
s.lstrip('pSix.mle') # 这一次,首部的 p 被处理了…… 参数中的字符顺序对结果没有影响,换成 Sipx.mle 也一样……
```

'Simple is better than complex.'
'mple is better than complex.'
' is better than complex.'

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

# str.rstrip([chars])
s = "Simple is better than complex."
s
s.rstrip('Six.p') # p 全部处理完之后,p 并不在尾部,所以原字符串中的 p 字母不受影响;
s.rstrip('pSix.mle') # 这一次,尾部的 p 被处理了…… 参数中的字符顺序对结果没有影响,换成 Sipx.mle 也一样……
```

'Simple is better than complex.'
'Simple is better than comple'
'Simple is better than co'

拆分字符串

在计算机里,数据一般保存在文件之中。计算机擅长处理的是 “格式化数据”,即,这些数据按照一定的格式排列 —— 电子表格、数据库,就是一种保存方式。Microsoft 的 Excel 和 Apple 的 Numbers,都可以将表格导出为 `.csv` 文件。这是文本文件,里面的每一行可能由多个数据构成,数据之间用 `,`(或 `;`、`\t`)分隔:

```text
Name,Age,Location
John,18,New York
Mike,22,San Francisco
Janny,25,Miami
Sunny,21,Shanghai
```

文本文件中的这样一段内容,被读进来之后,保存在某个变量,那么,那个变量的值长成这个样子:

> `'Name,Age,Location\nJohn,18,New York\nMike,22,San Francisco\nJanny,25,Miami\nSunny,21,Shanghai'`

我们可以对这样的字符串进行很多操作,最常用的比如,`str.splitlines()`, `str.split()`;还有个 `str.partition()`,有空的人可以去[官方文档](https://docs.python.org/3/library/stdtypes.html#str.partition)看看说明。

`str.splitlines()` 返回的是个列表(List)—— 这又是一个前面曾简要提起过,但会在后面的章节才能详细讲解的概念 —— 由被拆分的每一行作为其中的元素。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

s = """Name,Age,Location
John,18,New York
Mike,22,San Francisco
Janny,25,Miami
Sunny,21,Shanghai"""

s # s 被打印出来的时候,\n 都被转换成换行了
s.splitlines() # 注意输出结果前后的方括号,[],表示这个返回结果是一个 List
```

'Name,Age,Location\nJohn,18,New York\nMike,22,San Francisco\nJanny,25,Miami\nSunny,21,Shanghai'

['Name,Age,Location',
'John,18,New York',
'Mike,22,San Francisco',
'Janny,25,Miami',
'Sunny,21,Shanghai']

`str.split()`, 是将一个字符串,根据分隔符进行拆分:

> `str.split(sep=None, maxsplit=-1)`

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

s = """Name,Age,Location
John,18,New York
Mike,22,San Francisco
Janny,25,Miami
Sunny,21,Shanghai"""

r = s.splitlines()[2] # 取出返回列表中索引值为 2 的那一行
r
r.split() # 如果没有给 str.split() 传递参数,那么默认为用 None 分割(各种空白,比如,\t 和 \r 都被当作 None)
r.split(sep=',')
r.split(',') # 上一行可以这样写。

r.split(sep=',', maxsplit=1) # 第二个参数指定拆分几次
# r.split(sep=',', 1) # 上一行不能这样写。
r.split(sep=',', maxsplit=0) # 0 次,即不拆分
r.split(sep=',', maxsplit=-1) # 默认值是 -1,拆分全部
```

'Mike,22,San Francisco'
['Mike,22,San', 'Francisco']
['Mike', '22', 'San Francisco']
['Mike', '22', 'San Francisco']
['Mike', '22,San Francisco']
['Mike,22,San Francisco']
['Mike', '22', 'San Francisco']

拼接字符串

`str.join()` 是将来非常常用的,它的官方文档说明却很少:

> `str.join(_iterable_)`
>
> Return a string which is the concatenation of the strings in iterable. A _[`TypeError`](https://docs.python.org/3/library/exceptions.html#TypeError)_ will be raised if there are any non-string values in iterable, including [`bytes`](https://docs.python.org/3/library/stdtypes.html#bytes) objects. The separator between elements is the string providing this method.

它接收的参数是 [iterable](https://docs.python.org/3/glossary.html#term-iterable),虽然你还没办法知道 iterable 究竟是什么,但这个 Method 的例子貌似可以看懂(可能你会产生 “那个方括号究竟是干什么的” 的疑问,也可能对前面章节提到的列表还有印象):

```python
s = ''
t = ['P', 'y', 't', 'h', 'o', 'n']
s.join(t)
```

'Python'

字符串排版

将字符串居中放置 —— 前提是设定整行的长度:

> `str.center(width[, fillchar])`

注意,第 2 个参数可选,且只接收单个字符 —— `char` 是 _character_ 的缩写。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

s = 'Sparse is better than dense!'
s.title().center(60)
s.title().center(60, '=')
s.title().center(10) # 如果宽度参数小于字符串长度,则返回原字符串

s = 'Sparse is better than dense!'
s.title().rjust(60)
s.title().rjust(60, '.')
```

' Sparse Is Better Than Dense! '

'================Sparse Is Better Than Dense!================'

'Sparse Is Better Than Dense!'

' Sparse Is Better Than Dense!'

'................................Sparse Is Better Than Dense!'

将字符串靠左或者靠右对齐放置:

> * `str.ljust(width)`
> * `str.rjust(width)`

另外,还有个字符串 Method 是,将字符串转换成左侧由 `0` 填充的指定长度字符串。例如,这在批量生成文件名的时候就很有用……

```python
for i in range(1, 11):
filename = str(i).zfill(3) + '.mp3'
print(filename)
```

001.mp3
002.mp3
003.mp3
004.mp3
005.mp3
006.mp3
007.mp3
008.mp3
009.mp3
010.mp3

格式化字符串

所谓对字符串进行格式化,指的是将特定变量插入字符串特定位置的过程。常用的 Methods 有两个,一个是 `str.format()`,另外一个是 `f-string`。

##### 使用 str.format()

这个 Method 的[官方文档说明](https://docs.python.org/3/library/stdtypes.html#str.format),你现在是死活看不懂的:

> `str.format(*args, **kwargs)`

参数前面多了个 `*`…… 没办法,现在讲不清楚,讲了也听不明白…… 先跳过,以下只关注怎么用这个 Method。

它的作用是:

> * 在一个字符串中,插入一个或者多个占位符 —— 用大括号 `{}` 括起来;
> * 而后将 `str.format()` 相应的参数,依次插入占位符中;

占位符中可以使用由零开始的索引。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

name = 'John'
age = 25
'{} is {} years old.'.format(name, age)
# 不写占位符索引就默认每个占位符的索引从第一个开始是 0, 1, 2 ...(占位符数量 - 1)
# '{} {}'.format(a, b) 和 '{0} {1}'.format(a, b) 是一样的。

# '{0} is {2} years old.'.format(name, age)
# 这一句会报错,因为 2 超出实际参数索引极限

# 两个连续使用的大括号,不被认为是占位符;且只打印出一对大括号
"Are you {0}? :-{{+}}".format(name)

# "%s is %d years old." % (name, age)
# 上一行这是兼容 Python 2 的老式写法,可以从此忽略……

# str.fomrat() 里可以直接写表达式……
'{} is a grown up? {}'.format(name, age >= 18)
```

'John is 25 years old.'
'Are you John? :-{+}'
'John is a grown up? True'

##### 使用 f-string

_f-string_ 与 `str.format()` 的功用差不多,只是写法简洁一些 —— 在字符串标示之前加上一个字母 `f`:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

# https://docs.python.org/3/library/stdtypes.html#printf-style-bytes-formatting
# f-string

name = 'John'
age = 25
f'{name} is {age} years old.'
f'{name} is a grown up? {age >= 18}'
```

'John is 25 years old.'
'John is a grown up? True'

只不过,str.format() 的用法中,索引顺序可以任意指定,于是相对更为灵活,下面的例子只是为了演示参数位置可以任意指定:

```python
name = 'John'
age = 25
'{1} is {0} years old.'.format(name, age)
```

'25 is John years old.'

字符串属性

字符串还有一系列的 Methods,返回的是布尔值,用来判断字符串的构成属性:

```python
# str.isalnum()
print("'1234567890'.isalnum():", \
'1234567890'.isalnum()) # '3.14'.isalnum() 返回的是 False

# str.isalpha()
print("'abcdefghij'.isalpha():", \
'abcdefghij'.isalpha())

# str.isascii()
print("'山巅一寺一壶酒'.isascii():", \
'山巅一寺一壶酒'.isascii())

# str.isdecimal()
print("'0.123456789'.isdecimal():", \
'0.1234567890'.isdecimal())

# str.isdigit()
print("'0.123456789'.isdigit():", \
'0.1234567890'.isdigit()) # 注意,如果字符串是 identifier,返回值也是 False

# str.isnumeric()
print("'0.123456789'.isnumeric():", \
'0.1234567890'.isnumeric())

# str.islower()
print("'Continue'.islower():", \
'Continue'.islower())

# str.isupper()
print("'Simple Is Better Than Complex'.isupper():", \
'Simple Is Better Than Complex'.isupper())

# str.istitle()
print("'Simple Is Better Than Complex'.istitle():", \
'Simple Is Better Than Complex'.istitle())

# str.isprintable()
print("'\t'.isprintable():", \
'\t'.isprintable())

# str.isspace()
print("'\t'.isspace():", \
'\t'.isspace())

# str.isidentifier()
print("'for'.isidentifier():", \
'for'.isidentifier())
```

'1234567890'.isalnum(): True
'abcdefghij'.isalpha(): True
'山巅一寺一壶酒'.isascii(): False
'0.123456789'.isdecimal(): False
'0.123456789'.isdigit(): False
'0.123456789'.isnumeric(): False
'Continue'.islower(): False
'Simple Is Better Than Complex'.isupper(): False
'Simple Is Better Than Complex'.istitle(): True
' '.isprintable(): False
' '.isspace(): True
'for'.isidentifier(): True

总结

这一章节显得相当繁杂。然而,这一章和下一章(关于容器),都是 “用来锻炼自己耐心的好材料”……

不过,若是自己动手整理成一个表格,总结归纳一下这一章节的内容,你就会发现其实没多繁杂,总之就还是那点事,怎么处理字符串?用操作符、用内建函数,用 Methods。只不过,字符串的操作符和数值的操作符不一样 —— 类型不一样,操作符就当然不一样了么!—— 最不一样的地方是,字符串是有序容器的一种,所以,它有索引,所以可以根据索引提取…… 至于剩下的么,就是很常规的了,用函数处理,用 Methods 处理,只不过,Methods 相对多了一点而已。

整理成表格之后,就会发现想要全部记住其实并没多难……

> * 为了表格在一屏中可以正确显示,本来应该规规矩矩写 `str.xxx`,但写成了 `s.xxx`……
> * 另外,操作那一行,为了分类记忆方便,把 `len()` 和 `s.join()` 也放进去了……

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/string-concepts.png?raw=true)

“记住” 的方法并不是马上就只盯着表格看…… 正确方法是反复阅读这一章内容中的代码,并逐一运行,查看输出结果;还要顺手改改看看,多多体会。多次之后,再看着表格回忆知识点,直到牢记为止。

为什么数值没有像字符串值这样详细论述?

上一章中,我们概括地讲了各种类型的值的运算。而后并没有继续深入讲解数字的运算,而是直接 “跳” 到了这一章关于字符串的内容。其实,只要一张表格和一个列表就足够了(因为之前零零散散都讲过):

Python 针对数字常用的操作符和内建函数,按照**优先级**从低到高排列:

|名称 | 操作 | 结果 | 官方文档链接 |
| --- | ----------------- | ------------------------------------------------------------| ------------------------------------------------------------ |
| 加 | `1 + 2` | 3 | |
| 减 | `2 - 1` | 1 | |
| 乘 | `3 * 5` | 15 | |
| 除 | `6 / 2` | 3.0 | |
| 商 | `7 // 3` | 2 | |
| 余 | `7 % 3` | 1 | |
| 负 | `-6` | -6 | |
| 正 | `+6` | 6 | |
| 绝对值 | `abs(-1)` | 1 | [`abs()`](https://docs.python.org/3/library/functions.html#abs) |
| 转换为整数| `int(3.14)` | 3 | [`int()`](https://docs.python.org/3/library/functions.html#int) |
| 转换为浮点数 | `float(3)` | 3.0 | [`float()`](https://docs.python.org/3/library/functions.html#float) |
| 商余 | `divmod(7, 3)` | 2, 1 | [`divmod()`](https://docs.python.org/3/library/functions.html#divmod) |
| 幂 | `pow(2, 10)` | 1024 | [`pow()`](https://docs.python.org/3/library/functions.html#pow) |
| 幂 | `3 ** 2` | 9 | |

Python 用来处理数值的内建函数:

> * `abs(n)` 函数返回参数 `n` 的*绝对值*;
> * `int(n)` 用来将*浮点数字* `n` 转换成*整数*;
> * `float(n)` 用来将*整数* `n` 转换成*浮点数字*;
> * `divmod(n, m)` 用来计算 `n` 除以 `m`,返回两个整数,一个是*商*,另外一个是*余*;
> * `pow(n, m)` 用来做乘方运算,返回 `n` 的 `m` *次方*;
> * `round(n)` 返回离浮点数字 `n` 最近的那个*整数*。

Python 做更为复杂的数学计算的模块(Module)是 math module,参阅:

> https://docs.python.org/3/library/math.html

-----
**脚注**

<a name='fn1'>[1]</a>:请查阅 https://en.wikipedia.org/wiki/Unicode

<a href='#fn1b'><small>↑Back to Content↑</small></a>

数据容器

在 Python 中,有个**数据容器**(Container)的概念。

其中包括**字符串**、由 `range()` 函数生成的**等差数列**、**列表**(List)、**元组**(Tuple)、**集合**(Set)、**字典**(Dictionary)。

这些容器,各有各的用处。其中又分为*可变*容器(Mutable)和*不可变*容器(Immutable)。可变的有列表、集合、字典;不可变的有字符串、`range()` 生成的等差数列、元组。集合,又分为 *Set* 和 *Frozen Set*;其中,Set 是*可变的*,Frozen Set 是*不可变的*。

字符串、由 `range()` 函数生成的等差数列、列表、元组是**有序类型**(Sequence Type),而集合与字典是*无序*的。

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/python-containers-final.png?raw=true)

另外,集合没有*重合*元素。

迭代(Iterate)

数据容器里的元素是可以被**迭代的**(Iterable),它们其中包含的元素,可以被逐个访问,以便被处理。

对于数据容器,有一个操作符,`in`,用来判断某个元素是否属于某个容器。

由于数据容器的可迭代性,再加上这个操作符 `in`,在 Python 语言里写循环格外容易且方便(以字符串这个字符的容器作为例子):

```python
for c in 'Python':
print(c)
```

P
y
t
h
o
n

在 Python 出现之前,想要完成这样一个访问字符串中的每一个字符的循环,大抵上应该是这样的(比如 C 语言):

``` C
# Written in C
char *string;

scanf("%s",string);
int i=strlen(string);
int k = 0;
while(k<i){
printf("%c", string[k]);
k++;
}
```

在 Python 中,简单的 for 循环,只需要指定一个次数就可以了,因为有 range() 这个函数:

```python
for i in range(10):
print(i)
```

0
1
2
3
4
5
6
7
8
9

即便是用比 C 更为 “现代” 一点的 JavaScript,也大抵上应该是这样的:

```c
var i;
for (i = 0; i < 10; i++) {
console.log(i)
}
```

当然,有时候我们也需要比较复杂的计数器,不过,Python 也不只有 `for` 循环,还有 `while` 循环,在必要的时候可以写复杂的计数器。

列表(List)

列表和字符串一样,是个*有序类型*(Sequence Type)的容器,其中包含着有索引编号的元素。

列表中的元素可以是不同类型。不过,在解决现实问题的时候,我们总是倾向于创建由同一个类型的数据构成的列表。遇到由不同类型数据构成的列表,我们更可能做的是想办法把不同类型的数据分门别类地拆分出来,整理清楚 —— 这种工作甚至有个专门的名称与之关联:*数据清洗*。

列表的生成

生成一个列表,有以下几种方式:

```python
a_list = []
b_list = [1, 2, 3]
list(), or list(iterable) # 这是 Type Casting
[(expression with x) for x in iterable]
```

```python
a_list = []
a_list.append(1)
a_list.append(2)
print(a_list, f'has a length of {len(a_list)}.')

#range() 返回的不是 list,需要用 list() 转换,否则也没办法调用 .append()
b_list = list(range(1, 9))
b_list.append(11)
print(b_list, f'has a length of {len(b_list)}.')

c_list = [2**x for x in range(8)]
print(c_list, f'has a length of {len(c_list)}.')
```

[1, 2] has a length of 2.
[1, 2, 3, 4, 5, 6, 7, 8, 11] has a length of 9.
[1, 2, 4, 8, 16, 32, 64, 128] has a length of 8.

这最后一种方式颇为神奇:

```python
[2**x for x in range(8)]
```

这种做法,叫做 **[List Comprehension](https://docs.python.org/3.7/tutorial/datastructures.html#tut-listcomps)**。

*Comprehend* 这个词的意思除了 “理解” 之外,还有另外一个意思,就是 “包括、囊括” —— 这样的话,你就大概能理解这种做法为什么被称作 *List Comprehension* 了。中文翻译中,怎么翻译的都有,“列表生成器”、“列表生成式” 等等,都挺好。但是,被翻译成 “列表解析器”,就不太好了,给人的感觉是操作反了……

List comprehension 可以嵌套使用 `for`,甚至可以加上条件 `if`。官方文档里有个例子,是用来把两个元素并不完全相同的列表去同后拼成一个列表(下面稍作了改写):

```python
import random

n = 10

# 生成一个 n 个元素的序列,每个元素是 1~100 之间的随机数
a_list = [random.randrange(1, 100) for i in range(n)]
print(f'a_list comprehends {len(a_list)} random numbers: {a_list}')

# 从 a_list 里把偶数都挑出来
b_list = [x for x in a_list if x % 2 == 0]
print(f'... and it has {len(b_list)} even numbers: {b_list}')
```

a_list comprehends 10 random numbers: [52, 34, 7, 96, 33, 79, 95, 18, 37, 46]
... and it has 5 even numbers: [52, 34, 96, 18, 46]

列表的操作符

列表的操作符和字符串一样,因为它们都是有序容器。列表的操作符有:

> * 拼接:`+`(与字符串不一样的地方是,不能用空格 `' '` 了)
> * 复制:`*`
> * 逻辑运算:`in` 和 `not in`,`<`、`<=`、`>`、`>=`、`!=`、`==`

而后两个列表也和两个字符串一样,可以被比较,即,可以进行逻辑运算;比较方式也跟字符串一样,从两个列表各自的第一个元素开始逐个比较,“一旦决出胜负马上停止”:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

a_list = [1, 2, 3]
b_list = [4, 5, 6]
c_list = a_list + b_list * 3
c_list
7 not in c_list
a_list > b_list
```

[1, 2, 3, 4, 5, 6, 4, 5, 6, 4, 5, 6]
True
False

根据索引提取列表元素

列表当然也可以根据索引操作,但由于列表是可变序列,所以,不仅可以提取,还可以删除,甚至替换。

```python
import random
n = 3
a_list = [random.randrange(65, 91) for i in range(n)]
b_list = [chr(random.randrange(65, 91)) for i in range(n)]
print(a_list)
c_list = a_list + b_list + a_list * 2
print(c_list)

print()
# 根据索引提取(Slicing)
print(c_list[3]) # 返回索引值为 3 的元素值
print(c_list[:]) # 相当于 c_list,返回整个列表
print(c_list[5:]) # 从索引为 5 的值开始直到末尾
print(c_list[:3]) # 从索引 0 开始,直到索引 3 之前(不包括 3)
print(c_list[2:6]) # 从索引 2 开始,直到索引 6 之前(不包括 6)

print()
# 根据索引删除
del c_list[3]
print(c_list) # del 是个命令,del c_list[3] 是一个语句;不能这么写:print(del c_list[3])
del c_list[5:8]
print(c_list)

print()
# 根据索引替换
c_list[1:5:2] = ['a', 2] # s[start:stop:step] = t,跟 range 的三个参数类似;
# len(t) = len([start:stop:step]) 必须为真
print(c_list)
```

[77, 66, 79]
[77, 66, 79, 'L', 'Z', 'R', 77, 66, 79, 77, 66, 79]

L
[77, 66, 79, 'L', 'Z', 'R', 77, 66, 79, 77, 66, 79]
['R', 77, 66, 79, 77, 66, 79]
[77, 66, 79]
[79, 'L', 'Z', 'R']

[77, 66, 79, 'Z', 'R', 77, 66, 79, 77, 66, 79]
[77, 66, 79, 'Z', 'R', 77, 66, 79]

[77, 'a', 79, 2, 'R', 77, 66, 79]

需要注意的地方是:**列表**(List)是可变序列,而**字符串**(str)是不可变序列,所以,对字符串来说,虽然也可以根据索引提取,但没办法根据索引删除或者替换。

```python
s = 'Python'[2:5]
print(s)
del s[2] # 这一句会报错
```

tho

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

<ipython-input-7-c9c999709965> in <module>
1 s = 'Python'[2:5]
2 print(s)
----> 3 del s[3] # 这一句会报错

TypeError: 'str' object doesn't support item deletion

之前提到过:

> 字符串常量(String Literal)是不可变有序容器,所以,虽然字符串也有一些 Methods 可用,但那些 Methods 都不改变它们自身,而是在操作后返回一个值给另外一个变量。

而对于列表这种*可变容器*,我们可以对它进行操作,结果是*它本身被改变*了。

```python
s = 'Python'
L = list(s)
print(s)
print(L)
del L[2]
print(L) # 用 del 对 L 操作之后,L 本身少了 1 个元素
```

Python
['P', 'y', 't', 'h', 'o', 'n']
['P', 'y', 'h', 'o', 'n']

列表可用的内建函数

列表和字符串都是容器,它们可使用的内建函数也其实都是一样的:

> * `len()`
> * `max()`
> * `min()`

```python
import random
n = 3

# 生成 3 个随机数,构成一个列表
a_list = [random.randrange(65, 91) for i in range(n)]
b_list = [chr(random.randrange(65, 91)) for i in range(n)]
print(a_list)
print(b_list)

# 列表可以使用操作符 + 和*
c_list = a_list + b_list + a_list * 2
print(c_list)

a_list *= 3
print(a_list)

# 内建函数操作 len()、max()、min()
print(len(c_list))
print(max(b_list)) # 内建函数内部做了异常处理,可以比较字符和数字 —— 初学者最讨厌这种事情了……
print(min(b_list)) # 注意,max() 和 min() 应用的是 b_list, len() 应用的是 c_list —— 请自行找到对应的 list 进行分析。

print('X' not in b_list)
```

[89, 84, 85]
['X', 'B', 'X']
[89, 84, 85, 'X', 'B', 'X', 89, 84, 85, 89, 84, 85]
[89, 84, 85, 89, 84, 85, 89, 84, 85]
12
X
B
False

Methods

字符串常量和 range() 都是不可变的(Immutable);而列表则是**可变类型**(Mutable type),所以,它最起码可以被排序 —— 使用 `sort()` Method:

```python
import random
n = 10
a_list = [random.randrange(1, 100) for i in range(n)]
print(f'a_list comprehends {len(a_list)} random numbers:\n', a_list)

a_list.sort()
print('the list sorted:\n', a_list)

a_list.sort(reverse=True) #reverse 参数,默认是 False
print('the list sorted reversely:\n', a_list)
```

a_list comprehends 10 random numbers:
[98, 9, 95, 15, 80, 70, 98, 82, 88, 46]
the list sorted:
[9, 15, 46, 70, 80, 82, 88, 95, 98, 98]
the list sorted reversely:
[98, 98, 95, 88, 82, 80, 70, 46, 15, 9]

如果列表中的元素全都是由字符串构成的,当然也可以排序:

```python
import random
n = 10

a_list = [chr(random.randrange(65, 91)) for i in range(n)]
# chr() 函数会返回指定 ascii 码的字符,ord('A') 是 65
print(f'a_list comprehends {len(a_list)} random string elements:\n', a_list)

a_list.sort()
print('the list sorted:\n', a_list)

a_list.sort(reverse=True) #reverse 参数,默认是 False
print('the list sorted reversely:\n', a_list)

print()

b_list = [chr(random.randrange(65, 91)) +\
chr(random.randrange(97, 123))\
for i in range(n)]
# 可以在行末加上 \ 符号,表示 “该行未完待续……”

print(f'b_list comprehends {len(b_list)} random string elements:\n', b_list)

b_list.sort()
print('the sorted:\n', b_list)

b_list.sort(key=str.lower, reverse=True)
# key 参数,默认是 None
# key=str.lower 的意思是,在比较排序的时候,先全都转换成小写再比较排序……
# —— 但并不改变原有值
print('the sorted reversely:\n', b_list)
```

a_list comprehends 10 random string elements:
['B', 'U', 'H', 'D', 'C', 'V', 'V', 'Q', 'U', 'P']
the list sorted:
['B', 'C', 'D', 'H', 'P', 'Q', 'U', 'U', 'V', 'V']
the list sorted reversely:
['V', 'V', 'U', 'U', 'Q', 'P', 'H', 'D', 'C', 'B']

b_list comprehends 10 random string elements:
['Nl', 'Mh', 'Ta', 'By', 'Ul', 'Nc', 'Gu', 'Rp', 'Pv', 'Bu']
the sorted:
['Bu', 'By', 'Gu', 'Mh', 'Nc', 'Nl', 'Pv', 'Rp', 'Ta', 'Ul']
the sorted reversely:
['Ul', 'Ta', 'Rp', 'Pv', 'Nl', 'Nc', 'Mh', 'Gu', 'By', 'Bu']

**注意**:不能乱比较…… 被比较的元素应该是同一类型 —— 所以,不是由同一种数据类型元素构成的列表,不能使用 `sort()` Method。下面的代码会报错:

```python
a_list = [1, 'a', 'c']
a_list = a_list.sort() # 这一句会报错
```

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

<ipython-input-12-acb9480a455d> in <module>
1 a_list = [1, 'a', 'c']
----> 2 a_list = a_list.sort() # 这一句会报错

TypeError: '<' not supported between instances of 'str' and 'int'

**可变序列**还有一系列可用的 **Methods**:`a.append()`,`a.clear()`,`a.copy()`,`a.extend(t)`,`a.insert(i,x)`,`a.pop([i])`,`a.remove(x)`,`a.reverse()`……

```python
import random
n = 3
a_list = [random.randrange(65, 91) for i in range(n)]
b_list = [chr(random.randrange(65, 91)) for i in range(n)]
print(a_list)
c_list = a_list + b_list + a_list * 2
print(c_list)

# 在末尾追加一个元素
c_list.append('100')
print(c_list)

# 清空序列
print()
print(a_list)
a_list.clear()
print(a_list)

print()
# 拷贝一个列表
d_list = c_list.copy()
print(d_list)
del d_list[6:8]
print(d_list)
print(c_list) # 对一个拷贝操作,不会更改 “原件”

print()
# 演示拷贝 .copy() 与赋值 = 的不同
e_list = d_list
del e_list[6:8]
print(e_list)
print(d_list) # 对 e_list 操作,相当于对 d_list 操作

# 在末尾追加一个列表
print()
print(a_list)
a_list.extend(c_list) # 相当于 a_list += c_list
print(a_list)

# 在某索引位置插入一个元素
print()
print(a_list)
a_list.insert(1, 'example') # 在索引 1 的位置插入 'example'
a_list.insert(3, 'example') # 在索引 3 的位置插入 'example';
print(a_list)

排序

a_list.sort() 这一句会出错,因为当前列表中的元素,是 int 和 str 混合的。

print()
print(a_list)
a_list.reverse()
print(a_list)
x = a_list.reverse() # reverse() 只对当前序列操作,并不返回一个逆序列表;返回值是 None
print(x)
```

[90, 88, 73]
[90, 88, 73, 'T', 'N', 'Y', 90, 88, 73, 90, 88, 73]
[90, 88, 73, 'T', 'N', 'Y', 90, 88, 73, 90, 88, 73, '100']

[90, 88, 73]
[]

[90, 88, 73, 'T', 'N', 'Y', 90, 88, 73, 90, 88, 73, '100']
[90, 88, 73, 'T', 'N', 'Y', 73, 90, 88, 73, '100']
[90, 88, 73, 'T', 'N', 'Y', 90, 88, 73, 90, 88, 73, '100']

[90, 88, 73, 'T', 'N', 'Y', 88, 73, '100']
[90, 88, 73, 'T', 'N', 'Y', 88, 73, '100']

[]
[90, 88, 73, 'T', 'N', 'Y', 90, 88, 73, 90, 88, 73, '100']

[90, 88, 73, 'T', 'N', 'Y', 90, 88, 73, 90, 88, 73, '100']
[90, 'example', 88, 'example', 73, 'T', 'N', 'Y', 90, 88, 73, 90, 88, 73, '100']

[90, 'example', 88, 'example', 73, 'T', 'N', 'Y', 90, 88, 73, 90, 88, 73, '100']
['100', 73, 88, 90, 73, 88, 90, 'Y', 'N', 'T', 73, 'example', 88, 'example', 90]
None

有一个命令、两个 Methods 与删除单个元素相关联,`del`,`a.pop([i])`,`a.remove(x)`,请注意它们之间的区别。

```python
import random
n = 3
a_list = [random.randrange(65, 91) for i in range(n)]
print(a_list)

# 插入
print()
a_list.insert(1, 'example') # 在索引 1 的位置插入 'example'

# 删除
print()
print(a_list)
a_list.remove('example') # 去除 'example' 这个元素,如果有多个 'example',只删除第一个
print(a_list)

pop() 删除并返回被删除的值

print()
print(a_list)
p = a_list.pop(2) # 去除索引为 2 的元素,且返回元素的值,赋值给 p
print(a_list)
print(p)

# pop() 与 del,或者 remove() 的区别
print()
a_list.insert(2, 'example')
a_list.insert(2, 'example')
print(a_list)
del a_list[2]
print(a_list)

print()
print(a_list.remove('example')) # a_list.remove() 这个 Method 的返回值是 None
print(a_list)
```

[88, 84, 69]

[88, 'example', 84, 69]
[88, 84, 69]

[88, 84, 69]
[88, 84]
69

[88, 84, 'example', 'example']
[88, 84, 'example']

None
[88, 84]

小结

看起来是个新概念,例子全部读完也很是要花上一段时间,然而,从操作上来看,操作列表和操作字符串的差异并不大,重点在于一个是 Immutable,另外一个是 Mutable,所以,例如像 `a.sort()`,`a.remove()` 这样的事,列表能做,字符串不能做 —— 字符串也可以排序,但那是排序之后返回给另外一个变量;而列表可以直接改变自身……

而整理成表格之后呢,理解与记忆真的是零压力:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/list-concepts.png?raw=true)

元组(Tuple)

在完整掌握列表的创建与操作之后,再理解元组(Tuple)就容易了,因为它们之间的主要区别只有两个:

> * List 是*可变*有序容器,Tuple 是*不可变*有序容器。
> * List 用*方括号*标识 `[]`,Tuple 用*圆括号* 标识 `()`。

创建一个元组的时候,用圆括号:

```python
a = ()
```

这样就创建了一个空元组。

多个元素之间,用 `,` 分离。

创建一个含多个元素的元组,可以省略这个括号。

```python
a = 1, 2, 3 # 不建议这种写法
b = (1, 2, 3) # 在创建元组的时候建议永远不省略圆括号……
print(a)
print(b)
a == b
```

(1, 2, 3)
(1, 2, 3)
True

**注意**:创建单个元素的元组,无论是否使用圆括号,在那唯一的元素后面一定要*补上一个逗号* `,`:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

a = 2, # 注意这个末尾的逗号 , 它使得 a 变量被定义为一个元组,而不是数字
a

b = 2 # 整数,赋值
b

c = (2) # 不是元组
c
type(c) # 还是 int

d = (2,) # 这才是元组
d
a == d
```

(2,)
2
2
int
(2,)
True

元组是不可变序列,所以,你没办法从里面删除元素。

但是,你可以在末尾追加元素。所以,严格意义上,对元组来讲,“不可变” 的意思是说,“**当前已有部分不可变**”……

```python
a = 1,
print(a)
print(id(a))
a += 3, 5
print(a)
print(id(a)) # id 并不相同 —— 实际上是在内存中另外新创建了一个元组……
```

(1,)
4593032496
(1, 3, 5)
4592468976

初学者总是很好奇 List 和 Tuple 的区别。首先是使用场景,在将来需要更改的时候,创建 List
;在将来不需要更改的时候,创建 Tuple。其次,从计算机的角度来看,Tuple 相对于 List 占用更小的内存。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

n = 10000 #@param {type:"number"}
a = range(n)
b = tuple(a) # 把 a 转换成元组
c = list(a) # 把 a 转换成列表
a.__sizeof__()
b.__sizeof__()
c.__sizeof__()
```

48

80024

90088

等你了解了 Tuple 的标注方式,你就会发现,`range()` 函数返回的等差数列就是一个 Tuple —— `range(6)` 就相当于 `(0, 1, 2, 3, 4, 5)`。

集合(Set)

**集合**(Set)这个容器类型与列表不同的地方在于,首先它*不包含重合元素*,其次它是*无序*的;进而,集合又分为两种,Set,*可变的*,Frozen Set,*不可变的*。

创建一个集合,用**花括号** `{}` 把元素括起来,用 `,` 把元素隔开:

```python
primes = {2, 3, 5, 7, 11, 13, 17}
primes
```

{2, 3, 5, 7, 11, 13, 17}

创建

注意:创建空集合的时候,必须用 `set()`,而不能用 `{}`:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

a = {} # 注意这样创建的是一个 dict(字典),而不是 set 集合
b = set() # 这样创建的才是空集合
type(a)
type(b)
```

dict
set

也可以将序列数据转换(Casting)为集合。转换后,返回的是一个已**去重**的集合。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

a = "abcabcdeabcdbcdef"
b = range(10)
c = [1, 2, 2, 3, 3, 1]
d = ('a', 'b', 'e', 'b', 'a')
set(a)
set(b)
set(c)
set(d)
```

{'a', 'b', 'c', 'd', 'e', 'f'}
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
{1, 2, 3}
{'a', 'b', 'e'}

Set 当然也可以进行 *Comprehension*:

```python
a = "abcabcdeabcdbcdef"
b = {x for x in a if x not in 'abc'}
b
```

{'d', 'e', 'f'}

操作

将序列类型数据转换成 Set,就等于**去重**。当然,也可以用 `in` 来判断某个元素是否属于这个集合。`len()`、`max()`、`min()`,也都可以用来操作 Set,但 `del` 却不行 —— 因为 Set 中的元素没有索引(它不是有序容器)。从 Set 里删除元素,得用 `set.remove(elem)`;而 Frozen Set 是不可变的,所以不能用 `set.remove(elem)` 操作。

对于集合,有相应的操作符可以对它们进行集合运算:

> * 并集:`|`
> * 交集:`&`
> * 差集:`-`
> * 对称差集:`^`

之前用 `set('abcabcdeabcdbcdef')` 作为简单例子还凑合能用;但这样对读者无意义的集合,无助于进一步的理解。

事实上,每种数据结构(Data Structures —— 在这一章里,我们一直用的概念是 “容器”,其实是指同一事物的两种称呼)都有自己的应用场景。比如,当我们需要管理很多用户时,集合就可以派上很大用场。

假定两个集合中有些人是 admins,有些人是 moderators:

```python
admins = {'Moose', 'Joker', 'Joker'}
moderators = {'Ann', 'Chris', 'Jane', 'Moose', 'Zero'}
```

那么:

```python
admins = {'Moose', 'Joker', 'Joker'}
moderators = {'Ann', 'Chris', 'Jane', 'Moose', 'Zero'}

admins # 去重自动完成
'Joker' in admins # Joker 是否是 admins?
'Joker' in moderators # Joker 是否是 moderator?
admins | moderators # admins、moderator,或者身兼两职的,即,两个角色中的所有人 in admins or moderators or both
admins & moderators # 既是 admins 又是 moderator 的都有谁?in both admins and moderators
admins - moderators # 是 admins 但不是 moderator 的都有谁?in admins but not in moderators
admins ^ moderators # admins 和 moderator 中不是身兼两职的都有谁?in admins or moderator but not both
```

{'Joker', 'Moose'}
True
False
{'Ann', 'Chris', 'Jane', 'Joker', 'Moose', 'Zero'}
{'Moose'}
{'Joker'}
{'Ann', 'Chris', 'Jane', 'Joker', 'Zero'}

```python
# 这个 cell 集合运算图示需要安装 matplotlib 和 matplotlib-venn
# !pip install matplotlib
# !pip install matplotlib-venn
import matplotlib.pyplot as plt
from matplotlib_venn import venn2

admins = {'Moose', 'Joker', 'Joker'}
moderators = {'Ann', 'Chris', 'Jane', 'Moose', 'Zero'}

v = venn2(subsets=(admins, moderators), set_labels=('admins', 'moderators'))
v.get_label_by_id('11').set_text('\n'.join(admins & moderators))
v.get_label_by_id('10').set_text('\n'.join(admins - moderators))
v.get_label_by_id('01').set_text('\n'.join(moderators - admins))

plt.show()
```

[〔图:png〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/Part.1.E.6.containers_66_0.png?raw=true)

以上的操作符,都有另外一个版本,即,用 Set 这个类的 Methods 完成。

| 意义 | 操作符 | Methods | Methods 相当于 |
| :------: | :----: | :---------------------: | :--: |
| 并集 | <code>&#124;</code> | `set.union(*others)` | <code>set &#124; other &#124; ...</code> |
| 交集 | `&` | `set.intersection(*others)` | `set & other & ...` |
| 差集 | `-` | `set.difference(*others)` | `set - other - ...` |
| 对称差集 | `^` | `set.symmetric_difference(other)` | `set ^ other` |

注意,并集、交集、差集的 Methods,可以接收多个集合作为参数 `(*other)`,但对称差集 Method 只接收一个参数 `(other)`。

对于集合,推荐更多使用 Methods 而不是操作符的主要原因是:更易读 —— 对人来说,因为有意义、有用处的代码终将需要人去维护。

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

admins = {'Moose', 'Joker', 'Joker'}
moderators = {'Chris', 'Moose', 'Jane', 'Zero'}

admins.union(moderators)
admins.intersection(moderators)
admins.difference(moderators)
admins.symmetric_difference(moderators)
```

{'Chris', 'Jane', 'Joker', 'Moose', 'Zero'}
{'Moose'}
{'Joker'}
{'Chris', 'Jane', 'Joker', 'Zero'}

逻辑运算

两个集合之间可以进行逻辑比较,返回布尔值。

**set** `==` **other**
> `True`: set 与 other 相同

**set** `!=` **other**
> `True`: set 与 other 不同

**isdisjoint**(_other_)
> `True`: set 与 other 非重合;即,`set & other == None`

**issubset**(_other_),**set** `<=` **other**
> `True`: set 是 other 的子集

**set** `<` **other**
> `True`: set 是 other 的真子集,相当于 `set <= other && set != other`

**issuperset**(_other_),**set** `>=` **other**
> `True`: set 是 other 的超集

**set** `>` **other**
> `True`: set 是 other 的真超集,相当于 `set >= other && set != other`

更新

对于集合,有以下更新它自身的 Method:

**add**(*elem*)
> 把 elem 加入集合

**remove**(*elem*)
> 从集合中删除 elem;如果集合中不包含该 elem,会产生 KeyError 错误。

**discard**(*elem*)
> 如果该元素存在于集合中,删除它。

**pop**()
> 从集合中随机删除一个元素,并返回删除的元素的值,针对空集合做此操作会产生 KeyError 错误。

**clear**()
从集合中删除所有元素。

**set.update**(*_others_),相当于 `set |= other | ...`
> 更新 set, 加入 others 中的所有元素;

**set.intersection_update**(*_others_),相当于 `set &= other & ...`
> 更新 set, 保留同时存在于 set 和所有 others 之中的元素;

**set.difference_update**(*_others_),相当于 `set -= other | ...`
> 更新 set, 删除所有在 others 中存在的元素;

**set.symmetric_difference_update**(_other_),相当于 `set ^= other`
> 更新 set, 只保留存在于 set 或 other 中的元素,但不保留同时存在于 set 和 other 中的元素;**注意**,该 Method *只接收一个参数*。

冻结集合

还有一种集合,叫做冻结集合(Frozen Set),Frozen Set 之于 Set,正如 Tuple 之于 List,前者是不可变容器(Immutable),后者是可变容器(Mutable),无非是为了节省内存使用而设计的类别。

有空去看看这个链接就可以了:

> https://docs.python.org/3/library/stdtypes.html#frozenset

字典(Dictionary)

Map 是容器中的单独一类,**映射**(Map)容器。映射容器只有一种,叫做**字典**(Dictionary)。先看一个例子:

```python
phonebook = {'ann':6575, 'bob':8982, 'joe':2598, 'zoe':1225}
phonebook
```

字典里的每个元素,由两部分组成,_key_(键)和 _value_(值),二者由一个冒号连接。

比如,`'ann':6575` 这个字典元素,_key_ 是 `'ann'`,_value_ 是 `6575`。

字典直接使用 _key_ 作为索引,并映射到与它匹配的 _value_:

```python
phonebook = {'ann':6575, 'bob':8982, 'joe':2598, 'zoe':1225}
phonebook['bob']
```

8982

在同一个字典里,_key_ 都是唯一的。当创建字典的时候,如果其中有重复的 _key_ 的话,就跟 Set 那样会 “**自动去重**” —— 保留的是众多重复的 _key_ 中的最后一个 _key:value_(或者说,最后一个 _key:value_ “之前那个 _key_ 的 _value_ 被**更新**了”)。字典这个数据类型之所以叫做 Map(映射),是因为字典里的 _key_ 都映射且只映射一个对应的 _value_。

```python
phonebook = {'ann':6575, 'bob':8982, 'joe':2598, 'zoe':1225, 'ann':6585}
phonebook
```

{'ann': 6585, 'bob': 8982, 'joe': 2598, 'zoe': 1225}

在已经了解如何操作列表之后,再去理解字典的操作,其实没什么难度,无非就是字典多了几个 Methods。

提蓄一下自己的耐心,把下面的若干行代码都仔细阅读一下,猜一猜输出结果都是什么?

字典的生成

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

aDict = {}
bDict = {'a':1, 'b':2, 'c':3}
aDict
bDict
```

{}
{'a': 1, 'b': 2, 'c': 3}

更新某个元素

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

phonebook1 = {'ann':6575, 'bob':8982, 'joe':2598, 'zoe':1225, 'ann':6585}

phonebook1['joe']
phonebook1['joe'] = 5802
phonebook1
phonebook1['joe']
```

2598
{'ann': 6585, 'bob': 8982, 'joe': 5802, 'zoe': 1225}
5802

添加元素

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

phonebook1 = {'ann':6575, 'bob':8982, 'joe':2598, 'zoe':1225, 'ann':6585}
phonebook2 = {'john':9876, 'mike':5603, 'stan':6898, 'eric':7898}

phonebook1.update(phonebook2)
phonebook1
```

{'ann': 6585,
'bob': 8982,
'joe': 2598,
'zoe': 1225,
'john': 9876,
'mike': 5603,
'stan': 6898,
'eric': 7898}

删除某个元素

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

phonebook1 = {'ann':6575, 'bob':8982, 'joe':2598, 'zoe':1225, 'ann':6585}

del phonebook1['ann']
phonebook1
```

{'bob': 8982, 'joe': 2598, 'zoe': 1225}

逻辑操作符

```python
phonebook1 = {'ann':6575, 'bob':8982, 'joe':2598, 'zoe':1225, 'ann':6585}

'ann' in phonebook1

phonebook1.keys()
'stan' in phonebook1.keys()

phonebook1.values()
1225 in phonebook1.values()

phonebook1.items()
('stan', 6898) in phonebook1.items()
```

True
dict_keys(['ann', 'bob', 'joe', 'zoe'])
False
dict_values([6585, 8982, 2598, 1225])
True
dict_items([('ann', 6585), ('bob', 8982), ('joe', 2598), ('zoe', 1225)])
False

可用来操作的内建函数

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

phonebook1 = {'ann':6575, 'bob':8982, 'joe':2598, 'zoe':1225, 'ann':6585}
phonebook2 = {'john':9876, 'mike':5603, 'stan':6898, 'eric':7898}
phonebook1.update(phonebook2)

len(phonebook1)
max(phonebook1)
min(phonebook1)
list(phonebook1)
tuple(phonebook1)
set(phonebook1)
sorted(phonebook1)
sorted(phonebook1, reverse=True)
```

8
'zoe'
'ann'
['ann', 'bob', 'joe', 'zoe', 'john', 'mike', 'stan', 'eric']
('ann', 'bob', 'joe', 'zoe', 'john', 'mike', 'stan', 'eric')
{'ann', 'bob', 'eric', 'joe', 'john', 'mike', 'stan', 'zoe'}
['ann', 'bob', 'eric', 'joe', 'john', 'mike', 'stan', 'zoe']
['zoe', 'stan', 'mike', 'john', 'joe', 'eric', 'bob', 'ann']

常用 Methods

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

phonebook1 = {'ann':6575, 'bob':8982, 'joe':2598, 'zoe':1225, 'ann':6585}
phonebook2 = {'john':9876, 'mike':5603, 'stan':6898, 'eric':7898}

phonebook3 = phonebook2.copy()
phonebook3

phonebook3.clear()
phonebook3

phonebook2 # .copy() 的 “原件” 不会发生变化

p = phonebook1.popitem()
p
phonebook1

p = phonebook1.pop('adam', 3538)
p
phonebook1

p = phonebook1.get('adam', 3538)
p
phonebook1

p = phonebook1.setdefault('adam', 3538)
p
phonebook1
```

{'john': 9876, 'mike': 5603, 'stan': 6898, 'eric': 7898}
{}
{'john': 9876, 'mike': 5603, 'stan': 6898, 'eric': 7898}
('zoe', 1225)
{'ann': 6585, 'bob': 8982, 'joe': 2598}
3538
{'ann': 6585, 'bob': 8982, 'joe': 2598}
3538
{'ann': 6585, 'bob': 8982, 'joe': 2598}
3538
{'ann': 6585, 'bob': 8982, 'joe': 2598, 'adam': 3538}

迭代各种容器中的元素

我们总是有这样的需求:对容器中的元素逐一进行处理(运算)。这样的时候,我们就用 `for` 循环去迭代它们。

对于迭代 `range()` 和 `list` 中的元素我们已经很习惯了:

```python
for i in range(3):
print(i)
```

0
1
2

```python
for i in [1, 2, 3]:
print(i)
```

1
2
3

迭代的同时获取索引

有时,我们想同时得到有序容器中的元素及其索引,那么可以调用 `enumerate()` 函数来帮我们:

```python
s = 'Python'
for i, c in enumerate(s):
print(i, c)

```

0 P
1 y
2 t
3 h
4 o
5 n

```python
for i, v in enumerate(range(3)):
print(i, v)
```

0 0
1 1
2 2

```python
L = ['ann', 'bob', 'joe', 'john', 'mike']
for i, L in enumerate(L):
print(i, L)
```

0 ann
1 bob
2 joe
3 john
4 mike

```python
t = ('ann', 'bob', 'joe', 'john', 'mike')
for i, t in enumerate(t):
print(i, t)
```

0 ann
1 bob
2 joe
3 john
4 mike

迭代前排序

可以用 `sorted()` 和 `reversed()` 在迭代前先排好序:

```python
t = ('bob', 'ann', 'john', 'mike', 'joe')
for i, t in enumerate(sorted(t)):
print(i, t)
```

0 ann
1 bob
2 joe
3 john
4 mike

```python
t = ('bob', 'ann', 'john', 'mike', 'joe')
for i, t in enumerate(sorted(t, reverse=True)):
print(i, t)
```

0 mike
1 john
2 joe
3 bob
4 ann

```python
t = ('bob', 'ann', 'john', 'mike', 'joe')
for i, t in enumerate(reversed(t)):
print(i, t)
```

0 joe
1 mike
2 john
3 ann
4 bob

同时迭代多个容器

可以在 `zip()` 这个函数的帮助下,同时迭代两个或者两个以上的容器中的元素(这样做的前提是,多个容器中的元素数量最好相同):

```python
chars = 'abcdefghijklmnopqrstuvwxyz'
nums = range(1, 27)
for c, n in zip(chars, nums):
print(f"Let's assume {c} represents {n}.")
```

Let's assume a represents 1.
Let's assume b represents 2.
Let's assume c represents 3.
Let's assume d represents 4.
Let's assume e represents 5.
Let's assume f represents 6.
Let's assume g represents 7.
Let's assume h represents 8.
Let's assume i represents 9.
Let's assume j represents 10.
Let's assume k represents 11.
Let's assume l represents 12.
Let's assume m represents 13.
Let's assume n represents 14.
Let's assume o represents 15.
Let's assume p represents 16.
Let's assume q represents 17.
Let's assume r represents 18.
Let's assume s represents 19.
Let's assume t represents 20.
Let's assume u represents 21.
Let's assume v represents 22.
Let's assume w represents 23.
Let's assume x represents 24.
Let's assume y represents 25.
Let's assume z represents 26.

迭代字典中的元素

```python
phonebook1 = {'ann':6575, 'bob':8982, 'joe':2598, 'zoe':1225, 'ann':6585}

for key in phonebook1:
print(key, phonebook1[key])
```

ann 6585
bob 8982
joe 2598
zoe 1225

```python
phonebook1 = {'ann':6575, 'bob':8982, 'joe':2598, 'zoe':1225, 'ann':6585}

for key, value in phonebook1.items():
print(key, value)
```

ann 6585
bob 8982
joe 2598
zoe 1225

总结

这一章的内容,只不过是 “多” 而已,一旦逻辑关系理顺,就会觉得很简单。而这一章的开头,已经是最好的总结了。

最后需要补充的,只是两个参考链接,以后有什么搞不明白的地方,去那里翻翻就能找到答案:

> * https://docs.python.org/3/tutorial/datastructures.html#dictionaries
> * https://docs.python.org/3/library/stdtypes.html#typesmapping

文件

我们需要处理的数据,一定是很多,所以才必须由计算机帮我们处理 —— 大量的数据保存、读取、写入,需要的就是文件(Files)。在这一章里,我们只介绍最简单的文本文件。

创建文件

创建一个文件,最简单的方式就是用 Python 的内建函数 `open()`。

`open()` 函数的[官方文档](https://docs.python.org/3/library/functions.html#open)很长,以下是个简化版:

> `open(file, mode='r')`

第二个参数,`mode`,默认值是 `'r'`,可用的 `mode` 有以下几种:

| 参数字符 | 意义 |
| -------- | ------------------------------- |
| `'r'` | 只读模式 |
| `'w'` | 写入模式(重建)|
| `'x'` | 排他模式 —— 如果文件已存在则打开失败 |
| `'a'` | 追加模式 —— 在已有文件末尾追加 |
| `'b'` | 二进制文件模式 |
| `'t'` | 文本文件模式(默认)|
| `'+'` | 读写模式(更新)|

创建一个新文件,用这样一个语句就可以:

```python
open('/tmp/test-file.txt', 'w')
```

<_io.TextIOWrapper name='/tmp/test-file.txt' mode='w' encoding='UTF-8'>

当然,更多的时候,我们会把这个函数的返回值,一个所谓的 [file object](https://docs.python.org/3/glossary.html#term-file-object),保存到一个变量中,以便后面调用这个 file object 的各种 Methods,比如获取文件名 `file.name`,比如关闭文件 `file.close()`:

```python
f = open('/tmp/test-file.txt', 'w')
print(f.name)
f.close()
```

/tmp/test-file.txt

删除文件

删除文件,就得调用 `os` 模块了。删除文件之前,要先确认文件是否存在,否则删除命令会失败。

```python
import os

f = open('/tmp/test-file1.txt', 'w')
print(f.name)
f.close() #关闭文件,否则无法删除文件
if os.path.exists(f.name):
os.remove(f.name)
print(f'{f.name} deleted.')
else:
print(f'{f.name} does not exist.')
```

/tmp/test-file1.txt
/tmp/test-file1.txt deleted.

读写文件

创建文件之后,我们可以用 `f.write()` 把数据写入文件,也可以用 `f.read()` 读取文件。

```python
f = open('/tmp/test-file.txt', 'w')
f.write('first line\nsecond line\nthird line\n')
f.close()

f = open('/tmp/test-file.txt', 'r')
s = f.read()
print(s)
f.close()
```

first line
second line
third line

文件有很多行的时候,我们可以用 `file.readline()` 操作,这个 Method 每次调用,都会返回文件中的新一行。

```python
f = open('/tmp/test-file.txt', 'w')
f.write('first line\nsecond line\nthird line\n')
f.close()

f = open('/tmp/test-file.txt', 'r')
s = f.readline() # 返回的是 'first line\n'
print(s)
s = f.readline() # 返回的是 'second line\n'
print(s)
f.close()
```

first line
second line

**注意**,返回结果好像跟你想的不太一样。这时候,之前见过的 `str.strip()` 就派上用场了:

```python
f = open('/tmp/test-file.txt', 'w')
f.write('first line\nsecond line\nthird line\n')
f.close()

f = open('/tmp/test-file.txt', 'r')
s = f.readline().strip() # 返回的是 'first line','\n' 被去掉了……
print(s)
s = f.readline().strip() # 返回的是 'second line','\n' 被去掉了……
print(s)
f.close()
```

first line
second line

与之相对的,我们可以使用 `file.readlines()` 这个 Method,将文件作为一个列表返回,列表中的每个元素对应着文件中的每一行:

```python
f = open('/tmp/test-file.txt', 'w')
f.write('first line\nsecond line\nthird line\n')
f.close()

f = open('/tmp/test-file.txt', 'r')
s = f.readlines() # 返回的是一个列表,注意,readlines,最后的 's'
print(s)
f.close()
```

['first line\n', 'second line\n', 'third line\n']

既然返回的是列表,那么就可以被迭代,逐一访问每一行:

```python
f = open('/tmp/test-file.txt', 'w')
f.write('first line\nsecond line\nthird line\n')
f.close()

f = open('/tmp/test-file.txt', 'r')
for line in f.readlines():
print(line)
f.close()
```

first line
second line
third line

与之相对的,我们也可以用 `file.writelines()` 把一个列表写入到一个文件中,按索引顺序(从 0 开始)逐行写入列表的对应元素::

```python
a_list = ['first line\n', 'second line\n', 'third line\n']
f = open('/tmp/test-file.txt', 'w')
f.writelines(a_list)
f.close()

f = open('/tmp/test-file.txt', 'r')
for line in f.readlines():
print(line)
f.close()
```

first line
second line
third line

with 语句块

针对文件操作,Python 有个另外的语句块写法,更便于阅读:

```python
with open(...) as f:
f.write(...)
...
```

这样,就可以把针对当前以特定模式打开的某个文件的各种操作都写入同一个语句块了:

```python
import os

with open('/tmp/test-file.txt', 'w') as f:
f.write('first line\nsecond line\nthird line\n')

with open('/tmp/test-file.txt', 'r') as f:
for line in f.readlines():
print(line)

if os.path.exists(f.name):
os.remove(f.name)
print(f'{f.name} deleted.')
else:
print(f'{f.name} does not exist.')
```

first line
second line
third line
test-file.txt deleted.

另外,用 `with` 语句块的另外一个附加好处就是不用写 `file.close()` 了……

另一个完整的程序

若干年前,我在写某本书的时候,需要一个例子 —— 用来说明 “**即便是结论正确,论证过程乱七八糟也不行!**”

作者就是这样,主要任务之一就是给论点找例子找论据。找得到不仅*恰当*且又*精彩*的例子和论据的,就是好作者。后面这个 “*精彩*” 二字要耗费很多时间精力,因为它意味着说 “要找到*很多*例子而后在里面选出*最精彩*的那个!” —— 根本不像很多人以为的那样,是所谓的 “信手拈来”。

找了很多例子都不满意…… 终于有一天,我看到这么个说法:

> 如果把字母 `a` 计为 `1`、`b` 计为 `2`、`c` 计为 `3` …… `z` 计为 `26`,那么:
>
> - knowledge = 96
> - hardwork = 98
> - attitude = 100
>
> 所以结论是:
>
> - 知识(*knowledge*)与勤奋(*hardwork*)固然都很重要;
> - 但是,决定成败的却是态度(**attitude**)!

结论虽然有道理 —— 可这论证过程实在是太过分了罢……

我很高兴,觉得这就是个*好例子*!并且,加工一下,会让读者觉得很精彩 —— 如果能找到一些按照同样的计算方式能得到 100 的单词,并且还是那种一看就是 “反例” 的单词……

凭直觉,英文单词几十万,如此这般等于 100 的单词岂不是数不胜数?并且,一定会有很多负面意义的单词如此计算也等于 100 罢?然而,这种事情凭直觉是不够的,手工计算又会被累死…… 于是,面对如此荒谬的论证过程,我们竟然 “无话可说”。

幸亏我是会写程序的人。所以,不会 “干着急没办法”,我有能力让计算机帮我把活干了。

很快就搞定了,找到很多很多个如此计算加起来等于 100 的英文单词,其中包括:

> - connivance(纵容)
> - coyness(羞怯)
> - flurry(慌张)
> - impotence(阳痿)
> - stress(压力)
> - tuppence(微不足道的东西)
> - ……

所以,决定成败的可以是 “慌张”(flurry),甚至是 “阳痿”(impotence)?这不明显是胡说八道嘛!

—— 精彩例子制作完毕,我把它放进了书里。

那,具体的过程是什么样的呢?

首先我得找到一个英文单词列表,很全的那种。这事用不着写程序,Google 一下就可以了。我搜索的关键字是 “[english word list](https://www.google.com/search?q=english+word+list)”,很直观吧?然后就找到一个:[https://github.com/dwyl/english-words](https://github.com/dwyl/english-words);这个链接里有一个 [words-alpha.txt](https://raw.githubusercontent.com/dwyl/english-words/master/words_alpha.txt) 文件,其中包含接近 37,0101 个单词,应该够用了!下载下来用程序处理就可以了!

因为文件里每行一个单词,所以,就让程序打开文件,将文件读入一个列表,而后迭代这个列表,逐一计算那个单词每个字母所代表的数字,并加起来看看是否等于 100?如果是,就将它们输出到屏幕…… 好像不是很难。

```python
with open('words_alpha.txt', 'r') as file:
for word in file.readlines():
pass # 先用 pass 占个位,一会儿再写计算过程
```

按照上面那说法,把 `a` 记为 `1`,直至把 `z` 记为 `26`,这事并不难,因为有 `ord()` 函数啊 —— 这个函数返回字符的 Unicode 编码:`ord('a')` 的值是 `97`,那按上面的说法,用 `ord('a') - 96` 就相当于得到了 `1` 这个数值…… 而 `ord('z') - 96` 就会得到 `26` 这个数值。

```python
ord('a')
```

97

那么,计算 `'knowledge'` 这个字符串的代码很简单:

```python
word = 'knowledge'
sum = 0
for char in word:
sum += ord(char) - 96
print(sum)
```

96

果然,得到的数值等于 `96` —— 不错。把它写成一个函数罢:`sum_of_word(word)`:

```python
def sum_of_word(word):
sum = 0
for char in word:
sum += ord(char) - 96
return sum

sum_of_word('attitude')
```

100

那让程序就算把几十万行都算一遍也好像很简单了:

```python
def sum_of_word(word):
sum = 0
for char in word:
sum += ord(char) - 96
return sum

with open('words_alpha.txt', 'r') as file:
for word in file.readlines():
if sum_of_word(word) == 100:
print(word)
```

abstrusenesses
acupuncturist
adenochondrosarcoma
...
worshipability
zeuctocoelomatic
zygapophysis

嗯?怎么输出结果跟想得不一样?找到的词怎么都 “奇形怪状” 的…… 而且,输出结果中也没有 `attitude` 这个词。

插入个中止语句,`break`,把找到的第一个词中的每个字符和它所对应的值都拿出来看看?

```python
def sum_of_word(word):
sum = 0
for char in word:
sum += ord(char) - 96
return sum

with open('words_alpha.txt', 'r') as file:
for word in file.readlines():
if sum_of_word(word) == 100:
print(word)
for c in word: # 把字母和值都打出来,看看对不对?
print(c, ord(c) - 96)
break # 找到一个之后就停下来。
```

abstrusenesses

a 1
b 2
s 19
t 20
r 18
u 21
s 19
e 5
n 14
e 5
s 19
s 19
e 5
s 19

-86

怎么有个 `-86`?!仔细看看输出结果,看到每一行之间都被插入了一个空行,想到应该是从文件里读出的行中,包含 `\n` 这种换行符…… 如果是那样的话,那么 `ord('\n') -96` 返回的结果是 `-86` 呢,怪不得找到的词都 “奇形怪状” 的……

```python
ord('\n') -96
```

-86

改进一下呗 —— 倒也简单,在计算前把读入字符串前后的空白字符都给删掉就好了,用 `str.strip()` 就可以了:

```python
def sum_of_word(word):
sum = 0
for char in word:
sum += ord(char) - 96
return sum

with open('words_alpha.txt', 'r') as file:
for word in file.readlines():
if sum_of_word(word.strip()) == 100:
print(word)
```

abactinally
abatements
abbreviatable
...
zithern
zoogleas
zorgite

如果想把符合条件的词保存到一个文件 `results.txt` 里的话,那么:

```python
def sum_of_word(word):
sum = 0
for char in word:
sum += ord(char) - 96
return sum

with open('results.txt', 'w') as result:
with open('words_alpha.txt', 'r') as file:
for word in file.readlines():
if sum_of_word(word.strip()) == 100:
result.write(word)
```

竟然这么简单就搞定了?!

这 10 行的代码,在几秒钟内从 370,101 个英文单词中找到 3,771 个如此计算等于 100 的词汇。

喝着咖啡翻一翻 `results.txt`,很快就找到了那些用来做反例格外恰当的词汇。

真无法想象当年的自己若是不懂编程的话现在会是什么样子……

总结

这一章我们介绍了文本文件的基本操作:

> * 打开文件,直接用内建函数,`open()`,基本模式有 `r` 和 `w`;
> * 删除文件,得调用 `os` 模块,使用 `os.remove()`,删除文件前最好确认文件确实存在……
> * 读写文件分别有 `file.read()`、`file.write()`、`file.readline()`、`file.readlines()`、`file.writelines()`;
> * 可以用 `with` 把相关操作都放入同一个语句块……

如何从容应对含有过多 “过早引用” 的知识?

“过早引用”([Forward References](https://en.wikipedia.org/wiki/Forward_declaration#id=Forward_reference),另译为 “前置引用”),原本是计算机领域的术语。

在几乎所有的编程语言中,对于变量的使用,都有 “先声明再使用” 的要求。直接使用未声明的变量是被禁止的。Python 中,同样如此。如果在从未给 `an_undefined_variable` 赋值的情况下,直接调用这个变量,比如,`print(an_undefined_variable)`,那就会报错:`NameError: name 'an_undefined_variable' is not defined`。

```python
print(an_undefined_variable)
```

---------------------------------------------------------------------------

NameError Traceback (most recent call last)

<ipython-input-1-7e0e1cc14e37> in <module>
----> 1 print(an_undefined_variable)

NameError: name 'an_undefined_variable' is not defined

充满过早引用的知识结构,在大脑中会构成类似 M.C. Escher 善画的那种 “不可能图形” 那样的 “结构”。

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/four-cones.png?raw=true)

在上图中,前三个椎形一般不会造成视觉困惑 —— 尤其是第一个。

若是加上虚线,比如,第二个和第三个,那么由于我们预设虚线表示 “原本应该看不见的部分”,于是,`C` 点的位置相对于 `B` 和 `D` 应该更靠近自己;`C'` 的位置,相对于 `B'` 和 `D'` 应该更远离自己……

然而,在第四个椎形中,由于 `B"D"` 和 `A"C"` 都是实线,于是,我们一下子就失去了判断依据,不知道 `C"` 究竟是离自己更近还是更远?

对一个点的位置困惑,连带着它与其它三个点之间的关系。可若那不是锥体,而是立方体呢?每个点的位置困惑会造成对它与更多点之间的更多联系的困惑…… 若是更多面体呢?

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/2_necker_cubes.png?raw=true)

把这些令人困惑的点,比喻成 “过早引用”,你就明白为什么 “很多过早引用” 的知识结构会那么令人困惑,处理起来那么令人疲惫了吧?

过早引用就是无所不在

可生活、学习、工作,都不是计算机,它们可不管这套,管它是否定义过,管它是否定义清晰,直接甩出来就用的情况比比皆是。

对绝大多数 “不懂事” 的小朋友来说,几乎所有痛苦的根源都来自这里:“懂事” 的定义究竟是怎样的呢?什么样算作懂事,什么样算作不懂事?弄不好,即便整个童年都在揣摩这个事,到最后还是迷迷糊糊。他们的父母,从未想过对孩子说话也好提要求也好,最好 “先声明再使用”,或者即便事先声明过也语焉不详…… 于是,这些可怜的孩子,能做的只有在惶恐中摸索,就好像在黑暗中拼图一样。

可事实上,他们的父母也不容易。因为确实有太多细节,给小朋友讲了也没用,或者讲也讲不清楚,又或者拼命解释清楚了,但小朋友就是听不进去…… 所以,令人恼火的 “过早引用”,有时候真的是只能那样的存在。

谈恋爱的时候也是这样。太多的概念,千真万确地属于过早引用。爱情这东西究竟是什么,刚开始的时候谁都弄不大明白。并且事实证明,身边的绝大多数人跟自己一样迷糊。至于从小说电影里获得的 “知识”,虽然自己看心神愉悦,但几乎肯定给对方带来无穷无尽的烦恼 —— 于对方来说你撒出来的是漫天飞舞的过早引用……

到了工作阶段,技术岗位还相对好一点,其他领域,哪哪儿都是过早引用,并且还隐藏着不可见,搞得人们都弄出了一门玄学,叫做 “潜规则”。

人们岁数越大,交朋友越来越不容易。最简单的解释就是,每个人的历史,对他人来说都构成 “过早引用”。所以,理解万岁?太难了吧,幼儿园、小学的时候,人们之间几乎不需要刻意相互理解,都没觉得有这个必要;中学的时候,相互理解就已经开始出现不同程度的困难了,因为过早引用的积累。大学毕业之后,再工作上几年,不仅相互理解变得越来越困难,还有另外一层更大的压力 —— 生活中要处理的事情越来越多,脑力消耗越来越大,遇到莫名其妙的过早引用,哪儿有心思处理?

不懂也要硬着头皮读完

这是事实:大多数难以掌握的技能都有这个特点。人们通常用 “学习曲线陡峭” 来形容这类知识,只不过,这种形容只限于形容而已,对学习没有实际的帮助。面对这样的实际情况,有没有一套有效的应对策略呢?

首先是要学会一个重要的技能:

> **读不懂也要读完,然后重复很多遍。**

这是最重要的起点。听起来简单,甚至有点莫名其妙 —— 但以后你就会越来越深刻地体会到,这么简单的策略,绝大多数人竟然不懂,也因此吃了很多很多亏。

充满了过早引用的知识结构,就不可能是一遍就读懂的。别说这种信息密度极高的复杂且重要的知识获取了,哪怕你去看一部好电影,也要多刷几遍才能彻底看懂,不是嘛?比如,Quentin Tarantino 导演的 [Pulp Fiction (1994)](https://www.imdb.com/title/tt0110912/)、David Fincher 导演的 [Fight Club (1999)](https://www.imdb.com/title/tt0137523/)、Christopher Nolan 导演的 [Inception (2010)](https://www.imdb.com/title/tt1375666/)、或者 Martin Scorsese 导演的 [Shutter Island (2010)](https://www.imdb.com/title/tt1130884/)……

所以,从一开始就要做好*将要重复很多遍*的准备,从一开始就要做好*第一次只能读懂个大概*的准备。

古人说,读书百遍其义自见,道理就在这里了 —— 只不过,他们那时候没有计算机术语可以借用,所以,这道理本身成了 “过早引用”,对那些根本就没有过 “读书百遍” 经历的人,绝对以为那只不过是在忽悠自己……

有经验的读书者,拿来一本书开始自学技能的时候,他会先翻翻目录(Table Of Contents),看看其中有没有自己完全没有接触过的概念;然后再翻翻术语表(Glossary),看看是否可以尽量理解;而后会看看索引(Index),根据页码提示,直接翻到相关页面进一步查找…… 在通读书籍之前,还会看看书后的参考文献(References),看看此书都引用了哪些大牛的书籍,弄不好会顺手多买几本。

这样做,显然是老到 —— 这么做的最大好处是 “尽力消解了大量的过早引用”,为自己减少了极大的理解负担。

所以,第一遍的正经手段是 “**囫囵吞枣**地读完”。

囫囵吞枣从一开始就是贬义词。但在当前这个特殊的情况下,它是最好的策略。那些只习惯于一上来就仔细认真的人,在这里很吃亏,因为他们越是仔细认真,越是容易被各种过早引用搞得灰心丧气;相应地,他们的挫败感积累得越快;到最后弄不好最先放弃的是他们 —— 失败的原因竟然是因为 “太仔细了”……

第一遍囫囵吞枣,用个正面一点的描述,就是 “*为探索未知领域先画个潦草的地图*”。地图这东西,有总比没有好;虽然说它最好精确,但即便是 “不精确的地图” 也比 “完全没地图” 好一万倍,对吧?更何况,这地图总是可以不断校正的,不是吗?世界上哪个地图不是一点一点校正过来才变成今天这般精确的呢?

磨练 “只字不差” 的能力

通过阅读习得新技能(尤其是 “尽量只通过阅读习得新技能”),肯定与 “通过阅读获得心灵愉悦” 很不相同。

读个段子、读个小说,读个当前热搜文章,通常情况下不需要 “精读” —— 草草浏览已经足够,顶多对自己特别感兴趣的地方,慢下来仔细看看……

但是,若是为了习得新技能去阅读,就要施展 “**只字不差地阅读**” 这项专门的技能。

对,“只字不差地阅读” 是所有自学能力强的人都会且都经常使用的技能。尤其是当你在阅读一个重要概念的定义之时,你就是这么干的:定义中的每个字都是有用的,每个词的内涵外延都是需要进行推敲的,它是什么,它不是什么,它的内涵外延都是什么,因此,在使用的时候需要注意什么……

很有趣的一个现象是,绝大多数自学能力差的人,都是把一切都当作小说去看,随便看看,粗略看看……

你有没有注意到一个现象,人们在看电影的时候,绝大多数人会错过绝大多数细节;但这好像并不会削减他们的观影体验;并且,他们有能力使用错过了无数细节之后剩下的那些碎片拼接出一个 “完整的故事” —— 当然,通常干脆是 “另一个貌似完整的故事”。于是,当你在跟他们讨论同一个电影的时候,常常像是你们没坐在同一个电影院,看的不是同一个电影似的……

所谓的自学能力差,很可能最重要的坑就在这里:

> 每一次学习新技能的时候,很多人只不过是因为做不到只字不差地阅读,于是总是会错过很多细节;于是,最终就好像 “看了另外一个山寨版电影一样”,实际上 “习得了另外一个山寨版技能”……

在学习 Python 语言的过程中,有个例子可以说明以上的现象。

在 Python 语言中,`for` 循环可以附加一个 `else` 部分。你到 Google 上搜索一下 [`for else python`](https://www.google.com/search?q=for+else+python) 就能看到有多少人在 “追问” 这是干什么的?还有另外一些链接,会告诉你 “for... else” 这个 “秘密” 的含义,将其称为 “语法糖” 什么的……

其实,[官方教程](https://docs.python.org/3/tutorial/controlflow.html#break-and-continue-statements-and-else-clauses-on-loops)里写的非常清楚的,并且还给出了一个例子:

> Loop statements may have an else clause; it is executed when the loop terminates through exhaustion of the list (with for) or when the condition becomes false (with while), but not when the loop is terminated by a break statement. This is exemplified by the following loop, which searches for prime numbers:
>
> ```python
> >>> for n in range(2, 10):
> ... for x in range(2, n):
> ... if n % x == 0:
> ... print(n, 'equals', x, '*', n//x)
> ... break
> ... else:
> ... # loop fell through without finding a factor
> ... print(n, 'is a prime number')
> ...
> 2 is a prime number
> 3 is a prime number
> 4 equals 2 * 2
> 5 is a prime number
> 6 equals 2 * 3
> 7 is a prime number
> 8 equals 2 * 4
> 9 equals 3 * 3
> ```

只有两种情况,

> * 要么干脆就没读过,
> * 要么是读了,却没读到这个细节……

—— 后者更为可怕,跟花了同样的钱看了另外一个残缺版本的电影似的……

为什么说 “只字不差地阅读” 是一项专门的技能呢?你自己试过就知道了。明明你已经刻意让自己慢下来,也刻意揣摩每个字每个词的含义,甚至为了理解正确,做了很多笔记…… 可是,当你再一次 “只字不差地阅读” 的时候,你经常会 “惊讶地发现”,自己竟然有若干处遗漏的地方!对,这就是一种需要多次练习、长期训练才能真正掌握的技能。绝对不像听起来那么简单。

所以,_到了第二遍第三遍就必须施展 “只字不差地阅读” 这项专门的技能了_,只此一点,你就已然**与众不同**了。

好的记忆力很重要

“就算读不懂也要读完” 的更高境界,是 “**就算不明白也要先记住**”。

人们普遍讨厌 “死记硬背”…… 不过,说实话,这很肤浅。虽然确实也有 “擅长死记硬背却就是什么都不会的人”,但是,其实有更多记忆力强的人,实际上更可能是 “博闻强识”。

面对 “过早引用” 常见的知识领域,好记忆力是超强加分项。记不清、记不住、甚至干脆忘了 —— 这是自学过程中最耽误事的缺点。尤其在有 “过早引用知识点” 存在的时候,更是如此。

然而,很多人并没有意识到的是,记忆力也是 “一门手艺” 而已。并且,事实上,它是任何时候都可以通过刻意练习加强的 “手艺”。

更为重要的是,记忆力这个东西,有一百种方法去弥补 —— 比如,最明显、最简单的办法就是 “好记性不如烂笔头”……

所以,在绝大多数正常情况下,所谓的 “记不清、记不住、甚至干脆忘了”,都只不过是懒的结果 —— 若是一个人懒,且不肯承认自己懒,又因为不肯承认而已就不去纠正,那…… 那就算了,那就那么活下去罢。

然而,提高对有效知识的记忆力还有另外一个简单实用的方法 —— 而市面上有各种 “快速记忆法”,通常相对于这个方法来看用处并不大。

这个方法就是以下要讲到的 “整理归纳总结” —— **反复做整理归纳总结,记不住才怪呢!**

尽快开始整理归纳总结

从另外一个角度,这类体系的知识书籍,对作者来说,不仅是挑战,还是摆脱不了的负担。

Python 官方网站上的 [The Python Tutorial](https://docs.python.org/3/tutorial/index.html),是公认的最好的 Python 教材 —— 因为那是 Python 语言的作者 [Guido van Rossum](https://en.wikipedia.org/wiki/Guido_van_Rossum) 写的……

虽然 Guido van Rossum 已经很小心了,但还是没办法在讲解上避免大量的过早引用。他的小心体现在,在目录里就出现过五次 **More**:

> * More Control Flow Tools
> * More on Defining Functions
> * More on Lists
> * More on Conditions
> * More on Modules

好几次,他都是先粗略讲过,而后在另外一处再重新深入一遍…… 这显然是一个最尽力的作者了 —— 无论是在创造一个编程语言上,还是在写一本教程上。

然而,即便如此,这本书对任何初学者来说,都很难。当个好作者不容易。

于是,这只能是读者自己的工作了 —— 因为即便是最牛的作者,也只能到这一步了。

第一遍囫囵吞枣之后,马上就要开始 “**总结**、**归纳**、**整理**、**组织** 关键知识点” 的工作。自己动手完成这些工作,是所谓学霸的特点。他们只不过是掌握了这样一个其他人从未想过必须掌握的简单技巧。他们一定有个本子,里面是各种*列表*、*示意图*、*表格* —— 这些都是最常用的知识(概念)整理组织归纳工具,这些工具的用法看起来简单的要死。

这个技巧说出来、看起来都非常简单。然而,也许正因为它看起来如此简单,才被绝大多数人忽略…… 与学霸们相对,绝大多数非学霸都有一模一样的糊弄自己的理由:反正有别人做好的,拿过来用就是了!—— 听起来那么理直气壮……

可实际上,**自己动手做做就知道了** —— 整理、归纳、组织,再次反复,是个相当麻烦的过程。非学霸们自己不动手做的真正原因只不过是:嫌麻烦、怕麻烦。一个字总结,就是,**懒**!可是,谁愿意承认自己懒呢?没有人愿意。于是,都给自己找个冠冕堂皇的理由,比如,上面说的 “反正别人已经做好了,我为什么还要再做一遍呢?” 再比如,“这世界就是懒人推进的!”

久而久之,各种爱面子的说法完美地达成了自我欺骗的效果,最后连自己都信了!于是,身上多了一个明明存在却永远找不到的漏洞 —— **且不自知**。

我在第一次粗略读过整个 [Python Official Tutorial](https://docs.python.org/3/tutorial/datastructures.html) 中的第五章之后,顺手整理了一下 Containers 的概念表格:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/python-containers.png?raw=true)

**可这张图错了!**

因为我最早 “合理囫囵吞枣” 的时候,`Bytes` 这种数据类型全部跳过;而后来多轮反复之后继续深入,又去读 [The Python Language Reference](https://docs.python.org/3/reference/datamodel.html) 的第五章 `Data Model` 之后,` 发现 Set` 也有 Immutable,是 `Frozen Set`…… 当然,最错的是,整理的过程中,一不小心把 “Ordered” 给弄反了!

于是肯定需要再次整理,若干次改进之后,那张图就变成了下面这个样子:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/python-containers-final.png?raw=true)

> 另外,从 Python 3.7 开始,Dictionary 是 insertion ordered 了:<br />
https://docs.python.org/3/library/collections.html#ordereddict-objects

这个自己动手的过程其实真的 “很麻烦”,但它实际上是帮助自己强化记忆的过程,并且对自我记忆强化来说,绝对是不可或缺的过程。习惯于自己动手做罢!习惯于自己不断修改罢!

再给你看个善于学习的人的例子:

> https://nvie.com/posts/iterators-vs-generators/

作者 Vincent Driessen 在这个帖子里写到:

> I'm writing this post as a pocket reference for later.

人家随手做个图,都舍不得不精致:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/iter-relationships.gif?raw=true)

自学能力强的人有个特点,就是**不怕麻烦**。小时候经常听到母亲念叨,“怕麻烦!那还活着干嘛啊?活着多麻烦啊!” —— 深刻。

先关注使用再研究原理

作为人类,我们原本很擅长运用自己并不真正理解的物件、技能、原理、知识的……

三千多年以前,一艘欧洲腓尼基人的商船在贝鲁斯河上航行的时候搁浅了…… 于是,船员们纷纷登上沙滩。饿了怎么办?架火做饭呗。吃完饭,船员们惊讶地发现锅下面的沙地上有很多亮晶晶、闪闪发光的东西!今天的化学知识对当年的他们来说,是那一生不可触摸的 “过早引用”。他们并不懂这个东西的本质、原理,但稍加研究,他们发现的是,锅底沾有他们运输的天然苏打…… 于是,他们的总结是,天然苏打和沙子(我们现在知道沙子中含有石英砂)被火烧,可能会产生这个东西。几经实验,成功了。于是,腓尼基人学会了制做玻璃球,发了大财……

两千五六百年之前,释加牟尼用他的理解以及在那个时代有限的概念详细叙述了打坐的感受 —— 他曾连续打坐 6 年。今天,西方科学家们在深入研究脑科学的时候,发现 [Meditation](https://en.wikipedia.org/wiki/Meditation) 对大脑有特别多的好处…… 这些好处就是好处,与宗教全然没有任何关系的好处。

> * [Harvard neuroscientist: Meditation not only reduces stress, here's how it changes your brain](https://www.washingtonpost.com/news/inspired-life/wp/2015/05/26/harvard-neuroscientist-meditation-not-only-reduces-stress-it-literally-changes-your-brain/)
> * [This Is Your Brain on Meditation -- The science explaining why you should meditate every day](https://www.psychologytoday.com/us/blog/use-your-mind-change-your-brain/201305/is-your-brain-meditation)
> * [Researchers study how it seems to change the brain in depressed patients](https://news.harvard.edu/gazette/story/2018/04/harvard-researchers-study-how-mindfulness-may-change-the-brain-in-depressed-patients/)
> * [Meditation's Calming Effects Pinpointed in the Brain](https://www.scientificamerican.com/article/meditations-calming-effects-pinpointed-in-brain/)
> * [Different meditation types train distinct parts of your brain](https://www.newscientist.com/article/2149489-different-meditation-types-train-distinct-parts-of-your-brain/)

你看,我们原本就是可以直接使用自己并不真正理解的物件、技能、原理、知识的!可为什么后来就不行了呢?

或者说,从什么时候开始,我们开始害怕自己并不真正理解的东西,不敢去用,甚至连试都不敢去试了呢?

有一个相当恼人的解释:**上学上坏了**。

学校里教的全都是属于 “先声明再使用” 的知识。反过来,不属于这种体系架构的知识,学校总是回避的 —— 比如,关于投资与交易的课程,从来看不见地球上有哪个义务教育体系把它纳入教学范围。虽然,投资与交易,是每个人都应该掌握、都必须掌握的不可或缺的技能,某种意义上它甚至比数学语文都更重要,然而,学校就是不会真教这样的东西。

而且,现在的人上学的时间越来越长。小学、初中、高中、本科加起来 16 年…… 这么长时间的 “熏陶”,只能给大多数人造成幻觉,严重、深刻,甚至不可磨灭的幻觉,误以为所有的知识都是这种类型…… 可偏偏,这世界上真正有用的、真正必要的知识,几乎全都不是这种类型 —— 颇令人恼火。

现在的你,不一样了 —— 你要跳出来。养成一个习惯:

> 不管怎么样,先用起来,反正,研究透原理,不可能马上做到,需要时间漫漫。

用错了没关系,改正就好。用得不好没关系,用多了就会好。只要开始用起来,理解速度就会加快 —— 实践出真知,不是空话。

有的时候,就是因为没有犯过错,所以不可能有机会改正,于是,就从未做对过。

尊重前人的总结和建议

生活中,年轻人最常犯的错误就是把那句话当作屁:

> 不听老人言,吃亏在眼前。

对年轻人来讲,老人言确实很讨厌,尤其是与自己当下的感受相左的时候。

然而,这种 “讨厌” 的感觉,更多的时候是陷阱,因为那些老人言只不过是过早引用,所以,在年轻人的脑子里 “无法执行”,“报错为类型错误”……

于是,很多人一不小心就把 “不听老人言” 和 “独立思考” 混淆起来,然后最终自己吃了亏。可尴尬在于,等自己意识到自己吃亏了的时候吧,大量的时间早已飘逝,是为 “无力回天”。

你可以观察到一个现象,学霸(好学生)的特点之一就是 “老师让干啥就干啥”,没废话。

比如,上面告诉你了,“必须自己动手”,那你就从现在开始老老实实地在一切必要的情况下自己动手去 “**总结**、**归纳**、**整理**、**组织** 关键知识点”…… 那你就必然能够学好。但针对这么个建议,你反复在那里问,“为什么呀?”,“有没有更简单的办法啊?”…… 那你就完了,死定了。

学写代码的过程中,有很多重要的东西实际上并不属于 “编程语言范畴”。比如,如何为变量命名、如何组织代码,这些 “规范”,不是违背了就会马上死掉的<a href='#fn1' name='fn1b'><sup>[1]</sup></a>;并且,初来乍到的时候,这些东西看起来就是很啰嗦、很麻烦的…… 然而,这些东西若是不遵守,甚至干脆不了解,那么最终的结果是,你永远不可能写出大项目,永远是小打小闹 —— 至于为什么,可以用那句你最讨厌的话回答你:

> 等你长大了就懂了……

自学编程的好处之一,就是有机会让一个人见识到 “规范”、“建议” 的好处。也有机会让一个人见识到不遵守这些东西会吃怎样的亏(往往是现世报)。

Python 中有一个概念叫 PEP,Python Enhancement Proposals,必须找时间阅读,反复阅读,牢记于心:

> https://www.python.org/dev/peps/pep-0008/

到最后,你会体会到,这不只是编程的事,这种东西背后的思考与体量,对整个人生都有巨大帮助。

-----
**脚注**

<a name='fn1'>[1]</a>:也可能真的会死…… 请看一篇 2018 年 9 月份的一则新闻,发生在旧金山的事情:[Developer goes rogue, shoots four colleagues at ERP code maker](https://www.theregister.co.uk/2018/09/20/developer_work_shooting/)

<a href='#fn1b'><small>↑Back to Content↑</small></a>

官方教程:The Python Tutorial

虽然,第一部分总计七章关于编程内容的编排是非常特别且相当有效的:

> * 它并没有像其它教程那样,从 “Hello world!” 入手;
> * 它也没有使用与市面上所有编程教材一样的内容先后顺序;
> * 它一上来就让你明白了程序的灵魂:布尔运算;
> * 它很快就让你明白有意义的程序其实只有两个核心构成:运算和流程控制;
> * 它让你很快理解函数从另外一个角度看只不过是 “程序员作为用户所使用的产品”;
> * 它让你重点掌握了最初级却最重要的数据类型,字符串;
> * 它让你从容器的角度了解了 Python 中绝大多数 “重要的数据类型”;
> * 最重要的是,它不承诺你 “速成”,但承诺 “领你入门”…… 显然,它做到了。

但是,第一部分的内容核心目标是让你 “**脱盲**” —— 它的作用还做不到让你 “已然学会编程”,它更多是让你从此开始有能力去阅读更多的重要资源,比如,官方的教程和参考。第一部分的内容更像地图上的 “**图例**”,而不是地图本身。

第一部分反复读过之后,最重要的结果就是:

> 现在你有能力自己查询官方文档了……

起码,在此之后,再去阅读 [The Python Tutorial](https://docs.python.org/3/tutorial/index.html),不那么费力了,最起码,可以靠自己理解绝大多数内容……

在继续阅读本书内容的同时,有空就要反复翻 The Python Tutorial。

官方文档中最重要的链接

Python 也许是目前所有编程语言中在文档建设(Documenting)方面做得最好的(好像真的不需要在这句话后面加上 “之一”)。Python 社区为了建设完善的文档,甚至有专门的文档制作工具 —— 得益于 Python 社区从一开始就非常重视[文档规范](https://devguide.python.org/documenting/) —— [Sphinx](http://www.sphinx-doc.org/en/master/)。你在网络上最经常看到的计算机类文档,很可能都在这个网站上:[Read the Docs](https://readthedocs.org)……

Python 的官方文档网址是:

> https://docs.python.org/3/

其中对初学者最重要的两个链接是:

> * **[Tutorial](https://docs.python.org/3/tutorial/index.html)**: https://docs.python.org/3/tutorial/index.html
> * **[Library Reference](https://docs.python.org/3/library/index.html)**: https://docs.python.org/3/library/index.html

理论上来讲,只要有了基础的概念,自己反复阅读官方的 The Python Tutorial 是最好的,没什么入门书籍比它更好 —— 因为它的作者是 Python 的作者,那个被称为 “善意独裁者” 的 [Guido van Rossum](https://en.wikipedia.org/wiki/Guido_van_Rossum)。

此人很帅,但更帅的是他的车牌(摘自 Guido van Rossume 的[个人主页](https://gvanrossum.github.io)):

[〔图〕](https://gvanrossum.github.io/images/license.jpg?raw=true)

为什么一定要阅读官方文档

买一两本 Python 教程是不可能完整掌握 Python 的 —— 其实,这句话里的 Python 替换成任何一种语言也是一样的。

教程和官方文档的各种属性是非常不一样的,比如,针对读者群,组织方式,语言表达…… 最不一样的地方在 “全面性”。任何一本单独的教程,都不可能像官方文档那样全面。各种单独教程的优势在于,它们更多地针对初学者、入门者设计,但与此同时,在全面性、深入性上做了妥协。

比如,在当前这本书里,不会涉及 [Bytes Object](https://docs.python.org/3/library/stdtypes.html#bytes-objects) —— 并非只有我一个人这么做,著名的 Python 教程《[Think Python: How to Think Like a Computer Scientist](http://greenteapress.com/thinkpython2/html/index.html)》、《[Dive into Python](https://linux.die.net/diveintopython/html/toc/index.html)》等等都没有涉及 Bytes Object 这个话题。

由于官方文档实际上没办法对入门者、初学者过分友好 —— 毕竟,全面、权威、准确才是它更应该做到的事情 —— 所以,很多人在刚开始的时候求助于各类非官方的教材、教程。原本应该是入门以后就理应 “只读官方文档”,或者 “第一查询对象只能是官方文档”,但在很多人那里竟然变成了 “从一开始到最后都在回避官方文档(或者说 ‘最专业的说明文字’)”,这就不好了,真的很吃亏,且自己都无法知道自己究竟吃了多少亏 —— 总以为自己已经学完了,但实际上从一开始就一点都不全面。

请牢记且遵守这个原则:

> **第一查询对象只能是官方文档**。

所以,当我用 Google 查询的时候,经常使用这样的格式:

> `<queries> site:python.org`

有时甚至会指定在哪个目录里搜索:

> `bytes site:python.org/3/library`,你试试这个连接:[bytes site:python.org/3/library](https://www.google.com/search?q=byte+site%3Apython.org%2F3%2Flibrary)

这个原则对任何语言都适用。将来你在学习任何新软件包(库)、语言更新后的新特性、甚至另外一个新语言的时候,都要这么做。所谓的超强自学能力,基本上就是由一些类似这样的小习惯和另外一些特别基础的方法构成的强大能力。

将官方文档拉回本地

把 The Python Tutorial 拉回本地阅读,可能更为方便,尤其是可以用 Sphinx 重新制作之后,页面左侧可以总是显示完整的目录:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/local-tutorial.png?raw=true)

也可以把这个教程转换成 epub 格式,以便在移动设备上阅读;甚至可以把这些个页面的 `.rst` 源文件转换成 `.ipynb` 文件,以便用 Jupyter Lab 浏览时可以直接执行其中的代码……

**注意**

> 此页的 Code Cell 中都是可执行的 bash 命令……

在此页执行它们对你来说是没意义的 —— 因为它们的执行结果在服务器上;这其中的命令,应该在你本地计算机上的 Terminal 中执行,你才能在本地计算机上获取结果。

安装 git

```bash
%%bash
which git
git --version

# 没有的话就执行以下命令安装 git
# conda install -c anaconda git
```

下载源文件

The Python Tutorial 的源文件位置在:

> https://github.com/python/cpython/tree/master/Doc/tutorial

repo 地址是:

> https://github.com/python/cpython.git

使用 git 将 repo 下载到 `~/Download/` 目录:

```bash
%%bash
cd ~/Downloads
# 总计 175M(文件大小随版本变动),所以需要一点时间
git clone https://github.com/python/cpython.git
cd cpython/Doc/tutorial
ls
```

安装 rst2ipynb

```bash
%%bash
# rst2ipynb needs pandoc and notedown...
which pandoc
which notedown
# 没有这两样东西的话,需要执行下面两行进行安装……
# conda install -c conda-forge pandoc
# conda install -c conda-forge notedown

# install rst2ipynb
cd ~/Downloads
git clone https://github.com/nthiery/rst-to-ipynb.git
cd rst-to-ipynb
pip install .
which rst2ipynb
```

批量转换 rst 至 ipynb

这个 rst2ipynb 的程序有点讨厌,一次只能处理一个文件…… 下面是一个 bash 程序,其实将来学起来也不难,看着跟 Python 差不多…… 下面的代码执行过后会出现很多 “警告” —— 没关系,文件会正常转换的。

```bash
%%bash
cd ~/Downloads/cpython/Doc/tutorial/
for f in *.rst
do
rst2ipynb $f -o "${f/%.rst/.ipynb}"
done
mkdir ipynbs
mv *.ipynb ipynbs/
```

如此这般,你就把 rst 文件都转换成 ipynb 文件,保存在 `~/Downloads/cpython/Doc/tutorial/ipynbs/` 之中了。随便把它挪到你喜欢的什么地方。用本地的 Jupyterlab 浏览,或者用 [Nteract](https://nteract.io) App 浏览。

如果以后你经常需要批量转换某个目录内的 `rst` 文件,那就把 bash function 放在 `~/.bash_profile` 文件里,在最后面追加以下代码:

```bash
function rsti {
for f in *.rst
do
rst2ipynb $f -o "${f/%.rst/.ipynb}"
done
}
```

而后在 Terminal 里执行一遍:

```bash
source ~/.bash_profile
```

而后,在有 `.rst` 文件的目录下输入 `rsti` 执行即可……

用 Sphinx 生成 html/epub 版本

```bash
%%bash
which sphinx-quickstart
# 没有的话就执行下一行:
# conda install -c anaconda sphinx
sphinx-quickstart --version
sphinx-quickstart --help
```

生成 html 版本和 epub 版本:

```bash
%%bash
cd ~/Downloads/cpython/Doc/tutorial/
sphinx-quickstart -q output --sep -p 'The Python Tutorial' -a 'Guido van Rossum' -r '1.0' -v '1.0' -l 'en' --suffix '.rst' --master 'index' --ext-autodoc --ext-doctest --ext-intersphinx --ext-todo --ext-coverage --ext-imgmath --ext-mathjax --ext-ifconfig --ext-viewcode --makefile --no-batchfile --no-use-make-mode
cp -f *.rst output/source/
cd output
make html
make epub

# 生成的 html 版本应该在 output/build/html 目录下;
# 生成的 epub 版本应该在 output/build/epub 目录下。

# sphinx-quickstart -q output \
# --sep \
# -p 'The Python Tutorial' \
# -a 'Guido van Rossum' \
# -v '1.0'
# -r '1.0' \
# -l 'en' \
# --suffix '.rst' \
# --master 'index' \
# --ext-autodoc \
# --ext-doctest \
# --ext-intersphinx \
# --ext-todo \
# --ext-coverage \
# --ext-imgmath \
# --ext-mathjax \
# --ext-ifconfig \
# --ext-viewcode \
# --makefile \
# --no-batchfile \
# --no-use-make-mode
```

用 Sphinx 这样生成的版本,支持本地目录内搜索,也确实比在网站上看更方便一点:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/search-generated-by-sphinx.png?raw=true)

下载已经转换好的版本

万一有什么情况中间出错又搞不定的话,就直接下载已经转换好的版本:

```bash
%%bash
cd ~/Downloads
git clone https://github.com/xiaolai/the-python-tutorial-in-other-formats.git
```

完整的 Python Doc 制作

其实,Python 的整个文档,已经是做好了制作文件的文档库:

> `cpython/Doc/Makefile`

只不过,将所有文件编译到一个 epub 里,在 iPad 之类的移动设备上打开有点费劲 —— 在我的设备上显示有 7701 页,翻页都要顿一顿……

想要使用这个官方的 `Makefile` 的话,首先确认自己已经安装过 Sphinx,其次需要补充安装一个包:

```bash
pip install blurb
```

而后在 Terminal 中转到 Doc 所在目录,执行以下命令:

```bash
make html
make epub
```

笨拙与耐心

自学的过程,实际上需要拆解为以下四个阶段,虽然它们之间常常有部分重叠:

> * 学
> * 练
> * 用
> * 造

只要识字,就会忍不住阅读;只要感觉上有 “值得学” 的,就会忍不住去学 —— 事实上每个人时时刻刻都在学习,只不过,学习的目标选择与学习的方式以及效率均不相同而已。

绝大多数人从未区分过这几个阶段,也从未研究过这几个阶段分别应该如何对待。这就解释了为什么那么多人虽然总是忍不住阅读,总是忍不住学习,但终其一生都没有真正掌握过像样的技能…… 因为他们在第一个阶段就出错,到了第二个阶段就放弃,第三个阶段是直接跳过去的,总是 “对付着用”,至于第四个阶段么,想都没想过……

第一部分的内容,基本用来展示 “学” 的过程。**学,就需要重复**,甚至很多次重复,尤其是在面对充满了 “过早引用” 现象的知识结构的时候。

反复学,最锻炼的是 “归纳整理” 的能力。而且,最有意思的,这在大多数情况下还是自动发生的 —— 只要你不断重复,你的大脑会在不自主之间把那些已经掌握的知识点与当前尚未掌握的知识点区分开来,前者处理起来轻松容易,甚至可以跳过;后者需要投入更多的注意力去仔细处理…… 在这个过程中,绝大多数的归纳整理工作自动完成了。最后再加上一点 “刻意的、收尾性的归纳总结整理工作” —— 大功告成。

绝大多数人总是希望自己一遍就能学会 —— 于是,注定了失败;而面对注定的失败,却并不知道那与天分、智商全无关系,因为谁都是那样的;于是,默默认定自己没有天分,甚至怀疑自己的智商;于是,默默地离开,希望换个地方验证自己的天分与智商;于是,再次面临注定的失败;于是,一而再再而三地被 “证明” 为天分不够智商不够…… 于是,就变成了那条狗:

> 在一项心理学实验中,狗是被测试对象。把狗关进栅栏里;然而那栅栏并不算太高,原本狗一使劲就能跳过去。但狗带着电项圈,只要它被发现有要跳过栅栏的企图,它就会被电击…… 几次尝试之后,狗就放弃了跳出栅栏 —— 虽然它其实能跳过去。更为惊人的结果是,随后把这条狗关进很低的栅栏,甚至是它都不用跳直接就可以跨过去的栅栏,它也老老实实地呆在栅栏里。

自学是门手艺,编程很大程度上也是一门手艺,掌握它在绝大多数情况下与天分智商全无关系 —— 很多人是在十来岁的时候学会编程的基本技能的。所有的手艺,最基本特征就是:

> **主要靠时间**

这就跟你看人们的车技一样,二十年安全驾龄和刚上路的肯定不一样,但这事跟天分、智商真的没什么关系……

到了第二部分,我们终于进入 “用” 的阶段 —— 嗯?“练” 怎么跳过去了?没有,我们的策略是,**以用带练** —— 在不断应用的过程中带动刻意练习。

练和学,是多少有些重合部分的。比如,你可能反复 “学” 了第一部分两三遍,而后就进入了第二部分,开始接触 “用”,在 “用” 的过程中,只要有空,或者只要有需求,你就可能回去 “复习” 一遍第一部分的内容……

无论之前的 “学”,重复了多少遍,一旦开始练,就注定体会到各式各样的笨拙。一会儿漏写了一个引号或者一个括号,一会儿不小心使用了非英文字符的全角符号,要么就是发现自己犯的错误愚蠢且反复发生,比如,语句块起始语句末尾忘了写冒号……

这再正常不过了。

每次自学什么新东西的时候,你就把自己想象成 “再次出生的婴儿” —— 其实每次自学,的的确确都是重生。一旦掌握了一项新的技能,你就不再是从前的那个自己,你是另外一个人了。

看着婴儿蹒跚学步,的确笨拙,但谁会觉得它不可爱呢?

同样的道理,刚开始用一个技能的时候,笨拙其实就是可爱 —— 只不过这时候旁人不再这么觉得而已了,只不过因为你披着一张成年人的皮。然而,你的大脑中正在学习的那一部分,和新生婴儿的大脑没有任何区别。

在第一部分的时候,“练” 的必要其实并不大…… 甚至,因为这是 “[过早引用](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.F.deal-with-forward-references.md)” 太多的知识结构,所以,急于练习反倒会有副作用。由于对自己所面对的复杂知识结构(就是过早引用太多的知识结构)认识不够,没有提前的应对策略,所以,他们根据原本的习惯,边学边练,学不明白当然就练不明白,于是,走着走着就挫败感太强,于是,就自然而然地放弃了…… 而且,弄不好,越练越容易出现 “不必要的挫败感”。

一切 “主要靠时间” 的活动都一样,都需要在从事之前认真做 “心理建设”。通常情况下,读一本教程,上个学习班,就 “会” 了 —— 几乎肯定是错觉或者幻觉。

> - 首先要明白,这肯定是个比 “天真的想象” 要长得多的过程。
> - 其次要明白,并且要越来越自然地明白,哪儿哪儿都需要很多重复。读,要读很多遍;练,要练很多遍;做,要做很多遍……

有了这样的心理建设,就相对更容易保持耐心。

人们缺乏耐心的常见根源就是 “之前根本就没准备花那么多时间和精力”,所以当然很容易超出 “时间和精力的预算”,当然相对更容易焦虑 —— 就好像没多少本钱的人做生意常常更容易失败一样。

这也解释了为什么我在写这本书的过程中,心中锁定的目标群体是大一学生和高一学生(甚至也有初一学生):

> 他们最有自学的 “本钱”……

离开学校之后,绝大多数人很难再有 “一看一下午”、“一练一整天”、“一玩一整夜” 的本钱。又由于生活的压力越来越大,对 “能够使用” 新技能的 “需求” 越来越紧迫,于是,对任何一次自学的 “时间精力投资” 都缩手缩脚,小里小气……

**预算观念**非常重要 —— 这个观念的存在与否,成熟与否,基本上决定一个人未来的盈利能力。

大多数人对此不仅不成熟,甚至干脆没有预算观念!—— 这也是为什么绝大多数人不适合创业的最根本原因。

不夸张地讲,未来的你只需要恪守一个原则,就很可能会因此超越 99% 的人:

> 绝对不做预算不够的事情。

说来惭愧,我是四十多岁之后,在创业和投资中经历了大量的失败,付了不可想像的学费之后,才深刻理解这条看起来过分简单的原则 —— 亏得本科还是学会计毕业的呢!我的运气在于,在自学这件事上,从来给出的预算都很多……

大约 1984 年,我在远在边疆的延吉市的本地青少年宫参加了一个要交 10 元学费的暑期计算机兴趣班,老师姓金,教的是 BASIC 语言,用的机器是这样的:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/Acorn-Electron.png?raw=true)

它要外接上一个九寸的单色显示器,那时候还没有磁盘,所以,只要一断电,就什么都没有了……

后来上了大学,买书自学 C++,结果在一个地方被卡住了:

我写的代码死活编译不过去…… 当时的编译器错误提醒还不像今天这么完善,当时也没有什么 Google 可以随时看看网上是否早就有人遇到过跟我一样的问题…… 当时我身边根本就没有什么别人在玩这些东西,当时的学校里的电脑还很少,需要提前申请所谓的 “上机时间”,后来呢?后来就放弃了。

当时是什么东西卡住我了呢?说来都能笑死,或者都能被气死:

```c
if (c = 1) {
...
}
```

习惯于写 BASIC 的我,“被习惯蒙蔽了双眼”,反复检查那么多遍,完全没看到应该写的是 `c == 1`!

一晃就好几年过去,有一天在书店看到一本 C++ 教程,想起来多年前放弃的东西,就把书买回来,在家里的电脑上重新玩了起来…… 然后就想起来问题在哪儿了,然后这事就被我重新捡起来了……

学完了也就学完了,真没地儿用。没地儿用,当然就很少练。

又过了好几年,我去新东方教书。2003 年,在写词汇书的过程中,需要统计词频,C++ 倒是用不上,用之前学过它的经验,学了一点 Python,写程序统计词频 ——《TOEFL 核心词汇 21 天突破》到今天还在销售。一个当年 10 块钱学费开始学的技能,就因为这本书,这些年给我 “变现” 了很多钱。

我有很多东西都是这样,隔了好多年,才重新捡起来,而后继续前行的。

最搞笑的是弹吉他。

十五六岁的时候,父亲给我买了一把吉他,理由是:

> 你连唱歌都跑调,将来咋学英语啊?

然后我就开始玩。花 5 块钱上了个培训班,第一天学了一个曲子,第二天就因为打架把吉他砸坏了,害得父亲又去给我买了一把更便宜的……

那个年代学吉他的人,第一首肯定是 “[Romance d'Amour](https://www.youtube.com/results?search_query=Romance+d%27Amour+)”(爱的罗曼史),我当然不例外。那曲子好听啊,好听到弹得不好也好听的地步。

然后吧,有一天,在一姑娘面前显摆,竟然没有弹错!弹完之后很得意…… 结果那姑娘一脸迷茫,隔了两三秒钟说,“不还有第二段吗?” —— 我一脸懵蛋,“…… 啊?”

可是吧,那第二段我终究没有学…… 其实也练过,但后来因为指骨摔断了,所以再后来的许多年,弹吉他只用拨片。直到…… 直到四十五岁那年。有一天,忽然想起这事,于是找来琴谱试了一下,花了一会的时间顺了下来。

所以,我猜我的 “自学能力强” 这件事本身,其实只不过是我投入的预算很多造成的 —— 活到老学到老,在我这里不是空话,所以,相对于别人,我这里只不过是预算太多、太充裕了而已。

于是,我学的时候重复得比别人多;练的时候重复得比别人多;到最后用得也相对比别人多很多 —— 这跟是否有天分或者聪明与否全然没有关系。

当然,学到的东西多了,就变得更聪明了似的。有高中学历的人通常情况下比只有小学学历的人更聪明 —— 其实就是这个原因而已。而这个现象与天生的智商并不总是正相关。

有个现象,不自学的人不知道。

真正开始自学且不断自学之后,刚开始总是觉得时间不够用 —— 因为当时的自己和其他人没什么太大区别。

随着时间的推移,不仅差异会出现,自我认知差异也开始越来越明显:

> 别人的时间都白过了,自己的时间都有产出……

到了下一个阶段,在其他人不断焦虑的情况下,自己却开始越来越淡定:

> 因为早已习惯了投入大量时间换取新技能……

等后来真的开始用这些技能做事,不断地做其他人因为时间白过了或者因为投入的 “预算” 不够而学不会做不到的事情 —— 并且还能充分明白,这并不是自己聪明、有天分的结果;只不过是做了该做的事情,投入了该投入的 “成本” 和 “预算” 而已……

于是,就真的能够理解下面这句话背后的深意:

> 人生很长,何必惊慌。

反正,这事跟天分与智商几乎没有任何关系。

刻意练习

在自学的过程中,总是有一些部分需要刻意练习。就好像小时候我们学习汉字,有些人总是把 “武” 这个字上加上一个撇 —— 对他们来说,不去写那个不该有的撇,就是需要刻意练习的。另外一些人倒是不在这个字上出错,但候和侯傻傻地分不清楚(类似的例子不计其数),那么,对他们来说就有了另外需要刻意练习的地方……

手艺这个东西,尤其需要刻意练习。我们说,手艺么,主要是靠时间…… 这里的 “时间”,准确地讲,就是 “刻意练习” 的时间,而不是任何时间。

我当过很长时间的英语老师。异常苦于一件事:最有用的道理最没人听。

学英语根本就不应该那么难,学了十六年也学不明白 —— 至于嘛!然而,最简单的道理,人们就是听不进去。他们之所以不行,就是因为从未刻意练习。学英语最简单的刻意练习就是朗读。每天朗读一小时,一百天下来就会超越绝大多数人,也会超越自己原本可能永远跨不过去的那个坎 —— 神奇的是,朗读什么无所谓,反正现在有声书那么多…… 更神奇的是,刚开始朗读得好不好听,甚至好不好都无所谓,反正没几天就能体会到大幅进步…… 最神奇的是,这么简单的事,99.99% 的人不做 —— 你身在中国,能理解这个比例真的没有夸张。

顺带推荐一下王渊源(John Gordan)的微信公众号:

> 清晨朗读会

到 2019 年 2 月 21 日,王渊源同学的清晨朗读会带着大伙朗读了第 1000 天……

许多年前,资质平庸的我,一直苦恼一件事:

> * 为什么自己无论干什么都笨手笨脚、差这儿差那儿的……
> * 为什么与此同时,总是能看到另外一些人,给人感觉 “一出手就是高手” 呢?!

这事折磨了我好多年…… 直到后来我当了老师,每年面前流过几万名学生之后,我才 “羞耻” 地反应过来:

> * 我花在刻意练习上的时间**太少**了;
> * 并且,也没有**刻意思考**哪些地方我应该去刻意练习。

而那些看起来 “一出手就是高手” 的人,则恰恰相反,他们不仅花很多时间刻意练习,还总是刻意思考在哪些地方尤其要刻意练习 —— 就是这一点差别造成了那么大的差距。

比如,小时候玩琴,因为手骨摔断了,于是就中断了很多刻意练习 —— 后来换成拨片之后,也习惯不好,不做很多基础练习,只是顺着感觉 “胡搞瞎搞”…… 于是,我的琴艺永远是自娱自乐也就刚刚够用的水准,永远上不了下一个台阶。我认识的人之中,许岑同学在这方面就是我这种情况的反向典范。

然而,我深刻地意识到,在另外一些地方,若是再 “混” 下去,那这辈子就别想有什么名堂了。所以,我就下决心在必要的地方一定要刻意地练习。印象中我第一个应用这种思考模式与决心的地方就是写书。我花了很长时间去准备第一本书,并且刻意地思考在哪些地方应该刻意地用力 —— 比如,在取书名这件在别人眼里可能并不是很重要的事上,我每天都琢磨,前后换了二十几个名字,最终选定…… 其后每一本出版的书籍,在书名选择上我都 “殚精竭虑” —— 最终的结果是,我的第一本书就是畅销书、长销书 —— 后面的每一本都是。

对,所谓的 “混”,解释很简单:

> **不做刻意练习的人就是在混时间**。

需要刻意练习的地方,因人而异。有的人就是不可能让 “武” 字带把刀,不需要刻意练习,但另外一些人不是。有些人就是朗读十分钟的效果跟别人朗读一小时的效果一样地好,但更多的人并不是……

然而,这并不是所谓的 “天分” 差异,这大抵上只相当于正态分布坐标略不相同而已。每个人都一样,都有各自必须刻意练习的地方,都有对别人来说无比容易可偏偏对自己来说就是很难的地方,而且,在这件事上,大家的点虽然各不相同,但总体上需要刻意练习的部分比例都差不多 —— 虽然说不清楚那个比例到底是多少。

比如,在学一门新的编程语言时,我常常做这样的刻意练习:

> **在纸上用笔写程序……**

而后,看着纸上的程序,把自己的大脑当作解析器,去判断每一句执行后的结果……

反复确认之后,再在编辑器里输入这个程序 —— 用很慢的速度,确保自己的输入无误……

然后再一运行,十有八九出错 —— 要再反复检查修改很多次才能顺利执行到最后……

为什么要这么做呢?因为我发现自己一旦学另外一个语言的时候,自己的大脑就会经常把这个新的语言与之前学过的其他语言相混淆,这很痛苦。我必须想出一个办法,让之前的也好,之后的也罢,干脆刻在自己的脑子里,不可能相互混淆。

我相信不是所有人都有我这样的烦恼和痛苦,虽然他们在我不烦恼不痛苦的地方也可能烦恼痛苦……

然而,于我来讲,这就是**我需要刻意练习**的地方 —— 这也是我**刻意思考**之后才找到的需要刻意练习的地方。

你也一样。你需要刻意练习的地方,需要你自己去刻意思考 —— 你和别人不一样,没有人和你一样,就这样。

这种事情,过去还真的是所谓 “书本上学不到” 的东西 —— 因为没有哪个作者能做到 “遍历世上所有人的所有特殊情况”…… 不过,互联网这本大书貌似正在突破这种限制,因为有无数的作者在写书,每个人所关注的点也不一样,再加上搜索引擎,所以,你总是可以在互联网这本大书中找到 “竟然与我一样的人”!

于是,你可能感受到了,其实吧,所谓 “刻意练习”,其实是 “刻意思考哪里需要刻意练习” 之后最自然不过的事情 —— 所以,**“刻意思考” 才是关键。**

应对策略很简单:

> 准备个专门的地方记录

我现在用的最多的就是 iPhone 上的 Notes,一旦遇到什么 “疑似需要刻意练习” 的点,就顺手记录在那里以防不小心忘记或者不小心遗漏。

而后有时间的时候就拿出来看看,排列一下优先级,琢磨一下刻意练习的方式,而后找时间刻意练习,如此这般,做到 “尽量不混日子”……

有时候,刻意练习是很容易的,比如,为了让自己记住当前正在学习的编程语言的语法规则,直至 “刻在脑子里一般”,需要做的无非是把编辑器中的 “Auto completion”(自动补全)先关掉三个月 —— 麻烦一点就麻烦一点,坚决不让 Tab 键帮自己哗啦哗啦写上一大片…… 那不叫麻烦,那叫刻意练习。

人们常说:

> 凡事,就怕**琢磨**……

那些高手,无一例外都是善于琢磨的人…… 可是,他们在琢磨什么呢?为什么他们会琢磨那些事呢?

你看,所谓的琢磨,其实真的不是很难,只不过,在此之前,你不知道该琢磨什么而已,一旦知道了,剩下的都再自然不过,都会自然而然地发生 —— 事实上,所谓的差别差距,只不过一线间而已。

为什么从函数开始?

读完第一部分之后,你多多少少已经 “写” 了一些程序,虽然我们总是说,“这就是让你脱盲”;也就是说,从此之后,你多多少少能够读懂程序,这就已经很好了。

可是你无论如何都避免不了已经写了一些,虽然,那所谓的 “写”,不过是 “改” 而已 —— 但毕竟也是一大步。

绝大多数编程书籍并不区分学习者的 “读” 与 “写” 这两个实际上应该分离的阶段 —— 虽然现实中这两个阶段总是多多少少重叠一部分。

在一个比较自然的过程中,我们总是先学会阅读,而后才开始练习写作;并且,最终,阅读的量一定远远大于写作的量 —— 即,输入远远大于输出。当然,貌似也有例外。据说,香港作家倪匡,他自己后来很少读书,每天咣当咣当地像是打扫陈年旧物倒垃圾一样写作 —— 他几乎是全球最具产量的畅销小说作家,貌似地球另外一端的史蒂芬・金都不如他多。又当然,他的主要输入来自于他早年丰富的人生经历,人家读书,他阅世,所以,实际上并不是输入很少,恰恰相反,是输入太多……

所以,正常情况下,输入多于输出,或者,输入远远多于输出,不仅是自然现象,也是无法改变的规则。

于是,我在安排内容的时候,也刻意如此安排。

第一部分,主要在于启动读者在编程领域中的 “阅读能力”,到第二部分,才开始逐步启动读者在编程领域中的 “写作能力”。

在第二部分启动之前,有时间有耐心的读者可以多做一件事情。

Python 的代码是开源的,它的代码仓库在 Github 上:

> https://github.com/python/

在这个代码仓库中,有一个目录下,保存着若干 Python Demo 程序:

> https://github.com/python/cpython/tree/3.6/Tools/demo

这个目录下的 README 中有说明:

> This directory contains a collection of demonstration scripts for
> various aspects of Python programming.
>
> * `beer.py` Well-known programming example: Bottles of beer.
> * `eiffel.py` Python advanced magic: A metaclass for Eiffel post/preconditions.
> * `hanoi.py` Well-known programming example: Towers of Hanoi.
> * `life.py` Curses programming: Simple game-of-life.
> * `markov.py` Algorithms: Markov chain simulation.
> * `mcast.py` Network programming: Send and receive UDP multicast packets.
> * `queens.py` Well-known programming example: N-Queens problem.
> * `redemo.py` Regular Expressions: GUI script to test regexes.
> * `rpython.py` Network programming: Small client for remote code execution.
> * `rpythond.py` Network programming: Small server for remote code execution.
> * `sortvisu.py` GUI programming: Visualization of different sort algorithms.
> * `ss1.py` GUI/Application programming: A simple spreadsheet application.
> * `vector.py` Python basics: A vector class with demonstrating special methods.

最起码把这其中的以下几个程序都精读一下,看看自己的理解能力:

> * [beer.py](https://github.com/python/cpython/blob/3.6/Tools/demo/beer.py) Well-known programming example: Bottles of beer.
> * [eiffel.py](https://github.com/python/cpython/blob/3.6/Tools/demo/eiffel.py) Python advanced magic: A metaclass for Eiffel post/preconditions.
> * [hanoi.py](https://github.com/python/cpython/blob/3.6/Tools/demo/hanoi.py) Well-known programming example: Towers of Hanoi.
> * [life.py](https://github.com/python/cpython/blob/3.6/Tools/demo/life.py) Curses programming: Simple game-of-life.
> * [markov.py](https://github.com/python/cpython/blob/3.6/Tools/demo/markov.py) Algorithms: Markov chain simulation.
> * [queens.py](https://github.com/python/cpython/blob/3.6/Tools/demo/queens.py) Well-known programming example: N-Queens problem.

就算读不懂也没关系,把读不懂的部分标记下来,接下来就可以 “带着问题学习”……

在未来的时间里,一个好的习惯就是,有空了去读读别人写的代码 —— 理解能力的提高,就靠这个了。你会发现这事跟其他领域的学习没什么区别。你学英语也一样,读多了,自然就读得快了,理解得快了,并且在那过程中自然而然地习得了很多 “句式”,甚至很多 “说理的方法”、“讲故事的策略”…… 然后就自然而然地会写了,从能写一点开始,慢慢到 “很能写”!

为了顺利启动第一部分的 “阅读”,特意找了个不一样的入口,“布尔运算”;第二部分,从 “阅读” 过渡到 “写作”,我也同样特意寻找了一个不一样的入口:**从函数开始写起**。

从小入手,从来都是自学的好方法。我们没有想着一上来就写程序,而是写 “子程序”、“小程序”、“短程序”。从结构化编程的角度来看,写函数的一个基本要求就是:

> - 完成一个功能;
> - 只完成一个功能;
> - 没有任何错误地只完成一个功能……

然而,即便是从小入手,任务也没有变得过分简单。其中涉及的话题理解起来并不容易,尽管我们尽量用最简单的例子。涉及的话题有:

> - 参数的传递
> - 多参数的传递
> - 匿名函数以及函数的别称
> - 递归函数
> - 函数文档
> - 模块
> - 测试驱动编程
> - 可执行程序

这些都是你未来写自己的工程时所必须仰仗的基础,马虎不得,疏漏不得。

另外,这一部分与第一部分有一个刻意不同的编排,这一部分的每一章之后,**没有写总结** —— 那个总结需要读者自己动手完成。你需要做的不仅仅是每一个章节的总结,整个第二部分读完之后,还要做针对整个 “深入了解函数”(甚至应该包括第一部分已经读过的关于函数的内容)的总结…… 并且,关于函数,这一章并未完全讲完呢,第三部分还有生成器、迭代器、以及装饰器要补充 —— 因为它们多多少少都涉及到下一部分才能深入的内容,所以,在这一部分就暂时没有涉及。

你要习惯,归纳、总结、整理的工作,从来都不是一次就能完成的,都需要反复多次之后才能彻底完成。必须习惯这种流程 —— 而不是像那些从未自学过的人一样,对这种东西想当然地全不了解。

另外,从现代编程方法论来看,“写作” 部分一上来就从函数入手也的确是 “更正确” 的,因为结构化编程的核心就是拆分任务,把任务拆分到不能再拆分为止 —— 什么时候不能再拆分了呢?就是当一个函数只完成一个功能的时候……

关于参数(上)

之前就提到过,从结构上来看,每个函数都是一个完整的程序,因为一个程序,核心构成部分就是输入、处理、输出:

> * 它可以有**输入** —— 即,它能接收外部通过参数传递的值;
> * 它可以有**处理** —— 即,内部有能够完成某一特定任务的代码;尤其是,它可以根据 “输入” 得到 “输出”;
> * 它可以有**输出** —— 即,它能向外部输送返回值……

所以,在我看来,有了一点基础知识之后,最早应该学习的是 “如何写函数” —— 这个起点会更好一些。

这一章的内容,看起来会感觉与 [Part1.E.4 函数那一章](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.E.4.functions.md)部分重合。但这两章的出发点不一样:

> * [Part1.E.4 函数那一章](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.E.4.functions.md),只是为了让读者有 “阅读” 函数说明文档的能力;
> * 这一章,是为了让读者能够开始动手写函数给自己或别人用……

为函数取名

哪怕一个函数内部什么都不干,它也得有个名字,然后名字后面要加上圆括号 `()`,以明示它是个函数,而不是某个变量。

定义一个函数的关键字是 `def`,以下代码定义了一个什么都不干的函数:

```python
def do_nothing():
pass

do_nothing()
```

为函数取名(为变量取名也一样)有些基本的注意事项:

> - 首先,名称不能以数字开头。能用在名称开头的有,大小写字母和下划线 `_`;
>
> - 其次,名称中不能有空格,要么使用下划线连接词汇,如,`do_nothing`,要么使用 [Camel Case](https://en.wikipedia.org/wiki/Camel_case),如 `doNothing` —— 更推荐使用下划线;
>
> - 再次,名称不能与关键字重合 —— 以下是 Python 的 Keyword List:

| - | Python | Keyword | List | - |
| ---------- | ---------- | ---------- | ---------- | ---------- |
| `and` | `as` | `assert` | `async` | `await` |
| `break` | `class` | `continue` | `def` | `del` |
| `elif` | `else` | `except` | `False` | `finally` |
| `for` | `from` | `global` | `if` | `import` |
| `in` | `is` | `lambda` | `None` | `nonlocal` |
| `not` | `or` | `pass` | `raise` | `return` |
| `True` | `try` | `while` | `with` | `yield` |

你随时可以用以下代码查询关键字列表:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

import keyword
keyword.kwlist # 列出所有关键字
keyword.iskeyword('if') # 查询某个词是不是关键字
```

['False',
'None',
'True',
'and',
'as',
'assert',
'async',
'await',
'break',
'class',
'continue',
'def',
'del',
'elif',
'else',
'except',
'finally',
'for',
'from',
'global',
'if',
'import',
'in',
'is',
'lambda',
'nonlocal',
'not',
'or',
'pass',
'raise',
'return',
'try',
'while',
'with',
'yield']

True

关于更多为函数、变量取名所需要的注意事项,请参阅:

> * [PEP 8 -- Style Guide for Python Code: Naming Conventions](https://www.python.org/dev/peps/pep-0008/#naming-conventions)
> * [PEP 526 -- Syntax for Variable Annotations](https://www.python.org/dev/peps/pep-0526/)
>
> 注:PEPs,是 Python Enhancement Proposals 的缩写:https://www.python.org/dev/peps/

不接收任何参数的函数

在定义函数的时候,可以定义成不接收任何参数;但调用函数的时候,依然需要写上函数名后面的圆括号 `()`:

```python
def do_something():
print('This is a hello message from do_something().')

do_something()
```

This is a hello message from do_something().

没有 return 语句的函数

函数内部,不一定非要有 `return` 语句 —— 上面 `do_somthing()` 函数就没有 `return` 语句。但如果函数内部并未定义返回值,那么,该函数的返回值是 `None`,当 `None` 被当作布尔值对待的时候,相当于是 `False`。

这样的设定,使得函数调用总是可以在条件语句中被当作判断依据:

```python
def do_something():
print('This is a hello message from do_something().')

if not do_something(): # 由于该函数名称的缘故,这一句代码的可读性很差……
print("The return value of 'do_something()' is None.")
```

This is a hello message from do_something().
The return value of 'do_something()' is None.

`if not do_something(): ` 翻译成自然语言,应该是,“如果 `do_something()` 的返回值是 ‘非真’,那么:……”

接收外部传递进来的值

让我们写个判断闰年年份的函数,取名为 `is_leap()`,它接收一个年份为参数,若是闰年,则返回 `True`,否则返回 `False`。

根据闰年的定义:

> * 年份应该是 4 的倍数;
> * 年份能被 100 整除但不能被 400 整除的,不是闰年。

所以,相当于要在能被 4 整除的年份中,排除那些能被 100 整除却不能被 400 整除的年份。

```python
def is_leap(year):
leap = False
if year % 4 == 0:
leap = True
if year % 100 == 0 and year % 400 != 0:
leap = False
return leap

is_leap(7)
is_leap(12)
is_leap(100)
is_leap(400)
```

False
True
False
True

```python
# 另外一个更为简洁的版本,理解它还挺练脑子的
# cpython/Lib/datetime.py
def _is_leap(year):
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
_is_leap(300)
```

False

函数可以同时接收多个参数。比如,我们可以写个函数,让它输出从大于某个数字到小于另外一个数字的斐波那契数列;那就需要定义两个参数,调用它的时候也需要传递两个参数:

```python
def fib_between(start, end):
a, b = 0, 1
while a < end:
if a >= start:
print(a, end=' ')
a, b = b, a + b

fib_between(100, 10000)
```

144 233 377 610 987 1597 2584 4181 6765

当然可以把这个函数写成返回值是一个列表:

```python
def fib_between(start, end):
r = []
a, b = 0, 1
while a < end:
if a >= start:
r.append(a)
a, b = b, a + b
return r

fib_between(100, 10000)
```

[144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]

变量的作用域

下面的代码,经常会让初学者迷惑:

```python
def increase_one(n):
n += 1
return n

n = 1
print(increase_one(n))
# print(n)
```

2

当 `increase_one(n)` 被调用之后,`n` 的值究竟是多少呢?或者更准确点问,随后的 `print(n)` 的输出结果应该是什么呢?

输出结果是 `1`。

在程序执行过程中,变量有**全局变量**(Global Variables)和**局域变量**(Local Variables)之分。

> 首先,每次某个函数被调用的时候,这个函数会开辟一个新的区域,这个函数内部所有的变量,都是局域变量。也就是说,即便那个函数内部某个变量的名称与它外部的某个全局变量名称相同,它们也不是同一个变量 —— 只是名称相同而已。
>
> 其次,更为重要的是,当外部调用一个函数的时候,准确地讲,传递的不是变量,而是那个变量的*值*。也就是说,当 `increase_one(n)` 被调用的时候,被传递给那个恰好名称也叫 `n` 的局域变量的,是全局变量 `n` 的值,`1`。
>
> 而后,`increase_one()` 函数的代码开始执行,局域变量 `n` 经过 `n += 1` 之后,其中存储的值是 `2`,而后这个值被 `return` 语句返回,所以,`print(increase(n))` 所输出的值是函数被调用之后的返回值,即,`2`。
>
> 然而,全局变量 `n` 的值并没有被改变,因为局部变量 `n`(它的值是 `2`)和全局变量 `n`(它的值还是 `1`)只不过是名字相同而已,但它们并不是同一个变量。

以上的文字,可能需要反复阅读若干遍;几遍下来,消除了疑惑,以后就彻底没问题了;若是这个疑惑并未消除,或者关键点并未消化,以后则会反复被这个疑惑所坑害,浪费无数时间。

不过,有一种情况要格外注意 —— 在函数内部处理被传递进来的值是可变容器(比如,列表)的时候:

```python
def be_careful(a, b):
a = 2
b[0] = 'What?!'

a = 1
b = [1, 2, 3]
be_careful(a, b)
a, b
```

(1, ['What?!', 2, 3])

所以,一个比较好的习惯是,如果传递进来的值是列表,那么在函数内部对其操作之前,先创建一个它的拷贝:

```python
def be_careful(a, b):
a = 2
b_copy = b.copy()
b_copy[0] = 'What?!'

a = 1
b = [1, 2, 3]
be_careful(a, b)
a, b
```

(1, [1, 2, 3])

关于参数(下)

可以接收一系列值的位置参数

如果你在定义参数的时候,在一个*位置参数*(Positional Arguments)前面标注了星号,`*`,那么,这个位置参数可以接收一系列值,在函数内部可以对这一系列值用 `for ... in ...` 循环进行逐一的处理。

带一个星号的参数,英文名称是 “Arbitrary Positional Arguments”,姑且翻译为 “随意的位置参数”。

还有带两个星号的参数,一会儿会讲到,英文名称是 “Arbitrary Keyword Arguments”,姑且翻译为 “随意的关键字参数”。

> 有些中文书籍把 “Arbitrary Positional Arguments” 翻译成 “可变位置参数”。事实上,在这样的地方,无论怎样的中文翻译都是令人觉得非常吃力的。前面的这个翻译还好了,我还见过把 “Arbitrary Keyword Arguments” 翻译成 “武断的关键字参数” 的 —— 我觉得这样的翻译肯定会使读者产生说不明道不白的疑惑。
>
> 所以,**入门之后就尽量只用英文**是个好策略。虽然刚开始有点吃力,但后面会很省心,很长寿 —— 是呀,少浪费时间、少浪费生命,其实就相当于更长寿了呀!

```python
def say_hi(*names):
for name in names:
print(f'Hi, {name}!')
say_hi()
say_hi('ann')
say_hi('mike', 'john', 'zeo')
```

Hi, ann!
Hi, mike!
Hi, john!
Hi, zeo!

`say_hi()` 这一行没有任何输出。因为你在调用函数的时候,没有给它传递任何值,于是,在函数内部代码执行的时候,`name in names` 的值是 `False`,所以,`for` 循环内部的代码没有被执行。

在函数内部,是把 `names` 这个参数当作容器处理的 —— 否则也没办法用 `for ... in ...` 来处理。而在调用函数的时候,我们是可以将一个容器传递给函数的 Arbitrary Positional Arguments 的 —— 做法是,在调用函数的时候,在参数前面加上星号 `*`:

```python
def say_hi(*names):
for name in names:
print(f'Hi, {name}!')

names = ('mike', 'john', 'zeo')
say_hi(*names)
```

Hi, mike!
Hi, john!
Hi, zeo!

实际上,因为以上的 `say_hi(*names)` 函数内部就是把接收到的参数当作容器处理的,于是,在调用这个函数的时候,向它传递任何容器都会被同样处理:

```python
def say_hi(*names):
for name in names:
print(f'Hi, {name}!')

a_string = 'Python'
say_hi(*a_string)

a_range = range(10)
say_hi(*a_range)

a_list = list(range(10, 0, -1))
say_hi(*a_list)

a_dictionary = {'ann':2321, 'mike':8712, 'joe': 7610}
say_hi(*a_dictionary)
```

Hi, P!
Hi, y!
Hi, t!
Hi, h!
Hi, o!
Hi, n!
Hi, 0!
Hi, 1!
Hi, 2!
Hi, 3!
Hi, 4!
Hi, 5!
Hi, 6!
Hi, 7!
Hi, 8!
Hi, 9!
Hi, 10!
Hi, 9!
Hi, 8!
Hi, 7!
Hi, 6!
Hi, 5!
Hi, 4!
Hi, 3!
Hi, 2!
Hi, 1!
Hi, ann!
Hi, mike!
Hi, joe!

_在定义可以接收一系列值的位置参数时,建议在函数内部为该变量命名时总是用**复数**_,因为函数内部,总是需要 `for` 循环去迭代元组中的元素,这样的时候,名称的复数形式对代码的可读性很有帮助 —— 注意以上程序第二行。以中文为母语的人,在这个细节上常常感觉 “不堪重负” —— 因为中文的名词没有复数 —— 但必须习惯。(同样的道理,若是用拼音命名变量,就肯定是为将来挖坑……)

**注意**:一个函数中,可以接收一系列值的位置参数只能有一个;并且若是还有其它位置参数存在,那就必须把这个可以接收一系列值的位置参数排在所有其它位置参数之后。

```python
def say_hi(greeting, *names):
for name in names:
print(f'{greeting}, {name.capitalize()}!')

say_hi('Hello', 'mike', 'john', 'zeo')
```

Hello, Mike!
Hello, John!
Hello, Zeo!

为函数的某些参数设定默认值

可以在定义函数的时候,为某些参数设定默认值,这些有默认值的参数,又被称作关键字参数(Keyword Arguments)。从这个函数的 “用户” 角度来看,这些设定了默认值的参数,就成了 “可选参数”。

```python
def say_hi(greeting, *names, capitalized=False):
for name in names:
if capitalized:
name = name.capitalize()
print(f'{greeting}, {name}!')

say_hi('Hello', 'mike', 'john', 'zeo')
say_hi('Hello', 'mike', 'john', 'zeo', capitalized=True)
```

Hello, mike!
Hello, john!
Hello, zeo!
Hello, Mike!
Hello, John!
Hello, Zeo!

可以接收一系列值的关键字参数

之前我们看到,可以设定一个位置参数(Positional Argument),接收一系列的值,被称作 “Arbitrary Positional Argument”;

同样地,我们也可以设定一个可以接收很多值的关键字参数(Arbitrary Keyword Argument)。

```python
def say_hi(**names_greetings):
for name, greeting in names_greetings.items():
print(f'{greeting}, {name}!')

say_hi(mike='Hello', ann='Oh, my darling', john='Hi')
```

Hello, mike!
Oh, my darling, ann!
Hi, john!

既然在函数内部,我们在处理接收到的 Arbitrary Keyword Argument 时,用的是对字典的迭代方式,那么,在调用函数的时候,也可以直接使用字典的形式:

```python
def say_hi(**names_greetings):
for name, greeting in names_greetings.items():
print(f'{greeting}, {name}!')

a_dictionary = {'mike':'Hello', 'ann':'Oh, my darling', 'john':'Hi'}
say_hi(**a_dictionary)

say_hi(**{'mike':'Hello', 'ann':'Oh, my darling', 'john':'Hi'})
```

Hello, mike!
Oh, my darling, ann!
Hi, john!
Hello, mike!
Oh, my darling, ann!
Hi, john!

至于在函数内部,你用什么样的迭代方式去处理这个字典,是你自己的选择:

```python
def say_hi_2(**names_greetings):
for name in names_greetings:
print(f'{names_greetings[name]}, {name}!')
say_hi_2(mike='Hello', ann='Oh, my darling', john='Hi')
```

Hello, mike!
Oh, my darling, ann!
Hi, john!

函数定义时各种参数的排列顺序

在定义函数的时候,各种不同类型的参数应该按什么顺序摆放呢?对于之前写过的 `say_hi()` 这个函数,

```python
def say_hi(greeting, *names, capitalized=False):
for name in names:
if capitalized:
name = name.capitalize()
print(f'{greeting}, {name}!')

say_hi('Hi', 'mike', 'john', 'zeo')
say_hi('Welcome', 'mike', 'john', 'zeo', capitalized=True)
```

Hi, mike!
Hi, john!
Hi, zeo!
Welcome, Mike!
Welcome, John!
Welcome, Zeo!

如果,你想给其中的 `greeting` 参数也设定个默认值怎么办?写成这样好像可以:

```python
def say_hi(greeting='Hello', *names, capitalized=False):
for name in names:
if capitalized:
name = name.capitalize()
print(f'{greeting}, {name}!')

say_hi('Hi', 'mike', 'john', 'zeo')
say_hi('Welcome', 'mike', 'john', 'zeo', capitalized=True)
```

Hi, mike!
Hi, john!
Hi, zeo!

Welcome, Mike!
Welcome, John!
Welcome, Zeo!

但 `greeting` 这个参数虽然有默认值,可这个函数在被调用的时候,还是必须要给出这个参数,否则输出结果出乎你的想象:

```python
def say_hi(greeting='Hello', *names, capitalized=False):
for name in names:
if capitalized:
name = name.capitalize()
print(f'{greeting}, {name}!')

say_hi('mike', 'john', 'zeo')
```

mike, john!
mike, zeo!

设定了默认值的 `greeting`,竟然不像你想象的那样是 “可选参数”!所以,你得这样写:

```python
def say_hi(*names, greeting='Hello', capitalized=False):
for name in names:
if capitalized:
name = name.capitalize()
print(f'{greeting}, {name}!')

say_hi('mike', 'john', 'zeo')
say_hi('mike', 'john', 'zeo', greeting='Hi')
```

Hello, mike!
Hello, john!
Hello, zeo!
Hi, mike!
Hi, john!
Hi, zeo!

这是因为函数被调用时,面对许多参数,Python 需要按照既定的规则(即,顺序)判定每个参数究竟是哪一类型的参数:

> **Order of Arguments**
> 1. Positional
> 1. Arbitrary Positional
> 1. Keyword
> 1. Arbitrary Keyword

所以,即便你在定义里写成

```python
def say_hi(greeting='Hello', *names, capitalized=False):
...
```

在调用该函数的时候,无论你写的是
```python
say_hi('Hi', 'mike', 'john', 'zeo')
```

还是
```python
say_hi('mike', 'john', 'zeo')
```

Python 都会认为接收到的第一个值是 Positional Argument —— 因为在定义中,`greeting` 被放到了 Arbitrary Positional Arguments 之前。

化名与匿名

化名

在 Python 中,我们可以给一个函数取个**化名**(alias)。

以下的代码,我们先是定义了一个名为 `_is_leap` 的函数,而后为它另取了一个化名:

```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

def _is_leap(year):
return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

year_leap_bool = _is_leap
year_leap_bool #<function __main__._is_leap(year)>
year_leap_bool(800) # _is_leap(800) -> True

id(year_leap_bool) # id() 这个函数可以查询某对象的内存地址
id(_is_leap) # year_leap_bool 和 _is_leap 其实保存在同一个地址中,也就是说,它们是同一个对象。

type(year_leap_bool)
type(_is_leap) # 它们都是 function
```

<function __main__._is_leap(year)>

True

4547071648

4547071648

function

function

我们可以看到的是,`id(year_leap_bool)` 和 `id(_is_leap)` 的内存地址是一样的 —— 它们是同一个对象,它们都是函数。所以,当你写 `year_leap_bool = _is_leap` 的时候,相当于给 `_is_leap()` 这个函数取了个化名。

在什么样的情况下,要给一个函数取一个化名呢?

在任何一个工程里,为函数或者变量取名都是很简单却不容易的事情 —— 因为可能会重名(虽然已经尽量用变量的作用域隔离了),可能会因取名含混而令后来者费解……

所以,仅仅为了少敲几下键盘而给一个函数取个更短的化名,实际上并不是好主意,更不是好习惯。尤其现在的编辑器都支持自动补全和多光标编辑的功能,变量名再长都不构成负担。

更多的时候,为函数取一个化名,应该是为了提高代码可读性 —— 对自己或其他人都很重要。

lambda

写一个很短的函数可以用 `lambda` 关键字。

下面是用 `def` 关键字写函数:

```python
def add(x, y):
return x + y
add(3, 5)
```

8

下面是用 `lambda` 关键字写函数:

```python
add = lambda x, y: x + y
add(3, 5)
```

8

lambda 的语法结构如下:

> `lambda_expr ::= "lambda" [parameter_list] ":" expression`

以上使用的是 BNF 标注。当然,BNF 是你目前并不熟悉的,所以,有疑惑别当回事。

反正你已经见到示例了:

```python
lambda x, y: x + y
```

先写上 `lambda` 这个关键字,其后分为两个部分,`:` 之前是参数,之后是表达式;这个表达式的值,就是这个函数的返回值。

> **注意**:`lambda` 语句中,`:` 之后有且只能有一个表达式。

而这个函数呢,没有名字,所以被称为 “匿名函数”。

`add = lambda x, y: x + y`

就相当于是给一个没有名字的函数取了个名字。

lambda 的使用场景

那 lambda 这种匿名函数的用处在哪里呢?

作为某函数的返回值

第一个常见的用处是*作为另外一个函数的返回值*。

让我们看看 [The Python Tutorial](https://docs.python.org/3/tutorial/controlflow.html#lambda-expressions) 中的一个例子。

```python
def make_incrementor(n):
return lambda x: x + n

f = make_incrementor(42)
f(0)

f(1)
```

42

43

这个例子乍看起来很令人迷惑。我们先看看 `f = make_incrementor(42)` 之后,`f` 究竟是什么东西:

```python
def make_incrementor(n):
return lambda x: x + n

f = make_incrementor(42)
f

id(make_incrementor)
id(f)
```

<function __main__.make_incrementor.<locals>.<lambda>(x)>

4428443296

4428726888

首先,要注意,`f` 并不是 `make_incrementor()` 这个函数的化名,如果是给这个函数取个化名,写法应该是:

```python
f = make_incrementor
```

那 `f` 是什么呢?它是 `<function __main__.make_incrementor.<locals>.<lambda>(x)>`:

> * `f = make_incrementor(42)` 是将 `make_incrementor(42)` 的返回值保存到 `f` 这个变量之中;
> * 而 `make_incrementor()` 这个函数接收到 `42` 这个参数之后,返回了一个函数:`lambda x: x + 42`;
> * 于是,`f` 中保存的函数是 `lambda x: x + 42`;
> * 所以,`f(0)` 是向这个匿名函数传递了 `0`,而后,它返回的是 `0 + 42`。

作为某函数的参数

可以拿一些可以接收函数为参数的内建函数做例子。比如,[`map()`](https://docs.python.org/3/library/functions.html#map)。

> `map`(*function*, *iterable*, *...*)
>
> Return an iterator that applies *function* to every item of *iterable*, yielding the results. If additional *iterable* arguments are passed, *function* must take that many arguments and is applied to the items from all iterables in parallel. With multiple iterables, the iterator stops when the shortest iterable is exhausted. For cases where the function inputs are already arranged into argument tuples, see [`itertools.starmap()`](https://docs.python.org/3/library/itertools.html#itertools.starmap).

`map()` 这个函数的第一个参数,就是用来接收函数的。随后的参数,是 `iterable` —— 就是可被迭代的对象,比如,各种容器,例如:列表、元组、字典什么的。

```python
def double_it(n):
return n * 2

a_list = [1, 2, 3, 4, 5, 6]

b_list = list(map(double_it, a_list))
b_list

c_list = list(map(lambda x: x * 2, a_list))
c_list
```

[2, 4, 6, 8, 10, 12]

[2, 4, 6, 8, 10, 12]

显然用 `lambda` 更为简洁。另外,类似完成 `double_it(n)` 这种简单功能的函数,常常有 “用过即弃” 的必要。

```python
phonebook = [
{
'name': 'john',
'phone': 9876
},
{
'name': 'mike',
'phone': 5603
},
{
'name': 'stan',
'phone': 6898
},
{
'name': 'eric',
'phone': 7898
}
]

phonebook
list(map(lambda x: x['name'], phonebook))
list(map(lambda x: x['phone'], phonebook))
```

[{'name': 'john', 'phone': 9876},
{'name': 'mike', 'phone': 5603},
{'name': 'stan', 'phone': 6898},
{'name': 'eric', 'phone': 7898}]

['john', 'mike', 'stan', 'eric']

[9876, 5603, 6898, 7898]

可以给 map() 传递若干个可被迭代对象:

```python
a_list = [1, 3, 5]
b_list = [2, 4, 6]

list(map(lambda x, y: x * y, a_list, b_list))
```

[2, 12, 30]

以上的例子都弄明白了,再去看 [The Python Tutorial](https://docs.python.org/3/tutorial/controlflow.html#lambda-expressions) 中的例子,就不会有任何疑惑了:

```python
pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
pairs.sort(key=lambda p: p[1])
pairs
```

[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

递归函数

递归(Recursion)

在函数中有个理解门槛比较高的概念:**递归函数**(Recursive Functions)—— 那些**在自身内部调用自身的函数**。说起来都比较拗口。

先看一个例子,我们想要有个能够计算 `n` 的*阶乘*(factorial)`n!` 的函数,`f()`,规则如下:

> - `n! = n × (n-1) × (n-2)... × 1`
> - 即,`n! = n × (n-1)!`
> - 且,`n >= 1`
>
> **注意**:以上是数学表达,不是程序,所以,`=` 在这一小段中是 “*等于*” 的意思,**不是程序语言中的赋值符号**。

于是,计算 `f(n)` 的 Python 程序如下:

```python
def f(n):
if n == 1:
return 1
else:
return n * f(n-1)

print(f(5))
```

120

递归函数的执行过程

以 factorial(5) 为例,让我们看看程序的流程:

[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/recursive-function-call.png?raw=true)

当 f(5) 被调用之后,函数开始运行……
* 因为 `5 > 1`,所以,在计算 `n * f(n-1)` 的时候要再次调用自己 `f(4)`;所以必须等待 `f(4)` 的值返回;
* 因为 `4 > 1`,所以,在计算 `n * f(n-1)` 的时候要再次调用自己 `f(3)`;所以必须等待 `f(3)` 的值返回;
* 因为 `3 > 1`,所以,在计算 `n * f(n-1)` 的时候要再次调用自己 `f(2)`;所以必须等待 `f(2)` 的值返回;
* 因为 `2 > 1`,所以,在计算 `n * f(n-1)` 的时候要再次调用自己 `f(1)`;所以必须等待 `f(1)` 的值返回;
* 因为 `1 == 1`,所以,这时候不会再次调用 `f()` 了,于是递归结束,开始返回,这次返回的是 `1`;
* 下一步返回的是 `2 * 1`;
* 下一步返回的是 `3 * 2`;
* 下一步返回的是 `4 * 6`;
* 下一步返回的是 `5 * 24` —— 至此,外部调用 `f(5)` 的最终返回值是 `120`……

加上一些输出语句之后,能更清楚地看到大概的执行流程:

```python
def f(n):
print('\tn =', n)
if n == 1:
print('Returning...')
print('\tn =', n, 'return:', 1)
return 1
else:
r = n * f(n-1)
print('\tn =', n, 'return:', r)
return r

print('Call f(5)...')
print('Get out of f(n), and f(5) =', f(5))
```

Call f(5)...
n = 5
n = 4
n = 3
n = 2
n = 1
Returning...
n = 1 return: 1
n = 2 return: 2
n = 3 return: 6
n = 4 return: 24
n = 5 return: 120
Get out of f(n), and f(5) = 120

有点烧脑…… 不过,分为几个层面去逐个突破,你会发现它真的很好玩。

递归的终点

递归函数在内部必须有一个能够让自己停止调用自己的方式,否则永远循环下去了……

其实,我们所有人很小就见过递归应用,只不过,那时候不知道那就是递归而已。听过那个无聊的故事罢?

> 山上有座庙,庙里有个和尚,和尚讲故事,说……
> > 山上有座庙,庙里有个和尚,和尚讲故事,说……
> > > 山上有座庙,庙里有个和尚,和尚讲故事,说……

写成 Python 程序大概是这样:

```python
def a_monk_telling_story():
print('山上有座庙,庙里有个和尚,和尚讲故事,他说…… ')
return a_monk_telling_story()

a_monk_telling_story()
```

这是个*无限循环*的递归,因为这个函数里*没有设置中止自我调用的条件*。无限循环还有个不好听的名字,叫做 “死循环”。

在著名的电影**盗梦空间**(_2010_)里,从整体结构上来看,“入梦” 也是个 “递归函数”。只不过,这个函数和 `a_monk_telling_story()` 不一样,它并不是死循环 —— 因为它设定了*中止自我调用的条件*:

> 在电影里,醒过来的条件有两个
>> * 一个是在梦里死掉;
>> * 一个是在梦里被 kicked 到……
>
> 如果这两个条件一直不被满足,那就进入 limbo 状态 —— 其实就跟死循环一样,出不来了……

为了演示,我把故事情节改变成这样:
> * 入梦,`in_dream()`,是个递归函数;
> * 入梦之后醒过来的条件有两个:
>> * 一个是在梦里死掉,`dead is True`;
>> * 一个是在梦里被 kicked,`kicked is True`……
>>
>> 以上两个条件中任意一个被满足,就苏醒……

至于为什么会死掉,如何被 kick,我偷懒了一下:管它怎样,管它如何,反正,每个条件被满足的概率是 1/10……(也只有这样,我才能写出一个简短的,能够运行的 “*盗梦空间程序*”。)

把这个很抽象的故事写成 Python 程序,看看一次入梦之后能睡多少天,大概是这样:

```python
import random

def in_dream(day=0, dead=False, kicked=False):
dead = not random.randrange(0,10) # 1/10 probability to be dead
kicked = not random.randrange(0,10) # 1/10 probability to be kicked
day += 1
print('dead:', dead, 'kicked:', kicked)

if dead:
print((f"I slept {day} days, and was dead to wake up..."))
return day
elif kicked:
print(f"I slept {day} days, and was kicked to wake up...")
return day

return in_dream(day)

print('The in_dream() function returns:', in_dream())
```

dead: False kicked: False
dead: False kicked: False
dead: False kicked: False
dead: False kicked: False
dead: False kicked: False
dead: False kicked: False
dead: False kicked: False
dead: True kicked: True
I slept 8 days, and was dead to wake up...
The in_dream() function returns: 8

如果疑惑为什么 `random.randrange(0,10)` 能表示 1/10 的概率,请返回去重新阅读[第一部分中关于布尔值的内容](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.E.2.values-and-their-operators.md)。

另外,在 Python 中,若是需要将某个值与 True 或者 False 进行比较,尤其是在条件语句中,推荐写法是(参见 [PEP8](https://www.python.org/dev/peps/pep-0008/)):

```python
if condition:
pass
```

就好像上面代码中的 `if dead:` 一样。

而不是(虽然这么写通常也并不妨碍程序正常运行<a href='#fn1' name='fn1b'><sup>[1]</sup></a>):

```python
if condition is True:
pass
```

抑或:

```python
if condition == True:
pass
```

让我们再返回来接着讲递归函数。正常的**递归函数一定有个退出条件**。否则的话,就*无限循环*下去了…… 下面的程序在执行一会儿之后就会告诉你:`RecursionError: maximum recursion depth exceeded`(上面那个 “山上庙里讲故事的和尚说” 的程序,真要跑起来,也是这样):

```python
def x(n):
return n * x(n-1)
x(5)
```

---------------------------------------------------------------------------

RecursionError Traceback (most recent call last)

<ipython-input-3-daa4d33fb39b> in <module>
1 def x(n):
2 return n * x(n-1)
----> 3 x(5)

<ipython-input-3-daa4d33fb39b> in x(n)
1 def x(n):
----> 2 return n * x(n-1)
3 x(5)

... last 1 frames repeated, from the frame below ...

<ipython-input-3-daa4d33fb39b> in x(n)
1 def x(n):
----> 2 return n * x(n-1)
3 x(5)

RecursionError: maximum recursion depth exceeded

不用深究上面盗梦空间这个程序的其它细节,不过,通过以上三个递归程序 —— 两个很扯淡的例子,一个正经例子 —— 你已经看到了递归函数的共同特征:

> 1. 在 `return` 语句中返回的是*自身的调用*(或者是*含有自身的表达式*)
> 2. 为了避免死循环,*一定要有至少一个条件*下返回的不再是自身调用……

变量的作用域

再回来看计算阶乘的程序 —— 这是正经程序。这次我们把程序名写完整,`factorial()`:

```python
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n-1)

print(factorial(5))
```

120

最初的时候,这个函数的执行流程之所以令人迷惑,是因为初学者对*变量*的**作用域**把握得不够充分。

变量根据作用域,可以分为两种:全局变量(Global Variables)和局部变量(Local Variables)。

可以这样简化理解:

> * 在函数内部被赋值而后使用的,都是*局部变量*,它们的作用域是*局部*,无法被函数外的代码调用;
> * 在所有函数之外被赋值而后开始使用的,是*全局变量*,它们的作用域是*全局*,在函数内外都可以被调用。

定义如此,但通常程序员们会严格地遵守一条原则:

> 在函数内部绝对不调用全局变量。即便是必须改变全局变量,也只能通过函数的返回值在函数外改变全局变量。

你也必须遵守同样的原则。而这个原则同样可以在日常的工作生活中 “调用”:

> 做事的原则:自己的事自己做,别人的事,最多通过自己的产出让他们自己去搞……

再仔细观察一下以下代码。当一个变量被当做参数传递给一个函数的时候,这个变量本身并不会被函数所改变。比如,`a = 5`,而后,再把 `a` 当作参数传递给 `f(a)` 的时候,这个函数当然应该返回它内部任务完成之后应该传递回来的值,但 `a` 本身不会被改变。

```python
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n-1)

a = 5
b = factorial(a) # a 并不会因此改变;
print(a, b)
a = factorial(a) # 这是你主动为 a 再一次赋值……
print(a, b)
```

5 120
120 120

理解了这一点之后,再看 `factorial()` 这个递归函数的递归执行过程,你就能明白这个事实:

> 在每一次 factorial(n) 被调用的时候,它都会形成一个作用域,`n` 这个变量作为参数把它的值传递给了函数,*但是*,`n` 这个变量本身并不会被改变。

我们再修改一下上面的代码:

```python
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n-1)

n = 5 # 这一次,这个变量名称是 n
m = factorial(n) # n 并不会因此改变;
print(n, m)
```

5 120

在 `m = factorial(n)` 这一句中,`n` 被 `factorial()` 当做参数调用了,但无论函数内部如何操作,并不会改变变量 `n` 的值。

关键的地方在这里:在函数内部出现的变量 `n`,和函数外部的变量 `n` 不是一回事 —— **它们只是名称恰好相同而已**,函数参数定义的时候,用别的名称也没什么区别:

```python
def factorial(x): # 在这个语句块中出现的变量,都是局部变量
if x == 1:
return 1
else:
return x * factorial(x-1)

n = 5 # 这一次,这个变量名称是 n
m = factorial(n) # n 并不会因此改变;
print(n, m)
# 这个例子和之前再之前的示例代码有什么区别吗?
# 本质上没区别,就是变量名称换了而已……
```

5 120

函数开始执行的时候,`x` 的值,是由外部代码(即,函数被调用的那一句)传递进来的。即便函数内部的变量名称与外部的变量名称相同,它们也不是同一个变量。

递归函数三原则

现在可以小小总结一下了。

一个递归函数,之所以是一个有用、有效的递归函数,是因为它要遵守递归三原则。正如,一个机器人之所以是个合格的机器人,是因为它遵循[阿西莫夫三铁律](https://zh.wikipedia.org/wiki/%E6%9C%BA%E5%99%A8%E4%BA%BA%E4%B8%89%E5%AE%9A%E5%BE%8B)(Three Laws of Robotics)一样<a href='#fn2' name='fn2b'><sup>[2]</sup></a>。

> 1. 根据定义,递归函数必须在内部调用自己;
> 2. 必须设定一个退出条件;
> 3. 递归过程中必须能够逐步达到退出条件……

从这个三原则望过去,`factorial()` 是个合格有效的递归函数,满足第一条,满足第二条,尤其还满足第三条中的 “*逐步达到*”!

而那个扯淡的盗梦空间递归程序,说实话,不太合格,虽然它满足第一条,也满足第二条,第三条差点蒙混过关:它不是*逐步达到*,而是*不管怎样肯定能达到* —— 这明显是两回事…… 原谅它罢,它的作用就是当例子,一次正面的,一次负面的,作为例子算是功成圆满了!

刚开始的时候,初学者好不容易搞明白递归函数究竟是怎么回事之后,就不由自主地想 “我如何才能学会递归式思考呢?” —— 其实吧,这种想法本身可能并不是太正确或者准确。

准确地讲,递归是一种解决问题的方式。当我们需要解决的问题,可以被逐步拆分成很多越来越小的模块,然后每个小模块还都能用同一种算法处理的时候,用递归函数最简洁有效。所以,只不过是在遇到可以用递归函数解决问题的时候,才需要去写递归函数。

从这个意义上来看,递归函数是程序员为了自己方便而使用的,并不是为了计算机方便而使用 —— 计算机么,你给它的任务多一点或者少一点,对它来讲无所谓,反正有电就能运转,它自己又不付电费……

理论上来讲,所有用递归函数能完成的任务,不用递归函数也能完成,只不过代码多一点,啰嗦一点,看起来没有那么优美而已。

还有,递归,不像 “序列类型” 那样,是某个编程语言的特有属性。它其实是一种特殊算法,也是一种编程技巧,任何编程语言,都可以使用递归算法,都可以通过编写递归函数巧妙地解决问题。

但是,学习递归函数本身就很烧脑啊!这才是最大的好事。从迷惑,到不太迷惑,到清楚,到很清楚,再到特别清楚 —— 这是个非常有趣,非常有成就感的过程。

这种过程锻炼的是脑力 —— 在此之后,再遇到大多数人难以理解的东西,你就可以使用这一次积累的经验,应用你已经磨炼过的脑力。有意思。

至此,封面上的那个 “伪代码” 应该很好理解了:

```python
def teach_yourself(anything):
while not create():
learn()
practice()
return teach_yourself(another)

teach_yourself(coding)
```

自学还真的就是递归函数呢……

思考与练习

普林斯顿大学的一个网页,有很多递归的例子

https://introcs.cs.princeton.edu/java/23recursion/

-----
**脚注**

<a name='fn1'>[1]</a>:参见 Stackoverflow 上的讨论:[Boolean identity == True vs is True](https://stackoverflow.com/questions/27276610/boolean-identity-true-vs-is-true)

<a href='#fn1b'><small>↑Back to Content↑</small></a>

<a name='fn2'>[2]</a>:关于[阿西莫夫三铁律](https://zh.wikipedia.org/wiki/%E6%9C%BA%E5%99%A8%E4%BA%BA%E4%B8%89%E5%AE%9A%E5%BE%8B)(Three Laws of Robotics)的类比,来自著名的 Python 教程,[Think Python: How to Think Like a Computer Scientist](http://greenteapress.com/thinkpython2/html/index.html)

<a href='#fn2b'><small>↑Back to Content↑</small>

版权声明:© 超限思维 · LookMinds。本页为己方整理的亲口亲笔原稿处理稿/编者新译,非原文替代;原文版权归原作者/平台。本人原作请回原链。