第 12 章 成为真正的 Python 开发者
“想回到过去揍年轻的自己吗?选择软件开发作为你的事业吧!”
—— Elliot Loh(https://twitter.com/loh/status/411282297816498176)
本章会介绍 Python 开发中的艺术和科学,还有一些“最佳实践”。掌握它们之后,你就可以成为一个正牌的 Python 开发者。
12.1 关于编程
首先是一些基于我个人经历的和编程相关的建议。
最初我准备走科学这条路,我自学了编程来分析和展示实验数据。我以为计算机编程和会计一样——准确但是无聊。当我发现自己真正享受这个过程时非常惊讶。部分乐趣来自逻辑方面,就像解开谜题一样,但是还有一部分是创造力。你必须正确地编写程序才能得到正确的结果,但是可以用任何喜欢的方式来完成它。这是一种不寻常的左右脑平衡思考训练。
在掉进编程这个大坑后,我发现这个领域有很多方向,每个方向都有很多不同的任务和不同的人。你可以选择计算机图形学、操作系统、商业应用,甚至科学。
如果你是一个程序员,可能也有类似的经历。如果你不是,或许应该尝试一下编程,看看是否符合你的个性,至少也可以帮你完成一些工作。就像我在本书前面提到的一样,数学能力并不是那么重要。看起来逻辑思考的能力最重要,语言能力也很有用。最后,耐心很重要,尤其是寻找代码中的 bug 时。
12.2 寻找Python代码
当你需要开发功能时,最快的解决方法就是“偷”。好吧……是从一个经过允许的来源“偷”代码。
Python 标准库(http://docs.python.org/3/library/)既宽又深,而且特别整洁。深入进去能学到很多东西。
就像各种体育运动的名人堂一样,一个模块需要经过时间的检验才能加入标准库。新的包一直在出现,本书已经介绍过一些,它们有的能做一些新的东西,有些可以更好地完成旧的工作。Python 的广告语是内置电池(batteries included),但你可能需要一种新电池。
所以,除了标准库之外,还能去哪儿寻找优秀的 Python 代码呢?
首先要推荐的就是 Python 包索引(PyPi,https://pypi.python.org/pypi)。之前被命名为巨蟒剧团中的奶酪店(Cheese Shop)。这个网站上的 Python 包一直在更新,我编写本书时已经超过了 39 000 个。当你使用 pip
(参见下一节)时它就会搜索 PyPi。PyPi 的主页显示的是最近添加的包。你也可以直接搜索。举例来说,表 12-1 列出了 genealogy
的搜索结果。
表12-1:PyPi中搜索genealogy的结果
包 | 权重 | 描述 |
---|---|---|
Gramps 3.4.2 | 5 | 分析、组织并分享你的家谱 |
pythonfs-stack 0.2 | 2 | Python 封装过的所有 FamilySearch API |
human-names 0.1.1 | 1 | 人名 |
nameparser 0.2.8 | 1 | 一个简单的 Python 模块,用于将人名解析成具体的组成部件 |
最匹配的包权重最高,因此 Gramps
看起来最符合要求。可以去 Python 网站(https://pypi.python.org/pypi/Gramps/3.4.2)来查看文档和下载链接。
另一个很流行的仓库是 GitHub。可以在“流行”(https://github.com/trending?l=python)中查看当前流行的 Python 包。
流行 Python 菜谱(http://code.activestate.com/recipes/langs/python/)有 4000 多个短 Python 程序,涉及多个方面。
12.3 安装包
有 3 种安装 Python 包的方法:
推荐使用
pip
,你可以使用pip
来安装绝大多数 Python 包;有时可以使用操作系统自带的包管理工具;
从源代码安装。
如果你对同一个领域的多个包感兴趣,或许可以找到一个包含这些包的 Python 发行版。举例来说,在附录 C 中,可以使用一系列数学和科学程序,如果手动安装很麻烦,可以使用类似 Anaconda 这样的发行版。
12.3.1 使用pip
Python 的包有一些限制。之前有一个安装工具叫 easy_install
,现在已经被 pip
替代了,但是它们都没有成为标准的 Python 安装工具。如果想使用 pip
,如何安装它呢?从 Python 3.4 开始,pip
终于成为了 Python 的一部分,避免了不必要的步骤。如果使用的是 Python 3 之前的版本并且没有 pip
,那你可以从 http://www.pip-installer.org 下载。
pip
最简单的使用方法就是通过下面的命令安装一个包的最新版:
$ pip install flask
你会看到详细的安装过程,这样就可以确保安装正常进行:下载,运行 setup.py
,在硬盘上安装文件,等等。
也可以要求 pip
安装指定的版本:
$ pip install flask==0.9.0
或者指定最小版本(当你必须使用的一些特性在某个版本之后开始出现时,这个功能特别有用):
$ pip install 'flask>=0.9.0'f
在这条命令中,单引号可以防止 shell 把 >
解析成输出重定向,那样会把输出写入一个名为 =0.9.0
的文件中。
如果你想安装多个 Python 包,可以使用 requirements 文件(https://pip.pypa.io/en/latest/reference/pip_install.html#requirements-file-format)。虽然它有很多选项,但是最简单的使用方法是列出所有包,一个包一行,加上可选的目标版本或者相对版本:
$ pip -r requirements.txt
你的示例 requirements.txt 文件可能是这样:
flask==0.9.0
django
psycopg2
12.3.2 使用包管理工具
苹果的 OS X 中有第三方包管理工具 homebrew(brew
,http://brew.sh/)和 ports
(http://www.macports.org/)。它们的原理和 pip
类似,但并不是只能安装 Python 包。
Linux 的不同发行版有不同的包管理工具,最流行的是 apt-get
、yum
、dpkg
和 zypper
。
Windows 有 Windows 安装工具,需要后缀为 .msi
的包文件。如果想在 Windows 上安装 Python,那可能就是 MSI 格式的。
12.3.3 从源代码安装
有时候,一个 Python 包是新出的,或者作者还没有把它发布到 pip
上。如果要安装这样的包,通常需要这样做:
(1) 下载代码;
(2) 如果是压缩文件,使用 zip
、tar 或者其他合适的工具来解压缩;
(3) 在包含 setup.py 文件的目录中运行 python install setup.py
。
下载和安装时一定要小心。虽然在 Python 程序中加入病毒难度不小(因为这些程序是可读的文本),有时还是会遇到有毒的程序。
12.4 集成开发环境
我编写本书中的软件时使用的是纯文本编辑器,但是你不一定非要在命令行窗口或者纯文本编辑器中写代码。有许多免费和收费的集成开发环境(IDE),它们有图形窗口(GUI)并且支持文本编辑器、调试器、库搜索,等等。
12.4.1 IDLE
IDLE(https://docs.python.org/3/library/idle.html)是唯一一个标准发行版中包含的 Python IDE。它是基于 tkinter 开发的,图形界面比较简单。
12.4.2 PyCharm
PyCharm(http://www.jetbrains.com/pycharm/)是一个新出的 IDE,有许多特性。社区版是免费的,你也可以使用学生身份或者开源项目来获取免费的专业版许可。图 12-1 是初始界面。
图 12-1:PyCharm 的初始界面
12.4.3 IPython
IPython(http://ipython.org/)在附录 C 中有介绍,既是一个发布平台也是一个扩展 IDE。
12.5 命名和文档
你绝对记不住自己写过什么。很多次我看着自己最近写的代码,心里想的是:它们到底是从哪儿来的。这就是文档的重要性。文档可以包括注释和文档字符串,也可以把信息记录在变量名、函数名、模块名和类名中。不要像下面这样啰嗦:
>>> # 这里我要给变量"num"赋值10:
... num = 10
>>> # 我希望它确实赋值成功了
... print(num)
10
>>> # 好的
相反,要说清楚为什么要赋值 10
,为什么变量名是 num
。如果在编写华氏到摄氏度的转换,那应该让变量名自己表达它们的意义,而不是写一些魔法代码。加一些测试更好:
def ftoc(f_temp):
"把华氏温度<f_temp>转换为摄氏温度并返回"
f_boil_temp = 212.0
f_freeze_temp = 32.0
c_boil_temp = 100.0
c_freeze_temp = 0.0
f_range = f_boil_temp - f_freeze_temp
c_range = c_boil_temp - c_freeze_temp
f_c_ratio = c_range / f_range
c_temp = (f_temp - f_freeze_temp) * f_c_ratio + c_freeze_temp
return c_temp
if __name__ == '__main__':
for f_temp in [-40.0, 0.0, 32.0, 100.0, 212.0]:
c_temp = ftoc(f_temp)
print('%f F => %f C' % (f_temp, c_temp))
运行测试:
$ python ftoc1.py
-40.000000 F => -40.000000 C
0.000000 F => -17.777778 C
32.000000 F => 0.000000 C
100.000000 F => 37.777778 C
212.000000 F => 100.000000 C
这段代码(至少)有两处可以改进的地方。
Python 没有常量,但是 PEP8 格式规范建议(http://legacy.python.org/dev/peps/pep-0008/#constants)使用大写字母和下划线(比如
ALL_CAPS
)来表示常量名。我们据此修改示例中的常量。我们基于常量值提前进行了一些计算,应该把它们移动到模块顶层,这样它们就只会计算一次,否则每次调用
ftoc()
时都需要计算。
修改后的版本如下所示:
F_BOIL_TEMP = 212.0
F_FREEZE_TEMP = 32.0
C_BOIL_TEMP = 100.0
C_FREEZE_TEMP = 0.0
F_RANGE = F_BOIL_TEMP - F_FREEZE_TEMP
C_RANGE = C_BOIL_TEMP - C_FREEZE_TEMP
F_C_RATIO = C_RANGE / F_RANGE
def ftoc(f_temp):
"把华氏温度<f_temp>转换为摄氏温度并返回"
c_temp = (f_temp - F_FREEZE_TEMP) * F_C_RATIO + C_FREEZE_TEMP
return c_temp
if __name__ == '__main__':
for f_temp in [-40.0, 0.0, 32.0, 100.0, 212.0]:
c_temp = ftoc(f_temp)
print('%f F => %f C' % (f_temp, c_temp))
12.6 测试代码
有时候,我会修改一些代码,然后对自己说:“看起来没问题,发布吧。”接着程序就出问题了。唉,每次遇到这种情况时(还好这种情况已经越来越少了),我都觉得自己是个笨蛋并发誓下次要写更多的测试。
测试 Python 程序最简单的办法就是添加一些 print()
语句。Python 交互式解释器的读取 - 求值 - 打印循环(REPL)允许快速添加和测试修改。然而,你或许不会想要在产品级代码中添加 print()
语句,因此需要记住自己添加的所有 print()
语句并在最后删除它们。但是,这样做很容易出现剪切 - 粘贴错误。
12.6.1 使用pylint
、pyflakes
和pep8
检查代码
在创建真实的测试程序之前,需要运行 Python 代码检查器。最流行的是 pylint
(http://www.pylint.org/)和 pyflakes
(https://pypi.python.org/pypi/pyflakes/)。你可以使用 pip
来安装它们:
$ pip install pylint
$ pip install pyflakes
它们可以检查代码错误(比如在赋值之前引用变量)和代码风格问题(就像穿着格子衣服和条纹衣服一样)。下面是一段毫无意义的程序,有一个 bug 和一个风格问题:
a = 1
b = 2
print(a)
print(b)
print(c)
下面是 pylint
输出内容的一部分:
$ pylint style1.py
No config file found, using default configuration
************* Module style1
C: 1,0: Missing docstring
C: 1,0: Invalid name "a" for type constant
(should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
C: 2,0: Invalid name "b" for type constant
(should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
E: 5,6: Undefined variable 'c'
往下翻,Global evaluation
下面是我们的分数(最高为 10.0):
Your code has been rated at -3.33/10
好吧。我们首先来修复 bug。pylint
输出中以 E
开头的表示这是一个 Error
(错误),原因是我们在给 c
赋值之前打印了它。修复一下:
a = 1
b = 2
c = 3
print(a)
print(b)
print(c)
$ pylint style2.py
No config file found, using default configuration
************* Module style2
C: 1,0: Missing docstring
C: 1,0: Invalid name "a" for type constant
(should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
C: 2,0: Invalid name "b" for type constant
(should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
C: 3,0: Invalid name "c" for type constant
(should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
好的,没有 E
了,分数也从 -3.33 变成了 4.29:
Your code has been rated at 4.29/10
pylint
需要一个文档字符串(出现在模块或者函数内部第一行的一段短文本,用来描述代码),而且它认为短变量名 a
、b
和 c
太土了。我们来让 pylint
高兴点儿,把 style2.py 修改为 style3.py:
"这里是模块文档字符串"
def func():
"函数的文档字符串在这里。妈妈我在这儿!"
first = 1
second = 2
third = 3
print(first)
print(second)
print(third)
func()
$ pylint style3.py
No config file found, using default configuration
嘿,没有任何抱怨了。我们的分数呢?
Your code has been rated at 10.00/10
还可以,是吧?
另一个格式检查工具是 pep8
(https://pypi.python.org/pypi/pep8),可以使用熟悉的方式安装:
$ pip install pep8
它对我们的代码有何意见呢?
$ pep8 style3.py
style3.py:3:1: E302 expected 2 blank lines, found 1
为了满足格式要求,它建议我在文档字符串后面添加一个空行。
12.6.2 使用unittest
进行测试
我们已经通过了代码风格的考验,下面该真正地测试程序逻辑了。
最好先编写独立的测试程序,在提交代码到源码控制系统之前确保通过所有测试。写测试看起来是一件很麻烦的事,但是它们真的能帮助你更快地发现问题,尤其是回归测试(破坏之前还能正常工作的代码)。工程师们已经从惨痛的经历中领悟到一个真理:即使是很小的看起来没有任何问题的改动,也可能出问题。如果看那些优秀的 Python 包就会发现,它们大多都有测试集。
标准库中有两个测试包。首先介绍 unittest
。假设我们编写了一个单词首字母转大写的模块,第一版直接使用标准字符串函数 capitalize()
,之后会看到许多意料之外的结果。把下面的代码保存为 cap.py:
def just_do_it(text):
return text.capitalize()
测试就是先确定输入对应的期望输出(本例期望的输出是输入文本的首字母大写版本),然后把输入传入需要测试的函数,并检查返回值和期望输出是否相同。期望输出被称为断言,因此在 unittest
中,可以使用 assert
(断言)开头的方法来检查返回的结果,比如下面代码中的 assertEqual
方法。
把下面的测试脚本保存为 test_cap.py:
import unittest
import cap
class TestCap(unittest.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def test_one_word(self):
text = 'duck'
result = cap.just_do_it(text)
self.assertEqual(result, 'Duck')
def test_multiple_words(self):
text = 'a veritable flock of ducks'
result = cap.just_do_it(text)
self.assertEqual(result, 'A Veritable Flock Of Ducks')
if __name__ == '__main__':
unittest.main()
setUp()
方法会在每个测试方法执行之前执行,tearDown()
方法是在每个测试方法执行之后执行。它们通常用来分配和回收测试需要的外部资源,比如数据库连接或者一些测试数据。在本例中,我们的测试方法已经足够进行测试,因此不需要再定义 setUp()
和 tearDown()
,但是放一个空方法也没关系。我们测试的核心是函数 test_one_word()
和 test_multiple_words()
。它们会运行我们定义的 just_do_it()
函数,传入不同的输出并检查返回值是否和期望输出一样。
运行一下这个脚本,它会调用那两个测试方法:
$ python test_cap.py
F.
======================================================================
FAIL: test_multiple_words (__main__.TestCap)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_cap.py", line 20, in test_multiple_words
self.assertEqual(result, 'A Veritable Flock Of Ducks')
AssertionError: 'A veritable flock of ducks' != 'A Veritable Flock Of Ducks'
- A veritable flock of ducks
? ^ ^ ^ ^
+ A Veritable Flock Of Ducks
? ^ ^ ^ ^
----------------------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
看起来第一个测试(test_one_word
)通过了,但是第二个(test_multiple_words
)失败了。上箭头(^
)指出了字符串不相同的地方。
为什么多个单词会失败?可以阅读 string
的 capitalize
(https://docs.python.org/3/library/stdtypes.html#str.capitalize)函数文档来寻找线索:它只会把第一个单词的第一个字母转成大写。或许我们应该先阅读文档。
为了修复错误,我们需要另一个函数。往下翻一翻网页,可以看到 title()
函数(https://docs.python.org/3/library/stdtypes.html#str.title)。我们把 cap.py 中的 capitalize()
替换成 title()
:
def just_do_it(text):
return text.title()
再次运行测试,看看结果如何:
$ python test_cap.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
看起来没问题了。不过,其实还是有问题的。我们还需要在 test_cap.py 中添加另一个方法:
def test_words_with_apostrophes(self):
text = "I'm fresh out of ideas"
result = cap.just_do_it(text)
self.assertEqual(result, "I'm Fresh Out Of Ideas")
再试一次:
$ python test_cap.py
..F
======================================================================
FAIL: test_words_with_apostrophes (__main__.TestCap)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_cap.py", line 25, in test_words_with_apostrophes
self.assertEqual(result, "I'm Fresh Out Of Ideas")
AssertionError: "I'M Fresh Out Of Ideas" != "I'm Fresh Out Of Ideas"
- I'M Fresh Out Of Ideas
? ^
+ I'm Fresh Out Of Ideas
? ^
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1)
函数把 I'm
中的 m
大写了。浏览一下 title()
的文档会发现,它不能处理撇号。我们真得应该先完整地阅读一遍文档。
在标准库 string
文档的底部有另一个函数:一个名为 capwords()
的辅助函数。试试这个:
def just_do_it(text):
from string import capwords
return capwords(text)
$ python test_cap.py
...
----------------------------------------------------------------------
Ran 3 tests in 0.004s
OK
终于完成了!呃,还有问题。向 test_cap.py 中再加一个测试:
def test_words_with_quotes(self):
text = "\"You're despicable,\" said Daffy Duck"
result = cap.just_do_it(text)
self.assertEqual(result, "\"You're Despicable,\" Said Daffy Duck")
能通过吗?
$ python test_cap.py
...F
======================================================================
FAIL: test_words_with_quotes (__main__.TestCap)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_cap.py", line 30, in test_words_with_quotes
self.assertEqual(result, "\"You're
Despicable,\" Said Daffy Duck")
AssertionError: '"you\'re Despicable," Said Daffy Duck'
!= '"You\'re Despicable," Said Daffy Duck'
- "you're Despicable," Said Daffy Duck
? ^
+ "You're Despicable," Said Daffy Duck
? ^
----------------------------------------------------------------------
Ran 4 tests in 0.004s
FAILED (failures=1)
似乎第一个双引号并没有被目前为止最好用的函数 capwords
正确处理。它试着把 "
转成大写,并把其他内容转成小写(You're
)。此外,字符串的其余部分应该保持不变。
做测试的人可以发现这些边界条件,但是开发者在面对自己的代码时通常有盲区。
unittest
提供了数量不多但非常有用的断言,你可以用它们检查值、确保类能够匹配、判断是否触发错误,等等。
12.6.3 使用doctest
进行测试
标准库中的第二个测试包是 doctest
(https://docs.python.org/3/library/doctest.html)。使用这个包可以把测试写到文档字符串中,也可以起到文档的作用。它看起来有点像交互式解释器:字符 >>>
后面是一个函数调用,下一行是执行结果。你可以在交互式解释器中运行测试并把结果粘贴到测试文件中。我们修改一下 cap.py(暂时不考虑那个双引号的问题):
def just_do_it(text):
"""
>>> just_do_it('duck')
'Duck'
>>> just_do_it('a veritable flock of ducks')
'A Veritable Flock Of Ducks'
>>> just_do_it("I'm fresh out of ideas")
"I'm Fresh Out Of Ideas"
"""
from string import capwords
return capwords(text)
if __name__ == '__main__':
import doctest
doctest.testmod()
运行时如果测试全部通过不会产生任何输出:
$ python cap.py
加上冗杂选项(-v
),看看会出现什么:
$ python cap.py -v
Trying:
just_do_it('duck')
Expecting:
'Duck'
ok
Trying:
just_do_it('a veritable flock of ducks')
Expecting:
'A Veritable Flock Of Ducks'
ok
Trying:
just_do_it("I'm fresh out of ideas")
Expecting:
"I'm Fresh Out Of Ideas"
ok
1 items had no tests:
__main__
1 items passed all tests:
3 tests in __main__.just_do_it
3 tests in 2 items.
3 passed and 0 failed.
Test passed.
12.6.4 使用nose
进行测试
第三方包 nose
(https://nose.readthedocs.org/en/latest/)和 unittest
类似。下面是安装命令:
$ pip install nose
不需要像使用 unittest
一样创建一个包含测试方法的类。任何名称中带 test
的函数都会被执行。我们修改一下之前的 unittest
示例并保存为 test_cap_nose.py:
import cap
from nose.tools import eq_
def test_one_word():
text = 'duck'
result = cap.just_do_it(text)
eq_(result, 'Duck')
def test_multiple_words():
text = 'a veritable flock of ducks'
result = cap.just_do_it(text)
eq_(result, 'A Veritable Flock Of Ducks')
def test_words_with_apostrophes():
text = "I'm fresh out of ideas"
result = cap.just_do_it(text)
eq_(result, "I'm Fresh Out Of Ideas")
def test_words_with_quotes():
text = "\"You're despicable,\" said Daffy Duck"
result = cap.just_do_it(text)
eq_(result, "\"You're Despicable,\" Said Daffy Duck")
运行测试:
$ nosetests test_cap_nose.py
...F
======================================================================
FAIL: test_cap_nose.test_words_with_quotes
----------------------------------------------------------------------
Traceback (most recent call last):
File "Users.../site-packages/nose/case.py", line 198, in runTest
self.test(*self.arg)
File "Users.../book/test_cap_nose.py", line 23, in test_words_with_quotes
eq_(result, "\"You're Despicable,\" Said Daffy Duck")
AssertionError: '"you\'re Despicable," Said Daffy Duck'
!= '"You\'re Despicable," Said Daffy Duck'
----------------------------------------------------------------------
Ran 4 tests in 0.005s
FAILED (failures=1)
这和我们使用 unittest
进行测试得到的错误一样。幸运的是,在本章结尾的练习中,我们会修复它。
12.6.5 其他测试框架
出于某些原因,开发者会编写 Python 测试框架。如果你很感兴趣,可以试试 tox
(http://tox.readthedocs.org/en/latest/)和 py.test
(http://pytest.org/latest/)。
12.6.6 持续集成
当你的团队每天会产生很多代码时,就需要在出现改动时进行自动测试。你可以让源码控制系统,在提交代码时进行自动化测试。这样每个人都知道是谁破坏了构建并消失在吃午饭的人群中。
这些系统很庞大,我不会在这里介绍安装和使用方法。如果某一天,你需要用到它们,至少知道可以去哪儿找。
buildbot
(http://buildbot.net/)
用 Python 写成的源码控制系统,可以自动构建、测试和发布。
jenkins
(http://jenkins-ci.org/)
用 Java 写成,应该是目前最受欢迎的 CI(持续集成)系统。
travis-ci
(http://travis-ci.com/)
这个自动化项目托管在 GitHub 上,对开源项目是免费的。
12.7 调试Python代码
“调试的难度是写代码的两倍。以此类推,如果你绞尽脑汁编写巧妙的代码,那你一定无法调试它。”
——Brian Kernighan
测试先行。测试越完善,之后需要修复的 bug 越少。不过,bug 总是无法避免的,发现 bug 就要去修复它。之前就说过,Python 中最简单的调试方法就是打印字符串。vars()
是非常有用的一个函数,可以提取本地变量的值,包括函数参数:
>>> def func(*args, **kwargs):
... print(vars())
...
>>> func(1, 2, 3)
{'args': (1, 2, 3), 'kwargs': {}}
>>> func(['a', 'b', 'argh'])
{'args': (['a', 'b', 'argh'],), 'kwargs': {}}
就像 4.9 节介绍的,装饰器可以在不修改函数代码的前提下,在函数运行之前或者之后执行其他代码。这意味着你可以使用装饰器在任何 Python 函数(不仅是你自己写的函数)之前或者之后做一些事情。我们定义一个装饰器 dump
,它可以打印出输入的参数和函数的返回值(对设计师来说,垃圾堆通常需要装饰一下 1):
1dump 的意思是倾倒垃圾,在计算机中是一个常用的术语,用来打印一些信息。——译者注
def dump(func):
"打印输入参数和输出值"
def wrapped(*args, **kwargs):
print("Function name: %s" % func.__name__)
print("Input arguments: %s" % ' '.join(map(str, args)))
print("Input keyword arguments: %s" % kwargs.items())
output = func(*args, **kwargs)
print("Output:", output)
return output
return wrapped
下面是被装饰函数。这个函数名为 double()
,接收带名称和不带名称的数值参数,把它们的值乘 2 并放到一个列表中返回:
from dump1 import dump
@dump
def double(*args, **kwargs):
"每个参数乘2"
output_list = [ 2 arg for arg in args ]
output_dict = { k:2v for k,v in kwargs.items() }
return output_list, output_dict
if __name__ == '__main__':
output = double(3, 5, first=100, next=98.6, last=-40)
运行结果:
$ python test_dump.py
Function name: double
Input arguments: 3 5
Input keyword arguments: dict_items([('last', -40), ('first', 100),
('next', 98.6)])
Output: ([6, 10], {'last': -80, 'first': 200, 'next': 197.2})
12.8 使用pdb
进行调试
上面提到的技能很有用,但仍然无法代替真正的调试器。大多数 IDE 都自带了调试器,有各种各样的特性和用户界面。这里,我会介绍标准的 Python 调试器 pdb
(https://docs.python.org/3/library/pdb.html)。
如果使用
-i
标志运行程序,出现错误时 Python 会自动进入交互式解释器。
下面来看一个在处理某些数据时存在 bug 的程序,这种 bug 很难发现。这是计算机早期出现的一个真实的 bug,困扰了程序员们很长时间。
我们需要从一个文件中读出国家和它们的首都,它们由逗号分割:首都,国家。它们的大小写可能不正确,在输出时需要修复这个问题。哦,还有可能出现多余的空白,需要正确处理。最后,虽然程序知道是否到达文件结尾,出于某些原因,我们的领导希望在遇到单词 quit
时终止程序(大小写可能不正确)。下面是数据文件示例:
France, Paris
venuzuela,caracas
LithuniA,vilnius
quit
我们来设计一下算法(解决问题的方法)。下面是伪代码,它看起来像一个程序,其实只是用普通的语言来描述逻辑,还需要转换成真实的程序。程序员喜欢 Python 的一个原因就是它看起来很像伪代码,因此把伪代码转换成真正的程序比较简单:
for each line in the text file:
read the line
strip leading and trailing spaces
if 'quit' occurs in the lowercase copy of the line:
stop
else:
split the country and capital by the comma character
trim any leading and trailing spaces
convert the country and capital to titlecase
print the capital, a comma, and the country
为了满足要求,需要去掉名字首尾的空格,还需要使用小写形式来和 quit
进行比较,并把城市和国家名转换成首字母大写。据此,我们可以快速写出 capitals.py,看起来应该可以正常工作:
def process_cities(filename):
with open(filename, 'rt') as file:
for line in file:
line = line.strip()
if 'quit' in line.lower():
return
country, city = line.split(',')
city = city.strip()
country = country.strip()
print(city.title(), country.title(), sep=',')
if __name__ == '__main__':
import sys
process_cities(sys.argv[1])
使用之前准备好的示例数据文件来测试一下。预备、瞄准、发射:
$ python capitals.py cities1.csv
Paris,France
Caracas,Venuzuela
Vilnius,Lithunia
很好!通过测试,我们把它放到生产环境中测试一下,使用下面的数据文件来处理首都和国家名称,直到出错:
argentina,buenos aires
bolivia,la paz
brazil,brasilia
chile,santiago
colombia,Bogotá
ecuador,quito
falkland islands,stanley
french guiana,cayenne
guyana,georgetown
paraguay,Asunción
peru,lima
suriname,paramaribo
uruguay,montevideo
venezuela,caracas
quit
运行程序,只打印出 5 行内容,但是数据文件中有 15 行:
$ python capitals.py cities2.csv
Buenos Aires,Argentina
La Paz,Bolivia
Brazilia,Brazil
Santiago,Chile
Bogotá,Colombia
发生了什么?我们可以修改 capitals.py,在一些可能出错的地方添加 print()
语句,不过这里尝试一下调试器。
如果要使用调试器,需要在命令行中使用 -m pdb
来导入 pdb
模块,如下所示:
$ python -m pdb capitals.py cities2.csv
> Userswilliamlubanovic/book/capitals.py(1)<module>()
-> def process_cities(filename):
(Pdb)
这条命令会启动程序并停在第一行。如果你输入 c
(继续),程序会一直运行下去,直到正常结束或者出现错误:
(Pdb) c
Buenos Aires,Argentina
La Paz,Bolivia
Brazilia,Brazil
Santiago,Chile
Bogotá,Colombia
The program finished and will be restarted
> Userswilliamlubanovic/book/capitals.py(1)<module>()
-> def process_cities(filename):
程序正常结束,和没使用调试器之前一样。我们再试一次,使用一些命令来缩小问题出现的范围。看起来这是一个逻辑错误,而不是语法错误或者异常(否则会打印出错误信息)。
输入 s
(单步)来一行一行执行 Python 代码。这会单步执行所有 Python 代码:你自己的、标准库的、你用到的其他模块的。使用 s
时也会进入函数内部并继续单步执行。输入 n
(下一个)也可以单步执行,但是不会进入函数;如果遇到一个函数,n
会执行整个函数并前进到下一行代码。因此,不确定问题在哪时使用 s
,确定问题不在函数中时使用 n
,函数很长时尤其有用。通常来说,你需要单步执行自己的代码,跳过库代码,因为后者往往已经通过测试。我们在程序开头使用 s
来进入函数 process_cities()
:
(Pdb) s
> Userswilliamlubanovic/book/capitals.py(12)<module>()
-> if __name__ == '__main__':
(Pdb) s
> Userswilliamlubanovic/book/capitals.py(13)<module>()
-> import sys
(Pdb) s
> Userswilliamlubanovic/book/capitals.py(14)<module>()
-> process_cities(sys.argv[1])
(Pdb) s
--Call--
> Userswilliamlubanovic/book/capitals.py(1)process_cities()
-> def process_cities(filename):
(Pdb) s
> Userswilliamlubanovic/book/capitals.py(2)process_cities()
-> with open(filename, 'rt') as file:
输入 l
(列表)来查看之后的几行:
(Pdb) l
1 def process_cities(filename):
2 -> with open(filename, 'rt') as file:
3 for line in file:
4 line = line.strip()
5 if 'quit' in line.lower():
6 return
7 country, city = line.split(',')
8 city = city.strip()
9 country = country.strip()
10 print(city.title(), country.title(), sep=',')
11
(Pdb)
箭头(->
)指示当前行。
我们可以继续使用 s
或者 n
,看看是否能发现问题,不过这里使用调试器的另一个重要特性:断点。断点会把程序暂停在你指定的位置。在本例中,我们想知道 process_cities()
为什么在读完所有输入之前退出。第 3 行(for line in file:
)会读入输入文件的每一行,看起来没什么问题。函数中能在读入所有数据之前的地方只有第 6 行(return
)。我们在第 6 行设置一个断点:
(Pdb) b 6
Breakpoint 1 at Userswilliamlubanovic/book/capitals.py:6
接着继续运行程序,直到停在断点或者读入所有内容之后正常退出:
(Pdb) c
Buenos Aires,Argentina
La Paz,Bolivia
Brasilia,Brazil
Santiago,Chile
Bogotá,Colombia
> Userswilliamlubanovic/book/capitals.py(6)process_cities()
-> return
它在第 6 行的断点停下了,也就是说,程序在处理完哥伦比亚之后就准备退出了。打印 line
的值,看看读入的是什么:
(Pdb) p line
'ecuador,quito'
它有什么特别的吗?没有啊。
真的吗? quito ?我们的领导一定不希望正常数据中的字符串 quit
终止程序运行,看起来用它当哨兵值(表示终止运行)似乎是一个愚蠢的主意。你应该马上告诉他,我在这儿等你。
如果现在你还没丢掉工作,可以使用 b
命令查看所有的断点:
(Pdb) b
Num Type Disp Enb Where
1 breakpoint keep yes at Userswilliamlubanovic/book/capitals.py:6
breakpoint already hit 1 time
l
命令会显示代码行,当前行(->
)和断点(B
)。l
命令默认会显示从上次使用 l
之后直到现在的所有代码,可以包含一个可选的起始行(这里从第 1 行开始):
(Pdb) l 1
1 def process_cities(filename):
2 with open(filename, 'rt') as file:
3 for line in file:
4 line = line.strip()
5 if 'quit' in line.lower():
6 B-> return
7 country, city = line.split(',')
8 city = city.strip()
9 country = country.strip()
10 print(city.title(), country.title(), sep=',')
11
好了,修复一下 quit
问题,只让它在匹配整行时退出:
def process_cities(filename):
with open(filename, 'rt') as file:
for line in file:
line = line.strip()
if 'quit' == line.lower():
return
country, city = line.split(',')
city = city.strip()
country = country.strip()
print(city.title(), country.title(), sep=',')
if __name__ == '__main__':
import sys
process_cities(sys.argv[1])
再试一次:
$ python capitals2.py cities2.csv
Buenos Aires,Argentina
La Paz,Bolivia
Brasilia,Brazil
Santiago,Chile
Bogotá,Colombia
Quito,Ecuador
Stanley,Falkland Islands
Cayenne,French Guiana
Georgetown,Guyana
Asunción,Paraguay
Lima,Peru
Paramaribo,Suriname
Montevideo,Uruguay
Caracas,Venezuela
本章简单介绍了一下调试器,只是告诉你调试器可以做什么以及最常用的命令。
记住:测试越多,调试越少。
12.9 记录错误日志
有时候,你需要使用比 print()
更高端的工具来记录日志。日志通常是系统中的一个文件,用于持续记录信息。信息中通常会包含很多有用的内容,比如时间戳或者运行程序的用户的名字。通常来说,日志每天会被旋转(重命名)并压缩,这样它们就不会占用太多磁盘空间。如果程序出错,你可以查看对应的日志文件来了解发送了什么。异常信息非常重要,因为它们会告诉你出错的行数和原因。
Python 标准库模块中有一个 logging
(https://docs.python.org/3/library/logging.html)。我发现关于它的许多描述都很难懂。使用一段时间之后可以更好地理解,但是对于新手来说,有点太复杂了。logging
模块包含以下的内容:
你想保存到日志中的消息;
不同的优先级以及对应的函数:
debug()
、info()
、warn()
、error()
和critical()
;一个或多个 logger 对象,主要通过它们使用模块;
把消息写入终端、文件、数据库或者其他地方的 handler;
创建输出的 formatter;
基于输入进行筛选的过滤器。
下面是最简单的日志示例,导入模块并使用它的函数:
>>> import logging
>>> logging.debug("Looks like rain")
>>> logging.info("And hail")
>>> logging.warn("Did I hear thunder?")
WARNING:root:Did I hear thunder?
>>> logging.error("Was that lightning?")
ERROR:root:Was that lightning?
>>> logging.critical("Stop fencing and get inside!")
CRITICAL:root:Stop fencing and get inside!
看到了吗,debug()
和 info()
什么都没做,另外两个函数在每条消息之前打印出来级别 :root:
。到目前为止,它们很像有个性的 print()
语句,有些还对我们抱有敌意 2。
2因为有些消息的语气很严厉。——译者注
不过它们很有用。你可以在一个日志文件中搜索指定级别的消息,通过比较时间戳来看,在服务器崩溃之前发生了什么。
查看文档之后我们找到了第一个问题的答案(第二个稍后介绍):默认的优先级是 WARNING
,所以前两个函数没有输出。当调用第一个函数(logging.debug()
)时它就被锁定了。我们可以使用 basicConfig()
来设置默认的级别。DEBUG
是最低一级,因此下面的例子会打印出所有级别的消息:
>>> import logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> logging.debug("It's raining again")
DEBUG:root:It's raining again
>>> logging.info("With hail the size of hailstones")
INFO:root:With hail the size of hailstones
上面的例子都是直接使用默认的 logging
函数,没有创建 logger 对象。每个 logger 都有一个名称。创建一个名为 bunyan
的 logger:
>>> import logging
>>> logging.basicConfig(level='DEBUG')
>>> logger = logging.getLogger('bunyan')
>>> logger.debug('Timber!')
DEBUG:bunyan:Timber!
如果 logger 名称中包含点号,会生成不同层级的 logger,每层可以有不同的属性。也就是说,名为 quark
的 logger 比名为 quark.charmed 的层级更高。特殊的 root logger 在最顶层,它的名字是 ''
。
到目前为止,我们只是打印出消息,和 print()
的差别并不大。可以使用 handler 把消息输出到不同的地方。最常见的是写入日志文件,如下所示:
>>> import logging
>>> logging.basicConfig(level='DEBUG', filename='blue_ox.log')
>>> logger = logging.getLogger('bunyan')
>>> logger.debug("Where's my axe?")
>>> logger.warn("I need my axe")
>>>
啊哈,这些内容并没有出现在屏幕上,而是写入了文件 blue_ox.log:
DEBUG:bunyan:Where's my axe?
WARNING:bunyan:I need my axe
调用 basicConfig()
时使用 filename
参数会创建一个 FileHandler
并对 logger 进行设置。logging
模块至少包含 15 种 handler,比如电子邮件、Web 服务器、屏幕和文件。
最后,你可以控制消息的格式。在我们的第一个例子中,默认的格式是这样:
WARNING:root:消息...
如果给 basicConfig()
传入 format
字符串,可以改变格式:
>>> import logging
>>> fmt = '%(asctime)s %(levelname)s %(lineno)s %(message)s'
>>> logging.basicConfig(level='DEBUG', format=fmt)
>>> logger = logging.getLogger('bunyan')
>>> logger.error("Where's my other plaid shirt?")
2014-04-08 23:13:59,899 ERROR 1 Where's my other plaid shirt?
我们修改了消息的格式并让 logger 把消息输出到屏幕。logging
模块可以识别出 fmt
格式化字符串中的变量名。我们使用了 asctime
(一个包含日期和时间的 ISO 8601 字符串)、levelname
、lineno
(行号)和 message
。它们都是内置的变量,你也可以创建自定义变量。
logging
还有许多这里没有提到的功能,比如说,你可以同时把消息输出到多个位置,每个位置都有不同的优先级和格式。这个包的扩展性很强,不过有时候会牺牲一些简洁性。
12.10 优化代码
一般来说,Python 已经足够快了,直到它不够快的那一刻之前。大多数情况下,你都可以使用更好的算法或者数据结构来加速,关键是知道把这些技巧用在哪里。即使是经验丰富的程序员也常常会犯错误。你需要像裁缝一样耐心,在裁剪之前认真测量。下面来介绍一下计时器。
12.10.1 测量时间
之前已经看到过,time
模块中的 time
函数会返回一个浮点数,表示当前的纪元时间。测量时间最简单的方法就是先获取当前时间、做一些事情、获取新的时间,然后用新时间减去初始时间。具体代码是 time1.py:
from time import time
t1 = time()
num = 5
num *= 2
print(time() - t1)
在本例中,我们测量了把 5
赋值给变量 num
并给它乘以 2 所需要的时间。这并不是真正的基准测试,只是告诉你如何测量 Python 代码的执行时间。把它运行几次,看看变化幅度:
$ python time1.py
2.1457672119140625e-06
$ python time1.py
2.1457672119140625e-06
$ python time1.py
2.1457672119140625e-06
$ python time1.py
1.9073486328125e-06
$ python time1.py
3.0994415283203125e-06
大概是两百万或者三百万分之一秒。试试更慢的代码,比如 sleep
。如果睡眠一秒,计时器应该比一秒稍大一些。把下面的代码保存为 time2.py:
from time import time, sleep
t1 = time()
sleep(1.0)
print(time() - t1)
为了检验我们的猜测,多次运行这段代码:
$ python time2.py
1.000797986984253
$ python time2.py
1.0010130405426025
$ python time2.py
1.0010390281677246
意料之中,它需要运行一秒多一点。如果不是这个结果,那计时器和 sleep()
肯定有一个出了问题。
有一种更简单的方法来测量代码片段的执行时间:标准模块 timeit
(https://docs.python.org/3/library/timeit.html)。它有一个函数叫作(你应该能猜到)timeit()
,这个函数会运行你的测试代码 count 次并打印结果。调用形式:timeit.timeit(
code,number,count)
。
在本节的例子中,code
需要放在引号中,这样它们会在 timeit()
内部运行,而不是直接运行。(下一节会看到如何用 timeit()
来测量函数的运行时间。)运行一次之前的示例代码并计时。把下面的代码存为 timeit1.py:
from timeit import timeit
print(timeit('num = 5; num *= 2', number=1))
运行几次:
$ python timeit1.py
2.5600020308047533e-06
$ python timeit1.py
1.9020008039660752e-06
$ python timeit1.py
1.7380007193423808e-06
和上面差不多,这两行代码需要大约两百万分之一秒来运行。我们可以使用 timeit
模块的 repeat()
函数的 repeat
参数来运行多次。把下面的代码保存为 timeit2.py:
from timeit import repeat
print(repeat('num = 5; num *= 2', number=1, repeat=3))
运行一下:
$ python timeit2.py
[1.691998477326706e-06, 4.070025170221925e-07, 2.4700057110749185e-07]
第一次运行用了大约两百万分之一秒,第二和第三次运行快了很多。为什么?原因可能有很多,比如说,由于我们测试的代码片段太小,它的运行速度取决于计算机同时在做的其他事情、Python 系统对计算的优化方式以及其他许多事。
也有可能只是碰巧。下面来尝试一些相比变量赋值和 sleep
更加真实的代码。我们会测量一些代码并比较不同算法(程序逻辑)和数据结构(存储方式)的效率。
12.10.2 算法和数据结构
Python 之禅(http://legacy.python.org/dev/peps/pep-0020/)提到,应该有一种,最好只有一种,明显的解决方法。不幸的是,有时候并没有那么明显,你需要比较各种方案。举例来说,如果要构建一个列表,使用 for
循环好还是列表解析好?我们如何定义更好?是更快、更容易理解、占用内存更少还是更具 Python 风格?
在接下来的练习中,我们会用不同的方式构建列表,比较速度、可读性和 Python 风格。下面是 time_lists.py:
from timeit import timeit
def make_list_1():
result = []
for value in range(1000):
result.append(value)
return result
def make_list_2():
result = [value for value in range(1000)]
return result
print('make_list_1 takes', timeit(make_list_1, number=1000), 'seconds')
print('make_list_2 takes', timeit(make_list_2, number=1000), 'seconds')
在每个函数中,我们都向列表添加了 1000 个元素,还分别调用两个函数 1000 次。注意,这里调用 timeit()
时,传入的第一个参数是函数名不是字符串形式的代码。运行一下:
$ python time_lists.py
make_list_1 takes 0.14117428699682932 seconds
make_list_2 takes 0.06174145900149597 seconds
列表解析至少比用 append()
添加元素快两倍。通常来说,列表解析要比手动添加快。
可以使用这个方法来加速你的代码。
12.10.3 Cython、NumPy和C扩展
如果你已经尽了最大努力仍然无法达到想要的速度,还有一些方法可以选择。
Cython(http://cython.org/)混合了 Python 和 C,它的设计目的是把带有性能注释的 Python 代码翻译成 C 代码。这些注释非常简单,比如声明一些变量、函数参数或者函数返回值的类型。对于科学上的数字运算循环来说,添加这些注释之后会让程序快很多,速度能达到之前的一千倍。可以在 Cython wiki(https://github.com/cython/cython/wiki)查看文档和示例。
NumPy 是 Python 的一个数学库,它是用 C 语言编写的,运行速度很快。你可以在附录 C 中阅读 NumPy 的相关信息。
为了提高性能并且方便使用,Python 的很多代码和标准库都是用 C 写成并用 Python 进行封装。这些钩子可以直接在你的程序中使用。如果你熟悉 C 和 Python 并且真的想提高程序性能,可以写一个 C 扩展,这样做难度很大但是效果很好。
12.10.4 PyPy
大约 20 年前,Java 刚出现时,速度就像一只得了关节炎的雪纳瑞一样慢。当 Sun 和其他公司看到它的价值之后,它们投入了几百万美元来优化 Java 的解释器和底层的 Java 虚拟机(JVM),借鉴了其他语言(比如 Smalltalk 和 LISP)的许多技术。微软也投入了很大精力来优化它的 C# 语言和 .NET 虚拟机。
Python 不属于任何人,因此没人投入这么多努力来提高它的速度。你使用的很可能是标准的 Python 实现。它是由 C 写成,通常被称为 CPython(和 Cython 不一样)。
和 PHP、Perl 甚至 Java 一样,Python 并不会被编译成机器语言,而是被翻译成中间语言(通常被称为字节码或者 p-代码),然后被虚拟机解释执行。
PyPy(http://pypy.org/)是一个新出现的 Python 解释器,实现了许多 Java 中的加速技术。它的基准测试(http://speed.pypy.org/)显示 PyPy 几乎完全超越了 CPython——平均快 6 倍,最高快 20 倍。它支持 Python 2 和 Python 3,你可以下载并用它代替 CPython。PyPy 在不断改进,某一天可能会取代 CPython。可以阅读官网的最新发布内容,看看是否可以用在你的项目上。
12.11 源码控制
当你在一个小团队或者小项目中工作时,通常很容易跟踪自己的改动,直到你犯了很愚蠢的错误并花费很多时间来修复它。源码控制系统会保护你的代码不被破坏(比如被你自己的愚蠢错误)。如果你和其他开发者在一个团队工作,源码控制就变得极其重要。这个领域有很多收费和开源的包可以用。在 Python 所处的开源领域,最流行的是 Mercurial 和 Git,它们都是分布式版本控制系统,会生成代码仓库的多个副本。早期的系统,比如 Subversion,是工作在单个服务器上的。
12.11.1 Mercurial
Mercurial(http://mercurial.selenic.com/)是用 Python 写成的。它很容易学习,有很多有用的子命令,比如从 Mercurial 仓库下载代码、添加文件、提交改动、从不同的源合并改动。bitbucket(https://bitbucket.org/)和其他网站(https://mercurial.selenic.com/wiki/MercurialHosting)提供了免费或者收费的托管服务。
12.11.2 Git
Git(http://git-scm.com/)最初是用于 Linux 内核开发,但是现在基本上已经统治了开源领域。它和 Mercurial 很像,不过有些人发现它更难精通。GitHub(https://github.com/)是最大的 Git 托管平台,拥有超过一百万个仓库。除了 GitHub,还有一些其他的 Git 托管平台(https://git.wiki.kernel.org/index.php/GitHosting)。
本书中的程序示例都放在 GitHub(https://github.com/madscheme/introducing-python)的一个公共 Git 仓库中。如果你的计算机上有 git
程序,可以使用下面的命令来下载这些程序:
$ git clone https://github.com/madscheme/introducing-python
也可以点击 GitHub 页面上的按钮来下载代码:
点击“Clone in Desktop”来打开桌面版的
git
,如果之前已经安装过;点击“Download ZIP”来下载压缩归档的程序。
如果你没有 git
,但是想尝试一下,可以阅读安装教程(http://git-scm.com/book/en/v2/GettingStarted-Installing-Git)。我会介绍命令行版本的 git
,不过你也可以试试 GitHub 这样的网站,它们会提供额外的服务并且有时候会更好用。git
有许多特性,但有时候不太直观。
下面来尝试一下 git
。我们不会太深入,不过会展示一些命令和它们的输出。
创建一个新目录并进入:
$ mkdir newdir
$ cd newdir
在当前目录 newdir 中创建一个本地 Git 仓库:
$ git init
Initialized empty Git repository in Userswilliamlubanovic/newdir/.git/
在 newdir 中创建一个 Python 文件 test.py,内容如下所示:
print('Oops')
把这个文件添加到 Git 仓库:
$ git add test.py
Git 先生,感觉如何?
$ git status
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: test.py
这表示 test.py 已经是本地仓库的一部分,但是它的改动还没有被提交。我们来提交一下:
$ git commit -m "simple print program"
[master (root-commit) 52d60d7] my first commit
1 file changed, 1 insertion(+)
create mode 100644 test.py
-m "my first commit"
是你的提交说明。如果忽略说明, git
会打开一个编辑器并要求你输入说明。它会被记录到这个文件在 git
中的提交历史里。
看看现在的状态:
$ git status
On branch master
nothing to commit, working directory clean
好,所有改动都已经提交。这表示我们可以进行任何改动并且不用担心丢失原始版本。修改一下 test.py,把 Oops
改成 Ops!
并保存文件:
print('Ops!')
再来看看 git
的感觉:
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: test.py
no changes added to commit (use "git add" and/or "git commit -a")
使用 git diff
来查看上次提交之后的改动:
$ git diff
diff --git a/test.py b/test.py
index 76b8c39..62782b2 100644
--- a/test.py
+++ b/test.py
@@ -1 +1 @@
-print('Oops')
+print('Ops!')
如果尝试提交改动,git
会抱怨:
$ git commit -m "change the print string"
On branch master
Changes not staged for commit:
modified: test.py
no changes added to commit
staged for commit
的意思是需要先 add
文件,可以把这个动作翻译成:嘿,Git,往这儿看:
$ git add test.py
也可以输入 git add .
来添加当前目录下的所有改动文件。如果你修改了很多文件并且想要提交所有改动,这个命令会非常方便。现在提交改动:
$ git commit -m "my first change"
[master e1e11ec] my first change
1 file changed, 1 insertion(+), 1 deletion(-)
如果想查看你对 test.py 做的所有事情,按照时间倒序排列,可以使用 git log
:
$ git log test.py
commit e1e11ecf802ae1a78debe6193c552dcd15ca160a
Author: William Lubanovic <bill@madscheme.com>
Date: Tue May 13 23:34:59 2014 -0500
change the print string
commit 52d60d76594a62299f6fd561b2446c8b1227cfe1
Author: William Lubanovic <bill@madscheme.com>
Date: Tue May 13 23:26:14 2014 -0500
simple print program
12.12 复制本书代码
你可以下载本书中所有程序的代码。查看 Git 仓库(https://github.com/madscheme/introducing-python)并按照教程把仓库复制到你的本地电脑上。如果你有 git
,可以运行命令 git clone https://github.com/madscheme/introducing\-python
来复制仓库。你也可以下载 zip 格式的文件。
12.13 更多内容
这本书只是入门教程,大部分内容是基础知识,实践方面内容不多。下面,我会推荐一些有用的 Python 教程。
12.13.1 书
下面是我觉得非常有用的图书,从入门到精通都有,涵盖了 Python 2 和 Python 3:
Barry, Paul. Head First Python. O'Reilly, 2010.
Beazley, David M. Python Essential Reference (4th Edition). Addison-Wesley, 2009.
Beazley, David M. and Brian K. Jones. Python Cookbook (3rd Edition). O'Reilly, 2013.
Chun, Wesley. Core Python Applications Programming (3rd Edition). Prentice Hall, 2012.
McKinney, Wes. Python for Data Analysis: Data Wrangling with Pandas, NumPy, and IPython. O'Reilly, 2012.
Summerfield, Mark. Python in Practice: Create Better Programs Using Concurrency, Libraries, and Patterns. Addison-Wesley, 2013.
当然,除此之外还有很多(https://wiki.python.org/moin/PythonBooks)。
12.13.2 网站
可以在下面这些网站上找到很多有用的教程:
Zed Shaw 的 Learn Python the Hard Way(http://learnpythonthehardway.org/book/)
Mark Pilgrim 的 Dive Into Python 3(http://www.diveintopython3.net/)
Michael Driscoll 的 Mouse Vs. Python(http://www.blog.pythonlibrary.org/)
如果你对 Python 世界的新东西很感兴趣,可以看看下面这些网站:
comp.lang.python(https://groups.google.com/forum/#!forum/comp.lang.python)
comp.lang.python.announce(https://groups.google.com/forum/#!forum/comp.lang.python.announce)
python subreddit(http://www.reddit.com/r/python)
Planet Python(http://planet.python.org/)
最后是一些下载 Python 代码的好地方:
The Python Package Index(https://pypi.python.org/pypi)
stackoverflow Python questions(http://stackoverflow.com/questions/tagged/python)
ActiveState Python recipes(http://code.activestate.com/recipes/langs/python/)
Python packages trending on GitHub(https://github.com/trending?l=python)
12.13.3 社区
有很多计算机社区:热心的、好辩的、沉闷的、时髦的、无聊的,等等。Python 社区非常友好和民主。你可以基于位置发现 Python 社区——meetups(http://python.meetup.com/)和遍布全世界(https://wiki.python.org/moin/LocalUserGroups)的本地用户社区。还有一些分布式的基于共同兴趣的社区。举例来说,PyLadies(http://www.pyladies.com/)是由那些对 Python 和开源感兴趣的女性组成。
12.13.4 大会
Python 有遍布全世界(https://www.python.org/community/workshops/)的大会(http://www.pycon.org/)和实践课程,每年在南美(https://us.pycon.org/2015/)和欧洲(https://europython.eu/en/)举办的大会规模最大。
12.14 后续内容
等等,还没结束!附录 A、附录 B 和附录 C 分别介绍了 Python 在艺术、商业和科学方面的应用。你至少能找到一个感兴趣的包。
互联网上有很多闪闪发亮的东西,只有你自己知道,对你来说,哪些是装饰品哪些是银弹 3。即使你现在还没有遇到狼人,也应该在口袋中备一些银弹,以防万一。
3传说中,银弹是狼人的克星。在《人月神话》中,作者用银弹比喻能快速解决难题的方法。这里作者的意思也是如此,学会 Python 以备不时之需。——译者注
书的最后是每章结尾练习的答案、安装 Python 和其相关内容的详细教程,以及我觉得很有用的速查表。虽然你对这些东西已经很熟悉了,不过万一需要可以去书后查阅。