Python高级编程(第2版)

978-7-115-46015-8
作者: 【波兰】Michał Jaworski(贾沃斯基) 【法】Tarek Ziadé(莱德)
译者: 张亮阿信
编辑: 胡俊英
分类: Python

图书目录:

详情

本书针对Python3.5版本进行编写,介绍Python开发中最好的实践和专业设计理念,除了包括一些第三方库和工具外,还涵盖用Nose进行测试驱动开发、用Buildbot进行连续累计以及用Trac进行项目管理等内容。最后,还介绍了一些传统话题,例如建摸、优化以及设计模式。

图书摘要

版权信息

书名:Python高级编程(第2版)

ISBN:978-7-115-46015-8

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [波兰] Michał Jaworski [法] Tarek Ziadé

  译    张 亮  阿 信

  责任编辑 胡俊英

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315

Python作为一种高级程序设计语言,凭借其简洁、易读及可扩展性日渐成为程序设计领域备受推崇的语言之一。

本书基于Python 3.5版本进行讲解,通过13章的内容,深度揭示了Python编程的高级技巧。本书从Python语言及其社区的现状开始介绍,对Python语法、命名规则、Python包的编写、部署代码、扩展程序开发、管理代码、文档编写、测试开发、代码优化、并发编程、设计模式等重要话题进行了全面系统化的讲解。

本书适合想要进一步提高自身Python编程技能的读者阅读,也适合对Python编程感兴趣的读者参考学习。全书结合典型且实用的开发案例,可以帮助读者创建高性能的、可靠且可维护的Python应用。


张亮(hysic),毕业于北京大学物理学院,是一名爱好机器学习和数据分析的核安全工程师,主要负责本书前6章的翻译,并对本书进行了技术审读。

阿信,软件工程师,业余时间喜欢读书,也喜欢翻译。


Michał Jaworski有着7年Python编程的经验。他还是graceful的创建者,这是一个构建于falcon之上的REST框架。他曾在不同的公司担任过多种角色,从一名普通的全栈开发人员到软件架构师再到一家快节奏创业公司的工程副总裁。他目前是Opera软件公司TV Store(电视应用商店)团队的首席后端工程师。他在设计高性能的分布式服务方面拥有丰富的经验。他还是一些流行的Python开源项目的活跃贡献者。

Tarek Ziadé是Mozilla的工程经理,与一个专门用Python为Firefox构建大规模Web应用的团队合作。他对Python打包做出过贡献,而且从早期Zope开始就使用过各种不同的Python Web框架。

Tarek还创建了Afpy——法国的Python用户组,并且用法语写过两本关于Python的书。他还在诸如Solutions Linux、PyCon、OSCON和EuroPython等国际活动中做过多次法语演讲和教学。


Facundo Batista是Python编程语言方面的专家,拥有超过15年的Python编程经验。他是这门语言的核心开发者,也是Python软件基金会的成员。他还获得了2009年的社区服务奖,奖励他组织了阿根廷的PyCon及其Python社区,以及对标准库的贡献和在翻译Python文档方面所做的工作。

他还在阿根廷与其他国家(美国和欧洲)的主要Python会议上发表演讲。总之,他有丰富的分布式协同经验,10多年来一直参与FLOSS开发并与全球人员合作。

他曾在Telefónica Móviles和Ericsson担任电信工程师,还曾在Cyclelogic担任Python专家(首席开发工程师),目前的职务是Canonical的高级软件开发工程师。

他还喜欢打网球,同时是两个可爱宝宝的父亲。


Python很棒!

从20世纪80年代末出现的最早版本到当前版本,Python的发展一直遵循着相同的理念:提供一个同时具备可读性和生产力的多范式编程语言。

人们曾经将Python看作另一种脚本语言,认为它不适合构建大型系统。但多年以来,在一些先驱公司的努力下,Python显然可以用于构建几乎任何类型的系统。

实际上,许多其他语言的开发者也醉心于Python,并将它作为首选语言。

如果你购买了这本书,可能已经知道这些内容了,所以无需再向你证明这门语言的优点。

本书展现了作者多年构建各种Python应用的经验,从几个小时完成的小型系统脚本,到许多开发人员历经数年编写的大型应用。

本书描述了开发人员使用Python的最佳实践。

本书包含了一些主题,这些主题并不关注语言本身,而是更多地关注如何利用相关的工具和技术。

换句话说,本书描述了高级Python开发人员每天的工作方式。

第1章介绍了Python语言及其社区的现状。本章展示了Python不断变化的方式及原因,还解释了为什么这些事实对任何想要自称Python专家的人来说是很重要的。本章还介绍了最流行和最公认的Python工作方式——常用的生产力工具和现已成为标准的约定。

第2章深入介绍迭代器、生成器、描述符等内容。本章还包括关于Python习语和CPython类型内部实现的有用注释,这些类型的计算复杂度是对这些习语的阐释。

第3章介绍了语法最佳实践,但重点放在类级别以上。本章包括Python中更高级的面向对象的概念和机制。学习这些知识是为了理解本章最后一节的内容,其中介绍的是Python元编程的各种方法。

第4章介绍了如何选择好的名称。它是对PEP 8中命名最佳实践的扩展,并且给出了一些如何设计良好API的提示。

第5章介绍如何创建Python包以及使用哪些工具,以便在官方的Python包索引或其他包仓库中正确地分发。对于Python包还补充了一些工具的简要回顾,这些工具可以让你用Python源代码创建独立可执行文件。

第6章主要针对Python Web开发人员和后端工程师,因为讲的是代码部署。本章解释了如何构建Python应用,使其可以轻松部署到远程服务器,还介绍了可以将这个过程自动化的工具。本章是第5章的延续,因此还介绍了如何使用包和私有包仓库来简化应用部署。

第7章解释了为什么为Python编写C扩展程序有时可能是一个好的解决方案。本章还展示了只要使用了正确的工具,它并不像想象中那么难。

第8章深入介绍了项目代码库的管理方式,还介绍了如何设置各种持续开发流程。

第9章包含文档相关的内容,提供了有关技术写作和Python项目文档化方式的建议。

第10章解释了测试驱动开发的基本原理,还介绍了可用于这种开发方法的工具。

第11章解释了何为优化,介绍了分析技术和优化策略指南。

第12章是对第11章的扩展,为Python程序中经常出现的性能问题提供了一些常用的解决方案。

第13章介绍了Python并发这一宏大的主题。本章解释了并发的概念、何时需要编写并发应用,以及Python程序员主要使用的并发方法。

第14章用一套有用的设计模式以及Python的代码示例对本书进行了总结。

本书面向的是可以在任何操作系统上使用Python 3进行软件开发的人员。

这不是一本面向初学者的书,所以我假设你已经在开发环境中安装了Python,或者知道如何安装Python。不管怎样,本书考虑到以下事实:不是每个人都需要充分了解Python的最新功能或官方推荐的工具。因此,第1章概述了常见的实用程序(例如虚拟环境和pip),这些实用程序现在已经成为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:

新术语重要词语将以粗体显示。你会在屏幕上看到的单词(例如在菜单或对话框中)将以下面这种文本形式出现:“单击Next按钮可跳转至下一屏”。

〓〗  警告或重要提示。

 

〓〗 提示和技巧。

我们十分欢迎读者的反馈意见。让我们了解你对本书的看法——喜欢哪些内容,不喜欢哪些内容。这些反馈对我们很重要,因为它有助于我们编写出对读者真正有帮助的书。

一般性的反馈请发送邮件至feedback@packtpub.com,并在邮件主题中注明本书的标题。

如果你是某个领域的专家,并且有兴趣写一本书或者参与出版一本书,请参阅我们的作者指南。

现在你已经成为这本Packt图书的拥有者,为了让你的购买物超所值,我们还为你提供了许多其他方面的服务。

你可以用自己的账号在Packt的官方网站下载本书的示例代码文件。如果你是在其他地方购买的本书,可以访问Packt的官方网站并注册,文件会直接通过邮件发送给你。

下载代码文件的步骤如下所示。

你还可以在Packt网站的本书页面单击Code Files按钮来下载代码文件。在Search框输入本书的书名即可访问该页面。请注意,你需要登录Packt账号。

文件下载完成后,请确保用下列软件的最新版本对文件夹进行解压或提取。

本书的代码包也托管在GitHub,网址为https://github.com/PacktPublishing/Expert-Python- Programming_Second-Edition。在GitHub上还有大量图书和视频资源。快去看一下吧!

尽管我们已经竭尽全力确保本书内容的准确性,但错误在所难免。如果你发现了书中的错误,无论是正文错误还是代码错误,希望你能将其报告给我们,我们将不胜感激。这样不仅能够减少其他读者的困惑,还能帮助我们改进本书后续版本的质量。如果你需要提交勘误,请访问http://www.packtpub.com/submit-errata,选择相应的书名,单击Errata Submission Form链接,然后输入你的勘误信息并提交。一旦通过验证,我们将接受你提交的勘误,同时勘误内容也将被上传到我们的网站,或者被添加到对应勘误区的现有勘误列表中。

想要查看之前提交的勘误,请访问https://www.packtpub.com/books/content/support,并在搜索框中输入相应的书名。你想查看的信息将出现在Errata下面。

所有媒体在互联网上都一直饱受版权侵害的困扰。Packt坚持对版权和授权进行全力保护。如果你在互联网上发现我社图书任何形式的盗版,请立即为我们提供网址或网站名称,以便我们采取进一步的措施。

请将疑似盗版材料的链接发送到copyright@packtpub.com。

我们感谢你对作者的保护,这有助于我们继续为你提供更有价值的内容。

如果你对本书的某个方面抱有疑问,请通过questions@packtpub.com联系我们,我们会尽力为你解决。


Python很适合开发者使用。

无论你或你的客户用的是什么操作系统,都可以使用Python。例如你可以在Linux上工作,然后部署到其他系统上,除非你的代码与特定平台相关,或者用到了特定平台的库。但这一特性已经不新鲜了(Ruby、Java等很多其他语言都可以做到这一点)。本书还会讲到Python的其他特性,所有这些特性是使得Python成为一家公司主力开发语言的重要原因。

本书主要讲的是Python的3.5版本,如果没有明确说明的话,书中所有代码示例都是用这个版本的Python编写的。由于这一版本尚未被广泛使用,本章将会向读者介绍一下Python 3的当前现状,同时介绍Python的现代开发方法。本章主要包括以下内容。

