Python 专家级编程第二版(一)
原文:
zh.annas-archive.org/md5/4CC2EF9A4469C814CC3EEBD966D2E707译者:飞龙
前言
Python 很棒!
从 20 世纪 80 年代末的最早版本到当前版本,它以相同的理念发展:提供一种多范式编程语言,考虑可读性和生产力。
人们过去常常将 Python 视为又一种脚本语言,并不认为它适合用来构建大型系统。然而,多年来,多亏了一些先锋公司,显而易见的是 Python 可以用来构建几乎任何类型的系统。
事实上,许多来自其他语言的开发人员都被 Python 所吸引,并将其作为自己的首选语言。
这是您可能已经知道的内容,所以无需进一步说服您使用这种语言的优点。
本书是根据多年使用 Python 构建各种应用程序的经验而写成的,从几小时内完成的小型系统脚本到由数十名开发人员在数年内编写的非常大型应用程序。
它描述了开发人员在使用 Python 时使用的最佳实践。
本书涵盖了一些不专注于语言本身的主题,而是关于用于处理它的工具和技术。
换句话说,这本书描述了高级 Python 开发人员每天的工作。
本书涵盖的内容
第一章,Python 的当前状态,展示了 Python 语言及其社区的当前状态。它展示了 Python 是如何不断变化的,为什么它在变化,以及为什么这些事实对任何想要称自己为 Python 专业人士的人都很重要。本章还介绍了在 Python 中工作的最流行和规范的方式——流行的生产工具和现在已经成为事实标准的约定。
第二章,语法最佳实践——类级别以下,以高级方式介绍了迭代器、生成器、描述符等。它还涵盖了关于 Python 成语和内部 CPython 类型实现的有用注释,以及它们的计算复杂性作为展示成语的理由。
第三章,语法最佳实践——类级别以上,解释了语法最佳实践,但侧重于类级别以上。它涵盖了 Python 中可用的更高级的面向对象的概念和机制。为了理解本章的最后一部分,需要掌握这些知识,该部分介绍了 Python 中元编程的不同方法。
第四章,选择良好的名称,涉及选择良好的名称。它是对 PEP 8 的扩展,包括命名最佳实践,还提供了有关设计良好 API 的建议。
第五章,编写包,解释了如何创建 Python 包以及使用哪些工具来正确在官方 Python 包索引或任何其他包存储库上分发它。关于包的信息还附有一个简要回顾,介绍了允许您从 Python 源代码创建独立可执行文件的工具。
第六章,部署代码,主要面向 Python 网络开发人员和后端工程师,因为它涉及代码部署。它解释了 Python 应用程序应该如何构建,以便轻松部署到远程服务器,以及您可以使用哪些工具来自动化该过程。这一章与第五章,编写包,相呼应,因为它展示了如何使用包和私有包存储库来简化应用程序部署。
第七章,“其他语言中的 Python 扩展”,解释了为什么有时为 Python 编写 C 扩展可能是一个好的解决方案。它还表明只要使用适当的工具,这并不像看起来那么困难。
第八章,“管理代码”,提供了一些关于如何管理项目代码库的见解,并解释了如何设置各种持续开发流程。
第九章,“记录您的项目”,涵盖了文档编写,并提供了有关技术写作以及如何记录 Python 项目的建议。
第十章,“测试驱动开发”,解释了测试驱动开发的基本原则以及可以在这种开发方法中使用的工具。
第十一章,“优化-一般原则和分析技术”,解释了优化。它提供了分析技术和优化策略指南。
第十二章,“优化-一些强大的技术”,扩展了第十一章,“优化-一般原则和分析技术”,提供了一些常见的解决方案,用于解决 Python 程序中经常出现的性能问题。
第十三章,“并发”,介绍了 Python 中广泛的并发主题。它解释了并发是什么,何时可能需要编写并发应用程序,以及 Python 程序员的并发主要方法。
第十四章,“有用的设计模式”,通过一组有用的设计模式和 Python 中的示例实现来结束本书。
您需要为本书做好准备
这本书是为那些在任何 Python 3 可用的操作系统下工作的开发人员编写的。
这不是一本面向初学者的书,所以我假设您已经在您的环境中安装了 Python,或者知道如何安装它。无论如何,这本书考虑到并非每个人都需要完全了解最新的 Python 功能或官方推荐的工具。这就是为什么第一章提供了常见实用工具(如虚拟环境和 pip)的回顾,这些现在被认为是专业 Python 开发人员的标准工具。
这本书是为谁写的
这本书是为希望在掌握 Python 方面更进一步的 Python 开发人员编写的。而且我所说的开发人员主要是专业人士,所以是以 Python 为生写软件的程序员。这是因为它主要关注的是对于在 Python 中创建高性能、可靠和可维护软件至关重要的工具和实践。
这并不意味着爱好者找不到有趣的东西。这本书对于任何对学习 Python 高级概念感兴趣的人来说都应该很棒。任何具有基本 Python 技能的人都应该能够理解本书的内容,尽管对于经验较少的程序员来说可能需要额外的努力。对于那些仍然使用 Python 2.7 或更早版本的人来说,这也应该是 Python 3.5 的一个很好的介绍。
最终,应该从阅读本书中受益最多的群体是 Web 开发人员和后端工程师。这是因为本书中有两个特别重要的主题:可靠的代码部署和并发。
惯例
在本书中,您将找到一些区分不同信息类型的文本样式。以下是这些样式的一些示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“使用str.encode(encoding, errors)方法,该方法使用注册的编码器对字符串进行编码。”
一块代码设置如下:
[print("hello world")
print "goodbye python2"
当我们希望引起您对代码块的特定部分的注意时,相关行或项目以粗体设置:
cdef long long fibonacci_cc(unsigned int n) nogil:
if n < 2:
return n
else:
return fibonacci_cc(n - 1) + fibonacci_cc(n - 2)
任何命令行输入或输出都以以下方式编写:
$ pip show pip
---
Metadata-Version: 2.0
Name: pip
Version: 7.1.2
Summary: The PyPA recommended tool for installing Python packages.
Home-page: https://pip.pypa.io/
Author: The pip developers
Author-email: python-virtualenv@groups.google.com
License: MIT
Location: /usr/lib/python2.7/site-packages
Requires:
新术语和重要单词以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中出现在文本中,就像这样:“单击下一步按钮将您移动到下一个屏幕。”
注意
警告或重要说明显示在这样的框中。
提示
提示和技巧显示如下。
第一章:Python 的当前状态
Python 对开发人员来说是很好的。
无论您或您的客户使用什么操作系统,它都可以工作。除非您编写特定于平台的内容,或者使用特定于平台的库,您可以在 Linux 上工作,然后在其他系统上部署,例如。然而,这已经不再是不寻常的事情了(Ruby、Java 和许多其他语言也是这样工作的)。再加上我们将在本书中发现的其他优点,Python 成为公司主要开发语言的明智选择。
本书专注于 Python 的最新版本 3.5,并且所有的代码示例都是用这个版本的语言编写的,除非另一个版本被明确提到。因为这个版本还没有被广泛使用,本章包含了一些关于 Python 3 当前现状的描述,以便向读者介绍它,以及一些关于 Python 开发的现代方法的介绍信息。本章涵盖以下主题:
-
如何在 Python 2 和 Python 3 之间保持兼容性
-
如何在应用程序和操作系统级别上处理环境隔离问题,以便进行开发
-
如何增强 Python 提示
-
如何使用 pip 安装软件包
一本书总是以一些开胃菜开始的。因此,如果您已经熟悉 Python(尤其是最新的 3.x 分支),并且知道如何正确地隔离开发环境,您可以跳过本章的前两节,快速阅读其他部分。它们描述了一些非必要但可以极大提高 Python 生产力的工具和资源。但是一定要阅读有关应用级环境隔离和 pip 的部分,因为它们的安装对本书的其余部分是强制性的。
我们现在在哪里,我们将走向何方?
Python 的历史始于 20 世纪 80 年代末,但它的 1.0 发布日期是在 1994 年,所以它并不是一种非常年轻的语言。这里可以提到一系列主要的 Python 发布日期,但真正重要的是一个日期:2008 年 12 月 3 日——Python 3.0 的发布日期。
在撰写本文时,距离第一个 Python 3 发布已经过去了七年。自 PEP 404 的创建以来已经过去了四年,这是官方文件,它“取消发布”了 Python 2.8,并正式关闭了 2.x 分支。尽管已经过去了很长时间,但在 Python 社区中存在着一个特定的二分法——虽然语言发展非常迅速,但有一大群用户不愿意向前发展。
为什么以及 Python 如何改变?
答案很简单——Python 之所以改变,是因为有这样的需求。竞争是不会停止的。每隔几个月,就会有一种新语言突然冒出来,声称解决了所有先前语言的问题。大多数这样的项目很快就失去了开发人员的注意,它们的流行程度是由突然的热潮所推动的。
无论如何,这都是一些更大问题的迹象。人们设计新语言,是因为他们发现现有的语言无法以最佳方式解决他们的问题。不承认这种需求是愚蠢的。此外,Python 的越来越广泛的使用表明它在许多方面都可以,也应该得到改进。
Python 的许多改进通常是由其使用领域的特定需求驱动的。最重要的一个是 Web 开发,这需要改进来处理 Python 中的并发性。
一些变化仅仅是由于 Python 项目的年龄和成熟度所致。多年来,它已经收集了一些混乱的东西,例如非组织化和冗余的标准库模块或一些糟糕的设计决策。首先,Python 3 发布旨在为语言带来重大的清理和更新,但时间表明这个计划有些事与愿违。很长一段时间以来,它被许多开发人员视为一种好奇心,但希望情况正在改变。
了解最新变化-PEP 文档
Python 社区有一种处理变化的成熟方式。虽然关于 Python 语言的推测性想法大多在特定的邮件列表上讨论(<python-ideas@python.org>),但没有新的文件称为 PEP,就不会发生重大变化。PEP是Python Enhancement Proposal的缩写。它是提出对 Python 的改变的文件,也是社区讨论的起点。这些文件的整个目的、格式和工作流程也是以 Python Enhancement Proposal 的形式标准化的,准确地说,是 PEP 1 文档(www.python.org/dev/peps/pep-0001)。
PEP 文档对 Python 非常重要,根据主题的不同,它们有不同的用途:
-
通知:它们总结了核心 Python 开发人员所需的信息,并通知 Python 发布时间表
-
标准化:它们提供代码风格、文档或其他指南
-
设计:它们描述了提出的功能。
所有提出的 PEP 的列表都可以在一个文档中找到——PEP 0(www.python.org/dev/peps/)。由于它们在一个地方很容易获得,而且实际的 URL 也很容易猜到,它们通常在书中按编号引用。
对于那些想知道 Python 语言发展方向的人,但没有时间跟踪 Python 邮件列表上的讨论,PEP 0 文档可以是一个很好的信息来源。它显示了哪些文件已经被接受但尚未实施,以及哪些仍在考虑中。
PEP 还有其他用途。人们经常问类似的问题:
-
为什么 A 功能以这种方式工作?
-
为什么 Python 没有 B 功能?
在大多数这样的情况下,详细答案可以在特定的 PEP 文档中找到,其中已经提到了这样的功能。有很多 PEP 文档描述了提出但未被接受的 Python 语言功能。这些文件被留作历史参考。
写作本书时的 Python 3 采用情况
那么,由于新的激动人心的功能,Python 3 在其社区中被广泛采用吗?可惜,还没有。跟踪 Python 3 分支与大多数流行软件包兼容性的流行页面 Python 3 Wall of Superpowers(python3wos.appspot.com)直到不久前还被称为 Python 3 Wall of Shame。这种情况正在改变,提到的页面上列出的软件包表格每个月都在慢慢变得“更绿”。但是,这并不意味着所有构建应用程序的团队很快就会只使用 Python 3。当所有流行软件包都在 Python 3 上可用时,常见的借口——我们使用的软件包尚未移植——将不再有效。
这种情况的主要原因是,将现有应用程序从 Python 2 迁移到 Python 3 始终是一个挑战。有一些工具,比如 2to3 可以执行自动代码转换,但不能保证结果是 100%正确。此外,这样翻译的代码可能不如原始形式那样表现良好,需要手动调整。将现有复杂代码库移植到 Python 3 可能需要巨大的努力和成本,一些组织可能无法承担。但这样的成本可以分摊。一些良好的软件架构设计方法,如面向服务的架构或微服务,可以帮助逐步实现这一目标。新项目组件(服务或微服务)可以使用新技术编写,现有项目可以逐个移植。
从长远来看,转移到 Python 3 对项目只会产生有益的影响。根据 PEP-404,在 Python 2.x 分支中将不再发布 2.8 版本。此外,将来可能会有一段时间,像 Django、Flask 和 numpy 这样的所有主要项目都将放弃任何 2.x 兼容性,只能在 Python 3 上使用。
我对这个话题的个人看法可能是有争议的。我认为对于社区来说,最好的激励是在创建新软件包时完全放弃对 Python 2 的支持。当然,这大大限制了这类软件的影响范围,但这可能是改变那些坚持使用 Python 2.x 的人思维方式的唯一途径。
Python 3 和 Python 2 之间的主要区别
已经说过 Python 3 与 Python 2 破坏了向后兼容性。但这并不意味着完全重新设计。也不意味着每个为 2.x 版本编写的 Python 模块都将在 Python 3 下停止工作。可以编写完全跨兼容的代码,将在两个主要版本上运行,而无需额外的工具或技术,但通常只适用于简单的应用程序。
我为什么要在意?
尽管我在本章前面提到了我对 Python 2 兼容性的个人看法,但现在不可能就这样忘记它。仍然有一些有用的包(比如在第六章中提到的 fabric,部署代码)真的值得使用,但在不久的将来可能不会被移植。
有时候,我们可能会受到我们所在组织的限制。现有的遗留代码可能非常复杂,以至于移植它在经济上是不可行的。因此,即使我们决定从现在开始只在 Python 3 世界中生活,也不可能在一段时间内完全不使用 Python 2。
如今,要成为专业开发人员,很难不回馈社区,因此帮助开源开发者将现有软件包添加 Python 3 兼容性是偿还使用它们所带来的“道义债务”的好方法。当然,这是不可能做到的,而不知道 Python 2 和 Python 3 之间的差异。顺便说一句,这对于那些刚接触 Python 3 的人来说也是一个很好的练习。
主要的语法差异和常见陷阱
Python 文档是每个版本之间差异的最佳参考。无论如何,为了方便读者,本节总结了最重要的差异。这并不改变文档对于那些尚不熟悉 Python 3 的人来说是必读的事实(参见docs.python.org/3.0/whatsnew/3.0.html)。
Python 3 引入的破坏性更改通常可以分为几个组:
-
语法更改,其中一些语法元素被移除/更改,其他元素被添加
-
标准库的更改
-
数据类型和集合的更改
语法更改
使现有代码难以运行的语法更改是最容易发现的——它们将导致代码根本无法运行。具有新语法元素的 Python 3 代码将无法在 Python 2 上运行,反之亦然。被移除的元素将使 Python 2 代码与 Python 3 明显不兼容。具有这些问题的运行代码将立即导致解释器失败,引发SyntaxError异常。以下是一个破损脚本的示例,其中恰好有两个语句,由于语法错误,都不会被执行:
print("hello world")
print "goodbye python2"
在 Python 3 上运行时的实际结果如下:
$ python3 script.py
**File "script.py", line 2
**print "goodbye python2"
**^
SyntaxError: Missing parentheses in call to 'print'
这样的差异列表有点长,而且,任何新的 Python 3.x 版本可能会不时地添加新的语法元素,这些元素会在早期的 Python 版本(甚至在同一个 3.x 分支上)上引发错误。其中最重要的部分在第二章和第三章中都有所涵盖,因此这里没有必要列出所有这些内容。
从 Python 2.7 中删除或更改的事项列表较短,因此以下是最重要的事项:
-
print不再是语句,而是一个函数,因此括号现在是必需的。 -
捕获异常从
except exc, var变为except exc as var。 -
<>比较运算符已被移除,改用!=。 -
from module import *(docs.python.org/3.0/reference/simple_stmts.html#import)现在只允许在模块级别上使用,不再在函数内部使用。 -
from .[module] import name现在是相对导入的唯一接受的语法。所有不以点字符开头的导入都被解释为绝对导入。 -
sort()函数和列表的sorted()方法不再接受cmp参数。应该使用key参数代替。 -
整数的除法表达式,如 1/2 会返回浮点数。截断行为是通过
//运算符实现的,比如1//2。好处是这也可以用于浮点数,所以5.0//2.0 == 2.0。
标准库中的变化
标准库中的重大变化是在语法变化之后最容易捕捉到的。每个后续版本的 Python 都会添加、弃用、改进或完全删除标准库模块。这样的过程在 Python 的旧版本(1.x 和 2.x)中也是常见的,因此在 Python 3 中并不令人震惊。在大多数情况下,根据被移除或重新组织的模块(比如urlparse被移动到urllib.parse),它将在导入时立即引发异常。这使得这类问题很容易被捕捉到。无论如何,为了确保所有这类问题都得到覆盖,完整的测试代码覆盖是必不可少的。在某些情况下(例如,当使用延迟加载模块时),通常在导入时注意到的问题在一些模块作为函数调用的代码中使用之前不会出现。这就是为什么在测试套件中确保每行代码实际执行非常重要。
提示
延迟加载模块
延迟加载模块是在导入时不加载的模块。在 Python 中,import语句可以包含在函数内部,因此导入将在函数调用时发生,而不是在导入时发生。在某些情况下,这种模块的加载可能是一个合理的选择,但在大多数情况下,这是对设计不佳的模块结构的一种变通方法(例如,避免循环导入),并且通常应该避免。毫无疑问,没有理由去延迟加载标准库模块。
数据类型和集合的变化
Python 表示数据类型和集合的变化需要开发人员在尝试保持兼容性或简单地将现有代码移植到 Python 3 时付出最大的努力。虽然不兼容的语法或标准库变化很容易被注意到并且最容易修复,但集合和类型的变化要么不明显,要么需要大量重复的工作。这样的变化列表很长,再次,官方文档是最好的参考。
然而,这一部分必须涵盖 Python 3 中字符串文字处理方式的变化,因为尽管这是一个非常好的变化,现在使事情更加明确,但它似乎是 Python 3 中最具争议和讨论的变化。
所有字符串文字现在都是 Unicode,bytes文字需要b或B前缀。对于 Python 3.0 和 3.1,使用u前缀(如u"foo")已被删除,并将引发语法错误。放弃该前缀是所有争议的主要原因。这使得在不同分支的 Python 版本中创建兼容的代码变得非常困难——版本 2.x 依赖于该前缀以创建 Unicode 文字。该前缀在 Python 3.3 中被重新引入以简化集成过程,尽管没有任何语法意义。
用于维护跨版本兼容性的流行工具和技术
在 Python 版本之间保持兼容性是一项挑战。这可能会增加很多额外的工作,具体取决于项目的规模,但绝对是可行的,也是值得做的。对于旨在在许多环境中重复使用的软件包,这是绝对必须的。没有明确定义和测试过的兼容性边界的开源软件包很不可能变得流行,但也是,从不离开公司网络的封闭的第三方代码可以从在不同环境中进行测试中获益。
值得注意的是,虽然本部分主要关注 Python 的各个版本之间的兼容性,但这些方法也适用于与外部依赖项(如不同的软件包版本、二进制库、系统或外部服务)保持兼容性。
整个过程可以分为三个主要领域,按重要性排序:
-
定义和记录目标兼容性边界以及如何管理它们
-
在每个环境和每个声明为兼容的依赖版本中进行测试
-
实施实际的兼容性代码
定义什么被认为是兼容的是整个过程中最重要的部分,因为它为代码的用户(开发人员)提供了对其工作方式和未来可能发生变化的期望和假设的能力。我们的代码可以作为不同项目中的依赖项使用,这些项目可能也致力于管理兼容性,因此理解其行为方式的能力至关重要。
虽然本书试图总是提供几种选择,而不是对特定选项给出绝对建议,但这是少数例外之一。到目前为止,定义未来兼容性可能如何改变的最佳方法是使用语义化版本(semver.org/),或简称 semver。它描述了一种广泛接受的标准,通过版本说明符仅由三个数字组成,标记了代码变化的范围。它还提供了一些建议,关于如何处理弃用策略。以下是其摘要的一部分:
给定版本号MAJOR.MINOR.PATCH,递增:
-
当您进行不兼容的 API 更改时,使用
MAJOR版本 -
在向后兼容的方式中添加功能时的
MINOR版本 -
当您进行向后兼容的错误修复时,使用
PATCH版本
预发布和构建元数据的附加标签可作为MAJOR.MINOR.PATCH格式的扩展。
当涉及测试时,令人沮丧的事实是,为了确保代码与每个声明的依赖版本和每个环境(这里是 Python 版本)兼容,必须在这些组合的每个组合中进行测试。当项目具有大量依赖项时,这当然可能是不可能的,因为随着每个新版本的依赖项,组合的数量会迅速增长。因此,通常需要做出一些权衡,以便运行完整的兼容性测试不会花费很长时间。在第十章中介绍了一些帮助测试所谓矩阵的工具,测试驱动开发,讨论了测试。
注意
使用遵循 semver 的项目的好处通常是只需要测试主要版本,因为次要和补丁版本保证不包含向后不兼容的更改。只有在这样的项目可以信任不违反这样的合同时才成立。不幸的是,每个人都会犯错误,并且许多项目甚至在补丁版本上也会发生向后不兼容的更改。然而,由于 semver 声明了次要和补丁版本更改的严格兼容性,违反它被认为是一个错误,因此可以在补丁版本中修复。
兼容性层的实现是最后的,也是最不重要的,如果该兼容性的边界被明确定义并经过严格测试。但是,仍然有一些工具和技术,每个对这样一个主题感兴趣的程序员都应该知道。
最基本的是 Python 的__future__模块。它将一些新版本 Python 的功能移回到旧版本,并采用 import 语句的形式:
from __future__ import <feature>
future语句提供的功能是与语法相关的元素,不能通过其他方式轻松处理。此语句仅影响其使用的模块。以下是 Python 2.7 交互会话的示例,它从 Python 3.0 中引入了 Unicode 文字:
Python 2.7.10 (default, May 23 2015, 09:40:32) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> type("foo") # old literals
<type 'str'>
>>> from __future__ import unicode_literals
>>> type("foo") # now is unicode
<type 'unicode'>
以下是所有可用的__future__语句选项的列表,关心 2/3 兼容性的开发人员应该知道:
-
division:这添加了 Python 3 的除法运算符(PEP 238) -
absolute_import:这使得以点字符开头的import语句的每种形式都被解释为绝对导入(PEP 328) -
print_function:这将把print语句改为函数调用,因此print周围的括号变得必需(PEP 3112) -
unicode_literals:这使得每个字符串文字都被解释为 Unicode 文字(PEP 3112)
__future__语句选项的列表非常短,只涵盖了一些语法特性。像元类语法(这是一个高级特性,涵盖在第三章中,语法最佳实践-类级别以上)这样的其他更改,要维护起来就困难得多。可靠地处理多个标准库重组也不能通过future语句解决。幸运的是,有一些工具旨在提供一致的可用兼容性层。最常见的是 Six(pypi.python.org/pypi/six/),它提供了整个通用的 2/3 兼容性样板作为单个模块。另一个有前途但稍微不那么受欢迎的工具是 future 模块(python-future.org/)。
在某些情况下,开发人员可能不希望在一些小包中包含额外的依赖项。一个常见的做法是额外的模块,它收集所有兼容性代码,通常命名为compat.py。以下是从python-gmaps项目(github.com/swistakm/python-gmaps)中获取的这样一个compat模块的示例:
# -*- coding: utf-8 -*-
import sys
if sys.version_info < (3, 0, 0):
import urlparse # noqa
def is_string(s):
return isinstance(s, basestring)
else:
from urllib import parse as urlparse # noqa
def is_string(s):
return isinstance(s, str)
即使在依赖于 Six 进行 2/3 兼容性的项目中,这样的compat.py模块也很受欢迎,因为这是一种非常方便的方式来存储处理与用作依赖项的不同版本的包的兼容性的代码。
提示
下载示例代码
您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的支持选项卡上。
-
单击代码下载和勘误。
-
在搜索框中输入书名。
-
选择您要下载代码文件的书籍。
-
从下拉菜单中选择您购买此书的位置。
-
点击代码下载。
下载文件后,请确保使用最新版本的解压或提取文件夹:
-
WinRAR / 7-Zip for Windows
-
Zipeg / iZip / UnRarX for Mac
-
7-Zip / PeaZip for Linux
该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Expert-Python-Programming_Second-Edition。我们还有其他丰富的书籍和视频代码包可供使用,网址为github.com/PacktPublishing/。去看看吧!
不仅仅是 CPython
主要的 Python 实现是用 C 语言编写的,称为CPython。当语言发展时,C 实现会相应地进行更改。除了 C,Python 还有其他几种实现,它们试图跟上主流。它们中的大多数都比 CPython 落后了几个里程碑,但为在特定环境中使用和推广该语言提供了绝佳的机会。
我为什么要在意呢?
有很多可用的替代 Python 实现。关于这个主题的 Python Wiki 页面(wiki.python.org/moin/PythonImplementations)列出了 20 多种不同的语言变体、方言或 Python 解释器的实现,这些实现都是用其他语言而不是 C 构建的。其中一些只实现了核心语言语法、特性和内置扩展的子集,但至少有一些几乎完全兼容 CPython。最重要的是要知道,虽然其中一些只是玩具项目或实验,但大多数是为了解决一些真正的问题而创建的——这些问题要么无法用 CPython 解决,要么需要开发人员付出太多的努力。这些问题的例子有:
-
在嵌入式系统上运行 Python 代码
-
与 Java 或.NET 等运行时框架编写的代码集成,或者与不同语言编写的代码集成
-
在 Web 浏览器中运行 Python 代码
本节提供了一个主观上最受欢迎和最新的选择的简短描述,这些选择目前适用于 Python 程序员。
Stackless Python
Stackless Python 自称是 Python 的增强版本。Stackless 之所以被命名为 Stackless,是因为它避免依赖 C 调用堆栈来进行自己的堆栈。实际上,它是修改过的 CPython 代码,还添加了一些当时核心 Python 实现中缺失的新功能。其中最重要的是由解释器管理的微线程,它是普通线程的一种廉价而轻量级的替代,普通线程必须依赖于系统内核上下文切换和任务调度。
最新可用的版本是 2.7.9 和 3.3.5,分别实现了 Python 2.7 和 3.3 版本。Stackless 提供的所有附加功能都通过内置的stackless模块作为该发行版中的一个框架暴露出来。
Stackless 并不是 Python 的最受欢迎的替代实现,但它值得知道,因为它引入的想法对语言社区产生了很大的影响。核心切换功能是从 Stackless 中提取出来的,并发布为一个名为greenlet的独立包,现在已经成为许多有用的库和框架的基础。此外,它的大多数功能已经在 PyPy 中重新实现,PyPy 是另一个稍后将介绍的 Python 实现。参考stackless.readthedocs.org/。
Jython
Jython 是语言的 Java 实现。它将代码编译成 Java 字节码,并允许开发人员在其 Python 模块中无缝使用 Java 类。Jython 允许人们在复杂的应用系统中使用 Python 作为顶级脚本语言,例如 J2EE。它还将 Java 应用程序引入 Python 世界。使 Apache Jackrabbit(这是一个基于 JCR 的文档存储库 API;请参见jackrabbit.apache.org)在 Python 程序中可用是 Jython 允许的一个很好的例子。
Jython 的最新可用版本是 Jython 2.7,对应于语言的 2.7 版本。它被宣传为几乎实现了所有核心 Python 标准库,并使用相同的回归测试套件。Jython 3.x 的版本正在开发中。
与 CPython 实现相比,Jython 的主要区别是:
-
真正的 Java 垃圾回收,而不是引用计数
-
缺乏全局解释器锁(GIL)允许更好地利用多核在多线程应用程序中
该语言实现的主要弱点是不支持 CPython 扩展 API,因此不支持用 C 编写的 Python 扩展将无法在 Jython 中运行。这可能会在未来发生变化,因为计划在 Jython 3.x 中支持 CPython 扩展 API。
一些 Python Web 框架,如 Pylons,被认为正在推动 Jython 的发展,使其在 Java 世界中可用。参见www.jython.org。
IronPython
IronPython 将 Python 引入了.NET Framework。该项目得到了微软的支持,IronPython 的主要开发人员在这里工作。这对于推广一种语言来说是非常重要的实现。除了 Java,.NET 社区是最大的开发者社区之一。值得注意的是,微软提供了一套免费的开发工具,可以将 Visual Studio 变成一个功能齐全的 Python IDE。这被分发为名为PVTS(Visual Studio 的 Python 工具)的 Visual Studio 插件,并且作为开源代码在 GitHub 上可用(microsoft.github.io/PTVS)。
最新的稳定版本是 2.7.5,与 Python 2.7 兼容。与 Jython 类似,Python 3.x 实现周围也有一些开发,但目前还没有稳定版本。尽管.NET 主要在 Microsoft Windows 上运行,但也可以在 Mac OS X 和 Linux 上运行 IronPython。这要归功于 Mono,一个跨平台的开源.NET 实现。
IronPython 相对于 CPython 的主要区别或优势如下:
-
与 Jython 类似,缺乏全局解释器锁(GIL)允许更好地利用多核在多线程应用程序中
-
用 C#和其他.NET 语言编写的代码可以轻松集成到 IronPython 中,反之亦然
-
可以在所有主要的 Web 浏览器中通过 Silverlight 运行
IronPython 的弱点与 Jython 非常相似,因为它不支持 CPython 扩展 API。这对于希望使用诸如 numpy 之类的大部分基于 C 扩展的软件包的开发人员来说非常重要。有一个名为 ironclad 的项目(参见github.com/IronLanguages/ironclad),旨在允许使用这些扩展与 IronPython 无缝集成,尽管其最后已知支持的版本是 2.6,开发似乎已经停止。参见ironpython.net/。
PyPy
PyPy 可能是最令人兴奋的实现,因为其目标是将 Python 重写为 Python。在 PyPy 中,Python 解释器本身就是用 Python 编写的。我们有一个 C 代码层来执行 Python 的 CPython 实现的基本工作。然而,在 PyPy 实现中,这个 C 代码层被重写为纯 Python。
这意味着您可以在执行时更改解释器的行为,并实现在 CPython 中无法轻松完成的代码模式。
PyPy 目前旨在与 Python 2.7 完全兼容,而 PyPy3 与 Python 3.2.5 版本兼容。
过去,PyPy 主要因理论原因而引人关注,它吸引了那些喜欢深入了解语言细节的人。它并不常用于生产,但这在多年来已经发生了改变。如今,许多基准测试显示,令人惊讶的是,PyPy 通常比 CPython 实现要快得多。该项目有自己的基准测试网站,跟踪每个版本的性能,使用数十种不同的基准测试进行测量(参见speed.pypy.org/)。这清楚地表明,启用 JIT 的 PyPy 至少比 CPython 快几倍。这和 PyPy 的其他特性使越来越多的开发人员决定在生产环境中切换到 PyPy。
PyPy 与 CPython 实现相比的主要区别是:
-
使用垃圾收集而不是引用计数
-
集成跟踪 JIT 编译器可以显著提高性能
-
从 Stackless Python 借用的应用级 Stackless 功能
与几乎所有其他替代 Python 实现一样,PyPy 缺乏对 CPython 扩展 API 的全面官方支持。尽管如此,它至少通过其 CPyExt 子系统提供了对 C 扩展的某种支持,尽管文档贫乏且功能尚不完整。此外,社区正在努力将 NumPy 移植到 PyPy,因为这是最受欢迎的功能。参见pypy.org。
Python 开发的现代方法
选择的编程语言的深入理解是作为专家最重要的事情。这对于任何技术来说都是真实的。然而,如果不了解特定语言社区内的常用工具和实践,要开发出优秀的软件是非常困难的。Python 没有任何一个特性是其他语言中找不到的。因此,在语法、表现力或性能的直接比较中,总会有一个或多个领域更好的解决方案。但 Python 真正脱颖而出的领域是围绕该语言构建的整个生态系统。多年来,Python 社区一直在完善标准实践和库,帮助更快地创建更可靠的软件。
提到的生态系统中最明显和重要的部分是大量解决各种问题的免费开源软件包。编写新软件总是一个昂贵且耗时的过程。能够重用现有代码而不是“重复造轮子”大大减少了开发的时间和成本。对一些公司来说,这是他们的项目经济可行的唯一原因。
因此,Python 开发人员花了很多精力来创建工具和标准,以处理他人创建的开源软件包。从虚拟隔离环境、改进的交互式 shell 和调试器,到帮助发现、搜索和分析PyPI(Python 软件包索引)上可用的大量软件包的程序。
Python 环境的应用级隔离
如今,许多操作系统都将 Python 作为标准组件。大多数 Linux 发行版和基于 Unix 的系统,如 FreeBSD、NetBSD、OpenBSD 或 OS X,都默认安装了 Python,或者可以通过系统软件包存储库获得。其中许多甚至将其用作一些核心组件的一部分——Python 驱动 Ubuntu(Ubiquity)、Red Hat Linux(Anaconda)和 Fedora(再次是 Anaconda)的安装程序。
由于这个事实,PyPI 上的许多软件包也可以作为系统软件包管理工具(如apt-get(Debian,Ubuntu),rpm(Red Hat Linux)或emerge(Gentoo))管理的本地软件包。尽管应该记住,可用库的列表非常有限,而且与 PyPI 相比,它们大多已经过时。这就是为什么pip应该始终被用来获取最新版本的新软件包,作为PyPA(Python Packaging Authority)的建议。尽管它是 CPython 2.7.9 和 3.4 版本的独立软件包,但它默认随每个新版本捆绑发布。安装新软件包就像这样简单:
pip install <package-name>
除其他功能外,pip允许强制使用特定版本的软件包(使用pip install package-name==version语法)并升级到最新可用版本(使用--upgrade开关)。本书中介绍的大多数命令行工具的完整使用说明可以通过简单地运行带有-h或--help开关的命令来轻松获得,但这里有一个示例会话,演示了最常用的选项:
$ pip show pip
---
Metadata-Version: 2.0
Name: pip
Version: 7.1.2
Summary: The PyPA recommended tool for installing Python packages.
Home-page: https://pip.pypa.io/
Author: The pip developers
Author-email: python-virtualenv@groups.google.com
License: MIT
Location: /usr/lib/python2.7/site-packages
Requires:
$ pip install 'pip<7.0.0'
Collecting pip<7.0.0
**Downloading pip-6.1.1-py2.py3-none-any.whl (1.1MB)
**100% |████████████████████████████████| 1.1MB 242kB/s
Installing collected packages: pip
**Found existing installation: pip 7.1.2
**Uninstalling pip-7.1.2:
**Successfully uninstalled pip-7.1.2
Successfully installed pip-6.1.1
You are using pip version 6.1.1, however version 7.1.2 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
$ pip install --upgrade pip
You are using pip version 6.1.1, however version 7.1.2 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
Collecting pip
**Using cached pip-7.1.2-py2.py3-none-any.whl
Installing collected packages: pip
**Found existing installation: pip 6.1.1
**Uninstalling pip-6.1.1:
**Successfully uninstalled pip-6.1.1
Successfully installed pip-7.1.2
在某些情况下,pip可能不是默认可用的。从 Python 3.4 版本开始(也是 Python 2.7.9),它始终可以使用ensurepip模块进行引导:
$ python -m ensurepip
Ignoring indexes: https://pypi.python.org/simple
Requirement already satisfied (use --upgrade to upgrade): setuptools in /usr/lib/python2.7/site-packages
Collecting pip
Installing collected packages: pip
Successfully installed pip-6.1.1
有关如何为旧版本的 Python 安装 pip 的最新信息,请访问项目的文档页面pip.pypa.io/en/stable/installing/。
为什么要隔离?
pip可用于安装系统范围的软件包。在基于 Unix 和 Linux 的系统上,这将需要超级用户权限,因此实际调用将是:
sudo pip install <package-name>
请注意,这在 Windows 上不是必需的,因为它默认不提供 Python 解释器,通常由用户手动安装 Python 而不需要超级用户权限。
无论如何,不建议直接从 PyPI 全局安装系统范围的软件包,应该避免这样做。这似乎与之前的使用pip是 PyPA 建议的说法相矛盾,但这其中有一些严重的原因。如前所述,Python 往往是许多通过操作系统软件包存储库可用的软件包的重要组成部分,并且可能支持许多重要的服务。系统分发维护者在选择正确的软件包版本以匹配各种软件包依赖关系方面付出了很多努力。经常情况下,从系统软件包存储库中可用的 Python 软件包包含自定义补丁或者保持过时,只是为了确保与其他一些系统组件的兼容性。使用pip强制更新此类软件包到破坏某些向后兼容性的版本可能会破坏一些关键的系统服务。
仅在本地计算机上进行开发目的的这样做也不是一个好的借口。这样鲁莽地使用pip几乎总是在自找麻烦,并最终会导致非常难以调试的问题。这并不意味着全局安装来自 PyPI 的软件包是严格禁止的,但它应该始终是有意识地并且了解相关风险的情况下进行的。
幸运的是,有一个简单的解决方案——环境隔离。有各种工具可以在不同系统抽象级别上隔离 Python 运行时环境。主要思想是将项目依赖项与不同项目和/或系统服务所需的软件包隔离开来。这种方法的好处是:
-
它解决了“项目 X 依赖于 1.x 版本,但项目 Y 需要 4.x”困境。开发人员可以在具有不同依赖关系的多个项目上工作,甚至可能发生冲突,而不会影响彼此。
-
项目不再受限于系统分发存储库中提供的软件包版本。
-
由于新的包版本只在这样的环境中可用,所以不会有破坏依赖于特定包版本的其他系统服务的风险。
-
项目依赖的包列表可以很容易地被“冻结”,因此很容易重现它们。
最简单和最轻量级的隔离方法是使用应用级虚拟环境。它们只专注于隔离 Python 解释器和其中可用的包。它们非常容易设置,通常足以确保在开发小型项目和包时进行适当的隔离。
不幸的是,在某些情况下,这可能不足以确保足够的一致性和可重现性。对于这种情况,系统级别的隔离是工作流程的一个很好的补充,本章后面将解释一些可用的解决方案。
流行的解决方案
有几种隔离 Python 运行时的方法。最简单和最明显的方法,尽管最难维护,是手动更改PATH和PYTHONPATH环境变量和/或将 Python 二进制文件移动到不同的位置,以影响它发现可用包的方式,并将其更改为我们想要存储项目依赖项的自定义位置。幸运的是,有几种可用的工具可以帮助维护虚拟环境以及安装包在系统中的存储方式。主要有:virtualenv、venv和buildout。它们在底层的操作实际上与我们手动操作的相同。实际的策略取决于具体的工具实现,但通常它们更方便使用并且可以提供额外的好处。
virtualenv
Virtualenv 是这个列表中迄今为止最受欢迎的工具。它的名字简单地代表虚拟环境。它不是标准 Python 发行版的一部分,因此需要使用pip来获取。它是值得系统范围内安装的包之一(在 Linux 和基于 Unix 的系统上使用sudo)。
一旦安装完成,可以使用以下命令创建一个新的虚拟环境:
virtualenv ENV
在这里,ENV应该被新环境的期望名称替换。这将在当前工作目录路径中创建一个新的ENV目录。它将包含几个新的目录:
-
bin/:这是存储新 Python 可执行文件和其他包提供的脚本/可执行文件的地方。 -
lib/和include/:这些目录包含了虚拟环境中新 Python 的支持库文件。新的包将安装在ENV/lib/pythonX.Y/site-packages/中。
一旦创建了新的环境,就需要在当前 shell 会话中使用 Unix 的 source 命令激活它:
source ENV/bin/activate
这会通过影响其环境变量改变当前 shell 会话的状态。为了让用户意识到他已经激活了虚拟环境,它会通过在其开头添加(ENV)字符串来改变 shell 提示。以下是一个创建新环境并激活它的示例会话:
$ virtualenv example
New python executable in example/bin/python
Installing setuptools, pip, wheel...done.
$ source example/bin/activate
(example)$ deactivate
$**
关于virtualenv的重要事情是,它完全依赖于存储在文件系统上的状态。它不提供任何额外的能力来跟踪应该安装在其中的包。这些虚拟环境不可移植,不应该移动到另一个系统/机器上。这意味着需要为每个新的应用部署从头开始创建新的虚拟环境。因此,virtualenv用户使用的一个良好的实践是将所有项目依赖项存储在requirements.txt文件中(这是命名约定),如下面的代码所示:
# lines followed by hash (#) are treated as a comments
# strict version names are best for reproducibility
eventlet==0.17.4
graceful==0.1.1
# for projects that are well tested with different
# dependency versions the relative version specifiers
# are acceptable too
falcon>=0.3.0,<0.5.0
# packages without versions should be avoided unless
# latest release is always required/desired
pytz
有了这样的文件,所有依赖项都可以很容易地使用pip进行安装,因为它接受 requirements 文件作为其输出。
pip install -r requirements.txt
需要记住的是,要求文件并不总是理想的解决方案,因为它并没有定义确切的依赖项列表,只有要安装的依赖项。因此,整个项目在开发环境中可以正常工作,但如果要求文件过时并且不反映环境的实际状态,它将无法在其他环境中启动。当然,有pip freeze命令可以打印当前环境中的所有软件包,但不应该盲目使用它——它会输出所有内容,甚至是仅用于测试而不在项目中使用的软件包。书中提到的另一个工具buildout解决了这个问题,因此对于一些开发团队来说,它可能是更好的选择。
注意
对于 Windows 用户,在 Windows 下,virtualenv使用不同的命名方式来命名其目录的内部结构。您需要使用Scripts/,Libs/和Include/,而不是bin/,lib/,include/,以更好地匹配该操作系统上的开发约定。激活/停用环境的命令也不同;您需要使用ENV/Scripts/activate.bat和ENV/Scripts/deactivate.bat,而不是在activate和deactivate脚本上使用source。
venv
虚拟环境很快在社区内得到了很好的建立,并成为了一个受欢迎的工具。从 Python 3.3 开始,创建虚拟环境得到了标准库的支持。使用方式几乎与 Virtualenv 相同,尽管命令行选项的命名约定有很大不同。新的venv模块提供了一个pyvenv脚本来创建一个新的虚拟环境。
pyvenv ENV
这里,ENV应该被新环境的期望名称所替换。此外,现在可以直接从 Python 代码中创建新环境,因为所有功能都是从内置的venv模块中公开的。其他用法和实现细节,如环境目录的结构和激活/停用脚本,大部分与 Virtualenv 相同,因此迁移到这个解决方案应该是简单而无痛的。
对于使用较新版本 Python 的开发人员,建议使用venv而不是 Virtualenv。对于 Python 3.3,切换到venv可能需要更多的努力,因为在这个版本中,它不会默认在新环境中安装setuptools和pip,因此用户需要手动安装它们。幸运的是,这在 Python 3.4 中已经改变,而且由于venv的可定制性,可以覆盖其行为。详细信息在 Python 文档中有解释(参见docs.python.org/3.5/library/venv.html),但一些用户可能会发现它太棘手,会选择在特定版本的 Python 中继续使用 Virtualenv。
buildout
Buildout 是一个强大的用于引导和部署用 Python 编写的应用程序的工具。书中还将解释一些其高级功能。很长一段时间以来,它也被用作创建隔离的 Python 环境的工具。因为 Buildout 需要一个声明性的配置,必须在依赖关系发生变化时进行更改,而不是依赖于环境状态,因此这些环境更容易复制和管理。
很不幸,这已经改变了。自 2.0.0 版本以来,buildout软件包不再尝试提供与系统 Python 安装的任何级别的隔离。隔离处理留给其他工具,如 Virtualenv,因此仍然可以拥有隔离的 Buildouts,但事情变得有点复杂。必须在隔离的环境中初始化 Buildout 才能真正实现隔离。
与 Buildout 的旧版本相比,这有一个主要缺点,因为它依赖于其他解决方案进行隔离。编写此代码的开发人员不再能确定依赖关系描述是否完整,因为一些软件包可以通过绕过声明性配置进行安装。当然,这个问题可以通过适当的测试和发布程序来解决,但它给整个工作流程增加了一些复杂性。
总之,Buildout 不再是提供环境隔离的解决方案,但其声明性配置可以提高虚拟环境的可维护性和可重现性。
选择哪一个?
没有一种最佳解决方案适用于所有用例。在一个组织中适用的东西可能不适合其他团队的工作流程。此外,每个应用程序都有不同的需求。小型项目可以轻松依赖于单独的virtualenv或venv,但更大的项目可能需要buildout的额外帮助来执行更复杂的组装。
之前没有详细描述的是,Buildout 的旧版本(buildout<2.0.0)允许在与 Virtualenv 提供的类似结果的隔离环境中组装项目。不幸的是,该项目的 1.x 分支不再维护,因此不鼓励将其用于此目的。
我建议尽可能使用venv模块而不是 Virtualenv。因此,这应该是针对 Python 版本 3.4 及更高版本的项目的默认选择。在 Python 3.3 中使用venv可能有点不方便,因为缺乏对setuptools和pip的内置支持。对于针对更广泛的 Python 运行时(包括替代解释器和 2.x 分支)的项目,似乎 Virtualenv 是最佳选择。
系统级环境隔离
在大多数情况下,软件实现可以快速迭代,因为开发人员重用许多现有组件。不要重复自己——这是许多程序员的流行规则和座右铭。使用其他软件包和模块将它们包含在代码库中只是这种文化的一部分。可以被视为“重复使用组件”的还有二进制库、数据库、系统服务、第三方 API 等。甚至整个操作系统也应该被视为被重复使用。
基于 Web 的应用程序的后端服务是这类应用程序可以有多复杂的一个很好的例子。最简单的软件堆栈通常由几个层组成(从最低层开始):
-
数据库或其他类型的存储设备
-
Python 实现的应用程序代码
-
像 Apache 或 NGINX 这样的 HTTP 服务器
当然,这样的堆栈可能会更简单,但这是非常不可能的。事实上,大型应用程序通常非常复杂,很难区分单个层。大型应用程序可以使用许多不同的数据库,分为多个独立的进程,并使用许多其他系统服务进行缓存、排队、日志记录、服务发现等。遗憾的是,复杂性没有限制,代码似乎只是遵循热力学第二定律。
真正重要的是,并非所有软件堆栈元素都可以在 Python 运行时环境的级别上进行隔离。无论是 NGINX 这样的 HTTP 服务器还是 PostgreSQL 这样的 RDBMS,它们通常在不同系统上有不同版本。确保开发团队中的每个人使用每个组件的相同版本是非常困难的,没有适当的工具。理论上,一个团队中所有开发人员在一个项目上工作时能够在他们的开发环境中获得相同版本的服务。但是,如果他们不使用与生产环境相同的操作系统,所有这些努力都是徒劳的。而且,强迫程序员在他所钟爱的系统之外工作是不可能的。
问题在于可移植性仍然是一个巨大的挑战。并非所有服务在生产环境中都能像在开发者的机器上那样完全相同地工作,这种情况很可能不会改变。即使是 Python,尽管已经付出了很多工作来使其跨平台,但在不同的系统上可能会有不同的行为。通常,这些情况都有很好的文档记录,而且只会发生在直接依赖系统调用的地方,但依赖程序员记住一长串兼容性怪癖的能力是相当容易出错的策略。
解决这个问题的一个流行的解决方案是通过将整个系统隔离为应用程序环境。这通常是通过利用不同类型的系统虚拟化工具来实现的。虚拟化当然会降低性能,但对于具有硬件虚拟化支持的现代计算机来说,性能损失通常是可以忽略不计的。另一方面,可能获得的潜在收益列表非常长:
-
开发环境可以完全匹配生产中使用的系统版本和服务,有助于解决兼容性问题
-
系统配置工具(如 Puppet、Chef 或 Ansible)的定义(如果使用)可以被重用于配置开发环境
-
如果创建这样的环境是自动化的,新加入的团队成员可以轻松地加入项目
-
开发人员可以直接使用低级别的系统功能,这些功能可能在他们用于工作的操作系统上不可用,例如,在 Windows 中不可用的FUSE(用户空间文件系统)
使用 Vagrant 创建虚拟开发环境
Vagrant 目前似乎是提供创建和管理开发环境的最流行的工具。它适用于 Windows、Mac OS 和一些流行的 Linux 发行版(参见www.vagrantup.com)。它没有任何额外的依赖。Vagrant 以虚拟机或容器的形式创建新的开发环境。具体的实现取决于虚拟化提供者的选择。VirtualBox 是默认提供者,并且它已经与 Vagrant 安装程序捆绑在一起,但也有其他提供者可用。最显著的选择是 VMware、Docker、LXC(Linux 容器)和 Hyper-V。
Vagrant 中提供的最重要的配置是一个名为Vagrantfile的单个文件。它应该独立于每个项目。它提供的最重要的内容如下:
-
虚拟化提供者的选择
-
用作虚拟机镜像的 Box
-
配置方法的选择
-
虚拟机和虚拟机主机之间的共享存储
-
需要在虚拟机和其主机之间转发的端口
Vagrantfile的语法语言是 Ruby。示例配置文件提供了一个很好的模板来启动项目,并且有很好的文档,因此不需要了解这种语言。可以使用一个命令创建模板配置:
vagrant init
这将在当前工作目录中创建一个名为Vagrantfile的新文件。通常最好将此文件存储在相关项目源的根目录。这个文件已经是一个有效的配置,将使用默认提供者和基础盒子镜像创建一个新的虚拟机。默认情况下不启用任何配置。添加了Vagrantfile后,可以使用以下命令启动新的虚拟机:
vagrant up
初始启动可能需要几分钟,因为实际的盒子必须从网络上下载。每次启动已经存在的虚拟机时,还会有一些初始化过程,这可能需要一些时间,具体取决于所使用的提供者、盒子和系统性能。通常,这只需要几秒钟。一旦新的 Vagrant 环境启动并运行,开发人员可以使用以下简写连接到 SSH:
vagrant ssh
这可以在Vagrantfile位置下的项目源树中的任何位置完成。为了开发者的方便起见,我们将在上面的目录中查找配置文件,并将其与相关的 VM 实例进行匹配。然后,它建立安全外壳连接,因此开发环境可以像任何普通的远程机器一样进行交互。唯一的区别是整个项目源树(根定义为Vagrantfile位置)在 VM 的文件系统下的/vagrant/下是可用的。
容器化与虚拟化
容器是完整机器虚拟化的替代方案。这是一种轻量级的虚拟化方法,其中内核和操作系统允许运行多个隔离的用户空间实例。操作系统在容器和主机之间共享,因此在理论上需要的开销比完整虚拟化要少。这样的容器只包含应用程序代码和其系统级依赖项,但从内部运行的进程的角度来看,它看起来像一个完全隔离的系统环境。
软件容器主要得益于 Docker 而变得流行;这是其中一种可用的实现。Docker 允许以称为Dockerfile的简单文本文档描述其容器。根据这些定义,可以构建和存储容器。它还支持增量更改,因此如果容器中添加了新内容,则不需要从头开始重新创建。
不同的工具,如 Docker 和 Vagrant,似乎在功能上有重叠,但它们之间的主要区别在于这些工具被构建的原因。如前所述,Vagrant 主要是作为开发工具构建的。它允许用单个命令引导整个虚拟机,但不允许简单地打包并部署或发布。另一方面,Docker 则是专门为此而构建的——准备完整的容器,可以作为一个整体包发送和部署到生产环境。如果实施得当,这可以极大地改善产品部署的过程。因此,在开发过程中使用 Docker 和类似的解决方案(例如 Rocket)只有在它也必须在生产环境中的部署过程中使用时才有意义。仅在开发过程中用于隔离目的可能会产生太多的开销,而且还有一个不一致的缺点。
流行的生产力工具
生产力工具是一个有点模糊的术语。一方面,几乎每个发布并在网上可用的开源代码包都是一种提高生产力的工具——它提供了现成的解决方案,使得没有人需要花时间去解决它(理想情况下)。另一方面,有人可能会说整个 Python 都是关于生产力的。这两者无疑都是真的。这种语言和围绕它的社区几乎所有的东西似乎都是为了使软件开发尽可能地高效。
这创造了一个积极的反馈循环。由于编写代码是有趣且容易的,许多程序员会利用业余时间创建使其更容易和有趣的工具。这个事实将被用作生产力工具的一个非常主观和非科学的定义的基础——一种使开发更容易和更有趣的软件。
自然而然,生产力工具主要关注开发过程的某些元素,如测试、调试和管理软件包,并不是它们帮助构建的产品的核心部分。在某些情况下,它们甚至可能根本没有在项目的代码库中被提及,尽管它们每天都在使用。
最重要的生产力工具pip和venv在本章的前面已经讨论过。其中一些工具有针对特定问题的软件包,如性能分析和测试,并在本书中有它们自己的章节。本节专门介绍了其他一些值得一提的工具,但在本书中没有专门的章节可以介绍它们。
自定义 Python shell - IPython,bpython,ptpython 等。
Python 程序员在交互式解释器会话中花费了大量时间。它非常适合测试小的代码片段,访问文档,甚至在运行时调试代码。默认的交互式 Python 会话非常简单,不提供诸如制表符补全或代码内省助手之类的许多功能。幸运的是,默认的 Python shell 可以很容易地扩展和自定义。
交互提示可以通过启动文件进行配置。启动时,它会查找PYTHONSTARTUP环境变量,并执行由该变量指向的文件中的代码。一些 Linux 发行版提供了一个默认的启动脚本,通常位于您的主目录中。它被称为.pythonstartup。制表符补全和命令历史记录通常会提供以增强提示,并且基于readline模块。(您需要readline库。)
如果您没有这样的文件,可以轻松创建一个。以下是一个添加了使用<Tab>键和历史记录的最简单启动文件的示例:
# python startup file
import readline
import rlcompleter
import atexit
import os
# tab completion
readline.parse_and_bind('tab: complete')
# history file
histfile = os.path.join(os.environ['HOME'], '.pythonhistory')
try:
readline.read_history_file(histfile)
except IOError:
pass
atexit.register(readline.write_history_file, histfile)
del os, histfile, readline, rlcompleter
在您的主目录中创建此文件,并将其命名为.pythonstartup。然后,在环境中添加一个PYTHONSTARTUP变量,使用您文件的路径:
设置PYTHONSTARTUP环境变量
如果您正在运行 Linux 或 Mac OS X,最简单的方法是在您的主文件夹中创建启动脚本。然后,将其链接到设置为系统 shell 启动脚本的PYTHONSTARTUP环境变量。例如,Bash 和 Korn shells 使用.profile文件,您可以插入一行如下:
export PYTHONSTARTUP=~/.pythonstartup
如果您正在运行 Windows,可以在系统首选项中以管理员身份设置新的环境变量,并将脚本保存在一个常用位置,而不是使用特定的用户位置。
编写PYTHONSTARTUP脚本可能是一个很好的练习,但独自创建一个良好的自定义 shell 是一项只有少数人能够抽出时间来完成的挑战。幸运的是,有一些自定义 Python shell 实现极大地改善了 Python 交互式会话的体验。
IPython
IPyhton(ipython.scipy.org)提供了一个扩展的 Python 命令行。提供的功能中,最有趣的是:
-
动态对象内省
-
从提示中访问系统 shell
-
直接支持分析
-
调试设施
现在,IPython 是一个名为 Jupyter 的更大项目的一部分,它提供了可以用许多不同语言编写的具有实时代码的交互式笔记本。
bpython
bpython(bpython-interpreter.org/)将自己宣传为 Python 解释器的时髦界面。以下是该项目页面上强调的一些内容:
-
内联语法高亮
-
类似 Readline 的自动完成,建议在您输入时显示
-
任何 Python 函数的预期参数列表
-
自动缩进
-
Python 3 支持
ptpython
ptpython(github.com/jonathanslenders/ptpython/)是另一种高级 Python shell 主题的方法。在这个项目中,核心提示工具的实现可作为一个名为prompt_toolkit的单独包使用(来自同一作者)。这使您可以轻松创建各种美观的交互式命令行界面。
它经常与 bpython 在功能上进行比较,但主要区别在于它启用了与 IPython 和其语法的兼容模式,从而启用了额外的功能,如%pdb,%cpaste或%profile。
交互式调试器
代码调试是软件开发过程的一个重要组成部分。许多程序员可能会花费大部分时间仅使用广泛的日志记录和print语句作为他们的主要调试工具,但大多数专业开发人员更喜欢依赖某种调试器。
Python 已经内置了一个名为pdb的交互式调试器(参见docs.python.org/3/library/pdb.html)。它可以从命令行上调用现有的脚本,因此如果程序异常退出,Python 将进入事后调试:
python -m pdb script.py
事后调试虽然有用,但并不涵盖每种情况。它仅在应用程序以某种异常退出时有用。在许多情况下,有错误的代码只是表现异常,但并不会意外退出。在这种情况下,可以使用这个单行习惯用法在特定代码行上设置自定义断点:
import pdb; pdb.set_trace()
这将导致 Python 解释器在运行时在此行开始调试会话。
pdb对于追踪问题非常有用,乍一看,它可能看起来非常熟悉,就像著名的 GDB(GNU 调试器)。由于 Python 是一种动态语言,pdb会话非常类似于普通的解释器会话。这意味着开发人员不仅限于追踪代码执行,还可以调用任何代码,甚至执行模块导入。
遗憾的是,由于其根源(bdb),对pdb的第一次体验可能会有点压倒性,因为存在着诸如h、b、s、n、j和r等神秘的短字母调试器命令。每当有疑问时,在调试器会话期间键入help pdb命令将提供广泛的用法和额外信息。
pdb 中的调试器会话也非常简单,不提供诸如制表符补全或代码高亮之类的附加功能。幸运的是,PyPI 上有一些包可提供这些功能,这些功能可以从前一节提到的替代 Python shell 中使用。最值得注意的例子有:
-
ipdb:这是一个基于ipython的独立包 -
ptpdb:这是一个基于ptpython的独立包 -
bpdb:这是与bpython捆绑在一起的
有用的资源
网络上充满了对 Python 开发人员有用的资源。最重要和明显的资源已经在前面提到过,但为了保持这个列表的一致性,这里重复一遍:
-
Python 文档
-
PyPI—Python 包索引
-
PEP 0—Python 增强提案索引
其他资源,如书籍和教程,虽然有用,但往往很快就会过时。不会过时的是由社区积极策划或定期发布的资源。其中最值得推荐的两个是:
-
Awesome-python (
github.com/vinta/awesome-python),其中包括一个经过策划的流行包和框架的列表 -
Python Weekly (
www.pythonweekly.com/)是一个流行的新闻通讯,每周向订阅者提供数十个新的有趣的 Python 包和资源
这两个资源将为读者提供数月的额外阅读。
总结
本章从 Python 2 和 3 之间的差异开始,提出了如何处理当前情况的建议,其中大部分社区都在两个世界之间挣扎。然后,它涉及到了由于语言的两个主要版本之间的不幸分裂而出现的 Python 开发的现代方法。这些主要是环境隔离问题的不同解决方案。本章以对流行的生产工具和进一步参考的流行资源的简短总结结束。
第二章:语法最佳实践-类级别以下
编写高效的语法能力随着时间自然而然地产生。如果你回顾一下你的第一个程序,你可能会同意这一点。正确的语法会让你眼前一亮,而错误的语法会让人感到不安。
除了实现的算法和程序的架构设计,对程序的编写方式进行精心设计对其未来的发展至关重要。许多程序因其晦涩的语法、不清晰的 API 或非常规的标准而被抛弃并从头开始重写。
但是 Python 在过去几年里发生了很大的变化。因此,如果你被邻居(当地 Ruby 开发者用户组的一个嫉妒的家伙)绑架了一段时间并远离了新闻,你可能会对它的新功能感到惊讶。从最早的版本到当前版本(此时为 3.5),已经进行了许多增强,使语言更加清晰、干净和易于编写。Python 的基础并没有发生根本性的变化,但现在玩耍它们的工具更加人性化。
本章介绍了现代语法的最重要元素以及有关它们使用的提示:
-
列表推导式
-
迭代器和生成器
-
描述符和属性
-
装饰器
-
with和contextlib
有关代码性能提升或内存使用的速度改进的提示在第十一章 优化-一般原则和分析技术和第十二章 优化-一些强大的技术中有所涉及。
Python 的内置类型
Python 提供了一组很棒的数据类型。这对于数字类型和集合类型都是如此。关于数字类型,它们的语法没有什么特别之处。当然,对于每种类型的字面量定义和一些(也许)不太为人所知的运算符细节有一些差异,但对于开发人员来说,选择余地并不多。当涉及到集合和字符串时情况就不同了。尽管"应该只有一种方法来做某事"的信条,但 Python 开发人员确实有很多选择。对于初学者来说,一些看起来直观简单的代码模式经常被有经验的程序员认为是非Pythonic的,因为它们要么效率低下,要么太啰嗦。
解决常见问题的Pythonic模式(许多程序员称之为习语)通常看起来只是美学。这是完全错误的。大多数习语是由 Python 内部实现以及内置结构和模块的工作方式驱动的。了解更多这样的细节对于对语言的深入理解至关重要。此外,社区本身也不乏关于 Python 工作原理的神话和刻板印象。只有通过自己深入挖掘,你才能判断关于 Python 的流行说法中哪些是真的。
字符串和字节
对于只习惯于在 Python 2 中编程的程序员来说,字符串可能会带来一些困惑。在 Python 3 中,只有一种数据类型能够存储文本信息。它是str或者简单地说,字符串。它是一个存储 Unicode 码点的不可变序列。这是与 Python 2 的主要区别,其中str表示字节字符串-现在由bytes对象处理(但处理方式并不完全相同)。
Python 中的字符串是序列。这一事实应该足以将它们包括在涵盖其他容器类型的部分中,但它们与其他容器类型在一个重要的细节上有所不同。字符串对它们可以存储的数据类型有非常具体的限制,那就是 Unicode 文本。
字节及其可变替代品(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。因此,用单引号(')、双引号(")或三引号(单引号或双引号)括起来的字面量代表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 字符串包含与字节表示独立的“抽象”文本。这使它们无法在磁盘上保存或在网络上传输而不进行编码为二进制数据。有两种方法可以将字符串对象编码为字节序列:
-
使用
str.encode(encoding, errors)方法,使用注册的编解码器对字符串进行编码。编解码器使用encoding参数指定,默认为'utf-8'。第二个 errors 参数指定错误处理方案。它可以是'strict'(默认值)、'ignore'、'replace'、'xmlcharrefreplace'或任何其他已注册的处理程序(参考内置codecs模块文档)。 -
使用
bytes(source, encoding, errors)构造函数创建一个新的字节序列。当源是str类型时,encoding参数是必需的,且没有默认值。encoding和errors参数的使用与str.encode()方法相同。
由bytes表示的二进制数据可以以类似的方式转换为字符串:
-
使用
bytes.decode(encoding, errors)方法,使用为编码注册的编解码器对字节进行解码。此方法的参数与str.encode()的参数具有相同的含义和默认值。 -
使用
str(source, encoding, error)构造函数创建一个新的字符串实例。与bytes()构造函数类似,str()调用中的encoding参数没有默认值,如果字节序列用作源,则必须提供。
提示
命名——字节与字节字符串
由于 Python 3 中的更改,一些人倾向于将bytes实例称为字节字符串。这主要是由于历史原因——Python 3 中的bytes是与 Python 2 中的str类型最接近的序列类型(但不完全相同)。但是,bytes实例是字节序列,也不需要表示文本数据。因此,为了避免混淆,最好总是将它们称为bytes或字节序列,尽管它们与字符串相似。在 Python 3 中,字符串的概念是保留给文本数据的,现在始终是str。
实现细节
Python 字符串是不可变的。这也适用于字节序列。这是一个重要的事实,因为它既有优点又有缺点。它还影响了在 Python 中高效处理字符串的方式。由于不可变性,字符串可以用作字典键或set集合元素,因为一旦初始化,它们将永远不会改变其值。另一方面,每当需要修改字符串(即使只有微小的修改)时,都需要创建一个全新的实例。幸运的是,bytearray作为bytes的可变版本不会引入这样的问题。字节数组可以通过项目赋值进行就地修改(无需创建新对象),并且可以像列表一样动态调整大小 - 使用附加、弹出、插入等。
字符串连接
知道 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 中的常量折叠,一些复杂的文字(不仅仅是字符串),例如
'a' + 'b' + 'c'到'abc'可以在编译时转换为更短的形式。当然,这仅对相对较短的常量(文字)启用。
最终,如果字符串连接的数量事先已知,最佳的可读性是通过适当的字符串格式化来确保的,可以使用str.format()方法或%运算符。在性能不是关键或优化字符串连接的收益非常小的代码部分,建议使用字符串格式化作为最佳选择。
提示
常量折叠和窥孔优化器
CPython 在编译源代码上使用窥孔优化器以提高性能。该优化器直接在 Python 的字节码上实现了许多常见的优化。如前所述,常量折叠就是其中之一。生成的常量受硬编码值的长度限制。在 Python 3.5 中,它仍然不变,等于 20。无论如何,这个特定的细节更像是一个好奇心,而不是日常编程中可以依赖的东西。有关窥孔优化器执行的其他有趣优化的信息可以在 Python 源代码的Python/peephole.c文件中找到。
集合
Python 提供了一系列内置的数据集合,如果选择得当,可以有效地解决许多问题。您可能已经知道的类型是那些具有专用文字的类型:
-
列表
-
元组
-
字典
-
集合
当然,Python 不仅限于这四种选择,并通过其标准库扩展了可能的选择列表。在许多情况下,解决问题的解决方案可能就像为数据结构做出良好选择一样简单。本书的这一部分旨在通过深入了解可能的选择来简化这样的决定。
列表和元组
Python 中最基本的两种集合类型是列表和元组,它们都表示对象的序列。它们之间的基本区别对于任何花费了几个小时以上的 Python 用户来说应该是显而易见的—列表是动态的,因此可以改变其大小,而元组是不可变的(它们在创建后无法修改)。
尽管元组具有许多各种优化,使得小对象的分配/释放变得快速,但它们是存储元素位置本身信息的推荐数据类型。例如,元组可能是存储一对(x, y)坐标的良好选择。无论如何,关于元组的细节都相当无聊。在本章的范围内,它们唯一重要的事情是tuple是不可变的,因此可散列的。这意味着什么将在字典部分中介绍。比元组更有趣的是它的动态对应物list,它是如何工作的,以及如何有效地处理它。
实现细节
许多程序员很容易将 Python 的list类型与其他语言的标准库中经常找到的链表概念混淆,比如 C、C++或 Java。实际上,在 CPython 中,列表根本不是列表。在 CPython 中,列表是作为可变长度数组实现的。尽管这些实现细节通常在这些项目中没有记录,但这对于 Jython 和 IronPython 等其他实现也是正确的。造成这种混淆的原因很明显。这种数据类型被命名为list,并且还具有可以从任何链表实现中预期的接口。
这为什么重要,意味着什么?列表是最流行的数据结构之一,它们的使用方式极大地影响了每个应用程序的性能。此外,CPython 是最流行和使用最广泛的实现,因此了解其内部实现细节至关重要。
具体来说,Python 中的列表是对其他对象的连续数组的引用。指向此数组的指针和长度存储在列表头结构中。这意味着每次添加或删除项目时,都需要调整引用数组的大小(重新分配)。幸运的是,在 Python 中,这些数组是以指数过分分配创建的,因此不是每个操作都需要调整大小。这就是为什么在复杂度方面,附加和弹出元素的摊销成本可以很低。不幸的是,在 Python 中,一些在普通链表中被认为是“便宜”的其他操作具有相对较高的计算复杂度:
-
使用
list.insert方法在任意位置插入项目—复杂度为 O(n) -
使用
list.delete或使用del删除项目—复杂度为 O(n)
在这里,n是列表的长度。至少使用索引检索或设置元素是一个与列表大小无关的操作。以下是大多数列表操作的平均时间复杂度的完整表格:
| 操作 | 复杂度 |
|---|---|
| - --- | --- |
| - 复制 | O(n) |
| - 添加 | O(1) |
| - 插入 | O(n) |
| - 获取项目 | O(1) |
| - 删除项目 | O(n) |
| - 迭代 | O(n) |
| - 获取长度为k的切片 | O(k) |
| - 删除切片 | O(n) |
| - 设置长度为k的切片 | O(k+n) |
| - 扩展 | O(k) |
| - 乘以k | O(nk) |
- 测试存在性(element in list) | O(n) |
- min()/max() | O(n) |
| - 获取长度 | O(1) |
在需要真正的链表(或者简单地说,具有appends和pop的数据结构,复杂度为 O(1))的情况下,Python 在collections内置模块中提供了deque。这是栈和队列的一般化,应该在需要双向链表的任何地方都能正常工作。
列表推导
正如您可能知道的,编写这样的代码是痛苦的:
>>> evens = []
>>> for i in range(10):
... if i % 2 == 0:
... evens.append(i)
...**
>>> evens
[0, 2, 4, 6, 8]
这对于 C 语言可能有效,但实际上对于 Python 来说会使事情变慢,因为:
-
它使解释器在每次循环中工作,以确定序列的哪一部分必须被更改
-
它使您保持一个计数器来跟踪哪个元素必须被处理
-
它需要在每次迭代时执行额外的函数查找,因为
append()是列表的方法
列表推导是这种模式的正确答案。它使用了自动化前一种语法的部分的奇怪特性:
>>> [i for i in range(10) if i % 2 == 0]
[0, 2, 4, 6, 8]
除了这种写法更有效外,它更短,涉及的元素更少。在更大的程序中,这意味着更少的错误和更容易阅读和理解的代码。
提示
列表推导和内部数组调整
有一种迷思在一些 Python 程序员中流传,即列表推导可以是内部数组代表列表对象必须在每次添加时调整大小的一种变通方法。有人说数组将在恰到好处的大小时只分配一次。不幸的是,这是不正确的。
在计算推导时,解释器无法知道最终容器的大小,也无法为其预分配数组的最终大小。因此,内部数组的重新分配与for循环中的模式相同。然而,在许多情况下,使用推导创建列表既更清晰又更快,而不是使用普通循环。
其他习惯用法
Python 习惯用法的另一个典型例子是使用enumerate。这个内置函数提供了一种方便的方法,在循环中使用序列时获得索引。考虑以下代码片段作为例子:
>>> i = 0
>>> for element in ['one', 'two', 'three']:
... print(i, element)
... i += 1
...
0 one
1 two
2 three
这可以被以下更短的代码替换:
>>> for i, element in enumerate(['one', 'two', 'three']):
... print(i, element)
...
0 one
1 two
2 three
当需要将多个列表(或任何可迭代对象)的元素以一对一的方式聚合时,可以使用内置的zip()函数。这是对两个相同大小的可迭代对象进行统一迭代的非常常见的模式:
>>> for item in zip([1, 2, 3], [4, 5, 6]):
... print(item)
...**
(1, 4)
(2, 5)
(3, 6)
请注意,zip()的结果可以通过另一个zip()调用进行反转:
>>> for item in zip(*zip([1, 2, 3], [4, 5, 6])):
... print(item)
...**
(1, 2, 3)
(4, 5, 6)
另一个流行的语法元素是序列解包。它不仅限于列表和元组,而且适用于任何序列类型(甚至字符串和字节序列)。它允许您将一系列元素解包到另一组变量中,只要在赋值运算符的左侧有与序列中元素数量相同的变量:
>>> first, second, third = "foo", "bar", 100
>>> first
'foo'
>>> second
'bar'
>>> third
100
解包还允许您使用星号表达式捕获单个变量中的多个元素,只要它可以被明确解释。解包也可以在嵌套序列上执行。当在由序列构建的一些复杂数据结构上进行迭代时,这可能会很方便。以下是一些更复杂解包的示例:
>>> # starred expression to capture rest of the sequence
>>> first, second, *rest = 0, 1, 2, 3
>>> first
0
>>> second
1
>>> rest
[2, 3]
>>> # starred expression to capture middle of the sequence
>>> first, *inner, last = 0, 1, 2, 3
>>> first
0
>>> inner
[1, 2]
>>> last
3
>>> # nested unpacking
>>> (a, b), (c, d) = (1, 2), (3, 4)
>>> a, b, c, d
(1, 2, 3, 4)
字典
字典是 Python 中最通用的数据结构之一。dict允许将一组唯一的键映射到值,如下所示:
{
1: ' one',
2: ' two',
3: ' three',
}
字典文字是一件非常基本的事情,你应该已经知道它们。无论如何,Python 允许程序员使用类似于前面提到的列表推导的推导来创建一个新的字典。这是一个非常简单的例子:
squares = {number: number**2 for number in range(100)}
重要的是,使用列表推导的相同好处也适用于字典推导。因此,在许多情况下,它们更有效、更短、更清晰。对于更复杂的代码,当需要许多if语句或函数调用来创建字典时,简单的for循环可能是更好的选择,特别是如果它提高了可读性。
对于 Python 3 中的 Python 程序员,有一个关于迭代字典元素的重要说明。字典方法:keys()、values()和items()不再具有列表作为它们的返回值类型。此外,它们的对应方法iterkeys()、itervalues()和iteritems()在 Python 3 中已经消失,而不是返回迭代器。现在,keys()、values()和items()返回的是视图对象:
-
keys(): 这返回dict_keys对象,提供了字典的所有键的视图 -
values(): 这返回dict_values对象,提供了字典的所有值的视图 -
items(): 这返回dict_items对象,提供了字典的所有(key, value)两个元组的视图
视图对象以动态方式查看字典内容,因此每次字典发生更改,视图都会反映这些更改,如下例所示:
>>> words = {'foo': 'bar', 'fizz': 'bazz'}
>>> items = words.items()
>>> words['spam'] = 'eggs'
>>> items
dict_items([('spam', 'eggs'), ('fizz', 'bazz'), ('foo', 'bar')])
视图对象将旧方法的返回列表的行为与它们的“iter”对应方法返回的迭代器相结合。视图不需要在内存中冗余存储所有值(像列表一样),但仍然允许获取它们的长度(使用len)和测试成员资格(使用in子句)。视图当然是可迭代的。
最后一个重要的事情是keys()和values()方法返回的两个视图确保了相同的键和值顺序。在 Python 2 中,如果要确保检索到的键和值的顺序相同,你不能在这两个调用之间修改字典内容。dict_keys和dict_values现在是动态的,因此即使在keys()和values()调用之间更改字典的内容,迭代的顺序也在这两个视图之间保持一致。
实现细节
CPython 使用伪随机探测的哈希表作为字典的底层数据结构。这似乎是一个非常深入的实现细节,但在不久的将来很不可能改变,因此对于程序员来说也是一个非常有趣的事实。
由于这个实现细节,只有可哈希的对象才能用作字典键。如果一个对象是可哈希的,那么它在其生命周期内的哈希值永远不会改变,并且可以与不同的对象进行比较。每个 Python 的内置类型都是不可变的,因此也是可哈希的。可变类型,如列表、字典和集合,是不可哈希的,因此不能用作字典键。定义类型是否可哈希的协议由两个方法组成:
-
__hash__: 这提供了内部dict实现所需的哈希值(作为整数)。对于用户定义类的实例对象,它是从它们的id()派生的。 -
__eq__: 这比较具有相同值的两个对象。默认情况下,所有用户定义类的实例对象都不相等,除了它们自己。
相等的两个对象必须具有相同的哈希值。反之则不需要成立。这意味着哈希碰撞是可能的——具有相同哈希的两个对象可能不相等。这是允许的,每个 Python 实现都必须能够解决哈希碰撞。CPython 使用开放寻址来解决这种碰撞(en.wikipedia.org/wiki/Open_addressing)。然而,碰撞的概率极大地影响性能,如果碰撞概率很高,字典将无法从其内部优化中受益。
虽然三个基本操作:添加、获取和删除项目的平均时间复杂度为 O(1),但它们的摊销最坏情况复杂度要高得多——O(n),其中n是当前字典大小。此外,如果将用户定义的类对象用作字典键,并且它们的哈希不当(存在高风险的碰撞),那么这将对字典的性能产生巨大的负面影响。CPython 字典的完整时间复杂度表如下:
| 操作 | 平均复杂度 | 分摊最坏情况复杂度 |
|---|---|---|
| 获取项 | O(1) | O(n) |
| 集合项 | O(1) | O(n) |
| 删除项 | O(1) | O(n) |
| 复制 | O(n) | O(n) |
| 迭代 | O(n) | O(n) |
还有一点很重要,那就是复制和迭代字典的最坏情况复杂度中的n是字典曾经达到的最大大小,而不是当前的项数。换句话说,迭代曾经很大但在时间上大大缩小的字典可能需要花费出乎意料的长时间。因此,在某些情况下,如果需要经常迭代,可能最好创建一个新的字典对象,而不是仅仅从以前的字典中删除元素。
弱点和替代方案
使用字典的一个常见陷阱是它们不保留添加新键的顺序。在某些情况下,当字典键使用连续的键,其哈希值也是连续的值(例如使用整数)时,由于字典的内部实现,结果顺序可能是相同的:
>>> {number: None for number in range(5)}.keys()
dict_keys([0, 1, 2, 3, 4])
然而,使用其他哈希方式不同的数据类型表明顺序不会被保留。以下是 CPython 的一个例子:
>>> {str(number): None for number in range(5)}.keys()
dict_keys(['1', '2', '4', '0', '3'])
>>> {str(number): None for number in reversed(range(5))}.keys()
dict_keys(['2', '3', '1', '4', '0'])
如前面的代码所示,结果顺序既取决于对象的哈希,也取决于添加元素的顺序。这是不可靠的,因为它可能会随着不同的 Python 实现而变化。
然而,在某些情况下,开发人员可能需要保留添加顺序的字典。幸运的是,Python 标准库在collections模块中提供了一个有序字典OrderedDict。它可以选择接受一个可迭代对象作为初始化参数:
>>> from collections import OrderedDict
>>> OrderedDict((str(number), None) for number in range(5)).keys()
odict_keys(['0', '1', '2', '3', '4'])
它还具有一些额外的功能,比如使用popitem()方法从两端弹出项,或者使用move_to_end()方法将指定的元素移动到其中一个端点。有关该集合的完整参考,请参阅 Python 文档(参见docs.python.org/3/library/collections.html)。
另一个重要的注意事项是,在非常古老的代码库中,dict可能被用作保证元素唯一性的原始集合实现。虽然这会给出正确的结果,但除非针对的是低于 2.3 的 Python 版本,否则应该避免这样做。以这种方式使用字典在资源方面是浪费的。Python 有一个内置的set类型来实现这个目的。实际上,它在 CPython 中有一个非常相似的内部实现,但也提供了一些额外的功能以及特定的与集合相关的优化。
集合
集合是一种非常健壮的数据结构,主要在元素的顺序不如它们的唯一性和测试效率重要的情况下非常有用。它们与类似的数学概念非常相似。集合以两种形式作为内置类型提供:
-
set(): 这是一个可变的、无序的、有限的唯一不可变(可哈希)对象的集合 -
frozenset(): 这是一个不可变的、可哈希的、无序的唯一不可变(可哈希)对象的集合
frozenset()的不可变性使其可以用作字典键,也可以用作其他set()和frozenset()元素。普通的可变set()不能在另一个集合或 frozenset 内容中使用,否则会引发TypeError:
>>> set([set([1,2,3]), set([2,3,4])])
Traceback (most recent call last):
**File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'set'
以下的集合初始化是完全正确的:
>>> set([frozenset([1,2,3]), frozenset([2,3,4])])
{frozenset({1, 2, 3}), frozenset({2, 3, 4})}
>>> frozenset([frozenset([1,2,3]), frozenset([2,3,4])])
frozenset({frozenset({1, 2, 3}), frozenset({2, 3, 4})})
可变集合可以通过三种方式创建:
-
使用接受可选可迭代对象作为初始化参数的
set()调用,比如set([0, 1, 2]) -
使用集合推导,例如
{element for element in range(3)} -
使用集合字面量,例如
{1, 2, 3}
请注意,对于集合,使用文字和理解需要额外小心,因为它们在形式上与字典文字和理解非常相似。此外,空集对象没有文字 - 空花括号{}保留用于空字典文字。
实现细节
在 CPython 中,集合与字典非常相似。事实上,它们是使用虚拟值实现的字典,其中只有键是实际的集合元素。此外,集合利用映射中缺少值的优化。
由于这一点,集合允许非常快速的添加、删除和检查元素是否存在,平均时间复杂度为 O(1)。然而,由于 CPython 中集合的实现依赖于类似的哈希表结构,这些操作的最坏情况复杂度为 O(n),其中n是集合的当前大小。
其他实现细节也适用。要包含在集合中的项目必须是可散列的,如果用户定义类的实例在集合中的哈希值很差,这将对性能产生负面影响。
基本集合之外 - collections 模块
每种数据结构都有其缺点。没有单一的集合可以适用于每个问题,而且四种基本类型(元组、列表、集合和字典)仍然不是一种广泛的选择。这些是最基本和重要的集合,具有专用的文字语法。幸运的是,Python 在其标准库中提供了更多选项,通过collections内置模块。其中一个已经提到了(deque)。以下是此模块提供的最重要的集合:
-
namedtuple():这是一个用于创建元组子类的工厂函数,其索引可以作为命名属性访问 -
deque:这是一个双端队列,类似于堆栈和队列的列表泛化,可以在两端快速添加和弹出 -
ChainMap:这是一个类似字典的类,用于创建多个映射的单个视图 -
Counter:这是一个用于计算可散列对象的字典子类 -
OrderedDict:这是一个保留条目添加顺序的字典子类 -
defaultdict:这是一个字典子类,可以使用提供的默认值提供缺失的值
注意
有关来自 collections 模块的选定集合的更多详细信息以及在何处值得使用它们的建议,请参见第十二章,“优化 - 一些强大的技术”。
高级语法
客观地说,很难判断语言语法的哪个元素是先进的。对于本章关于高级语法元素的目的,我们将考虑那些与任何特定的内置数据类型没有直接关系,并且在开始时相对难以理解的元素。可能难以理解的最常见的 Python 特性是:
-
迭代器
-
生成器
-
装饰器
-
上下文管理器
迭代器
迭代器只不过是实现迭代器协议的容器对象。它基于两种方法:
-
__next__:这返回容器的下一个项目 -
__iter__:这返回迭代器本身
可以使用iter内置函数从序列创建迭代器。考虑以下示例:
>>> i = iter('abc')
>>> next(i)
'a'
>>> next(i)
'b'
>>> next(i)
'c'
>>> next(i)
Traceback (most recent call last):
**File "<input>", line 1, in <module>
StopIteration
当序列耗尽时,会引发StopIteration异常。它使迭代器与循环兼容,因为它们捕获此异常以停止循环。要创建自定义迭代器,可以编写一个具有__next__方法的类,只要它提供返回迭代器实例的特殊方法__iter__:
class CountDown:def __init__(self, step):
self.step = step
def __next__(self):
"""Return the next element."""
if self.step <= 0:
raise StopIteration
self.step -= 1
return self.step
def __iter__(self):
"""Return the iterator itself."""
return self
以下是这种迭代器的示例用法:
>>> for element in CountDown(4):
... print(element)
...**
3
2
1
0
迭代器本身是一个低级特性和概念,程序可以没有它们。但是它们为一个更有趣的特性 - 生成器提供了基础。
yield 语句
生成器提供了一种优雅的方式来编写返回元素序列的简单高效的代码。基于yield语句,它们允许您暂停函数并返回中间结果。函数保存其执行上下文,如果必要的话可以稍后恢复。
例如,斐波那契数列可以用迭代器编写(这是关于迭代器的 PEP 中提供的示例):
def fibonacci():
a, b = 0, 1
while True:
yield b
a, b = b, a + b
您可以像使用next()函数或for循环一样从生成器中检索新值:
>>> fib = fibonacci()
>>> next(fib)
1
>>> next(fib)
1
>>> next(fib)
2
>>> [next(fib) for i in range(10)]
[3, 5, 8, 13, 21, 34, 55, 89, 144, 233]
这个函数返回一个generator对象,一个特殊的迭代器,它知道如何保存执行上下文。它可以被无限调用,每次产生套件的下一个元素。语法简洁,算法的无限性不再影响代码的可读性。它不必提供一种使函数可停止的方法。事实上,它看起来类似于伪代码中设计系列的方式。
在社区中,生成器并不经常使用,因为开发人员不习惯以这种方式思考。开发人员多年来一直习惯于使用直接函数。每当处理返回序列的函数或在循环中工作时,都应该考虑使用生成器。逐个返回元素可以提高整体性能,当它们被传递给另一个函数进行进一步处理时。
在这种情况下,用于计算一个元素的资源大部分时间不那么重要,而用于整个过程的资源更为重要。因此,它们可以保持较低,使程序更加高效。例如,斐波那契数列是无限的,但生成它的生成器不需要无限的内存来一次提供值。一个常见的用例是使用生成器流式传输数据缓冲区。它们可以被第三方代码暂停、恢复和停止,而不需要在开始处理之前加载所有数据。
例如,标准库中的tokenize模块可以从文本流中生成标记,并为每个处理的行返回一个iterator,可以传递给某些处理:
>>> import tokenize
>>> reader = open('hello.py').readline
>>> tokens = tokenize.generate_tokens(reader)
>>> next(tokens)
TokenInfo(type=57 (COMMENT), string='# -*- coding: utf-8 -*-', start=(1, 0), end=(1, 23), line='# -*- coding: utf-8 -*-\n')
>>> next(tokens)
TokenInfo(type=58 (NL), string='\n', start=(1, 23), end=(1, 24), line='# -*- coding: utf-8 -*-\n')
>>> next(tokens)
TokenInfo(type=1 (NAME), string='def', start=(2, 0), end=(2, 3), line='def hello_world():\n')
在这里,我们可以看到open迭代文件的行,generate_tokens在管道中迭代它们,执行额外的工作。生成器还可以帮助打破复杂性,并提高基于几个套件的一些数据转换算法的效率。将每个套件视为iterator,然后将它们组合成一个高级函数是避免一个庞大、丑陋和难以阅读的函数的好方法。此外,这可以为整个处理链提供实时反馈。
在下面的例子中,每个函数定义了对序列的转换。然后它们被链接并应用。每个函数调用处理一个元素并返回其结果:
def power(values):
for value in values:
print('powering %s' % value)
yield value
def adder(values):
for value in values:
print('adding to %s' % value)
if value % 2 == 0:
yield value + 3
else:
yield value + 2
以下是使用这些生成器的可能结果:
>>> elements = [1, 4, 7, 9, 12, 19]
>>> results = adder(power(elements))
>>> next(results)
powering 1
adding to 1
3
>>> next(results)
powering 4
adding to 4
7
>>> next(results)
powering 7
adding to 7
9
提示
保持代码简单,而不是数据
最好有很多简单的可迭代函数,可以处理值序列,而不是一次计算整个集合的复杂函数。
关于generators,Python 中另一个重要的功能是能够使用next函数与代码进行交互。yield变成了一个表达式,可以通过一个称为send的新方法传递一个值:
def psychologist():
print('Please tell me your problems')
while True:
answer = (yield)
if answer is not None:
if answer.endswith('?'):
print("Don't ask yourself too much questions")
elif 'good' in answer:
print("Ahh that's good, go on")
elif 'bad' in answer:
print("Don't be so negative")
以下是使用我们的psychologist()函数的示例会话:
>>> free = psychologist()
>>> next(free)
Please tell me your problems
>>> free.send('I feel bad')
Don't be so negative
>>> free.send("Why I shouldn't ?")
Don't ask yourself too much questions
>>> free.send("ok then i should find what is good for me")
Ahh that's good, go on
send的作用类似于next,但使yield返回函数定义内传递的值。因此,函数可以根据客户端代码改变其行为。为了完成这种行为,还添加了另外两个函数——throw和close。它们将错误引发到生成器中:
-
throw:这允许客户端代码发送任何类型的异常来引发。 -
close:这样做的方式相同,但会引发特定的异常GeneratorExit。在这种情况下,生成器函数必须再次引发GeneratorExit或StopIteration。
注意
生成器是 Python 中其他概念的基础——协程和异步并发,这些概念在第十三章中有所涵盖,并发。
装饰器
Python 中添加装饰器是为了使函数和方法包装(接收一个函数并返回一个增强的函数)更易于阅读和理解。最初的用例是能够在其定义的头部将方法定义为类方法或静态方法。没有装饰器语法,这将需要一个相当稀疏和重复的定义:
class WithoutDecorators:
def some_static_method():
print("this is static method")
some_static_method = staticmethod(some_static_method)
def some_class_method(cls):
print("this is class method")
some_class_method = classmethod(some_class_method)
如果装饰器语法用于相同的目的,代码会更短,更容易理解:
class WithDecorators:
@staticmethod
def some_static_method():
print("this is static method")
@classmethod
def some_class_method(cls):
print("this is class method")
一般语法和可能的实现
装饰器通常是一个命名对象(不允许lambda表达式),在调用时接受一个参数(它将是装饰的函数),并返回另一个可调用对象。这里使用“可调用”而不是“函数”是有预谋的。虽然装饰器经常在方法和函数的范围内讨论,但它们并不局限于它们。事实上,任何可调用的东西(任何实现__call__方法的对象都被认为是可调用的)都可以用作装饰器,而且它们返回的对象通常不是简单的函数,而是更复杂的类的实例,实现了自己的__call__方法。
装饰器语法只是一种语法糖。考虑以下装饰器的用法:
@some_decorator
def decorated_function():
pass
这总是可以被显式的装饰器调用和函数重新分配替代:
def decorated_function():
pass
decorated_function = some_decorator(decorated_function)
然而,后者不太可读,而且如果在单个函数上使用多个装饰器,很难理解。
提示
装饰器甚至不需要返回一个可调用对象!
事实上,任何函数都可以用作装饰器,因为 Python 不强制装饰器的返回类型。因此,使用一些函数作为装饰器,它接受一个参数但不返回可调用的,比如str,在语法上是完全有效的。如果用户尝试以这种方式调用装饰过的对象,最终会失败。无论如何,装饰器语法的这一部分为一些有趣的实验创造了一个领域。
作为一个函数
有许多编写自定义装饰器的方法,但最简单的方法是编写一个返回包装原始函数调用的子函数的函数。
通用模式如下:
def mydecorator(function):
def wrapped(*args, **kwargs):
# do some stuff before the original
# function gets called
result = function(*args, **kwargs)
# do some stuff after function call and
# return the result
return result
# return wrapper as a decorated function
return wrapped
作为类
虽然装饰器几乎总是可以使用函数来实现,但在某些情况下,使用用户定义的类是更好的选择。当装饰器需要复杂的参数化或依赖于特定状态时,这通常是正确的。
作为类的非参数化装饰器的通用模式如下:
class DecoratorAsClass:
def __init__(self, function):
self.function = function
def __call__(self, *args, **kwargs):
# do some stuff before the original
# function gets called
result = self.function(*args, **kwargs)
# do some stuff after function call and
# return the result
return result
参数化装饰器
在实际代码中,通常需要使用可以带参数的装饰器。当函数用作装饰器时,解决方案很简单——必须使用第二层包装。这是装饰器的一个简单示例,它重复执行装饰函数指定的次数,每次调用时:
def repeat(number=3):
"""Cause decorated function to be repeated a number of times.
Last value of original function call is returned as a result
:param number: number of repetitions, 3 if not specified
"""
def actual_decorator(function):
def wrapper(*args, **kwargs):
result = None
for _ in range(number):
result = function(*args, **kwargs)
return result
return wrapper
return actual_decorator
这种方式定义的装饰器可以接受参数:
>>> @repeat(2)
... def foo():
... print("foo")
...**
>>> foo()
foo
foo
请注意,即使带有默认值的参数化装饰器,其名称后面的括号也是必需的。使用具有默认参数的前述装饰器的正确方法如下:
>>> @repeat()
... def bar():
... print("bar")
...**
>>> bar()
bar
bar
bar
如果省略这些括号,当调用装饰函数时将导致以下错误:
>>> @repeat
... def bar():
... pass
...**
>>> bar()
Traceback (most recent call last):
**File "<input>", line 1, in <module>
TypeError: actual_decorator() missing 1 required positional
argument: 'function'
保留内省的装饰器
使用装饰器的常见陷阱是在使用装饰器时不保留函数元数据(主要是文档字符串和原始名称)。所有先前的示例都有这个问题。它们通过组合创建了一个新函数,并返回了一个新对象,而没有尊重原始函数的身份。这使得以这种方式装饰的函数的调试更加困难,并且也会破坏大多数可能使用的自动文档工具,因为原始文档字符串和函数签名不再可访问。
但让我们详细看一下。假设我们有一些虚拟装饰器,除了装饰和一些其他函数被装饰以外,什么都不做:
def dummy_decorator(function):
def wrapped(*args, **kwargs):
"""Internal wrapped function documentation."""
return function(*args, **kwargs)
return wrapped
@dummy_decorator
def function_with_important_docstring():
"""This is important docstring we do not want to lose."""
如果我们在 Python 交互会话中检查function_with_important_docstring(),我们会注意到它已经失去了原始名称和文档字符串:
>>> function_with_important_docstring.__name__
'wrapped'
>>> function_with_important_docstring.__doc__
'Internal wrapped function documentation.'
解决这个问题的一个合适的方法是使用functools模块提供的内置wraps()装饰器:
from functools import wraps
def preserving_decorator(function):
@wraps(function)
def wrapped(*args, **kwargs):
"""Internal wrapped function documentation."""
return function(*args, **kwargs)
return wrapped
@preserving_decorator
def function_with_important_docstring():
"""This is important docstring we do not want to lose."""
通过这种方式定义的装饰器,重要的函数元数据得到了保留:
>>> function_with_important_docstring.__name__
'function_with_important_docstring.'
>>> function_with_important_docstring.__doc__
'This is important docstring we do not want to lose.'
用法和有用的示例
由于装饰器在模块首次读取时由解释器加载,它们的使用应该限于可以通用应用的包装器。如果装饰器与方法的类或增强的函数签名相关联,应将其重构为常规可调用对象以避免复杂性。无论如何,当装饰器处理 API 时,一个良好的做法是将它们分组在一个易于维护的模块中。
装饰器的常见模式有:
-
参数检查
-
缓存
-
代理
-
上下文提供者
参数检查
检查函数接收或返回的参数在特定上下文中执行时可能是有用的。例如,如果一个函数要通过 XML-RPC 调用,Python 将无法像静态类型语言那样直接提供其完整签名。当 XML-RPC 客户端请求函数签名时,需要此功能来提供内省能力。
提示
XML-RPC 协议
XML-RPC 协议是一种轻量级的远程过程调用协议,它使用 XML 通过 HTTP 来编码调用。它经常用于简单的客户端-服务器交换而不是 SOAP。与提供列出所有可调用函数的页面的 SOAP 不同,XML-RPC 没有可用函数的目录。提出了一种允许发现服务器 API 的协议扩展,并且 Python 的xmlrpc模块实现了它(参考docs.python.org/3/library/xmlrpc.server.html)。
自定义装饰器可以提供这种类型的签名。它还可以确保输入和输出符合定义的签名参数:
rpc_info = {}
def xmlrpc(in_=(), out=(type(None),)):
def _xmlrpc(function):
# registering the signature
func_name = function.__name__
rpc_info[func_name] = (in_, out)
def _check_types(elements, types):
"""Subfunction that checks the types."""
if len(elements) != len(types):
raise TypeError('argument count is wrong')
typed = enumerate(zip(elements, types))
for index, couple in typed:
arg, of_the_right_type = couple
if isinstance(arg, of_the_right_type):
continue
raise TypeError(
'arg #%d should be %s' % (index, of_the_right_type))
# wrapped function
def __xmlrpc(*args): # no keywords allowed
# checking what goes in
checkable_args = args[1:] # removing self
_check_types(checkable_args, in_)
# running the function
res = function(*args)
# checking what goes out
if not type(res) in (tuple, list):
checkable_res = (res,)
else:
checkable_res = res
_check_types(checkable_res, out)
# the function and the type
# checking succeeded
return res
return __xmlrpc
return _xmlrpc
装饰器将函数注册到全局字典中,并保留其参数和返回值的类型列表。请注意,示例被大大简化以演示参数检查装饰器。
使用示例如下:
class RPCView:
@xmlrpc((int, int)) # two int -> None
def meth1(self, int1, int2):
print('received %d and %d' % (int1, int2))
@xmlrpc((str,), (int,)) # string -> int
def meth2(self, phrase):
print('received %s' % phrase)
return 12
当它被读取时,这个类定义会填充rpc_infos字典,并且可以在特定环境中使用,其中检查参数类型:
>>> rpc_info
{'meth2': ((<class 'str'>,), (<class 'int'>,)), 'meth1': ((<class 'int'>, <class 'int'>), (<class 'NoneType'>,))}
>>> my = RPCView()
>>> my.meth1(1, 2)
received 1 and 2
>>> my.meth2(2)
Traceback (most recent call last):
**File "<input>", line 1, in <module>
**File "<input>", line 26, in __xmlrpc
**File "<input>", line 20, in _check_types
TypeError: arg #0 should be <class 'str'>
缓存
缓存装饰器与参数检查非常相似,但侧重于那些内部状态不影响输出的函数。每组参数都可以与唯一的结果相关联。这种编程风格是函数式编程的特征(参考en.wikipedia.org/wiki/Functional_programming),并且可以在输入值集合是有限的情况下使用。
因此,缓存装饰器可以将输出与计算所需的参数一起保留,并在后续调用时直接返回。这种行为称为记忆化(参考en.wikipedia.org/wiki/Memoizing),作为装饰器实现起来非常简单:
import time
import hashlib
import pickle
cache = {}
def is_obsolete(entry, duration):
return time.time() - entry['time']> duration
def compute_key(function, args, kw):
key = pickle.dumps((function.__name__, args, kw))
return hashlib.sha1(key).hexdigest()
def memoize(duration=10):
def _memoize(function):
def __memoize(*args, **kw):
key = compute_key(function, args, kw)
# do we have it already ?
if (key in cache and
not is_obsolete(cache[key], duration)):
print('we got a winner')
return cache[key]['value']
# computing
result = function(*args, **kw)
# storing the result
cache[key] = {
'value': result,
'time': time.time()
}
return result
return __memoize
return _memoize
使用有序参数值构建SHA哈希键,并将结果存储在全局字典中。哈希是使用 pickle 制作的,这是一个冻结传递的所有对象状态的快捷方式,确保所有参数都是良好的候选者。例如,如果线程或套接字被用作参数,将会发生PicklingError。(参见docs.python.org/3/library/pickle.html。)duration参数用于在上次函数调用后经过太长时间后使缓存值无效。
以下是一个使用示例:
>>> @memoize()
... def very_very_very_complex_stuff(a, b):
... # if your computer gets too hot on this calculation
... # consider stopping it
... return a + b
...
>>> very_very_very_complex_stuff(2, 2)
4
>>> very_very_very_complex_stuff(2, 2)
we got a winner
4
>>> @memoize(1) # invalidates the cache after 1 second
... def very_very_very_complex_stuff(a, b):
... return a + b
...
>>> very_very_very_complex_stuff(2, 2)
4
>>> very_very_very_complex_stuff(2, 2)
we got a winner
4
>>> cache
{'c2727f43c6e39b3694649ee0883234cf': {'value': 4, 'time':
1199734132.7102251)}
>>> time.sleep(2)
>>> very_very_very_complex_stuff(2, 2)
4
缓存昂贵的函数可以显著提高程序的整体性能,但必须小心使用。缓存的值也可以与函数本身绑定,以管理其范围和生命周期,而不是集中的字典。但无论如何,一个更有效的装饰器会使用基于高级缓存算法的专用缓存库。
注意
第十二章,优化-一些强大的技术,提供了关于缓存的详细信息和技术。
代理
代理装饰器用于标记和注册具有全局机制的函数。例如,一个保护代码访问的安全层,取决于当前用户,可以使用一个带有可调用的关联权限的集中检查器来实现。
class User(object):
def __init__(self, roles):
self.roles = roles
class Unauthorized(Exception):
pass
def protect(role):
def _protect(function):
def __protect(*args, **kw):
user = globals().get('user')
if user is None or role not in user.roles:
raise Unauthorized("I won't tell you")
return function(*args, **kw)
return __protect
return _protect
这个模型经常被用在 Python 的 web 框架中来定义可发布类的安全性。例如,Django 提供了装饰器来保护函数的访问。
这是一个例子,其中当前用户保存在全局变量中。装饰器在访问方法时检查他或她的角色:
>>> tarek = User(('admin', 'user'))
>>> bill = User(('user',))
>>> class MySecrets(object):
... @protect('admin')
... def waffle_recipe(self):
... print('use tons of butter!')
...
>>> these_are = MySecrets()
>>> user = tarek
>>> these_are.waffle_recipe()
use tons of butter!
>>> user = bill
>>> these_are.waffle_recipe()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in wrap
__main__.Unauthorized: I won't tell you
上下文提供程序
上下文装饰器确保函数可以在正确的上下文中运行,或在函数之前和之后运行一些代码。换句话说,它设置并取消特定的执行环境。例如,当一个数据项必须在多个线程之间共享时,必须使用锁来确保它受到多重访问的保护。这个锁可以编码在装饰器中,如下所示:
from threading import RLock
lock = RLock()
def synchronized(function):
def _synchronized(*args, **kw):
lock.acquire()
try:
return function(*args, **kw)
finally:
lock.release()
return _synchronized
@synchronized
def thread_safe(): # make sure it locks the resource
pass
上下文装饰器更多地被上下文管理器(with语句)的使用所取代,这也在本章后面描述。
上下文管理器-with语句
try...finally语句对于确保一些清理代码即使发生错误也会运行是有用的。有许多这样的用例,比如:
-
关闭文件
-
释放锁
-
制作临时代码补丁
-
在特殊环境中运行受保护的代码
with语句通过提供一种简单的方式来包装一段代码来因素化这些用例。这允许您在块执行之前和之后调用一些代码,即使这个块引发异常。例如,通常是这样处理文件的:
>>> hosts = open('/etc/hosts')
>>> try:
... for line in hosts:
... if line.startswith('#'):
... continue
... print(line.strip())
... finally:
... hosts.close()
...
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
注意
这个例子是特定于 Linux 的,因为它读取位于etc中的主机文件,但任何文本文件都可以以同样的方式在这里使用。
通过使用with语句,可以重写成这样:
>>> with open('/etc/hosts') as hosts:
... for line in hosts:
... if line.startswith('#'):
... continue
... print(line.strip )
...
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
在上面的例子中,open作为上下文管理器确保在执行for循环后文件将被关闭,即使发生异常。
与此语句兼容的其他项目是threading模块中的类:
-
threading.Lock -
threading.RLock -
threading.Condition -
threading.Semaphore -
threading.BoundedSemaphore
一般语法和可能的实现
最简单形式的with语句的一般语法是:
with context_manager:
# block of code
...
此外,如果上下文管理器提供一个上下文变量,可以使用as子句在本地存储它:
with context_manager as context:
# block of code
...
请注意,可以同时使用多个上下文管理器,如下所示:
with A() as a, B() as b:
...
这相当于将它们嵌套,如下所示:
with A() as a:
with B() as b:
...
作为一个类
任何实现上下文管理器协议的对象都可以用作上下文管理器。这个协议包括两个特殊方法:
-
__enter__(self):更多信息可以在docs.python.org/3.3/reference/datamodel.html#object.__enter__找到 -
__exit__(self, exc_type, exc_value, traceback):更多信息可以在docs.python.org/3.3/reference/datamodel.html#object.__exit__找到
简而言之,with语句的执行如下:
-
__enter__方法被调用。任何返回值都绑定到指定为子句的目标。 -
执行内部代码块。
-
__exit__方法被调用。
__exit__接收三个参数,当代码块内发生错误时会填充这些参数。如果没有发生错误,所有三个参数都设置为None。当发生错误时,__exit__不应重新引发它,因为这是调用者的责任。它可以通过返回True来防止异常被引发。这是为了实现一些特定的用例,比如我们将在下一节中看到的contextmanager装饰器。但对于大多数用例,这个方法的正确行为是做一些清理,就像finally子句所做的那样;无论在块中发生了什么,它都不返回任何东西。
以下是一个实现了这个协议的一些上下文管理器的示例,以更好地说明它是如何工作的:
class ContextIllustration:
def __enter__(self):
print('entering context')
def __exit__(self, exc_type, exc_value, traceback):
print('leaving context')
if exc_type is None:
print('with no error')
else:
print('with an error (%s)' % exc_value)
当没有引发异常时,输出如下:
>>> with ContextIllustration():
... print("inside")
...**
entering context
inside
leaving context
with no error
当引发异常时,输出如下:
>>> with ContextIllustration():
... raise RuntimeError("raised within 'with'")
...**
entering context
leaving context
with an error (raised within 'with')
Traceback (most recent call last):
**File "<input>", line 2, in <module>
RuntimeError: raised within 'with'
作为函数-上下文管理器模块
使用类似乎是实现 Python 语言中提供的任何协议的最灵活的方式,但对于许多用例来说可能是太多的样板文件。标准库中添加了一个contextlib模块,提供了一些与上下文管理器一起使用的帮助器。它最有用的部分是contextmanager装饰器。它允许您在单个函数中提供__enter__和__exit__部分,中间用yield语句分隔(请注意,这会使函数成为生成器)。使用这个装饰器编写的前面的示例将如下所示:
from contextlib import contextmanager
@contextmanager
def context_illustration():
print('entering context')
try:
yield
except Exception as e:
print('leaving context')
print('with an error (%s)' % e)
# exception needs to be reraised
raise
else:
print('leaving context')
print('with no error')
如果发生任何异常,函数需要重新引发它以便传递它。请注意,context_illustration如果需要的话可以有一些参数,只要它们在调用中提供。这个小助手与基于类的迭代器 API 一样简化了正常的基于类的上下文 API。
这个模块提供的另外三个帮助器是:
-
closing(element):这会返回一个上下文管理器,在退出时调用元素的 close 方法。这对于处理流的类非常有用。 -
supress(*exceptions):如果在 with 语句的主体中发生指定的任何异常,则抑制它们。 -
redirect_stdout(new_target)和redirect_stderr(new_target):这将代码块内的sys.stdout或sys.stderr输出重定向到另一个文件或类文件对象。
其他你可能还不知道的语法元素
Python 语法中有一些不太流行且很少使用的元素。这是因为它们要么提供的收益很少,要么它们的使用方法很难记住。因此,许多 Python 程序员(即使有多年的经验)根本不知道它们的存在。这些特性的最显著的例子如下:
-
for … else子句 -
函数注释
for … else …语句
在for循环之后使用else子句允许您仅在循环以“自然”方式结束而不是用break语句终止时执行代码块:
>>> for number in range(1):
... break
... else:
... print("no break")
...
>>>
>>> for number in range(1):
... pass
... else:
... print("break")
...
break
在某些情况下,这很方便,因为它有助于消除可能需要的一些“标记”变量,如果用户想要存储信息,以确定是否发生了break。这使得代码更清晰,但可能会让不熟悉这种语法的程序员感到困惑。有人说else子句的这种含义是违反直觉的,但这里有一个简单的提示,可以帮助您记住它的工作原理-记住for循环后的else子句只是表示“没有 break”。
函数注释
函数注释是 Python 3 最独特的功能之一。官方文档指出注释是关于用户定义函数使用的类型的完全可选的元数据信息,但实际上,它们并不局限于类型提示,Python 及其标准库也没有利用这样的注释。这就是为什么这个功能是独特的-它没有任何语法意义。注释可以简单地为函数定义,并且可以在运行时检索,但仅此而已。如何处理它们留给开发人员。
一般语法
Python 文档中略微修改的示例最好地展示了如何定义和检索函数注释:
>>> def f(ham: str, eggs: str = 'eggs') -> str:
... pass
...**
>>> print(f.__annotations__)
{'return': <class 'str'>, 'eggs': <class 'str'>, 'ham': <class 'str'>}
如所示,参数注释由表达式定义,该表达式评估为注释值,并在冒号之前。返回注释由冒号后的def语句结束和参数列表后面的->之间的表达式定义。
一旦定义,注释将作为函数对象的__annotations__属性以字典的形式可用,并且可以在应用运行时检索。
任何表达式都可以用作注释,并且它位于默认参数旁边,这允许创建一些令人困惑的函数定义,如下所示:
>>> def square(number: 0<=3 and 1=0) -> (\
... +9000): return number**2
>>> square(10)
100
然而,这种注释的用法除了混淆之外没有其他目的,即使没有它们,编写难以阅读和维护的代码也相对容易。
可能的用途
尽管注释具有巨大潜力,但它们并不被广泛使用。一篇解释 Python 3 新增功能的文章(参见docs.python.org/3/whatsnew/3.0.html)表示,这一功能的目的是“通过元类、装饰器或框架鼓励实验”。另一方面,正式提出函数注释的PEP 3107列出了以下一系列可能的用例:
-
提供类型信息
-
类型检查
-
让 IDE 显示函数期望和返回的类型
-
函数重载/通用函数
-
外语桥梁
-
适应
-
谓词逻辑函数
-
数据库查询映射
-
RPC 参数编组
-
其他信息
-
参数和返回值的文档
尽管函数注释与 Python 3 一样古老,但仍然很难找到任何流行且积极维护的软件包,除了类型检查之外还使用它们。因此,函数注释仍然主要用于实验和玩耍-这是它们被包含在 Python 3 的初始版本中的初衷。
总结
本章涵盖了与 Python 类和面向对象编程无直接关系的各种最佳语法实践。本章的第一部分专门讨论了围绕 Python 序列和集合的语法特性,还讨论了字符串和字节相关序列。本章的其余部分涵盖了两组独立的语法元素-相对于初学者来说相对难以理解的元素(如迭代器、生成器和装饰器)和相对较少知名的元素(for…else子句和函数注释)。