第 12 章 成为真正的 Python 开发者

“想回到过去揍年轻的自己吗?选择软件开发作为你的事业吧!”

—— Elliot Lohhttps://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 最简单的使用方法就是通过下面的命令安装一个包的最新版:

  1. $ pip install flask

你会看到详细的安装过程,这样就可以确保安装正常进行:下载,运行 setup.py,在硬盘上安装文件,等等。

也可以要求 pip 安装指定的版本:

  1. $ pip install flask==0.9.0

或者指定最小版本(当你必须使用的一些特性在某个版本之后开始出现时,这个功能特别有用):

  1. $ 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)。虽然它有很多选项,但是最简单的使用方法是列出所有包,一个包一行,加上可选的目标版本或者相对版本:

  1. $ pip -r requirements.txt

你的示例 requirements.txt 文件可能是这样:

  1. flask==0.9.0
  2. django
  3. psycopg2

12.3.2 使用包管理工具

苹果的 OS X 中有第三方包管理工具 homebrew(brewhttp://brew.sh/)和 portshttp://www.macports.org/)。它们的原理和 pip 类似,但并不是只能安装 Python 包。

Linux 的不同发行版有不同的包管理工具,最流行的是 apt-getyumdpkgzypper

Windows 有 Windows 安装工具,需要后缀为 .msi 的包文件。如果想在 Windows 上安装 Python,那可能就是 MSI 格式的。

12.3.3 从源代码安装

有时候,一个 Python 包是新出的,或者作者还没有把它发布到 pip 上。如果要安装这样的包,通常需要这样做:

(1) 下载代码;

(2) 如果是压缩文件,使用 zip、tar 或者其他合适的工具来解压缩;

(3) 在包含 setup.py 文件的目录中运行 python install setup.py

第 12 章 成为真正的 Python 开发者 - 图1 下载和安装时一定要小心。虽然在 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 命名和文档

你绝对记不住自己写过什么。很多次我看着自己最近写的代码,心里想的是:它们到底是从哪儿来的。这就是文档的重要性。文档可以包括注释和文档字符串,也可以把信息记录在变量名、函数名、模块名和类名中。不要像下面这样啰嗦:

  1. >>> # 这里我要给变量"num"赋值10:
  2. ... num = 10
  3. >>> # 我希望它确实赋值成功了
  4. ... print(num)
  5. 10
  6. >>> # 好的

相反,要说清楚为什么要赋值 10,为什么变量名是 num。如果在编写华氏到摄氏度的转换,那应该让变量名自己表达它们的意义,而不是写一些魔法代码。加一些测试更好:

  1. def ftoc(f_temp):
  2. "把华氏温度<f_temp>转换为摄氏温度并返回"
  3. f_boil_temp = 212.0
  4. f_freeze_temp = 32.0
  5. c_boil_temp = 100.0
  6. c_freeze_temp = 0.0
  7. f_range = f_boil_temp - f_freeze_temp
  8. c_range = c_boil_temp - c_freeze_temp
  9. f_c_ratio = c_range / f_range
  10. c_temp = (f_temp - f_freeze_temp) * f_c_ratio + c_freeze_temp
  11. return c_temp
  12. if __name__ == '__main__':
  13. for f_temp in [-40.0, 0.0, 32.0, 100.0, 212.0]:
  14. c_temp = ftoc(f_temp)
  15. print('%f F => %f C' % (f_temp, c_temp))

运行测试:

  1. $ python ftoc1.py
  2. -40.000000 F => -40.000000 C
  3. 0.000000 F => -17.777778 C
  4. 32.000000 F => 0.000000 C
  5. 100.000000 F => 37.777778 C
  6. 212.000000 F => 100.000000 C

这段代码(至少)有两处可以改进的地方。

  • Python 没有常量,但是 PEP8 格式规范建议(http://legacy.python.org/dev/peps/pep-0008/#constants)使用大写字母和下划线(比如 ALL_CAPS)来表示常量名。我们据此修改示例中的常量。

  • 我们基于常量值提前进行了一些计算,应该把它们移动到模块顶层,这样它们就只会计算一次,否则每次调用 ftoc() 时都需要计算。

修改后的版本如下所示:

  1. F_BOIL_TEMP = 212.0
  2. F_FREEZE_TEMP = 32.0
  3. C_BOIL_TEMP = 100.0
  4. C_FREEZE_TEMP = 0.0
  5. F_RANGE = F_BOIL_TEMP - F_FREEZE_TEMP
  6. C_RANGE = C_BOIL_TEMP - C_FREEZE_TEMP
  7. F_C_RATIO = C_RANGE / F_RANGE
  8. def ftoc(f_temp):
  9. "把华氏温度<f_temp>转换为摄氏温度并返回"
  10. c_temp = (f_temp - F_FREEZE_TEMP) * F_C_RATIO + C_FREEZE_TEMP
  11. return c_temp
  12. if __name__ == '__main__':
  13. for f_temp in [-40.0, 0.0, 32.0, 100.0, 212.0]:
  14. c_temp = ftoc(f_temp)
  15. print('%f F => %f C' % (f_temp, c_temp))

12.6 测试代码

有时候,我会修改一些代码,然后对自己说:“看起来没问题,发布吧。”接着程序就出问题了。唉,每次遇到这种情况时(还好这种情况已经越来越少了),我都觉得自己是个笨蛋并发誓下次要写更多的测试。

测试 Python 程序最简单的办法就是添加一些 print() 语句。Python 交互式解释器的读取 - 求值 - 打印循环(REPL)允许快速添加和测试修改。然而,你或许不会想要在产品级代码中添加 print() 语句,因此需要记住自己添加的所有 print() 语句并在最后删除它们。但是,这样做很容易出现剪切 - 粘贴错误。

12.6.1 使用pylintpyflakespep8检查代码

在创建真实的测试程序之前,需要运行 Python 代码检查器。最流行的是 pylinthttp://www.pylint.org/)和 pyflakeshttps://pypi.python.org/pypi/pyflakes/)。你可以使用 pip 来安装它们:

  1. $ pip install pylint
  2. $ pip install pyflakes

它们可以检查代码错误(比如在赋值之前引用变量)和代码风格问题(就像穿着格子衣服和条纹衣服一样)。下面是一段毫无意义的程序,有一个 bug 和一个风格问题:

  1. a = 1
  2. b = 2
  3. print(a)
  4. print(b)
  5. print(c)

下面是 pylint 输出内容的一部分:

  1. $ pylint style1.py
  2. No config file found, using default configuration
  3. ************* Module style1
  4. C: 1,0: Missing docstring
  5. C: 1,0: Invalid name "a" for type constant
  6. (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
  7. C: 2,0: Invalid name "b" for type constant
  8. (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
  9. E: 5,6: Undefined variable 'c'

往下翻,Global evaluation 下面是我们的分数(最高为 10.0):

  1. Your code has been rated at -3.33/10

好吧。我们首先来修复 bug。pylint 输出中以 E 开头的表示这是一个 Error(错误),原因是我们在给 c 赋值之前打印了它。修复一下:

  1. a = 1
  2. b = 2
  3. c = 3
  4. print(a)
  5. print(b)
  6. print(c)
  7. $ pylint style2.py
  8. No config file found, using default configuration
  9. ************* Module style2
  10. C: 1,0: Missing docstring
  11. C: 1,0: Invalid name "a" for type constant
  12. (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
  13. C: 2,0: Invalid name "b" for type constant
  14. (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
  15. C: 3,0: Invalid name "c" for type constant
  16. (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)

好的,没有 E 了,分数也从 -3.33 变成了 4.29:

  1. Your code has been rated at 4.29/10

pylint 需要一个文档字符串(出现在模块或者函数内部第一行的一段短文本,用来描述代码),而且它认为短变量名 abc 太土了。我们来让 pylint 高兴点儿,把 style2.py 修改为 style3.py:

  1. "这里是模块文档字符串"
  2. def func():
  3. "函数的文档字符串在这里。妈妈我在这儿!"
  4. first = 1
  5. second = 2
  6. third = 3
  7. print(first)
  8. print(second)
  9. print(third)
  10. func()
  11. $ pylint style3.py
  12. No config file found, using default configuration

嘿,没有任何抱怨了。我们的分数呢?

  1. Your code has been rated at 10.00/10

还可以,是吧?

另一个格式检查工具是 pep8https://pypi.python.org/pypi/pep8),可以使用熟悉的方式安装:

  1. $ pip install pep8

它对我们的代码有何意见呢?

  1. $ pep8 style3.py
  2. style3.py:3:1: E302 expected 2 blank lines, found 1

为了满足格式要求,它建议我在文档字符串后面添加一个空行。

12.6.2 使用unittest进行测试

我们已经通过了代码风格的考验,下面该真正地测试程序逻辑了。

最好先编写独立的测试程序,在提交代码到源码控制系统之前确保通过所有测试。写测试看起来是一件很麻烦的事,但是它们真的能帮助你更快地发现问题,尤其是回归测试(破坏之前还能正常工作的代码)。工程师们已经从惨痛的经历中领悟到一个真理:即使是很小的看起来没有任何问题的改动,也可能出问题。如果看那些优秀的 Python 包就会发现,它们大多都有测试集。

标准库中有两个测试包。首先介绍 unittest。假设我们编写了一个单词首字母转大写的模块,第一版直接使用标准字符串函数 capitalize(),之后会看到许多意料之外的结果。把下面的代码保存为 cap.py:

  1. def just_do_it(text):
  2. return text.capitalize()

测试就是先确定输入对应的期望输出(本例期望的输出是输入文本的首字母大写版本),然后把输入传入需要测试的函数,并检查返回值和期望输出是否相同。期望输出被称为断言,因此在 unittest 中,可以使用 assert(断言)开头的方法来检查返回的结果,比如下面代码中的 assertEqual 方法。

把下面的测试脚本保存为 test_cap.py:

  1. import unittest
  2. import cap
  3. class TestCap(unittest.TestCase):
  4. def setUp(self):
  5. pass
  6. def tearDown(self):
  7. pass
  8. def test_one_word(self):
  9. text = 'duck'
  10. result = cap.just_do_it(text)
  11. self.assertEqual(result, 'Duck')
  12. def test_multiple_words(self):
  13. text = 'a veritable flock of ducks'
  14. result = cap.just_do_it(text)
  15. self.assertEqual(result, 'A Veritable Flock Of Ducks')
  16. if __name__ == '__main__':
  17. unittest.main()

setUp() 方法会在每个测试方法执行之前执行,tearDown() 方法是在每个测试方法执行之后执行。它们通常用来分配和回收测试需要的外部资源,比如数据库连接或者一些测试数据。在本例中,我们的测试方法已经足够进行测试,因此不需要再定义 setUp()tearDown(),但是放一个空方法也没关系。我们测试的核心是函数 test_one_word()test_multiple_words()。它们会运行我们定义的 just_do_it() 函数,传入不同的输出并检查返回值是否和期望输出一样。

运行一下这个脚本,它会调用那两个测试方法:

  1. $ python test_cap.py
  2. F.
  3. ======================================================================
  4. FAIL: test_multiple_words (__main__.TestCap)
  5. ----------------------------------------------------------------------
  6. Traceback (most recent call last):
  7. File "test_cap.py", line 20, in test_multiple_words
  8. self.assertEqual(result, 'A Veritable Flock Of Ducks')
  9. AssertionError: 'A veritable flock of ducks' != 'A Veritable Flock Of Ducks'
  10. - A veritable flock of ducks
  11. ? ^ ^ ^ ^
  12. + A Veritable Flock Of Ducks
  13. ? ^ ^ ^ ^
  14. ----------------------------------------------------------------------
  15. Ran 2 tests in 0.001s
  16. FAILED (failures=1)

看起来第一个测试(test_one_word)通过了,但是第二个(test_multiple_words)失败了。上箭头(^)指出了字符串不相同的地方。

为什么多个单词会失败?可以阅读 stringcapitalizehttps://docs.python.org/3/library/stdtypes.html#str.capitalize)函数文档来寻找线索:它只会把第一个单词的第一个字母转成大写。或许我们应该先阅读文档。

为了修复错误,我们需要另一个函数。往下翻一翻网页,可以看到 title() 函数(https://docs.python.org/3/library/stdtypes.html#str.title)。我们把 cap.py 中的 capitalize() 替换成 title()

  1. def just_do_it(text):
  2. return text.title()

再次运行测试,看看结果如何:

  1. $ python test_cap.py
  2. ..
  3. ----------------------------------------------------------------------
  4. Ran 2 tests in 0.000s
  5. OK

看起来没问题了。不过,其实还是有问题的。我们还需要在 test_cap.py 中添加另一个方法:

  1. def test_words_with_apostrophes(self):
  2. text = "I'm fresh out of ideas"
  3. result = cap.just_do_it(text)
  4. self.assertEqual(result, "I'm Fresh Out Of Ideas")

再试一次:

  1. $ python test_cap.py
  2. ..F
  3. ======================================================================
  4. FAIL: test_words_with_apostrophes (__main__.TestCap)
  5. ----------------------------------------------------------------------
  6. Traceback (most recent call last):
  7. File "test_cap.py", line 25, in test_words_with_apostrophes
  8. self.assertEqual(result, "I'm Fresh Out Of Ideas")
  9. AssertionError: "I'M Fresh Out Of Ideas" != "I'm Fresh Out Of Ideas"
  10. - I'M Fresh Out Of Ideas
  11. ? ^
  12. + I'm Fresh Out Of Ideas
  13. ? ^
  14. ----------------------------------------------------------------------
  15. Ran 3 tests in 0.001s
  16. FAILED (failures=1)

函数把 I'm 中的 m 大写了。浏览一下 title() 的文档会发现,它不能处理撇号。我们真得应该先完整地阅读一遍文档。

在标准库 string 文档的底部有另一个函数:一个名为 capwords() 的辅助函数。试试这个:

  1. def just_do_it(text):
  2. from string import capwords
  3. return capwords(text)
  4. $ python test_cap.py
  5. ...
  6. ----------------------------------------------------------------------
  7. Ran 3 tests in 0.004s
  8. OK

终于完成了!呃,还有问题。向 test_cap.py 中再加一个测试:

  1. def test_words_with_quotes(self):
  2. text = "\"You're despicable,\" said Daffy Duck"
  3. result = cap.just_do_it(text)
  4. self.assertEqual(result, "\"You're Despicable,\" Said Daffy Duck")

能通过吗?

  1. $ python test_cap.py
  2. ...F
  3. ======================================================================
  4. FAIL: test_words_with_quotes (__main__.TestCap)
  5. ----------------------------------------------------------------------
  6. Traceback (most recent call last):
  7. File "test_cap.py", line 30, in test_words_with_quotes
  8. self.assertEqual(result, "\"You're
  9. Despicable,\" Said Daffy Duck")
  10. AssertionError: '"you\'re Despicable," Said Daffy Duck'
  11. != '"You\'re Despicable," Said Daffy Duck'
  12. - "you're Despicable," Said Daffy Duck
  13. ? ^
  14. + "You're Despicable," Said Daffy Duck
  15. ? ^
  16. ----------------------------------------------------------------------
  17. Ran 4 tests in 0.004s
  18. FAILED (failures=1)

似乎第一个双引号并没有被目前为止最好用的函数 capwords 正确处理。它试着把 " 转成大写,并把其他内容转成小写(You're)。此外,字符串的其余部分应该保持不变。

做测试的人可以发现这些边界条件,但是开发者在面对自己的代码时通常有盲区。

unittest 提供了数量不多但非常有用的断言,你可以用它们检查值、确保类能够匹配、判断是否触发错误,等等。

12.6.3 使用doctest进行测试

标准库中的第二个测试包是 doctesthttps://docs.python.org/3/library/doctest.html)。使用这个包可以把测试写到文档字符串中,也可以起到文档的作用。它看起来有点像交互式解释器:字符 >>> 后面是一个函数调用,下一行是执行结果。你可以在交互式解释器中运行测试并把结果粘贴到测试文件中。我们修改一下 cap.py(暂时不考虑那个双引号的问题):

  1. def just_do_it(text):
  2. """
  3. >>> just_do_it('duck')
  4. 'Duck'
  5. >>> just_do_it('a veritable flock of ducks')
  6. 'A Veritable Flock Of Ducks'
  7. >>> just_do_it("I'm fresh out of ideas")
  8. "I'm Fresh Out Of Ideas"
  9. """
  10. from string import capwords
  11. return capwords(text)
  12. if __name__ == '__main__':
  13. import doctest
  14. doctest.testmod()

运行时如果测试全部通过不会产生任何输出:

  1. $ python cap.py

加上冗杂选项(-v),看看会出现什么:

  1. $ python cap.py -v
  2. Trying:
  3. just_do_it('duck')
  4. Expecting:
  5. 'Duck'
  6. ok
  7. Trying:
  8. just_do_it('a veritable flock of ducks')
  9. Expecting:
  10. 'A Veritable Flock Of Ducks'
  11. ok
  12. Trying:
  13. just_do_it("I'm fresh out of ideas")
  14. Expecting:
  15. "I'm Fresh Out Of Ideas"
  16. ok
  17. 1 items had no tests:
  18. __main__
  19. 1 items passed all tests:
  20. 3 tests in __main__.just_do_it
  21. 3 tests in 2 items.
  22. 3 passed and 0 failed.
  23. Test passed.

12.6.4 使用nose进行测试

第三方包 nosehttps://nose.readthedocs.org/en/latest/)和 unittest 类似。下面是安装命令:

  1. $ pip install nose

不需要像使用 unittest 一样创建一个包含测试方法的类。任何名称中带 test 的函数都会被执行。我们修改一下之前的 unittest 示例并保存为 test_cap_nose.py:

  1. import cap
  2. from nose.tools import eq_
  3. def test_one_word():
  4. text = 'duck'
  5. result = cap.just_do_it(text)
  6. eq_(result, 'Duck')
  7. def test_multiple_words():
  8. text = 'a veritable flock of ducks'
  9. result = cap.just_do_it(text)
  10. eq_(result, 'A Veritable Flock Of Ducks')
  11. def test_words_with_apostrophes():
  12. text = "I'm fresh out of ideas"
  13. result = cap.just_do_it(text)
  14. eq_(result, "I'm Fresh Out Of Ideas")
  15. def test_words_with_quotes():
  16. text = "\"You're despicable,\" said Daffy Duck"
  17. result = cap.just_do_it(text)
  18. eq_(result, "\"You're Despicable,\" Said Daffy Duck")

运行测试:

  1. $ nosetests test_cap_nose.py
  2. ...F
  3. ======================================================================
  4. FAIL: test_cap_nose.test_words_with_quotes
  5. ----------------------------------------------------------------------
  6. Traceback (most recent call last):
  7. File "Users.../site-packages/nose/case.py", line 198, in runTest
  8. self.test(*self.arg)
  9. File "Users.../book/test_cap_nose.py", line 23, in test_words_with_quotes
  10. eq_(result, "\"You're Despicable,\" Said Daffy Duck")
  11. AssertionError: '"you\'re Despicable," Said Daffy Duck'
  12. != '"You\'re Despicable," Said Daffy Duck'
  13. ----------------------------------------------------------------------
  14. Ran 4 tests in 0.005s
  15. FAILED (failures=1)

这和我们使用 unittest 进行测试得到的错误一样。幸运的是,在本章结尾的练习中,我们会修复它。

12.6.5 其他测试框架

出于某些原因,开发者会编写 Python 测试框架。如果你很感兴趣,可以试试 toxhttp://tox.readthedocs.org/en/latest/)和 py.testhttp://pytest.org/latest/)。

12.6.6 持续集成

当你的团队每天会产生很多代码时,就需要在出现改动时进行自动测试。你可以让源码控制系统,在提交代码时进行自动化测试。这样每个人都知道是谁破坏了构建并消失在吃午饭的人群中。

这些系统很庞大,我不会在这里介绍安装和使用方法。如果某一天,你需要用到它们,至少知道可以去哪儿找。

用 Python 写成的源码控制系统,可以自动构建、测试和发布。

用 Java 写成,应该是目前最受欢迎的 CI(持续集成)系统。

这个自动化项目托管在 GitHub 上,对开源项目是免费的。

12.7 调试Python代码

“调试的难度是写代码的两倍。以此类推,如果你绞尽脑汁编写巧妙的代码,那你一定无法调试它。”

——Brian Kernighan

测试先行。测试越完善,之后需要修复的 bug 越少。不过,bug 总是无法避免的,发现 bug 就要去修复它。之前就说过,Python 中最简单的调试方法就是打印字符串。vars() 是非常有用的一个函数,可以提取本地变量的值,包括函数参数:

  1. >>> def func(*args, **kwargs):
  2. ... print(vars())
  3. ...
  4. >>> func(1, 2, 3)
  5. {'args': (1, 2, 3), 'kwargs': {}}
  6. >>> func(['a', 'b', 'argh'])
  7. {'args': (['a', 'b', 'argh'],), 'kwargs': {}}

就像 4.9 节介绍的,装饰器可以在不修改函数代码的前提下,在函数运行之前或者之后执行其他代码。这意味着你可以使用装饰器在任何 Python 函数(不仅是你自己写的函数)之前或者之后做一些事情。我们定义一个装饰器 dump,它可以打印出输入的参数和函数的返回值(对设计师来说,垃圾堆通常需要装饰一下 1):

1dump 的意思是倾倒垃圾,在计算机中是一个常用的术语,用来打印一些信息。——译者注

  1. def dump(func):
  2. "打印输入参数和输出值"
  3. def wrapped(*args, **kwargs):
  4. print("Function name: %s" % func.__name__)
  5. print("Input arguments: %s" % ' '.join(map(str, args)))
  6. print("Input keyword arguments: %s" % kwargs.items())
  7. output = func(*args, **kwargs)
  8. print("Output:", output)
  9. return output
  10. return wrapped

下面是被装饰函数。这个函数名为 double(),接收带名称和不带名称的数值参数,把它们的值乘 2 并放到一个列表中返回:

  1. from dump1 import dump
  2. @dump
  3. def double(*args, **kwargs):
  4. "每个参数乘2"
  5. output_list = [ 2 arg for arg in args ]
  6. output_dict = { k:2v for k,v in kwargs.items() }
  7. return output_list, output_dict
  8. if __name__ == '__main__':
  9. output = double(3, 5, first=100, next=98.6, last=-40)

运行结果:

  1. $ python test_dump.py
  2. Function name: double
  3. Input arguments: 3 5
  4. Input keyword arguments: dict_items([('last', -40), ('first', 100),
  5. ('next', 98.6)])
  6. Output: ([6, 10], {'last': -80, 'first': 200, 'next': 197.2})

12.8 使用pdb进行调试

上面提到的技能很有用,但仍然无法代替真正的调试器。大多数 IDE 都自带了调试器,有各种各样的特性和用户界面。这里,我会介绍标准的 Python 调试器 pdbhttps://docs.python.org/3/library/pdb.html)。

第 12 章 成为真正的 Python 开发者 - 图3 如果使用 -i 标志运行程序,出现错误时 Python 会自动进入交互式解释器。

下面来看一个在处理某些数据时存在 bug 的程序,这种 bug 很难发现。这是计算机早期出现的一个真实的 bug,困扰了程序员们很长时间。

我们需要从一个文件中读出国家和它们的首都,它们由逗号分割:首都,国家。它们的大小写可能不正确,在输出时需要修复这个问题。哦,还有可能出现多余的空白,需要正确处理。最后,虽然程序知道是否到达文件结尾,出于某些原因,我们的领导希望在遇到单词 quit 时终止程序(大小写可能不正确)。下面是数据文件示例:

  1. France, Paris
  2. venuzuela,caracas
  3. LithuniA,vilnius
  4. quit

我们来设计一下算法(解决问题的方法)。下面是伪代码,它看起来像一个程序,其实只是用普通的语言来描述逻辑,还需要转换成真实的程序。程序员喜欢 Python 的一个原因就是它看起来很像伪代码,因此把伪代码转换成真正的程序比较简单:

  1. for each line in the text file:
  2. read the line
  3. strip leading and trailing spaces
  4. if 'quit' occurs in the lowercase copy of the line:
  5. stop
  6. else:
  7. split the country and capital by the comma character
  8. trim any leading and trailing spaces
  9. convert the country and capital to titlecase
  10. print the capital, a comma, and the country

为了满足要求,需要去掉名字首尾的空格,还需要使用小写形式来和 quit 进行比较,并把城市和国家名转换成首字母大写。据此,我们可以快速写出 capitals.py,看起来应该可以正常工作:

  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. if __name__ == '__main__':
  12. import sys
  13. process_cities(sys.argv[1])

使用之前准备好的示例数据文件来测试一下。预备、瞄准、发射:

  1. $ python capitals.py cities1.csv
  2. Paris,France
  3. Caracas,Venuzuela
  4. Vilnius,Lithunia

很好!通过测试,我们把它放到生产环境中测试一下,使用下面的数据文件来处理首都和国家名称,直到出错:

  1. argentina,buenos aires
  2. bolivia,la paz
  3. brazil,brasilia
  4. chile,santiago
  5. colombia,Bogotá
  6. ecuador,quito
  7. falkland islands,stanley
  8. french guiana,cayenne
  9. guyana,georgetown
  10. paraguay,Asunción
  11. peru,lima
  12. suriname,paramaribo
  13. uruguay,montevideo
  14. venezuela,caracas
  15. quit

运行程序,只打印出 5 行内容,但是数据文件中有 15 行:

  1. $ python capitals.py cities2.csv
  2. Buenos Aires,Argentina
  3. La Paz,Bolivia
  4. Brazilia,Brazil
  5. Santiago,Chile
  6. Bogotá,Colombia

发生了什么?我们可以修改 capitals.py,在一些可能出错的地方添加 print() 语句,不过这里尝试一下调试器。

如果要使用调试器,需要在命令行中使用 -m pdb 来导入 pdb 模块,如下所示:

  1. $ python -m pdb capitals.py cities2.csv
  2. > Userswilliamlubanovic/book/capitals.py(1)<module>()
  3. -> def process_cities(filename):
  4. (Pdb)

这条命令会启动程序并停在第一行。如果你输入 c(继续),程序会一直运行下去,直到正常结束或者出现错误:

  1. (Pdb) c
  2. Buenos Aires,Argentina
  3. La Paz,Bolivia
  4. Brazilia,Brazil
  5. Santiago,Chile
  6. Bogotá,Colombia
  7. The program finished and will be restarted
  8. > Userswilliamlubanovic/book/capitals.py(1)<module>()
  9. -> def process_cities(filename):

程序正常结束,和没使用调试器之前一样。我们再试一次,使用一些命令来缩小问题出现的范围。看起来这是一个逻辑错误,而不是语法错误或者异常(否则会打印出错误信息)。

输入 s(单步)来一行一行执行 Python 代码。这会单步执行所有 Python 代码:你自己的、标准库的、你用到的其他模块的。使用 s 时也会进入函数内部并继续单步执行。输入 n(下一个)也可以单步执行,但是不会进入函数;如果遇到一个函数,n 会执行整个函数并前进到下一行代码。因此,不确定问题在哪时使用 s,确定问题不在函数中时使用 n,函数很长时尤其有用。通常来说,你需要单步执行自己的代码,跳过库代码,因为后者往往已经通过测试。我们在程序开头使用 s 来进入函数 process_cities()

  1. (Pdb) s
  2. > Userswilliamlubanovic/book/capitals.py(12)<module>()
  3. -> if __name__ == '__main__':
  4. (Pdb) s
  5. > Userswilliamlubanovic/book/capitals.py(13)<module>()
  6. -> import sys
  7. (Pdb) s
  8. > Userswilliamlubanovic/book/capitals.py(14)<module>()
  9. -> process_cities(sys.argv[1])
  10. (Pdb) s
  11. --Call--
  12. > Userswilliamlubanovic/book/capitals.py(1)process_cities()
  13. -> def process_cities(filename):
  14. (Pdb) s
  15. > Userswilliamlubanovic/book/capitals.py(2)process_cities()
  16. -> with open(filename, 'rt') as file:

输入 l(列表)来查看之后的几行:

  1. (Pdb) l
  2. 1 def process_cities(filename):
  3. 2 -> with open(filename, 'rt') as file:
  4. 3 for line in file:
  5. 4 line = line.strip()
  6. 5 if 'quit' in line.lower():
  7. 6 return
  8. 7 country, city = line.split(',')
  9. 8 city = city.strip()
  10. 9 country = country.strip()
  11. 10 print(city.title(), country.title(), sep=',')
  12. 11
  13. (Pdb)

箭头(->)指示当前行。

我们可以继续使用 s 或者 n,看看是否能发现问题,不过这里使用调试器的另一个重要特性:断点。断点会把程序暂停在你指定的位置。在本例中,我们想知道 process_cities() 为什么在读完所有输入之前退出。第 3 行(for line in file:)会读入输入文件的每一行,看起来没什么问题。函数中能在读入所有数据之前的地方只有第 6 行(return)。我们在第 6 行设置一个断点:

  1. (Pdb) b 6
  2. Breakpoint 1 at Userswilliamlubanovic/book/capitals.py:6

接着继续运行程序,直到停在断点或者读入所有内容之后正常退出:

  1. (Pdb) c
  2. Buenos Aires,Argentina
  3. La Paz,Bolivia
  4. Brasilia,Brazil
  5. Santiago,Chile
  6. Bogotá,Colombia
  7. > Userswilliamlubanovic/book/capitals.py(6)process_cities()
  8. -> return

它在第 6 行的断点停下了,也就是说,程序在处理完哥伦比亚之后就准备退出了。打印 line 的值,看看读入的是什么:

  1. (Pdb) p line
  2. 'ecuador,quito'

它有什么特别的吗?没有啊。

真的吗? quito ?我们的领导一定不希望正常数据中的字符串 quit 终止程序运行,看起来用它当哨兵值(表示终止运行)似乎是一个愚蠢的主意。你应该马上告诉他,我在这儿等你。

如果现在你还没丢掉工作,可以使用 b 命令查看所有的断点:

  1. (Pdb) b
  2. Num Type Disp Enb Where
  3. 1 breakpoint keep yes at Userswilliamlubanovic/book/capitals.py:6
  4. breakpoint already hit 1 time

l 命令会显示代码行,当前行(->)和断点(B)。l 命令默认会显示从上次使用 l 之后直到现在的所有代码,可以包含一个可选的起始行(这里从第 1 行开始):

  1. (Pdb) l 1
  2. 1 def process_cities(filename):
  3. 2 with open(filename, 'rt') as file:
  4. 3 for line in file:
  5. 4 line = line.strip()
  6. 5 if 'quit' in line.lower():
  7. 6 B-> return
  8. 7 country, city = line.split(',')
  9. 8 city = city.strip()
  10. 9 country = country.strip()
  11. 10 print(city.title(), country.title(), sep=',')
  12. 11

好了,修复一下 quit 问题,只让它在匹配整行时退出:

  1. def process_cities(filename):
  2. with open(filename, 'rt') as file:
  3. for line in file:
  4. line = line.strip()
  5. if 'quit' == 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. if __name__ == '__main__':
  12. import sys
  13. process_cities(sys.argv[1])

再试一次:

  1. $ python capitals2.py cities2.csv
  2. Buenos Aires,Argentina
  3. La Paz,Bolivia
  4. Brasilia,Brazil
  5. Santiago,Chile
  6. Bogotá,Colombia
  7. Quito,Ecuador
  8. Stanley,Falkland Islands
  9. Cayenne,French Guiana
  10. Georgetown,Guyana
  11. Asunción,Paraguay
  12. Lima,Peru
  13. Paramaribo,Suriname
  14. Montevideo,Uruguay
  15. Caracas,Venezuela

本章简单介绍了一下调试器,只是告诉你调试器可以做什么以及最常用的命令。

记住:测试越多,调试越少。

12.9 记录错误日志

有时候,你需要使用比 print() 更高端的工具来记录日志。日志通常是系统中的一个文件,用于持续记录信息。信息中通常会包含很多有用的内容,比如时间戳或者运行程序的用户的名字。通常来说,日志每天会被旋转(重命名)并压缩,这样它们就不会占用太多磁盘空间。如果程序出错,你可以查看对应的日志文件来了解发送了什么。异常信息非常重要,因为它们会告诉你出错的行数和原因。

Python 标准库模块中有一个 logginghttps://docs.python.org/3/library/logging.html)。我发现关于它的许多描述都很难懂。使用一段时间之后可以更好地理解,但是对于新手来说,有点太复杂了。logging 模块包含以下的内容:

  • 你想保存到日志中的消息;

  • 不同的优先级以及对应的函数:debug()info()warn()error()critical()

  • 一个或多个 logger 对象,主要通过它们使用模块;

  • 把消息写入终端、文件、数据库或者其他地方的 handler;

  • 创建输出的 formatter;

  • 基于输入进行筛选的过滤器。

下面是最简单的日志示例,导入模块并使用它的函数:

  1. >>> import logging
  2. >>> logging.debug("Looks like rain")
  3. >>> logging.info("And hail")
  4. >>> logging.warn("Did I hear thunder?")
  5. WARNING:root:Did I hear thunder?
  6. >>> logging.error("Was that lightning?")
  7. ERROR:root:Was that lightning?
  8. >>> logging.critical("Stop fencing and get inside!")
  9. CRITICAL:root:Stop fencing and get inside!

看到了吗,debug()info() 什么都没做,另外两个函数在每条消息之前打印出来级别 :root:。到目前为止,它们很像有个性的 print() 语句,有些还对我们抱有敌意 2。

2因为有些消息的语气很严厉。——译者注

不过它们很有用。你可以在一个日志文件中搜索指定级别的消息,通过比较时间戳来看,在服务器崩溃之前发生了什么。

查看文档之后我们找到了第一个问题的答案(第二个稍后介绍):默认的优先级是 WARNING,所以前两个函数没有输出。当调用第一个函数(logging.debug())时它就被锁定了。我们可以使用 basicConfig() 来设置默认的级别。DEBUG 是最低一级,因此下面的例子会打印出所有级别的消息:

  1. >>> import logging
  2. >>> logging.basicConfig(level=logging.DEBUG)
  3. >>> logging.debug("It's raining again")
  4. DEBUG:root:It's raining again
  5. >>> logging.info("With hail the size of hailstones")
  6. INFO:root:With hail the size of hailstones

上面的例子都是直接使用默认的 logging 函数,没有创建 logger 对象。每个 logger 都有一个名称。创建一个名为 bunyan 的 logger:

  1. >>> import logging
  2. >>> logging.basicConfig(level='DEBUG')
  3. >>> logger = logging.getLogger('bunyan')
  4. >>> logger.debug('Timber!')
  5. DEBUG:bunyan:Timber!

如果 logger 名称中包含点号,会生成不同层级的 logger,每层可以有不同的属性。也就是说,名为 quark 的 logger 比名为 quark.charmed 的层级更高。特殊的 root logger 在最顶层,它的名字是 ''

到目前为止,我们只是打印出消息,和 print() 的差别并不大。可以使用 handler 把消息输出到不同的地方。最常见的是写入日志文件,如下所示:

  1. >>> import logging
  2. >>> logging.basicConfig(level='DEBUG', filename='blue_ox.log')
  3. >>> logger = logging.getLogger('bunyan')
  4. >>> logger.debug("Where's my axe?")
  5. >>> logger.warn("I need my axe")
  6. >>>

啊哈,这些内容并没有出现在屏幕上,而是写入了文件 blue_ox.log:

  1. DEBUG:bunyan:Where's my axe?
  2. WARNING:bunyan:I need my axe

调用 basicConfig() 时使用 filename 参数会创建一个 FileHandler 并对 logger 进行设置。logging 模块至少包含 15 种 handler,比如电子邮件、Web 服务器、屏幕和文件。

最后,你可以控制消息的格式。在我们的第一个例子中,默认的格式是这样:

  1. WARNING:root:消息...

如果给 basicConfig() 传入 format 字符串,可以改变格式:

  1. >>> import logging
  2. >>> fmt = '%(asctime)s %(levelname)s %(lineno)s %(message)s'
  3. >>> logging.basicConfig(level='DEBUG', format=fmt)
  4. >>> logger = logging.getLogger('bunyan')
  5. >>> logger.error("Where's my other plaid shirt?")
  6. 2014-04-08 23:13:59,899 ERROR 1 Where's my other plaid shirt?

我们修改了消息的格式并让 logger 把消息输出到屏幕。logging 模块可以识别出 fmt 格式化字符串中的变量名。我们使用了 asctime(一个包含日期和时间的 ISO 8601 字符串)、levelnamelineno(行号)和 message。它们都是内置的变量,你也可以创建自定义变量。

logging 还有许多这里没有提到的功能,比如说,你可以同时把消息输出到多个位置,每个位置都有不同的优先级和格式。这个包的扩展性很强,不过有时候会牺牲一些简洁性。

12.10 优化代码

一般来说,Python 已经足够快了,直到它不够快的那一刻之前。大多数情况下,你都可以使用更好的算法或者数据结构来加速,关键是知道把这些技巧用在哪里。即使是经验丰富的程序员也常常会犯错误。你需要像裁缝一样耐心,在裁剪之前认真测量。下面来介绍一下计时器

12.10.1 测量时间

之前已经看到过,time 模块中的 time 函数会返回一个浮点数,表示当前的纪元时间。测量时间最简单的方法就是先获取当前时间、做一些事情、获取新的时间,然后用新时间减去初始时间。具体代码是 time1.py:

  1. from time import time
  2. t1 = time()
  3. num = 5
  4. num *= 2
  5. print(time() - t1)

在本例中,我们测量了把 5 赋值给变量 num 并给它乘以 2 所需要的时间。这并不是真正的基准测试,只是告诉你如何测量 Python 代码的执行时间。把它运行几次,看看变化幅度:

  1. $ python time1.py
  2. 2.1457672119140625e-06
  3. $ python time1.py
  4. 2.1457672119140625e-06
  5. $ python time1.py
  6. 2.1457672119140625e-06
  7. $ python time1.py
  8. 1.9073486328125e-06
  9. $ python time1.py
  10. 3.0994415283203125e-06

大概是两百万或者三百万分之一秒。试试更慢的代码,比如 sleep。如果睡眠一秒,计时器应该比一秒稍大一些。把下面的代码保存为 time2.py:

  1. from time import time, sleep
  2. t1 = time()
  3. sleep(1.0)
  4. print(time() - t1)

为了检验我们的猜测,多次运行这段代码:

  1. $ python time2.py
  2. 1.000797986984253
  3. $ python time2.py
  4. 1.0010130405426025
  5. $ python time2.py
  6. 1.0010390281677246

意料之中,它需要运行一秒多一点。如果不是这个结果,那计时器和 sleep() 肯定有一个出了问题。

有一种更简单的方法来测量代码片段的执行时间:标准模块 timeithttps://docs.python.org/3/library/timeit.html)。它有一个函数叫作(你应该能猜到)timeit(),这个函数会运行你的测试代码 count 次并打印结果。调用形式:timeit.timeit(code,number,count)

在本节的例子中,code 需要放在引号中,这样它们会在 timeit() 内部运行,而不是直接运行。(下一节会看到如何用 timeit() 来测量函数的运行时间。)运行一次之前的示例代码并计时。把下面的代码存为 timeit1.py:

  1. from timeit import timeit
  2. print(timeit('num = 5; num *= 2', number=1))

运行几次:

  1. $ python timeit1.py
  2. 2.5600020308047533e-06
  3. $ python timeit1.py
  4. 1.9020008039660752e-06
  5. $ python timeit1.py
  6. 1.7380007193423808e-06

和上面差不多,这两行代码需要大约两百万分之一秒来运行。我们可以使用 timeit 模块的 repeat() 函数的 repeat 参数来运行多次。把下面的代码保存为 timeit2.py:

  1. from timeit import repeat
  2. print(repeat('num = 5; num *= 2', number=1, repeat=3))

运行一下:

  1. $ python timeit2.py
  2. [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:

  1. from timeit import timeit
  2. def make_list_1():
  3. result = []
  4. for value in range(1000):
  5. result.append(value)
  6. return result
  7. def make_list_2():
  8. result = [value for value in range(1000)]
  9. return result
  10. print('make_list_1 takes', timeit(make_list_1, number=1000), 'seconds')
  11. print('make_list_2 takes', timeit(make_list_2, number=1000), 'seconds')

在每个函数中,我们都向列表添加了 1000 个元素,还分别调用两个函数 1000 次。注意,这里调用 timeit() 时,传入的第一个参数是函数名不是字符串形式的代码。运行一下:

  1. $ python time_lists.py
  2. make_list_1 takes 0.14117428699682932 seconds
  3. 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 程序,可以使用下面的命令来下载这些程序:

  1. $ 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。我们不会太深入,不过会展示一些命令和它们的输出。

创建一个新目录并进入:

  1. $ mkdir newdir
  2. $ cd newdir

在当前目录 newdir 中创建一个本地 Git 仓库:

  1. $ git init
  2. Initialized empty Git repository in Userswilliamlubanovic/newdir/.git/

在 newdir 中创建一个 Python 文件 test.py,内容如下所示:

  1. print('Oops')

把这个文件添加到 Git 仓库:

  1. $ git add test.py

Git 先生,感觉如何?

  1. $ git status
  2. On branch master
  3. Initial commit
  4. Changes to be committed:
  5. (use "git rm --cached <file>..." to unstage)
  6. new file: test.py

这表示 test.py 已经是本地仓库的一部分,但是它的改动还没有被提交。我们来提交一下:

  1. $ git commit -m "simple print program"
  2. [master (root-commit) 52d60d7] my first commit
  3. 1 file changed, 1 insertion(+)
  4. create mode 100644 test.py

-m "my first commit" 是你的提交说明。如果忽略说明, git 会打开一个编辑器并要求你输入说明。它会被记录到这个文件在 git 中的提交历史里。

看看现在的状态:

  1. $ git status
  2. On branch master
  3. nothing to commit, working directory clean

好,所有改动都已经提交。这表示我们可以进行任何改动并且不用担心丢失原始版本。修改一下 test.py,把 Oops 改成 Ops! 并保存文件:

  1. print('Ops!')

再来看看 git 的感觉:

  1. $ git status
  2. On branch master
  3. Changes not staged for commit:
  4. (use "git add <file>..." to update what will be committed)
  5. (use "git checkout -- <file>..." to discard changes in working directory)
  6. modified: test.py
  7. no changes added to commit (use "git add" and/or "git commit -a")

使用 git diff 来查看上次提交之后的改动:

  1. $ git diff
  2. diff --git a/test.py b/test.py
  3. index 76b8c39..62782b2 100644
  4. --- a/test.py
  5. +++ b/test.py
  6. @@ -1 +1 @@
  7. -print('Oops')
  8. +print('Ops!')

如果尝试提交改动,git 会抱怨:

  1. $ git commit -m "change the print string"
  2. On branch master
  3. Changes not staged for commit:
  4. modified: test.py
  5. no changes added to commit

staged for commit 的意思是需要先 add 文件,可以把这个动作翻译成:嘿,Git,往这儿看

  1. $ git add test.py

也可以输入 git add . 来添加当前目录下的所有改动文件。如果你修改了很多文件并且想要提交所有改动,这个命令会非常方便。现在提交改动:

  1. $ git commit -m "my first change"
  2. [master e1e11ec] my first change
  3. 1 file changed, 1 insertion(+), 1 deletion(-)

如果想查看你对 test.py 做的所有事情,按照时间倒序排列,可以使用 git log

  1. $ git log test.py
  2. commit e1e11ecf802ae1a78debe6193c552dcd15ca160a
  3. Author: William Lubanovic <bill@madscheme.com>
  4. Date: Tue May 13 23:34:59 2014 -0500
  5. change the print string
  6. commit 52d60d76594a62299f6fd561b2446c8b1227cfe1
  7. Author: William Lubanovic <bill@madscheme.com>
  8. Date: Tue May 13 23:26:14 2014 -0500
  9. 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 网站

可以在下面这些网站上找到很多有用的教程:

如果你对 Python 世界的新东西很感兴趣,可以看看下面这些网站:

最后是一些下载 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 和其相关内容的详细教程,以及我觉得很有用的速查表。虽然你对这些东西已经很熟悉了,不过万一需要可以去书后查阅。