每本书的开头都要来点开胃小菜。如果你对Python已经很熟悉了(特别是最新的3.x版本),并且掌握了开发中做环境隔离的正确方法,你可以跳过本章的前两节,快速阅读其他小节即可。其他小节会讲到一些工具和资源,它们并非必不可少,但可以大大提高Python开发效率。不过一定要读一下关于应用层环境隔离和pip的一节,因为这一节提到的工具会在本书后面的内容中用到。

Python的历史最早可追溯到20世纪80年代末,但是1.0版的发行时间是在1994年,所以Python并不是一门非常年轻的语言。这里本该介绍Python主要版本发布的整个时间线,但其实真正重要的日期只有一个:2008年12月3日,也就是Python 3.0的发布日期。

在写作本书时,Python 3的首次发布已经过去了7年。PEP 404也已经创建了4年,PEP 404是“取消发布"(un-release)Python 2.8并正式关闭Python 2.x分支的官方文档。虽然过去了这么长的时间,Python 社区中依然存在明显的分歧。语言本身在迅速发展,但大量用户却并不想更新版本。

原因很简单。Python升级是因为有这样的需求。语言之间的竞争随时都在上演。每隔几个月都会突然冒出一门新语言,声称解决了之前所有语言中存在的问题。对于大多数类似的项目,开发人员很快就会失去兴趣,它们的名气也只是一时炒作。

不管怎样,这也表示存在着更严重的问题。人们之所以设计新的编程语言,是因为他们发现现有的语言无法以最佳方式来解决问题。认识不到这样的需求是目光短浅的。此外,Python的使用范围也越来越广泛,人们发现它有许多可以改进的地方,也应该做出这样的改进。

Python的很多改进往往是由特定应用领域的需求驱动的。其中最重要的领域是Web开发,这一领域需要Python改进对并发的处理。

有些变化只是由于Python项目的历史原因导致的。这些年已经发现了Python的一些不合理之处,有些是标准库模块结构混乱或冗余,有些是程序设计缺陷。最初,发布Python 3是要对这门语言进行较大的清理与更新,但结果显示,这个计划并没有收到预期的效果。在很长一段时间内,很多开发人员对Python 3只是抱着好奇的态度而已,但希望这种情形正在好转。

Python社区有一种应对变化的固定方法。虽然各种各样的Python语言修改意见主要在邮件列表(python-ideas@python.org)中进行讨论,但只有发布了名为PEP的新文档,新的变化才会生效。PEP的全称是Python改进提案(Python Enhancement Proposal,PEP)。它是提交Python变化的书面文档,也是社区对这一变化进行讨论的出发点。这些文档的整个目的、格式和工作流程的标准格式也都包含在一份Python改进提案中,也就是PEP 1文档(http://www.python.org/dev/peps/pep-0001)。

PEP文档对Python的作用十分重要,根据讨论的主题,PEP主要有以下3种用途。

所有提交过的PEP都被汇总在一个文档中,就是PEP 0(https://www.python.org/dev/peps/)。由于这些PEP都在同一个网站上很容易找到,其URL也很容易猜到,因此本书一般用编号来指代这些文档。

如果你对Python语言的未来发展方向感兴趣,但又没时间跟踪Python邮件列表中的讨论,那么PEP 0会是很好的信息来源。它会告诉你,哪些文档已被接受但尚未实施,哪些文档仍在审议中。

PEP还有其他的用途。人们通常会问这样的问题:

大多数情况下,关于该功能的某个PEP文档已经给出了上述问题的详细回答。很多提交的关于Python语言功能的PEP文档并没有通过。这些文档可作为历史资料来参考。

Python 3有许多强大的新功能,那么它在社区中广泛普及了吗?遗憾的是,并没有。有一个著名的网站叫“Python 3荣耀之墙(Python 3 Wall of Superpowers)”,里面记录了大多数常用软件包与Python 3的兼容性,不久前这个网站刚刚改名为“Python 3耻辱之墙(Python 3 Wall of Shame)”。目前这种状况正在逐步改善,上述网站的软件包列表中绿色的比例也在每月缓慢增加[1]。尽管如此,但这并不代表很快所有应用开发团队都只使用Python 3。当所有常用软件包都支持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的向后兼容。但它并不是完全重新设计的。而且,也并不是说2.x版本的Python模块在Python 3下都无法运行。代码可以完全跨版本兼容,无需其他工具或技术在两大版本上都可以运行,但一般只有简单应用才能做到这一点。

本章前面说过我个人对Python 2兼容性的看法,但是目前不可能完全忽视这一点。还有一些Python包(例如第6章将讲到的fabric)十分实用,但可能短期内不会迁移到Python 3。

另外,有时我们还会受到所在公司的制约。现有的遗留代码可能非常复杂,迁移代码的费用难以承受。所以即使我们现在决定只用Python 3,短期内也不可能完全放弃Python 2。

如今想要自称专业开发者,没有对社区的回馈是说不过去的,所以帮助开源软件开发者向现有软件包中添加对Python 3的兼容,可以很好地偿还在使用这些软件包时产生的“道德债(moral debt)”。当然,不了解Python 2和Python 3的差异是无法做到这一点的。顺便提一下,对于Python 3新手来说,这也是一项很好的练习。

要比较不同版本之间的差异,最好的参考资料就是Python文档。不过为了方便读者,本节总结了其中最重要的内容。但不熟悉Python 3的读者还是要去阅读官方文档。

Python 3引入的重要差异一般可分为以下几个方面。

1.语法变化

有些语法变化会导致当前代码无法运行,这些变化是最容易发现的,它们会导致代码根本无法运行。包含新语法元素的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版本上也会报错)。其中最重要的语法差异将会在第2章和第3章中讲到,所以这里无需全部列出。

与Python 2.7相比,删除或改动的内容要相对少一些,下面给出最重要的变化内容。

2.标准库中的变化

语法变化很容易发现,标准库中的重大变化也是非常容易发现的。Python的每个后续版本都会向标准库模块中添加、弃用、改进或完全删除某些内容。在旧版Python(1.x和2.x)中也会定期有这样的变化,所以出现在Python 3中并不让人吃惊。大多数情况下,对于删除或重组的模块(例如urlparse移到了urllib.parse),在运行解释器时会对导入语句抛出异常。这样的问题很容易发现。无论如何,为了确保能够发现所有类似的问题,完整的代码测试覆盖率是必不可少的。在某些情况下(例如使用延迟加载模块时),这个通常在全局导入时出现的问题并不会出现,直到在代码中将某些模块作为函数调用时才会出现。因此,在测试期间确保每行代码都要实际运行是很重要的。

 

延迟加载模块

延迟加载模块是指在全局导入时并不加载的模块。在Python中,import语句可以包含在函数内部,这样导入是在函数调用时才会发生,而不是在全局导入时发生。在某些情况下,模块的这种加载方式可能比较合理,但大多数情况下,这只是对设计不佳的模块结构的变通方法(例如避免循环导入),通常应避免这种加载方式。当然,对于标准库模块来说,没有理由使用延迟加载。

3.数据类型与集合的变化

开发人员在努力保持兼容性或只是将现有代码迁移到Python 3上时,需要特别注意Python中数据类型与集合的表示方式的变化。虽然不兼容的语法变化或标准库变化很容易发现,也很容易修复,但集合与数据类型的变化要么难以察觉,要么需要大量的重复工作。这样的变化列表会很长,再次重申,官方文档是最好的参考资料。

不过,这一节必须讲一下Python 3中字符串处理方式的变化,因为这是Python 3中最具争议也是讨论最多的变化,尽管这是一件好事,使很多问题变得更加明确。

现在所有字符串都是Unicode,字节(bytes)需要加一个bB的前缀。Python 3.0和3.1不支持使用u前缀(例如u"foo"),使用的话会引发语法错误。不支持这个前缀是引发所有争议的主要原因。这导致难以编写能够兼容Python不同分支的代码,2.x版需要用这个前缀来创建Unicode。Python 3.3又恢复了这个前缀,虽然没有任何语法上的意义,只是为了简化兼容过程。

在Python不同版本之间保持兼容性是一项挑战。根据项目的大小不同,这项挑战可能会增加许多额外的工作量,但绝对可行,也很值得去做。对于在许多环境中都会用到的Python包来说,必须要保持跨版本兼容性。如果开源包没有定义明确并经过测试的兼容范围(compatibility bound),是不太可能流行起来的。而且,对于只在公司网络封闭使用的第三方代码来说,也可以大大受益于在不同环境中的测试。

这里应该注意,虽然这一部分内容主要关注Python不同版本之间的兼容,但这些方法也适用于保持与外部依赖项之间的兼容,外部依赖项包括不同的包版本、二进制库、系统或外部服务等。

整个过程主要分为3个部分,按重要性排序如下。

告知兼容范围是整个过程中最重要的一部分,因为这可以让代码使用者(开发人员)对代码的工作原理和未来的变化方式有一定的预期和假设。我们的代码可能用于多个不同项目的依赖,这些项目也在努力管理兼容性,所以把代码兼容性说清楚还是很重要的。

本书总是尽量给出几个选择,而不会强烈推荐某个特定选项,而这里是少数几个例外之一。目前来看,管理兼容性未来变化的最佳方法,就是正确使用语义化版本(Semantic Versioning semver)的版本号。它是一个广为接受的标准,用仅包含3个数字的版本标识符来标记代码的变化范围。它还给出了如何处理弃用的方法建议。下面是摘录semver官网的摘要。

版本格式:主版本号.次版本号.修订号,版本号递增规则如下。

先行版本号及版本编译信息可以加到“主版本号.次版本号.修订号”的后面,作为延伸。

测试时就会发现一个悲伤的事实,为了保证代码与每个依赖版本和每个环境(这里环境指的是Python版本)都保持兼容,必须在所有可能的组合中对代码进行测试。当然,如果项目的依赖很多,做到这一点基本是不可能的,因为随着依赖版本数目的增加,组合的数目也会迅速增加。因此,通常需要做一些权衡,使得运行所有兼容性测试无需花费数年的时间。第10章中介绍一般的测试,里面也介绍了所谓的矩阵测试中工具的选择。

 

项目遵循semver的好处在于,通常只有主版本才需要测试,因为次版本和修订版本中保证没有向后不兼容的变化。只有项目不违背这样的约定,这种说法才能成立。不幸的是,每个人都会犯错,许多项目中都出现了后向不兼容的变化,甚至在修订版本中也出现了这种变化。尽管如此,由于semver声称对次版本和修订版本的变化保持严格的向后兼容,那么打破这个规则就可以视为bug,可以在修订版本中进行修复。

如果明确定义了兼容范围并严格测试,那么实现兼容层就是最后一步,也是最不重要的一步。但是,每一位对这个话题感兴趣的程序员都应该知道下列工具和技术。

最基本的就是Python的__future__模块。它将Python新版本中的一些功能反向迁移到旧版本中,采用的是导入语句的形式:

   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") # 旧的字面值
<type 'str'>
>>> from __future__ import unicode_literals
>>> type("foo") # 现在变成了unicode
<type 'unicode'>

下面列出了所有可用的__future__语句,关注2/3兼容性的开发者都应该知道。

__future__中的可选语句列表很短,只包含几个语法功能。对于其他变化的内容,例如元类语法(第3章会讲到这一高级特性),维持其兼容性则困难得多。future语句也无法完全解决多个标准库重组的问题。幸运的是,有些工具旨在提供一致可用的兼容层。最有名的就是Six模块,提供了常用的2/3兼容性的整个样板。另一个很有前途但名气稍逊的工具是future模块。

在某些情况下,开发人员可能不想在一些小型Python包里添加其他依赖项。通常的做法是将所有兼容性代码放在一个附加模块中,该模块通常命名为compat.py。下面是来自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)

