《自学是门手艺》(第 2 部分 · 第 23–46 章)
《自学是门手艺》,共 46 章,本页第 23–46 章。你在调用函数的时候,你像是函数这个产品的用户。而你写一个函数,像是做一个产品,这个产品将来可能会被很多用户使用 —— 包括你自己。
Source: https://github.com/xiaolai/the-craft-of-selfteaching
《自学是门手艺》(第 2 部分 · 第 23–46 章)
> 作者:李笑来 · 本人在 GitHub 公开的书稿 · [GitHub 仓库](https://github.com/xiaolai/the-craft-of-selfteaching) · 本馆按 2026-05-21 的版本收录,共 46 章,本页为第 23–46 章。图片以链接形式给出。
函数的文档
你在调用函数的时候,你像是函数这个产品的用户。
而你写一个函数,像是做一个产品,这个产品将来可能会被很多用户使用 —— 包括你自己。
产品,就应该有产品说明书,别人用得着,你自己也用得着 —— 很久之后的你,很可能把当初的各种来龙去脉忘得一干二净,所以也同样需要产品说明书,别看那产品曾经是你自己设计的。
Python 在这方面很用功,把函数的 “产品说明书” 当作语言内部的功能,这也是为什么 Python 有 [Sphinx](http://www.sphinx-doc.org) 这种工具,而绝大多数其他语言没有的原因之一罢。
Docstring
在函数定义内部,我们可以加上 **Docstring**;将来函数的 “用户” 就可以通过 `help()` 这个内建函数,或者 `.__doc__` 这个 Method 去查看这个 Docstring,即,该函数的 “产品说明书”。
先看一个 Docstring 以及如何查看某个函数的 Docstring 的例子:
```python
def is_prime(n):
"""
Return a boolean value based upon
whether the argument n is a prime number.
"""
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
help(is_prime)
print(is_prime.__doc__)
is_prime.__doc__
```
Help on function is_prime in module __main__:
is_prime(n)
Return a boolean value based upon
whether the argument n is a prime number.
Return a boolean value based upon
whether the argument n is a prime number.
'\n Return a boolean value based upon\n whether the argument n is a prime number.\n '
Docstring 可以是多行字符串,也可以是单行字符串:
```python
def is_prime(n):
"""Return a boolean value based upon whether the argument n is a prime number."""
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
help(is_prime)
print(is_prime.__doc__)
is_prime.__doc__
```
Help on function is_prime in module __main__:
is_prime(n)
Return a boolean value based upon whether the argument n is a prime number.
Return a boolean value based upon whether the argument n is a prime number.
'Return a boolean value based upon whether the argument n is a prime number.'
Docstring 如若存在,必须在函数定义的内部语句块的开头,也必须与其它语句一样保持相应的缩进(Indention)。Docstring 放在其它地方不起作用:
```python
def is_prime(n):
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
"""
Return a boolean value based upon
whether the argument n is a prime number.
"""
help(is_prime)
print(is_prime.__doc__)
is_prime.__doc__
```
Help on function is_prime in module __main__:
is_prime(n)
None
书写 Docstring 的规范
规范,虽然是人们最好遵守的,但其实通常是很多人并不遵守的东西。
既然学,就要**像样** —— 这真的很重要。所以,非常有必要认真阅读 Python [PEP 257](https://www.python.org/dev/peps/pep-0257/) 关于 Docstring 的规范。
简要总结一下 PEP 257 中必须掌握的规范:
> 1. 无论是单行还是多行的 Docstring,一概使用三个双引号扩起;
> 2. 在 Docstring 内部,文字开始之前,以及文字结束之后,都不要有空行;
> 3. 多行 Docstring,第一行是概要,随后空一行,再写其它部分;
> 4. 完善的 Docstring,应该概括清楚以下内容:参数、返回值、可能触发的错误类型、可能的副作用,以及函数的使用限制等等;
> 5. 每个参数的说明都使用单独的一行……
由于我们还没有开始研究 Class,所以,关于 Class 的 Docstring 应该遵守什么样的规范就暂时略过了。然而,这种规范你总是要反复去阅读参照的。关于 Docstring,有两个规范文件:
> * [PEP 257: Docstring Convensions](https://www.python.org/dev/peps/pep-0257/)
> * [PEP 258: Docutils Design Specification](https://www.python.org/dev/peps/pep-0258/)
需要**格外注意**的是:
> Docstring 是**写给人看的**,所以,在复杂代码的 Docstring 中,写 **Why** 要远比写 _What_ 更重要 —— 你先记住这点,以后的体会自然会不断加深。
Sphinx 版本的 Docstring 规范
Sphinx 可以从 `.py` 文件里提取所有 Docstring,而后生成完整的 Documentation。将来若是你写大型的项目,需要生成完善的文档的时候,你就会发现 Sphinx 是个 “救命” 的家伙,省时、省力、省心、省命……
在这里,没办法一下子讲清楚 Sphinx 的使用,尤其是它还用它自己的一种标记语言,reStructureText,文件尾缀使用 `.rst`……
但是,可以看一个例子:
```python
class Vehicle(object):
'''
The Vehicle object contains lots of vehicles
:param arg: The arg is used for ...
:type arg: str
:param `*args`: The variable arguments are used for ...
:param `**kwargs`: The keyword arguments are used for ...
:ivar arg: This is where we store arg
:vartype arg: str
'''
def __init__(self, arg, *args, **kwargs):
self.arg = arg
def cars(self, distance, destination):
'''We can't travel a certain distance in vehicles without fuels, so here's the fuels
:param distance: The amount of distance traveled
:type amount: int
:param bool destinationReached: Should the fuels be refilled to cover required distance?
:raises: :class:`RuntimeError`: Out of fuel
:returns: A Car mileage
:rtype: Cars
'''
pass
help(Vehicle)
```
Help on class Vehicle in module __main__:
class Vehicle(builtins.object)
| Vehicle(arg, *args, **kwargs)
|
| The Vehicle object contains lots of vehicles
| :param arg: The arg is used for ...
| :type arg: str
| :param `*args`: The variable arguments are used for ...
| :param `**kwargs`: The keyword arguments are used for ...
| :ivar arg: This is where we store arg
| :vartype arg: str
|
| Methods defined here:
|
| __init__(self, arg, *args, **kwargs)
| Initialize self. See help(type(self)) for accurate signature.
|
| cars(self, distance, destination)
| We can't travel a certain distance in vehicles without fuels, so here's the fuels
|
| :param distance: The amount of distance traveled
| :type amount: int
| :param bool destinationReached: Should the fuels be refilled to cover required distance?
| :raises: :class:`RuntimeError`: Out of fuel
|
| :returns: A Car mileage
| :rtype: Cars
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
通过插件,Sphinx 也能支持 Google Style Docstring 和 Numpy Style Docstring。
以下两个链接,放在这里,以便你将来查询:
> * [sphinx.ext.napoleon – Support for NumPy and Google style docstrings](http://www.sphinx-doc.org/en/master/usage/extensions/napoleon.html)
> * [sphinx.ext.autodoc – Include documentation from docstrings](https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html)
保存到文件的函数
写好的函数,当然最好保存起来,以便将来随时调用。
模块
我们可以将以下内容保存到一个名为 `mycode.py` 的文件中 —— 这样可以被外部调用的 `.py` 文件,有个专门的称呼,**模块**(Module)—— 于是,它(任何一个 `.py` 文件)也可以被称为*模块*:
```python
# %load mycode.py
# 当前这个 Code Cell 中的代码,保存在当前文件夹中的 mycode.py 文件中
# 以下的代码,是使用 Jupyter 命令 %load mycode.py 导入到当前 Code Cell 中的:
def is_prime(n):
"""
Return a boolean value based upon
whether the argument n is a prime number.
"""
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
def say_hi(*names, greeting='Hello', capitalized=False):
"""
Print a string, with a greeting to everyone.
:param *names: tuple of names to be greeted.
:param greeting: 'Hello' as default.
:param capitalized: Whether name should be converted to capitalized before print. False as default.
:returns: None
"""
for name in names:
if capitalized:
name = name.capitalize()
print(f'{greeting}, {name}!')
```
而后,我们就可以在其它地方这样使用(以上代码现在已经保存在当前工作目录中的 `mycode.py`):
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import mycode
help(mycode.is_prime)
help(mycode.say_hi)
mycode.__name__
mycode.is_prime(3)
mycode.say_hi('mike', 'zoe')
```
Help on function is_prime in module mycode:
is_prime(n)
Return a boolean value based upon
whether the argument n is a prime number.
Help on function say_hi in module mycode:
say_hi(*names, greeting='Hello', capitalized=False)
Print a string, with a greeting to everyone.
:param *names: tuple of names to be greeted.
:param greeting: 'Hello' as default.
:param capitalized: Whether name should be converted to capitalzed before print. False as default.
:returns: None
'mycode'
True
Hello, mike!
Hello, zoe!
以上这个**模块**([Module](https://docs.python.org/3/tutorial/modules.html))的名称,就是 `mycode`。
模块文件系统目录检索顺序
当你向 Python 说 `import ...` 的时候,它要去寻找你所指定的文件,那个文件应该是 `import` 语句后面引用的名称,再加上 `.py` 构成的名字的文件。Python 会按照以下顺序去寻找:
> * 先去看看内建模块里有没有你所指定的名称;
> * 如果没有,那么就按照 `sys.path` 所返回的目录列表顺序去找。
你可以通过以下代码查看你自己当前机器的 `sys.path`:
```python
import sys
sys.path
```
在 `sys.path` 所返回的目录列表中,你当前的工作目录排在第一位。
有时,你需要指定检索目录,因为你知道你要用的模块文件在什么位置,那么可以用 `sys.path.append()` 添加一个搜索位置:
```python
import sys
sys.path.append("/My/Path/To/Module/Directory")
import my_module
```
系统内建的模块
你可以用以下代码获取系统内建模块的列表:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import sys
sys.builtin_module_names
"_sre" in sys.builtin_module_names # True
"math" in sys.builtin_module_names # False (根据自己电脑库的安装情况,结果会有不同)
```
('_abc',
'_ast',
'_codecs',
'_collections',
'_functools',
'_imp',
'_io',
'_locale',
'_operator',
'_signal',
'_sre',
'_stat',
'_string',
'_symtable',
'_thread',
'_tracemalloc',
'_warnings',
'_weakref',
'atexit',
'builtins',
'errno',
'faulthandler',
'gc',
'itertools',
'marshal',
'posix',
'pwd',
'sys',
'time',
'xxsubtype',
'zipimport')
True
False
跟变量名、函数名,不能与关键字重名一样,你的模块名称也最好别与系统内建模块名称重合。
引入指定模块中的特定函数
当你使用 `import mycode` 的时候,你向当前工作空间引入了 `mycode` 文件中定义的所有函数,相当于:
```python
from mycode import *
```
你其实可以只引入当前需要的函数,比如,只引入 `is_prime()`:
```python
from mycode import is_prime
```
这种情况下,你就不必使用 `mycode.is_prime()` 了;而是就好像这个函数就写在当前工作空间一样,直接写 `is_prime()`:
```python
from mycode import is_prime
is_prime(3)
```
True
注意,如果当前目录中并没有 `mycode.py` 这个文件,那么,`mycode` 会被当作目录名再被尝试一次 —— 如果当前目录内,有个叫做 `mycode` 的目录(或称文件夹)且该目录下同时要存在一个 [`__init__.py`](https://docs.python.org/3/reference/import.html#regular-packages) 文件(通常为空文件,用于标识本目录形成一个包含多个模块的 **包**([packages](https://docs.python.org/3/reference/import.html#regular-packages)),它们处在一个独立的 **命名空间**([namespace](https://docs.python.org/3/glossary.html#term-namespace-package))),那么,`from mycode import *` 的作用就是把 `mycode` 这个文件夹中的所有 `.py` 文件全部导入……
如果我们想要导入 `foo` 这个目录中的 `bar.py` 这个模块文件,那么,可以这么写:
```python
import foo.bar
```
或者
```python
from foo import bar
```
引入并使用化名
有的时候,或者为了避免混淆,或者为了避免输入太多字符,我们可以为引入的函数设定 **化名**(alias),而后使用化名调用函数。比如:
```python
from mycode import is_prime as isp
isp(3)
```
True
甚至干脆给整个模块取个化名:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import mycode as m
m.is_prime(3)
m.say_hi('mike', 'zoe')
```
True
Hello, mike!
Hello, zoe!
模块中不一定只有函数
一个模块文件中,不一定只包含函数;它也可以包含函数之外的可执行代码。只不过,在 `import` 语句执行的时候,模块中的非函数部分的可执行代码,只执行一次。
有一个 Python 的彩蛋,恰好是可以用在此处的最佳例子 —— 这个模块是 `this`,它的文件名是 [`this.py`](https://github.com/python/cpython/blob/master/Lib/this.py):
```python
import this
```
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
这个 `this` 模块中的代码如下:
```python
s = """Gur Mra bs Clguba, ol Gvz Crgref
Ornhgvshy vf orggre guna htyl.
Rkcyvpvg vf orggre guna vzcyvpvg.
Fvzcyr vf orggre guna pbzcyrk.
Pbzcyrk vf orggre guna pbzcyvpngrq.
Syng vf orggre guna arfgrq.
Fcnefr vf orggre guna qrafr.
Ernqnovyvgl pbhagf.
Fcrpvny pnfrf nera'g fcrpvny rabhtu gb oernx gur ehyrf.
Nygubhtu cenpgvpnyvgl orngf chevgl.
Reebef fubhyq arire cnff fvyragyl.
Hayrff rkcyvpvgyl fvyraprq.
Va gur snpr bs nzovthvgl, ershfr gur grzcgngvba gb thrff.
Gurer fubhyq or bar-- naq cersrenoyl bayl bar --boivbhf jnl gb qb vg.
Nygubhtu gung jnl znl abg or boivbhf ng svefg hayrff lbh'er Qhgpu.
Abj vf orggre guna arire.
Nygubhtu arire vf bsgra orggre guna *evtug* abj.
Vs gur vzcyrzragngvba vf uneq gb rkcynva, vg'f n onq vqrn.
Vs gur vzcyrzragngvba vf rnfl gb rkcynva, vg znl or n tbbq vqrn.
Anzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!"""
d = {}
for c in (65, 97):
for i in range(26):
d[chr(i+c)] = chr((i+13) % 26 + c)
print("".join([d.get(c, c) for c in s]))
```
这个 `this.py` 文件中也没有什么函数,但这个文件里所定义的变量,我们都可以在 `import this` 之后触达:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import this
this.d
this.s
```
{'A': 'N',
'B': 'O',
'C': 'P',
'D': 'Q',
'E': 'R',
'F': 'S',
'G': 'T',
'H': 'U',
'I': 'V',
'J': 'W',
'K': 'X',
'L': 'Y',
'M': 'Z',
'N': 'A',
'O': 'B',
'P': 'C',
'Q': 'D',
'R': 'E',
'S': 'F',
'T': 'G',
'U': 'H',
'V': 'I',
'W': 'J',
'X': 'K',
'Y': 'L',
'Z': 'M',
'a': 'n',
'b': 'o',
'c': 'p',
'd': 'q',
'e': 'r',
'f': 's',
'g': 't',
'h': 'u',
'i': 'v',
'j': 'w',
'k': 'x',
'l': 'y',
'm': 'z',
'n': 'a',
'o': 'b',
'p': 'c',
'q': 'd',
'r': 'e',
's': 'f',
't': 'g',
'u': 'h',
'v': 'i',
'w': 'j',
'x': 'k',
'y': 'l',
'z': 'm'}
"Gur Mra bs Clguba, ol Gvz Crgref\n\nOrnhgvshy vf orggre guna htyl.\nRkcyvpvg vf orggre guna vzcyvpvg.\nFvzcyr vf orggre guna pbzcyrk.\nPbzcyrk vf orggre guna pbzcyvpngrq.\nSyng vf orggre guna arfgrq.\nFcnefr vf orggre guna qrafr.\nErnqnovyvgl pbhagf.\nFcrpvny pnfrf nera'g fcrpvny rabhtu gb oernx gur ehyrf.\nNygubhtu cenpgvpnyvgl orngf chevgl.\nReebef fubhyq arire cnff fvyragyl.\nHayrff rkcyvpvgyl fvyraprq.\nVa gur snpr bs nzovthvgl, ershfr gur grzcgngvba gb thrff.\nGurer fubhyq or bar-- naq cersrenoyl bayl bar --boivbhf jnl gb qb vg.\nNygubhtu gung jnl znl abg or boivbhf ng svefg hayrff lbh'er Qhgpu.\nAbj vf orggre guna arire.\nNygubhtu arire vf bsgra orggre guna *evtug* abj.\nVs gur vzcyrzragngvba vf uneq gb rkcynva, vg'f n onq vqrn.\nVs gur vzcyrzragngvba vf rnfl gb rkcynva, vg znl or n tbbq vqrn.\nAnzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!"
试试吧,试试能否独立读懂这个文件里的代码 —— 对初学者来说,还是挺练脑子的呢!
它先是通过一个规则生成了一个密码表,保存在 `d` 这个字典中;而后,将 `s` 这个变量中保存的 “密文” 翻译成了英文……
或许,你可以试试,看看怎样能写个函数出来,给你一段英文,你可以把它加密成跟它一样的 “密文”?
dir() 函数
你的函数,保存在模块里之后,这个函数的用户(当然也包括你),可以用 `dir()` 函数查看模块中可触达的变量名称和函数名称:
```python
import mycode
dir(mycode)
```
['__builtins__',
'__cached__',
'__doc__',
'__file__',
'__loader__',
'__name__',
'__package__',
'__spec__',
'is_prime',
'say_hi']
测试驱动的开发
写一个函数,或者写一个程序,换一种说法,其实就是 “实现一个算法” —— 而所谓的 “算法”,Wikipedia 上的定义是这样的:
> In mathematics and computer science, an **algorithm** is an unambiguous specification of how to solve a class of problems. Algorithms can perform *calculation*, _data processing_, and *automated reasoning tasks*.
“算法”,其实没多神秘,就是 “解决问题的步骤” 而已。
在第二部分的第一章里,我们看过一个判断是否为闰年的函数:
> 让我们写个判断闰年年份的函数,取名为 is_leap(),它接收一个年份为参数,若是闰年,则返回 True,否则返回 False。
>
> 根据闰年的定义:
>
> > * 年份应该是 4 的倍数;
> > * 年份能被 100 整除但不能被 400 整除的,不是闰年。
> > * 所以,相当于要在能被 4 整除的年份中,排除那些能被 100 整除却不能被 400 整除的年份。
不要往回翻!现在自己动手尝试着写出这个函数?你会发现其实并不容易的……
```python
def is_leap(year):
pass
```
第一步,跟很多人想象得不一样,第一步不是上来就开始写……
第一步是先假定这个函数写完了,我们需要验证它返回的结果对不对……
这种 “通过先想办法验证结果而后从结果倒推” 的开发方式,是一种很有效的方法论,叫做 “Test Driven Development”,以测试为驱动的开发。
如果我写的 `is_leap(year)` 是正确的,那么:
> * `is_leap(4)` 的返回值应该是 `True`
> * `is_leap(200)` 的返回值应该是 `False`
> * `is_leap(220)` 的返回值应该是 `True`
> * `is_leap(400)` 的返回值应该是 `True`
能够罗列出以上四种情况,其实只不过是根据算法 “考虑全面” 之后的结果 —— 但你自己试试就知道了,无论多简单的事,想要 “考虑全面” 好像并不容易……
所以,在写 `def is_leap(year)` 中的内容之前,我只是用 `pass` 先把位置占上,而后在后面添加了四个用来测试结果的语句 —— 它们的值,现在当然都是 `False`…… 等我把整个函数写完了,写正确了,那么它们的值就都应该变成 `True`。
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
def is_leap(year):
pass
is_leap(4) is True
is_leap(200) is False
is_leap(220) is True
is_leap(400) is True
```
False
False
False
False
考虑到更多的年份不是闰年,所以,排除顺序大抵上应该是这样:
> * 先假定都不是闰年;
> * 再看看是否能被 `4` 整除;
> * 再剔除那些能被 `100` 整除但不能被 `400` 整除的年份……
于是,先实现第一句:“先假定都不是闰年”:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
def is_leap(year):
r = False
return r
is_leap(4) is True
is_leap(200) is False
is_leap(220) is True
is_leap(400) is True
```
False
True
False
False
然后再实现这部分:“年份应该是 4 的倍数”:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
def is_leap(year):
r = False
if year % 4 == 0:
r = True
return r
is_leap(4) is True
is_leap(200) is False
is_leap(220) is True
is_leap(400) is True
```
True
False
True
True
现在剩下最后一条了:“剔除那些能被 `100` 整除但不能被 `400` 整除的年份”…… 拿一个参数值,比如,`200` 为例:
> * 因为它能被 `4` 整除,所以,使 `r = True`,
> * 然后再看它是否能被 `100` 整除 —— 能 —— 既然如此再看它能不能被 `400` 整除,
> * 如果不能,那就让 `r = False`;
> * 如果能,就保留 `r` 的值……
> 如此这般,`200` 肯定使得 `r = False`。
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
def is_leap(year):
r = False
if year % 4 == 0:
r = True
if year % 100 == 0:
if year % 400 !=0:
r = False
return r
is_leap(4) is True
is_leap(200) is False
is_leap(220) is True
is_leap(400) is True
```
True
True
True
True
尽管整个过程读起来很直观,但真的要自己从头到尾操作,就可能四处出错,不信你就试试 —— 这一页最下面添加一个单元格,自己动手从头写到尾试试……
当然,Python 内建库中的 `datetime.py` 模块里的代码更简洁,之前给你看过:
```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 语言中,有专门用来 “试错” 的流程控制 —— 今天的绝大多数编程语言都有这种 “试错语句”。
当一个程序开始执行的时候,有两种错误可能会导致程序执行失败:
> * 语法错误(Syntax Errors)
> * 意外(Exceptions)
比如,在 Python3 中,你写 `print i`,而没有写 `print(i)`,那么你犯的是语法错误,于是,解析器会直接提醒你,你在第几行犯了什么样的语法错误。语法错误存在的时候,程序无法启动执行。
但是,有时会出现这种情况:语法上完全正确,但出现了**意外**。这种错误,都是程序已经执行之后才发生的(Runtime Errors)—— 因为只要没有语法错误,程序就可以启动。比如,你写的是 `print(11/0)`:
```python
print(11/0)
```
---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
<ipython-input-2-5544d98276be> in <module>
----> 1 print(11/0)
ZeroDivisionError: division by zero
虽然这个语句本身没有语法错误,但这个表达式是不能被处理的。于是,它触发了 `ZeroDivisionError`,这个 “意外” 使得程序不可能继续执行下去。
在 Python 中,定义了大量的常见 “意外”,并且按层级分类:
> 在第三部分阅读完毕之后,可以回来重新查看以下官方文档:<br />
> https://docs.python.org/3/library/exceptions.html
``` bash
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
| +-- ModuleNotFoundError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning
```
拿 `FileNotFoundError` 为例 —— 当我们想要打开一个文件之前,其实应该有个办法提前验证一下那个文件是否存在。如果那个文件并不存在,就会引发 “意外”。
```python
f = open('test_file.txt', 'r')
```
---------------------------------------------------------------------------
FileNotFoundError Traceback (most recent call last)
<ipython-input-3-5fac19176fe6> in <module>
----> 1 f = open('test_file.txt', 'r')
FileNotFoundError: [Errno 2] No such file or directory: 'test_file.txt'
在 Python 中,我们可以用 `try` 语句块去执行那些可能出现 “意外” 的语句,`try` 也可以配合 `except`、`else`、`finally` 使用。从另外一个角度看,`try` 语句块也是一种特殊的流程控制,专注于 “当意外发生时应该怎么办?”
```python
try:
f = open('test_file.txt', 'r')
except FileNotFoundError as fnf_error:
print(fnf_error)
```
[Errno 2] No such file or directory: 'test_file.txt'
如此这般的结果是:
> 当程序中的语句 `f = open('test_file.txt', 'r')` 因为 `test_file.txt` 不存在而引发意外之时,`except` 语句块会接管流程;而后,又因为在 `except` 语句块中我们指定了 `FileNotFoundError`,所以,若是 `FileNotFoundError` 真的发生了,那么,`except` 语句块中的代码,即,`print(fnf_error)` 会被执行……
你可以用的试错流程还有以下变种:
```python
try:
do_something()
except built_in_error as name_of_error:
do_something()
else:
do_something()
```
或者:
```python
try:
do_something()
except built_in_error as name_of_error:
do_something()
else:
do_something()
finally:
do_something()
```
甚至可以嵌套:
```python
try:
do_something()
except built_in_error as name_of_error:
do_something()
else:
try:
do_something()
except built_in_error as name_of_error:
do_something()
...
```
更多关于错误处理的内容,请在阅读完第三部分中与 Class 相关的内容之后,再去详细阅读以下官方文档:
> * [Errors and Exceptions](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/docs.python.org/3/tutorial/errors.html)
> * [Built-in Exceptions](https://docs.python.org/3/library/exceptions.html)
> * [Handling Exceptions](https://wiki.python.org/moin/HandlingExceptions)
理论上,这一章不应该套上这么大的标题:《测试驱动开发》,因为在实际开发过程中,所谓测试驱动开发要使用更为强大更为复杂的模块、框架和工具,比如,起码使用 Python 内建库中的 [unittest](https://docs.python.org/3/library/unittest.html) 模块。
在写程序的过程中,为别人(和将来的自己)写注释、写 Docstring;在写程序的过程中,为了保障程序的结果全面正确而写测试;或者干脆在最初写的时候就考虑到各种意外所以使用试错语句块 —— 这些明明是天经地义的事情,却是绝大多数人不做的…… 因为感觉有点麻烦。
这里是 “聪明反被聪明误” 的最好示例长期堆积的地方。很多人真的是因为自己很聪明,所以才觉得 “没必要麻烦” —— 这就好像当年苏格拉底仗着自己记忆力无比强大甚至干脆过目不忘于是鄙视一切记笔记的人一样。
但是,随着时间的推移,随着工程量的放大,到最后,那些 “聪明人” 都被自己坑死了 —— 聪明本身搞不定工程,能搞定工程的是智慧。苏格拉底自己并没完成任何工程,是他的学生柏拉图不顾他的嘲笑用纸笔记录了一切;而后柏拉图的学生亚里士多德才有机会受到苏格拉底的启发,写了《前分析篇》,提出对人类影响至今的 “三段论”……
千万不要因为这第二部分中所举的例子太容易而把自己迷惑了。刻意选择简单的例子放在这里,是为了让读者更容易集中精力去理解关于自己动手写函数的方方面面 —— 可将来你自己真的动手去做,哪怕真的去阅读真实的工程代码,你就会发现,难度还是很高的。现在的轻敌,会造成以后的溃败。
现在还不是时候,等你把整本书都完成之后,记得回来再看这个链接:
> * [doctest —— Test interactive Python examples](https://docs.python.org/3/library/doctest.html)
> * [unittest —— Unit testing framework](https://docs.python.org/3/library/unittest.html)
可执行的 Python 文件
理论上来讲,你最终可以把任何一个程序,无论大小,都封装(或者囊括)到仅仅一个函数之中。按照惯例(Convention),这个函数的名称叫做 `main()`:
```python
def routine_1():
print('Routine 1 done.')
def routine_2():
sub_routine_1()
sub_routine_2()
print('Routine 2 done.')
def sub_routine_1():
print('Sub-routine 1 done.')
def sub_routine_2():
print('Sub-routine 2 done.')
def main():
routine_1()
routine_2()
print('This is the end of the program.')
if __name__ == '__main__':
main()
```
Routine 1 done.
Sub-routine 1 done.
Sub-routine 2 done.
Routine 2 done.
This is the end of the program.
当一个模块(其实就是存有 Python 代码的 `.py` 文件,例如:`mycode.py`)被 `import` 语句导入的时候,这个模块的 `__name__` 就是模块名(例如:`'mycode'`)。
而当一个模块被命令行运行的时候,这个模块的 `__name__` 就被 Python 解释器设定为 `'__main__'`。
把一个程序整个封装到 `main()` 之中,而后在模块代码里加上:
```python
if __name__ == '__main__':
main()
```
这么做的结果是:
> 1. 当 Python 文件被当作模块,被 `import` 语句导入时,`if` 判断失败,`main()` 函数不被执行;
> 2. 当 Python 文件被 `python -m` 运行的时候,`if` 判断成功,`main()` 函数才被执行。
还记得那个 Python 的彩蛋吧?`this.py` 的代码如下:
```python
s = """Gur Mra bs Clguba, ol Gvz Crgref
Ornhgvshy vf orggre guna htyl.
Rkcyvpvg vf orggre guna vzcyvpvg.
Fvzcyr vf orggre guna pbzcyrk.
Pbzcyrk vf orggre guna pbzcyvpngrq.
Syng vf orggre guna arfgrq.
Fcnefr vf orggre guna qrafr.
Ernqnovyvgl pbhagf.
Fcrpvny pnfrf nera'g fcrpvny rabhtu gb oernx gur ehyrf.
Nygubhtu cenpgvpnyvgl orngf chevgl.
Reebef fubhyq arire cnff fvyragyl.
Hayrff rkcyvpvgyl fvyraprq.
Va gur snpr bs nzovthvgl, ershfr gur grzcgngvba gb thrff.
Gurer fubhyq or bar-- naq cersrenoyl bayl bar --boivbhf jnl gb qb vg.
Nygubhtu gung jnl znl abg or boivbhf ng svefg hayrff lbh'er Qhgpu.
Abj vf orggre guna arire.
Nygubhtu arire vf bsgra orggre guna *evtug* abj.
Vs gur vzcyrzragngvba vf uneq gb rkcynva, vg'f n onq vqrn.
Vs gur vzcyrzragngvba vf rnfl gb rkcynva, vg znl or n tbbq vqrn.
Anzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!"""
d = {}
for c in (65, 97):
for i in range(26):
d[chr(i+c)] = chr((i+13) % 26 + c)
print("".join([d.get(c, c) for c in s]))
```
所以,只要 `import this`,`this.py` 中的代码就被执行:
```python
import this
```
我在当前目录下,保存了一个文件 `that.py`,它的内容如下 —— 其实就是把 `this.py` 之中的代码封装到 `main()` 函数中了:
```python
# %load that.py
def main():
s = """Gur Mra bs Clguba, ol Gvz Crgref
Ornhgvshy vf orggre guna htyl.
Rkcyvpvg vf orggre guna vzcyvpvg.
Fvzcyr vf orggre guna pbzcyrk.
Pbzcyrk vf orggre guna pbzcyvpngrq.
Syng vf orggre guna arfgrq.
Fcnefr vf orggre guna qrafr.
Ernqnovyvgl pbhagf.
Fcrpvny pnfrf nera'g fcrpvny rabhtu gb oernx gur ehyrf.
Nygubhtu cenpgvpnyvgl orngf chevgl.
Reebef fubhyq arire cnff fvyragyl.
Hayrff rkcyvpvgyl fvyraprq.
Va gur snpr bs nzovthvgl, ershfr gur grzcgngvba gb thrff.
Gurer fubhyq or bar-- naq cersrenoyl bayl bar --boivbhf jnl gb qb vg.
Nygubhtu gung jnl znl abg or boivbhf ng svefg hayrff lbh'er Qhgpu.
Abj vf orggre guna arire.
Nygubhtu arire vf bsgra orggre guna *evtug* abj.
Vs gur vzcyrzragngvba vf uneq gb rkcynva, vg'f n onq vqrn.
Vs gur vzcyrzragngvba vf rnfl gb rkcynva, vg znl or n tbbq vqrn.
Anzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!"""
d = {}
for c in (65, 97):
for i in range(26):
d[chr(i+c)] = chr((i+13) % 26 + c)
print("".join([d.get(c, c) for c in s]))
if __name__ == '__main__':
main()
```
于是,当你在其它地方导入它的时候,`import that`,`main()` 函数的内容不会被执行:
```python
import that
```
但是,你在命令行中,用 `python that.py`,或者 `python -m that` 将 `that.py` 当作可执行模块运行的时候,`main()` 就会被执行 —— 注意,不要写错,`python -m that.py` 会报错的 —— 有 `-m` 参数,就不要写文件尾缀 `.py`:
```bash
%%bash
python that.py
```
```bash
%%bash
python -m that
```
像 `that.py` 那样把整个程序放进 `main()` 函数之后,`import that` 不会自动执行 main 函数里的代码。不过,你可以调用 that.main():
```python
import that
that.main()
```
当然,`that.py` 之中没有任何 Docstring,所以 `help(that)` 的结果是这样的:
```python
import that
help(that)
```
所以,之前那个从 37 万多个词汇中挑出 3700 多个字母加起来等于 100 的词汇的程序,也可以写成以下形式:
```python
#!/usr/bin/env python
def sum_of_word(word):
sum = 0
for char in word:
sum += ord(char) - 96
return sum
def main(wordlist, result):
with open(result, 'w') as result:
with open(wordlist, 'r') as file:
for word in file.readlines():
if sum_of_word(word.strip()) == 100:
result.write(word)
if __name__ == '__main__':
main('words_alpha.txt', 'results.txt')
```
至于以上代码中的第一行,`#!/usr/bin/env python` 是怎么回事,建议你自己动手解决一下,去 Google:
> [`python3 script executable`](https://www.google.com/search?q=python3+script+executable)
你会很快弄明白的……
另外,再搜索一下:
> [`python3 script executable parameters retrieving`](https://www.google.com/search?q=python3+script+executable+parameters+retrieving)
你就可以把以上程序改成在命令行下能够接收指定参数的 Python 可执行文件……
顺带说,`import this` 的彩蛋有更好玩的玩法:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import this
love = this
this is love # True
love is True # False
love is False # False
love is not True or False # True
love is not True or False; love is love # True True
```
True
False
False
True
True
True
在 Terminal 里输入 `python ⏎` 而后在 Interactive Shell 里逐句输入试试。`love = this` 后面的每一句,都是布尔运算,想想看为什么是那样的结果?
```python
import this
love = this
this is love
# True, 试试看,id(this) 和 id(love) 是同一个值
# 即,它们的内存地址相同
love is True
# False, id(love) 和 id(True) 不是同一个值
love is False
# 同上
love is not True or False
# is not 的优先级比 or 高;所以相当于是:
# (love is not True) or False,于是返回 True
love is not True or False; love is love
# 重复一次上一句 —— `;` 是语句分隔符
# 而后 love is love 当然是 True
```
注意以下代码中,`id()` 函数的输出结果:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import this
love = this
this is love
love is True
love is False
love is not True or False
love is not True or False; love is love
id(love)
id(this)
id(True)
id(False)
love is not True
```
True
False
False
True
True
True
4345330968
4345330968
4308348176
4308349120
True
Python 的操作符优先级,完整表格在这里:
> [Operator precedence](https://docs.python.org/3/reference/expressions.html#operator-precedence)
Python 的更多彩蛋:
> [Python Easter Eggs](https://github.com/OrkoHunter/python-easter-eggs)
刻意思考
随着时间的推移,你会体会到它的威力:
> 刻意思考哪儿需要刻意练习
只不过是一句话而已,却因知道或不知道,竟然会逐渐形成天壤之别的差异,也是神奇。
刻意思考,就是所谓的琢磨。琢磨这事,一旦开始就简单得要死,可无从下手的时候就神秘无比。让我们再看一个 “刻意思考” —— 即,琢磨 —— 的应用领域:
> **这东西能用在哪儿呢?**
很多人学了却没怎么练,有一个很现实的原因 —— 没什么地方用得上。
这也怪我们的应试教育,大学前上 12 年学,“学”(更多是被逼的)的绝大多数东西,只有一个能够切实体会到的用处,考试 —— 中考、高考,以及以它们为目标的无数 “模考”…… 于是,反过来,不管学什么东西,除了考试之外,几乎无法想象其他的用处。
一旦我们启动了对某项技能的自学之后,在那过程中,最具价值的刻意思考就是,时时刻刻琢磨 “这东西能用在哪儿呢?”
比如,当你看到字符串的 Methods 中有一个 `str.zfill()` 的时候,马上就能想到,“嗯!这可以用来批量更名文件……”
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/rename-macos.png?raw=true)
虽然现在的 Mac OS 操作系统里已经有相当不错的批量更名工具内建在 Finder 之中(选中多个文件之后,在右键菜单中能看到 `rename` 命令),但这是近期才加进去的功能,几年前却没有 —— 也就是说,几年前的时候,有人可以用 `str.zfill()` 写个简单的程序完成自己的工作,而另外一些人仅因为操作系统没有提供类似的功能就要么手工做,要么干脆忍着忘了算了……
但更多的时候,需要你花时间去琢磨,才能找到用处。
找到用处,有时候还真挺难的 —— 因为人都一样,容易被自己的眼界所限,放眼望过去,没有用处,自然也就不用了,甚至不用学了,更不用提那就肯定是感觉不用练了……
所以,仔细想想罢 —— 那些在学校里帮老师干活的小朋友们,更多情况下还真不是很多人以为的 “拍马屁”(不排除肯定有哈),只不过是在 “主动找活干”……
**找活干**,是应用所学的最有效方式,有活干,所以就有问题需要解决,所以就有机会反复攻关,在这个过程中,**以用带练**……
所以,很多人在很多事上都想反了。
人们常常取笑那些呼哧呼哧干活的人,笑着说,“能者多劳”,觉得他们有点傻。
这话真的没错。但这么说更准:**劳者多能** —— 你看,都想反了吧?
到最后,一切自学能力差的人,外部的表现都差不多,都起码包括这么一条:眼里没活。他们也不喜欢干活,甚至也没想过,玩乐也是干活(每次逢年过节玩得累死那种)—— 从消耗或者成本的角度来看根本没啥区别 —— 只不过那些通常都是没有产出的活而已。
在最初想不出有什么用处的时候,还可以退而求其次,看看 “别人想出什么用处没有?” —— 比如,我去 Google `best applications of python skill`,在第一个页面我就发现了这么篇文章:“[What exactly can you do with Python? ](https://medium.freecodecamp.org/what-can-you-do-with-python-the-3-main-applications-518db9a68a78)”,翻了一会儿觉得颇有意思……
再高阶一点的刻意思考(琢磨),无非是在 “这东西能用在哪儿呢?” 这句话里加上一个字而已:
> 这东西**还**能用在哪儿呢?
我觉得这个问题对思维训练的帮助非常深刻 —— 别看只是多了一个字而已。
当我读到在编程的过程中有很多的 “约定” 的时候,就琢磨着:
> * 哦,原来约定如此重要……
> * 哦,原来竟然有那么多人不重视约定……
> * 哦,原来就应该直接过滤掉那些不遵守约定的人……
> —— 那这个原理(东西)还能用在哪儿呢?
> —— 哦,在生活中也一样,遇到不遵守约定的人或事,直接过滤,不要浪费自己的生命……
学编程真的很有意思,因为这个领域是世界上最聪明的人群之一开辟出来并不断共同努力着发展的,所以,在这个世界里有很多思考方式,琢磨方式,甚至可以干脆称为 “做事哲学” 的东西,可以普遍应用在其它领域,甚至其它任何领域。
比如,在开发方法论中,有一个叫做 [MoSCoW Method](https://en.wikipedia.org/wiki/MoSCoW_method) 的东西,1994 年由 Clegg Dai 在《Case Method Fast-Track: A RAD Approach》一书中提出的 —— 两个 `o` 字母放在那里,是为了能够把这个缩写读出来,发音跟莫斯科一样。
简单说,就是,凡事都可以分为:
> * Must have
> * Should have
> * Could have
> * Won't have
于是,在开发的时候,把所谓的需求打上这 4 个标签中的某一个,以此分类,就很容易剔除掉那些实际上做了还不如不做的功能……
琢磨一下罢,这个东西还可以用在什么地方?
显然,除了编程之外,其他应用领域挺多的,这个原则相当地有启发性……
我写书就是这样的。在准备的过程中 —— 这个过程比绝大多数人想象得长很多 —— 我会罗列所有我能想到的相关话题…… 等我觉得已经再也没有什么可补充的时候,再为这些话题写上几句话构成大纲…… 这时候就会发现很多话题其实应该是同一个话题。如此这般,一次扩张,一次收缩之后,就会进行下一步,应用 MoSCoW 原则,给这些话题打上标签 —— 在这过程中,总是发现很多之前感觉必要的话题,其实可以打上 `Won't have` 的标签,于是,把它们剔除,然后从 `Must have` 开始写起,直到 `Should have`,至于 `Could have` 看时间是否允许,看情况,比如,看有没有最后期限限制……
在写书这事上,我总是给人感觉很快,事实上也是,因为有方法论 —— 但显然,那方法论不是从某一本 “如何写书” 的书里获得的,而是从另外一个看起来完全不相关的领域里习得后琢磨到的……
所谓的 “活学活用”,所谓的 “触类旁通”,也不过如此。
战胜难点
无论学什么,都有难点。所谓的 “学习曲线陡峭”,无非就是难点靠前、难点很多、难点貌似很难而已。
然而,相信我,所有的难点,事实上都可以被拆解成更小的单元,而后在逐一突破的时候,就没那么难了。逐一突破全部完成之后,再拼起来重新审视的时候就会发现那所谓的难常常只不过是错觉、幻觉而已 —— 我把它称为**困难幻觉**。
把一切都当作手艺看的好处之一就是心态平和,因为你知道那不靠天分和智商,它靠的是另外几件事:不混时间,刻意思考,以及刻意练习 —— 其实吧,老祖宗早就有总结:
> 天下无难事,只怕**有心人**……
大家都是人,咋可能没 “心” 呢?
想成为有心人,其实无非就是学会拆解之后逐一突破,就这么简单。
第三部分所用的例子依然非常简单 —— 这当然是作者的刻意;但是,所涉及的话题都是被认为 “很难很难”、“很不容易理解”、“初学者就没必要学那些了” 之类的话题:
> * 类,以及面向对象编程(Class,OOP)
> * 迭代器、生成器、装饰器(Iterators、Generators、Decorators)
> * 正则表达式(Regular Expressions)
> * 巴科斯-诺尔范式(Backus Normal Form)
尤其是最后一个,巴科斯-诺尔范式,几乎所有的编程入门书籍都不会提到……
然而,这些内容,在我设计《自学是门手艺》内容的过程中,是被当作 `Must have`,而不是 `Should have`,当然更不是 `Could have` 或者 `Won't have` 的。
它们属于 `Must have` 的原因也很简单:
> 无论学什么都一样,难的部分不学会,就等于整个没学。
—— 仅因为不够全面。
有什么必要干前功尽弃的事情呢?要么干脆别学算了,何必把自己搞成一个半吊子?—— 可惜,这偏偏是绝大多数人的习惯,学什么都一样,容易的部分糊弄糊弄,困难的部分直接回避…… 其实,所有焦虑,都是这样在许多年前被埋下,在许多年后生根发芽、茂盛发达的 —— 你想想看是不是如此?
虽然别人认为难,你刚开始也会有这样的错觉,但只要你开始施展 “读不懂也要读完,读完之后再读很多遍” 的手段,并且还 “不断自己动手归纳总结整理”,你就会 “发现”,其实没什么大不了的,甚至你会有错觉:
> “突然” 之间一切都明了了!
那个 “突然”,其实就是阿基米德的 [Eureka](https://en.wikipedia.org/wiki/Eureka_effect),那个他从澡堂里冲出来大喊大叫的词汇。
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/archimedes-eureka.png?raw=true)
其实吧,泡澡和冥想,还真是最容易产生 Eureka 状态的两种活动。原理在于,泡澡和打坐的时候,大脑都极其放松,乃至于原本相互之间并无联系的脑神经突触之间突然产生相互关联;而那结果就是日常生活中所描述的 “融会贯通”,于是,突然之间,Eureka!
自学者总是感觉幸福度很高,就是这个原因。日常中因为自学,所以总是遇到更多的困难。又因为这些东西不过是手艺,没什么可能终其一生也解决不了,恰恰相反,都是假以时日必然解决的 “困难”…… 于是,自学者恰恰因为遇到的 “困难” 多,所以才有更多遇到 “Eureka” 的可能性,那种幸福,还真的难以表述,即便表述清楚了,身边的人也难以理解,因为自学者就是很少很少。
对很多人来说,阅读的难点在于起初的时候它总是显得异常枯燥。
刚识字、刚上学的时候,由于理解能力有限,又由于年龄的关系于是耐心有限,所以,那时需要老师耐心陪伴、悉心引导。这就好像小朋友刚出生的时候,没有牙齿,所以只能喝奶差不多…… 然而,到了一定程度之后一定要断奶,是不是?可绝大多数人的实际情况却是,小学的时候爱上了 “奶嘴”(有人带着阅读),而后一生没有奶嘴就吃不下任何东西。
他们必须去 “上课”,需要有人给他们讲书。不仅如此,讲得 “不生动”、“不幽默” 还不行;就算那职业提供奶嘴的人(这是非常令人遗憾的真相:很多的时候,所谓的 “老师” 本质上只不过就是奶妈而已)帅气漂亮、生动幽默、尽职尽力…… 最终还是一样的结果 —— 绝大多数人依然没有完整掌握所有应该掌握的细节。
开始 “自学” 的活动,本质上来看,和**断奶**其实是一回事。
> * 知识就是知识,它没有任何义务去具备幽默生动的属性;
> * 手艺就是手艺,它没有任何义务去具备有趣欢乐的属性。
幽默与生动,是要自己去扮演的角色;有趣与欢乐,是要自己去挖掘的幸福 —— 它们从来都并不自动包含在知识和手艺之中。只有当它们被 “有心人” 掌握、被 “有心人” 应用、甚至被 “有心人” 拿去创造的时候,也只有 “有心人” 才能体会到那幽默与生动、那有趣与欢乐。
所以,有自学能力的人,不怕枯燥 —— 因为那本来就理应是枯燥的。这就好像人生本无意义,有意义的人生都是自己活出来的一样,有意义的知识都是自己用出来的 —— 对不用它的人,用不上它的人来说,只能也只剩下无法容忍的枯燥。
能够**耐心**读完那么多在别人看来 “极度枯燥” 的资料,是自学者的擅长。可那在别人看来 “无以伦比” 的耐心,究竟是哪儿来的呢?如何造就的呢?没断奶的人想象不出来。其实也很简单,首先,平静地接受了它枯燥的本质;其次,就是经过多次实践已然明白,无论多枯燥,总能读完;无论多难,多读几遍总能读懂…… 于是,到最后,**只不过是习惯了而已。**
第三部分关于编程的内容过后,还有若干关于自学的内容。
在反复阅读编程部分突破难点的过程之中、过程之后,你会对那些关于自学的内容有更深更生动的认识。很多道理过去你都知道是对的,只不过因为没有遇到过生动的例子 —— 主要是没遇到过能让自己感到生动的例子 —— 于是你就一直没有重视起来,于是,就还是那句话,那一点点的差异,造成了后来那么大的差距。
然而,既然知道了真相的你,以后就再也没办法蒙蔽自己了 —— 这就是收获,这就是进步。
类 —— 面向对象编程
面向对象编程
注意:当前这一小节所论述的内容,不是专属于哪个编程语言(比如 Python、JavaScript 或者 Golang)。
面向对象编程([Object Oriented Programming, OOP](https://en.wikipedia.org/wiki/Object-oriented_programming))是一种编程的范式(Paradigm),或者说,是一种方法论(Methodology)—— 可以说这是个很伟大的方法论,在我看来,现代软件工程能做那么复杂的宏伟项目,基本上都得益于这个方法论的普及。
争议
现在,OOP 的支持者与反对者在数量上肯定不是一个等级,绝大多数人支持 OOP 这种编程范式。
但是,从另外一个角度,反对 OOP 的人群中,牛人比例更高 —— 这也是个看起来颇为吊诡的事实。
比如,Erlang 的发明者,[Joe Armstrong](https://en.wikipedia.org/wiki/Joe_Armstrong_(programmer)) 就很讨厌 OOP,觉得它效率低下。他用的类比也确实令人忍俊不禁,说得也挺准的:
> 支持 OOP 的语言的问题在于,它们总是随身携带着一堆并不明确的环境 —— 你明明只不过想要个香蕉,可你所获得的是一个大猩猩手里拿着香蕉…… 以及那大猩猩身后的整个丛林!<br /> —— [Coders at Work](http://www.codersatwork.com)
创作 UTF-8 和 Golang 的程序员 [Rob Pike](https://en.wikipedia.org/wiki/Rob_Pike),更看不上 OOP,在 2004 年的一个讨论帖里直接把 OOP 比作 “[Roman numerals of computing](https://groups.google.com/forum/#!topic/comp.os.plan9/VUUznNK2t4Q%5B151-175%5D)” —— 讽刺它就是很土很低效的东西。八年后又[挖坟把一个 Java 教授写的 OOP 文章嘲弄了一番](https://plus.google.com/+RobPikeTheHuman/posts/hoJdanihKwb):“也不知道是什么脑子,认为写 6 个新的 Class 比直接用 1 行表格搜索更好?”
[Paul Graham](https://en.wikipedia.org/wiki/Paul_Graham_(programmer) —— 就是那个著名的 Y-Combinator 的创始人 —— 也一样对 OOP 不以为然,在 [Why Arc isn't Especially Object-Oriented](http://www.paulgraham.com/noop.html) 中,说他认为 OOP 之所以流行,就是因为平庸程序员(Mediocre programers)太多,大公司用这种编程范式去阻止那帮家伙,让他们捅不出太大的娄子……
然而,争议归争议,应用归应用 —— 就好像英语的弊端不见得比其他语言少,可就是最流行,那怎么办呢?用呗 —— 虽然该抱怨的时候也得抱怨抱怨。
从另外一个角度望过去,大牛们如此评价 OOP 也是很容易理解的 —— 因为他们太聪明,又因为他们太懒得花时间去理解或容忍笨蛋…… 我们不一样,最不一样的地方在于,我们不仅更多容忍他人,而且更能够容忍自己的愚笨,所以,视角就不同了,仅此而已。
并且,上面两位大牛写的编程语言,现在也挺流行,Joe Armstrong 的 Erlang 和 Rob Pike 的 Golang,弄不好早晚你也得去学学,去用用……
基本术语
面向对象编程(OOP),是使用**对象**(Objects)作为核心的编程方式。进而就可以把对象(Objects)的数据和运算过程**封装**(Encapsulate)在内部,而外部仅能根据事先设计好的**界面**(Interface)与之沟通。
比如,你可以把灯泡想象成一个对象,使用*灯泡*的人,只需要与*开关*这个界面(Interface)打交道,而不必关心灯泡内部的设计和原理 —— 说实话,这是个很伟大的设计思想。
生活中,我们会遇到无数有意无意应用了这种设计思想的产品 —— 并不仅限于编程领域。你去买个车回来,它也一样是各种封装之后的对象。当你转动方向盘(操作界面)的时候,你并不需要关心汽车设计者是如何做到把那个方向盘和车轮车轴联系在一起并如你所愿去转向的;你只需要知道的是,逆时针转动方向盘是左转,而顺时针转动方向盘是右转 —— 这就可以了!
在程序设计过程中,我们常常需要对标现实世界创造对象。这时候我们用的最直接手段就是**抽象**(Abstract)。抽象这个手段,在现实中漫画家们最常用。为什么你看到下面的图片觉得它们俩看起来像是人?尤其是在你明明知道那肯定不是人的情况下,却已然接受那是两个漫画小人的形象?
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/a-cartoon.png?raw=true)
这种描绘方式,就是抽象,很多 “没必要” 的细节都被去掉了(或者反过来说,没有被采用),留下的两个特征,一个是头,一个是双眼 —— 连那双 “眼睛” 都抽象到只剩下一个黑点了……
这种被保留下来的 “必要的特征”,叫做对象的**属性**(Attributes),进而,这些抽象的对象,既然是 “人” 的映射,它们实际上也能做一些抽象过后被保留下来的 “必要的行为”,比如,说话,哭笑,这些叫做对象的**方法**(Methods)。
从用编程语言创造对象的角度去看,所谓的界面,就由这两样东西构成:
> * *属性* —— 用自然语言描述,通常是名词(Nouns)
> * *方法* —— 用自然语言描述,通常是动词(Verbs)
从另外一个方面来看,在设计复杂对象的时候,抽象到极致是一种必要。
我们为生物分类,就是一层又一层地抽象的过程。当我们使用 “生物” 这个词的时候,它并不是某一个特定的我们能够指称的东西…… 然后我们开始给它分类……
[[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/Tree-of-Life.gif?raw=true)](https://crucialconsiderations.org/wp-content/uploads/2016/08/Tree-of-Life.jpg)
所以,当我们在程序里创建对象的时候,做法常常是
> * 先创建最抽象的**类**(Class)
> * 然后再创建**子类**(Subclass)……
它们之间是从属关系是:
> Class ⊃ Subclass
在 OOP 中,这叫**继承**(Inheritance)关系。比如,狗这个*对象*,就可以是从哺乳动物这个对象*继承*过来的。如果哺乳动物有 “头” 这个*属性*(Attributes),那么在狗这个对象中就没必要再重新定义这个属性了,因为既然狗是从哺乳动物继承过来的,那么它就拥有哺乳动物的所有属性……
每当我们创建好一个类之后,我们就可以根据它创建它的许多个**实例**(Instances)。比如,创建好了 “狗” 这个类之后,我们就可以根据这个类创建很多条狗…… 这好多条狗,就是狗这个类的*实例*。
现在能把这些术语全部关联起来了吗?
> * 对象,封装,抽象
> * 界面,属性,方法
> * 继承,类,子类,实例
这些就是关于 “面向对象编程” 方法论的最基本的术语 —— 无论在哪种编程语言里,你都会频繁地遇到它们。
**对象**、**类**,这两个词,给人的感觉是经常被通用 —— 习惯了还好,但对有些初学者来说,就感觉那是生命不能承受之重。—— 这次不是英文翻译中文时出现的问题,在英文世界里,这些词的互通使用和滥用也使相当一部分人(我怀疑是大部分人)最终掌握不了 OOP 这个方法论。
细微的差异在于 “视角” 的不同。
之前提到函数的时候,我用的说辞是,
> - 你写了一个函数,而后你要为这个产品的使用者写说明书……
> - —— 当然,产品使用者之中也包括未来的你……
类(Class)这个东西也一样,它也有创作者和使用者。
你可以这样分步理解:
> * 你创造了一个类(Class),这时候你是创作者,从你眼里望过去,那就是个类(Class);
> * 而后你根据这个类的定义,创建了很多实例(Instances);
> * 接下来一旦你开始使用这些实例的时候,你就成了使用者,从使用者角度望过去,手里正在操作的,就是各种对象(Objects)……
最后,补充一下,不要误以为所有的 Classes 都是对事物(即,名词)的映射 —— 虽然大多数情况下确实如此。
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/code-review.png?raw=true)
对基本概念有了一定的了解之后,再去看 Python 语言是如何实现的,就感觉没那么难了。
类 —— Python 的实现
既然已经在不碰代码的情况下,把 OOP 中的主要概念梳理清楚了,以下的行文中,那些概念就直接用英文罢,省得理解上还得再绕个弯……
Defining Class
Class 使用 `class` 关键字进行定义。
与函数定义不同的地方在于,Class 接收参数不是在 `class Classname():` 的括号里完成 —— 那个圆括号有另外的用处。
让我们先看看代码,而后再逐一解释:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import datetime
class Golem:
def __init__(self, name=None):
self.name = name
self.built_year = datetime.date.today().year
def say_hi(self):
print('Hi!')
g = Golem('Clay')
g.name
g.built_year
g.say_hi
g.say_hi()
type(g)
type(g.name)
type(g.built_year)
type(g.__init__)
type(g.say_hi)
```
'Clay'
2019
<bound method Golem.say_hi of <__main__.Golem object at 0x10430e7b8>>
Hi!
__main__.Golem
str
int
method
method
以上,我们创建了一个 Class:
```python
class Golem:
def __init__(self, name=None):
self.name = name
self.built_year = datetime.date.today().year
```
其中定义了当我们根据这个 Class 创建一个实例的时候,那个 Object 的初始化过程,即 `__init__()` 函数 —— 又由于这个函数是在 Class 中定义的,我们称它为 Class 的一个 Method。
这里的 `self` 就是个变量,跟程序中其它变量的区别在于,它是一个系统默认可以识别的变量,用来指代将来用这个 Class 创建的 Instance。
比如,我们创建了 Golem 这个 Class 的一个 Instance,`g = Golem('Clay')` 之后,我们写 `g.name`,那么解析器就去找 `g` 这个实例所在的 Scope 里有没有 `self.name`……
注意:`self` 这个变量的定义,是在 `def __init__(self, ...)` 这一句里完成的。对于这个变量的名称取名没有强制要求,你实际上可以随便用什么名字,很多 C 程序员会习惯于将这个变量命名为 `this` —— 但根据惯例,你最好还是只用 `self` 这个变量名,省得给别人造成误会。
在 Class 的代码中,如果定义了 `__init__()` 函数,那么系统就会将它当作 Instance 在创建后被初始化的函数。这个函数名称是强制指定的,初始化函数必须使用这个名称;注意 `init` 两端各有两个下划线 `_`。
当我们用 `g = Golem('Clay')` 这一句创建了一个 Golem 的 Instance 的时候,以下一连串的事情发生了:
> * `g` 从此之后就是一个根据 Golem 这个 Class 创建的 Instance,对使用者来说,它就是个 Object;
> * 因为 Golem 这个 Class 的代码中有 `__init__()`,所以,当 `g` 被创建的时候,`g` 就需要被初始化……
> * 在 `g` 所在的变量目录中,出现了一个叫做 `self` 的用来指代 `g` 本身的变量;
> * self.name 接收了一个参数,`'Clay'`,并将其保存了下来;
> * 生成了一个叫做 `self.built_year` 的变量,其中保存的是 `g` 这个 Object 被创建时的年份……
对了,Golem 和 Robot 一样,都是机器人的意思;Golem 的本义来自于犹太神话,一个被赋予了生命的泥人……
Inheritance
我们刚刚创建了一个 Golem Class,如果我们想用它 Inherite 一个新的 Class,比如,`Running_Golem`,一个能跑的机器人,那就像以下的代码那样做 —— 注意 `class Running_Golem` 之后的圆括号:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import datetime
class Golem:
def __init__(self, name=None):
self.name = name
self.built_year = datetime.date.today().year
def say_hi(self):
print('Hi!')
class Running_Golem(Golem): # 刚刚就说,这个圆括号另有用途……
def run(self):
print("Can't you see? I'm running...")
rg = Running_Golem('Clay')
rg.run
rg.run()
rg.name
rg.built_year
rg.say_hi()
```
<bound method Running_Golem.run of <__main__.Running_Golem object at 0x1068b37b8>>
Can't you see? I'm running...
'Clay'
2019
Hi!
如此这般,我们根据 Golem 这个 Class 创造了一个 Subclass —— `Running_Golem`,既然它是 Golem 的 Inheritance,那么 Golem 有的 Attributes 和 Methods 它都有,并且还多了一个 Method —— `self.run`。
Overrides
当我们创建一个 Inherited Class 的时候,可以重写(Overriding)Parent Class 中的 Methods。比如这样:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import datetime
class Golem:
def __init__(self, name=None):
self.name = name
self.built_year = datetime.date.today().year
def say_hi(self):
print('Hi!')
class runningGolem(Golem):
def run(self):
print("Can't you see? I'm running...")
def say_hi(self): # 不再使用 Parent Class 中的定义,而是新的……
print('Hey! Nice day, Huh?')
rg = runningGolem('Clay')
rg.run
rg.run()
rg.name
rg.built_year
rg.say_hi()
```
<bound method runningGolem.run of <__main__.runningGolem object at 0x1068c8128>>
Can't you see? I'm running...
'Clay'
2019
Hey! Nice day, Huh?
Inspecting A Class
当我们作为用户想了解一个 Class 的 Interface,即,它的 Attributes 和 Methods 的时候,常用的有三种方式:
```python
1. help(object)
2. dir(object)
3. object.__dict__
```
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import datetime
class Golem:
def __init__(self, name=None):
self.name = name
self.built_year = datetime.date.today().year
def say_hi(self):
print('Hi!')
class runningGolem(Golem):
def run(self):
print('Can\'t you see? I\'m running...')
def say_hi(self): # 不再使用 Parent Class 中的定义,而是新的……
print('Hey! Nice day, Huh?')
rg = runningGolem('Clay')
help(rg)
dir(rg)
rg.__dict__
hasattr(rg, 'built_year')
```
Help on runningGolem in module __main__ object:
class runningGolem(Golem)
| runningGolem(name=None)
|
| Method resolution order:
| runningGolem
| Golem
| builtins.object
|
| Methods defined here:
|
| run(self)
|
| say_hi(self)
|
| ----------------------------------------------------------------------
| Methods inherited from Golem:
|
| __init__(self, name=None)
| Initialize self. See help(type(self)) for accurate signature.
|
| ----------------------------------------------------------------------
| Data descriptors inherited from Golem:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
['__class__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'__weakref__',
'built_year',
'name',
'run',
'say_hi']
{'name': 'Clay', 'built_year': 2019}
True
Scope
每个变量都属于某一个 **Scope**(变量的作用域),在同一个 Scope 中,变量可以被引用被操作…… 这么说非常抽象,难以理解 —— 只能通过例子说明。
我们先给 Golem 这个 Class 增加一点功能 —— 我们需要随时知道究竟有多少个 Golem 处于活跃状态…… 也因此顺带给 Golem 加上一个 Method:`cease()` —— 哈!机器人么,想关掉它,说关掉它,就能关掉它;
另外,我们还要给机器人设置个使用年限,比如 10 年;
…… 而外部会每隔一段时间,用 `Golem.is_active()` 去检查所有的机器人,所以,不需要外部额外操作,到了年头,它应该能关掉自己。—— 当然,又由于以下代码是简化书写的,核心目的是为了讲解 Scope,所以并没有专门写模拟 10 年后某些机器人自动关闭的情形……
在运行以下代码之前,需要先介绍三个 Python 的内建函数:
> * `hasattr(object, attr)` 查询这个 `object` 中有没有这个 `attr`,返回布尔值
> * `getattr(object, attr)` 获取这个 `object` 中这个 `attr` 的值
> * `setattr(object, attr, value)` 将这个 `object` 中的 `attr` 值设置为 `value`
现在的你,应该一眼望过去,就已经能掌握这三个内建函数的用法 —— 还记得之前的你吗?眼睁睁看着,那些字母放在那里对你来说没任何意义…… 这才多久啊!
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import datetime
class Golem:
population = 0
__life_span = 10
def __init__(self, name=None):
self.name = name
self.built_year = datetime.date.today().year
self.__active = True
Golem.population += 1 # 执行一遍之后,试试把这句改成 population += 1
def say_hi(self):
print('Hi!')
def cease(self):
self.__active = False
Golem.population -= 1
def is_active(self):
if datetime.date.today().year - self.built_year >= Golem.__life_span:
self.cease()
return self.__active
g = Golem()
hasattr(Golem, 'population') # True
hasattr(g, 'population') # True
hasattr(Golem, '__life_span') # False
hasattr(g, '__life_span') # False
hasattr(g, '__active') # False
Golem.population # 1
setattr(Golem, 'population', 10)
Golem.population # 10
x = Golem()
Golem.population # 11
x.cease()
Golem.population # 10
getattr(g, 'population') # 10
g.is_active()
```
True
True
False
False
False
1
10
11
10
10
True
如果你试过把第 13 行的 `Golem.population += 1` 改成 `population += 1`,你会被如下信息提醒:
```python
12 self.__active = True
---> 13 population += 1
UnboundLocalError: local variable 'population' referenced before assignment
```
—— 本地变量 `population` 尚未赋值,就已经提前被引用…… 为什么会这样呢?因为在你所创建 `g` 之后,马上执行的是 `__init()__` 这个初始化函数,而 `population` 是在这个函数之外定义的……
如果你足够细心,你会发现这个版本中,有些变量前面有两个下划线 `__`,比如,`__life_span` 和 `self.__active`。这是 Python 的定义,变量名前面加上一个以上下划线(Underscore)`_` 的话,那么该变量是 “私有变量”(Private Variables),不能被外部引用。而按照 Python 的惯例,我们会使用两个下划线起始,去命名私有变量,如:`__life_span`。你可以回去试试,把所有的 `__life_span` 改成 `_life_span`(即,变量名开头只有一个 `_`,那么,`hasattr(Golem, '_life_span')` 和 `hasattr(g, '_life_span')` 的返回值就都变成了 `True`。
看看下面的图示,理解起来更为直观一些,其中每个方框代表一个 Scope:
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/class-variables-scope.png?raw=true)
整个代码启动之后,总计有 4 个 Scopes 如图所示:
> * ① `class Golem` 之外;
> * ② `class Golem` 之内;
> * ③ `__init__(self, name=None)` 之内;
> * ④ `cease(self)` 之内;
在 Scope ① 中,可以引用 `Golem.population`,在生成一个 Golem 的实例 `g` 之后,也可以引用 `g.population`;但 `Golem.__life_span` 和 `g.__active` 在 Scope ① 是不存在的;
在 Scope ② 中,存在两个变量,`population` 和 `__life_span`;而 `__life_span` 是 Private(私有变量,因为它的变量名中前两个字符是下划线 `__`;于是,在 Scope ① 中,不存在 `Golem.__life_span` —— `hasattr(Golem, '__life_span')` 的值为 `False`;
在 Scope ③ 中和 Scope ④ 中,由于都给它们传递了 `self` 这个参数,于是,在这两个 Scope 里,都可以引用 `self.xxx`,比如 `self.population`,比如 `self.__life_span`;
在 Scope ③ 中,`population` 是不存在的,如果需要引用这个值,可以用 `Golem.population`,也可以用 `self.population`。同样的道理,在 Scope ③ 中 `__life_span` 也不存在,如果想用这个值,可以用 `Golem.__life_span` 或者 `self.__life_span`;
Scope ④ 与 Scope ③ 平行存在。所以在这里,`population` 和 `__life_span` 也同样并不存在。
**补充**
在本例子中,在 `__init__(self, name=None)` 函数中 `self.population` 和 `Golem.population` 都可以使用,但使用效果是不一样的:
> * `self.population` 总是去读取 `Golem` 类中 `population` 的初始值,即使后面通过 `setattr(Golem, 'population', 10)` 更改 `population` 的值后,`self.population` 的值仍为 `0`,但 `Golem.population` 值则为 `10`,你可以自己动手尝试一下。
Encapsulation
到目前为止,Golem 这个 Class 看起来不错,但有个问题,它里面的数据,外面是可以随便改的 —— 虽然,我们已经通过给变量 life_span 前面加上两个下划线,变成 `__life_span`,使其成为私有变量,外部不能触达(你不能引用 `Golem.__life_span`),可 Golem.population 就不一样,外面随时可以引用,还可以随时修改它,只需要写上一句:
```python
Golem.population = 1000000
```
我们干脆把 `population` 这个变量也改成私有的罢:`__population`,而后需要从外界查看这个变量的话,就在 Class 里面写个函数,返回那个值好了:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import datetime
class Golem:
__population = 0
__life_span = 10
def __init__(self, name=None):
self.name = name
self.built_year = datetime.date.today().year
self.__active = True
Golem.__population += 1
def say_hi(self):
print('Hi!')
def cease(self):
self.__active = False
Golem.__population -= 1
def is_active(self):
if datetime.date.today().year - self.built_year >= Golem.__life_span:
self.cease
return self.__active
def population(self):
return Golem.__population
g = Golem('Clay')
g.population
g.population()
```
<bound method Golem.population of <__main__.Golem object at 0x1068da160>>
1
如果,你希望外部能够像获得 Class 的属性那样,直接写 `g.population`,而不是必须加上一个括号 `g.population()` 传递参数(实际上传递了一个隐含的 `self` 参数),那么可以在 `def population(self):` 之前的一行加上一句 `@property`:
```python
class Golem:
__population = 0
...
@property
def population(self):
return Golem.__population
```
如此这般之后,你就可以用 `g.population` 了:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import datetime
class Golem:
__population = 0
__life_span = 10
def __init__(self, name=None):
self.name = name
self.built_year = datetime.date.today().year
self.__active = True
Golem.__population += 1
def say_hi(self):
print('Hi!')
def cease(self):
self.__active = False
Golem.__population -= 1
def is_active(self):
if datetime.date.today().year - self.built_year >= Golem.__life_span:
self.cease
return self.__active
@property
def population(self):
return Golem.__population
g = Golem('Clay')
g.population
# g.population = 100
```
1
如此这般之后,不仅你可以直接引用 `g.population`,并且,在外部不能再直接给 `g.population` 赋值了,否则会报错:
```python
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-16-5d8c475304d3> in <module>
26 g = Golem('Clay')
27 g.population
---> 28 g.population = 100
AttributeError: can't set attribute
```
到此为止,Encapsulation 就做得不错了。
如果你非得希望从外部可以设置这个值,那么,你就得再写个函数,并且在函数之前加上一句:
```python
...
@property
def population(self):
return Golem.__population
@population.setter
def population(self, value):
Golem.__population = value
```
这样之后,`.population` 这个 Attribute 就可以从外部被设定其值了(虽然在当前的例子中显得没必要让外部设定 `__population` 这个值…… 以下仅仅是为了举例):
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import datetime
class Golem:
__population = 0
__life_span = 10
def __init__(self, name=None):
self.name = name
self.built_year = datetime.date.today().year
self.__active = True
Golem.__population += 1
def say_hi(self):
print('Hi!')
def cease(self):
self.__active = False
Golem.__population -= 1
def is_active(self):
if datetime.date.today().year - self.built_year >= Golem.__life_span:
self.cease
return self.__active
@property
def population(self):
return Golem.__population
@population.setter
def population(self, value):
Golem.__population = value
g = Golem('Clay')
g.population
g.population = 100
ga = Golem('New')
g.population
ga.population
help(Golem)
Golem.__dict__
g.__dict__
hasattr(Golem, 'population')
getattr(Golem, 'population')
setattr(Golem, 'population', 10000)
g.population # 所以,在很多的情况下,不把数据封装在 Class 内部的话,后面会有很多麻烦。
```
1
101
101
Help on class Golem in module __main__:
class Golem(builtins.object)
| Golem(name=None)
|
| Methods defined here:
|
| __init__(self, name=None)
| Initialize self. See help(type(self)) for accurate signature.
|
| cease(self)
|
| is_active(self)
|
| say_hi(self)
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
|
| population
mappingproxy({'__module__': '__main__',
'_Golem__population': 101,
'_Golem__life_span': 10,
'__init__': <function __main__.Golem.__init__(self, name=None)>,
'say_hi': <function __main__.Golem.say_hi(self)>,
'cease': <function __main__.Golem.cease(self)>,
'is_active': <function __main__.Golem.is_active(self)>,
'population': <property at 0x1068f9d68>,
'__dict__': <attribute '__dict__' of 'Golem' objects>,
'__weakref__': <attribute '__weakref__' of 'Golem' objects>,
'__doc__': None})
{'name': 'Clay', 'built_year': 2019, '_Golem__active': True}
True
<property at 0x1068f9d68>
10000
函数工具
这一章要讲的是迭代器、生成器和装饰器,这些都是函数工具。有人把它们称为 **DIG**(Decorator,Iterator,Generator)—— 它们都是真正掌握 Python 的关键。
迭代器(Iterator)
我们已经见过 Python 中的所有容器,都是可迭代的 —— 准确地讲,是可以通过迭代遍历每一个元素:
```python
string = "this is a string."
list = ['item 1', 'item 2', 3, 5]
set = (1, 2, 3, 4, 5)
for c in string:
print(c, end=', ')
print()
for L in list:
print(L, end=', ')
print()
for s in set:
print(s, end=', ')
print()
```
t, h, i, s, , i, s, , a, , s, t, r, i, n, g, .,
item 1, item 2, 3, 5,
1, 2, 3, 4, 5,
有个内建函数,就是用来把一个 “可迭代对象”(Iterable)转换成 “迭代器”(Iterator)的 —— `iter()`。
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
i = iter("Python")
type(i)
s = iter((1, 2, 3, 4, 5))
type(s)
L = iter(['item 1', 'item 2', 3, 5])
type(L)
```
str_iterator
tuple_iterator
list_iterator
迭代器如何使用呢?有个 `next()` 函数:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
i = iter("Python")
next(i)
next(i)
next(i)
next(i)
next(i)
next(i)
# next(i) 前面已经到 'n' 了,再调用就会有 StopIteration 错误提示。
```
'P'
'y'
't'
'h'
'o'
'n'
在 `i` 这个迭代器里一共有 6 个元素,所以,`next(i)` 在被调用 6 次之后,就不能再被调用了,一旦再被调用,就会触发 StopIteration 错误。
那我们怎么自己写一个迭代器呢?
迭代器是个 Object,所以,写迭代器的时候写的是 Class,比如,我们写一个数数的迭代器,Counter:
```python
class Counter(object):
def __init__(self, start, stop):
self.current = start
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.current > self.stop:
raise StopIteration
else:
c = self.current
self.current += 1
return c
c = Counter(11, 20)
next(c)
next(c)
next(c)
for c in Counter(101, 105):
print(c, end=', ')
type(Counter)
```
11
12
13
101
102
103
104
105
type
这里的重点在于两个函数的存在,`__iter__(self)` 和 `__next__(self)`。
```python
def __iter__(self):
return self
```
这两句是约定俗成的写法,写上它们,`Counter` 这个类就被会被识别为 Iterator 类型。而后再有 `__next__(self)` 的话,它就是个完整的迭代器了。除了可以用 `for loop` 之外,也可以用 `while loop` 去遍历迭代器中的所有元素:
```python
class Counter(object):
def __init__(self, start, stop):
self.current = start
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.current > self.stop:
raise StopIteration
else:
c = self.current
self.current += 1
return c
for c in Counter(101, 103):
print(c, sep=', ')
c = Counter(201, 203)
while True:
try:
print(next(c), sep=', ')
except StopIteration:
break
```
101
102
103
201
202
203
生成器(Generator)
那用函数(而不是 Class)能不能写一个 Counter 呢?答案是能,用生成器(Generator)就行。
```python
def counter(start, stop):
while start <= stop:
yield start
start += 1
for i in counter(101, 105):
print(i)
```
101
102
103
104
105
哎呀!怎么感觉这个简洁很多呢?
不过,是否简洁并不是问题,这次看起来用生成器更简单,无非是因为当前的例子更适合用生成器而已。在不同的情况下,用迭代器和用生成器各有各的优势。
这里的关键在于 `yield` 这个语句。它和 `return` 最明显的不同在于,在它之后的语句依然会被执行 —— 而 `return` 之后的语句就被忽略了。
但正因为这个不同,在写生成器的时候,只能用 `yield`,而没办法使用 `return` —— 你现在可以回去把上面代码中的 `yield` 改成 `return` 看看,然后体会一下它们之间的不同。
生成器函数被 `next()` 调用后,执行到 `yield` 生成一个值返回(然后继续执行 `next()` 外部剩余的语句);下次再被 `next()` 调用的时候,从上次生成返回值的 `yield` 语句处继续执行…… 如果感觉费解,就多读几遍 —— 而后再想想若是生成器中有多个 `yield` 语句会是什么情况?
还有一种东西,叫做生成器表达式。先看个例子:
```python
even = (e for e in range(10) if not e % 2)
# odd = (o for o in range(10) if o % 2)
print(even)
for e in even:
print(e)
```
<generator object <genexpr> at 0x107cc0048>
0
2
4
6
8
其实,这种表达式我们早就在 List Comprehension 里见过 —— 那就是通过生成器表达式完成的。
**注意**
仔细看 `even = (e for e in range(10) if not e % 2)` 中最外面那层括号,用了圆括号,`even` 就是用生成器创造的迭代器(Iterator),若是用了方括号,那就是用生成器创造的列表(List)—— 当然用花括号 `{}` 生成的就是集合(Set)……
```python
# even = (e for e in range(10) if not e % 2)
odd = [o for o in range(10) if o % 2]
print(odd)
for o in odd:
print(o)
```
[1, 3, 5, 7, 9]
1
3
5
7
9
```python
# even = (e for e in range(10) if not e % 2)
odd = {o for o in range(10) if o % 2}
print(odd)
for o in odd:
print(o)
```
{1, 3, 5, 7, 9}
1
3
5
7
9
**生成器表达式必须在括号内使用**(参见官方 [HOWTOS](https://docs.python.org/3/howto/functional.html#generator-expressions-and-list-comprehensions)),包括函数的参数括号,比如:
```python
sum_of_even = sum(e for e in range(10) if not e % 2)
print(sum_of_even)
```
20
函数内部当然可以包含其它的函数,以下就是一个函数中包含着其它函数的结构示例:
```python
def a_func():
def b_func():
pass
def c_func():
pass
def d_func():
pass
b_func()
return True
```
想象一下,如果,我们让一个函数返回的是另外一个函数呢?我们一步一步来:
```python
def a_func():
def b_func():
print("Hi, I'm b_func!")
print("Hi, I'm a_func!")
a_func()
```
Hi, I'm a_func!
```python
def a_func():
def b_func():
print("Hi, I'm b_func!")
print("Hi, I'm a_func!")
b_func()
a_func()
```
Hi, I'm a_func!
Hi, I'm b_func!
上一个代码,我们可以写成这样 —— 让 `a_func()` 将它内部的 `b_func()` 作为它的返回值:
```python
def a_func():
def b_func():
print("Hi, I'm b_func!")
print("Hi, I'm a_func!")
return b_func()
a_func()
```
Hi, I'm a_func!
Hi, I'm b_func!
如果我们在 `return` 语句里只写函数名呢?好像这样:
```python
def a_func():
def b_func():
print("Hi, I'm b_func!")
print("Hi, I'm a_func!")
return b_func
a_func()
```
Hi, I'm a_func!
<function __main__.a_func.<locals>.b_func()>
这次返回的不是调用 `b_func()` 这个函数的执行结果,返回的是 `b_func` 这个*函数本身*。
装饰器(Decorator)
函数也是对象
这是关键:
> 函数本身也是对象(即,Python 定义的某个 Class 的一个 Instance)。
于是,函数本身其实可以与其它的数据类型一样,作为其它函数的参数或者返回值。
让我们分步走 —— 注意,在以下代码中,`a_decorator` 返回的一个函数的调用 `wrapper()` 而不是 `wrapper` 这个函数本身:
```python
def a_decorator(func):
def wrapper():
print('We can do sth. before a func is called...')
func()
print('... and we can do sth. after it is called...')
return wrapper()
def a_func():
print("Hi, I'm a_func!")
a_func()
a_decorator(a_func)
```
Hi, I'm a_func!
We can do sth. before a func is called...
Hi, I'm a_func!
... and we can do sth. after it is called...
如果返回的是函数本身,`wrapper`,输出结果跟你想的并不一样:
```python
def a_decorator(func):
def wrapper():
print('We can do sth. before a func is called...')
func()
print('... and we can do sth. after it is called...')
return wrapper #
def a_func():
print("Hi, I'm a_func!")
a_func()
a_decorator(a_func)
```
Hi, I'm a_func!
<function __main__.a_decorator.<locals>.wrapper()>
装饰器操作符
不过,Python 提供了一个针对函数的操作符 `@`,它的作用是…… 很难一下子说清楚,先看看以下代码:
```python
def a_decorator(func):
def wrapper():
print('We can do sth. before calling a_func...')
func()
print('... and we can do sth. after it was called...')
return wrapper
@a_decorator
def a_func():
print("Hi, I'm a_func!")
a_func()
```
We can do sth. before calling a_func...
Hi, I'm a_func!
... and we can do sth. after it was called...
注意:以上的代码中,`a_decorator(func)` 返回的是 `wrapper` 这个函数本身。
在我们定义 `a_func()` 的时候,在它之前,加上了一句 `@a_decorator`;这么做的结果是:
> 每次 `a_func()` 在被调用的时候,因为它之前有一句 `@a_decorator`,所以它会先被当作参数传递到 `a_decorator(func)` 这个函数中…… 而后,真正的执行,是在 `a_decorator()` 里被完成的。
—— 被 `@` 调用的函数,叫做 “装饰器”(Decorator),比如,以上代码中的 `a_decorator(func)`。
现在可以很简单直接地说清楚装饰器的作用了:
```python
@a_decorator
def a_func():
...
```
等价于
```python
def a_func():
...
a_func = a_decorator(a_func)
```
就是用 `a_decorator` 的调用结果替换掉原来的函数。`a_decorator` 返回值是什么,以后调用 `a_func` 时就是在调用这个返回值,而 `a_decorator` 本身此时已经执行完毕了。
装饰器的用途
Decorator 最常用的场景是什么呢?最常用的场景就是用来改变其它函数的行为。
```python
def an_output():
return 'The quick brown fox jumps over the lazy dog.'
print(an_output())
```
The quick brown fox jumps over the lazy dog.
```python
def uppercase(func):
def wrapper():
original_result = func()
modified_restult = original_result.upper()
return modified_restult
return wrapper
@uppercase
def an_output():
return 'The quick brown fox jumps over the lazy dog.'
print(an_output())
```
THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.
你还可以给一个函数加上一个以上的装饰器:
```python
def uppercase(func):
def wrapper():
original_result = func()
modified_restult = original_result.upper()
return modified_restult
return wrapper
def strong(func):
def wrapper():
original_result = func()
modified_restult = '<strong>'+original_result+'</strong>'
return modified_restult
return wrapper
@strong
@uppercase
def an_output():
return 'The quick brown fox jumps over the lazy dog.'
print(an_output())
```
<strong>THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG.</strong>
你把两个装饰器的顺序调换一下写成下面这样试试:
```python
@uppercase
@strong
def an_output():
...
```
装饰器的执行顺序是 “自下而上” —— 其实是 “由里到外” 更为准确。体会一下。
装饰带有参数的函数
到现在我们见到的使用装饰器的函数都是没有参数的:`an_output` 以及之前的 `a_func`。
如果被装饰的函数有参数怎么办?装饰器自身内部又应该怎么写?
这时候,Python 的 `*args` and `**kwargs` 的威力就显现出来了 —— 之前怕麻烦没有通过仔细反复阅读搞定这 “一个星号、两个星号、直接晕倒” 的知识点的人,现在恐怕要吃亏了……
装饰器函数本身这么写:
```python
def a_decorator(func):
def wrapper(*args, **kwargs):
return original_result
# ...
return wrapper
```
在这里,`(*args, **kwargs)` 非常强大,它可以匹配所有函数传进来的所有参数…… 准确地讲,`*args` 接收并处理所有传递进来的位置参数,`**kwargs` 接收并处理所有传递进来的关键字参数。
假设我们有这么个函数:
```python
def say_hi(greeting, name=None):
return greeting + '! ' + name + '.'
print(say_hi('Hello', 'Jack'))
```
Hello! Jack.
如果我们想在装饰器里对函数名、参数,都做些事情 —— 比如,我们写个 `@trace` 用来告诉用户调用一个函数的时候都发生了什么……
```python
def trace(func):
def wrapper(*args, **kwargs):
print(f"Trace: You've called a function: {func.__name__}(),",
f"with args: {args}; kwargs: {kwargs}")
original_result = func(*args, **kwargs)
print(f"Trace: {func.__name__}{args} returned: {original_result}")
return original_result
return wrapper
@trace
def say_hi(greeting, name=None):
return greeting + '! ' + name + '.'
print(say_hi('Hello', name = 'Jack'))
```
Trace: You've called a function: say_hi(), with args: ('Hello',); kwargs: {'name': 'Jack'}
Trace: say_hi('Hello',) returned: Hello! Jack.
Hello! Jack.
有了以上的基础知识之后,再去阅读 Python Decorator Library 的 Wiki 页面就会轻松许多:
> https://wiki.python.org/moin/PythonDecoratorLibrary
学会装饰器究竟有多重要?
装饰器一定要学会 —— 因为很多人就是不会。
Oreilly.com 上有篇文章,《5 reasons you need to learn to write Python decorators》中,其中的第五条竟然是:**Boosting your career**!
> Writing decorators isn't easy at first. It's not rocket science, but takes enough effort to learn, and to grok the nuances involved, that many developers will never go to the trouble to master it. And that works to your advantage. When you become the person on your team who learns to write decorators well, and write decorators that solve real problems, other developers will use them. Because once the hard work of writing them is done, decorators are so easy to use. This can massively magnify the positive impact of the code you write. And it just might make you a hero, too.
>
> As I've traveled far and wide, training hundreds of working software engineers to use Python more effectively, teams have consistently reported writing decorators to be one of the most valuable and important tools they've learned in my advanced Python programming workshops.
为什么有那么多人就是学不会呢?—— 只不过是因为在此之前,遇到 `*args` `**kwargs` 的时候,“一个星号、两个星号、直接晕倒”…… 而后并未再多挣扎一下。
正则表达式
正则表达式本质上是个独立的语言,短小却格外强悍 —— 乃至于,如果你竟然没学会它的话,你的之前学的编程技能干脆与残疾无异。
Wikipedia 上对正则表达式的说明如下:
> **正则表达式**(英语:Regular Expression,在代码中常简写为 regex、regexp 或 RE),又称*正规表示式*、*正规表示法*、*正规运算式*、*规则运算式*、*常规表示法*,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些符合某个模式的文本。许多程序设计语言都支持利用正则表达式进行字符串操作。例如,在 Perl 中就内建了一个功能强大的正则表达式引擎。正则表达式这个概念最初是由 Unix 中的工具软件(例如 sed 和 grep)普及开的。
以下是绝大多数翻译成中文的教程中对正则表达式进行讲解时所使用的描述:
> 一个正则表达式(Regular Expression)通常被称为一个模式(Pattern)。
我常常觉得当初要是它被翻译成 “规则表达式”,那么很可能初学者不会感到那么大的压力 —— 谁都一样,看着由 “每个都认识的字构成的词组” 却不能直观地想到它究竟是什么东西,都会感到莫名的压力。
**Regular**,其实在它的众多语义中,取以下释义最符合 Regular Expression 的原意<a href='#fn1' name='fn1b'><sup>[1]</sup></a>:
> ⑭ Linguistics 规则的 ▸ regular verbs 规则动词
而 **Pattern** 这个词,在词典里有好几个对应的中文词汇:
> ① 图案;② 式样;③ 图样;④ 榜样;⑤ 模式;⑥ 样品;⑦ 模子
在当前语境之下,把 Pattern 翻译成 “模式”,显然不如 “模子” 更好(甚至连 “样品” 感觉都比 “模式” 更恰当)—— “模子” 这个词很直观啊,拿着一个模子去找与它一致的字符串…… “与规则一致”,英文用的是 **Match**,一般被翻译作 “匹配”。
在自学编程的过程中,处处都是这种语言翻译带来的迷惑、障碍,或者耽误。既然应该把 Regular Expression 理解为 “规则表达式” 更好,那其实吧,把 Pattern 直接理解为中文的 “*规则*”,可能更直观更准确,理解上更是毫无障碍:
> 一个规则表达式(Regular Expression)通常被称为一个规则(Pattern)。
那么,**规则表达式**里写的是什么呢?只能是**规则**了…… 到最后好像也就 “捕获”(Capture)这个词没什么歧义。
现在,我们已经把术语全部 “解密” 了,然后再看看下面的表述:
> 我们可以用书写特定的规则,用来在文本中捕获与规则一致的字符串,而后对其进行操作……
理解起来相当顺畅。
以下的 Python 代码中,[`\wo\w`](https://regexper.com#%5Cwo%5Cw) 就是一个*规则表达式*(或称为*规则*);
而 `re.findall(pttn, str)` 的作用就是,在 `str` 里找到所有与这个**规则**(Pattern,模式)**一致**(Match,匹配)的字符串:
```python
import re
str = 'The quick brown fox jumps over the lazy dog'
pttn = re.compile(r'\wo\w')
re.findall(pttn, str)
```
['row', 'fox', 'dog']
总结一下:
> **规则表达式**(Regular Expressions,通常缩写为 Regex)是最强大且不可或缺的文本处理工具 —— 它的用处就是在文本中**扫描/搜索**(Scan/Search)与某一**规则**(Pattern)**匹配**(Match,即,与规则一致)的所有实例,并且还可以按照规则**捕获**(Capture)其中的部分或者全部,对它们进行**替换**(Replace)。
接下来为了避免歧义,我们干脆用 Regex 这个缩写,以及与它相关的英文单词:pattern, match, capture, replace(ment)……
有时,使用 Regex 并不是为了 Replace,而是为了检查格式,比如,可以用 Regex 检查用户输入的密码是否过于简单(比如,全部都由数字构成),比如可以用来验证用户输入的电话号码、证件号码是否符合特定格式等等。
另外,在自学的过程中,想尽一切办法把一切术语用简单直白的 “人话” 重新表述,是特别有效的促进进步的行为模式。
视觉体验
所谓百闻不如一见。
眼见为实 —— 想办法让一个陌生的概念视觉上直观,是突破大多学习障碍的最简单粗暴直接有效的方式。
我们最好先直接看看 Regex 的工作过程。以下,是用微软发行的代码编辑工具 Visual Studio Code 针对一小段文本使用若干条 Regex 进行匹配的过程:
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/regex-test.gif?raw=true)
Python 的项目代码仓库里有一个很简短的 Demo 程序,叫 [`redemo.py`](https://github.com/python/cpython/blob/3.6/Tools/demo/redemo.py),它使用 [Tcl/Tk](https://docs.python.org/3/library/tkinter.html) 作为图形界面,也可以用来测试正则表达式。
它的代码地址是:
> https://raw.githubusercontent.com/python/cpython/3.6/Tools/demo/redemo.py
它运行起来长成这样:
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/redemo.png?raw=true)
目前(2019)网上最方便的 Regex 测试器,是 [regex101.com](https://regex101.com):
以下,就是在一段文本中,找出所有首写字母大写的词汇的*过程*,并将其先全部替换成小写,再将其全部替换为大写的过程;使用的正则表达式是 `([A-Z]\w+)`,替换表达式分别是 `\L$1` 和 `\U$1`:
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/regex101.gif?raw=true)
这个网站太好了,所以,平日里我是用 [Nativefier](https://github.com/jiahaog/nativefier) 工具将这个网站打包为一个 Mac Desktop App 使用。不过,它也有局限,就是被搜索文件略微大点就报错,说 `timeout`……
准备工作
我们需要个文本文件,用来当作练习使用正则表达式去搜索替换的目标。这个文件保存在当前的根目录,文件名称是:`regex-target-text-sample.txt`。
以下代码中,`pttn = r'beg[iau]ns?'` 这一句中的 [`beg[iau]ns?`](https://regexper.com#beg[iau]ns?) 就是 Regex 的 Pattern。
**注意**:在 Python 代码中,写 Pattern 的时候,之所以要在字符串 `'...'` 之前加上 `r`,写成 `r'...'`,是因为如果不用 raw string 的话,那么,每个转义符号都要写成 `\\`;如果用 raw string,转义符号就可以直接使用 `\` 本身了…… 当然,如果你想搜索 `\` 这个符号本身的话,那么还是得写 `\\`。
而 `re.findall(pttn, str)` 的意思是说,把 `str` 中所有与 `pttn` 这个规则一致的字符串都找出来:
```python
import re
with open('regex-target-text-sample.txt', 'r') as f:
str = f.read()
pttn = r'beg[iau]ns?'
re.findall(pttn, str)
```
['begin', 'began', 'begun', 'begin']
文件 `regex-target-text-sample.txt` 中的内容如下:
```html
<ol>
<li><pre>begin began begun bigins begining</pre></li>
<li><pre>google gooogle goooogle goooooogle</pre></li>
<li><pre>coloured color coloring colouring colored</pre></li>
<li><pre>never ever verb however everest</pre></li>
<li><pre>520 52000 5200000 520000000 520000000000</pre></li>
<li><pre>error wonderer achroiocythaemia achroiocythemia</pre></li>
<li><pre>The white dog wears a black hat.</pre></li>
<li><pre>Handel, Händel, Haendel</pre></li>
</ol>
<dl>(843) 542-4256</dl> <dl>(431) 270-9664</dl>
<dl>3336741162</dl> <dl>3454953965</dl>
<ul>
<li>[email protected]</li> <li>[email protected]</li>
<li>[email protected]</li> <li>[email protected]</li>
</ul>
<h3>URLs</h3>
https://docs.python.org/3/howto/regex.html
https://docs.python.org/3/library/re.html
<h3>passwords</h3>
Pasw0rd~
i*Eh,GF67E
a$4Bh9XE&E
<h3>duplicate words</h3>
<p>It's very very big.</p>
<p>Keep it simple, simple, simple!</p>
```
在以下的示例中,有时直接设定了 str 的值,而不是使用以上整个文本文件 —— 因为读者在阅读的时候,最好能直接看到被搜索的字符串。另外,如果使用整个文件,所得到的 Match 太多,也确实影响阅读。
优先级
毕竟,你已经不是 “啥都不懂” 的人了。你已经知道一个事实:编程语言无非是用来运算的。
所谓的运算,就有操作符(Operators)和操作元(Operands)—— 而操作符肯定是有优先级的,不然的话,那么多操作元和操作符放在一起,究竟先操作哪个呢?
Regex 也一样,它本身就是个迷你语言(Mini Language)。在 Regex 中,操作符肯定也有优先级。它的操作元有个专门的名称,**原子**(Atom)。
先大致看看它的操作符优先级,你就会对它有相当不错的了解:
| 排列 | 原子与操作符优先级 |(从高到低)|
|---|-----------------------------------|------------------------|
| 1 | 转义符号 (Escaping Symbol) | `\` |
| 2 | 分组、捕获 (Grouping or Capturing) | `(...)` `(?:...)` `(?=...)` `(?!...)` `(?<=...)` `(?<!...)` |
| 3 | 数量 (Quantifiers) | `a*` `a+` `a?` `a{n, m}` |
| 4 | 序列与定位(Sequence and Anchor)| `abc` `^` `$` `\b` `\B` |
| 5 | 或(Alternation)| `a\|b\|c` |
| 6 | 原子 (Atoms) | `a` `[^abc]` `\t` `\r` `\n` `\d` `\D` `\s` `\S` `\w` `\W` `.` |
当然,你若是在之前,没有自学过、理解过 Python(或者任何其它编程语言)表达式中的操作符优先级,那么一上来就看上面的表格不仅对你没有帮助,只能让你更迷惑。
—— 这就是理解能力逐步积累逐步加强的过程。
原子
在 Regex 的 Pattern 中,操作元,即,被运算的 “值”,被称为**原子**(Atom)。
本义字符
最基本的原子,就是本义字符,它们都是单个字符。
本义字符包括从 `a` 到 `z`,`A` 到 `Z`,`0` 到 `9`,还有 `_` —— 它们所代表的就是它们的字面值。
即,相当于,`string.ascii_letters` 和 `string.digits` 以及 `_`。
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import string
string.ascii_letters
string.digits
```
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
'0123456789'
以下字符在 Regex 中都有特殊含义:
> `\` `+` `*` `.` `?` `-` `^` `$` `|` `(` `)` `[` `]` `{` `}` `<` `>`
当你在写 Regex 的时候,如果你需要搜索的字符不是本义字符,而是以上这些特殊字符时,*建议*都直接加上转义符号 `\` 来表示,比如,你想搜索 `'`,那你就写 `\'`,或者你想搜索 `#` 那你就写 `\#`(事实上,`#` 并不是 Regex 的特殊符号,所以,它之前的转义符号可有可无)—— 这对初学者来说可能是最安全的策略。
跟过往一样,所有的细节都很重要,它们就是需要花时间逐步熟悉到牢记。
集合原子
集合原子还是原子。
标示集合原子,使用方括号 `[]`。`[abc]` 的意思是说,“`a` or `b` or `c`”,即,`abc` 中的任意一个字符。
比如,[`beg[iau]n`](https://regexper.com#beg[iau]n) 能够代表 `begin`、`began`,以及 `begun`。
```python
import re
str = 'begin began begun bigins begining'
pttn = r'beg[iau]n'
re.findall(pttn, str)
```
['begin', 'began', 'begun', 'begin']
在方括号中,我们可以使用两个操作符:`-`(区间)和 `^`(非)。
* `[a-z]` 表示从小写字母 `a` 到小写字母 `z` 中的任意一个字符。
* `[^abc]` 表示 `abc` 以外的其它任意字符,即,非 `[abc]`。
注意,一个集合原子中,`^` 符号只能用一次,只能紧跟在 `[` 之后。否则不起作用。
类别原子
类别原子,是指那些能够代表 “一类字符” 的原子,它们都得使用转义符号再加上另外一个符号表达,包括:
`\d` 任意数字;等价于 `[0-9]`
`\D` 任意非数字;等价于 `[^0-9]`
`\w` 任意本义字符;等价于 `[a-zA-Z0-9_]`
`\W` 任意非本义字符;等价于 `[^a-zA-Z0-9_]`
`\s` 任意空白;相当于 `[ \f\n\r\t\v]`(注意,方括号内第一个字符是空格符号)
`\S` 任意非空白;相当于 `[^ \f\n\r\t\v]`(注意,紧随 `^` 之后的是一个空格符号)
`.` 除 `\r` `\n` 之外的任意字符;相当于 `[^\r\n]`
类别原子挺好记忆的,如果你知道各个字母是哪个词的首字母的话:
> * `d` 是 digits
> * `w` 是 word characters
> * `s` 是 spaces
另外,在空白的集合 `[ \f\n\r\t\v]` 中:`\f` 是分页符;`\n` `\r` 是换行符;`\t` 是制表符;`\v` 是纵向制表符(很少用到)。各种关于空白的转义符也同样挺好记忆的,如果你知道各个字母是那个词的首字母的话:
> * `f` 是 flip
> * `n` 是 new line
> * `r` 是 return
> * `t` 是 tab
> * `v` 是 vertical tab
```python
import re
str = '<dl>(843) 542-4256</dl> <dl>(431) 270-9664</dl>'
pttn = r'\d\d\d\-'
re.findall(pttn, str)
```
['542-', '270-']
边界原子
我们可以用边界原子指定边界。也可以称作 “定位操作符”。
`^` 匹配被搜索字符串的开始位置;
`$` 匹配被搜索字符串的结束位置;
`\b` 匹配单词的边界;[`er\b`](https://regexper.com#er%5Cb),能匹配 `coder` 中的 `er`,却不能匹配 `error` 中的 `er`;
`\B` 匹配非单词边界;[`er\B`](https://regexper.com#er%5CB),能匹配 `error` 中的 `er`,却不能匹配 `coder` 中的 `er`。
```python
import re
str = 'never ever verb however everest'
pttn = r'er\b'
re.findall(pttn, str)
pttn = r'er\B'
re.findall(pttn, str)
```
['er', 'er', 'er']
['er', 'er']
**注意**:`^` 和 `$` 在 Python 语言中被 `\A` 和 `\Z` 替代。
事实上,每种语言或多或少都对 Regex 有自己的定制。不过,本章讨论的绝大多数细节,都是通用的。
组合原子
我们可以用圆括号 `()` 将多个单字符原子组合成一个原子 —— 这么做的结果是,`()` 内的字符串将被当作一整个原子,可以被随后我们要讲解的数量操作符操作。
另外,`()` 这个操作符,有两个作用:**组合**(Grouping),就是我们刚刚讲到的作用;而另外一个作用是**捕获**(Capturing),后面会讲到。
注意区别,[`er`](https://regexper.com#er)、[`[er]`](https://regexper.com#[er]) 和 [`(er)`](https://regexper.com#(er) 各不相同。
> * `er` 是两个原子,`'e'` 和紧随其后的 `'r'`
> * `[er]` 是一个原子,或者 `'e'` 或者 `'r'`;
> * `(er)` 是一个原子,`'er'`
下一节中讲到数量操作符的时候,会再次强调这点。
数量操作符
数量操作符有:`+` `?` `*` `{n, m}`。
它们是用来限定位于它们之前的原子允许出现的个数;不加数量限定则代表出现一次且仅出现一次:
`+` 代表前面的原子必须至少出现一次,即:` 出现次数 ≧ 1`
> 例如,[`go+gle`](https://regexper.com#go+gle)可以匹配 `google` `gooogle` `goooogle` 等;
`?` 代表前面的原子最多只可以出现一次,即:`0 ≦ 出现次数 ≦ 1`
> 例如,[`colou?red`](https://regexper.com#colou?red)可以匹配 `colored` 或者 `coloured`;
`*` 代表前面的原子可以不出现,也可以出现一次或者多次,即:` 出现次数 ≧ 0`
> 例如,[`520*`](https://regexper.com#520*)可以匹配 `52` `520` `52000` `5200000` `520000000000` 等。
`{n}` 之前的原子出现确定的 `n` 次;
`{n,}` 之前的原子出现至少 `n` 次;
`{n, m}` 之前的原子出现至少 `n` 次,至多 `m` 次
> 例如,[`go{2,5}gle`](https://regexper.com#go%7B2,5%7Dgle),能匹配 `google` `gooogle` `goooogle` 或 `gooooogle`,但不能匹配 `gogle` 和 `gooooooogle`
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import re
with open('regex-target-text-sample.txt', 'r') as f:
str = f.read()
pttn = r'go+gle'
re.findall(pttn, str)
pttn = r'go{2,5}gle'
re.findall(pttn, str)
pttn = r'colou?red'
re.findall(pttn, str)
pttn = r'520*'
re.findall(pttn, str)
```
['google', 'gooogle', 'goooogle', 'goooooogle']
['google', 'gooogle', 'goooogle']
['coloured', 'colored']
['520', '52000', '5200000', '520000000', '520000000000']
数量操作符是对它之前的原子进行操作的,换言之,数量操作符的操作元是操作符之前的原子。
上一节提到,要注意区别:`er`、`[er]` 和 `(er)` 各不相同。
> * `er` 是两个原子,`'e'` 之后 `'r'`
> * `[er]` 是一个原子,或者 `'e'` 或者 `'r'`;
> * `(er)` 是一个原子,`'er'`
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import re
str = 'error wonderer severeness'
pttn = r'er'
re.findall(pttn, str)
pttn = r'[er]'
re.findall(pttn, str)
pttn = r'(er)'
re.findall(pttn, str)
```
['er', 'er', 'er', 'er']
['e', 'r', 'r', 'r', 'e', 'r', 'e', 'r', 'e', 'e', 'r', 'e', 'e']
['er', 'er', 'er', 'er']
在以上的例子中,看不出 `er` 和 `(er)` 的区别,但是,加上数量操作符就不一样了 —— 因为*数量操作符只对它之前的那一个原子进行操作*:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import re
str = 'error wonderer severeness'
pttn = r'er+'
re.findall(pttn, str)
pttn = r'[er]+'
re.findall(pttn, str)
pttn = r'(er)+'
re.findall(pttn, str)
```
['err', 'er', 'er', 'er']
['err', 'r', 'erer', 'e', 'ere', 'e']
['er', 'er', 'er']
或操作符 `|`
或操作符 `|` 是所有操作符中优先级最低的,数量操作符的优先级比它高,所以,在 `|` 前后的原子被数量操作符(如果有的话)操作之后才交给 `|` 操作。
于是,[`begin|began|begun`](https://regexper.com#begin%7Cbegan%7Cbegun) 能够匹配 `begin` 或 `began` 或 `begun`。
```python
import re
str = 'begin began begun begins beginn'
pttn = r'begin|began|begun'
re.findall(pttn, str)
```
['begin', 'began', 'begun', 'begin', 'begin']
在集合原子中(即,`[]` 内的原子)各个原子之间的关系,只有 “或” —— 相当于方括号中的每个原子之间都有一个被省略的 `|`。
**注意**:方括号的 `|` 不被当作特殊符号,而是被当作 `|` 这个符号本身。在方括号中的圆括号,也被当作圆括号 `()` 本身,而无分组含义。
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
import re
str = 'achroiocythaemia achroiocythemia a|e'
pttn = r'[a|ae]'
re.findall(pttn, str)
pttn = r'[a|e]'
re.findall(pttn, str)
pttn = r'[ae]'
re.findall(pttn, str)
pttn = r'[(ae)]'
re.findall(pttn, str)
pttn = r'[a|ae|(ae)]'
re.findall(pttn, str)
```
['a', 'a', 'e', 'a', 'a', 'e', 'a', 'a', '|', 'e']
匹配并捕获
捕获(Capture),使用的是圆括号 `()`。使用圆括号得到的匹配的值被暂存成一个带有索引的列表,第一个是 `$1`,第二个是 `$2`…… 以此类推。随后,我们可以在替换的过程中使用 `$1` `$2` 中所保存的值。
**注意**:在 Python 语言中调用 `re` 模块之后,在 `re.sub()` 中调用被匹配的值,用的索引方法是 `\1`、`\2`…… 以此类推。
```python
import re
str = 'The white dog wears a black hat.'
pttn = r'The (white|black) dog wears a (white|black) hat.'
re.findall(pttn, str)
repl = r'The \2 dog wears a \1 hat.'
re.sub(pttn, repl, str)
repl = r'The \1 dog wears a \1 hat.'
re.sub(pttn, repl, str)
```
[('white', 'black')]
'The black dog wears a white hat.'
'The white dog wears a white hat.'
非捕获匹配
有时,你并不想捕获圆括号中的内容,在那个地方你使用括号的目的只是分组,而非捕获,那么,你就在圆括号内最开头加上 `?:` —— `(?:...)`:
```python
import re
str = 'The white dog wears a black hat.'
pttn = r'The (?:white|black) dog wears a (white|black) hat.'
re.findall(pttn, str) # 只捕获了一处,也就是说只有一个值将来可以被引用
repl = r'The \1 dog wears a \1 hat.' # 之前的一处捕获,在替换时可被多次引用
書評
re.sub(pttn, repl, str)
```
['black']
'The black dog wears a black hat.'
在 Python 代码中使用正则表达式,匹配和捕获以及随后的替换,有更灵活的方式,因为可以对那些值直接编程。`re.sub()` 中,`repl` 参数甚至可以接收另外一个函数作为参数 —— 以后你肯定会自行认真阅读以下页面中的所有内容:
> https://docs.python.org/3/library/re.html
非捕获匹配,还有若干个操作符:
`(?=pattern)`
> 正向肯定预查(look ahead positive assert),在任何匹配规则的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,[`Windows(?=95|98|NT|2000)`](https://regexper.com#%60Windows(?=95%7C98%7CNT%7C2000)%60)
能匹配 `Windows2000` 中的 `Windows`,但不能匹配 `Windows3.1` 中的 `Windows`。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
`(?!pattern)`
> 正向否定预查(negative assert),在任何不匹配规则的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如[`Windows(?!95|98|NT|2000)`](https://regexper.com#Windows(?=95%7C98%7CNT%7C2000))
能匹配 `Windows3.1` 中的 `Windows`,但不能匹配 `Windows2000` 中的 `Windows`。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
`(?<=pattern)`
>反向(look behind)肯定预查,与正向肯定预查类似,只是方向相反。例如,[`(?<=95|98|NT|2000)Windows`](https://regexper.com#(?%3C=95%7C98%7CNT%7C2000)Windows)
能匹配 `2000Windows` 中的 `Windows`,但不能匹配 `3.1Windows` 中的 `Windows`。
`(?<!pattern)`
>反向否定预查,与正向否定预查类似,只是方向相反。例如 `(?<!95|98|NT|2000)Windows`
能匹配 `3.1Windows` 中的 `Windows`,但不能匹配 `2000Windows` 中的 `Windows`。
控制标记
有几个全局控制标记(Flag)需要了解,其中最常默认指定的有 `G` 和 `M`:
`A`/`ASCII`,默认为 `False`
> * `\d`, `\D`, `\w`, `\W`, `\s`, `\S`, `\b`, 和 `\B` 等只限于 ASCII 字符
> * 行内写法:`(?a)`
> * Python re 模块中的常量:`re.A` `re.ASCII`
`I`/`IGNORECASE`,默认为 `False`
> * 忽略字母大小写
> * 行内写法:`(?i)`
> * Python re 模块中的常量:`re.I` `re.IGNORECASE`
`G`/`GLOBAL`,默认为 `True`
> * 找到第一个 match 之后不返回
> * 行内写法:`(?g)`
> * Python re 模块中这个标记不能更改,默认为 TRUE
`L`/`LOCALE`,默认为 `False`
> * 由本地语言设置决定 `\d`, `\D`, `\w`, `\W`, `\s`, `\S`, `\b`, 和 `\B` 等等的内容
> * 行内写法:`(?L)`
> * Python re 模块中的常量:`re.L` `re.LOCALE`
`M`/`MULTILINE`,默认为 `True`
> * 使用本标志后,`^` 和 `$` 匹配行首和行尾时,会增加换行符之前和之后的位置。
> * 行内写法:`(?m)`
> * Python re 模块中的常量:`re.M` `re.MULTILINE`
`S`/`DOTALL`,默认为 `False`
> * 使 `.` 完全匹配任何字符,包括换行;没有这个标志,`.` 匹配除了 `n` `r` 之外的任何字符。
> * 行内写法:`(?s)`
> * Python re 模块中的常量:`re.S` `re.DOTALL`
`X`/`VERBOSE`,默认为 `False`
> * 当该标志被指定时,Pattern 中的的空白符会被忽略,除非该空白符在圆括号或方括号中,或在反斜杠 `\ ` 之后。这样做的结果是允许将注释写入 Pattern,这些注释会被 Regex 解析引擎忽略。注释用 `#` 号来标识,不过该符号不能在字符串或反斜杠之后。
> * 行内写法:`(?x)`
> * Python re 模块中的常量:`re.X` `re.VERBOSE`
几个最常用的 Regex
以下是几个常用的 Regex<a href='#fn2' name='fn2b'><sup>[2]</sup></a>,值得保存:
* matching username
> [`/^[a-z0-9_-]{3,16}$/`](https://regexper.com#/%5E[a-z0-9_-]%7B3,16%7D$/)
* matching password<a href='#fn3' name='fn3b'><sup>[3]</sup></a>
> [`/^[a-z0-9_-]{6,18}$/`](https://regexper.com#/%5E[a-z0-9_-]%7B6,18%7D$/)
* matching a HEX value
> [`/^#?([a-f0-9]{6}|[a-f0-9]{3})$/`](https://regexper.com#/%5E#?([a-f0-9]%7B6%7D%7C[a-f0-9]%7B3%7D)$/)
* matching a slug
> [`/^[a-z0-9-]+$/`](https://regexper.com#/%5E[a-z0-9-]+$/)
* matching email address
> [`/^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/`](https://regexper.com#/%5E([a-z0-9_%5C.-]+)@([%5Cda-z%5C.-]+)%5C.([a-z%5C.]%7B2,6%7D)$/)
* matching a URL
> [`/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/`](https://regexper.com#/%5E(https?:%5C/%5C/)?([%5Cda-z%5C.-]+)%5C.([a-z%5C.]%7B2,6%7D)([%5C/%5Cw%20%5C.-]*)*%5C/?$/)
* matching an IP address
> [`/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/`](https://regexper.com/#%2F%5E%28%3F%3A%28%3F%3A25%5B0-5%5D%7C2%5B0-4%5D%5B0-9%5D%7C%5B01%5D%3F%5B0-9%5D%5B0-9%5D%3F%29%5C.%29%7B3%7D%28%3F%3A25%5B0-5%5D%7C2%5B0-4%5D%5B0-9%5D%7C%5B01%5D%3F%5B0-9%5D%5B0-9%5D%3F%29%24%2F)
* matching a HTML tag
> [`/^<([a-z]+)([^<]+)*(?:>(.*)<\/\1>|\s+\/>)$/`](https://regexper.com/#%2F%5E<%28%5Ba-z%5D%2B%29%28%5B%5E<%5D%2B%29*%28%3F%3A>%28.*%29<%5C%2F%5C1>%7C%5Cs%2B%5C%2F>%29%24%2F)
对了,告诉你一个小秘密:
> 以上的正则表达式,点击都能连接到 [regexper.com](https://regexper.com),在那里你能查看这些正则表达式的图形化示意图。另外,本文中,处于 markdown cell 的绝大多数正则表达式都有这样的连接…… 你可以重读的时候试试。
写 Regex 最烧脑的地方在于 “使其全面” —— 要考虑到各种意外情况。
当然,除非必要,也不要在 “全面” 这事上面花太多时间 —— 给你看一个据说是 “最大程度上能够匹配所有 email 地址的 Regex” <a href='#fn4' name='fn4b'><sup>[4]</sup></a>,我都懒得测试的一个正则表达式:
```regex
(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t]
)+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:
\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(
?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[
\t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\0
31]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\
](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+
(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:
(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z
|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)
?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\
r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[
\t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)
?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t]
)*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[
\t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*
)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t]
)+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)
*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+
|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r
\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:
\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t
]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031
]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](
?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?
:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?
:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)|(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?
:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?
[ \t]))*"(?:(?:\r\n)?[ \t])*)*:(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\\".\[\]
\000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|
\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>
@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"
(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t]
)*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\
".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?
:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[
\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\\".\[\] \000-
\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(
?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;
:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([
^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\"
.\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\
]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\
[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\
r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\]
\000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]
|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\\".\[\] \0
00-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\
.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,
;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|"(?
:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*
(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".
\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t])*(?:[
^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\]
]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(?:\r\n)?[ \t])*)(?:,\s*(
?:(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\
".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(
?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[
\["()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t
])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t
])+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?
:\.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|
\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*|(?:
[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".\[\
]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*\<(?:(?:\r\n)
?[ \t])*(?:@(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["
()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)
?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>
@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[
\t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,
;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\.(?:(?:\r\n)?[ \t]
)*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\
".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?
(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\["()<>@,;:\\".
\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:\.(?:(?:
\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[\[
"()<>@,;:\\".\[\]]))|"(?:[^\"\r\\]|\\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])
*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])
+|\Z|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*)(?:\
.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\\".\[\] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z
|(?=[\["()<>@,;:\\".\[\]]))|\[([^\[\]\r\\]|\\.)*\](?:(?:\r\n)?[ \t])*))*\>(?:(
?:\r\n)?[ \t])*))*)?;\s*)
```
趣事一则
[ELIZA](https://en.wikipedia.org/wiki/ELIZA) 是人类史上第一个 “聊天机器人” 程序,上个世纪六十年代,1966 年实现的。核心就是使用正则表达式匹配对方语句中的关键词汇,再经过替换,形成看似有意义的 “回应” —— 事实上,ELIZA 完全不知道自己在说什么……
几年后,就是七十年代初,1972 年,另外一个聊天机器人 [Parry](https://en.wikipedia.org/wiki/PARRY) <a href='#fn5' name='fn5b'><sup>[5]</sup></a>被写了出来…… 随后第二年,这两个聊天机器人在 ARPANET “碰面” 好好地聊了一下<a href='#fn6' name='fn6b'><sup>[6]</sup></a>:
> ELIZA: Good Evening. Tell me your problems. Please terminate input with a period or a question mark.
>
> PARRY: People get on my nerves sometimes.
>
> ELIZA: I am not sure I understand you fully.
>
> PARRY: You should pay more attention.
>
> ELIZA: Suppose you should pay more attention.
>
> PARRY: You're entitled to your own opinion.
>
> ELIZA: What makes you think I am entitled to my own opinion?
>
> PARRY: It bothers me just to be around people in general.
>
> ELIZA: Please go on.
>
> PARRY: I went to the track at Bay Meadows a while back.
>
> ELIZA: What does that suggest to you?
>
> PARRY: I went to the races.
>
> ......
以下是后人用 Python 重新实现的 ELIZA 的程序代码,你可以直接执行它,尝试一下与 ELIZA 聊聊天:
```python
# %load https://raw.githubusercontent.com/jezhiggins/eliza.py/master/eliza.py
#----------------------------------------------------------------------
# eliza.py
#
# a cheezy little Eliza knock-off by Joe Strout
# with some updates by Jeff Epler
# hacked into a module and updated by Jez Higgins
#----------------------------------------------------------------------
import string
import re
import random
class eliza:
def __init__(self):
self.keys = list(map(lambda x:re.compile(x[0], re.IGNORECASE),gPats))
self.values = list(map(lambda x:x[1],gPats))
#----------------------------------------------------------------------
# translate: take a string, replace any words found in dict.keys()
# with the corresponding dict.values()
#----------------------------------------------------------------------
def translate(self,str,dict):
words = str.lower().split()
keys = dict.keys();
for i in range(0,len(words)):
if words[i] in keys:
words[i] = dict[words[i]]
return ' '.join(words)
#----------------------------------------------------------------------
# respond: take a string, a set of regexps, and a corresponding
# set of response lists; find a match, and return a randomly
# chosen response from the corresponding list.
#----------------------------------------------------------------------
def respond(self,str):
# find a match among keys
for i in range(0, len(self.keys)):
match = self.keys[i].match(str)
if match:
# found a match ... stuff with corresponding value
# chosen randomly from among the available options
resp = random.choice(self.values[i])
# we've got a response... stuff in reflected text where indicated
pos = resp.find('%')
while pos > -1:
num = int(resp[pos+1:pos+2])
resp = resp[:pos] + \
self.translate(match.group(num),gReflections) + \
resp[pos+2:]
pos = resp.find('%')
# fix munged punctuation at the end
if resp[-2:] == '?.': resp = resp[:-2] + '.'
if resp[-2:] == '??': resp = resp[:-2] + '?'
return resp
#----------------------------------------------------------------------
# gReflections, a translation table used to convert things you say
# into things the computer says back, e.g. "I am" --> "you are"
#----------------------------------------------------------------------
gReflections = {
"am" : "are",
"was" : "were",
"i" : "you",
"i'd" : "you would",
"i've" : "you have",
"i'll" : "you will",
"my" : "your",
"are" : "am",
"you've": "I have",
"you'll": "I will",
"your" : "my",
"yours" : "mine",
"you" : "me",
"me" : "you"
}
#----------------------------------------------------------------------
# gPats, the main response table. Each element of the list is a
# two-element list; the first is a regexp, and the second is a
# list of possible responses, with group-macros labelled as
# %1, %2, etc.
#----------------------------------------------------------------------
gPats = [
[r'I need (.*)',
[ "Why do you need %1?",
"Would it really help you to get %1?",
"Are you sure you need %1?"]],
[r'Why don\'?t you ([^\?]*)\??',
[ "Do you really think I don't %1?",
"Perhaps eventually I will %1.",
"Do you really want me to %1?"]],
[r'Why can\'?t I ([^\?]*)\??',
[ "Do you think you should be able to %1?",
"If you could %1, what would you do?",
"I don't know -- why can't you %1?",
"Have you really tried?"]],
[r'I can\'?t (.*)',
[ "How do you know you can't %1?",
"Perhaps you could %1 if you tried.",
"What would it take for you to %1?"]],
[r'I am (.*)',
[ "Did you come to me because you are %1?",
"How long have you been %1?",
"How do you feel about being %1?"]],
[r'I\'?m (.*)',
[ "How does being %1 make you feel?",
"Do you enjoy being %1?",
"Why do you tell me you're %1?",
"Why do you think you're %1?"]],
[r'Are you ([^\?]*)\??',
[ "Why does it matter whether I am %1?",
"Would you prefer it if I were not %1?",
"Perhaps you believe I am %1.",
"I may be %1 -- what do you think?"]],
[r'What (.*)',
[ "Why do you ask?",
"How would an answer to that help you?",
"What do you think?"]],
[r'How (.*)',
[ "How do you suppose?",
"Perhaps you can answer your own question.",
"What is it you're really asking?"]],
[r'Because (.*)',
[ "Is that the real reason?",
"What other reasons come to mind?",
"Does that reason apply to anything else?",
"If %1, what else must be true?"]],
[r'(.*) sorry (.*)',
[ "There are many times when no apology is needed.",
"What feelings do you have when you apologize?"]],
[r'Hello(.*)',
[ "Hello... I'm glad you could drop by today.",
"Hi there... how are you today?",
"Hello, how are you feeling today?"]],
[r'I think (.*)',
[ "Do you doubt %1?",
"Do you really think so?",
"But you're not sure %1?"]],
[r'(.*) friend (.*)',
[ "Tell me more about your friends.",
"When you think of a friend, what comes to mind?",
"Why don't you tell me about a childhood friend?"]],
[r'Yes',
[ "You seem quite sure.",
"OK, but can you elaborate a bit?"]],
[r'(.*) computer(.*)',
[ "Are you really talking about me?",
"Does it seem strange to talk to a computer?",
"How do computers make you feel?",
"Do you feel threatened by computers?"]],
[r'Is it (.*)',
[ "Do you think it is %1?",
"Perhaps it's %1 -- what do you think?",
"If it were %1, what would you do?",
"It could well be that %1."]],
[r'It is (.*)',
[ "You seem very certain.",
"If I told you that it probably isn't %1, what would you feel?"]],
[r'Can you ([^\?]*)\??',
[ "What makes you think I can't %1?",
"If I could %1, then what?",
"Why do you ask if I can %1?"]],
[r'Can I ([^\?]*)\??',
[ "Perhaps you don't want to %1.",
"Do you want to be able to %1?",
"If you could %1, would you?"]],
[r'You are (.*)',
[ "Why do you think I am %1?",
"Does it please you to think that I'm %1?",
"Perhaps you would like me to be %1.",
"Perhaps you're really talking about yourself?"]],
[r'You\'?re (.*)',
[ "Why do you say I am %1?",
"Why do you think I am %1?",
"Are we talking about you, or me?"]],
[r'I don\'?t (.*)',
[ "Don't you really %1?",
"Why don't you %1?",
"Do you want to %1?"]],
[r'I feel (.*)',
[ "Good, tell me more about these feelings.",
"Do you often feel %1?",
"When do you usually feel %1?",
"When you feel %1, what do you do?"]],
[r'I have (.*)',
[ "Why do you tell me that you've %1?",
"Have you really %1?",
"Now that you have %1, what will you do next?"]],
[r'I would (.*)',
[ "Could you explain why you would %1?",
"Why would you %1?",
"Who else knows that you would %1?"]],
[r'Is there (.*)',
[ "Do you think there is %1?",
"It's likely that there is %1.",
"Would you like there to be %1?"]],
[r'My (.*)',
[ "I see, your %1.",
"Why do you say that your %1?",
"When your %1, how do you feel?"]],
[r'You (.*)',
[ "We should be discussing you, not me.",
"Why do you say that about me?",
"Why do you care whether I %1?"]],
[r'Why (.*)',
[ "Why don't you tell me the reason why %1?",
"Why do you think %1?" ]],
[r'I want (.*)',
[ "What would it mean to you if you got %1?",
"Why do you want %1?",
"What would you do if you got %1?",
"If you got %1, then what would you do?"]],
[r'(.*) mother(.*)',
[ "Tell me more about your mother.",
"What was your relationship with your mother like?",
"How do you feel about your mother?",
"How does this relate to your feelings today?",
"Good family relations are important."]],
[r'(.*) father(.*)',
[ "Tell me more about your father.",
"How did your father make you feel?",
"How do you feel about your father?",
"Does your relationship with your father relate to your feelings today?",
"Do you have trouble showing affection with your family?"]],
[r'(.*) child(.*)',
[ "Did you have close friends as a child?",
"What is your favorite childhood memory?",
"Do you remember any dreams or nightmares from childhood?",
"Did the other children sometimes tease you?",
"How do you think your childhood experiences relate to your feelings today?"]],
[r'(.*)\?',
[ "Why do you ask that?",
"Please consider whether you can answer your own question.",
"Perhaps the answer lies within yourself?",
"Why don't you tell me?"]],
[r'quit',
[ "Thank you for talking with me.",
"Good-bye.",
"Thank you, that will be $150. Have a good day!"]],
[r'(.*)',
[ "Please tell me more.",
"Let's change focus a bit... Tell me about your family.",
"Can you elaborate on that?",
"Why do you say that %1?",
"I see.",
"Very interesting.",
"%1.",
"I see. And what does that tell you?",
"How does that make you feel?",
"How do you feel when you say that?"]]
]
#----------------------------------------------------------------------
# command_interface
#----------------------------------------------------------------------
def command_interface():
print('Therapist\n---------')
print('Talk to the program by typing in plain English, using normal upper-')
print('and lower-case letters and punctuation. Enter "quit" when done.')
print('='*72)
print('Hello. How are you feeling today?')
s = ''
therapist = eliza();
while s != 'quit':
try:
s = input('> ')
except EOFError:
s = 'quit'
print(s)
while s[-1] in '!.':
s = s[:-1]
print(therapist.respond(s))
if __name__ == "__main__":
command_interface()
```
Therapist
---------
Talk to the program by typing in plain English, using normal upper-
and lower-case letters and punctuation. Enter "quit" when done.
========================================================================
Hello. How are you feeling today?
-----
**脚注**
<a name='fn1'>[1]</a>:释义摘自苹果电脑上系统内建的《牛津英汉双解辞典》
<a href='#fn1b'><small>↑Back to Content↑</small></a>
<a name='fn2'>[2]</a>:[8 Regular Expressions You Should Know](https://bit.ly/2tz8v9n) by Vasili
<a href='#fn2b'><small>↑Back to Content↑</small></a>
<a name='fn3'>[3]</a>:关于校验密码强度的正则表达式,往往需要设置更为复杂的规则,Stackoverflow 上的一则答复中有很好的示例:https://stackoverflow.com/a/21456918
<a href='#fn3b'><small>↑Back to Content↑</small></a>
<a name='fn4'>[4]</a>:http://www.ex-parrot.com/pdw/Mail-RFC822-Address.html
<a href='#fn4b'><small>↑Back to Content↑</small></a>
<a name='fn5'>[5]</a>:Parry 的源代码(用 Lisp 写的)在这里:http://www.cs.cmu.edu/afs/cs/project/ai-repository/ai/areas/classics/parry/</a>
<a href='#fn5b'><small>↑Back to Content↑</small></a>
<a name='fn6'>[6]</a>:ELIZA 和 Parry 的完整聊天记录在这里:https://tools.ietf.org/html/rfc439</a>
<a href='#fn6b'><small>↑Back to Content↑</small></a>
BNF 以及 EBNF
通常情况下,你很少会在入门书籍里读到关于 Backus-Naur Form(BNF,巴科斯-诺尔范式)和 Extended Backus-Naur Form(EBNF)的话题 —— 它们都被普遍认为是 “非专业人士无需了解的话题”,隐含的另外一层含义是 “反正就算给他们讲他们也无论如何看不懂”……
然而,在我眼里,这事非讲不可 —— 这是这本 “书” 的设计目标决定的。
严格意义上来讲,在《自学是门手艺》中,以自学编程为例,我完全没必要自己动手耗时费力写那么多东西 —— 如果仅仅是为了让读者 “入门” 的话。编程入门书籍,或者 Python 编程入门书籍,都已经太多太多了,其中质量过硬的书籍也多得去了 —— 并且,如果你没有英文阅读障碍,那你就会发现网上有太多非常优质的免费教程…… 真的轮不到李笑来同学再写一次。
我写这本书的目标是:
> 让读者从认知自学能力开始,通过自学编程作为第一个实践,逐步完整掌握自学能力,进而在随后漫长的人生中,需要什么就去学什么,
…… 不用非得找人教、找人带 —— 只有这样,**前途**这两个字才会变得实在。
于是,我最希望能做到的是,从这里了解了自学方法论,也了解了编程以及 Python 编程的基础概念之后,《自学是门手艺》的读者能够**自顾自地踏上征程,一路走下去** —— 至于走到哪里,能走到哪里,不是我一个作者一厢情愿能够决定的,是吧?
当然,会自学的人运气一定不会差。
于是,这本 “书” 的核心目标之一,换个说法就是:
> 我希望读者在读完《自学是门手艺》之后,有能力独立地去全面研读[官方文档](https://docs.python.org/3/) —— 甚至是各种编程语言、各种软件的相关的文档(包括它们的官方文档)。
自学编程,很像独自一人冲入了一个丛林,里面什么动物都有…… 而且那个丛林很大很大,虽然丛林里有的地方很美,可若是没有地图和指南针,你就会迷失方向。
其实吧,地图也不是没有 —— 别说 Python 了,无论什么编程语言(包括无论什么软件)都有很翔实的官方文档…… 可是吧,绝大多数人无论买多少书、上多少课,就是不去用官方 “地图”,就不!
—— 其实倒不是说 “第三方地图” 更好,实际的原因很不好意思说出来:
> * 这首先吧,觉得官方文档阅读量太大了……(嗯?那地图不是越详细越好吗?)
> * 那还有吧…… 也不是没去看过,**看不懂**……(嗯…… 这对初学者倒是个问题!)
所以,我要认为这本 “书” 的最重要工作是:
> 为读者解读清楚地图上的 “图例”,从此之后读者在任何需要的时候能够彻底读懂地图。
在阅读官方文档的时候,很多人在 [The Python Tutorial](https://docs.python.org/3/tutorial/index.html) 上就已经觉得吃力了…… 如果到了 [Standard Libraries](https://docs.python.org/3/library/index.html) 和 [Language References](https://docs.python.org/3/reference/index.html) 的部分,就基本上完全放弃了,比如,以下这段摘自 [string —— Common string operations](https://docs.python.org/3/library/string.html):
> Format Specification Mini-Language
> ...
> The general form of a standard format specifier is:
```
format_spec ::= [[fill]align][sign][#][0][width][grouping_option][.precision][type]
fill ::= <any character>
align ::= "<" | ">" | "=" | "^"
sign ::= "+" | "-" | " "
width ::= digit+
grouping_option ::= "_" | ","
precision ::= digit+
type ::= "b" | "c" | "d" | "e" | "E" | "f" | "F" | "g" | "G" |
"n" | "o" | "s" | "x" | "X" | "%"
```
> ...
读到这,看着一大堆的 `::=` `[]` `|` 当场傻眼了……
这是 BNF 描述,还是 Python 自己定制的 EBNF…… 为了理解它们,以后当然最好有空研究一下 “上下文无关文法”([Context-free Grammar](https://en.wikipedia.org/wiki/Context-free_grammar)),没准未来你一高兴就会去玩一般人不敢玩的各种 Parser,甚至干脆自己写门编程语言啥的…… 不过,完全可以跳过那些复杂的东西的 —— 因为你当前的目标只不过是 “能够读懂那些符号的含义”。
其实吧,真的不难的 —— 它就是语法描述的方法。
比如,什么是符合语法的整数(Integer)呢?符合以下语法描述的是整数(使用 Python 的 EBNF):
```
integer ::= [sign] digit +
sign ::= "+" | "-"
digit ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
```
以上的描述中,基本符号没几个,它们各自的含义是:
> * `::=` 表示定义;
> * `< >` 尖括号里的内容表示必选内容;
> * `[ ]` 中是可选项;
> * `" "` 双引号里的内容表示字符;
> * ` | ` 竖线两边的是可选内容,相当于or;
> * ` * ` 表示零个或者多个……
> * ` + ` 表示一个或者多个……
于是:
> 1. `interger` 定义是:由 “可选的 `sign`” 和 “一个或者多个 `digit` 的集合” 构成 —— 第一行末尾那个 `+` 的作用和正则表达式里的 `+` 一样;
> 2. `sign` 的定义是什么呢?要么是 `+` 要么是 `-;`
> 3. `digit` 的定义是什么呢?从 `"0"` 到 `"9"` 中的任何一个值……
于是,`99`、`+99`、`-99`,都是符合以上语法描述的 `integer`;但 `99+` 和 `99-` 肯定不符合以上语法描述的 `integer`。
很简单吧?反正就是在 `::=` 左边逐行列出一个语法构成的所有要素,而后在右边逐行逐一定义,直至全部要素定义完毕。
也许那些在此之前已经熟悉 BNF 范式的人会有点惊讶,“你怎么连 ‘*终结符*’ 和 ‘*非终结符*’ 这种最基本的概念都跳过了?” —— 是呀,即便不讲那俩概念也能把这事讲清楚到 “能马上开始用” 了的地步…… 这就是我经常说的,“人类有这个神奇的本领,擅长使用自己并不懂的东西……”
Python 对 BNF 的拓展,借鉴了正则表达式<a href='#fn1' name='fn1b'><sup>[1]</sup></a> —— 从最后两个符号的使用(`*` `+`)你可以看得出来。顺带说,这也是为什么这本 “书” 里非要讲其他入门书籍里不讲的正则表达式的原因之一。
又由于 Python 的社区文档是二十来年长期积累的,有时标注方法并不一致。比如,在描述 [Python Full Grammar specification](https://docs.python.org/3/reference/grammar.html) 的时候,他们用的语法标注符号体系就跟上面描述 String 的语法不一样了,是这样的:
> * `:` 表示定义;
> * `[ ]` 中是可选项;
> * `' '` 引号里的内容表示字符;
> * ` | ` 竖线两边的是可选内容,相当于or;
> * ` * ` 表示零个或者多个……
> * ` + ` 表示一个或者多个……
—— 用冒号 `:` 替代了 `::=`,用单引号 `''` 替代了双引号 `""`,而尖括号 `<>` 干脆不用了:
``` python
# Grammar for Python
# NOTE WELL: You should also follow all the steps listed at
# https://devguide.python.org/grammar/
# Start symbols for the grammar:
# single_input is a single interactive statement;
# file_input is a module or sequence of commands read from an input file;
# eval_input is the input for the eval() functions.
# NB: compound_stmt in single_input is followed by extra NEWLINE!
single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE
file_input: (NEWLINE | stmt)* ENDMARKER
eval_input: testlist NEWLINE* ENDMARKER
decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
decorators: decorator+
decorated: decorators (classdef | funcdef | async_funcdef)
async_funcdef: 'async' funcdef
funcdef: 'def' NAME parameters ['->' test] ':' suite
parameters: '(' [typedargslist] ')'
typedargslist: (tfpdef ['=' test] (',' tfpdef ['=' test])* [',' [
'*' [tfpdef] (',' tfpdef ['=' test])* [',' ['**' tfpdef [',']]]
| '**' tfpdef [',']]]
| '*' [tfpdef] (',' tfpdef ['=' test])* [',' ['**' tfpdef [',']]]
| '**' tfpdef [','])
tfpdef: NAME [':' test]
varargslist: (vfpdef ['=' test] (',' vfpdef ['=' test])* [',' [
'*' [vfpdef] (',' vfpdef ['=' test])* [',' ['**' vfpdef [',']]]
| '**' vfpdef [',']]]
| '*' [vfpdef] (',' vfpdef ['=' test])* [',' ['**' vfpdef [',']]]
| '**' vfpdef [',']
)
vfpdef: NAME
stmt: simple_stmt | compound_stmt
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt |
import_stmt | global_stmt | nonlocal_stmt | assert_stmt)
expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
('=' (yield_expr|testlist_star_expr))*)
annassign: ':' test ['=' test]
testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [',']
augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' |
'<<=' | '>>=' | '**=' | '//=')
# For normal and annotated assignments, additional restrictions enforced by the interpreter
del_stmt: 'del' exprlist
pass_stmt: 'pass'
flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt
break_stmt: 'break'
continue_stmt: 'continue'
return_stmt: 'return' [testlist]
yield_stmt: yield_expr
raise_stmt: 'raise' [test ['from' test]]
import_stmt: import_name | import_from
import_name: 'import' dotted_as_names
# note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS
import_from: ('from' (('.' | '...')* dotted_name | ('.' | '...')+)
'import' ('*' | '(' import_as_names ')' | import_as_names))
import_as_name: NAME ['as' NAME]
dotted_as_name: dotted_name ['as' NAME]
import_as_names: import_as_name (',' import_as_name)* [',']
dotted_as_names: dotted_as_name (',' dotted_as_name)*
dotted_name: NAME ('.' NAME)*
global_stmt: 'global' NAME (',' NAME)*
nonlocal_stmt: 'nonlocal' NAME (',' NAME)*
assert_stmt: 'assert' test [',' test]
compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt
async_stmt: 'async' (funcdef | with_stmt | for_stmt)
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
try_stmt: ('try' ':' suite
((except_clause ':' suite)+
['else' ':' suite]
['finally' ':' suite] |
'finally' ':' suite))
with_stmt: 'with' with_item (',' with_item)* ':' suite
with_item: test ['as' expr]
# NB compile.c makes sure that the default except clause is last
except_clause: 'except' [test ['as' NAME]]
suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT
test: or_test ['if' or_test 'else' test] | lambdef
test_nocond: or_test | lambdef_nocond
lambdef: 'lambda' [varargslist] ':' test
lambdef_nocond: 'lambda' [varargslist] ':' test_nocond
or_test: and_test ('or' and_test)*
and_test: not_test ('and' not_test)*
not_test: 'not' not_test | comparison
comparison: expr (comp_op expr)*
# <> isn't actually a valid comparison operator in Python. It's here for the
# sake of a __future__ import described in PEP 401 (which really works :-)
comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not'
star_expr: '*' expr
expr: xor_expr ('|' xor_expr)*
xor_expr: and_expr ('^' and_expr)*
and_expr: shift_expr ('&' shift_expr)*
shift_expr: arith_expr (('<<'|'>>') arith_expr)*
arith_expr: term (('+'|'-') term)*
term: factor (('*'|'@'|'/'|'%'|'//') factor)*
factor: ('+'|'-'|'~') factor | power
power: atom_expr ['**' factor]
atom_expr: ['await'] atom trailer*
atom: ('(' [yield_expr|testlist_comp] ')' |
'[' [testlist_comp] ']' |
'{' [dictorsetmaker] '}' |
NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False')
testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] )
trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME
subscriptlist: subscript (',' subscript)* [',']
subscript: test | [test] ':' [test] [sliceop]
sliceop: ':' [test]
exprlist: (expr|star_expr) (',' (expr|star_expr))* [',']
testlist: test (',' test)* [',']
dictorsetmaker: ( ((test ':' test | '**' expr)
(comp_for | (',' (test ':' test | '**' expr))* [','])) |
((test | star_expr)
(comp_for | (',' (test | star_expr))* [','])) )
classdef: 'class' NAME ['(' [arglist] ')'] ':' suite
arglist: argument (',' argument)* [',']
# The reason that keywords are test nodes instead of NAME is that using NAME
# results in an ambiguity. ast.c makes sure it's a NAME.
# "test '=' test" is really "keyword '=' test", but we have no such token.
# These need to be in a single rule to avoid grammar that is ambiguous
# to our LL(1) parser. Even though 'test' includes '*expr' in star_expr,
# we explicitly match '*' here, too, to give it proper precedence.
# Illegal combinations and orderings are blocked in ast.c:
# multiple (test comp_for) arguments are blocked; keyword unpackings
# that precede iterable unpackings are blocked; etc.
argument: ( test [comp_for] |
test '=' test |
'**' test |
'*' test )
comp_iter: comp_for | comp_if
sync_comp_for: 'for' exprlist 'in' or_test [comp_iter]
comp_for: ['async'] sync_comp_for
comp_if: 'if' test_nocond [comp_iter]
# not used in grammar, but may appear in "node" passed from Parser to Compiler
encoding_decl: NAME
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist
```
现在你已经能读懂 BNF 了,那么,可以再读读用 BNF 描述的 Regex 语法<a href='#fn2' name='fn2b'><sup>[2]</sup></a>,就当复习了 —— 很短的:
```html
BNF grammar for Perl-style regular expressions
<RE> ::= <union> | <simple-RE>
<union> ::= <RE> "|" <simple-RE>
<simple-RE> ::= <concatenation> | <basic-RE>
<concatenation> ::= <simple-RE> <basic-RE>
<basic-RE> ::= <star> | <plus> | <elementary-RE>
<star> ::= <elementary-RE> "*"
<plus> ::= <elementary-RE> "+"
<elementary-RE> ::= <group> | <any> | <eos> | <char> | <set>
<group> ::= "(" <RE> ")"
<any> ::= "."
<eos> ::= "$"
<char> ::= any non metacharacter | "\" metacharacter
<set> ::= <positive-set> | <negative-set>
<positive-set> ::= "[" <set-items> "]"
<negative-set> ::= "[^" <set-items> "]"
<set-items> ::= <set-item> | <set-item> <set-items>
<set-item> ::= <range> | <char>
<range> ::= <char> "-" <char>
```
真的没原来以为得那么神秘,是不?<a href='#fn3' name='fn3b'><sup>[3]</sup></a>
都学到这儿了…… 顺带再自学个东西吧。
这个东西叫 `glob`,是 Global 的缩写。你可以把它理解为 “超级简化版正则表达式” —— 它最初是 Unix/Posix 操作系统中用来匹配文件名的 “通配符”。
先看一张 1971 的 Unix 操作系统中关于 glob 的截图:
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/Unix_Glob_Reference.png?raw=true)
> A screenshot of the original 1971 Unix reference page for glob – note the owner is dmr, short for Dennis Ritchie.
glob 的主要符号只有这么几个:
> * `*`
> * `?`
> * `[abc]`
> * `[^abc]`
现在的你,打开 Wikipedia 上的关于 glob 和 Wildcard character 的页面,肯定能做到 “无障碍” 理解:
> * https://en.wikipedia.org/wiki/Glob_(programming)
> * https://en.wikipedia.org/wiki/Wildcard_character
顺带说,现在你再去读关于 Format String 的官方文档,就不会再觉得 “根本看不懂” 了,恰恰相反,你会觉得 “我怎么之前连这个都看不懂呢?”
> https://docs.python.org/3/library/string.html#format-string-syntax
在自学这件事上,失败者的死法看起来千变万化,但其实都是一样的…… 只不过是因为怕麻烦或者基础知识不够而不去读最重要的文档。
比如,学英语的时候死活不读语法书。其实英文语法书也没多难啊?再厚,不也是用来 “查” 的吗?不就是多记几个标记就可以读懂的吗?比如,词性标记,`v.`, `n.`, `adj.`, `adv.`, `prep.`... 不就是相当于地图上的图例吗?那语法书,和现在这里提到的官方文档,不都是 “自学者地图” 吗?
但就是这么一点点简单的东西,挡住了几乎所有人,真是可怕。
-----
**脚注**
<a name='fn1'>[1]</a>:[The Python Language Reference » 1.2 Notation](https://docs.python.org/3/reference/introduction.html#notation) —— 这个链接必须去看一看……
<a href='#fn1b'><small>↑Back to Content↑</small></a>
<a name='fn2'>[2]</a>:[Perl Style Regular Expressions in Prolog](http://www.cs.sfu.ca/~cameron/Teaching/384/99-3/regexp-plg.html) CMPT 384 Lecture Notes
Robert D. Cameron November 29 - December 1, 1999
<a href='#fn2b'><small>↑Back to Content↑</small></a>
<a name='fn3'>[3]</a>:很少有人注意到:在很多编程语言的文法文档中,`"$"` 被称为 `<eos>` —— 2017 年 5 月我投资了一个初创公司,听说他们的资产名称叫做 `eos`…… 我当场就被这个梗逗乐了。
<a href='#fn3b'><small>↑Back to Content↑</small></a>
拆解
在学习编程的过程中,你会不由自主地学会一个重要技能:
> **拆解**
这么简单的两个字,在人生中的作用重大到不可想像…… 而且它也的确是自学能力中最重要的底层能力之一。
横向拆解
我很幸运,12 岁的时候有机会学习编程(习得了最基本的概念,那时候学的编程语言是 BASIC),所以,相对其他人在 “拆解任务” 方面有更强的初始意识。
后来,15 岁开始学着玩吉他的时候,发现道理其实是一样的。
有个曲子很难(当然也非常好听),曲名是 [Recrerdes Da La Alhambra](https://www.youtube.com/watch?v=OOsRMECWKAE) 阿罕布拉宫的回忆。你看看曲谱就知道它多难了:
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/guitar-tab.gif?raw=true)
那怎么办?怎么办?!—— 我的办法听起来看起来都很笨:
> * 每次只弹一个小节;
> * 而且还是放慢速度弹,刚开始很慢很慢;
> * 等熟悉了之后逐渐快起来,直到正常速度;
> * 再开始弹下一个小节;
> * 同样是放慢速度弹,刚开始很慢很慢;
> * 等熟悉了之后逐渐快起来,直到正常速度;
> * 再把两个小节拼起来;
> * 有些小节拼起来相对容易,另外一些需要挣扎很久才顺畅;
如此这般,最终就把这个很难的曲子弹出来了 —— 其实所有的初学者都是这么干的。
可以听听这个曲子放松一下(当然肯定不是我弹的哈哈):
```python
from IPython.display import IFrame
IFrame('https://www.youtube.com/embed/OOsRMECWKAE?', width='800', height='450')
```
<iframe
width="800"
height="450"
src="https://www.youtube.com/embed/OOsRMECWKAE?"
frameborder="0"
allowfullscreen
></iframe>
提起这事,总是会不由自主地叹口气 —— 因为在这事上我运气太差,刚把这个曲子练完没多久,还没来得及找人显摆,就摔断了掌骨和指骨,给我的手指灵活性造成了不可修复的损伤,于是,后来只能用拨片玩玩吉他了……
话说回来,自学的一个重要技巧就是,
> 把那些很难的任务无限拆分 —— 直至每个子任务都很小,小到都**可操作**为止。
比如,正则表达式,这个你必须学会的东西,学会学好真的不那么容易。一切的技能都一样,没学会之前都是很难很难的,可学会之后用熟了,你就会 “发现” 那东西其实也没多难……
那刚开始的时候怎么办?你其实需要运用拆分的本领:
> * 先囫囵吞枣至少读一遍教程;
> * 给自己搭好测试的环境(或许在 Regex101.com 上,或许用一个编辑器,比如 VS Code);
> * 先不管什么意思,找一些 Regex 自己试试;
> * 正式进入 “精度” 状态,每一小节每一小节地突破;
> * 搞定一小节之后,就把它与之前的小节再反复翻两三遍;
> * 把学习任务拆分成若干块,再重新逐个突破,比如,匹配,替换,在编辑器中使用,在 Python 代码中使用;
> * 把各种操作符与特殊字符拆分成若干个组,而后,熟悉到牢记(而不用将来反复回来查询);
> ……
事实上,当你习惯这么做了之后,就会 “发现” 一切的自学任务,其实都不是 “难”,不过是 “繁杂程度不一” 而已。
很多人最终自学失败,要么是不懂拆分任务,要么就仅仅是**怕麻烦**而已 —— 还是那句话,人活着就挺麻烦的……
纵向拆解
拆解的第一种方法是把某个任务拆分成若干个小任务,正如上面的讲解那样,我称它为 “横向拆解”。
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/decompose-x.png?raw=true)
另外一种方法,我称它为 “纵向拆解”(有时,我也会用 “分层拆解” 这个说法)。
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/decompose-y.png?raw=true)
这种方式在自学复杂的概念体系时特别管用。
编程这种东西,之所以刚开始觉得难学,是因为其中涉及的概念除了之前我们强调的 “[过早引用](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.F.deal-with-forward-references)” 之外,还有个特征:
> 有的概念虽然并不同属一个层面,却常常纠缠在一起,没有明确的提示……
比如,常量、变量、字符串、函数、文件等等的概念,其实并不是某个特定的编程语言的专有概念,它们是所有编程语言都会涉及到的概念,因为计算机处理的就是这些东西,而无论哪个编程语言最终都要通过计算机去处理这些东西。
又比如说,分支与循环,每个编程语言都有对应的语句,所以,分支循环在逻辑判断、流程管理这个层面,而分支循环的 “**实现**” 应该划分到另外一个层面中去;而每个语言实现分支循环语句的语法多少有点差异 —— 这些细节属于那个编程语言本身:
```python
# Python 这么写:
for i in range(n):
...
```
```javascript
// JavaScript 这么写:
var i;
for (i = 0; i < n; i++) {
...;
}
```
自学正则表达式的时候也如此。最基本的规则是属于 Regex 自己的;而后各种语言的实现各不相同,那是各个编程语言层面的;在各种编辑器中,除了基础的规则之外,也有它们自己的定制…… 看起来细节很多,但分层分类之后,就会变得很容易理解、很容易记住。
遇到 “面向对象编程” 也是如此。类、实例、对象、继承、多态…… 这些其实并不属于某一个编程语言,但它们也确实在几乎所有编程语言中被实现、被应用 —— 所谓的难,无非是因为属于两个层面甚至多个层面的概念被拧在一起教、学、练……
再比如说,在我把这个用编程当作习得自学能力的第一个 “实战项目” 之时,甚至要把 “读” 和 “写” 分成两个层面,先照顾 “读”,至于 “写”,要等到有了基本的 “读” 的能力之后再说;即便是到了 “写”,还要划分至少两个层面,首先是从 “简单的函数” 开始,而非上来就要写个 “大程序”…… 这种拆分层面的技能好像可以用在方方面面呢!
所以,要在自学的过程中,不停地想办法把它们从层面上区分开来 —— 不能总是把它们混在一块儿 “大锅烩”。
日常生活中,我们会遇到被评价为 “理解能力强” 的人,而另外那些不被如此评价的人就很不理解,很迷惑:
> 我到底差在哪儿了,你不说我理解能力强?难道我的理解能力很差吗?
当老师当久了,经常被这种现象震惊:
> 原来很简单的东西竟然可能成为很多人一生的障碍。
—— 并且,这话重复多少遍都不过分。
大多数人不太在意自己脑中的概念之间的关系,因为平日里这也不怎么耽误事。但一旦遇到复杂一点的知识体系,就完全搞不定了…… 而所谓知识体系的复杂,无非就是新的概念多一些,概念之间的关联更复杂一些…… 而概念之间的关联更复杂一些,无非是各个概念之间不仅只有一种联系,最后会形成网状连接……
—— 在《通往财富自由之路》那本书里,我几乎用了整本书的篇幅去讲解、厘清概念及其之间关系的重要性。
复杂吗?其实并不复杂 —— 在横向纵向分别逐步分清之后。
可问题在于,脑子里概念关联一团糟的人,自己并不觉得,甚至无法知道 —— 他们是那种[跟你一块去看一场电影却能看到另外一部电影的人](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.C.must.learn.sth.only.by.reading.md)。说他们理解能力差过分吗?他们不能理解被评价为理解能力差,难道不是很自然吗?
分清概念的方法是什么?其实也不难,就是不断拆解,不断主动整理。每次用图表整理那些概念的时候,就会发现比原来更清晰一些,多次整理,最终就谙熟于心了。
想要再举更恰当更惊人的例子很难,勉为其难再举个例子。
当我在 2011 年遇到比特币的时候,现在回头看,在当时的情况下,我平日里习惯对概念及其关联进行各种纵向横向的拆解这件事给我 “创造” 了巨大的好运。后来我在《[INBlockchain 的开源区块链投资原则](https://github.com/xiaolai/INB-Principles/blob/master/Chinese.md)》里提过这事:
> “比特币” 这个概念,可以有多重的理解 —— 这也是为什么人们感到迷惑,或者相互之间很难达成一致理解的根本原因。
>
> 首先,比特币是世界上第一个,也是迄今为止最成功的区块链应用。
>
> 其次,比特币是一家世界银行,只不过它不属于任何权威管辖,它是由一个去中心化网络构成的。
>
> 另外,这家叫做比特币的,去中心化的世界银行,发行了一个货币,恰好也叫 “比特币”。有些人更喜欢使用相对小心的说法,把这个货币指称为 BTC,而不是 “比特币”(Bitcoin)。
>
> 最后,即便在比特币横空出世的七年后(2017),也很少有人意识到比特币(或者 BTC)其实也可以被理解为这家叫做比特币的去中心化的世界银行的股票。
—— 这无非就是把一个概念拆分成若干个层面再对每个层面准确理解而已。
但毫无疑问,这点靠很简单很简单的方法练就的理解能力,帮了我大忙。
触类旁通
无论听起来多么简单的任务,落实成代码肯定没那么简单,没那么容易。
以后你会越来越清楚的:写程序的主要工作量,往往并非来自于在编辑器里敲代码那个阶段。
> 更多的工作量,其实在于如何才能在脑子里把整个流程拆解清楚,考虑到各个方面……
所以,编程,更多是拿着纸笔梳理细节的工作。一旦所有的细节都想明白了,落实成代码其实是飞快的 —— 越是工程量大的项目越是如此。
这个道理在哪里都是相同、相通的。不说编程,说写书,也是一样的。
随着时间的推移,你花在 “拆解” 上的时间会越来越多,因为所有大的工程,都可以被拆解成小工程 —— 于是,也为了做出大工程,拆解的工作首先是必须,其次是最耗时费力但最值得的。
我身边很多人,包括出版社的专业编辑,都慨叹过我的 “写书速度”。我猜,实际上把他们惊到甚至惊倒的,并不是他们以为的 “李笑来写书的速度”,而是 “李笑来打字的速度” 而已。
当我告诉他们我要写一本什么书了的时候,实际上,有个工作早就完成了:“系统梳理要写的那本书的所有细节”,剩下的只是落笔把那些东西写出来而已 —— 当然,我是敲出来,用我那几乎无以伦比的输入速度敲出来 —— 那当然 “显得” 很快了!
创业也好,投资也罢,还是一样的。因为我这个人脸皮厚,不怕人们笑话,所以我可以平静地说这事:
> 我参与(或投资)过**很多**失败的创业项目……
对所有复盘的结果,无一例外,根源都是当初立项的时候,很多重要细节还没搞清楚,甚至没想到要去搞清楚,就已经开始行动…… 于是,在成本不断积累的情况下,没完没了地处理各种 “意外”,没完没了地重新制定目标,没完没了地拖延,没完没了地 “重新启动”…… 直至开始苟延残喘,最后不了了之。
拆解得不够,就容易导致想不清楚,想错,想歪……
也许,有人可能会理直气壮地反问,“怎么可能从一开始就把所有情况都想清楚么!” 唉,是呀,以前我也是这么想的…… 直到吃了很多亏,很多很多亏,很多很多很大很大的亏,才 “发现” 且不得不痛下决心去接受:事先想不清楚的,就不要去做。
这是一种特殊、且重要、又极有价值的能力。现实生活中,后来我也见过若干有这种能力的高人,比如,你可以到网上搜一个人名,[庄辰超](https://www.google.com/search?q=庄辰超),他就是我见过的能做到干什么事之前都能全都想清楚的真人活人之一。
自学的时候,拆解任务的重要性更是如此。
这本 “书” 的一个特点,就是把 “自学”(或者平日里称为 “学习”)这个流程,拆解为 “学”、“练”、“用”、后面还会讲到 “造” 总计四个环节来处理 —— 从内容编排本身就这么干,甚至,在开头相当一部分,就明确说明,“根本不指望你读过一遍就会了”,还反复提醒,“要重复读很多遍,虽然第一遍必须囫囵吞枣”……
对于初学者常面临的尴尬,我们也从一开始就提醒,编程语言,和你之前在学校里学的语文,本质上没什么区别,先学会读,而后在多读多读再多读的同时,开始练习写 —— 这才真的很自然。
即便是开始讲如何写,我们的做法也是从 “写函数” 开始,而不是 “来,让我们写个程序……” —— 这一点点看起来不起眼的差异,作用是很大的,因为从 “小而完整” 的东西开始做(任何事)非常重要。
[ “小” 无所谓,“完整” 才是关键](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.F.social-selfteaching.md)。
刚需幻觉
在前言之中,就举过一个例子,人们一不小心就把自己搭了进去…… 只不过因为没搞明白,道理就是道理,跟讲道理的老生其实全然没什么关系。
在自学中,耽误人的幻觉很多。比如,时间幻觉。人们总觉得自己时间不够了,所以学东西的时候总是很急…… 可实际上,练成一门手艺,到够用的地步,一两年足够;到很好的地步,三五年足够 —— 至于极好么,那是一辈子的事。结果呢,很多人瞎着急,乱 “省时间”,学啥都不全面,练啥都不足数足量,一晃三五年就过去,然后又开始焦虑,换个手艺再学学再试试…… 然后循环焦虑。
最坑人的幻觉,在我看来,就是刚需幻觉 —— 这又是我杜撰的一个词…… 听我慢慢讲。
感觉总是最大的坑
我的结论是:
> 绝大多数人的自学能力,基本上都是被 “自己的感觉” 耽误掉的。
即,原本每个人都有一定的自学能力,但最终,都被自己的感觉给干掉了,直至全然失去自学能力 —— 虽然其后也经常学习,但其后一生从事的全都是学习这个行为里最初级的模式:“模仿”。
为什么 “模仿” 是学习行为最初级的模式呢?
首先,模仿必须依赖模仿对象,受自己的视野所限。
其次,模仿只能处理表里如一的简单知识和技能;一旦遇到那些深层次的、表面上看不大出来的,模仿就无效了,即便是非要模仿、愣模仿,结果就只能是 “东施效颦”。
在《[把时间当作朋友](https://github.com/xiaolai/time-as-a-friend)》中,我就反复强调一件事:
> 不要问学它有什么用,学就是了……
这原本是自学的最佳策略之一,也是自学的最根本策略。然而,听进去这句话的人不多,即便是我举了无数的例子,即便是他可能当时也有一点认同…… 然而,转瞬间又变成了原来的状态,无论遇到什么自学机会都不由自主地问:
> 我学它有什么用啊?
如果,得到的答案中,那 “用处” 对自己不是 “刚需”,瞬间就失去了动力,瞬间就放弃了追求…… 直至某一天,突然 “发现” 那竟然是个刚需…… 于是,临时抱佛脚。人么,总是对追求的事情形影相随;又有谁会对曾经放弃的事情念念不忘呢?于是,下一次还是会再做 “预算不足” 的决定。
最终失去自学能力的人,都是因为对 “刚需” 的判断失误 —— 这就是我说的 “刚需幻觉”。
刚需幻觉
“刚需幻觉” 的根源在于:
> 你不会的东西,对你来说感觉上就不是刚需。
要不是这本 “书” 里有个自学编程那么大且立体的一个例子存在,供我反复地、多角度地阐述道理,上面这句话还真的不好解释 —— 即便解释清楚了,也因为缺乏生动的例证而失去效力。
正则表达式(Regex)就是很好的例子。
当你没有学会它的时候,甚至不知道它的时候,感觉上,你不可能觉得那是个刚需 —— 因为你一直都活得好好的,甚至在你全然不知道那个东西竟然存在的情况下。
你略微学了一下,没学会、没学好、或者干脆放弃了之后,在感觉上,你也同样不会觉得那是刚需。因为你不会用它,所以你当然 “用不上它”…… 你根本没办法知道你不懂正则表达式这个事实让你 “未得到” 什么(不是 “失去”,因为你不曾拥有)。
然而,只要你花了一点力气,真的掌握了它,你会有两个 “发现”:
> * 你根本离不开它<a href='#fn1' name='fn1b'><sup>[1]</sup></a>;
> * 它其实真没多难……
第二个 “发现” 总是让你无比庆幸 —— 幸亏学了,幸亏 “发现” 了,否则,自己耽误自己一辈子且完全不自知。庆幸!庆幸!!但第一个 “发现” 更为重要,因为但凡哪怕只有过一次这样的经历,你就可以永久摆脱 “刚需幻觉” 的诅咒。
编程也是一样的,当你开始学编程的时候,被身边的人发现,他们中的大多数一定会问你:
> 你学它要干嘛啊?
无论你怎么回答,他们的反应应该都是差不多的:反正就是不理解你的投入是否合理。
然后,等你学会了编程,掌握了这个技能,你的 “发现” 肯定包括那两点:
> * 你根本离不开它;
> * 它其实真没多难……
哪怕很简单的技能都是如此。许多年前,我专门花一两周的时间练习键盘盲打,以及把输入法方案选择改为微软双拼方案…… 也有人揶揄过我:“我靠,你练这玩意干嘛?难道将来要当一辈子打字员?”
当然,我很早就练就了一定的自学能力,所以很早就有一些自学经验,所以我用不着事后 “发现”,而是 “直接就知道”,将来:
> * 我根本离不开它;
> * 它其实真没多难……
事实上呢?事实上就是如此。打字速度极快,直接导致了后来我成为多产作家。无法想象如果我是个打字速度极慢的人,我如何写书,如何写那么多书和那么多文章;以及,后来我又是如何于 2018 年 9 月的某一天开始在 72 小时之内完成《韭菜的自我修养》初稿…… 可问题在于,这个技能难吗?在我长大的年代里,它算个技能;在今天,全都是从小就用键盘的人的世界里,谁不会呢?
所以,当面对一项新技能的时候,“觉得并非刚需” 肯定是幻觉。因为一个技能到底是不是刚需,在掌握它之前是不知道的……
> 只有真正掌握了那个技能之后,那个技能才会 “变” 成刚需。
并且,
> 一旦掌握了某项技能,它只能是刚需。
这种幻觉非常坑人。
我们的大脑,有一种神奇的功能 —— “无论如何都会用已有信息拼成一个完整图像的功能”。
“无论如何” 的意思是说,不管那已有信息有多么凌乱、有多么残缺、有多么无意义、人脑竟然还是可以拼出一个完整的 “有意义” 的图像。
这一点也经常被人利用。在新闻学里,就有著名的 “Framing Effect”:
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/framing-effect.png?raw=true)
这个原理也经常被各路营销使用:
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/framing-bias.png?raw=true)
很容易想象,经常被这些东西误导的人,“脑力” 欠缺到什么程度 —— 这当然也是他们全无自学能力的结果之一。
而当我们被刚需幻觉所左右的时候,我们明显是使用 “片面的信息” 拼出了 “完整的意义”,而后被其误导,严重误导,且全然不自知 —— 最气人的是,误导我们的竟然不是别人,竟然是我们自己的大脑!刚需幻觉如此,时间幻觉、困难幻觉亦如是。后面会讲到的注意力漂移,也依然是这种情况,自己才是真正的元凶……
所以,在决定学任何东西的时候,最好不要去咨询身边的人 —— 除非你确定对方是高手,最好是自学高手。否则,你遇到的永远是怀疑、打击、甚至嘲笑。最令人遗憾、最令人无奈的是,那些人其实也不是坏人,他们其实也不是故意,他们只是被自己的 “刚需幻觉” 误导了而已,他们(竟然)以为所有人都和他们一样…… 然后,若是你(居然)听信了他们的话,那就实在是太惨了!
**要学,想学,那就自顾自去学吧,用不着征求别人的意见!**
优势策略
如何不被 “刚需幻觉” 所迷惑、所限制呢?
首先,基础策略是,要深刻理解这个现象及其解释:
> 对任何一项技能来说,**刚需是自学的结果**,而不是自学的原因。
用 “是否为刚需” 作为自己是否开始学习的理由,注定会吃亏的,因为如此做的下场就是肯定被 “刚需幻觉” 所迷惑、所限制。
而后,也是更为重要的一个策略:
> **做个自驱动的人**,而非被外部驱动的被动的人。
这基本上是个越早建立越好的习惯。绝大多数人一生都是被动者,终生只是被外界驱动而已。所以,他们会在某一时刻,“发现” 某个技能是刚需,然后再去学,但可惜却肯定的是,到那时候预算总是并不充裕,总是捉襟见肘。
最后一个很简单却很有效的策略是三个字,[之前提到过的](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.2.E.deliberate-thinking.md):
> **找活干**
有活干,真的很幸福。
影响下一代
想象一下有些父母是这样跟孩子对话的:
> * 爸爸(妈妈),你在干什么呀?
> * 我在学 \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_(请自行填空)
> * 那你学它干什么用啊?
> * 等我学会就知道了……
而后又过了段时间……
> * 孩子,过来看看!这是爸爸(妈妈)做的东西!
我猜,这样的孩子,会从小就自然而然地破解掉 “刚需幻觉”。
所谓的 “潜移默化”,所谓的 “耳闻目染”,其实是很简单的东西。
然而,效果呢?效果不会因为策略的简单或者容易而消减。通常的情况是,越是简单容易的策略,效用越是惊人。
所以,正确结论是这样的:
> **一切的技能都是刚需。**
虽然,这并不意味着你不需要选择。
然而,至于学还是不学,其实从来都不是从 “有没有用” 来判断的,真正有意义的判断依据只有一条:
> 有没有时间?
有时间就学呗!没时间就挤时间呗!学得不足够好怎么办?花更多时间呗……
-----
**脚注**
<a name='fn1'>[1]</a>:写这本书的时候,前后弄出来那么多 `.ipynb` 文件 —— 于是,到最后哪怕 “生成个目录” 这样看起来简单的活,若是会用正则表达式,就能几分钟完成;但若是不会,那就得逐一手工提取、排序、编辑…… 对我来说怎么可能不是**刚需**!
```python
import re
import os
files = [f for f in os.listdir('.') if os.path.isfile(f)]
files.sort()
for f in files:
if '.ipynb' in f:
with open(f, 'r', encoding = 'UTF-8') as file:
str = file.read()
pttn = r'"# (.*)"\n'
r = re.findall(pttn, str)
if len(r) > 0:
print(f'> - [{f.replace(".ipynb", "")}(**{r[0]}**)]({f})') # 生成 markdown
```
> - [01.preface(**前言**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/01.preface.md)
> - [02.proof-of-work(**如何证明你真的读过这本书?**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/02.proof-of-work.md)
> - [Part.1.A.better.teachyourself(**为什么一定要掌握自学能力?**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.A.better.teachyourself.md)
> - [Part.1.B.why.start.from.learning.coding(**为什么把编程当作自学的入口?**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.B.why.start.from.learning.coding.md)
> - [Part.1.C.must.learn.sth.only.by.reading(**只靠阅读习得新技能**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.C.must.learn.sth.only.by.reading.md)
> - [Part.1.D.preparation.for.reading(**开始阅读前的一些准备**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.D.preparation.for.reading.md)
> - [Part.1.E.1.entrance(**入口**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.E.1.entrance.md)
> - [Part.1.E.2.values-and-their-operators(**值及其相应的运算**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.E.2.values-and-their-operators.md)
> - [Part.1.E.3.controlflow(**流程控制**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.E.3.controlflow.md)
> - [Part.1.E.4.functions(**函数**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.E.4.functions.md)
> - [Part.1.E.5.strings(**字符串**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.E.5.strings.md)
> - [Part.1.E.6.containers(**数据容器**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.E.6.containers.md)
> - [Part.1.E.7.files(**文件**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.E.7.files.md)
> - [Part.1.F.deal-with-forward-references(**如何从容应对含有过多 “过早引用” 的知识?**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.F.deal-with-forward-references.md)
> - [Part.1.G.The-Python-Tutorial-local(**官方教程:The Python Tutorial**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.G.The-Python-Tutorial-local.md)
> - [Part.2.A.clumsy-and-patience(**笨拙与耐心**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.2.A.clumsy-and-patience.md)
> - [Part.2.B.deliberate-practicing(**刻意练习**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.2.B.deliberate-practicing.md)
> - [Part.2.C.why-start-from-writing-functions(**为什么从函数开始?**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.2.C.why-start-from-writing-functions.md)
> - [Part.2.D.1-args(**关于参数(上)**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.2.D.1-args.md)
> - [Part.2.D.2-aargs(**关于参数(下)**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.2.D.2-aargs.md)
> - [Part.2.D.3-lambda(**化名与匿名**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.2.D.3-lambda.md)
> - [Part.2.D.4-recursion(**递归函数**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.2.D.4-recursion.md)
> - [Part.2.D.5-docstrings(**函数的文档**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.2.D.5-docstrings.md)
> - [Part.2.D.6-modules(**保存到文件的函数**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.2.D.6-modules.md)
> - [Part.2.D.7-tdd(**测试驱动的开发**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.2.D.7-tdd.md)
> - [Part.2.D.8-main(**可执行的 Python 文件**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.2.D.8-main.md)
> - [Part.2.E.deliberate-thinking(**刻意思考**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.2.E.deliberate-thinking.md)
> - [Part.3.A.conquering-difficulties(**战胜难点**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.A.conquering-difficulties.md)
> - [Part.3.B.1.classes-1(**类 —— 面向对象编程**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.B.1.classes-1.md)
> - [Part.3.B.2.classes-2(**类 —— Python 的实现**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.B.2.classes-2.md)
> - [Part.3.B.3.decorator-iterator-generator(**函数工具**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.B.3.decorator-iterator-generator.md)
> - [Part.3.B.4.regex(**正则表达式**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.B.4.regex.md)
> - [Part.3.B.5.bnf-ebnf-pebnf(**BNF 以及 EBNF**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.B.5.bnf-ebnf-pebnf.md)
> - [Part.3.C.breaking-good-and-bad(**拆解**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.C.breaking-good-and-bad.md)
> - [Part.3.D.indispensable-illusion(**刚需幻觉**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.D.indispensable-illusion.md)
> - [Part.3.E.to-be-thorough(**全面 —— 自学的境界**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.E.to-be-thorough.md)
> - [Part.3.F.social-selfteaching(**自学者的社交**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.F.social-selfteaching.md)
> - [Part.3.G.the-golden-age-and-google(**这是自学者的黄金时代**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.G.the-golden-age-and-google.md)
> - [Part.3.H.prevent-focus-drifting(**避免注意力漂移**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.H.prevent-focus-drifting.md)
> - [Q.good-communiation(**如何成为优秀沟通者**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Q.good-communiation.md)
> - [R.finale(**自学者的终点**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/R.finale.md)
> - [S.whats-next(**下一步干什么?**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/S.whats-next.md)
> - [T-appendix.editor.vscode(**Visual Studio Code 的安装与配置**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/T-appendix.editor.vscode.md)
> - [T-appendix.git-introduction(**Git 简介**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/T-appendix.git-introduction.md)
> - [T-appendix.jupyter-installation-and-setup(**Jupyterlab 的安装与配置**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/T-appendix.jupyter-installation-and-setup.md)
> - [T-appendix.symbols(**这些符号都代表什么?**)](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/T-appendix.symbols.md)
<a href='#fn1b'><small>↑Back to Content↑</small></a>
全面 —— 自学的境界
之前提到过那些 “貌似一出手就已然是高手” 的人,也为此做过一番解释:
> 他们的特点就是善于**刻意练习**……
为了真正做到刻意练习,更重要的是需要不断地进行**刻意思考** —— 刻意思考自己究竟应该在哪些地方必须刻意练习?
之前也说过,人和人非常不同,于是,需要刻意练习的地方也各不相同。
不过,倒是有一个方面,所有的自学者都必须刻意练习 —— 这是谁都逃不过的地方:
> **全面**
那些 “貌似一出手就已然是高手” 的人就是在这一方面超越了绝大多数人 —— 在每个层面上,他们都学习得更全面,练习得更全面,使用得更全面,在使用此技能去创造的时候,思考得也就自然更为全面。于是,就产生了 “全面碾压” 的效果。
然而,这是很难被人发现的 “秘密”,因为 “全面” 这个事实,只存在于这些高人的大脑之中,很难被展示出来…… 而他们不会想到这是个什么 “秘密” —— 因为他们一直就是这么做的,他们会误以为所有人都是这么做的。
小时候,我经常看到父亲备课到深夜。他手中的教科书,每一页的页边都密密麻麻地写着各种注释,实在没地方写了,就在那个地方插上一张纸,于是能写得更多…… 到最后,他的那本书,要比别人同样的书看起来厚很多。
许多年后,我竟然成了老师,于是,我就备课。我备课的方法自然是 “拷贝” 过来的,我父亲怎么做的,我见过,于是我也那么做。到最后,都到了这个地步:只有那书已经成了别人的两倍厚度心里才踏实。
又过了一段时间,在一个内部分享会中,我听到一位老师的说法,他问我:
> …… 李老师啊,您已经讲了这么久了,都熟到干脆不用备课的地步了吧?
我愣了一下,讲课前不用备课这事根本就不在我的想象范围之内啊!我父亲讲了那么多年的课,不还是经常备课到深夜嘛?我也一样做了,总是觉得 “还是有很多可以补充的地方” 啊!
然而,这个小插曲提醒了我一个现象:
> 我会那么做,我就会误以为所有人都会那么做……
我猜,那些 “貌似一出手就已然是高手” 的人,也一样,他们从未觉得这是什么 “秘密”,他就是那么做的,他们很久以来就是那么做的,他们误以为所有人都是那么做的。
从另外一方面,外人更希望他们拥有的是个 “秘密”。于是,因为自己并不知道那个 “秘密”,所以,自己做不到他们那样 —— 这样看起来就合理了,自己的心里也够舒服了,毕竟看起来理所应当了么。
把自学当作一门手艺,把所有的技能也都当作一门手艺,那就相对容易理解了:
> **全面**,是掌握一门手艺的基本。
为了全面,当然要靠时间。所以,关于 “混与不混”,我们有了更深刻却又更朴素的认识:
> 所谓的不混时间,无非就是**刻意练习**、**追求全面**。
也正是这个原因,几乎所有自学高手都懂这个道理:
> **绝对不能只靠一本书**
有个特别有趣的现象,我觉得绝大多数人平日里都挺大手大脚的,都挺舍不得对自己过分苛刻的,但一到买书这件事上,绝大多数人真的很节俭,真的很苛刻 —— 对待越严肃的知识越是如此。倒是在买本小说啊或者买张电影票的时候,基本不用过脑子。
他们好像完全不知道自己正在疯狂地虐待自己…… 的大脑。
对自己的胃好一点,我绝对认同 —— 因为我自己就是个吃货。
可是,在很长一段时间里,我完全不能理解人们为什么不由自主地对自己的大脑不好,不仅是不好,还是格外地不好,甚至干脆是虐待。
观察学生多了,也就慢慢理解了。
绝大多数人事实上从来没有习得过自学能力,他们终生都在被指导、被引领。而在校教育少则九年,多则十几年二十年,他们体验过太多 “不过尔尔” 的学习过程。他们肯定不是没花过钱,九年义务教育的过程中,就花了父母很多钱,后面若是上了大学,花钱更多…… 花过那么多钱,却总是没什么收获,在他们的经验中,“这次我应该小心点” 是再自然不过的事。
“第一次突破” 很重要。
如果一个人有过一次只通过阅读书籍即获得一项新技能的体验,那么,他们内心深处(更准的说法是大脑的底层操作系统)的那个成本计算方法就会发生改变,心里想的更可能是:
> * 这肯定是有用的,一旦学会,收益可不是几十块钱的书价或几百块课价那么一点点……
> * 至于是否能学会,主要看我投入的时间精力预算有多少……
我身边有很多自学能力非常强的人。
这些人买书的方式都是一样的,一旦他们决定学习什么技能的时候,第一个想到的是去买书,而不是去找人。他们之前体验过,他们就是很清楚:
> * 首先,书里什么都有;
> * 其次,仅靠阅读在大多数情况下绝对够了……
更为不一样的是,他们一定会买回来一大堆书 —— 而不是四处去问,“关于 xx 的哪一本书最好啊?”
在他们眼里,书是成本最低的东西,比起最终的收益来讲更是不值一提。
更为重要的是,**一本书绝对不够** —— 无论是谁写的,无论那个作者多么著名,影响力有多大…… 因为,书也好、教程也罢,这种东西就是有 “篇幅限制” 的。更为关键的是,每个作者都有不一样的视角、不一样的出发点和不一样的讲解方式、组织方法。
比如,我这本就跟别人写的很不一样。我的出发点是把编程当作一个自学的例子,重点在于学会如何自学,并且通过实践真的习得一个起初你觉得不是刚需,学会之后发现干脆离不开的、不可或缺的技能。这本书的内容组织方式也不一样 —— 反正你现在已经知道了。
另外,这本书的目标里有更重要的另外一个:“让你有能力靠自己能够理解所有的官方文档” —— 书里不用讲官方标准库里的每个模块、每个函数究竟如何使用,因为那些在官方文档里定义得非常清楚……
而其他人写的呢?比如 [Think Python](http://greenteapress.com/wp/think-python-2e/),比如 [A Bite of Python](https://python.swaroopch.com/),再比如 [Dive into Python](https://linux.die.net/diveintopython/html/)<a href='#fn1' name='fn1b'><sup>[1]</sup></a>,以及网上很多很多免费的 Python 教程都写得很好呢!
没有经验的人不懂而已。当你搞明白了一本书,后面再多读哪怕很多本的时间精力成本都是很低的,但每多读一本,都能让你在这个话题中变得**更为完整**。
针对同一个话题读很多本书的最常见体验就是:
> - 嗯?这个点很好玩!这个角度有意思!
> - 看看比比前面几个作者怎么论述的呢?
> - 嗯?!怎么我看过却竟然没注意到呢!
这最后一条真的是很令人恼火却又享受的体验。它令你恼火,是因为你竟然错过;它令你享受,是因为虽然错过却竟然还有弥补的机会!
总有一天你会明白的,一切的 “学会” 和 “学好” 之间的差异,无非是**全面程度**的差异。
于是,翻译过来,“学好” 竟然如此简单:
> **多读几本书。狠一点,就是多读很多本书。**
到最后,这种习惯会慢慢延伸到生活中。比如,我在遇到好歌的时候,总是想尽一切办法找到那首歌的很多版本,首唱者可能有很多版本,录音版、现场版,不同年份的版本等等;还有很多翻唱…… 看电影也一样,若是有翻拍版本,一定会找过来看 —— 不同国家的翻拍版本对比起来特别好玩。
甚至,到最后,你做东西的时候都会想着顺手多做几个版本。我这本 “书”,就肯定会有印刷版、电子版…… 到最后还会有个产品版 —— 这基本上是目前尚无其它作者做到的事情。
提高对所学知识技能的 “全面程度”,有个最狠的方法 —— 再次说出来不惊人,但实际效果惊到爆:
> **教是最好的学习方法。**
这真的不是我的总结,人类很早就意识到这个现象了罢?
孔老夫子在《礼记・学记》里就 “曰” 过:
> “学然后知不足,教然后知困。知不足,然后能自反也;知困,然后能自强也。故曰:教学相长也。”
到了孔子三十二代孙,孔颖达,解读《兑命》中所提 “学学半” 时,说到:
> “学学半者,上学为教,下学者谓习也……”
许嘉璐先生<a href='#fn2' name='fn2b'><sup>[2]</sup></a>有一本书,《未央续集》,提到这段解读的时候讲了个自己的例子:
> “我当了五十年的教师,经常遇到这种情况:
>
> > 备好课了,上讲台了,讲着讲着,突然发现有的地方疏漏了,某个字的读音没有查,文章前后的逻辑没有理清楚,下完课回去补救,下次就不会出现同样的情况了,这就是教学相长。”
所以,别说老师了,学生更是如此。
我经常讲我所观察到的班里的第一和第二的区别 —— 因为这是很好的例子。
第一总是很开放,乐于分享,别人问他问题,他会花时间耐心解答;第二总是很保守,不愿分享,不愿把时间 “浪费” 在帮助他人身上…… 注意,在 “浪费” 这个词上我加了引号 —— 这是有原因的。
我的观察是,这不是现象,这是原因:
> 第一之所以比第二强,更可能是因为他开放、乐于分享,才成了第一。
而不是因为他是第一,所以才开放,所以才乐于分享。
因为到最后,你会发现,第一并没有因为时间被占用而成绩退步,反而成绩更好。这是因为他总是在帮助其他同学的过程里,看到了自己也要避免的错误、发现了其它的解题思路、巩固了自己的知识点,所以他在社交的过程中学到了更多,同时还收获了同学们的友谊 —— 换言之,通过分享,通过反复讲解,他自己的 “全面程度” 得到了最快的提高。
而第二呢?第二其实有可能比第一更聪明呢 —— 他可是全靠自己走到那个地步的!可是,他没有用最狠的方式提高自己的全面程度,虽然排名第二,可他其实只不过是一个 “下学” 者,于是,他很吃力的 —— 虽然他实际上很聪明…… 于是,在这种感受下,他怎么肯愿意把那么吃力才获得的东西分享出去呢?
这真是个有趣且意味深长的现象。
另外一个有趣的现象是,“下学” 者永远等待 “上学” 者整理好的东西。之前在《[如何从容应对含有过多 “前置引用” 的知识?](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.F.deal-with-forward-references.md)》提到过一个对应策略:
> 尽快开始整理归纳总结
同时给出了建议:
> 一定要自己动手去做……
不仅如此,还描述了个我自己的例子,通过 “自己动手整理” 才发现自己之前竟然完全理解错了。
我这方面运气非常好,因为父母全都是大学教师,从小父母就鼓励我帮同学解答问题,这让我不知不觉在很早很早就开始了 “上学” 的阶段。
这一次写这本 “书” 的过程中,同样的 “奇迹” 再次发生在我身上。
说实话,正则表达式我一直没有完全掌握 —— 虽然偶尔用用,也都是边查边用。实在解决不了,就算了…… 现在回头想想,多少就是因为 “[仅凭感觉,并没觉得那绝对是刚需](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.D.indispensable-illusion.md)” <a href='#fn3' name='fn3b'><sup>[3]</sup></a> —— 当然,真正会了之后,马上开始时时刻刻都有可能使用,离开它简直活不下去 —— 写这书的后半程,有大量的重新组织的需要,很多文字替换,若是没有正则表达式,就干脆没法干……
我是如何完全掌握正则表达式的呢?就是因为写这本书。既然是写书,当然害怕自己在不经意中出错,此为其一。更为重要的是,必须先完整掌握之后才能有诸如 “为读者提供更好一点的理解起点”,“理解起来相对更简单直接的组织结构”,以及 “挖掘必须习得它的真正原因以便鼓励读者” 之类的畅销书卖点 —— 对作者来说,有什么比销量更重要的呢?
写一本好书,对我来讲,这个需求太刚了,刚到好像是**钛钢**的地步。
于是,本来就习惯于同一个话题多读好多本书的我,读了更多的书,翻了更多的教程,官方文档翻了更多遍,做了更多的笔记,每一章反复废掉原稿再次重写了很多遍…… 在这样的刺激下,“全面程度” 若是没有极速提高,那才怪了呢!
还有啊,我的英语,也是这么搞出来的。
所以,很多的时候,我这个人并不是 “好为人师”,细想想,貌似 “好为己师” 更为恰当一点。写书也好讲课也罢,其实自己的进步是最大的。
若是我没有在新东方教书七年,我连当前这个半吊子英文水准都不会有…… 为了在讲台上不犯错,多多少少都得多费一些功夫吧?我的经验和许嘉璐先生是一样的,无论备课多努力,后面还是会有纰漏…… 可是,这若只是做个 “下学” 者,岂不更惨?
哦,对了,其实所有的读者,都可以用这个简单的方法影响下一代:
> 有同学问,你就一定要耐心讲讲 —— **对自己有好处。**
当然,最直接的方法是把自己变成 “上学” 者,保持开放,乐于分享,而孩子只需通过 “耳闻目染” 就可以了。
-----
**脚注**
<a name='fn1'>[1]</a>:一点八卦:Dive into Python 的作者是 [Mark Pilgrim](https://en.wikipedia.org/wiki/Mark_Pilgrim);他是互联网上最著名的自绝于信息世界([infosuicide](https://www.urbandictionary.com/define.php?term=infosuicide))的三人之一。另外两位分别是比特币的作者 [Satoshi Nakamoto](https://en.wikipedia.org/wiki/Satoshi_Nakamoto),和 [Why the lucky stiff](https://whytheluckystiff.net/) 的作者 [Jonathan Gillette](https://en.wikipedia.org/wiki/Why_the_lucky_stiff)。
<a href='#fn1b'><small>↑Back to Content↑</small></a>
<a name='fn2'>[2]</a>:许嘉璐,1998 年至 2000 年全国人大常委会副委员长,民进中央主席,国家语言文字工作委员会主任。
<a href='#fn2b'><small>↑Back to Content↑</small></a>
<a name='fn3'>[3]</a>:写完这一段,给霍炬看,他当场嘲笑我,“哈!当年我就说,你应该学学 Vim,是不是到现在你都没学?” 我无言以对,因为真的就没学…… 然后,我想了想,回复他说,“好吧,我决定写一个 Vim 教程出来,嗯。”
<a href='#fn3b'><small>↑Back to Content↑</small></a>
自学者的社交
很多人有莫名其妙的误解,以为 “自学”(self-teaching)就一定是 “自己独自学”(solo-learning),殊不知,自学也需要社交。也有另外一些人,因为 “专心” 到一定程度,觉得社交很累,所以开始抵触一切社交。这些都不是全面的看法。
事实上,在任何领域,社交都是必须的,只不过,很多人没有建立、打磨过自己的社交原则,所以才被各种无效社交所累。就算讨厌,讨厌的也不应该是社交,而是无效社交。
在自学的任何一个阶段,学、练、用、造,社交都可能存在。
哪怕是在最枯燥,看起来最不需要社交的 “练” 的阶段,社交也会起很大的作用 —— 在自己累了的时候,看到有人还在练,看到很多人都在练,看到很多人其实也挺累的但还在练…… 这些都是让自己感觉没那么费劲的好办法。
实际上,在最初 “学” 的阶段,社交也是极为重要的。
生活中,你遇到过这样的现象没有:“看见别人打针,自己先疼得受不了……” 这是因为我们的大脑中有一种神经元,叫做**镜像神经元**(Mirror Neuron),它会让我们 “感同身受”,当我们看到另外一个人正在做什么的时候,镜像神经元会尽力给我们足够的刺激,让我们 “体验” 那个人的感受。以前人们不知道为什么哈欠竟然会 “传染”,现在科学家们很清楚了 —— 那就是镜像神经元在起作用。
镜像神经元的存在,使得我们有模仿能力、有通感能力、有同情心、有同理心…… 这也是为什么人类天然有社交需求的重要原因,因为我们的大脑皮层上都有很多的镜像神经元。
一般来说,物品、书籍之类非人的东西,都不大可能激活镜像神经元。只有看到人的时候,镜像神经元才会被激发。所以,你送给小朋友一把吉他,他不会有什么兴趣的。可若是你在弹琴的时候被他看见,他的镜像神经元就会因为你的行为而被触发,进而对弹奏吉他感兴趣 —— 注意,不是对吉他本身感兴趣。若是你在弹琴的时候,带着某种能够打动他的情绪,那他更容易被影响,因为情绪更能激发镜像神经元。也就是说,一切的学习起初都基于模仿,一切的模仿,都源自于看到真人的行为 —— 哪怕是在电影里看到,虽然只不过是影像而已,并非真人,但毕竟是真人的影像。
所以,无论学什么技能,都要找到用那种技能的人,这样我们的镜像神经元才可能更容易被激发,学习效果才会好。若是能找到热爱那项技能,乃至于一使用那项技能就很开心(最好的情绪之一)的人,那就更好了。激情这东西,是少数幸运儿才长期持有的东西,大多数人小时候挺多,过了十五六岁之后就开始有意无意磨灭了激情,且并不自知。
之前提到,
> 当我们看到另外一个人正在做什么的时候,镜像神经元会**尽力**给我们足够的刺激,让我们 “体验” 那个人的感受。
这句话里有个词很重要,“尽力”。因为镜像神经元只能调用我们大脑里已有的信息去模拟对方的感受,所以,它最多也就是 “尽力”,无法做到 “确保正确”。今天的糖尿病患者使用的皮下注射针头,已经可以做到很细,细到让使用者 “无感” 的地步,所以,当一个糖尿病患者给自己注射胰岛素的时候,他自己并不觉得疼,可是看的人却能 “疼” 到紧皱眉头的地步,为什么?因为旁观者的大脑里没有实际用那么细的针头注射胰岛素的经验,所以镜像神经元在旁观者 “感同身受” 时所调用的,其实是过往旁观者自己打针的体验 —— 被很粗的针头做静脉注射的痛苦体验。
所以,很多人误以为他们眼里的成功者靠的是 “坚持”、靠的是 “毅力”,这完全是自己的镜像神经元 “尽力” 的结果,是 “调用自己过往经验去**感同身受**的结果”…… 事实上呢?那些 “成功者” 其实并不在意成功,因为到死之前成长不应该也不可能结束,因为那是他们的生活方式,学习、进步、探索、迂回,甚至折腾、挫败和迷茫,都是他们生活中必不可少的内容,这是最初不自觉的选择,谈不上什么 “坚持”,谈不上什么 “毅力”…… 说实话,对他们来说,不让折腾才真痛苦呢,不学习才需要坚持和毅力呢!
再进一步,这也是为什么要选择朋友的原因。人与人之间有很大的差异,最大的差异来自于性格养成,大多数人会沦为表现型人格,只有少数人才会在不断调整中保持、呵护、进一步培养 “进取型” 人格。他们自然而然地更为乐观,更有耐心,更有承受力,更有战斗力,更能生产更能体验学习与进步的乐趣。与这样的人在一起,学习会更容易 —— 只因为镜像神经元会更容易地被正确激发。说清楚了,道理其实挺简单的。
有一次朋友跟我聊起他苦于没办法培养自己孩子的正经兴趣爱好…… 我说,其实很简单,只不过是你方法错了。你不用告诉孩子 “应该学什么,应该对什么感兴趣”,而是,想尽一切办法让孩子见识到拥有那个技能的,令他产生**羡慕情绪**的人 —— 只要孩子羡慕那个人,他就自然而然地有 “我也想这样” 的想法,到最后,谁都拦不住。这就是镜像神经元的力量。进而,所谓的社交,还真不一定是非要跟人说话、聊天…… 见识到,也是社交的最主要组成部分。
你看,谁说社交不重要?
进而,想要把一门手艺搞到真正 “精湛” 的地步,最有效的方法就是尽早进入 “造” 的阶段 —— 所谓的 “造”,就是不断**创造**的 “造”。
自学这门手艺,很简单,就是不断地学:
```python
def teach_yourself(anything):
while not create():
learn()
practice()
return teach_yourself(another)
teach_yourself(coding)
```
学上几个,自然就很精湛。而其它的用自学这门手艺习得的手艺,基本上都可以用 “是否做出了像样的作品” 作为检验自己的那门手艺是否达到了精湛的衡量指标。
硅谷有一家著名的孵化器,叫 Y-Combinator,现在的掌门人是个很年轻的人,Samuel H. Altman。他在那篇著名的文章《[Advice for ambitious 19 year olds](https://blog.samaltman.com/advice-for-ambitious-19-year-olds)》中有一个精彩的建议:
> No matter what you choose, build stuff and be around smart people. <br />
> 无论你选择了什么,都要造出东西来,要与聪明人打交道。
当然,对于 “聪明人” 这个概念,我和 Sam 的看法并不一致。在我看来,有好作品的人都很聪明,但还是那句话,那不是天分和智商,那分明是有效积累。
我个人最看重的个人品质之一,就是**有没有像样的作品**。
很少有人有像样的作品。人群中只有少数人最终能拿出完整的作品 —— 人与人之间的差异是如此之大,乃至于少数人有作品,更少数人有好的作品,只极少数极少数人才可能做出传世的作品;而与此同时,绝大多数人(万分之九千九百九十九的人)一辈子都没有像样的作品,他们连一篇作文都写不明白。于是,与有像样作品的人打交道,总是非常值得。
并且,跟他们打交道也不费劲,都是思考非常通透的人,通常沟通能力极强。哪怕沟通起来貌似费劲的那一小部分,事实上也不是难以沟通,那只不过是人家简单朴实而已。
我甚至经常建议我的合伙人们,在招人的时候,把这一点当作最靠谱的判断方式。少废话,少吹牛逼,给我看看你的作品。这个原则可以一下子过滤掉所有的不合格者。另外一个很自然的现象是,如果一个人能做出像样的东西来,那么他身边的聪明人密度无论如何都会比其他人的高出很多。
地球上有效社交密度最高的地方,是 [Github](https://github.com)。有些程序员们常开玩笑,说 Github 是全球最大的同性社交网站,事实上,他们不知道,女性程序员的比例正在逐步提高,而且女性在科学上,从来就没有屈居二线过<a href='#fn1' name='fn1b'><sup>[1]</sup></a>。
在 Github 上,找到自己感兴趣的项目,而后为那项目贡献一己之力,用自己的工作赢得社区的认同…… 这就是 Github 上的社交方式。若是自己做了有意义的项目,就会有更多人关注;若是那项目对很多人有用,那就不仅有很多人关注,更有很多人会像当初的你一样为这个项目做贡献…… 这就是程序员们的**有效社交**。
Github 能成为地球上最大的有效社交网络,没毛病,因为**用作品社交肯定是最高效的。**
所以,无论学什么,都要想尽一切办法尽快做出自己的作品。做一个产品出来的过程中,会磨练另外一项自学者不可或缺的能力和素质:
> **完整**
与之前提到的另外一项加起来,就构成了自学者的最基本素养:
> * 学就学得**全面**;
> * 做就做得**完整**。
无论多小的作品,都会让创作者感受到 “单一技能的必然无效性” —— 你试试就知道了。哪怕你想做个静态网站,你都会发现,仅仅学会 html/css 是不够的,因为部署到远端服务器上的时候,你无论如何都得学学 Linux 基本操作…… 而已然具备了自学者基本素养的你,自然会想办法 “全面掌握”,而不是糊弄一下而已。
更为重要的是,一旦你开始创作作品,你更大的 “发现” 就是,你肯定需要很多 “之前看起来并不相干的知识与技能”,而非 “只靠专业就够了”……
还是拿我出第一本书为例。那之前我没有写过书,若是出版了书放在书店,没有人知道李笑来是谁…… 于是,只有内容本身,并不保证那书能卖出去。除了把内容写出来之外,我必然要去学习很多之前完全没碰过的东西,比如 “如何才能做到系统持续地修订内容”;又比如,“如何与出版社编辑正常沟通”;再比如,“如何取一个好书名”…… 一个赛一个地 “与专业无关”。
所以,“做得完整”,从来都不是容易的事情。
从这个角度去理解,你就会明白那些高明的手艺人为什么总是做小东西 —— 那是因为在追求完整的过程中,你必然会发现,越小越容易完整。这也是为什么庸人总是好高骛远,因为他们不顾完整,所以就可以妄图建造海市蜃楼。
手艺人不怕做的事小。而且,“小” 无所谓,“完整” 才是关键。
有作品和没作品的人理解能力也不一样。做过作品的人,看到类似 MoSCoW Method 的做事原则,瞬间就能有所感悟,而没有作品的人,却不见得有一样的感受。
> 顺带给你看个 Wikipedia 上的链接列表,在编程领域里,有无数可以借鉴到生活中的哲学、方法论:
> - [If it ain't broke, don't fix it](https://en.wikipedia.org/wiki/If_it_ain%27t_broke,_don%27t_fix_it)
> - [KISS principle](https://en.wikipedia.org/wiki/KISS_principle)
> - [Don't repeat yourself](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)
> - [Feature creep](https://en.wikipedia.org/wiki/Feature_creep)
> - [List of software development philosophies](https://en.wikipedia.org/wiki/List_of_software_development_philosophies)
> - [Minimum viable product](https://en.wikipedia.org/wiki/Minimum_viable_product)
> - [MoSCoW method](https://en.wikipedia.org/wiki/MoSCoW_method)
> - [Overengineering](https://en.wikipedia.org/wiki/Overengineering)
> - [Worse is better](https://en.wikipedia.org/wiki/Worse_is_better)
> - [S.O.L.I.D.](https://en.wikipedia.org/wiki/SOLID)
> - [Unix philosophy](https://en.wikipedia.org/wiki/Unix_philosophy)
给自己足够长的时间去学;在充足 “预算” 之下耐心地练;不断找活干,以用代练;然后,最重要的是,一定要尽快尝试着做出属于自己的**完整**作品,无论大小。
只有这样,你才是个值得被交往的人。
-----
**脚注**
<a name='fn1'>[1]</a>:NPR:[Most Beautiful Woman' By Day, Inventor By Night](https://www.npr.org/2011/11/27/142664182/most-beautiful-woman-by-day-inventor-by-night)
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/Lamarr_patent.png?raw=true)
上图是保存在美国专利局的一个存档文件(US Patent 2,292,387, Aug 11, 1942)截图。这项专利的发明者是 [Hedy Lamarr](https://en.wikipedia.org/wiki/Hedy_Lamarr),人家长得是这样的:
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/hedy-lamarr-hero-getty.jpg?raw=true)
Hedy Lamarr 是好莱坞最知名的演员之一,并且,她也是自学高手:
> Although Lamarr had no formal training and was primarily self-taught, she worked in her spare time on various hobbies and inventions, which included an improved traffic stoplight and a tablet that would dissolve in water to create a carbonated drink. The beverage was unsuccessful; Lamarr herself said it tasted like Alka-Seltzer.
<a href='#fn1b'><small>↑Back to Content↑</small></a>
这是自学者的黄金时代
历史上,自学者从未像今天这样幸福。
以前不是这样的。比如,几乎每个中国人从小就听说过无数次那种 “为了拜师学艺一跪就几天甚至几个月也不一定被收下” 的故事。
在古代,拜师学艺的确是很难的事情。首先,真正的好老师确实难寻;其次,高手也没空当老师;再次,就算是肯收徒授艺的老师也总是时间精力极其有限…… 更为重要的是,那时候想自学也真不行 —— 根本就没有什么称得上是文献的东西可供阅读或检索,很多重要信息甚至干脆只存在于某些人的脑中,就算它们被落实成了文献,也相当有限,且散落深藏在各处 —— 就算凑齐了,也没有 [Google](https://www.google.com/ncr)!
对,最关键的是那时候没有 Google……
今天的互联网,已经不再是二十几年前刚出现的那样 “激进而简陋” 的东西了。经过多年的发展,互联网上的内容已经构成了真正意义上的 “全球唯一通用图书馆” —— 而针对它可用的检索工具中最好的,当然是 Google。
于是,今天,自学者在真正的意义上身处于一个黄金时代 —— **没有什么是不能自学的**。注意措辞,在这句话前面甚至根本不用加上 “几乎” 这样的限定以示准确 —— 你想学什么,就能学什么,而不是 “只有先拜师才能开始学艺”。
今天的你,想学什么就去问 Google;在学习中遇到什么问题,就直接问 Google —— 直接问它,通常总是比向某个人提问有效率得多。Google 就是这样,越用越离不开它……
其实很多人并不真的懂如何用好 Google 的,可是连这个它也能解决,因为你可以直接问它:
> [How to use google effectively](https://www.google.com/search?q=How+to+use+google+effectively)
经过多年的发展,Google 的使用体验越来越好,2019 年的今天,你搜索以上语句返回的页面里,Google 甚至在众多搜索结果中选了一条它 “认为” 是 “最佳” 的搜索结果:
[〔图:google-how-effectively〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/google-how-effectively.png?raw=true)
[lifehack.com 上的这篇文章](https://www.lifehack.org/articles/technology/20-tips-use-google-search-efficiently.html)也的的确确值得细读 —— 读过且真正理解之后,不夸张地讲,你的 “搜索技能” 已经足够灭掉整个人类当前活跃群体中的 99% 了…… 这个说法真的完全没有夸张,绝大多数人就是不会在搜索的过程中使用那些符号的,比如 `-` `*` `~` `@` `#` `:` `"` `..` —— 还有很多其它技巧…… 话说,你在 Google 上用过 `Time *place*` 吗?
已经掌握了正则表达式以及 glob 的你,学习一下如何使用这种符号,实在是太小菜一碟了 —— 然而,这么简单的东西所能带来的未来收益是非常惊人的,不信走着瞧。
可实际上,还是得应用我们之前说过的原则:
> 首选查询,肯定是官方文档。
这么做的重要理由之一,是为了让你少受 “二手知识” 的蒙蔽和误导。这里有一个绝佳的例子让你理解二手知识的局限性:[我写过的一本 “书”,发布在网上](https://github.com/xiaolai/ji) —— 这本 “书” 的另外一个 “神” 之处,在于它能让你 “顿悟” 阅读的力量,不管你是男生还是女生…… 若是这个链接错过了,你竟然会错过整个生活!
Google Search 的官方文档在这里:
> https://support.google.com/websearch
Google 还有更为强大的工具给你使用,叫做 Google Custom Search,官方文档在这里:
> https://support.google.com/customsearch/
对编程工作来说,Google 当然格外地更为重要 —— 互联网上积累的最多最专业的信息,当然是计算机相关信息。所以,当你遇到什么错误提示的时候,不仅要问 Google,还要优先问问 [Stackoverflow](https://stackoverflow.com) —— 连 Google 自己都这么干。在 [colab.research.google.com](https://colab.research.google.com)(Google 为它的 TensorFlow 服务搭建的 Jupyterlab 环境)上,如果你运行什么代码出错了的话,那么,出错信息下面会出现一个按钮,上面写着:`SEARCH STACK OVERFLOW`,点击它就直接给你 [Stackoverflow](https://stackoverflow.com) 上的搜索结果…… 真够意思!
[〔图:google-stackoverflow〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/google-stackoverflow.png?raw=true)
[Google](https://google.com)、[Stackoverflow](https://stackoverflow.com)、[Wikipedia](https://en.wikipedia.org)、[Youtube](https://youtube.com),这些都是你经常要去搜索的好地方。
二十年前,Google 刚出现的时候,谁能想象它今天这个样子呢?以下是 1998 年 11 月 11 日 http://google.com 这个网址的截图:
[〔图:google-homepage-1998〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/google-homepage-1998.png?raw=true)
当时,网站还没有正式上线,第一个链接是一个原型设计,用一个二级域名发布在斯坦福的网站上:http://google.stanford.edu/:
[〔图:google-prototype-1998〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/google-prototype-1998.png?raw=true)
那个时候,Google 还要向外人强调一下,他们已经有 2500 万页面可供检索!
事实上,到了 2008 年,Google 公告称,可供检索页面已经超过一万亿(One Trillion),到了 2016 年年底,这个数字已经超过 130 万亿……
换个角度看,这个数字同时也是互联网上信息的积累 —— 世界上再没有比互联网更大更全的 “书” 了。并且,由于 Google 的存在,互联网这本大书,是可检索的!
于是,**有事先问 Google** 就成了自学者的必备修养。
> 能 Google 出答案的问题,就不需要去麻烦别人。
这也是一个自学者的基本素养。
偶尔,也确实会遇到 Google 了很久,就是没有找到答案的情况…… 这样的时候,你可能需要想办法 “问人” 了。然而,最靠谱的通常并不见得是 “身边的人”,而是互联网上各种垂直社区里的其他用户……
向人问,也是有学问的 —— 很多人张口就问,结果呢?结果没人理。为什么呢?
作为一个有素养的自学者,有一篇文章必须精读:
> [How To Ask Questions The Smart Way](https://github.com/selfteaching/How-To-Ask-Questions-The-Smart-Way)
这是大神 Eric S. Raymond 和 Rick Moen 于 2001 年在网上发布的文章,被人们奉为经典;迄今为止经历了很多次的修订,最后一次是在 2014 年,`Revision 3.10` —— 前后被翻译成了许多种语言。
**不认真使用 Google,你就错过了整个人类历史上自学者最黄金的时代。**
避免注意力漂移
**注意力漂移**,是我杜撰的一个词,用来作为 “注意力集中” 的反义词 —— 因为更多的时候,我们并不是 “注意力不集中”,而是…… 而是更令人恼火的一个现象:
> “注意力所集中的焦点总是不断被自己偷偷换掉……”
比如,你本来只不过就想着去 Google 一个编程错误信息的解读,结果不知道怎么就 “注意到” 了另外一个东西,比如,“某编辑器的皮肤”,然后你就 “顺手” 把它下载下来,然后 “很自然地顺手把它装上”,而后又看了看,觉得有点必要于是 “顺手做了点定制”…… 然后欣赏了一会儿,并自我得意一番之后 “突然发现” 自己还没有解决两小时之前要解决的问题!
说这种现象 “令人恼火”,是因为那注意力所集中的焦点,是被**自己**偷偷换掉的!
好奇心越重的人,越是容易被注意力漂移所拖累。
好奇心是好东西,而且是必须认真呵护的东西 —— 几乎最重要、最强劲的自学动力,都混合着好奇心出现并持续。
在我成长的那个年代里,很多孩子本来是可以成为自学专家的,结果,99.99% 都被父母给毁了 —— 而且还是不经意地毁的。那些父母毁掉自己孩子的方法简单直接又粗暴、且毫不犹豫。
刚开始图好玩,小孩子问啥都回答…… 当然,最初的时候,小孩子问的问题也很容易回答;就算不容易,也很容易糊弄过去。没多久,父母就开始应付不过来了,一方面自己也没那么多耐心了,另外一方面是更为严重的问题 —— 自己的脑力不够。再加上那时候也没有 Google,所以,父母的反应惊人地一致:“去去去,赶紧睡觉!怎么就你事这么多?!”
一个个小朋友就这样被毁掉了,他们的好奇心就这样成了他们自己要主动避免的东西 —— 否则就会挨骂,谁愿意动不动就被一通数落呢?
好奇心是驱动一个人不断进步的最重要动力之一。所以必须不断呵护,呵护到老才对。
然而,就是这个如此金贵的东西,也会成为拖累;而且,若是真的被它拖累,那么最终真的会感觉非常遗憾,被好东西拖累 —— 太可惜了。
刚才所描述的,还只不过是两个小时而已的 “损失”。事实上,被注意力漂移所拖累的人,损失肯定远不止如此。在做 “工程” 或者 “项目” 的时候 —— 尤其是那种非实物类的工程或项目,比如,写个书,写个软件之类的 —— 注意力漂移导致的结果就是:
> 时间不断流逝,可是工程却永远没有结果。
这种损失,完全是任何正常人都没办法承受的…… 这话其实并不准,因为事实上据我观察,绝大多数人受到这种拖累的结果,自己其实无法想象 —— 因为永远没有完成过什么项目,永远没有完成过什么工程,又如何知道自己损失的究竟是什么呢?
到今天为止,我自己依然还是个需要不断与 “注意力漂移” 争斗的人 —— 许多年前,我注意到这个现象的时候,经过思考,就接受了一个事实:
> 注意力漂移不是能杜绝的现象,但必须在关键时刻有所应对……
如果当年的我没认真想过这事,没思索出对策,那么后来的我也不可能写那么多书,转行那么多次,自学那么多大大小小的技能…… 当然,各位读者也完全看不到现在正在阅读的文字 —— 因为它们不可能被完整地写出来,甚至干脆就不应该存在。
在罗列并比较众多策略之后,我选了一个看起来最不相干,却最本质的策略:
> **把 “全面完整” 放到最高优先级。**
而后,这些年全靠这个策略挺了过来……
当我想做什么事的时候,或者想学什么东西的时候,我会投入一定的时间去琢磨,这个事或者这个东西,要做得全面完整,或者要学得全面完整,那都应该做什么呢?在思考如此严肃的问题的时候,我还是习惯用纸和笔,写写画画 —— 迄今为止没有找到合适的电子设备和软件替代。
我买笔记本,不是为了记笔记的,因为记笔记这个东西,实在是在电脑上做更方便,许多年前开始就是如此了。我的笔记本主要用来做一件事:
> 罗列整理那些为了做到 “全面完整” 而必须优先做的事。
用列表也好、或者用图表也罢,反正都是要不断整理修订的,而它的存在,给了我一个优先级:
> 除了这上面罗列的这些东西之外,在当前时间段,别的都不如它们重要。
一旦发现自己的注意力没有集中在这上面的关键之时,一旦发现自己的注意力已经漂移到其它当前并不重要的事项上,就马上纠正。
谁都知道应该先做重要且紧急的事情,可问题在于,如何判断 “是否重要” 呢?**全面完整**这四个字就会给我指引。
一方面,是用全面完整来保持自己对重要事情的关注,另外一方面,还需要提高对抗**不相关完美诱惑**的能力。十年前,我写《[把时间当作朋友](https://github.com/xiaolai/time-as-a-friend)》的时候,还把这东西叫做 “脆弱的完美主义倾向”,现在我已经把这个概念升级了 —— 因为更准确地讲,那不是 “完美主义者的脆弱”,那是 “能力不及格者” 的 “轻重不分”。
早些年,我跟很多人一样痴迷于电脑这个东西,也跟很多人那样,用 Windows 惯出来了坏毛病 —— 动不动就重装系统…… 重装系统很浪费时间的,但那时也不知道为什么总是忍不住去干那事,哪怕有些小毛病,马上就受不了,弄的好像重装一个干净的操作系统会让自己的世界焕然一新一样。
再后来就明白了,这绝对就是**自己的毛病 —— 做事不分轻重**。
说实话,这也不是自己想明白的 —— 我没那么聪明。是因为遇到了一个高人。他的电脑桌面上,乱七八糟摆满了各种图标,从不整理。我问他这不影响效率吗?他说,明明有搜索你不用,到底是谁效率低下?我被问愣了,无言以对。
我又发现他根本没有装杀毒软件…… 我问为什么?他说,“养几个虫子玩玩也没什么不好……” 不过,他转念又告诉了我他的思考。他说,只要平时习惯好,病毒进来了也没啥可偷的,但更为关键的是,他用电脑是干活的,而不是干杀毒的活的…… 脑子如此清楚,让我自愧不如。
但学到了。
虽然我还是做不到桌面上图标很乱,虽然我是因为改用了 Mac OS,所以不装杀毒软件,但注意力要放到应该放的地方,这事我记住了,牢牢记住,从此之后许多年,从未忘过。每次发现自己轻重不分的时候,就会想起他,然后就改过自新。
如何成为优秀沟通者
一般认为,“手艺人” 的普遍特征之一就是缺乏沟通能力,或者沟通能力差 —— 也许是因为平时把所有的时间精力都投入到磨练手艺上去了罢。
但这肯定不是最主要的原因。你看手艺不怎么样的人沟通能力更差;手艺顶级的人却常常反过来沟通能力很强很强…… 为什么呢?
所以,最核心的理由,应该是一个人最基本的选择而已:
> 看一个人是否重视沟通能力。
因为若是一个人重视沟通能力,那么,他就自然而然地会想办法去进行刻意练习。如果他本人并不重视沟通能力,那么,自然就没有任何时间精力投入在这方面,结果就非常自然而然了。
非常遗憾,绝大多数人对沟通能力的重视远远不够 —— 他们也不是不重视,就是重视的程度实际上太差了。别说双向沟通了,即便是单向沟通,向别人问个问题这么 “简单” 的事,其实也需要 “学” 与 “练” —— [之前提到过](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.G.the-golden-age-and-google.md)的文章,事实上一定有读者并没有去认真阅读:
> …… 作为一个有素养的自学者,有一篇文章必须精读:
>
> > [How To Ask Questions The Smart Way](https://github.com/selfteaching/How-To-Ask-Questions-The-Smart-Way)
>
> 这里还有 John Gordon(王渊源)同学录制的英文朗读版:
>
> > https://github.com/selfteaching/How-To-Ask-Questions-The-Smart-Way
经过多次尝试就知道了,连 “描述清楚自己的问题” 都不是一件很容易做得足够清楚、足够好的事呢。
对沟通能力进行 “刻意练习” 的最佳方式是什么呢?其实还是[你已经知道的](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.E.to-be-thorough.md):
> **教是最好的学习方法**
讲课、写教程,甚至写书 —— 这是最高效的提升沟通能力的刻意练习手段。
人们常说,“杀鸡焉用宰牛刀”…… 在我看来,既然如此,备上一把宰牛刀挺好的,平时宰牛就用宰牛刀,备不住哪天需要杀鸡的时候,并不需要非得换把刀才行,依然可以用那把宰牛刀……
讲演能力、写作能力,其实同样也是手艺而已,但它们也确实是很必要的手艺。尤其是,具备这两项手艺的人,在现在这样的社会里收入一定不差,不信你就观察一下身边的世界、你的眼界可以触达的人群。
这两个手艺若比作宰牛刀的话,则日常生活里与单人沟通,只不过是杀鸡而已 —— 哈,这个类比真是不能说给沟通对象听,实在有太大的误会可能性。然而,说正经的,其实这也是为什么 “教师” 这个行业的人有不少能够跑出来成为优秀创业者、优秀投资人的原因之一。
当然,绝大多数连一门手艺都没有弄明白的初学者,阅读以下内容时,会觉得 “与当前的自己没关系”…… 不过,请注意,这绝对是 “[刚需幻觉](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.D.indispensable-illusion.md)”,千万不要被它误导。
已经说了无数遍了:绝大多数手艺都是这样的:
> 原理很简单,精湛与否取决于重复的次数。
哼,这句话不就是卖油翁说的吗?“无他,唯手熟尔” —— 是哦,卖油翁如是说。
以下,让我从入门开始讲起,而后步入进阶,最后到达高级……
入门
内容第一
无论是平日里讲话、还是台上讲课,抑或是写篇文章、写本书,永远都是内容第一,至于形式,并非不重要,但绝对不能喧宾夺主。
通常,我们用 “干货” 来描述内容的重要性。第一步就是准备干货,至于修辞啊、笑点啊、酷炫幻灯片啊等等等等,都必须是次要的,否则就是喧宾夺主。干货不够好,其它的做得越多、越好,最终越是容易露怯 —— 这很可怕。
所以,在你还没有确定自己值得讲、别人值得听的内容的情况下,就去学习如何制作幻灯片,在我看来完全是浪费时间。
使用工具的技巧之一,就是用最朴素的方法使用最好的工具,这样成本最低效果最好。所以,我在讲课、讲演的时候,通常就只用最简单的模版,空白、单色背景那种,而后一页里只写一句话 —— 就是接下来三五分钟里我要讲的重点(甚至干脆只是标题)…… 这样的幻灯片,我只需要几分钟就能做完。而后,我的所有时间精力都放在精心准备内容上去了。高效、低成本。
[做事不分轻重](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.H.prevent-focus-drifting.md),这不对。
内容质量
内容第一,就决定了另外一个事实:
> 不要讲或写你并不擅长的事。
换句话说,不要分享你做得不够好、做得不够精的手艺。讲课也好、写书也罢,都是分享。分享的意思是说,你有别人想要的东西 —— 别人想要的,就是他们学不好、做不好的东西…… 若是你自己手艺不强,手艺不精,其实就没什么可分享的,就算你想 “分享”,别人也不要。
你值得讲,别人值得听的,一定是你做得比别人更好的东西,就这么简单。
所以,在自己磨练手艺的时候,可以同时磨练沟通能力;然而,一旦需要讲,需要写,那么就说明,你自己确信自己做得比别人更好、比别人更精,所以,你值得讲,所以,人家值得听。
一旦你确定自己有值得讲、人家值得听的东西,那么,内容质量的第一要素已经完成了,接下来要注意哪些呢?只需要关注最重要的三个方面就可以了:
> - 重点突出
> - 例证生动
> - 消除歧义
首先,“重点突出”,是最简单朴素、成本最低的 “优秀结构”。
既然是沟通,就要了解对方。在绝大多数情况下,对方想要的才是重点。
可问题在于,我们平日里面对单个人的时候都觉得 “了解对方很困难”,我们又如何判断 “一个群体” 呢?与很多人想象得相反,判断群体远比判断个体容易得多…… 因为你可以用粗暴分类,比如,你把听众或读者划分为 “小白” 和 “专家”,那你就知道了,你面对的群体中,更可能 “大部分是小白,小部分是专家”。于是,你就可以思考,“小白们最想知道的是什么?”,“专家们最重视的是什么?”,于是,你就可以在脑子里对所谓的 “重点” 有应对策略。
再比如说,你可以 “粗暴” 地把群体分为友善者和刺儿头两种。有经验的老师,都会专门准备 “针对刺儿头” 的内容,因为,人群中永远存在刺儿头 —— 至少一个,所以,在现场,必须要有应对他们的策略。比如,当一个刺儿头说了一句蛮不讲理却引发哄堂大笑的话,你怎么办?每个人的策略不同,但你必须找到属于你的最佳策略。
进而,在找到重点之后,紧接着必须要做到的一步就是 “例证生动”。寻找好例子,是需要很多时间精力的事情 —— 从来就没有任何 “信手拈来” 的生动例子。信手拈来,是读者或者听众的感觉而已,对你来说,肯定是举重若轻的 —— 你明明是在举重,却被认为 “如若举轻”。在第一部分,[讲文件的那一章](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.1.E.7.files.md)里,我举了个 “我自己费尽心机找好例子” 的例子,相信你读过之后,一定有所感触。
最后一步,就是在前两步都完成之后,反复确认一件事情,消除**一切**歧义。这是真功夫,因为这东西很难把自己关在屋子里自顾自练成。并且,每个人有自己的 “容易引发误解” 的特殊属性,大家各不相同 —— 于是,只能靠自己探索。
无论如何都不能骂听众读者傻逼,他们的所有看起来傻逼的反应,都是你所说、你所写引发的 —— 这是百分之百清楚无误的事实。当年我写博客的时候,决不删除任何留言。其中最大的一个理由就是,无论那留言显得多么荒谬,甚至干脆是谩骂,都值得我认真思考:
> 我到底说什么了?居然引来这种反应?!
看多了,思考多了,你就有你自己的策略了。
内容组织
只有一个重点的时候,其实并不需要组织;但若是有一个以上的重点,那么这些重点之间会产生逻辑关系:
> - 并列
> - 递进
> - 转折
这是上中学的时候,所有人在语文课上都学习并掌握了的知识 —— 现在终于需要 “活学活用” 了。
在讲演、讲课、写教程、写非小说类书籍的时候,最有效的组织方式竟然是最简单的,并且只有一个:
> **层层递进**
你有两个重点需要分享,那么把更重要的那个重点放在后面;你有三个重点要分享,那么就把最重要的放在最后面…… 无论你有多少个重点,都按这种方式排列,准没错!
并且,另外一个建议是:
> `3` 是重点数量的极限
如果你必须有 `11` 个重点,那怎么办?那就把它们分到最多 3 个分组中,比如,像当前文章这样,“入门”、“进阶”、“高级”……
另外一个策略,是与 “递进” 的逻辑关系组合使用的 —— 你要区分针对你所谓的重点,对方的已知状态。大家都知道的,何必当作重点?于是,未知程度最高的,放在未知程度不那么高的重点后面。
这都是听起来无比简单,甚至好像 “无需讲述” 的 “重点”,但若是你多观察一下周遭的世界,你就明白了,很多人可能并不是不知道这些方法,但不知道为什么,他们就是不去应用这么简单有效的方法,也是怪得很!
进阶
当入门的手段都已经熟练了,就可以做很多所谓 “锦上添花” 的事情。锦上添花据说有很多种手段,比如,制造笑点啊,使用炫酷的幻灯片啊等等…… 但我只想给你讲一个学会了就够用,却也是最简单、最直接,然而又最有效的手段:
> **输送价值观**
事实上,你可以把当前这本 “书” 当作一个巨大且又生动的例子:
> 你把李笑来想象成一位在某个学校里为学生讲编程课的老师。那么,你就可以把当前这本书当作 “李笑来的讲义”…… 也许我和学生手里都拿着另外一本更著名的计算机专家所著的编程入门书籍,然而,我的讲义,就是按照我的顺序、我的内容编排来讲述的。
事实上我在写这本书的时候,从某个层面上来看,真的写的就是 “Python 官方文档” 以及 “Python 官方教程” 的辅助讲义,写作目标如此,写作方式亦如是。
那我做了什么最重要的事情呢?
> 我向我的群体输送了我觉得更有意义的价值观:
>
> > 自学是门手艺……
当年我在学校里讲英语课的时候,除了讲英语本身之外,我输送的价值观是:
> 能管理好自己的时间的人,学英语学起来更容易……
后来,这一部分单独被提取出来,在我离开那所学校之后,写成了长销书,《[把时间当作朋友](https://github.com/xiaolai/time-as-a-friend)》—— 你看,是一样的道理。
而所谓的价值观,定义很简单:
> 你的价值观,就是你认为什么比什么**更重要**。
价值观可大可小。大到集体利益与个人利益之间的比较,小到自学中 “全面” 压倒一切…… 然而,这世界总有独立于任何人存在的 “客观的价值比较”,只不过,每个人的 “价值观” 是自己的 “观点”。而我们每个人都希望自己的观点尽量摆脱自己的主观,尽量靠近那个 “客观的价值比较”…… 而一旦我们确定自己比原来的自己,甚至相对于其他人更进一步的时候,就很可能值得认真分享。
这个方法着实简单,然而却非常有效。这有点像什么呢?这有点像人家弹个吉他是 “蹦单音”,可你弹的却是 “曲调与和弦” 相辅相成的音乐…… 给你看一个 Youtube 上的《一生所爱》—— 这首曲子是我最喜欢的指弹版本 —— 这种弹法,不仅有旋律,还有和弦,并且还有打击乐器效果 “伴奏”:
```python
from IPython.display import IFrame
IFrame('https://www.youtube.com/embed/AjWTop5O5jo?', width='800', height='450')
```
<iframe
width="800"
height="450"
src="https://www.youtube.com/embed/AjWTop5O5jo?"
frameborder="0"
allowfullscreen
></iframe>
最初的时候,有个看起来很难以跨越的障碍:
> 感觉总是需要为自己塑造权威感 —— 否则就害怕没人听、没人看、没人信……
这是很多人掉进去的坑。刚开始的时候,自己就不是权威啊!无论怎么装神弄鬼,事实上就不是么!
很多人没想明白,因为害怕没人听、害怕没人看、害怕没人信,所以就开始各种作弊,包括各种装神弄鬼,各种欺世盗名…… 这么做,暂时管用,长期来看,肯定是吃亏的。
因为作弊其实并不难,装神弄鬼其实并不难,欺世盗名其实并不难 —— 真正难的是长期作弊…… 长期欺骗有多难呢?难到根本不可能的地步。尤其是在寿命越来越久的今天。《庄子》里说,“寿则多辱”,今天有了新解释 —— 大家都寿命很长,所以别骗人,因为早晚会露馅……
花那么长时间作弊什么的,还不如花那么长时间磨练手艺,你想想看是不是这个道理?
给你看一段视频,加州伯克利大学的 Brian Harvey 在课堂上告诉学生,“为什么不要作弊” 的真正原因:
```python
from IPython.display import IFrame
IFrame('https://www.youtube.com/embed/hMloyp6NI4E?', width='800', height='450')
```
<iframe
width="800"
height="450"
src="https://www.youtube.com/embed/hMloyp6NI4E?"
frameborder="0"
allowfullscreen
></iframe>
于是,千万别扭曲了自己,是什么样就是什么样,该怎么做就怎么做。而另外一件事是确定的:
> 分享多了,就自然进步了……
在求知的领域里,分享得越多,进步越快,且社交有效性提高得更多。
高级
无论什么手艺,大多数人都可以入门、少数人可以进阶…… 再往后,就通常被认为是个人 “造化” 了。
可这所谓的 “造化” 究竟指的是什么呢?
我觉得通过这本书,我可以向绝大多数普通人解释这个 “玄学词汇” 了……
> 这里所谓的 “造化”,指的应该是一个人的 “融会贯通的能力” —— 有 “造化” 的人,不过是把大量其他领域里的技能、技巧、甚至手艺学来而后应用到自己的手艺之中……
就这么简单。
有个特别好玩的例子。
现在大家已经熟悉了的吉他演奏中的 “指弹”(Percussive Guitar),可这种玩法在没有 Youtube 的时代里并不多见 —— 在我长大的年代里,甚至 “前所未闻”。不知道是谁,把打击乐器的手法融合到吉他演奏手法中去了,于是,在 Youtube 这样的视频工具出现之后,人们的 “见识” 成本降低了(过去也许要 “去西天取经才行”),很快就有人模仿,很快就有人更为擅长……
```python
from IPython.display import IFrame
IFrame('https://www.youtube.com/embed/nY7GnAq6Znw?', width='800', height='450')
```
<iframe
width="800"
height="450"
src="https://www.youtube.com/embed/nY7GnAq6Znw?"
frameborder="0"
allowfullscreen
></iframe>
有这种能力的人,普遍有两个特征:
> - 他们自学很多看起来不相干的手艺
> - 他们对自己的手艺充满尊重与热爱
我也只能猜个大概。这其中的第二个特征,很可能是第一个特征的根源,因为他们对自己的手艺充满了尊重与热爱,所以,他们追求全面,他们刻意练习,他们还刻意思考…… 由此引发了对一切可能与自己的手艺相关的东西都感兴趣 —— 虽然在外界看来那两样东西可能全无联系。于是,他们利用已经在自己的手艺中练就的自学能力,不断自学新的东西,不断 “发现” 所谓的 “新大陆”,不断用他们的所见所闻回过来锤炼或更新自己的手艺……
所以,弹钢琴或者弹吉他的也去学了打击乐器;讲课的人会去听相声专场,学习相声演员是如何抖包袱的;写书的人可能会像我琢磨 “取名” 的艺术 —— 甚至不惜去研究一下自己并不屑于相信的易经八卦;学会计的去研究了物理或者经济学;学编程的也去学了设计,学设计的也去学了编程;做前端的去学了后端,做后端的也学会了前端;做统计的学会了数据可视化…… 搞来搞去,计算机行业里有个著名的词汇诞生:全栈工程师。
其实,所有精湛的手艺人,都是**全栈**,不信你就仔细观察一下。
于是,所有在入门、进阶之后走得更远的手艺人,都明白且认同这个道理:
> **学无止境**
于是,最后一个重要技巧,不仅仅是 “不断磨练当前的手艺”,还有就是**不断向所有的手艺人学习**。
再进一步,技巧没用了…… 想再进一步,靠的是另外一个层次的东西 —— 那就是尊重与热爱。
这么多年来,在互联网上我最喜爱的老师,是麻省理工大学的 Walter Lewin 教授。
```python
from IPython.display import IFrame
IFrame('https://www.youtube.com/embed/sJG-rXBbmCc?', width='800', height='450')
```
<iframe
width="800"
height="450"
src="https://www.youtube.com/embed/sJG-rXBbmCc?"
frameborder="0"
allowfullscreen
></iframe>
十多年前,MIT 出了一套 Open Course,在线的免费课程。授课老师绝大多数都是本校的著名教授 —— 在那么多课程里,我一下子就爱上了这位教授。建议你有时间把他的所有课程看完,虽然你可能觉得物理这东西你并不感兴趣 —— 可事实上,看完你就知道了,你只不过是运气不好,你从来没遇到过这么可爱牛逼的教授而已…… 在他身上,你可以学到无数,甚至是 “不可言说” 的技巧、秘密和手艺。
关键在于,你一定会非常生动地、深刻地体会到他对物理、他对授课的**尊重和热爱**。看过之后,你一定会跟我有一样的慨叹:“是哦,**Love is the power.**”
没有什么比 “热爱” 和 “尊重” 更为高级的了。就这样。
自学者的终点
…… 磨练自学手艺的你,早晚会遇到这种情况:
> 必须学会一个没人教、没人带,甚至没有书可参考的技能。
这也许是我自己能够想象到的自学者所能经历的最高境界吧,因为那么多年过去之后的我也只不过走到这个地方而已…… 许多年过去,我通过自学习得的没有人教、没有人带、甚至没有书可参考的技能,很拿得出手的只有两个而已:
> * 制作长销书
> * 区块链投资
先说说区块链投资。
2011 年,我开始投资比特币的时候,不像现在。现在你能在 Amazon 上找到一大堆书,给你讲区块链技术、区块链投资…… 2011 年的时候,互联网这本大书上,连篇像样的文章都几乎没有。
在这样的领域里成为专家,其实比外界想象得容易 —— 当然,无论什么,外界总是觉得很难。为什么比外界想象得更容易呢?因为大家都不懂,仅此而已。
所以,剩下的事情很简单:
> 谁能持续研究谁就可能更先成为专家。
到最后,还是一样的,决定因素在于有效时间投入,再次仅此而已。
了解我的人都知道,在知识技能的分享方面,我从来都没有 “藏着掖着” 的习惯。我说 “仅此而已” 的时候,那就是 “仅此而已”,没有任何其它保留。
说研读比特币白皮书,就真的研读,反复研读,每年都要重复若干遍 —— 有人问了,有那个必要吗?是哦,对我有没有那个必要,我自己说了算。并且,就算真有,告诉别人有什么用?对吧?
> 这里是[《比特币白皮书》我翻译的版本](https://github.com/xiaolai/bitcoin-whitepaper-chinese-translation)。
说投资就真的投资,不是 “买两个试试”、“买几个玩玩” —— 我的做法是**重仓**。持仓之后继续研究,和两袖清风地读书研读肯定不一样。有一个与我极端相反的例子。此人大名鼎鼎,是《精通比特币》([Mastering Bitcoin](https://www.amazon.com/Mastering-Bitcoin-Programming-Open-Blockchain/dp/1491954388/))的作者,Andreas M. Antonopoulos,与我同岁,也是 1972 年生人。
他也是国外公认的比特币专家,但他不是投资人 —— 他几乎没有比特币。2017 年牛市的时候,人们听说大神 Andreas M. Antonopoulos 竟然几乎没有比特币,大为惊讶,向他捐款总计 102 个比特币左右 —— 千万不要误会我,我没有任何鄙视他的意思。这不是我说说而已,我是用行动支持他的人。他的书,《精通比特币》的中文版,还是我组织人翻译的。
我只是说,我和他不一样的地方在于,在某些方面我比他更实在,这也是事实。我相信我在实际投资后,对比特币也好区块链也罢,理解力会更深一些,因为驱动力不一样么,多简单的道理。
然而,仅仅 “谁能持续研究谁就更可能先成为专家” 这一条,其实并不够完成 “学会一个没人教、没人带,甚至没有书可参考的技能”。
这么多年来,我能够 “学会一些没人教、没人带,甚至没有书可参考的技能”(比如赚钱这事就是如此),更多的时候仰仗的是一个我已经告诉过你的 “秘密”…… 也许你想不起来了,但我一说你就能 “发现” 其实你真的[已经听我说过](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.2.D.deliberate-thinking.md):
> **刻意思考**:这东西我**还**能用在哪儿呢?
并且,我还讲过在自学编程的过程中,见识到的 MoSCoW Method 给我在写书方式上的影响。
> 我写书就是这样的。在准备的过程中 —— 这个过程比绝大多数人想象得长很多 —— 我会罗列所有我能想到的相关话题…… 等我觉得已经再也没有什么可补充的时候,再为这些话题写上几句话构成的大纲…… 这时候就会发现很多话题其实应该是同一个话题。如此这般,一次扩张,一次收缩之后,就会进行下一步,应用 MoSCoW 原则,给这些话题打上标签 —— 在这过程中,总是发现很多之前感觉必要的话题,其实可以打上 `Won't have` 的标签,于是,把它们剔除,然后从 `Must have` 开始写起,直到 `Should have`,至于 `Could have` 看时间是否允许,看情况,比如,看有没有最后期限限制……
>
> 在写书这事上,我总是给人感觉很快,事实上也是,因为有方法论 —— 但显然,那方法论不是从某一本 “如何写书” 的书里获得的,而是从另外一个看起来完全不相关的领域里习得后琢磨到的……
你看,把另外一个领域里的知识拿过来用,是在一个 “没人教、也没人带,甚至没有书籍可供参考” 的领域中,最基本的生存技巧。
再进一步,当我们在最开始的时候说,“尽量只靠阅读习得一项新技能” 的时候,有一个重点现在终于在之前的很多解释与讲解之后能说清楚了:
> 我们并不是不要老师这个角色了,准确地讲,我们只不过是不再需要 “传统意义上的老师” 了而已。
首先,我们把自己当作老师 —— 英文中,Self-teaching 这个词特别好,它描述得太准确了。很多的时候,想在身边找到好老师是很难的,甚至是不可能的。在这种情况下,我们没有别的选择,**我们只能把自己当作老师去教自己**。
其次,就算我们 100% 只依靠阅读,那内容不还是别人写的吗?写那内容的人,实际上就是老师。没错,书本,是历史上最早的远程教育形式,即便到今天也依然是最重要最有效的远程教学形式。阅读的好处在于,对老师的要求中没有地理位置的限制,若是能自由阅读英文,那就连国界限制都没有。“**书中自有颜如玉**” 这句话,显然是并不好色的人说的,因为他更爱书…… 这句话的意思其实是说:
> 各路牛人都在书里……
反正,写书的人群中,牛人比例相对较高,这是事实 —— 古今中外都一样。
进而,更为重要的是,一旦你把整个互联网当作一本大 “书”,把 Google 当作入口,实际发生的效果是:
> 你把 “老师” 这个角色**去中心化**了……
一方面 “老师” 这个角色的负担降低了,他们不用管你是谁,也不用管你怎么样了,他们该干嘛就干嘛;而另外一方面则对你更重要 —— 你学不同的东西,就可以找不同的老师;即便是相同的东西,你也可以找很多老师;对于任何一个老师,你都可以 “弱水三千只取一瓢”,也就是说,只挑他最厉害的部分去学…… 不就是多买几本书吗?不就是多搜索几次、多读一些文档吗?
最后,你竟然还有最厉害的一个小招数:
> 无论学会什么,都要进一步**刻意思考**:这东西我**还**能用在哪儿呢?
于是,你 “一下子” 就升级了 —— 用这样的方式,相对于别人,你最可能 “学会几乎任何一个没人教、没人带,甚至没有书可参考的技能”……
你看看自己的路径罢:从 “不得不把自己当作老师去教自己” 开始 —— 虽然起步是**不得不**,但这个 “不得不”,恰好是后来你变得更为强大的原因和起点…… 这就解释了为什么历史上有很多牛人的很多成就其实都是这样 “被迫” 获得的。
于是,我们终于可以好好总结一下了:
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/seflteaching-model.png?raw=true)
> * 你一定要想办法启动自学,否则你没有未来;
> * 你把自学当作一门手艺,长期反复磨练它;
> * 你懂得学、练、用、造各个阶段之间的不同,以及针对每个阶段的对应策略;
> * 面对 “过早引用” 过多的世界,你有你的应对方式;
> * 你会 “囫囵吞枣”,你会 “重复重复再重复”,你深刻理解 “读书百遍其义自见”;
> * 以后你最擅长的技能之一就是拆解拆解再拆解;
> * 你用你的拆解手艺把所有遇到的难点都拆解成能搞定的小任务;
> * 自学任何一门手艺之前你都不会去问 “有什么用”,而是清楚地知道,无论是什么只要学会了就只能也必然天天去用;
> * 你没有刚需幻觉,你也没有时间幻觉,你更没有困难幻觉,反正你就是相对更清醒;
> * 不管你新学什么手艺,你都知道只要假以时日你就肯定能做好,因为所有的手艺精湛,靠的只不过是充足的预算;
> * 你知道如何不浪费生命,因为只要不是在刻意练习、不是在刻意思考,那就是在 “混时间”;
> * 你总是在琢磨你能做个什么新作品;
> * 你刻意地使用你的作品作为有效社交工具,也用作品去过滤无效社交;
> * 你乐于分享,乐于阅读也更乐于写作 —— 因为这世界怎么帮助你的,你就想着要怎样回报;
> * 你把全面和完整当作最高衡量标准,也用这个标准去克制、应对自己的注意力漂移;
> * 你会不断自学新的手艺,因为你越来越理解单一技能的脆弱,越来越理解多项技能的综合威力;
> * 你越来越依赖互联网,它是你最喜欢的 “书”,而 Google 是你最好的朋友 —— 他总是能帮你找到更好的老师;
> * 偶尔,你会学会没人教、没人带、甚至没书可参考的手艺,别人都说你 “悟性” 高,可你自己清楚地知道那其实是怎么回事;
> * 你越来越明白,其实没什么 “秘密”,越简单、越朴素的道理越值得重视;
> * 你发现你用来思考的时间越来越多 —— 准确地讲,是 “琢磨”…… 只不过是因为你真会琢磨了 —— 你很清楚你应该花时间琢磨的是什么。
没有人教过我怎么写一本长销书(而不仅仅是畅销书),这显然是我通过自学习得的能力 —— 我也只能把自己当作老师教自己,这是不得已。然而,不得不的选择同样常常能给我带来好运…… 甚至,它也不是我通过阅读哪本书习得的能力 —— 因为这方面还真的没什么靠谱的书籍。然而,我竟然学会了 —— 靠什么?靠的就是上面说的那点事而已。
“秘密” 是什么?说出来后,你听起来肯定是感觉 “太简单了” 乃至于 “有点不像真的”…… 每次,我都很认真的问自己以下几个问题:
> * 我要写的内容,的确是正确的吗?
> * 我要写的内容,确实会对读者有用吗?
> * 有什么内容是必须有的、应该有的?
> * 我写的这个内容,十年后人们再看,还会觉得跟新的一样有用嘛?
> * 我的书名,就那么放在那里,会直接让买家产生不由自主购买的吸引力吗?
一旦这几个问题我有了清楚的答案,我就知道,我有能力制作一本新的长销书了 —— 真的没有什么别的 “秘密”。
在《通往财富自由之路》中,我分享过以下内容:
> 我认为一个人的自学能力(当时还在用 “学习能力” 这个词)分为三个层次:
> 1. 学会有人手把手教授的技能
> 1. 学会书本上所教授的技能
> 1. 学会没有人能教授的技能
这一次,我无非是把后两个层面用一个特大号的实例掰开了揉碎了讲清楚而已。
到最后,**没什么不能自学的,反正都只不过是手艺** —— 只不过,我们每个人都受限于自己的时间精力而已。所以,若是你正在读高一,正在读大一,那就好好珍惜自己有机会可以**随意设置充裕预算**的时光罢。若是你已为人父母,那就想办法用自己的行动影响下一代罢。然而,其实更为重要的是,无论什么时候,都要这么想:
> 若是还有很长的未来,现在真不算晚……
自学不过是一门手艺,而且还是谁都能掌握的。不要 “试试” 而已,而是 “直接开干” —— 这样才好。
最后还有个需要补充的是:很多人崇尚 “刻苦”,并且刻意强调其中的 “苦” —— 古训中所谓的 “吃得苦中苦,方为人上人” —— 这一点我并不认同,而且还是深刻地不认同。
我的观察是,所谓的 “苦”,是那些完全不会自学的人对自学者的所作所为的错误理解。
自学一点都不苦,道理也很简单:
> 因为自学者是自发去学的,**原动力在于自己**。而不像其他人,是被动地学,原动力并非在于自己。
由于原动力在于自己,遇到困难时,当然同样苦恼;可不一样的是,有持续的原动力去克服那些困难,于是,总是在不断克服困难之后获得更大的愉悦、更大的满足感。
所以,“刻”,我们很认同,刻意地练习、刻意地思考,刻意地保持好奇心,刻意地去学习一些看起来与当前所掌握的手艺完全不相干的知识…… 至于 “苦” 么,那是别人的误解,我们自己开心着呢 —— 无所不在、无处诉说的幸福。
**人生苦长,无需惊慌。**
祝你好运!
<p style="text-align: right"><strong>李笑来</strong></p>
<p style="text-align: right">初稿完成于 <em>2019</em> 年 <em>2</em> 月 <em>27</em> 日</p>
下一步干什么?
理论上,下一步你的选择很多。自学是门手艺,你可以用它去学任何你想要掌握的其它手艺。如果,你有意在编程这个领域继续深入,那么,以下就是一些不错的线索。
当然,最先应当做的是,去检查一下自己的 “突击” 的结果,去 Pythonbasics.org 做做练习:
> https://pythonbasics.org/Exercises/
除了我在这里介绍的之外,请移步 The Hitchhiker's Guide to Python,它更为全面:
> https://docs.python-guide.org/
Python 必读书籍
无论学什么,一本书肯定不够,以下是学习 Python 的基本必读书籍:
> * [The Python Tutorial](https://docs.python.org/3/tutorial/)
> * [The Hitchhiker's Guide to Python!](https://docs.python-guide.org/)
> * [Think Python: How to think like a computer scientist](http://greenteapress.com/wp/think-python-2e/)
> * [Automate the Boring Stuff with Python](https://automatetheboringstuff.com)
> * [Effective Python](https://effectivepython.com)
> * [Python Cookbook](https://www.amazon.com/Python-Cookbook-Recipes-Mastering-ebook/dp/B00DQV4GGY)
> * [Fluent Python](https://www.amazon.com/Fluent-Python-Concise-Effective-Programming-ebook/dp/B0131L3PW4)
> * [Problem Solving with Algorithms and Data Structures using Python](http://interactivepython.org/runestone/static/pythonds/index.html)
> * [Mastering Object-oriented Python - Transform Your Approach to Python Programming](https://www.amazon.com/dp/B00JVQ14UO/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)
更多 Python 书籍:
> https://pythonbooks.revolunet.com
千万别觉得多,只要真的全面掌握,后面再学别的,速度上都会因此快出很多很多……
Python Cheatsheet
你已经知道了,这种东西,肯定是自己整理的才对自己真的很有用…… 不过,你也可以把别人整理的东西当作 “用来检查自己是否有所遗漏” 的工具。
网上有无数 Python Cheatsheets,以下是 3 个我个人认为相当不错的:
> * [Comprehensive Python Cheatsheet](https://gto76.github.io/python-cheatsheet/)
> * [Python Crash Course - Cheat Sheets](https://github.com/ehmatthes/pcc/tree/master/cheat_sheets)
> * [Pysheeet](https://www.pythonsheets.com/)
Awesome Python
Github 上的 “居民” 现在已经养成了一个惯例,无论什么好东西,他们都会为其制作一个 “Awesome ...” 的页面,在里面齐心协力搜集相关资源。比如,你想学 Golang,那你去 Google 搜索 [`Awesome Go`](https://www.google.com/search?q=awesome+go),一定会给你指向到一个 Github 上的 “Awesome Go” 的页面……
以下是 Awesome Python 的链接:
> https://github.com/vinta/awesome-python
CS 专业的人都在学什么?
如果你真有兴趣把这门手艺学精,不妨看看 Computer Science 专业的人都在学什么……
下面这个链接值得认真阅读:
> http://matt.might.net/articles/what-cs-majors-should-know/
全栈工程师路径图
既然学了,就肯定不止 Python —— 在扎实的基础之上,学得越多学得越快。以下是一个 “全栈工程师路径图”,作者是位迪拜的帅哥 [Kamran Ahmed](https://github.com/kamranahmedse):
> https://github.com/kamranahmedse/developer-roadmap
>
> Below you find a set of charts demonstrating the paths that you can take and the technologies that you would want to adopt in order to become a frontend, backend or a devops. I made these charts for an old professor of mine who wanted something to share with his college students to give them a perspective; sharing them here to help the community.
>
> Check out my blog and say "hi" on Twitter.
>
> **Disclaimer**
>
> The purpose of these roadmaps is to give you an idea about the landscape and to guide you if you are confused about what to learn next and not to encourage you to pick what is hip and trendy. You should grow some understanding of why one tool would better suited for some cases than the other and remember hip and trendy never means best suited for the job
Introduction
[〔图:Web Developer Roadmap Introduction〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/intro.png?raw=true)
Frontend Roadmap
[〔图:Frontend Roadmap〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/frontend.png?raw=true)
Back-end Roadmap
[〔图:Back-end Roadmap〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/backend.png?raw=true)
DevOps Roadmap
[〔图:DevOps Roadmap〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/devops.png?raw=true)
路漫漫其修远兮……
但多有意思啊?这完全就是一场闯关游戏。
Visual Studio Code 的安装与配置
官方文档请查询:
> https://code.visualstudio.com/docs
允许命令行启动 VS Code
使用快捷键 `⇧⌘p` 呼出 Command Palette,在其中输入 `shell command`,而后选中 `Install 'code' command in PATH`。此后,就可以在 Terminal 命令行中使用 `code` 命令了。(Windows 系统安装 VS Code 时会自动配置好,并不需要此步骤)
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/vscode-shell.png?raw=true)
选择 Python 解析器版本
使用快捷键 `⇧⌘p` 呼出 Command Palette,在其中输入 `select interpreter`,而后选中 `Python: Select Interpreter`。
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/vscode-select-python-version1.png?raw=true)
而后,在系统中已安装的若干个版本中选择你需要的那一个。MacOS 系统自带一个 Python 2.7,而我们安装的 Anaconda 为系统另外安装了一个 Python 3.7。
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/vscode-select-python-version2.png?raw=true)
安装扩展
使用快捷键 `⇧⌘x` 呼出扩展面板。安装 anaconda 扩展,它会连带装上 python 扩展:
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/vscode-extensions.png?raw=true)
另外,为了输入方便,有两个扩展可选安装:
> * Tabout 有它之后,可以使用 TAB 键跳出光标后的括号、引号等等;
> * Sublime Text Keymap and Settings Importer 有它之后,可以在 VS Code 中使用 SublimeText 的快捷键,最重要的当属多光标编辑 `⇧⌘l`……
自动补全
专业编辑器最重要的功能之一,就是能够在你输入的时候它帮你做到 “自动补全”,通常使用的快捷键是 TAB 键 `⇥`。
TAB 键 `⇥` 触发的自动补全有两种:
> * 当前文件中已有的字符串。比如,之前你输入过 `sum_of_word`;那么,之后,你就可以输入 `su` 或者干脆 `sow` 而后按 TAB 键 `⇥`,“自动补全” 功能会帮你完成输入 `sum_of_word`
> * 已有的 Snippets。比如,当你需要输入 `if ...: ...` 的时候,实际上当你输入 `if` 或者甚至 `i` 之后,你就可以用 TAB 键 `⇥`,“自动补全” 功能会为你 “自动完成” 语句块的输入。
字符串自动补全,使用的是所谓的 Fuzzy Match。输入 `sum_of_word` 中所包含的任意字符的任意组合(按顺序),它都会尽量去匹配;所以,`su` 和 `sow` 都可以匹配 `sum_of_word`,再比如,`rst` 可以匹配 `result`。
在 Snippet 自动补全的过程中,常常有若干个 “TAB Stop”,即,有若干个位置可以使用 TAB 键 `⇥`(或者,`Shift + ⇥`)来回切换;这时,第一种字符串自动补全的功能就失效了,如果需要使用字符串自动补全,那么需要按快捷键 ESC `⎋` 退出 Snippet 自动补全模式。
以下的 gif 文件演示的是以下代码的输入过程:
```python
def sum_of_word(word):
sum = 0
for char in word:
sum += ord(char) - 96
return sum
with open('results.txt', 'w') as results:
with open('words_alpha.txt', 'r') as file:
for word in file.readlines():
if sum_of_word(word.strip()) == 100:
results.write(word)
```
因为有这样的功能,所以你在输入程序的时候其实是非常从容的,可以很慢输入,边思考边输入…… 可实际上,完成速度却很快。
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/vscodedemo.gif?raw=true)
另外,SublimeText 的多光标输入是很多程序员爱不释手的功能,于是,各种编辑器里都有第三方写的 SublimeText Keymap 插件,连 Jupyterlab 都有:
> https://github.com/ryantam626/jupyterlab_sublime
Git 简介
--- You should've learned Git yesterday.
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/git-time-machine-cover.png?raw=true)
内容目标
再一次,这一篇内容的目标,依然不是 “教程”,而是 “教程” 的 “图例” —— 如果我们把真正的教程比喻成 “地图” 的话。最全面的 Git 教程在网上,**Pro Git**,是免费的 —— 把它反复阅读若干遍,理解完整:
> https://git-scm.com/book/en/v2
并且还有各种语言的翻译版本 —— 也包括中文。
为什么你必须学会使用 Git?
Git 是一个分布式版本控制软件 —— 听起来也许跟你没关系,但**无论是谁**,都会因为能够使用 Git 而节约时间、提高效率。进而,如果你居然没有一个活跃的 [Github](https://github.com) 账户,那么你正在错过人类史上前所未有的共同协作时代 —— 半点都没有夸张。同样提供 Git 工具云服务的还有 [Gitlab](https://gitlab.com), [Bitbucket](https://bitbucket.org) 等等。
并且,[Github](https://github.com) 很可能是地球上第一个给人们提供 “[用作品社交](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/Part.3.F.social-selfteaching.md)” 方式的平台,你若是不能参与其中,实在是太可惜了!
从逻辑上理顺 Git 基本命令
Git 的作用,基本上可以被划分为三部分:
> - 备份文件
> - 跟踪文件变化
> - 与他人协作共同操作文件
在一个 git 仓库中,总计有四个 “抽象层”,它们分别是:
> * upstream repository 保存在云端的仓库
> * local repository 本地仓库
> * staging area 缓存区
> * working directory 工作区
其中,`local repository` 和 `staging area` 这两个抽象层的数据,保存在 `working directory` 根目录下的一个隐藏目录 `.git/` 下;需要使用 `ls -a` 才能看到。
当你使用 `git init` 命令将一个本地文件夹 `working directory` 初始化为 `local repository` 的之后,该文件夹内部的结构如下:
```
.
└── .git
├── HEAD
├── config
├── description
├── hooks
│ ├── applypatch-msg.sample
│ ├── commit-msg.sample
│ ├── fsmonitor-watchman.sample
│ ├── post-update.sample
│ ├── pre-applypatch.sample
│ ├── pre-commit.sample
│ ├── pre-push.sample
│ ├── pre-rebase.sample
│ ├── pre-receive.sample
│ ├── prepare-commit-msg.sample
│ └── update.sample
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
```
以下示意图中仅包含最基本的 Git 命令 —— 并且基本上都是独自使用 Git 时的常用命令。
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/git-command-relationships.png?raw=true)
在工作区 `working directory` 这个抽象层中,你完成各种日常工作,创建、编辑、删除…… 你可能需要用某个编辑器去修改文件,你也可能频繁使用各种 Bash 命令,如,`rm` `mkdir` `cp` `mv` 等等。
时不时,你可能会把一些处理完的文件 “加入缓存区”;等一个阶段的工作完成之后,你可能会把那些已经放入缓存区的文件**提交**到(commit)本地仓库;而后继续工作…… 根据情况,你也会将本地仓库的文件**推到**(push)云端,即,远端仓库。如果,你正在与他人协作,你也可能经常需要从云端**下拉**(pull)最新版本到本地。
Git 的安装
Mac
Mac 的操作系统 Mavericks (10.9) 以上版本中都内建有 Git,你可以在 Terminal 中通过以下命令查看是否有 Git:
``` bash
git --version
which git
```
也可以通过 Homebrew 安装最新版本的 Git:
``` bash
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
brew doctor
brew install git
```
还可以通过 Conda 安装:
``` bash
conda install -c anaconda git
```
Windows
前往 https://gitforwindows.org 下载并安装 Git for Windows。
此外,它还会提供 Git Bash —— 在 Windows 操作系统中使用与 \*Nix 操作系统一样的 Bash 命令行工具。
另外,在 Windows 操作系统中推荐使用 Git Bash 或者 PowerShell,而非 CMD 作为命令行工具。
Linux
大多数 Linux 操作系统要么基于 Debain,要么基于 Red-Hat —— 请自行查看 [List of Linux distributions](https://en.wikipedia.org/wiki/List_of_Linux_distributions),确定自己的 Linux 发行版究竟基于哪一个。
基于 Debian 的 Linux 发行版:
```bash
sudo apt-get update
sudo apt-get upgrade
sudo apt-get install git
```
基于 Red-Hat 的 Linux 发行版:
```bash
sudo yum upgrade
sudo yum install git
```
Git 本地配置
在命令行工具中执行以下命令:
```bash
git config --global user.name "<your_name>"
git config --global user.email <your_email_address>
```
Git GUI
Git 的图形化版本客户端,有很多种选择,以下几个跨平台的客户端最受欢迎:
> * [Github Desktop](https://desktop.github.com)
> * [GitAhead](https://gitahead.github.io/gitahead.com/)
> * [Fork](https://git-fork.com)
> * [GitKraken](https://www.gitkraken.com)
更多选择,请移步查看 [git-scm.com 上的 Git GUI Clients 列表](https://git-scm.com/downloads/guis/)。
需要了解的 Bash 基本命令
虽然 Git 也有图形化版本,但无论如何你都会接触到命令行工具。并且,谁都一样,早晚会遇到非使用命令行不可的情况。
以下是常用 Bash 命令的简要说明:
| 命令 | 简要说明 |
| ----- | --------------------------------------------------- |
| `cd` | Change Directory 的缩写;转到指定目录 |
| `ls` | List 的缩写;列出当前目录中的内容 |
| `mkdir` | Make Directory 的缩写;在当前目录中创建一个新的目录 |
| `pwd` | Present Working Directory 的缩写;显示当前工作目录 |
| `touch` | 创建一个指定名称的空新文件 |
| `rm` | Remove 的缩写;删除指定文件 |
| `rmdir` | Remove Directory 的缩写;删除指定目录 |
| `cp` | Copy 的缩写;拷贝指定文件 |
| `mv` | Move 的缩写;移动指定文件 |
| `cat` | Concatenate 的缩写;在屏幕中显示文件内容 |
| `chmod` | Change Mode 的缩写;改变文件的权限 |
| `man` | Manual 的缩写;显示指定命令的使用说明 |
其中,`chmod` 最常用的 4 个权限分别是:
| 文件权限模式 | 简要说明 |
| ----- | --------------------------------------------------- |
| `777` | 任何人都可以读、写、执行该文件 |
| `755` | 任何人都可以读、执行该文件,但只有所有者可以修改 |
| `700` | 只有所有者才能进行读、写、执行操作 |
| `+x` | 将文件设置为可执行 |
在使用 `man` 命令时,系统会使用 vim 文本编辑工具以只读模式打开帮助文件,常用键盘命令如下:
| 键盘命令 | 简要说明 |
| ----- | --------------------------------------------------- |
| `f` | 向后翻屏 |
| `b` | 向前翻屏 |
| `d` | 向后翻半屏 |
| `u` | 向前翻半屏 |
| `j` | 向后翻一行 |
| `k` | 向前翻一行 |
| `h` | 查看 vim 帮助 |
| `q` | 退出 |
一些不错的 Git 教程
除了 Pro Git 这本书之外,还有很多值得去看:
> * [GIT CHEATSHEET -- an interaction from nop software](http://ndpsoftware.com/git-cheatsheet.html)
> * [Learn Git Branching](https://learngitbranching.js.org/)
> * [Learn Git in a Month of Lunches](https://livebook.manning.com/#!/book/learn-git-in-a-month-of-lunches/about-this-book/)
> * [Git How To](https://githowto.com/)
> * [Git pretty - Solve Git Mess](http://justinhileman.info/article/git-pretty/)
> * [Visualizing Git Concepts with D3](http://onlywei.github.io/explain-git-with-d3/)
当然,你肯定早晚会去 Github 上找 “Awesome Git”:
> https://github.com/dictcp/awesome-git
Jupyterlab 的安装与配置
下载并安装 Anaconda
[Anaconda](https://www.anaconda.com) 是目前最方便的 Python 发行版,搭载了很多我们终将必用的软件包,除了 Python 之外,还有 [R 语言](https://www.r-project.org/),还包括 [Pandoc](https://pandoc.org/),[NumPy](http://www.numpy.org/),[SciPy](https://www.scipy.org/),[Matplotlib](https://matplotlib.org/)…… 等等。
无论是图形化界面安装,还是命令行界面安装,建议都把 Anaconda 安装在本地用户目录内,`~/`。请下载并安装 Python 3.x 的版本。
图形化界面安装的教程,官方的很详细,各个操作平台的说明都有:
> https://docs.anaconda.com/anaconda/install/
在 MacOS 的 Terminal 命令行下,可以直接下载并安装:
```bash
cd ~/Downloads/
wget https://repo.anaconda.com/archive/Anaconda3-2018.12-MacOSX-x86_64.sh
chmod +x Anaconda3-2018.12-MacOSX-x86_64.sh
./Anaconda3-2018.12-MacOSX-x86_64.sh
```
安装到最后一步,会问你是否要安装微软出品的 [Visual Studio Code](https://code.visualstudio.com),选择 `yes` —— 反正以后你的电脑上会不止一个文本编辑器…… 以后你可能还会安装的文本编辑器包括 [SublimeText](https://www.sublimetext.com), [Atom](https://atom.io) 等等。
安装完毕之后,打开 Terminal(Windows 系统需要打开之前安装的 Anaconda Prompt 输入),继续安装几个组件:
```bash
conda update conda
conda update anaconda
conda install -c conda-forge nodejs
conda install -c conda-forge jupyterlab # 这是用来升级 jupyter lab 到最新版的方法
```
安装完毕之后,可以看看各个你将要用到的可执行命令都在什么地方,用 `which` 命令(windows下用 `where` 命令):
```bash
which python
python --version
which node
node -v
which jupyter
jupyter lab --version
jupyter notebook --version
which pip
pip --version
```
第一次启动 Jupyter lab
打开 Terminal,`cd` 到你想打开 Jupyter lab 的目录(就是你保存 `ipynb` 文件的地方,以便在 Jupyter lab 中打开、浏览、编辑 `ipynb` 文件),在这里以用户根目录为例 `~/`:
```bash
cd ~
jupyter lab
```
此时的 Terminal 窗口不能关闭,否则 Jupyter lab 就停止运行了 —— 就将它放在那里。
随后会有个浏览器打开,指向 [http://localhost:8888/lab?](http://localhost:8888/lab?) —— 你就看到 Jupyter lab 的操作界面了。
目前,Jupyter lab 和 Jupyter notebook 是并存的,虽然前者是后者的下一步替代者。如果你依然习惯于使用 Jupyter notebook,那么,在浏览器中指向 [http://localhost:8888/tree?](http://localhost:8888/tree?) 看到的就是 Jupyter notebook.
配置 Jupyter lab
打开 Terminal,输入以下命令:
```bash
jupyter lab --generate-config
```
这会在 `~/.jupyter/` 目录下生成一个 `jupyter_notebook_config.py` 文件。
```bash
cd ~/.jupyter
code jupyter_notebook_config.py
```
上面的 code 命令,需要你已经安装 Visual Studio Code,并且在已经在其中设置了 `Install 'code' command in PATH`。参见附录 [Visual Studio Code 的安装与配置](https://github.com/xiaolai/the-craft-of-selfteaching/blob/987a8e8a9ce205b63886510e145437d14d8a13a4/markdown/T-appendix.editor.vscode.md)
事实上,你可以用你喜欢的任何编辑器打开 `~/.jupyter/jupyter_notebook_config.py` 文件。
文件内容很长,有空可以仔细看。可以直接将以下内容拷贝粘贴到文件底部,根据需求修改:
```json
#c.NotebookApp.token = ''
#c.NotebookApp.open_browser = False
#c.NotebookApp.notebook_dir = '~/'
#c.NotebookApp.default_url = '/tree'
```
逐条解释一下:
> `c.NotebookApp.token = ''`
每次打开 jupyter,它都会给你生成一个新的 Token —— 这是安全策略。但是,如果你只是在自己的电脑上使用,那么,这就给你制造了麻烦,因为若是你想同时用另外一个浏览器打开它,那你就需要从 Terminal 里拷贝那个 Token 出来。所以,你可以在配置文件里直接把它设置为空。
> `c.NotebookApp.open_browser = False`
每次你执行 `jupyter lab` 或者 `jupyter notebook` 命令的时候,它都会使用系统默认浏览器。
每个人的习惯不一样。比如我,会想到用一个平时不怎么用的浏览器专门用在 Jupyter 上,这样会防止自己在关闭其它网页的时候不小心把 Jupyter 关掉…… 那我就会把这项设定为 `False`。
> `c.NotebookApp.notebook_dir = '~/'`
在 Terminal 中执行 `jupyter` 命令的时候,它默认是在你当前所在的工作目录打开 `jupyter`,这同样是出于安全考虑。但是,如果你只是在自己的电脑上使用,且只有自己在使用,那么莫不如直接把它设置成 `~/`,即,你的用户根目录,这样会很方便地访问各种地方的文件……
> `c.NotebookApp.default_url = '/tree'`
这一项留给那些依然习惯于使用 jupyter notebook 的人,这样设置之后,即便是输入 `jupyter lab` 命令,打开的还是 jupyter notebook。
在 Terminal 里常用的与 Jupyter 有关的命令有:
```bash
jupyter lab
jupyter lab --version
conda install -c conda-forge jupyterlab # 这是用来升级 jupyter lab 到最新版的方法
jupyter notebook list # 查看正在运行的 jupyter lab/notebook
jupyter notebook stop # 停止 jupyter lab/notebook 服务
```
将 Jupyter lab 配置成系统服务
如果,你厌烦每次都要跑到 Terminal 里启动 Jupyter lab,可以把它配置成系统服务,每次开机启动它就自动运行。而你需要做的只不过是直接从浏览器中访问 [http://localhost:8888/](http://localhost:8888/)。
```bash
code ~/Library/LaunchAgents/com.jupyter.lab.plist
```
这条命令会让 Visual Studio Code 创建 `~/Library/LaunchAgents/com.jupyter.lab.plist` 文件并打开。
在其中拷贝粘贴以下内容,注意,要把其中的 `your_username` 修改为你的用户名:
``` xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>local.job</string>
<key>ProgramArguments</key>
<array>
<string>/Users/your_username/anaconda3/bin/jupyter</string>
<string>lab</string>
<string>--no-browser</string>
<string>--notebook-dir=/Users/your_username/</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/tmp/local.job.err</string>
<key>StandardOutPath</key>
<string>/tmp/local.job.out</string>
</dict>
</plist>
```
如果之前在 `jupyter_notebook_config.py` 文件里已经设置过
```json
c.NotebookApp.open_browser = False
c.NotebookApp.notebook_dir = '~/'
```
那么这两行就可以不要了:
```xml
<string>--no-browser</string>
<string>--notebook-dir=/Users/your_username/</string>
```
而后在 Terminal 里执行:
```bash
launchctl load ~/Library/LaunchAgents/com.jupyter.lab.plist
```
如果你想重新启动这个服务,那么执行:
```bash
launchctl unload ~/Library/LaunchAgents/com.jupyter.lab.plist
launchctl load ~/Library/LaunchAgents/com.jupyter.lab.plist
```
关于 Jupyter lab themes
对中文用户来说,Jupyter 的默认字号有点过小,阅读起来不是很舒适。但最佳的方案不是去寻找合适的 themes,而是直接使用支持 [Stylus](https://github.com/openstyles/stylus) 这类终端 CSS 定制插件的浏览器,Chrome/Firefox/Opera 都支持 Stylus 插件。
我用的 Stylus 定制 CSS 是这样的:
```css
a {color: #2456A4 !important;}
strong {color:#6392BF;}
em {color: #A9312A; font-style: normal !important;}
table {font-size: 90% !important;}
#jp-main-dock-panel {background-color: #f9f9f9;}
.jp-RenderedHTMLCommon {font-family: "Yuanti SC"; font-size: 100%;}
.jp-Notebook {background-color: #fbfafa;}
.CodeMirror, .jp-RenderedHTMLCommon pre {font-size: 90%;}
.jp-RenderedHTMLCommon pre {
padding: 10px 25px;
background-color: #fafafa;
border-left: 4px solid #dadada;
border-radius: 10px;
}
.jp-RenderedHTMLCommon pre code {
background-color: #fafafa;
}
.jp-RenderedHTMLCommon h1 code,
.jp-RenderedHTMLCommon h2 code,
.jp-RenderedHTMLCommon h3 code,
.jp-RenderedHTMLCommon h4 code,
.jp-RenderedHTMLCommon p code,
.jp-RenderedHTMLCommon li code,
.jp-RenderedHTMLCommon blockquote p code,
.jp-RenderedHTMLCommon blockquote li code,
.jp-RenderedHTMLCommon td code {
background-color: #f6f6f6;
font-size: 90%;
color:#2e2e2e;
padding: 4px 4px;
margin: 0 8px;
box-shadow: 0px 1px 2px 0px rgba(0,0,0,0.2);
border-radius: 4px;
}
```
这样就相当于我把 JupyterLab Light 这个 Theme 稍微 Tweak 了一下。
另,我写的内容里,为了重点突出,特别定制了 `strong` 和 `em` 两个元素的显示,让它们以不同的颜色展示;又因为中文并不适合斜体展示,所以,把 `em` 的 `font-style` 设定为 `normal`……
安装插件
Jupyter notebook 经过很多年的发展,现在有很多扩展插件,但也有其中一些并不兼容最新的 Jupyter lab。不过,刚开始的时候用不着那么多插件,你只用其中的两个就足够开始了:
> * [@jupyterlab/toc](https://github.com/jupyterlab/jupyterlab-toc)
> * [ryantam626/jupyterlab_sublime](https://github.com/ryantam626/jupyterlab_sublime)
首先在用快捷键 `⌘ ,` 打开 jupyter lab 的 Advanced Settings,在 Extension Manager 中,添加 User Overrides:
```json
{
"enabled": true
}
```
> **注**:JupyterLab 3.x / 4.x 的 Extension Manager 已支持在 Settings 界面中直接勾选启用,不再需要手动添加上面这段 JSON 配置。
而后在 Terminal 执行以下命令安装插件:
```bash
jupyter labextension install @jupyterlab/toc
jupyter labextension install @ryantam626/jupyterlab_sublime
jupyter lab build
```
toc 插件,自动将 ipynb 文件中的标题转换成目录。
[〔图〕](https://github.com/jupyterlab/jupyterlab-toc/raw/master/toc.gif)
jupyterlab_sublime 则可以让你在 Jupyter lab 的 cell 中,使用跟 SublimeText 一样的快捷键,比如 `⌘ D` 能够多选其它与当前选中内容一样的内容;比如 `⌘` 加鼠标点击,可以生成多个可编辑点……
[〔图〕](http://blog.rtwilson.com/wp-content/uploads/2016/03/IPyNbSublime.gif)
常用快捷键
以下是 MacOS 下 Jupyter lab 最常用的快捷键。快捷键在两种模式下执行,进入编辑模式用 `⏎`,回到命令模式用 `⎋`(ESC)。
另外,代码编辑过程中需要安装 Jupyterlab 插件 [@ryantam626/jupyterlab_sublime](https://github.com/ryantam626/jupyterlab_sublime) 之后才能使用 “多行同时编辑功能”。
| 快捷键 | 说明 | 模式 |
| --------------------------------------- | ------------------------------------------------------------ | ------ |
| `ESC` | 从编辑模式回到命令模式 | 命令 |
| `A` | 在当前 Cell 之前插入一个 Cell | |
| `B` | 在当前 Cell 之后插入一个 Cell | |
| `D`, `D` | 连续按两次 `d` 键,删除当前 Cell | |
| `Y` | 将当前 Cell 设置为 Code Cell | |
| `M` | 将当前 Cell 设置为 Markdown Cell | |
| `^ ⇧ - ` | 将当前 Cell 拆分为两个 | 编辑 |
| `⇧ M` | 合并选中的 Cells | |
| `⇧ J` or `⇧ ↓` | 连续向下选中 Cells | |
| `⇧ K` or `⇧ ↑` | 连续向上选中 Cells | |
| `⇧ ⏎` or `^ ⏎` | 运行当前 Cell 中的代码 | |
| `⇧ L` | 显示/隐藏代码行号 | |
| `⏎` | 当前 Cell 进入编辑模式 | 编辑 |
| `⇥` | 自动补全代码 | |
| `⇧ ⇥` | 呼出当前光标下词汇的 Docstring | |
| `⌘ D` | Sublime Keymap: 选中下一个相同字符串 | |
| `⇧ ⌘ L` | Sublime Keymap: 在选中的行内启动多行同时编辑 | |
| `⌘ + Mouse Click` | 生成下一个可同时编辑的光标点 | |
增加一些必要的快捷键
在 Settings > Keyboard Shortcuts 中,可以设定一些常用但系统并未给出的快捷键:
```json
{
"notebook:move-cells-down-down": {
"command": "notebook:move-cell-down",
"keys": [
"Alt J"
],
"selector": ".jp-Notebook:focus",
"title": "Move Cells Down",
"category": "Notebook Cell Operations"
},
"notebook:move-cells-down-up": {
"command": "notebook:move-cell-up",
"keys": [
"Alt K"
],
"selector": ".jp-Notebook:focus",
"title": "Move Cells Down",
"category": "Notebook Cell Operations"
},
"notebook:enable-output-scrolling": {
"command": "notebook:enable-output-scrolling",
"keys": [
"S"
],
"selector": ".jp-Notebook:focus",
"title": "Enable output scrolling",
"category": "Notebook Cell Operations"
},
"notebook:disable-output-scrolling": {
"command": "notebook:disable-output-scrolling",
"keys": [
"Alt S"
],
"selector": ".jp-Notebook:focus",
"title": "Enable output scrolling",
"category": "Notebook Cell Operations"
}
}
```
这样就添加了 4 个快捷键:
> * `⌥ J`: Move selected cells down
> * `⌥ K`: Move selected cells up
> * `S`: Enable output scrolling
> * `⌥ S`: Disable output scrolling
比如 Move Selected cells up:
[〔图〕](https://user-images.githubusercontent.com/86304/37438938-b5bc4994-27b2-11e8-8f58-184a58b33ba4.gif)
输出所有变量内容
默认情况下,Code Cell 只输出最后一个可以被 evaluate 的值,用 `_` 代表之前刚刚被 evaluate 的值。
```python
[1, 2, 3]
```
[1, 2, 3]
```python
_ # 执行完上面的 Cell,试试这个 Cell; 而后执行完下面的 Cell 之后再重新执行一次当前这个 Cell
```
[1, 2, 3]
```python
(1, 2, 3)
{1, 2, 3}
```
{1, 2, 3}
于是,为了显示最近 evaluate 的多个值,我们总是不得不使用很多的 `print()`……
如果觉得这事比较烦的话,可以在 Cell 最上面写上:
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
```
如果还想更省事一点,就把这个设置写入配置文件:
```python
c.InteractiveShell.ast_node_interactivity = "all"
```
```python
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
(1, 2, 3)
{1, 2, 3}
```
(1, 2, 3)
{1, 2, 3}
魔法函数
在 Code Cell 里,可以运行一些 “魔法函数”(Magic Functions),这是秉承了 IPython 的特性。绝大多数在 IPython 里能够使用的魔法函数在 Jupyterlab 里都可以直接使用。完整的 IPython 魔法函数请参照:
> https://ipython.readthedocs.io/en/stable/interactive/magics.html
Jupyterlab 里较为常用的魔法函数整理如下:
| 魔法函数 | 说明 |
| -------------------- | ------------------------------------------------------------ |
| `%lsmagic` | 列出所有可被使用的 Jupyter lab 魔法函数 |
| `%run` | 在 Cell 中运行 `.py` 文件:`%run file_name` |
| `%who` | 列出所有当前 Global Scope 中的变量;类似的还有:`%who df`,`%whos` |
| `%env` | 列出当前的环境变量 |
| `%load` | 将其他文件内容导入 Cell,`%load source`,`source` 可以是文件名,也可以是 URL。|
| `%time` | 返回 Cell 内代码执行的时间,相关的还有 `%timeit` |
| `%writefile` | 把 Cell 的内容写入文件,`%write file_name`;%write -a file_name,`-a` 是追加 |
| `%matplotlib inline` | 行内展示 matplotlib 的结果 |
| `%%bash` | 运行随后的 shell 命令,比如 %%bash ls;与之类似的还有 `%%HTML`,`%%python2`,`%%python3`,`%%ruby`,`%%perl`…… |
桌面版 Jupyter App
Nteract
支持各个操作系统,很好看、很好用。有一个小缺点是,不支持 `input()` 函数的调用。
> https://nteract.io/desktop
[〔图:https://cloud.githubusercontent.com/assets/836375/18421299/d95ad398-783b-11e6-8b23-d54cf7caad1e.png〕](https://cloud.githubusercontent.com/assets/836375/18421299/d95ad398-783b-11e6-8b23-d54cf7caad1e.png)
Pineapple
只支持 MacOS,也很好用 —— 缺点就是很难看……
> https://nwhitehead.github.io/pineapple/
[〔图:https://nwhitehead.github.io/pineapple/images/sshots.png〕](https://nwhitehead.github.io/pineapple/images/sshots.png?raw=true)
这些符号都代表什么?
以下的表格你可以用很多次 —— 每次学一门新语言的时候,都可以拿它整理一遍思路……
最初的时候,人们能向计算机输入的只能是 ASCII 码表中的字符。于是从一开始,计算机科学家们就绞尽脑汁去琢磨怎么把这些符号用好、用足……
于是,ASCII 码表中的字符常常在不同的地方有不同的用处。比如,`.`,在操作系统中,常常当作文件名和扩展名之间的分隔符;在很多编程语言中,都被当作调用 Class Attributes 和 Class Methods 的符号;在正则表达式中,`.` 代表除 `\r` `\n` 之外的任意字符……
把下面的表格打印出来,整理一下,在表格里填写每个符号在 Python 中都是用来做什么的?<a href='#fn1' name='fn1b'><sup>[1]</sup></a>
> 当前文件夹之中,有 `symbols.numbers` 文件,是用来打印以下表格的……
以后不管学什么语言,就拿这个表格过一遍,到时候只有一个感觉:越学越简单!
很多人最初的时候学不进去,就是因为 “一些符号的用法太多了,经常混淆,于是就觉得累了、烦了……” 然而,只要多重复几次多在脑子里过几遍就熟悉了 —— 若是真的熟悉了、若是真的掌握了,你就会觉得:真的没什么啊!哪儿有那么难啊?!
[〔图〕](https://raw.githubusercontent.com/selfteaching/the-craft-of-selfteaching/master/images/symbols.png?raw=true)
学编程的一个好玩之处就是:
> 但凡你学会了任何一门编程语言,你就会发现你的键盘上的每一个键你都用得上……
可是在此之前,你跟人家花了同样的价钱买来的硬件设备,你竟然有那么多根本就没用过的东西!
普通人花同样价钱买来的是台机器而已;可是你却不一样 —— 用同样的价钱买来的是一个特别听话的机器人,一个可以替你不分昼夜不辞辛苦干活的机器人 —— 这感觉非常神奇。
-----
**脚注**
<a name='fn1'>[1]</a>:Python 语法,请参阅 The Python Language Reference [2. Lexical analysis](https://docs.python.org/3/reference/lexical_analysis.html#operators)
<a href='#fn1b'><small>↑Back to Content↑</small></a>
版权声明:© 超限思维 · LookMinds。本页为己方整理的亲口亲笔原稿处理稿/编者新译,非原文替代;原文版权归原作者/平台。本人原作请回原链。