代码调试是软件开发过程中的重要环节。许多程序员浪费大量时间,仅使用大量的日

志记录和 print 语句作为主力调试工具,但大多数专业开发人员更喜欢使用某种调试器。

Python 已经内置了一款交互式调试器,名为 pdb。它可以在命令行中调用并作用在现

有脚本上,如果程序异常退出,Python 将会进入事后调试状态(post-mortem debugging):

python -m pdb script.py

事后调试虽然很有用,但并不会涵盖所有场景。只有在 bug 出现的同时应用程序抛出

异常并退出,事后调试才有用。大多数情况下,错误代码只是行为异常,但并不会意外退

出。这时可以在某行代码上设置自定义断点,只需添加下面这行代码:

import pdb; pdb.set_trace()

在运行代码时,Python 解释器会在该行代码处启动调试会话。

pdb 用于跟踪问题非常好用,第一眼看去,它和著名的 GDB(GNU 调试器)非常类

似。由于 Python 是一门动态语言,pdb 会话与普通解释器会话非常类似。开发人员不仅可

以跟踪代码运行,而且还可以任意调用代码,甚至执行模块导入。

遗憾的是,pdb 来源于 bdb,所以第一次使用 pdb 可能会有点难以适应,因为诸如 h、

b、s、n、j 和 r 这样的单字母调试命令会让人不知所云。每当有疑问时,在调试会话期

间输入 help pdb 命令,会给出大量的用法和附加信息。

pdb 中的调试会话也非常简单,并没有提供类似 tab 补全或代码高亮之类的附加功能。

幸运的是,PyPI 上有几个包可以在上节提到的 Python shell 中实现这些功能。最有名的例

子是。

• ipdb:基于 ipython 的独立包。

• ptpdb:基于 ptpython 的独立包。

• bpdb:与 bpython 绑定。

有用的资源

互联网为 Python 开发者提供了丰富的有用资源。前面已经提到过,但这里我们再重复

一遍,最重要的也是最显而易见的资源如下所示。

• Python 文档。

• PyPIPython 包索引。

• PEP 0Python 改进提案的索引。

类似书籍和教程之类的其他资源也很有用,但往往很快就会过时。社区积极维护的资

源或者定期发布的资源,都是不会过时的。最值得推荐的是下面两个:

• Awesome-Python 里面包括流行包和框架的列表。

• Python Weekly 是一个著名的业务通讯(newletter),每周向订阅者发送许多新鲜有

趣的 Python 包和资源。

这两个资源包含大量阅读资料,可供读者阅读数月。

小结

本章从 Python 2 和 3 之间的主题差异开始讲起,并针对目前 Python 社区撕裂为两大阵

营的现状给出了应对建议。然后介绍了 Python 开发的现代方法,令人吃惊的是,开发这些

方法主要是由于这种语言两大版本之间令人遗憾的撕裂。这些方法主要是解决环境隔离问

题。本章最后对常用的生产力工具做了简短的总结,并提供了一些常用资源,以供进一步

参考。

语法最佳实践——类级别以下

编写高效语法的能力会随着时间逐步提高。回头看看写的第一个程序,你可能就会同

意这个观点。正确的语法看起来赏心悦目,而错误的语法则令人烦恼。

除了实现的算法与程序架构设计之外,还要特别注意的是,程序的写法也会严重影响它未来

的发展。许多程序被丢弃并从头重写,就是因为难懂的语法、不清晰的API 或不合常理的标准。

不过Python 在最近几年里发生了很大变化。因此,如果你被邻居(一个爱嫉妒的人,来自本

地 Ruby 开发者用户组)绑架了一段时间,并且远离新闻,那么你可能会对 Python 的新特性感到

吃惊。从最早版本到目前的3.5 版,这门语言已经做了许多改进,变得更加清晰、更加整洁、也更

容易编写。Python 基础知识并没有发生很大变化,但现在使用的工具更符合人们的使用习惯。

本章将介绍现在这门语言的语法中最重要的元素,以及它们的使用技巧,如下所示。

• 列表推导(list comprehension)。

• 迭代器(iterator)和生成器(generator)。

• 描述符(descriptor)和属性(property)。

• 装饰器(decorator)。

• with 和 contextlib。

Python 的内置类型

Python 提供了许多好用的数据类型,既包括数字类型,也包括集合类型。对于数字类型

来说,语法并没有什么特别之处。当然,每种类型的定义会有些许差异,也有一些(可能)

不太有名的运算符细节,但留给开发人员的选择并不多。对于集合类型和字符串来说,情况

就发生变化了。虽然人们常说“做事的方法应该只有一种”,但留给 Python 开发人员的选择

确实有很多。在初学者看来,有些代码模式看起来既直观又简单,可是有经验的程序员往往

会认为它们不够 Pythonic,因为它们要么效率低下,要么就是过于啰嗦。

这种解决常见问题的 Pythonic 模式(许多程序员称之为习语[idiom])看起来往往只

是美观而已。但这种看法大错特错。大多数习语都揭示了 Python 的内部实现方式以及内置

结构和模块的工作原理。想要深入理解这门语言,了解更多这样的细节是很必要的。此外,

社区本身也会受到关于 Python 工作原理的一些谣言和成见的影响。只有自己深入钻研,你

才能够分辨出关于 Python 的流行说法的真假。

字符串与字节

对于只用 Python 2 编程的程序员来说,字符串的话题可能会造成一些困惑。Python 3 中

只有一种能够保存文本信息的数据类型,就是 str(string,字符串)。它是不可变的序列,

保存的是 Unicode 码位(code point)。这是与 Python 2 的主要区别,Python 2 用 str 表示字

节字符串,这种类型现在在 Python 3 中用 bytes 对象来处理(但处理方式并不完全相同)。