这样的compat.py模块十分常见,即使是利用Six保持2/3兼容性的项目也很常见,因为这种方法非常方便,用于保存在不同版本的依赖包之间保持兼容性的代码。

 

下载示例代码

你可以用自己的账号在Packt的官方网站下载本书的示例代码文件。如果你是在其他地方购买的本书,你可以访问Packt的官方网站并注册,文件会直接通过邮件发送给你。

下载代码文件的步骤如下。

  • 用你的电子邮件地址和密码登录或注册我们的网站。

  • 将鼠标指针悬停在顶部的SUPPORT选项卡上。

  • 单击Code Downloads & Errata

  • Search框中输入本书的名字。

  • 选择你要下载代码文件的书籍。

  • 从下拉菜单中选择本书的购买途径。

  • 单击Code Download

文件下载完成后,请确保用下列软件的最新版本对文件夹进行解压或提取。

  • 在Windows上用WinRAR或7-Zip。

  • 在Mac上用Zipeg、iZip或UnRarX。

  • 在Linux上用7-Zip或PeaZip。

本书的代码包也托管在GitHub,网址为https://github.com/ PacktPublishing/Expert-Python-Programming_Second- Edition。在GitHub上还有大量图书和视频资源。去看一下吧!

最重要的Python实现是用C语言编写的,叫作CPython。大多数人在讨论Python时指的都是CPython。随着语言的进化,C语言实现也相应发生变化。除了C之外,Python还有其他几种实现方式,这些实现方式都在努力地跟上主流。大多数实现方式的时间表都要落后于CPython,但它们提供了一个好机会,可以在具体环境中使用并推广Python语言。

Python实现有许多种。在Python官网上关于这一话题的维基百科页面中,主要介绍了20多种语言变体、方言或除C语言之外的Python解释器实现。其中一些只是实现了语言核心语法、功能和内置扩展的一个子集,但至少有几个与CPython几乎完全兼容。最重要的是,虽然其中一些只是玩具项目或实验,但大部分都是为了解决某些实际问题而创建的,这些问题要么是用CPython无法解决,要么需要开发人员花费巨大的精力。这些问题的实例包括如下几个。

本节将简要介绍目前Python开发人员可用的最流行和最新的Python实现。

Stackless Python自称Python增强版。之所以名为Stackless(无栈),是因为它没有依赖C语言的调用栈。它实际上是修改过的CPython代码,还添加了一些新的功能,在创建Stackless Python时Python核心实现中还没有这些功能。其中最重要的功能就是由解释器管理的微线程,用来替代依赖系统内核上下文切换和任务调度的普通线程,既轻量化又节约资源。

Stackless Python最新可用的版本是2.7.9和3.3.5,分别实现的是Python 2.7和3.3。在Stackless Python中,所有的额外功能都是内置stackless模块内的框架。

Stackless Python并不是最有名的Python实现,但很值得一提,因为它引入的思想对编程语言社区有很大的影响。将Stackless Python中的内核切换功能提取出来并作为一个独立包发布,名为greenlet,现在是许多有用的库和框架的基础。此外,它的大部分功能都在PyPy中重新实现,PyPy是另一个Python实现,我们将稍后介绍。

