书名:Python高手之路(第3版)
ISBN:978-7-115-43710-5
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
• 著 [法]Julien Danjou
译 王飞龙
责任编辑 杨海玲
• 人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
• 读者服务热线:(010)81055410
反盗版热线:(010)81055315
Published by arrangement with Julien Danjou.
ALL RIGHTS RESERVED
这不是一本常规意义上Python的入门书。这本书中没有Python关键字和for循环的使用,也没有细致入微的标准库介绍,而是完全从实战的角度出发,对构建一个完整的Python应用所需掌握的知识进行了系统而完整的介绍。更为难得的是,本书的作者是开源项目OpenStack的PTL(项目技术负责人)之一,因此本书结合了Python在OpenStack中的应用进行讲解,非常具有实战指导意义。
本书从如何开始一个新的项目讲起,首先是整个项目的结构设计,对模块和库的管理,如何编写文档,进而讲到如何分发,以及如何通过虚拟环境对项目进行测试。此外,本书还涉及了很多高级主题,如性能优化、插件化结构的设计与架构、Python 3的支持策略等。本书适合各个层次的Python程序员阅读和参考。
亲爱的中国读者你们好!
祝贺你,你正在读The Hacker ’s Guide to Python一书的中文版。我非常高兴看到这本书最终翻译完成,这样你就可以用自己的母语去阅读。这是这本书第三种语言的版本(之前已经有了英语和韩语两个版本)。能够有更多的读者看到这本书真是太棒了!
你将阅读的这本书的大部分内容来自我在OpenStack这个大规模项目中开发Python代码时的经验。你们是非常幸运的,因为这本书是由王飞龙翻译的,他是一名软件工程师,他和我同在OpenStack社区做开发工作。因为飞龙对本书涉及的内容有着很好的理解,所以高质量的翻译和对本书内容的精确表述很值得期待的。
真心希望你能喜欢这本书。祝你阅读愉快!
Julien Danjou
如果你正在读这本书,你肯定已经使用Python有一阵子了。你可能是通过一些文档学习的,钻研了一些已有的项目或者从头开发,但不管是哪种情况,你都已经在以自己的方式学习它了。直到3年前我加入OpenStack项目组之前,这其实也正是我个人熟悉Python的方法。
在此之前,我只是开发过一些“车库项目 ①”级别的Python库或应用程序,而一旦你参与开发涉及数百名开发人员并有着上万个用户的软件或库时,情况就会有所不同。OpenStack平台有超过200万行Python代码,所有代码都需要精确、高效,并根据用户对云计算应用程序的需求进行任意扩展。在有了这一规模的项目之后,类似测试和文档这类问题就一定需要自动化,否则根本无法完成。
我刚开始加入OpenStack的时候,我认为自己已经掌握了不少Python知识,但这3年,在起步时无法想象其规模的这样一个项目上,我学到了更多。而且我还有幸结识了很多业界最棒的Python黑客,并从他们身上获益良多——大到通用架构和设计准则,小到各种有用的经验和技巧。我想通过本书分享一些我所学到的最重要的东西,以便你能构建更好的Python应用,并且是更加高效地构建。
本书是第3版,这一版根据第1版发布之后的一些新变化进行了更新。希望你能喜欢。
① 作者这里的意思是规模很小,比较业余的项目。——译者注
你可能会问的第一个问题就是:“我的软件应该支持Python的哪些版本?”这是一个好问题,因为Python的每个新版本都会在引入新功能的同时弃用一些旧功能。而且,Python 2.x和Python 3.x之间有着巨大的差别,这两个分支之间的变化如此巨大,以至于很难让代码同时兼容二者。本书后面的章节会详细讨论,而且在刚开始一个新项目的时候很难说哪个版本更合适。
下面是一些简短的回答。
总而言之,只在确实需要(或者想自我挑战)的情况下支持2.6版本,必须支持2.7版本,如果想保证软件在可预见的未来也能运行,就需要也支持3.4及更高的版本。忽略那些更老的Python版本基本没什么问题,尽管同时支持所有这些版本是有可能的:CherryPy项目支持Python 2.3及所有后续版本。
编写同时支持Python 2.7和3.x版本的程序的方法将在第15章介绍。某些方法在后续的示例代码中也会涉及,本书中的所有示例代码都同时支持这两个主要版本。
项目结构应该保持简单,审慎地使用包和层次结构:过深的层次结构在目录导航时将如同梦魇,但过平的层次结构则会让项目变得臃肿。
一个常犯的错误是将单元测试放在包目录的外面。这些测试实际上应该被包含在软件的子包中,以便:
图1-1展示了一个项目的标准文件层次结构。
setup.py
是Python安装脚本的标准名称。在安装时,它会通过Python分发工具(distutils)进行包的安装。也可以通过README.rst
(或者README.txt
或其他合适的名字)为用户提供重要信息。requirements.txt
应该包含Python包所需要的依赖包,也就是说,所有这些包都会预先通过pip
这样的工具进行安装,以保证你的包能正常工作。还可以包含test-requirements.txt
,它只列出运行测试套件所需要的依赖包。最后,docs
文件夹应该包括reStructuredText
格式的文档,以便能够被Sphinx处理(参见5.1节)。
包中还经常需要包含一些额外的数据,如图片、shell脚本等。不过,关于这类文件如何保存并没有一个统一的标准。因此放到任何觉得合适的地方都可以。
图1-1 标准的包目录结构
下面这些顶层目录也经常出现。
etc
用来存放配置文件的样例。tools
用来存放与工具有关的shell脚本。bin
用来存放将被setup.py
安装的二进制脚本。data
用来存放其他类型的文件,如媒体文件。一个常见的设计问题是根据将要存储的代码的类型来创建文件或模块。使用functions.py
或者exceptions.py
这样的文件是很糟糕的方式。这种方式对代码的组织毫无帮助,只能让读代码的人在多个文件之间毫无理由地来回切换。
此外,应该避免创建那种只有一个__init__.py
文件的目录,例如,如果hooks.py
够用的话就不要创建hooks/__init__.py
。如果创建目录,那么其中就应该包含属于这一分类/模块的多个Python文件。
可能你已经有所了解,Python生态系统正在对包的元数据进行标准化。其中一项元数据就是版本号。
PEP 440(http://www.python.org/dev/peps/pep-0440/)针对所有的Python包引入了一种版本格式,并且在理论上所有的应用程序都应该使用这种格式。这样,其他的应用程序或包就能简单而可靠地识别它们需要哪一个版本的包。
PEP440中定义版本号应该遵从以下正则表达式的格式:
N[.N]+[{a|b|c|rc}N][.postN][.devN]
它允许类似1.2或1.2.3这样的格式,但需注意以下几点。
最终即将发布的组件也可以使用下面这种格式。
通常用到的还有以下这些后缀。
这一结构可以满足大部分常见的使用场景。
注意
你可能已经听说过语义版本,它对于版本号提出了自己的规则。这一规范和PEP 440部分重合,但二者并不完全兼容。例如,语义版本对于预发布版本使用的格式1.0.0.-alpha+001就与PEP 440不兼容。
如果需要处理更高级的版本号,可以考虑一下PEP 426(http://www.python.org/dev/ peps/pep-0426)中定义的源码标签,这一字段可以用来处理任何版本字符串,并生成同PEP要求一致的版本号。
许多分布式版本控制系统(Distributed Version Control System,DVCS)平台,如Git和Mercurial,都可以使用唯一标识的散列字符串①作为版本号。但遗憾的是,它不能与PEP 440中定义的模式兼容:问题就在于,唯一标识的散列字符串不能排序。不过,是有可能通过源码标签这个字段维护一个版本号,并利用它构造一个同PEP 440兼容的版本号的。
提示
pbr(即Python Build Reasonableness,https://pypi.python.org/pypi/pbr)将在6.2节中讨论,它可以基于项目的Git版本自动生成版本号。
没错,编码风格是一个不太讨巧的话题,不过这里仍然要讲一下。
Python具有其他语言少有的绝佳质量②:使用缩进来定义代码块。乍一看,似乎它解决了一个由来已久的“往哪里放大括号?”的问题,然而,它又带来了“如何缩进?”这个新问题。
而Python社区则利用他们的无穷智慧,提出了编写Python代码的PEP 8③(http://www. python.org/dev/peps/pep-0008/)标准。这些规范可以归纳成下面的内容。
import
语句只导入一个模块,同时要按标准库、第三方库和本地库的导入顺序进行分组。CamelCase
;异常的定义使用Error
前缀(如适用的话);函数的命名使用下划线分隔的小写字母,如separated_by_underscores
;用下划线开头定义私有的属性或方法,如_private
。这些规范其实很容易遵守,而且实际上很合理。大部分程序员在按照这些规范写代码时并没有什么不便。
然而,犯错在所难免,保持代码符合PEP 8规范的要求仍是一件麻烦事。工具pep8(https:// pypi.python.org/pypi/pep8)就是用来解决这个问题的,它能自动检查Python文件是否符合PEP 8要求,如示例1.1所示。
示例1.1 运行pep8
$ pep8 hello.py
hello.py:4:1: E302 expected 2 blank lines, found 1
$ echo $?
1
pep8会显示在哪行哪里违反了PEP 8,并为每个问题给出其错误码。如果违反了那些必须遵守的规范,则会报出错误(以E开头的错误码),如果是细小的问题则会报警告(以W开头的错误码)。跟在字母后面的3位数字则指出具体的错误或警告,可以从错误码的百位数看出问题的大概类别。例如,以E2开头的错误通常与空格有关,以E3开头的错误则与空行有关,而以W6开头的警告则表明使用了已废弃的功能。
社区仍然在争论对并非标准库一部分的代码进行PEP 8验证是否是一种好的实践。这里建议还是考虑一下,最好能定期用PEP 8验证工具对代码进行检测。一种简单的方式就是将其集成到测试套件中。尽管这似乎有点儿极端,但这能保证代码一直遵守PEP 8规范。8.7节中将介绍如何将pep8与tox集成,从而让这些检查自动化。
OpenStack项目从一开始就通过自动检查强制遵守PEP 8规范。尽管有时候这让新手比较抓狂,但这让整个代码库的每一部分都保持一致,要知道现在它有167万行代码。对于任何规模的项目这都是非常重要的,因为即使对于空白字符的顺序,不同的程序员也会有不同的意见。
也可以使用--ignore
选项忽略某些特定的错误或警告,如示例1.2所示。
示例1.2 运行pep8时指定--ignore
选项
$ pep8 --ignore=E3 hello.py
$ echo $?
0
这可以有效地忽略那些不想遵循的PEP 8标准。如果使用pep8对已有的代码库进行检查,这也可以暂时忽略某些问题,从而每次只专注解决一类问题。
注意
如果正在写一些针对Python的C语言代码(如模块),则PEP 7(http://www.python.org/dev/ peps/pep-0007/)标准描述了应该遵循的相应的编码风格。
还有一些其他的工具能够检查真正的编码错误而非风格问题。下面是一些比较知名的工具。
这些工具都是利用静态分析技术,也就是说,解析代码并分析代码而无须运行。
如果选择使用pyflakes,要注意它按自己的规则检查而非按PEP 8,所以仍然需要运行pep8。为了简化操作,一个名为flake8(https://pypi.python.org/pypi/flake8)的项目将pyflakes和pep8合并成了一个命令,而且加入了一些新的功能,如忽略带有#noqa
的行以及通过入口点(entry point)进行扩展。
为了追求优美而统一的代码,OpenStack选择使用flake8进行代码检查。不过随着时间的推移,社区的开发者们已经开始利用flake8的可扩展性对提交的代码进行更多潜在问题的检查。最终flake8的这个扩展被命名为hacking(https://pypi.python.org/pypi/hacking)。它可以检查except
语句的错误使用、Python 2与Python 3的兼容性问题、导入风格、危险的字符串格式化及可能的本地化问题。
如果你正开始一个新项目,这里强烈建议使用上述工具之一对代码的质量和风格进行自动检查。如果已经有了代码库,那么一种比较好的方式是先关闭大部分警告,然后每次只解决一类问题。
尽管没有一种工具能够完美地满足每个项目或者每个人的喜好,但flake8和hacking的结合使用是持续改进代码质量的良好方式。要是没想好其他的,那么这是一个向此目标前进的好的开始。
提示
许多文本编辑器,包括流行的GNU Emacs和vim,都有能够直接对代码运行pep8和flake8这类工具的插件(如Flycheck),能够交互地突出显示代码中任何不兼容PEP 8规范的部分。这种方式能够非常方便地在代码编写过程中修正大部分风格错误。
这个工具集很容易扩展。我在OpenStack中用hacking
扩展来扩展这个工具集,例如,我添加了一个检查,用于找出本应该声明为静态的方法。这部分内容会在11.1节中谈及。
Joshua Harlow是一名Python开发人员,自2012年起任雅虎OpenStack团队的技术主管之一,他还曾作为CTO小组的成员之一负责调研IaaS解决方案。自那时起他陆续开发了若干Python库,如Taskflow、automaton和Zake。
是什么促使你开始使用Python的?
我开始使用Python(那时候肯定还是Python 2.3或2.4)编程大概要追溯到2004年,那时我在IBM纽约(我的大部分亲戚和家人都住在纽约州北部,离他们很近)的Poughkeepsie实验室做实习生。我基本已经忘了当时具体做什么,但是主要用wxPython和一些Python代码自动化某些系统。那就是我第一段和Python有关的经历,而且我将其称为我所迈出的第一步(那时候基本就是不断地输入self
)。在实习结束之后我返回了学校(克拉克森大学,Clarkson University),后来我又读了研究生(罗彻斯特理工学院,Rochester Institute of Technology),并最终在毕业后加入雅虎,工作至今。
在接下来的几年里我在不同的团队(主要使用PHP、C++和Java)里工作过,而且还有幸和一些PHP核心开发人员一起工作,如Rasmus Lerdorf,他发明了PHP)和Sara Goleman(他负责雅虎官方网站的后台和其他一些工作)。
最终我止步于CTO小组,在那里我和其他一些成员负责调研可用的开源云平台。最终我们选择了OpenStack(我们还调研了CloudStack以及基本上已经废弃的Eucalyptus),它几乎完全用Python开发(很可能是现存的最大的两三个Python项目之一)。我自2012年加入OpenStack社区并先后参与了若干组件的开发,目前我主要专注于OpenStack内外皆可用的扩展库(在oslo项目下)开发。我坚信(并且作为一名鼓吹者),创建OpenStack内外皆100%可用的库是非常重要的,而且我感觉这可以确保社区所开发的设计/开发良好的库不光是被它自己所使用(这意味着那些库不能令更大的开源世界获益,我觉得这不太好,也不应该是开源该有的样子)。
对于这门语言,你喜欢什么不喜欢什么?
我喜欢的一些方面如下(不完整列表)。
一些我不太喜欢的方面如下(不完整列表)。
你最近在开发一个名为debtcollector
的新库,进展如何?
进展很顺利!前面提到的简洁性使得开发一个新库非常容易,而且能够很容易地发布以便为他人所用。因为其代码主要源自我开发的另一个库(taskflow④),所以代码的移植和扩展相对很容易而且不用太担心API的设计。我非常高兴地看到其他人(OpenStack社区内外都有)也对它有需求并开始使用这个库,我希望这个库能进一步成长,从而包含一些其他库(或应用)认为有用的更为丰富的废除模式。
依你之见,Python最大的缺憾是什么?
这是个很难的问题,因为并不多,所以我只挑两个我认为能让Python更现代的(我所认为的现代):
有机会再谈,愿你编程顺利,生活愉快!
① 对于Git,指的是git-describe(1)。
② 你可能有不同意见。
③ PEP 8 Style Guide for Python Code, 5th July 2001, Guido van Rossum, Barry Warsaw, Nick Coghlan
④ 感兴趣的话,欢迎参与,为这个项目做贡献,并随时加入我们的IRC讨论。
要使用模块和库,需要先导入。
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!
导入系统是相当复杂的,不过你可能已经了解了一些基本知识。这里会介绍一些关于这一子系统的内部机理。
sys
模块包含许多关于Python导入系统的信息。首先,当前可导入的模块列表可以通过sys.moudle
变量获取。它是一个字典,其中键(key)是模块名字,对应的值(value)是模块对象。
>>> sys.modules['os']
<module 'os' from '/usr/lib/python2.7/os.pyc'>
许多模块是内置的,这些内置的模块在sys.builtin_module_names
中列出。内置的模块可以根据传入Python构建系统的编译选项的不同而变化。
导入模块时,Python会依赖一个路径列表。这个列表存储在sys.path
变量中,并且告诉Python去哪里搜索要加载的模块。可以在代码中修改这个列表,根据需要添加或删除路径,也可以通过编写Python代码直接修改环境变量PYTHONPATH
。下面的方法几乎是相等的①。
>>> import sys
>>> sys.path.append('/foo/bar')
$ PYTHONPATH=/foo/bar python
>>> import sys
>>> '/foo/bar' in sys.path
True
在sys.path
中的顺序很重要,因为需要遍历这个列表来寻找请求的模块。
也可以通过自定义的导入器(importer)对导入机制进行扩展。Hy②正是利用这种技术告诉Python如何导入其他非标准的.py
或者.pyc
文件的。
顾名思义,导入钩子机制是由PEP 302(http://www.python.org/dev/peps/pep-0302/)定义的③。它允许扩展标准的导入机制,并对其进行预处理,也可以通过追加一个工厂类到sys.path_hooks
来添加自定义的模块查找器(finder)。
模块查找器对象必须有一个返回加载器对象的find_module(fullname, path=None)
方法,这个加载器对象必须包含一个负责从源文件中加载模块的load_module(fullname)
方法。
为了进一步说明,下面给出了Hy利用自定义的导入器导入.hy
而不是.py
结尾的源文件的方法,见示例2.1。
示例2.1 Hy模块导入器
class MetaImporter(object):
def find_on_path(self, fullname):
fls = ["%s/__init__.hy", "%s.hy"]
dirpath = "/".join(fullname.split("."))
for pth in sys.path:
pth = os.path.abspath(pth)
for fp in fls:
composed_path = fp % ("%s/%s" % (pth, dirpath))
if os.path.exists(composed_path):
return composed_path
def find_module(self, fullname, path=None):
path = self.find_on_path(fullname)
if path:
return MetaLoader(path)
sys.meta_path.append(MetaImporter())
一旦路径被确定是有效的且指向了一个模块,就会返回一个MetaLoader
对象。
Hy模块加载器
class MetaLoader(object):
def __init__(self, path):
self.path = path
def is_package(self, fullname):
dirpath = "/".join(fullname.split("."))
for pth in sys.path:
pth = os.path.abspath(pth)
composed_path = "%s/%s/__init__.hy" % (pth, dirpath)
if os.path.exists(composed_path):
return True
return False
def load_module(self, fullname):
if fullname in sys.modules:
return sys.modules[fullname]
if not self.path:
return
sys.modules[fullname] = None
mod = import_file_to_module(fullname, self.path) ❶
ispkg = self.is_package(fullname)
mod.__file__ = self.path
mod.__loader__ = self
mod.__name__ = fullname
if ispkg:
mod.__path__ = []
mod.__package__ = fullname
else:
mod.__package__ = fullname.rpartition('.')[0]
sys.modules[fullname] = mod
return mod
❶ import_file_to_module
读取一个Hy源文件,将其编译成Python代码,并返回一个Python模块对象。
uprefix
模块(https://pypi.python.org/pypi/uprefix)是这个功能起作用的另一个好的例子。Python 3.0到3.2并没有像Python 2中用来表示Unicode字符串的u
前缀④,这个模块通过在编译前删除字符串的前缀u
来确保在2.x和3.x之间的兼容性。
Python本身内置的巨大标准库提供了丰富的工具和功能,可以满足你能想到的任何需求。很多Python的初学者习惯于自己写代码实现一些基本的功能,然后会惊奇地发现很多功能已经内置了,直接就可以使用。
任何时候想要自己写函数处理一些简单的工作时,停下来先看看标准库。我的建议是至少大概浏览一遍标准库,这样下次再需要一个函数时就能知道是否可以利用标准库中已有的函数了。
后续章节会讨论其中的一些模块,如functools和itertools,下面是一些必须了解的标准库模块。
这个模块清单可以作为一个快速参考,帮助你了解各个库模块的作用。如果能记住一部分就更好了。花在查找标准库上的时间越少,意味着编写实际代码的时间就越多。
提示
大多数标准库都是用Python写的,所以可以直接查看其模块和函数的源代码。有疑问时只需打开代码自己一探究竟。尽管文档中已经包含了你想知道的一切,但总还是有机会让你学一些有用的东西。
你是否有过这样的经历,收到一件不错的生日礼物或圣诞礼物,但是打开后却发现送你的人忘了买电池?Python的“内置电池”哲学让作为程序员的你不会遇到这类问题,只要安装了Python,就拥有了完成任何功能所需的一切条件。
然而,Python标准库的开发者并不能预测你要实现的“任何”功能到底是什么。即使可以,大多数人也不想去处理一个几个GB的文件下载,即使可能只是需要写一个重命名文件的脚本。关键在于,即使拥有所有的扩展功能,仍然有许多功能是Python标准库没有涵盖的。不过,这并不是说有些事情是根本无法用Python实现的,这只是表明有些事情可能需要使用外部库。
Python标准库是安全且范围明确的:模块文档化程度很高,并且有足够多的人在经常使用它,从而可以保证在你想使用它时肯定不会遇到麻烦。而且,就算万一出了问题,也能确保在短时间内有人解决。但是,外部库就像是地图上标着“熊出没,请注意”的部分:可能缺少文档,功能有bug,更新较少或根本不更新。任何正式的项目都可能用到一些只有外部库提供的功能,但是需要谨记使用这些外部库可能带来的风险。
下面是来自一线的案例。OpenStack使用了SQLAlchemy——一个Python数据库开发工具包。如果了解SQL的话会知道,数据库的结构是会发生变化的,所以OpenStack还使用了sqlalchemy-migrate来处理数据库模式的升级。一切运行良好,直到有一天它们不行了,开始出现大量bug,并且没有好转的迹象。而且,OpenStack在当时是想要支持Python 3的,然而没有任何迹象表明sqlalchemy-migrate要支持Python 3。因此,显然sqlalchemy-migrate已经死了,我们需要切换到其他替代方案。截止到作者写作时,OpenStack正准备升级到Alembic(https://pypi.python.org/pypi/alembic),虽然也有一些工作要做,但好在不是那么痛苦。
所有这些引出一个重要的问题:“如何保证我不会掉进同样的陷阱里?”很遗憾,没办法保证。程序员也是人,没什么办法可以确保目前维护良好的库在几个月后仍然维护良好。但是,在OpenStack中我们使用下列检查表来根据需要给出建议(我建议你也这么做)。
尽管可能工作量巨大,但这一检查表对于依赖同样适用。如果知道应用程序会大量依赖一个特定的库,那么至少应该对这个库的每一个依赖使用这个检查表。
不管最终使用哪个库,都要像其他工具一样对待,因为即使是有用的工具也可能会造成严重的损害。尽管不常发生,但问问你自己:如果你有一把锤子,你会拿着它满屋跑因而可能意外地损坏屋子里的东西,还是会把它放在工具架上或者车库里,远离那些贵重而易碎的东西,仅在需要的时候才拿出来?
对于外部库道理是一样的,不管它们多么有用,都需要注意避免让这些库和实际的源代码耦合过于紧密。否则,如果出了问题,你就需要切换库,这很可能需要重写大量的代码。更好的办法是写自己的API,用一个包装器对外部库进行封装,将其与自己的源代码隔离。自己的程序无须知道用了什么外部库,只要知道API提供了哪些功能即可。想要换一个不同的库?只需要修改包装器就可以了。只要它仍然提供同样的功能,那么完全不需要修改其余的核心代码。也许会有例外,但应该不会太多。大部分库都被设计成只专注解决一定范围的问题,因此很容易隔离。
5.7.3节将会涉及如何使用入口点构建驱动系统(driver system),这个系统让你可以将项目的某些部分设计成可以根据需要切换的模块。
有许多不同的Python框架可用于开发不同的Python应用。如果是Web应用,可以使用Django、Pylons、TurboGears、Tornado、Zope或者Plone。如果你正在找事件驱动的框架,可以使用Twisted或者Circuits等。
框架和外部库的主要不同在于,应用程序是建立在框架之上的,代码对框架进行扩展而不是反过来。而外部库更像是对代码的扩展,赋予你的代码更多额外的能力,而框架会为你的代码搭好架子,只需要通过某种方式完善这个架子就行了,尽管这可能是把双刃剑。使用框架有很多好处,如快速构建原型并开发,但也有一些明显的缺点,如锁定(lock-in)问题。因此,在决定使用某个框架前需要把这些都考虑在内。
这里推荐的为Python应用选择框架的方法很大程度上类似于前面介绍过的外部库的选择方法,适用于框架是通过一组Python库来进行分发的情况。有时它们还包含用于创建、运行以及部署应用的工具,但这并不影响你采用的标准。前面已经提到过,在已经写了大量代码之后更换外部库是十分痛苦的,但更换框架比这还要难受1000倍,因为通常需要完全重写你的应用程序。举例来说,前面提及的Twisted框架还不能完全支持Python 3。如果你基于Twisted的程序在几年之后想要支持Python 3,那么你将非常不幸,除非全部重写代码选用另一个框架或者有人最终为Twisted提供了Python 3的升级支持。
有些框架与其他框架相比更加轻量级。一个简单的比较就是,Django提供了内置的ORM功能,而Flask则没有。一个框架提供的功能越少,将来遇到的问题越少。然而,框架缺少的每个功能同时也是另一个需要去解决的问题,要么自己写,要么千挑万选去找另一个能提供这个功能的库。愿意处理哪种场景取决于个人的选择,但需慎重选择。当问题出现时从一个框架升级至其他框架是极其艰巨的任务,就算Python再强大,对于这类问题也没有什么好办法。
我曾经有幸和Doug Hellmann一起工作过数月。他在DreamHost是一位非常资深的软件开发工程师,同时他也是OpenStack项目的贡献者。他发起过关于Python的网站Python Module of the Week,也出版过一本很有名的Pyhton书The Python Standard Library By Example,同时他也是Python的核心开发人员。我曾经咨询过Doug关于标准库以及库的设计与应用等方面的问题。
当你从头开发一个Python应用时,如何迈出第一步呢?它和开发一个已有的应用程序有什么不同?
从抽象角度看步骤都差不多,但是细节上有所不同。相对于对比开发新项目和已有项目,我个人在对应用程序和库开发的处理方式上有更多的不同。
当我要修改已有代码时,特别是这些代码是其他人创建的时,起初我需要研究代码是如何工作的,我需要改进哪些代码。我可能会添加日志或是输出语句,或是用pdb,利用测试数据运行应用程序,以便我理解它是如何工作的。我经常会做一些修改并测试它们,并在每次提交代码前添加可能的自动化测试。
创建一个新应用时,我会采取相同的逐步探索方法。我先创建一些代码,然后手动运行它们,在这个功能可以基本调通后,再编写测试用例确保我已经覆盖了所有的边界情况。创建测试用例也可以让代码重构更容易。
这正是smiley(https://pypi.python.org/pypi/smiley)的情况。在开发正式应用程序前,我先尝试用Python的trace API写一些临时脚本。对于smiley我最初的设想包括一个仪表盘并从另一个运行的应用程序收集数据,另一部分用来接收通过网络发送过来的数据并将其保存。在添加几个不同的报告功能的过程中,我意识到重放已收集的数据的过程和在一开始收集数据的过程基本是一样的。于是我重构了一些类,并针对数据收集、数据库访问和报告生成器创建了基类。通过让这些类遵循同样的API使我可以很容易地创建数据收集应用的一个版本,它可以直接将数据写入数据库而无须通过网络发送数据。
当设计一个应用程序时,我会考虑用户界面是如何工作的,但对于库,我会专注于开发人员如何使用其API。通过先写测试代码而不是库代码,可以让思考如何通过这个新库使开发应用程序变得更容易一点儿。我通常会以测试的方式创建一系列示例程序,然后依照其工作方式去构建这个库。
我还发现,在写任何库的代码之前先写文档让我可以全面考虑功能和流程的使用,而不需要提交任何实现的细节。它还让我可以记录我对设计所做出的选择,以便读者不仅可以理解如何使用这个库,还可以了解在创建它时我的期望是什么。这就是我用在stevedore上的方法。
我知道我想让stevedore能够提供一组类用来管理应用程序的插件。在设计阶段,我花了些时间思考我见过的使用插件的通用模式,并且写了几页粗略的文档描述这些类应该如何使用。我意识到,如果把大部分最复杂的参数放入类的构造函数中,方法map()
几乎是可互换的。这些设计笔记直接写进了stevedore官方文档的简介里,用来解释在应用程序中使用插件的不同模式和准则。
将一个模块加入Python标准库的流程是什么?
完整的流程和规范可以在Python Developer’s Guide中找到。
一个模块在被加入Python标准库之前,需要被证明是稳定且广泛使用的。模块需要提供的功能要么是很难正确实现的,要么是非常有用以至于许多开发人员已经创建了他们自己不同的变种。API应该非常清晰并且它的实现不能依赖任何标准库之外的库。
提议一个新模块的第一步是在社区通过python-ideas邮件列表非正式地了解一下大家对此的感兴趣程度。如果回应很积极,下一步就是创建一个Python增强提案(Python Enhancement Proposal,PEP),它包括添加这个模块的动机,以及如何过渡的一些实现细节。
因为包的管理和发现工作已经非常稳定了,尤其是pip和Python Package Index(PyPI),因此在标准库之外维护一个新的库可能更实用。单独的发布使得对于新功能和bug修复(bugfix)的更新可以更频繁,对于处理新技术或API的库来说这尤其重要。
标准库中的哪3个模块是你最想让人们深入了解并开始使用的?
最近我做了许多关于应用程序中动态加载扩展方面的工作。我使用abc模块为那些作为抽象基类进行的扩展定义API,以帮助扩展的作者们了解API的哪些方法是必需的,哪些是可选的。抽象基类已经在其他一些语言中内置了,但我发现很多Python程序员并不知道Python也有。
bisect模块中的二分查找算法是个很好的例子,一个广泛使用但不容易正确实现的功能,因此它非常适合放到标准库中。我特别喜欢它可以搜索稀疏列表,且搜索的值可能并不在其中。
collections模块中有许多有用的数据结构并没有得到广泛使用。我喜欢用namedtuple来创建一些小的像类一样的数据结构来保存数据但并不需要任何关联逻辑。如果之后需要添加逻辑的话,可以很容易将namedtuple转换成一个普通的类,因为namedtuple支持通过名字访问属性。另一个有意思的数据结构是ChainMap,它可以生成良好的层级命名空间。ChainMap能够用来为模板解析创建上下文或者通过清晰的流程定义来管理不同来源的配置。
许多项目(包括OpenStack)或者外部库,会在标准库之上封装一层自己的抽象。例如,我特别想了解对于日期/时间的处理。对此你有什么建议吗?程序员应该坚持使用标准库,还是应该写他们自己的函数,切换到其他外部库或是开始给Python提交补丁?
所有这些都可以。我倾向于避免重复造轮子,所以我强烈主张贡献补丁和改进那些能够用来作为依赖的项目。但是,有时创建另外的抽象并单独维护代码也是合理的,不管在应用程序内还是作为一个新的库。
你提到的例子中,OpenStack里的timeutils模块就是对Python的datetime模块的一层很薄的封装。大部分功能都简短且简单,但通过将这些最常见的操作封装为一个模块,我们可以保证它们在OpenStack项目中以一致的方式进行处理。因为许多函数都是应用相关的,某种意义上它们强化了一些问题决策,例如,字符串时间戳格式或者“现在”意味着什么,它们不太适合作为Python标准库的补丁或者作为一个通用库发布以及被其他项目采用。
与之相反,我目前正致力于将OpenStack的API服务项目从早期创建时使用的WSGI框架转成采用一个第三方Web开发框架。在Python中开发WSGI应用有很多选择,并且当我们可能需要增强其中一个以便其可以完全适应OpenStack API服务器的需要时,将这些可重用的修改贡献对于维护一个“私有的”框架似乎更可取。
当从标准库或其他地方导入并使用大量模块时,关于该做什么你有什么特别的建议吗?
我没有什么硬性限制,但是如果我有过多的导入时,我会重新考虑这个模块的设计并考虑将其拆到一个包中。与上层模块或者应用程序模块相比,对底层模块的这种拆分可能会发生得更快,因为对于上层模块我期望将更多片段组织在一起。
关于Python 3,有什么模块是值得一提而且能令开发人员有兴趣深入了解的?
支持Python 3的第三方库的数量已经到了决定性的时刻。针对Python 3开发新库或应用程序从未如此简单过,而且幸亏有3.3中加入的兼容性功能使同时维护对Python 2.7的支持也很容易。主要的Linux发行版正在致力于将Python 3默认安装。任何人要用Python创建新项目都应该认真考虑对Python 3的支持,除非有尚未移植的依赖。目前来说,不能运行在Python 3上的库基本会被视为“不再维护”。
许多开发人员将所有的代码都写入到应用程序中,但有些情况下可能有必要将代码封装成一个库。关于设计、规划、迁移等,做这些最好的方式是什么?
应用程序就是“胶水代码”的集合用来将库组织在一起完成特定目的。起初设计时可以将这些功能实现为一个库,然后在构建应用程序时确保库的代码能够很好地组织到逻辑单元中,这会让测试变得更简单。这还意味着应用程序的功能可以通过库进行访问,并且能够被重新组合以构建其他应用程序。未能采用这种方法的话意味着应用程序的功能和用户界面的绑定过于紧密,导致很难修改和重用。
对于计划开始构建自己的Python库的人们有什么样的建议呢?
我通常建议自顶向下设计库和API,对每一层应用单一职责原则(Single Responsibility Principle,SRP)这样的设计准则。考虑调用者如何使用这个库,并创建一个API去支持这些功能。考虑什么值可以存在一个实例中被方法使用,以及每个方法每次都要传入哪些值。最后,考虑实现以及是否底层的代码的组织应该不同于公共API。
SQLAlchemy是应用这些原则的绝好例子。声明式ORM、数据映射和表达式生成层都是单独的。开发人员可以自行决定对于API访问的正确的抽象程度,并基于他们的需求而不是被库的设计强加的约束去使用这个库。
当你随机看Python程序员的代码时遇到的最常见的编程错误是什么?
Python的习惯用法和其他语言的一个较大的不同在于循环和迭代。例如,我见过的最常见的反模式是使用for
循环过滤一个列表并将元素加入到一个新的列表中,然后再在第二个循环中处理这个结果(可能将列表作为参数传给一个函数)。我通常建议将过滤循环改成生成器表达式,因为生成器表达式更有效也更容易理解。列表的组合也很常见,以便它们的内容可以以某种方式一起被处理,但却没有使用itertools.chain()
。
还有一些我在代码评审时给出的更细小的建议。例如,使用dict()
而不是长的if:then:else
块作为查找表,确保函数总是返回相同的类型(如一个空列表而不是None),通过使用元组和新类将相关的值合并到一个对象中从而减少函数的参数,以及在公共API中定义要使用的类而不是依赖于字典。
有没有关于选择了一个“错误”的依赖的具体的例子是你亲身经历或目睹过的?
最近,我有个例子,pyparsing(https://pypi.python.org/pypi/pyparsing)的一个新发布取消了对Python 2的支持,这给我正在维护的一个库带来了一点儿小麻烦。对pyparsing的更新是个重大的修改,而且是明确标识成这样的,但是因为我没有在对cliff(https://pypi.python.org/pypi/cliff)的设置中限制依赖版本号,所以pyparsing的新发布给cliff的用户造成了问题。解决方案就是在cliff的依赖列表中对Python 2和Python 3提供不同的版本边界。这种情况突显了理解依赖管理和确保持续集成测试中适当的测试配置的重要性。
你怎么看待框架?
框架像任何工具类型一样。它们确实有帮助,但在选择框架时要特别谨慎,应确保它能够很好地完成当前的工作。
通过抽取公共部分到一个框架中,你可以将你的开发精力专注于应用中独特的方面。通过提供许多类似运行在开发模式或者写一个测试套件这样的引导代码,它们还可以帮你让一个应用程序迅速达到一个可用的状态而不是从头开发。它们还可以激励你在应用程序开发过程中保持一致,这意味着最终你的代码将更易于理解且更可重用。
虽然使用框架时还有其他一些潜在的缺点需要注意。决定使用某个特定框架通常能够反映应用程序本身的设计。如果设计的限制不能从根本上符合应用程序的需求,那么选择错误的框架会令应用的实现变得更难。如果你试着使用与框架建议不同的模式或惯用方式,你最终将不得不同框架做斗争。
① 说“几乎”是因为路径并不会被放在列表的同一级上,尽管根据你的使用情况它可能并不重要。
② Hy是Python上的Lisp实现,会在11.2节介绍。
③ 自Python 2.3版本实现的新导入钩子机制。
④ 它在Python 3.3中又被加了回来。