Python 中的字符串是序列。基于这一事实,应该把字符串放在其他容器类型的一节去

介绍,但字符串与其他容器类型在细节上有一个很重要的差异。字符串可以保存的数据类

型有非常明确的限制,就是 Unicode 文本。

bytes 以及可变的 bytearray 与 str 不同,只能用字节作为序列值,即 0 <= x <

256 范围内的整数。一开始可能会有点糊涂,因为其打印结果与字符串非常相似:

>>> print(bytes([102, 111, 111]))

b'foo'

对于 bytes 和 bytearray,在转换为另一种序列类型(例如 list 或 tuple)时可

以显示出其本来面目:

>>> list(b'foo bar')

[102, 111, 111, 32, 98, 97, 114]

>>> tuple(b'foo bar')

(102, 111, 111, 32, 98, 97, 114)

许多关于 Python 3 的争议都是关于打破字符串的向后兼容和 Unicode 的处理方式。从

Python 3.0 开始,所有没有前缀的字符串都是 Unicode。因此,所有用单引号(')、双引号

(")或成组的 3 个引号(单引号或双引号)包围且没有前缀的值都表示 str 数据类型:

>>> type("some string")

<class 'str'>

在 Python 2 中,Unicode 需要有 u 前缀(例如 u"some string")。从 Python 3.3 开

始,为保证向后兼容,仍然可以使用这个前缀,但它在 Python 3 中没有任何语法上的意义。

前面的一些例子中已经提到过字节,但为了保持前后一致,我们来明确介绍它的语法。

字节也被单引号、双引号或三引号包围,但必须有一个 b 或 B 前缀:

>>> type(b"some bytes")

<class 'bytes'>

注意,Python 语法中没有 bytearray 字面值。

最后同样重要的是,Unicode 字符串中包含无法用字节表示的“抽象”文本。因此,

如果 Unicode 字符串没有被编码为二进制数据的话,是无法保存在磁盘中或通过网络发送

的。将字符串对象编码为字节序列的方法有两种:

• 利用 str.encode(encoding, errors)方法,用注册编解码器(registered codec)

对字符串进行编码。编解码器由 encoding 参数指定,默认值为'utf-8'。第二

个 errors 参数指定错误的处理方案,可以取'strict'(默认值)、'ignore'、

'replace'、'xmlcharrefreplace'或其他任何注册的处理程序(参见内置

codecs 模块的文档)。

• 利用 bytes(source, encoding, errors)构造函数,创建一个新的字节序列。

如果 source 是 str 类型,那么必须指定 encoding 参数,它没有默认值。

encoding 和 errors 参数的用法与 str.encode()方法中的相同。

用类似方法可以将 bytes 表示的二进制数据转换成字符串:

• 利用 bytes.decode(encoding, errors)方法,用注册编解码器对字节进行

解码。这一方法的参数含义及其默认值与 str.encode()相同。

• 利用 str(source, encoding, error)构造函数,创建一个新的字符串实例。

与 bytes()构造函数类似,如果 source 是字节序列的话,必须指定 str 函数的

encoding 参数,它没有默认值。

1.实现细节

Python 字符串是不可变的。字节序列也是如此。这一事实很重要,因为它既有优点又有缺点。它还会影响 Python 高效处理字符串的方式。由于不变性,字符串可以作为字典的键或

set 的元素,因为一旦初始化之后字符串的值就不会改变。另一方面,每当需要修改过的字符

串时(即使只是微小的修改),都需要创建一个全新的字符串实例。幸运的是,bytearray

是 bytes 的可变版本,不存在这样的问题。字节数组可以通过元素赋值来进行原处修改(无

需创建新对象),其大小也可以像列表一样动态地变化(利用 append、pop、inseer 等方法)。

2.字符串拼接

由于 Python 字符串是不可变的,在需要合并多个字符串实例时可能会产生一些问题。

如前所述,拼接任意不可变序列都会生成一个新的序列对象。思考下面这个例子,利用多

个字符串的重复拼接操作来创建一个新字符串:

s = ""

for substring in substrings:

s += substring

这会导致运行时间成本与字符串总长度成二次函数关系。换句话说,这种方法效率极

低。处理这种问题可以用 str.join()方法。它接受可迭代的字符串作为参数,返回合并

后的字符串。由于这是一个方法,实际的做法是利用空字符串来调用它:

s = "".join(substrings)

字符串的这一方法还可以用于在需要合并的多个子字符串之间插入分隔符,看下面这

个例子:

>>> ','.join(['some', 'comma', 'separated', 'values'])

'some,comma,separated,values'

需要记住,仅仅因为 join()方法速度更快(对于大型列表来说更是如此),并不意味

着在所有需要拼接两个字符串的情况下都应该使用这一方法。虽然这是一种广为认可的做

法,但并不会提高代码的可读性。可读性是很重要的!在某些情况下,join()的性能可能

还不如利用加法的普通拼接,下面举几个例子。

• 如果子字符串的数量很少,而且已经包含在某个可迭代对象中,那么在某些情况下,

创建一个新序列来进行拼接操作的开销可能会超过使用 join()节省下来的开销。

• 在拼接短的字面值时,由于 CPython 中的常数折叠(constant folding),一些复杂的

字面值(不只是字符串)在编译时会被转换为更短的形式,例如'a' + 'b' + 'c'

被转换为'abc'。当然,这只适用于相对短的常量(字面值)。

最后,如果事先知道字符串的数目,可以用正确的字符串格式化方法来保证字符串拼

接的最佳可读性。字符串格式化可以用 str.format()方法或%运算符。如果代码段的性

  能不是很重要,或者优化字符串拼接节省的开销很小,那么推荐使用字符串格式化作为最

佳方法。