Jython是Python语言的Java实现。它将代码编译为Java字节代码,开发人员在Python模块中可以无缝使用Java类。Jython允许人们在复杂应用系统(例如J2EE)中使用Python作为顶层脚本语言,它还将Java应用引入到Python世界中。Jython的一个很好的例子就是,在Python程序中可以使用Apache Jackrabbit(这是一个基于JCR的文档仓库API。

Jython最新可用的版本是Jython 2.7,对应的是Python 2.7版。它宣称几乎实现了Python所有的核心标准库,并使用相同的回归测试套件。Jython 3.x版正在开发中。

Jython与CPython实现的主要区别如下所示。

这一语言实现的主要缺点是缺少对C/Python扩展API的支持,因此用C语言编写的Python扩展在Jython中无法运行。这种情况未来可能会发生改变,因为Jython 3.x计划支持C/Python扩展API。

某些Python Web框架(例如Pylons)被认为是促进Jython的开发,使其可用于Java世界。

IronPython将Python引入.NET框架中。这个项目受到微软的支持,IronPython的主要开发人员都在微软工作。它是推广语言的一种重要实现。除了Java,.NET社区是最大的开发者社区之一。还值得注意的是,微软提供了一套免费开发工具,可以将Visual Studio转换为成熟的Python IDE。这是作为Visual Studio的插件发布的,名为PTVS(Python Tools for Visual Studio,用于Visual Studio的Python工具),在GitHub可以找到其开源代码。

最新的稳定版本是2.7.5,与Python 2.7兼容。与Jython类似,Python 3.x的实现也在开发中,但还没有可用的稳定版本。虽然.NET主要在微软Windows系统上运行,但是IronPython也可以在Mac OS X和Linux系统上运行。这一点要感谢Mono,一个跨平台的开源.NET实现。

与CPython相比,IronPython的主要区别或优点如下。

说到弱点,IronPython也与Jython非常类似,因为它也不支持C/Python扩展API。对于想要使用主要基于C扩展的Python包(例如NumPy)的开发人员来说,这一点是很重要的。有一个叫作ironclad的项目,其目的是在IronPython中无缝使用这些扩展,虽然它最新支持的版本是2.6,开发似乎也停止了。

PyPy可能是最令人兴奋的Python实现,因为其目标就是将Python重写为Python。在PyPy中,Python解释器本身是用Python编写的。在Python的CPython实现中,有一个C代码层来实现具体细节。但在PyPy实现中,这个C代码层用Python完全重写。

这样你可以在代码运行期间改变解释器的行为,并实现CPython难以实现的代码模式。

目前PyPy的目的是与Python 2.7完全兼容,而PyPy3则与Python 3.2.5版兼容。

以前对PyPy感兴趣主要是理论上的原因,只有喜欢深入钻研语言细节的人才会对它感兴趣。它通常不用于生产环境,但这些年来这种状况已经发生改变。现在许多基准测试给出惊人的结果,PyPy通常比CPython实现要快得多。这个项目有自己的基准测试网站,记录了用数十种不同的基准测试对每一版本性能的测量结果(参见http://speed.pypy.org/)。网站清晰地显示,启用JIT的PyPy至少比CPython要快好几倍。由于PyPy的这一特性以及其他特性,使得越来越多的开发人员决定在生产环境中切换到PyPy。

PyPy与CPython实现的主要区别在于以下几个方面。

与几乎所有其他的Python实现类似,PyPy也缺乏对C/Python扩展API的完全官方支持。但它至少通过CPyExt子系统为C扩展提供了某种程度的支持,虽然文档不完整,功能也尚未完善。此外,社区正在努力将NumPy迁移到PyPy中,因为这是最需要的功能。

作为专家,最重要的是要对所选用的编程语言有深刻的理解。对于任何技术来说都是如此。但如果不知道在特定语言社区中的常用工具和实践的话,想开发一款好软件是相当困难的。Python所有的单项功能都可以在其他某种语言中找到。所以,直接比较语法、表现力(expressiveness)或性能的话,总会在一个或多个方面存在更好的解决方案。但Python真正出众的领域在于围绕语言打造的整个生态系统。多年来,Python社区完善了标准实践和标准库,有助于在更短的时间内创建更可靠的软件。

对于上文提到的生态系统,最明显也最重要的一部分就是大量免费的开源包,可以用来解决许多问题。编写新软件总是一个费钱又费时的过程。能够复用现有代码而无需重新造轮子(reinvent the wheel),可以大大降低开发的时间和成本。这也是某些公司的项目在经济上可行的唯一原因。

由于这个原因,Python开发者花费大量精力来创建工具和标准,方便使用他人创建的开源包。我们首先介绍虚拟隔离环境、改进的交互式shell和调试器,然后介绍一些程序,有助于发现、搜索和分析PyPIPython Package Index,Python包索引)上大量可用的Python包。

现在许多操作系统都将Python作为标准组件。对于大多数Linux发行版和基于Unix的系统(如FreeBSD、NetBSD、OpenBSD或OS X系统)来说,要么默认安装了Python,要么系统软件包仓库中包含Python。其中很多系统甚至将Python作为核心组件的一部分。有些操作系统的安装程序是用Python编写的,例如Ubuntu系统的Ubiquity、Red Hat Linux和Fedora系统的Anaconda。

基于这一事实,PyPI上的许多包也可以用系统包管理工具(如Debian和Ubuntu的apt-get、Red Hat Linux的rpm、Gentoo的emerge)作为本地包来管理。不过应该记住,可用的库非常有限,大部分也比PyPI上的版本要旧。因此,PyPA(Python Packaging Authority,Python包官方小组)推荐始终采用pip来获取最新版本的Python包。虽然从CPython 2.7.9版和3.4版开始,pip已经成为一个独立的Python包,但每一个新版本都会默认安装pip。安装新Python包的方法就是这么简单,如下所示:

pip install <package-name>

pip功能十分强大,可以强制安装特定版本的Python包(语法为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版和2.7.9版开始,总是可以使用`ensurepip`模块来引导启动`pip`,具体如下:

$ 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可用于安装系统级的Python包。在基于Unix的系统和Linux系统上,这么做需要超级用户权限,所以实际的调用如下所示:

sudo pip install <package-name>

注意,在Windows上并不需要这样做,因为没有默认安装Python解释器,Windows上的Python通常由用户手动安装,无需超级用户权限。

无论如何,不推荐直接从PyPI安装系统级的Python包,也应尽量避免这一做法。前面说PyPA推荐使用pip,这似乎与前面的说法相矛盾,但其中是有很重要的原因。如前所述,通过操作系统软件包仓库,Python往往是许多软件包的重要组成部分,也可以提供许多重要服务。系统发行版的维护者投入大量精力选择合适的软件包版本,以匹配各种包依赖。通常来说,系统软件包仓库中的Python包都包含自定义补丁,或者使用较旧的版本,只是为了保证与其他系统组件的兼容。利用pip将这些Python包强制更新至某一版本,打破了向后兼容,也可能会破坏某些关键的系统服务。

即使在本地计算机上,为了方便开发而做这些事情也不是一个好的理由。那样胡乱使用pip几乎总会引起麻烦,最终导致难以调试的问题。并不是说要严格禁止从PyPI全局安装Python包,但这么做时一定要清楚地认识到相关风险。

幸运的是,这个问题有一个简单的解决方案,就是环境隔离。在不同的系统抽象层中对Python运行环境进行隔离的工具有很多种。其主要作用是,将项目依赖与其他项目和/或系统服务需要的包进行隔离。这种方法的好处在于以下几个方面。

隔离最简单也最轻便的方法就是使用应用层的虚拟环境。它们仅隔离Python解释器和其中可用的Python包。其设置非常简单,通常也足以保证小项目和小软件包开发过程中的隔离。

不幸的是,在某些情况下,这种做法可能不足以保证充分的一致性和可重复性。对于这种情况,系统级隔离是对工作流程很好的补充,本章后面也会介绍一些可用的方案。

在运行时隔离Python的方法有几种。最简单也最显而易见的方法,就是手动修改PATHPYTHONPATH环境变量或将Python二进制文件移动到其他位置,以改变它发现可用Python包的方式,将环境变量修改成保存项目依赖的自定义位置,当然这种方法也最难维护。幸运的是,有几种工具可以帮助维护虚拟环境,并维护系统中安装包的存储方式。这些工具主要包括:virtualenvvenvbuildout。它们在底层做的事情实际上与我们手动做的一样。实际的策略取决于具体的工具实现。但一般来说,它们更方便使用,而且还有其他好处。

1.virtualenv

在这个工具列表中,virtualenv是目前最常用的工具。它名字的含义就是虚拟环境(virtual environment)。它并不是Python标准发行版的一部分,所以需要用pip来获取。它也是值得在系统层面安装的Python包之一(在Linux系统和基于Unix的系统中要用到sudo)。

安装完成后,利用下面的命令可以创建一个新的虚拟环境:

virtualenv ENV

这里的ENV应替换为新环境的名字。这将在当前工作目录路径中创建一个新的ENV目录。里面包含以下几个新目录。

创建好新环境后,需要用Unix的source命令在当前shell会话中激活它:

source ENV/bin/activate

这将会影响环境变量,从而改变当前shell会话的状态。为了告知用户已经激活了虚拟环境,shell提示符会在开头增加(ENV)字符串。下面举个例子,在会话中创建一个新环境并激活:

$ virtualenv example
New python executable in example/bin/python
Installing setuptools, pip, wheel...done.
$ source example/bin/activate
(example)$ deactivate
$

关于virtualenv要注意,最重要的是它完全依赖于在文件系统中的存储状态。它不会提供额外功能来跟踪应该安装哪些包。这些虚拟环境不可移植,不能移动到其他系统或机器。对每个新的应用部署来说,都需要从头开始创建新的虚拟环境。因此,virtualenv用户有一个良好实践,就是将所有项目依赖保存到一个requirements.txt文件(约定命名)中,正如下面的代码所示:

   # 井号(#)后面的内容是注释。

   # 明确版本号,可重复性高。
   eventlet==0.17.4
   graceful==0.1.1

   # 如果项目在不同依赖版本中都通过测试,
   # 也可以指定相对版本编号。
   falcon>=0.3.0,<0.5.0

   # 应尽量明确Python包的版本,
   # 除非始终需要最新版。
   pytz

有了这个文件,用pip就可以轻松安装所有依赖,因为它可以接受需求文件作为参数:

pip install -r requirements.txt

需要记住,需求文件并不总是理想的解决方案,因为它没有给定依赖的准确列表,而只给出了需要安装的依赖。因此,如果需求文件并非最新版,无法反映环境的实际状态,那么整个项目在开发环境中可以正常运行,但在其他环境中却无法启动。当然,pip freeze命令可以打印出当前环境所有的Python包,但不应该盲目使用这个命令。它会打印出所有内容,甚至那些仅用于测试而并不用于项目的Python包。本书提到的另一款工具buildout就解决了这个问题,所以可能是某些开发团队的更佳选择。

 

对于Windows用户来说,Windows下的virtualenv对内部目录结构使用了一种不同的命名方式。你要用Scripts/Libs/Include/3个目录,而不是bin/lib/include/,以更好地匹配这种操作系统上的开发约定。用于激活/关闭环境的命令也不一样。你要用ENV/Scripts/activate.batENV/Scripts/deactivate.bat,而不是将source命令作用在activatedeactivate脚本上。

2.venv

虚拟环境很快逐步完善,成为了社区中的常用工具。从Python 3.3开始,标准库已经支持创建虚拟环境。其用法与Virtualenv几乎相同,虽然命令行选项采用了不同的命名约定。新的venv模块提供了pyvenv脚本,可以用于创建新的虚拟环境:

pyvenv ENV

这里的ENV应替换为新环境的名字。此外,现在也可以用Python代码直接创建新的环境,因为所有功能都包含在内置的venv模块中。其他用法和实现细节(例如环境目录的结构、激活/关闭脚本)与Virtualenv几乎完全相同,所以换用这种方法应该很简单,也不会牵扯太多精力。

对于使用较新版本Python的开发人员来说,推荐使用venv而不是Virtualenv。对于Python 3.3版,切换到venv可能需要付出更多的精力,因为这一版本在新环境中没有默认安装setuptoolspip,所以用户需要手动安装它们。幸运的是,这一点在Python 3.4中已经修改,并且由于venv的可定制性,其内容可以被改写。对于细节的解释可参见Python文档,但有些用户可能会认为它过于复杂,仍然在这一版本的Python中继续使用Virtualenv。

3.buildout

buildout是一个强大工具,可与引导启动并部署用Python编写的应用。它的一些高级特性将在本书后面讲到。在很长一段时间内,他还被用作创建Python隔离环境的工具。由于buildout需要声明性的配置,每次依赖发生变化都必须修改配置,因此这些环境更容易复制和管理,无需依赖环境状态。

不幸的是,这一情况已发生变化。从2.0.0版开始,buildout包不再提供与系统Python在任何层级的隔离。处理隔离的任务留给其他工具来做,如virtualenv,所以仍然可以用buildout来做隔离,但事情变得有点复杂。buildout必须要在隔离环境中初始化才能真正隔离。

与之前版本的buildout相比,这一版本有一个主要缺点,就是它要依赖其他隔离方法。开发这些代码的开发人员不再确定对依赖的描述是否完整,因为有些Python包可以绕过声明性配置来安装。当然,这个问题可以通过适当的测试和发布过程来解决,但却使整个工作流程更加复杂。

总而言之,buildout不再是提供环境隔离的解决方案,但其声明性配置可以提高虚拟环境的可维护性和可重复性。

不存在适用于所有情况的最佳解决方案。一家公司认为好的解决方案可能并不适用于其他团队的工作流程。而且每个应用的需求也各不相同。小项目可以只使用virtualenvvenv,比较简单,但大型项目可能还需要buildout的帮助,以便进行更复杂的装配。

之前没有详细说明的是,在buildout早期版本(2.0.0版之前)中,可以在隔离环境中对项目进行装配,其结果与Virtualenv给出的结果类似。不幸的是,这个项目的1.x分支不再受到维护,所以不建议因为这个原因使用它。

我推荐尽可能使用venv模块,而不是virtualenv。因此,对于面向Python 3.4或更高版本的项目,应该默认选择venv。在Python 3.3中使用venv可能不太方便,因为没有内置setuptoolspip的支持。对于面向更多Python版本(包括其他解释器和2.x分支)的项目,virtualenv似乎是最佳选择。

在大多数情况下,软件实现之所以可以快速迭代,是因为开发人员复用了大量现有组件。不要重复你自己(Don't Repeat Yourself),这已经成为许多程序员的通用准则和座右铭。将其他包和模块用在代码库中只是这种文化的一部分。二进制库、数据库、系统服务、第三方API等也应该被当作“可复用组件”。甚至整个操作系统都是可复用的。

基于Web应用的后端服务是一个超级复杂的应用实例。最简单的软件栈(software stack)通常由几层组成(从最底层开始):

当然,这些软件可以进一步简化,但实际上是不可能的。事实上,大型应用往往复杂到难以区分每一层。大型应用会用到多种不同的数据库,被分为多个独立进程,还会用到许多其他系统服务来进行缓存、队列、记录日志、服务发现等等。遗憾的是,复杂度没有上限,代码似乎只是遵循热力学第二定律而已。

真正重要的是,并非所有的软件栈元素都可以在Python运行环境的层面进行隔离。无论是HTTP服务器(例如NGINX)还是关系型数据库管理系统(RDBMS,例如PostgreSQL),在不同的系统上通常都有不同的版本。如果没有合适的工具,很难保证开发团队中每个人使用的每个组件的版本完全相同。如果团队中所有开发人员都在开发同一个项目的话,那么所有人可能会在开发工具箱上获得相同版本的服务,这在理论上是可能的。但如果他们使用的操作系统与生产环境不同的话,所有这些努力都是徒劳的。当然也不可能强迫程序员在并非本人最喜欢的系统上工作。

问题在于,可移植性仍然是一项巨大的挑战。在生产环境中,并非所有服务的运行方式都和在开发人员电脑上完全相同,而且这一点不可能改变。即使是Python在跨平台方面付出了巨大的努力,但在不同系统上的行为也会有所不同。通常来说,这些情况都有详细的文档,只有直接进行系统调用时才会发生。但是,靠程序员的记忆力来记住一长串兼容性问题,是很容易出错的。

这个问题的常见解决方法就是将整个系统隔离为应用程序环境。一般可以利用各种类型的系统虚拟化工具来实现。当然,虚拟化会降低性能,但是现代计算机的硬件都支持虚拟化,性能损失通常可以忽略不计。另一方面,可能的好处却有很多,如下所示。

目前,Vagrant似乎是最流行的工具,用一种简单方便的方法来创建并管理开发环境。它可用于Windows、Mac OS和一些常见的Linux发行版,没有任何其他依赖。Vagrant以虚拟机或容器的形式来创建新的开发环境。具体实现取决于虚拟化供应商(provider)。VirtualBox是与Vagrant安装程序绑定的默认供应商,但也有其他供应商。最有名的供应商是VMware、Docker、LXC(Linux Containers)和Hyper-V。

Vagrant最重要的配置是一个名为Vagrantfile的文件。每个项目的这个文件都应该是独立的。该文件中最重要的内容如下所示。

Vagrantfile的语法语言是Ruby。示例配置文件提供了用于启动项目的优秀模板,并且还有详细的文档,因此无需掌握这种语言的知识。用一行命令就可以创建模板配置文件:

vagrant init

这一命令会在当前工作目录下创建一个名为Vagrantfile的新文件。通常最好将这个文件保存在相关项目的根目录下。这个文件已经是一个有效配置,可以利用默认供应商和基础镜像文件(base box)来创建新的虚拟机。默认不启用环境搭建(provisioning)。添加完Vagrantfile后,利用下面这个命令可以启动新的虚拟机:

vagrant up

初始启动可能需要几分钟的时间,因为需要从网上下载box文件。还有一些初始化过程可能要花费一些时间,这取决于使用的供应商、box文件和每次打开现有虚拟机时的系统性能。通常来说,这个过程只需要几秒。一旦启动并运行了新的Vagrant环境,开发者可以利用下面这个简短的命令连接SSH:

vagrant ssh

在项目源代码树中,在Vagrantfile之下的任何位置都可以运行这一命令。为了方便开发人员,我们会在上层目录中查找配置文件,并与相应的虚拟机实例进行匹配。然后它会建立安全的shell连接,可以像任何普通远程机器一样与开发环境进行交互。唯一的区别在于,整个项目的源代码树(根目录是Vagrantfile所在的位置)是在虚拟机文件系统的/vagrant/目录下。

容器是全机器虚拟化的替代方法。它是轻量级的虚拟化方法,内核与操作系统允许运行多个隔离的用户空间实例。容器和主机之间共享操作系统(OS),因此从理论上来说,这种方法的开销比完全虚拟化要少。这样的容器只包含应用程序代码和系统级的依赖,但从内部运行进程的角度来看,它看起来像一个完全隔离的系统环境。

软件容器之所以流行,主要是因为Docker,这是容器的可用实现之一。Docker可以用名为Dockerfile的简单文本文件的形式来描述其容器。可以创建并存储这样定义的容器。它还支持增量修改,如果向容器中添加了新的内容,无需从头重新创建。

像Docker和Vagrant这样不同的工具在功能上似乎有所交叉,但二者主要的区别在于构建这些工具的原因。如前所述,构建Vagrant主要用作开发工具。用一行命令就可以引导启动整个虚拟机,但无法原样打包并部署或发布。另一方面,Docker正是为此而创建的,可以将整个容器打包,发送到生产环境中并部署。如果顺利实现的话,这可以大大改进产品部署的过程。因此,只有Docker和类似的解决方案(例如Rocket)还要用于生产环境的部署过程时,在开发过程中使用这些方法才是有意义的。将Docker仅用于开发过程的隔离,可能会产生过大开销,还会有不一致的缺点。

生产力工具是一个模糊的术语。一方面,几乎所有在线发布的开源代码包都是一种生产力提升工具。它们为某些问题提供了现成的解决方案,因此人们不必再浪费时间(理想情况下)。另一方面,可以说整个Python都是关于生产力的。两种说法都没有错。Python这种语言的一切及其社区几乎都是为了尽可能高效地开发软件而设计的。

这就建立了一个正反馈循环。由于写代码简单又有趣,所以很多程序员用空闲时间创建工具,使写代码变得更加简单更加有趣。基于这一事实,这里为生产力工具给出一个非常主观而且不科学的定义:使开发过程更加简单、更加有趣的一款软件。

从定义来看,生产力工具主要关注开发过程中的某些特定环节,例如测试、调试和包管理,并不是所构建产品的核心部分。在某些情况下,虽然每天都会用到这些工具,但它们甚至不会出现在项目的代码库中。

最重要的生产力工具是pipvenv,本章前面已经讨论过了。有些生产力工具可以解决特定的问题(如分析和测试),本书有专门的章节来介绍。本节主要介绍一些其他章节没有提到而又十分值得推荐的工具。

Python程序员在交互式解释器会话上花费了大量时间。它非常适合测试短代码片段、访问文档、甚至在运行时调试代码。默认的Python交互式会话非常简单,并没有类似tab补全或代码内省助手(code introspection helper)的许多功能。幸运的是,对默认Python shell的扩展和定制是非常简单的。

用一个启动文件就可以配置交互式提示符。Python在启动时会寻找PYTHONSTARTUP环境变量,并执行这一变量指向的文件中的代码。有些Linux发行版提供了默认的启动脚本,一般位于主目录中,名为.pythonstartup。通常会提供tab补全功能和命令历史记录来加强提示符,这些功能是基于readline模块。(你需要安装readline库)。

如果你没有这样的文件,创建一个也很容易。下面是最简单的启动文件示例,添加了键补全功能和显示历史记录:

   # python启动文件
   import readline
   import rlcompleter
   import atexit
   import os

   # tab补全
   readline.parse_and_bind('tab: complete')

   # 历史记录
   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变量,其值为该文件的路径。

1.设置PYTHONSTARTUP环境变量

如果你用的是Linux或MAC OS X系统,最简单的方法就是在主文件夹中创建启动脚本。然后将它与系统shell启动脚本中的PYTHONSTARTUP环境变量链接在一起。举个例子,Bash和Korn shell用的都是.profile文件,你可以在里面插入这样一行:

export PYTHONSTARTUP=~/.pythonstartup

如果你用的是Windows,做法也很简单:以管理员身份在系统首选项中设置新的环境变量,然后将脚本保存在常用文件夹,不要使用特定的用户文件夹。

编写PYTHONSTARTUP脚本可能是一项很好的练习,但独自创建优秀的自定义shell却是一项很少人有时间完成的挑战。幸运的是,已经有一些自定义Python shell的实现,可以极大地提高Python交互式会话的体验。

2.IPython

IPython提供了一个扩展的Python命令行shell。它的功能很多,其中最有趣的功能如下所示。

现在,IPython已经成为大型项目Jupyter的一部分,该项目提供了实时代码的交互式notebook,支持多种不同的语言。

3.bpython

bpython自称Python解释器的优秀界面。下面是项目主页上重点强调的一些功能,如下所示。

4.ptpython

ptpython是另一款高级的Python shell。在这个项目中,核心提示符应用的实现是一个叫作prompt_toolkit的独立包(来自同一作者)。这样你可以轻松创建各种美观的交互式命令行界面。

通常会将ptpython与bpython在功能上进行比较,但主要区别在于,ptpython能够与IPython及其某些语法兼容,利用这些语法可以实现一些附加功能,例如%pdb%cpaste%profile

代码调试是软件开发过程中的重要环节。许多程序员浪费大量时间,仅使用大量的日志记录和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可能会有点难以适应,因为诸如hbsnjr这样的单字母调试命令会让人不知所云。每当有疑问时,在调试会话期间输入help pdb命令,会给出大量的用法和附加信息。

pdb中的调试会话也非常简单,并没有提供类似tab补全或代码高亮之类的附加功能。幸运的是,PyPI上有几个包可以在上节提到的Python shell中实现这些功能。最有名的例子是。

互联网为Python开发者提供了丰富的有用资源。前面已经提到过,但这里我们再重复一遍,最重要的也是最显而易见的资源如下所示。

类似书籍和教程之类的其他资源也很有用,但往往很快就会过时。社区积极维护的资源或者定期发布的资源,都是不会过时的。最值得推荐的是下面两个:

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

本章从Python 2和3之间的主题差异开始讲起,并针对目前Python社区撕裂为两大阵营的现状给出了应对建议。然后介绍了Python开发的现代方法,令人吃惊的是,开发这些方法主要是由于这种语言两大版本之间令人遗憾的撕裂。这些方法主要是解决环境隔离问题。本章最后对常用的生产力工具做了简短的总结,并提供了一些常用资源,以供进一步参考。

[1] 在这个网站上,如果某个软件包被标为绿色,则表示它支持Python 3,红色则表示不支持。——译者注


编写高效语法的能力会随着时间逐步提高。回头看看写的第一个程序,你可能就会同意这个观点。正确的语法看起来赏心悦目,而错误的语法则令人烦恼。

除了实现的算法与程序架构设计之外,还要特别注意的是,程序的写法也会严重影响它未来的发展。许多程序被丢弃并从头重写,就是因为难懂的语法、不清晰的API或不合常理的标准。

不过Python在最近几年里发生了很大变化。因此,如果你被邻居(一个爱嫉妒的人,来自本地Ruby开发者用户组)绑架了一段时间,并且远离新闻,那么你可能会对Python的新特性感到吃惊。从最早版本到目前的3.5版,这门语言已经做了许多改进,变得更加清晰、更加整洁、也更容易编写。Python基础知识并没有发生很大变化,但现在使用的工具更符合人们的使用习惯。

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

速度提升或内存使用的代码性能技巧将会在第11、12章中讲述。

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以及可变的bytearraystr不同,只能用字节作为序列值,即0 <= x < 256范围内的整数。一开始可能会有点糊涂,因为其打印结果与字符串非常相似:

>>> print(bytes([102, 111, 111]))
b'foo'

对于bytesbytearray,在转换为另一种序列类型(例如listtuple)时可以显示出其本来面目:

>>> 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中没有任何语法上的意义。

前面的一些例子中已经提到过字节,但为了保持前后一致,我们来明确介绍它的语法。字节也被单引号、双引号或三引号包围,但必须有一个bB前缀:

>>> type(b"some bytes")
< class 'bytes' >

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

最后同样重要的是,Unicode字符串中包含无法用字节表示的“抽象”文本。因此,如果Unicode字符串没有被编码为二进制数据的话,是无法保存在磁盘中或通过网络发送的。将字符串对象编码为字节序列的方法有两种:

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

 

命名——字节与字节字符串的对比 

由于Python 3中的变化,有些人倾向于将bytes实例称为字节字符串。这主要是由于历史原因——Python 3中的bytes是与Python 2中的str类型最为接近的序列类型(但并不完全相同)。不过bytes实例是字节序列,也不需要表示文本数据。所以为了避免混淆,虽然bytes实例与字符串具有相似性,但建议始终将其称为bytes或字节序列。Python 3中字符串的概念是为文本数据准备的,现在始终是str类型。

1.实现细节

Python字符串是不可变的。字节序列也是如此。这一事实很重要,因为它既有优点又有缺点。它还会影响Python高效处理字符串的方式。由于不变性,字符串可以作为字典的键或set的元素,因为一旦初始化之后字符串的值就不会改变。另一方面,每当需要修改过的字符串时(即使只是微小的修改),都需要创建一个全新的字符串实例。幸运的是,bytearraybytes的可变版本,不存在这样的问题。字节数组可以通过元素赋值来进行原处修改(无需创建新对象),其大小也可以像列表一样动态地变化(利用appendpopinseer等方法)。

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()的性能可能还不如利用加法的普通拼接,下面举几个例子。

最后,如果事先知道字符串的数目,可以用正确的字符串格式化方法来保证字符串拼接的最佳可读性。字符串格式化可以用str.format()方法或%运算符。如果代码段的性能不是很重要,或者优化字符串拼接节省的开销很小,那么推荐使用字符串格式化作为最佳方法。

 

常数折叠和窥孔优化程序 

CPython对编译过的源代码使用窥孔优化程序来提高其性能。这种优化程序直接对Python字节码实现了许多常见的优化。如上所述,常数折叠就是其功能之一。生成常数的长度不得超过一个固定值。在Python 3.5中这个固定值仍然是 20。不管怎样,这个具体细节只是为了满足读者的好奇心而已,并不能在日常编程中使用。窥孔优化程序还实现了许多有趣的优化,详细信息请参见Python源代码中的Python/peephole.c文件。

Python提供了许多内置的数据集合类型,如果选择明智的话,可以高效解决许多问题。你可能已经学过下面这些集合类型,它们都有专门的字面值,如下所示。

Python的集合类型当然不止这4种,它的标准库扩展了其可选列表。在许多情况下,问题的答案可能正如选择正确的数据结构一样简单。本书的这一部分将深入介绍各种集合类型,以帮你做出更好的选择。

1.列表与元组

Python最基本的两个集合类型就是列表与元组,它们都表示对象序列。只要是花几小时学过Python的人,应该都很容易发现二者之间的根本区别:列表是动态的,其大小可以改变;而元组是不可变的,一旦创建就不能修改。

虽然快速分配/释放小型对象的优化方法有很多,但对于元素位置本身也是信息的数据结构来说,推荐使用元组这一数据类型。举个例子,想要保存(x, y)坐标对,元组可能是一个很好的选择。反正关于元组的细节相当无趣。本章关于元组唯一重要的内容就是,tuple不可变的(immutable),因此也是可哈希的(hashable)。其具体含义将会在后面“字典”一节介绍。比元组更有趣的是另一种动态的数据结构list,以及它的工作原理和高效处理理方式。

(1)实现细节

许多程序员容易将Python的list类型与其他语言(如C、C++或Java)标准库中常见的链表的概念相混淆。事实上,CPython的列表根本不是列表。在CPython中,列表被实现为长度可变的数组。对于其他Python实现(如Jython和IronPython)而言,这种说法应该也是正确的,虽然这些项目的文档中没有记录其实现细节。造成这种混淆的原因很清楚。这种数据类型被命名为列表,还和链表实现有相似的接口。

为什么这一点很重要,这又意味着什么呢?列表是最常见的数据结构之一,其使用方式会对所有应用的性能带来极大影响。此外,CPython又是最常见也最常用的Python实现,所以了解其内部实现细节至关重要。

从细节上来看,Python中的列表是由对其他对象的引用组成的的连续数组。指向这个数组的指针及其长度被保存在一个列表头结构中。这意味着,每次添加或删除一个元素时,由引用组成的数组需要改变大小(重新分配)。幸运的是,Python在创建这些数组时采用了指数过分配(exponential over-allocation),所以并不是每次操作都需要改变数组大小。这也是添加或取出元素的平摊复杂度较低的原因。不幸的是,在普通链表中“代价很小”的其他一些操作在Python中的计算复杂度却相对较高:

这里n是列表的长度。至少利用索引来查找或修改元素的时间开销与列表大小无关。表2-1是一张完整的表格,列出了大多数列表操作的平均时间复杂度。

表2-1

操作

复杂度

复制

O(n)

添加元素

O(1)

插入元素

O(n)

获取元素

O(1)

修改元素

O(1)

删除元素

O(n)

遍历

O(n)

获取长度为k的切片

O(k)

删除切片

O(n)

修改长度为k的切片

O(k+n)

列表扩展(Extend)

O(k)

乘以k

O(nk)

测试元素是否在列表中(element in list)

O(n)

min()/max()

O(n)

获取列表长度

O(1)

对于需要真正的链表(或者简单来说,双端appendpop操作的复杂度都是O(1)的数据结构)的场景,Python在内置的collections模块中提供了deque(双端队列)。它是栈和队列的一般化,在需要用到双向链表的地方都可以使用这种数据结构。

(2)列表推导

你可能知道,编写这样的代码是很痛苦的:

>>> evens = []
>>> for i in range(10):
... if i % 2 == 0:
... evens.append(i)
...
>>> evens
[0, 2, 4, 6, 8]

这种写法可能适用于C语言,但在Python中的实际运行速度很慢,原因如下。

列表推导正是解决这个问题的正确方法。它使用编排好的功能对上述语法的一部分做了自动化处理:

>>> [i for i in range(10) if i % 2 == 0]
[0, 2, 4, 6, 8]

这种写法除了更加高效之外,也更加简短,涉及的语法元素也更少。在大型程序中,这意味着更少的错误,代码也更容易阅读和理解。

 

列表推导和内部数组调整大小 

有些Python程序员中会谣传这样的说法:每添加几个元素之后都要对表示列表对象的内部数组大小进行调整,这个问题可以用列表推导来解决。还有人说一次分配就可以将数组大小调整到刚刚好。不幸的是,这些说法都是不正确的。

解释器在对列表推导进行求值的过程中并不知道最终结果容器的大小,也就无法为它预先分配数组的最终大小。因此,内部数组的重新分配方式与for循环中完全相同。但在许多情况下,与普通循环相比,使用列表推导创建列表要更加整洁、更加快速。

(3)其他习语

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)

另一个常用的语法元素是序列解包(sequence unpacking)。这种方法并不限于列表和元组,而是适用于任意序列类型(甚至包括字符串和字节序列)。只要赋值运算符左边的变量数目与序列中的元素数目相等,你都可以用这种方法将元素序列解包到另一组变量中:

>>> first, second, third = "foo", "bar", 100
>>> first
'foo'
>>> second
'bar'
>>> third
100

解包还可以利用带星号的表达式获取单个变量中的多个元素,只要它的解释没有歧义即可。还可以对嵌套序列进行解包。特别是在遍历由序列构成的复杂数据结构时,这种方法非常实用。下面是一些更复杂的解包示例:

>>> # 带星号的表达式可以获取序列的剩余部分
>>> first, second, *rest = 0, 1, 2, 3
>>> first
0
>>> second
1
>>> rest
[2, 3]

>>> # 带星号的表达式可以获取序列的中间部分
>>> first, *inner, last = 0, 1, 2, 3
>>> first
0
>>> inner
[1, 2]
>>> last
3

>>> # 嵌套解包
>>> (a, b), (c, d) = (1, 2), (3, 4)
>>> a, b, c, d
(1, 2, 3, 4)

2.字典

字典是Python中最通用的数据结构之一。dict可以将一组唯一键映射到对应的值,如下所示:

   {
       1: ' one',
       2: ' two',
       3: ' three',
   }

字典是你应该已经了解的基本内容。不管怎样,程序员还可以用和前面列表推导类似的推导来创建一个新的字典。这里有一个非常简单的例子如下所示:

   squares = {number: number**2 for number in range(100)}

重要的是,使用字典推导具有与列表推导相同的优点。因此在许多情况下,字典推导要更加高效、更加简短、更加整洁。对于更复杂的代码而言,需要用到许多if语句或函数调用来创建一个字典,这时最好使用简单的for循环,尤其是它还提高了可读性。

对于刚刚接触Python 3的Python程序员来说,在遍历字典元素时有一点需要特别注意。字典的keys()values()items()3个方法的返回值类型不再是列表。此外,与之对应的iterkeys()itervalues()iteritems()本来返回的是迭代器,而Python 3中并没有这3个方法。现在keys()values()items()返回的是视图对象(view objects)。

视图对象可以动态查看字典的内容,因此每次字典发生变化时,视图都会相应改变,见下面这个例子:

>>> words = {'foo': 'bar', 'fizz': 'bazz'}
>>> items = words.items()
>>> words['spam'] = 'eggs'
>>> items
dict_items([('spam', 'eggs'), ('fizz', 'bazz'), ('foo', 'bar')])

视图对象既有旧的keys()values()items()方法返回的列表的特性,也有旧的iterkeys()itervalues()iteritems()方法返回的迭代器的特性。视图无需冗余地将所有值都保存在内存里(像列表那样),但你仍然可以获取其长度(使用len),也可以测试元素是否包含其中(使用in子句)。当然,视图是可迭代的。

最后一件重要的事情是,在keys()values()方法返回的视图中,键和值的顺序是完全对应的。在Python 2中,如果你想保证获取的键和值顺序一致,那么在两次函数调用之间不能修改字典的内容。现在dict _ keysdict _ values是动态的,所以即使在调用keys()values()之间字典内容发生了变化,那么这两个视图的元素遍历顺序也是完全一致的。

(1)实现细节

CPython使用伪随机探测(pseudo-random probing)的散列表(hash table)作为字典的底层数据结构。这似乎是非常高深的实现细节,但在短期内不太可能发生变化,所以程序员也可以把它当做一个有趣的事实来了解。

由于这一实现细节,只有可哈希的(hashable)对象才能作为字典的键。如果一个对象有一个在整个生命周期都不变的散列值(hash value),而且这个值可以与其他对象进行比较,那么这个对象就是可哈希的。Python所有不可变的内置类型都是可哈希的。可变类型(如列表、字典和集合)是不可哈希的,因此不能作为字典的键。定义可哈希类型的协议包括下面这两个方法。

如果两个对象相等,那么它们的散列值一定相等。反之则不一定成立。这说明可能会发生散列冲突(hash collision),即散列值相等的两个对象可能并不相等。这是允许的,所有Python实现都必须解决散列冲突。CPython用开放定址法(open addressing)来解决这一冲突(https://en.wikipedia.org/wiki/Open_addressing)。不过,发生冲突的概率对性能有很大影响,如果概率很高,字典将无法从其内部优化中受益。

字典的3个基本操作(添加元素、获取元素和删除元素)的平均时间复杂度为O(1),但它们的平摊最坏情况复杂度要高得多,为O(n),这里的n是当前字典的元素数目。此外,如果字典的键是用户自定义类的对象,并且散列方法不正确的话(发生冲突的风险很大),那么这会给字典性能带来巨大的负面影响。CPython字典的时间复杂度的完整表格如表2-2所示。

表2-2

操作

平均复杂度

平摊最坏情况复杂度

获取元素

O(1)

O(n)

修改元素

O(1)

O(n)

删除元素

O(1)

O(n)

复制

O(n)

O(n)

遍历

O(n)

O(n)

还有很重要的一点需要注意,在复制和遍历字典的操作中,最坏情况复杂度中的n是字典曾经达到的最大元素数目,而不是当前元素数目。换句话说,如果一个字典曾经元素个数很多,后来又大大减少了,那么遍历这个字典可能要花费相当长的时间。因此在某些情况下,如果需要频繁遍历某个字典,那么最好创建一个新的字典对象,而不是仅在旧字典中删除元素。

(2)缺点和替代方案

使用字典的常见陷阱之一,就是它并不会按照键的添加顺序来保存元素的顺序。在某些情况下,字典的键是连续的,对应的散列值也是连续值(例如整数),那么由于字典的内部实现,元素的顺序可能和添加顺序相同:

>>> {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'])

OrderedDict还有一些其他功能,例如利用popitem()方法在双端取出元素或者利用move _ to _ end()方法将指定元素移动到某一端。这种集合类型的完整参考可参见Python文档(https://docs.python.org/3/library/collections.html)。

还有很重要的一点是,在非常老的代码库中,可能会用dict来实现原始的集合,以确保元素的唯一性。虽然这种方法可以给出正确的结果,但只有在低于2.3的Python版本中才予以考虑。字典的这种用法十分浪费资源。Python有内置的set类型专门用于这个目的。事实上,CPython中set的内部实现与字典非常类似,但还提供了一些其他功能,以及与集合相关的特定优化。

3.集合

集合是一种鲁棒性很好的数据结构,当元素顺序的重要性不如元素的唯一性和测试元素是否包含在集合中的效率时,大部分情况下这种数据结构是很有用的。它与数学上的集合概念非常类似。Python的内置集合类型有两种。

由于frozenset()具有不变性,它可以用作字典的键,也可以作为其他set()frozenset()的元素。在一个set()frozenset()中不能包含另一个普通的可变set(),因为这会引发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})})

创建可变集合方法有以下3种,如下所示。

注意,使用集合的字面值和推导要格外小心,因为它们在形式上与字典的字面值和推导非常相似。此外,空的集合对象是没有字面值的。空的花括号{}表示的是空的字典字面值。

实现细节

CPython中的集合与字典非常相似。事实上,集合被实现为带有空值的字典,只有键才是实际的集合元素。此外,集合还利用这种没有值的映射做了其他优化。

由于这一点,可以快速向集合添加元素、删除元素或检查元素是否存在,平均时间复杂度均为O(1)。但由于CPython的集合实现依赖于类似的散列表结构,因此这些操作的最坏情况复杂度是O(n),其中n是集合的当前大小。

字典的其他实现细节也适用于集合。集合中的元素必须是可哈希的,如果集合中用户自定义类的实例的散列方法不佳,那么将会对性能产生负面影响。

4.超越基础集合类型——collections模块

每种数据结构都有其缺点。没有一种集合类型适合解决所有问题,4种基本类型(元组、列表、集合和字典)提供的选择也不算多。它们是最基本也是最重要的集合类型,都有专门的语法。幸运的是,Python标准库内置的collections模块提供了更多的选择。前面已经提到过其中一种(deque)。下面是这个模块中最重要的集合类型。

 

第12章介绍了从collections模块选择集合类型的更多细节,也给出了关于何时使用这些集合类型的建议。

在一种语言中,很难客观判断哪些语法元素属于高级语法。对于本章会讲到的高级语法元素,我们会讲到这样的元素,它们不与任何特定的内置类型直接相关,而且在刚开始学习时相对难以掌握。对于Python中难以理解的特性,其中最常见的是:

迭代器只不过是一个实现了迭代器协议的容器对象。它基于以下两个方法。

迭代器可以利用内置的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语句,生成器可以暂停函数并返回一个中间结果。该函数会保存执行上下文,稍后在必要时可以恢复。

举个例子,斐波纳契(Fibonacci)数列可以用生成器语法来实现。下列代码是来自于PEP 255(简单生成器)文档中的例子:

   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模块可以从文本流中生成令牌(token),并对处理过的每一行都返回一个迭代器,以供后续处理:

>>> 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

 

保持代码简单,而不是保持数据简单  

最好编写多个处理序列值的简单可迭代函数,而不要编写一个复杂函数,同时计算出整个集合的结果。

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的返回值。因此,这个函数可以根据客户端代码来改变自身行为。为完成这一行为,还添加了另外两个函数:throwclose。它们将向生成器抛出错误。

 

生成器是Python中协程、异步并发等其他概念的基础,这些概念将在第13章介绍。

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")

1.一般语法和可能的实现

装饰器通常是一个命名的对象(不允许使用lambda表达式),在被(装饰函数)调用时接受单一参数,并返回另一个可调用对象。这里用的是“可调用(callable)”。而不是之前以为的“函数”。装饰器通常在方法和函数的范围内进行讨论,但它的适用范围并不局限于此。事实上,任何可调用对象(任何实现了__ call __方法的对象都是可调用的)都可以用作装饰器,它们返回的对象往往也不是简单的函数,而是实现了自己的__ call __方法的更复杂的类的实例。

装饰器语法只是语法糖而已。看下面这种装饰器用法:

   @some_decorator
   def decorated_function():
       pass

这种写法总是可以替换为显式的装饰器调用和函数的重新赋值:

   def decorated_function():
       pass
   decorated_function = some_decorator(decorated_function)

但是,如果在一个函数上使用多个装饰器的话,后一种写法的可读性更差,也非常难以理解。

 

装饰器甚至不需要返回可调用对象! 

事实上,任何函数都可以用作装饰器,因为Python并没有规定装饰器的返回类型。因此,将接受单一参数但不返回可调用对象的函数(例如str)用作装饰器,在语法上是完全有效的。如果用户尝试调用这样装饰过的对象,最后终究会报错。不管怎样,针对这种装饰器语法可以做一些有趣的试验。

(1)作为一个函数

编写自定义装饰器有许多方法,但最简单的方法就是编写一个函数,返回包装原始函数调用的一个子函数。

通用模式如下:

   def mydecorator(function):
       def wrapped(*args, **kwargs):
           # 在调用原始函数之前,做点什么
           result = function(*args, **kwargs)
           # 在函数调用之后,做点什么,
           # 并返回结果
           return result
       # 返回wrapper作为装饰函数
       return wrapped
(2)作为一个类

虽然装饰器几乎总是可以用函数实现,但在某些情况下,使用用户自定义类可能更好。如果装饰器需要复杂的参数化或者依赖于特定状态,那么这种说法往往是对的。

非参数化装饰器用作类的通用模式如下:

   class DecoratorAsClass:
       def __init__(self, function):
           self.function = function

       def __call__(self, *args, **kwargs):
           # 在调用原始函数之前,做点什么
           result = self.function(*args, **kwargs)
           # 在调用函数之后,做点什么,
           # 并返回结果
           return result
(3)参数化装饰器

在实际代码中通常需要使用参数化的装饰器。如果用函数作为装饰器的话,那么解决方法很简单:需要用到第二层包装。下面一个简单的装饰器示例,给定重复次数,每次被调用时都会重复执行一个装饰函数:

   def repeat(number=3):
       """多次重复执行装饰函数。

       返回最后一次原始函数调用的值作为结果
       :param number: 重复次数,默认值是3
       """
       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'
(4)保存内省的装饰器

使用装饰器的常见错误是在使用装饰器时不保存函数元数据(主要是文档字符串和原始函数名)。前面所有示例都存在这个问题。装饰器组合创建了一个新函数,并返回一个新对象,但却完全没有考虑原始函数的标识。这将会使得调试这样装饰过的函数更加困难,也会破坏可能用到的大多数自动生成文档的工具,因为无法访问原始的文档字符串和函数签名。

但我们来看一下细节。假设我们有一个虚设的(dummy)装饰器,仅有装饰作用,还有其他一些被装饰的函数:

   def dummy_decorator(function):
       def wrapped(*args, **kwargs):
           """包装函数内部文档。"""
           return function(*args, **kwargs)
       return wrapped

   @dummy_decorator
   def function_with_important_docstring():
       """这是我们想要保存的重要文档字符串。"""

如果我们在Python交互式会话中查看function _ with _ important _ docstring(),会注意到它已经失去了原始名称和文档字符串:

>>> function_with_important_docstring.__name__
'wrapped'
>>> function_with_important_docstring.__doc__
'包装函数内部文档。'

解决这个问题的正确方法,就是使用functools模块内置的wraps()装饰器:

   from functools import wraps

   def preserving_decorator(function):
       @wraps(function)
       def wrapped(*args, **kwargs):
           """包装函数内部文档。"""
           return function(*args, **kwargs)
       return wrapped

   @preserving_decorator
   def function_with_important_docstring():
       """这是我们想要保存的重要文档字符串。"""

这样定义的装饰器可以保存重要的函数元数据:

>>> function_with_important_docstring.__name__
'function_with_important_docstring.'
>>> function_with_important_docstring.__doc__
'这是我们想要保存的重要文档字符串。'

2.用法和有用的例子

由于装饰器在模块被首次读取时由解释器来加载,所以它们的使用应受限于通用的包装器(wrapper)。如果装饰器与方法的类或所增强的函数签名绑定,那么应该将其重构为常规的可调用对象,以避免复杂性。在任何情况下,装饰器在处理API时,一个好的做法是将它们聚集在一个易于维护的模块中。

常见的装饰器模式如下所示。

(1)参数检查

检查函数接受或返回的参数,在特定上下文中执行时可能有用。举个例子,如果一个函数要通过XML-RPC来调用,那么Python无法像静态语言那样直接提供其完整签名。当XML-RPC客户端请求函数签名时,就需要用这个功能来提供内省能力。

 

XML-RPC协议

XML-RPC协议是一种轻量级的远程过程调用(Remote Procedure Call)协议,通过HTTP使用XML对调用进行编码。对于简单的客户端-服务器交换,通常使用这种协议而不是SOAP。SOAP提供了列出所有可调用函数的页面(WSDL),XML-RPC与之不同,并没有可用函数的目录。该协议提出了一个扩展,可以用来发现服务器API,Python的xmlrpc模块实现了这一扩展(参见https://docs.python.org/3/library/xmlrpc.server.html)。

自定义装饰器可以提供这种类型的签名,并确保输入和输出代表自定义的签名参数:

   rpc_info = {}

   def xmlrpc(in_=(), out=(type(None),)):
       def _xmlrpc(function):
           # 注册签名
           func_name = function.__name__
           rpc_info[func_name] = (in_, out)
           def _check_types(elements, 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))

           # 包装过的函数
           def __xmlrpc(*args):  # 没有允许的关键词
               # 检查输入的内容
               checkable_args = args[1:]  # 去掉self
               _check_types(checkable_args, in_)
               # 运行函数
               res = function(*args)
               # 检查输出的内容
               if not type(res) in (tuple, list):
                   checkable_res = (res,)
               else:
                   checkable_res = res
               _check_types(checkable_res, out)

               # 函数及其类型检查成功
               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'>), (,))}
>>> 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'>
(2)缓存

缓存装饰器与参数检查十分相似,不过它重点是关注那些内部状态不会影响输出的函数。每组参数都可以链接到唯一的结果。这种编程风格是函数式编程(functional programming,参见https://en.wikipedia.org/wiki/Functional_programming)的特点,当输入值有限时可以使用。

因此,缓存装饰器可以将输出与计算它所需要的参数放在一起,并在后续的调用中直接返回它。这种行为被称为memoizing(参见https://en.wikipedia.org/wiki/Memoization),很容易被实现为一个装饰器:

   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)

               # 是否已经拥有它了?
               if (key in cache and
                   not is_obsolete(cache[key], duration)):
                   print('we got a winner')
                   return cache[key]['value']
               # 计算
               result = function(*args, **kw)
               # 保存结果
               cache[key] = {
                   'value': result,
                   'time': time.time()
               }
               return result
           return __memoize
       return _memoize

利用已排序的参数值来构建SHA哈希键,并将结果保存在一个全局字典中。利用pickle来建立hash,这是冻结所有作为参数传入的对象状态的快捷方式,以确保所有参数都满足要求。举个例子,如果用一个线程或套接字作为参数,那么会引发PicklingError(参见https://docs.python.org/3/library/pickle.html)。duration参数的作用是,如果上一次函数调用已经过去了太长时间,那么它会使缓存值无效。

下面是一个使用示例:

>>> @memoize()
... 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
>>> @memoize(1) # 1秒后令缓存失效
... 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

缓存代价高昂的函数可以显著提高程序的总体性能,但必须小心使用。缓存值还可以与函数本身绑定,以管理其作用域和生命周期,代替集中化的字典。但在任何情况下,更高效的装饰器会使用基于高级缓存算法的专用缓存库。

 

第12章将会介绍与缓存相关的详细信息和技术。

(3)代理

代理装饰器使用全局机制来标记和注册函数。举个例子,一个根据当前用户来保护代码访问的安全层可以使用集中式检查器和相关的可调用对象要求的权限来实现:

   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
(4)上下文提供者

上下文装饰器确保函数可以运行在正确的上下文中,或者在函数前后运行一些代码。换句话说,它设定并复位一个特定的执行环境。举个例子,当一个数据项需要在多个线程之间共享时,就要用一个锁来保护它避免多次访问。这个锁可以在装饰器中编写,代码如下:

   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():  # 确保锁定资源
       pass

上下文装饰器通常会被上下文管理器(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模块的类:

一般语法和可能的实现

with语句的一般语法的最简单形式如下:

   with context_manager:
       # 代码块
       ...

此外,如果上下文管理器提供了上下文变量,可以用as子句保存为局部变量:

   with context_manager as context:
       # 代码块
       ...

注意,多个上下文管理器可以同时使用,如下所示:

   with A() as a, B() as b:
       ...

这种写法等价于嵌套使用,如下所示:

   with A() as a:
       with B() as b:
           ...
(1)作为一个类

任何实现了上下文管理器协议(context manager protocol)的对象都可以用作上下文管理器。该协议包含两个特殊方法。

简而言之,执行with语句的过程如下:

__ exit __接受代码块中出现错误时填入的3个参数。如果没有出现错误,那么这3个参数都被设为None。出现错误时,__ exit __不应该重新引发这个错误,因为这是调用者(caller)的责任。但它可以通过返回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'
(2)作为一个函数——contextlib模块

使用类似乎是实现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)
           # 需要再次抛出异常
           raise
       else:
           print('leaving context')
           print('with no error')

如果出现任何异常,该函数都需要再次抛出这个异常,以便传递它。注意,context _ illustration在需要时可以有一些参数,只要在调用时提供这些参数即可。这个小的辅助函数简化了常规的基于类的上下文API,正如生成器对基于类的迭代器API的作用一样。

这个模块还提供了其他3个辅助函数。

Python语法中有一些元素不太常见,也很少用到。这是因为它们能提供的好处很少,或者它们的用法很难记住。因此,许多Python程序员(即使有多年的经验)完全不知道这些语法元素的存在。其中最有名的例子如下:

for循环之后使用else子句,可以在循环“自然”结束而不是被break语句终止时执行一个代码块:

>>> for number in range(1):
... break
... else:
... print("no break")
...
>>>
>>> for number in range(1):
... pass
... else:
... print("break")
...
break

这一语句在某些情况下很有用,因为它有助于删除一些“哨兵(sentinel)”变量,如果出现break时用户想要保存信息,可能会需要这些变量。这使得代码更加清晰,但可能会使不熟悉这种语法的程序员感到困惑。有人说else子句的这种含义是违反直觉的,但这里介绍一个简单的技巧,可以帮你记住它的用法:for循环之后else子句的含义是“没有break”。

函数注解是Python 3最独特的功能之一。官方文档是这么说的:函数注解是关于用户自定义函数使用的类型的完全可选的元信息,但事实上,它并不局限于类型提示,而且在Python及其标准库中也没有单个功能可以利用这种注解。这就是这个功能独特的原因:它没有任何语法上的意义。可以为函数定义注解,并在运行时获取这些注解,但仅此而已。如何使用注解留给开发人员去思考。

1.一般语法

对Python官方文档中的示例稍作修改,就可以很好展示如何定义并获取函数注解:

>>> def f(ham: str, eggs: str = 'eggs') -> str:
... pass
...
>>> print(f.__annotations__)
{'return': < class 'str' >, 'eggs': < class 'str' >, 'ham': < class 'str' >}

如上所述,参数注解的定义为冒号后计算注解值的表达式。返回值注解的定义为表示def语句结尾的冒号与参数列表之后的-&gt;之间的表达式。

定义好之后,注解可以通过函数对象的__ annotations __属性获取,它是一个字典,在应用运行期间可以获取。

任何表达式都可以用作注解,其位置靠近默认参数,这样可以创建一些迷惑人的函数定义,如下所示:

>>> def square(number: 0< =3 and 1=0) - > (\
... +9000): return number**2
>>> square(10)
100

不过,注解的这种用法只会让人糊涂,没有任何其他作用。即使不用注解,编写出难以阅读和理解的代码也是相对容易的。

2.可能的用法

虽然注解有很大的潜力,但并没有被广泛使用。一篇介绍Python 3新增功能的文章(参见https://docs.python.org/3/whatsnew/3.0.html)称,此功能的目的是“鼓励通过元类、装饰器或框架进行试验”。另一方面,作为提议函数注解的官方文档,PEP 3107列出以下可能的使用场景:

虽然函数注解存在的时间和Python 3一样长,但仍然很难找到任一常见且积极维护的包,将函数注解用作类型检查之外的功能。所以函数注解仍主要用于试验和玩耍,这也是Python 3最初发布时包含该功能的最初目的。

本章介绍了不直接与Python类和面向对象编程相关的多个最佳语法实践。本章第一部分重点介绍了与Python序列和集合相关的语法特性,也讨论了字符串和字节相关的序列。本章其余部分介绍了两组独立的语法元素:一组是初学者相对难以理解的(例如迭代器、生成器和装饰器),另一组是鲜为人知的(for...else子句和函数注解)。


相关图书

深度学习的数学——使用Python语言
深度学习的数学——使用Python语言
动手学自然语言处理
动手学自然语言处理
Python高性能编程(第2版)
Python高性能编程(第2版)
图像处理与计算机视觉实践——基于OpenCV和Python
图像处理与计算机视觉实践——基于OpenCV和Python
Web应用安全
Web应用安全
Python数据科学实战
Python数据科学实战

相关文章

相关课程