Core Python Programming 2nd 中文版


Edit By Vheavens  Edit By Vheavens                                对《Python核心编程》的褒奖 “ The long-awaited second edition of Wesley Chun’s Core Python Programming proves to be well worth the wait—its deep and broad coverage and useful exercises will help readers learn and practice good Python.” —Alex Martelli, author of Python in a Nutshell and editor of Python Cookbook “对陈仲才《Python核心编程》第二版的漫长等待是值得的——它的深邃、 它的全面以及书中实用的练习将会帮助读者学会并掌握杰出的Python技能” ——亚历克斯·马特利(Alex Martelli)《Python in a Nutshell》一书作 者,以及《Python Cookbook》的编辑 “There has been lot of good buzz around Wesley Chun’s Core Python Programming. It turns out that all the buzz is well earned. I think this is the best book currently available for learning Python. I would recommend Chun’s book over Learning Python (O’Reilly), Programming Python (O’Reilly), or The Quick Python Book (Manning).” —David Mertz, Ph.D., IBM DeveloperWorks? “之前就听到许多对陈仲才《Python核心编程》的溢美之词,最终这些褒奖 被证明都没有言过其实。我想这是现今学习Python最出色的书籍了。我认为仲才 的书超越《Learning Python》(O’Reilly出版社),《Programming Python》 (O’Reilly社),以及《Quick Python Book》(Manning出版社)” ——大卫·梅尔兹(David Mertz),哲学博士,IBM DeveloperWorks “I have been doing a lot of research [on] Python for the past year and have seen a number of positive reviews of your book. The sentiment expressed confirms the opinion that Core Python Programming is now considered the standard introductory text.” —Richard Ozaki, Lockheed Martin “我在过去的一年里对Python进行了许多研究,并且读到了许多对你作品的 肯定评价。这些评述准确的表达了一个观点,这就是《Python核心编程》现在被 看作是一本权威的Python读本。” ——理查德·尾崎(Richard Ozaki),洛克西德·马丁(Lockheed Martin) Edit By Vheavens  Edit By Vheavens                                公司 “Finally, a book good enough to be both a textbook and a reference on the Python language now exists.” —Michael Baxter, Linux Journal “最终,一本杰出的融教材和参考书目于一身的Python书籍诞生了。。” ——迈克尔·巴克斯特(Michael Baxter),《Linux Journal》 “Very well written. It is the clearest, friendliest book I have come across yet for explaining Python, and putting it in a wider context. It does not presume a large amount of other experience. It does go into some important Python topics carefully and in depth. Unlike too many beginner books, it never condescends or tortures the reader with childish hide-and-seek prose games. [It] sticks to gaining a solid grasp of Python syntax and structure.” —http://python.org bookstore Web site “非常不错的作品。这是我迄今遇到的最清楚最友好的阐释Python的书籍了, 并且它还将Python引入了更广阔的领域。它没有冒昧的大量引用其他经验,而是 谨慎、深入的探索了Python的一些重要主题。与许多入门读物不同,本书不会用 小孩子捉迷藏般的文字游戏折磨或施惠于读者。它紧紧扣住了Python语法和结构 这两个主题。” ——http://python.org 网上书店 “[If ] I could only own one Python book, it would be Core Python Programming by Wesley Chun. This book manages to cover more topics in more depth than Learning Python but includes it all in one book that also more than adequately covers the core language. [If] you are in the market for just one book about Python, I recommend this book. You will enjoy reading it, including its wry programmer’s wit. More importantly, you will learn Python. Even more importantly, you will find it invaluable in helping you in your day-to-day Python programming life. Well done, Mr. Chun!” —Ron Stephens, Python Learning Foundation Edit By Vheavens  Edit By Vheavens                                “(假如)我只能拥有一本Python书籍,那肯定就是陈仲才的《Python核心 编程》。它涵盖了《Learning Python》的全部主题,但是却更加深入宽泛,所以 这绝不单单是一本充分包含了核心语言的书籍。如果你只想在市面上购买一本 Python书籍的话,我向你推荐本书。你会享受阅读的,包括它里面会经常幽程序 员的一默。更重要的是,你将学会Python。更更重要的是,你会在日复一日的编 程生活中受益不尽。好样的,仲才兄!” ——罗恩·斯蒂芬(Ron Stephens),Python学习基金 “I think the best language for beginners is Python, without a doubt. My favorite book is Core Python Programming.” —s003apr, MP3Car.com Forums “毫无疑问的,对于初学者最好的语言我认为就是Python。而《Python核心 编程》则是我的最爱。” ——s003apr,MP3Car.com论坛 “Personally, I really like Python. It’s simple to learn, completely intuitive, amazingly flexible, and pretty darned fast. Python has only just started to claim mindshare in the Windows world, but look for it to start gaining lots of support as people discover it. To learn Python, I’d start with Core PythonProgramming by Wesley Chun.” — Bill Boswell, MCSE, Microsoft Certified Professional Magazine Online “就我个人而言,我喜欢Python。它简单易学、十分直观、具有惊人的灵活 性、而且快到岂有此理!Python刚刚才开始引来了Windows世界应有的关注。但是, 如同人们发现它的过程一般,让这种关注的深入需要足够的支撑。学习Python, 我选择从陈仲才的《Python核心编程》开始。” ——比尔·博斯韦尔(Bill Boswell),微软认证系统工程师,《微软认证 专家在线杂志》 “If you learn well from books, I suggest Core Python Programming. It is by far the best I’ve found. I’m a Python newbie as well and in three months time I’ve been able to implement Python in projects at work (automating MSOffice, SQL DB stuff, etc.).” Edit By Vheavens  Edit By Vheavens                                —ptonman, Dev Shed Forums “如果你能通过书籍高效学习,我推荐《Python核心编程》。它是我迄今为 止发现的最好的书了。三个月前我还是一只Python菜鸟,如今我却已经可以在工 作的项目(微软Office自动化,SQL DB填充等等)中应用Python了。” ——ptonman,Dev Shed论坛 “Python is simply a beautiful language. It’s easy to learn, it’s cross-platform, and it works. It has achieved many of the technical goals that Java strives for.A one-sentence description of Python would be: ‘All other languages appear to have evolved over time—but Python was designed.’ And it was designed well. Unfortunately, there aren’t a large number of books for Python. The best one I’ve run across so far is Core Python Programming.” —Chris Timmons, C. R. Timmons Consulting “Python是一门美丽的语言。它简单易学,跨平台,而且运转良好。达成了 许多Java一直求索的技术目标。一言以蔽之就是:’其他的语言是与时代同步, 而Python则是未雨绸缪’而且计划得颇为出色。遗憾的是,并没有足够多的Python 书籍。至今最禁得起考验的就当属《Python核心编程》了。” ——克里斯·提曼斯(Chris Timmons),C. R. Timmons咨询公司 “If you like the Prentice Hall Core series, another good full-blown treatment to consider would be Core Python Programming. It addresses in elaborate concrete detail many practical topics that get little, if any, coverage in other books.” —Mitchell L Model, MLM Consulting “如果你喜欢Prentice Hall Core系列,那么《Python核心编程》就称得上 是另一席盛宴。它通过对技术细节的精心阐述令许多实用的主题变得易于消化, 同类书籍中,无出其右” ——米切尔·L·莫多(Mitchell L Model),MLM咨询公司 Edit By Vheavens  Edit By Vheavens                                core PYTHON programming Second Edition Python核心编程 (第二版) Vheavens Edit By Vheavens  Edit By Vheavens                                欢迎来到Python世界! 本章主题 z 什么是Python z Python的起源 z Python的特点 z 下载Python z 安装Python z 运行Python z Python文档 z 比较Python(与其它语言的比较) z 其它实现 Edit By Vheavens  Edit By Vheavens                                开篇将介绍一些 Python 的背景知识,包括什么是 Python、Python 的起源和它的一些关健 特性。一旦你来了兴致,我们就会向你介绍怎样获得 Python 以及如何在你的系统上安装并运 行它。本章最后的练习将会帮助你非常自如地使用 Python,包括使用交互式解释器以及创建 并运行脚本程序。 1.1 什么是 Python Python 是一门优雅而健壮的编程语言,它继承了传统编译语言的强大性和通用性,同时也 借鉴了简单脚本和解释语言的易用性。它可以帮你完成工作,而且一段时间以后,你还能看明 白自己写的这段代码。你会对自己如此快地学会它和它强大的功能感到十分的惊讶,更不用提 你已经完成的工作了!只有你想不到,没有 Python 做不到 1.2 起源 贵铎·范·罗萨姆(Guido van Rossum)于 1989 年底始创了 Python,那时,他还在荷兰 的 CWI(Centrum voor Wiskunde en Informatica,国家数学和计算机科学研究院)。1991 年 初,Python 发布了第一个公开发行版。这一切究竟是如何开始的呢?像 C、C++、Lisp、Java 和 Perl 一样,Python 来自于某个研究项目,项目中的那些程序员利用手边现有的工具辛苦的 工作着,他们设想并开发出了更好的解决办法。 Edit By Vheavens  Edit By Vheavens                                那时范·罗萨姆是一位研究人员,对解释型语言 ABC 有着丰富的设计经验,这个语言同样 也是在 CWI 开发的。但是他不满足其有限的开发能力。已经使用并参与开发了像 ABC 这样的高 级语言后,再退回到 C 语言显然是不可能的。他所期望的工具有一些是用于完成日常系统管理 任务的,而且它还希望能够访问 Amoeba 分布式操作系统的系统调用。尽管范·罗萨姆也曾想过 为 Amoeba 开发专用语言,但是创造一种通用的程序设计语言显然更加明智,于是在1989 年末, Python 的种子被播下了。 1.3 特点 尽管 Python 已经流行了超过 15 年,但是一些人仍旧认为相对于通用软件开发产业而言, 它还是个新丁。我们应当谨慎地使用“相对”这个词,因为“网络时代”的程序开发,几年看 上去就像几十年。 当人们询问:“什么是 Python?”的时候,很难用任何一个具象来描述它。人们更倾向于 一口气不加思索地说出他们对 Python 的所有感觉,Python 是___(请填写)__,这些特点究竟 又是什么呢?为了让你能知其所以然,我们下面会对这些特点进行逐一地阐释。 1.3.1 高级 伴随着每一代编程语言的产生,我们会达到一个新的高度。汇编语言是上帝献给那些挣扎 在机器代码中的人的礼物,后来有了 FORTRAN、 C 和 Pascal 语言,它们将计算提升到了崭新 的高度,并且开创了软件开发行业。伴随着 C 语言诞生了更多的像 C++、Java 这样的现代编译 语言。我们没有止步于此,于是有了强大的、可以进行系统调用的解释型脚本语言,例如 Tcl、 Perl 和 Python。 这些语言都有高级的数据结构,这样就减少了以前“框架”开发需要的时间。像 Python 中 的列表(大小可变的数组)和字典(哈希表)就是内建于语言本身的。在核心语言中提供这些 重要的构建单元,可以鼓励人们使用它们,缩短开发时间与代码量,产生出可读性更好的代码。 在 C 语言中,对于混杂数组(Python 中的列表)和哈希表(Python 中的字典)还没有相 应的标准库,所以它们经常被重复实现,并被复制到每个新项目中去。这个过程混乱而且容易 产生错误。C++使用标准模版库改进了这种情况,但是标准模版库是很难与 Python 内建的列表 和字典的简洁和易读相提并论的。 Edit By Vheavens  Edit By Vheavens                                1.3.2 面向对象 建议:面向对象编程为数据和逻辑相分离的结构化和过程化编程添加了新的活力。面向对 象 编程支持将特定的行为、特性以及和/或功能与它们要处理或所代表的数据结合在一起。 Python 的面向对象的特性是与生俱来的。然而,Python 绝不想 Java 或 Ruby 仅仅是一门面向对 象语言,事实上它融汇了多种编程风格。例如,它甚至借鉴了一些像 Lisp 和 Haskell 这样的函 数语言的特性。 1.3.3 可升级 大家常常将 Python 与批处理或 Unix 系统下的 shell 相提并论。简单的 shell 脚本可以用 来处理简单的任务,就算它们可以在长度上(无限度的)增长,但是功能总会有所穷尽。Shell 脚本的代码重用度很低,因此,你只能止步于小项目。实际上,即使一些小项目也可能导致脚 本又臭又长。Python 却不是这样,你可以不断地在各个项目中完善你的代码,添加额外的新的 或者现存的 Python 元素,也可以重用您脑海中的代码。Python 提倡简洁的代码设计、高级的 数据结构和模块化的组件,这些特点可以让你在提升项目的范围和规模的同时,确保灵活性、 一致性并缩短必要的调试时间。 “可升级”这个术语最经常用于衡量硬件的负载,通常指为系统添加了新的硬件后带来 的性能提升。我们乐于在这里对这个引述概念加以区分,我们试图用“可升级”来传达一种观 念,这就是:Python 提供了基本的开发模块,你可以在它上面开发你的软件,而且当这些需要 扩展和增长时,Python 的可插入性和模块化架构则能使你的项目生机盎然和易于管理。 1.3.4 可扩展 就算你的项目中有大量的 Python 代码,你也依旧可以有条不紊地通过将其分离为多个文件 或模块加以组织管理。而且你可以从一个模块中选取代码,而从另一个模块中读取属性。更棒 的是,对于所有模块,Python 的访问语法都是相同的。不管这个模块是 Python 标准库中的还 是你一分钟之前创造的,哪怕是你用其他语言写的扩展都没问题!借助这些特点,你会感觉自 己根据需要“扩展”了这门语言,而且你已经这么做了。 代码中的瓶颈,可能是在性能分析中总排在前面的那些热门或者一些特别强调性能的地方, 可以作为 Python 扩展用 C 重写。 。需要重申的是,这些接口和纯 Python 模块的接口是一模 一样的,乃至代码和对象的访问方法也是如出一辙的。唯一不同的是,这些代码为性能带来了 显著的提升。自然,这全部取决你的应用程序以及它对资源的需求情况。很多时候,使用编译 Edit By Vheavens  Edit By Vheavens                                型代码重写程序的瓶颈部分绝对是益处多多的,因为它能明显提升整体性能。 程序设计语言中的这种可扩展性使得工程师能够灵活附加或定制工具,缩短开发周期。虽 然像 C、C++乃至 Java 等主流第三代语言(3GL)都拥有该特性,但是这么容易地使用 C 编写 扩展确实是 Python 的优势。此外,还有像 PyRex 这样的工具,允许 C 和 Python 混合编程, 使编写扩展更加轻而易举,因为它会把所有的代码都转换成 C 语言代码。 因为 Python 的标准实现是使用 C 语言完成的(也就是 CPython),所以要使用 C 和 C++ 编写 Python 扩展。Python 的 Java 实现被称作 Jython,要使用 Java 编写其扩展。最后, 还有 IronPython,这是针对 .NET 或 Mono 平台的 C# 实现。你可以使用 C# 或者 VB.Net 扩 展 IronPython。 1.3.5 可移植性 在各种不同的系统上可以看到 Python 的身影,这是由于在今天的计算机领域,Python 取 得了持续快速的成长。因为 Python 是用 C 写的,又由于 C 的可移植性,使得 Python 可以运行 在任何带有 ANSI C编译器的平台上。尽管有一些针对不同平台开发的特有模块,但是在任何一 个平台上用 Python 开发的通用软件都可以稍事修改或者原封不动的在其他平台上运行。这种 可移植性既适用于不同的架构,也适用于不同的操作系统。 1.3.6 易学 Python 关键字少、结构简单、语法清晰。这样就使得学习者可以在相对更短的时间内轻松 上手。对初学者而言,可能感觉比较新鲜的东西可能就是 Python 的面向对象特点了。那些还未 能全部精通 OOP(Object Oriented Programming, 面向对象的程序设计)的人对径直使用Python 还是有所顾忌的,但是 OOP 并非必须或者强制的。入门也是很简单的,你可以先稍加涉猎,等 到有所准备之后才开始使用。 1.3.7 易读 Python 与其他语言显著的差异是,它没有其他语言通常用来访问变量、定义代码块和进行 模式匹配的命令式符号。通常这些符号包括:美元符号($)、分号(;)、波浪号(~)等等。 没有这些分神的家伙,Python 代码变得更加定义清晰和易于阅读。让很多程序员沮丧(或者欣 慰)的是,不像其他语言,Python 没有给你多少机会使你能够写出晦涩难懂的代码,而是让其 他人很快就能理解你写的代码,反之亦然。如前所述,一门语言的可读性让它更易于学习。我 们甚至敢冒昧的声称,即使对那些之前连一行 Python 代码都没看过的人来说,那些代码也是 相当容易理解的。看看下一章节——“起步”中的例子,然后告诉我们你的进展是多么神速。 Edit By Vheavens  Edit By Vheavens                                1.3.8 易维护 源代码维护是软件开发生命周期的组成部分。只要不被其他软件取代或者被放弃使用,你 的软件通常会保持继续的再开发。这通常可比一个程序员在一家公司的在职时间要长得多了。 Python 项目的成功很大程度上要归功于其源代码的易于维护,当然这也要视代码长度和复杂度 而定。然而,得出这个结论并不难,因为 Python 本身就是易于学习和阅读的。Python 另外一 个激动人心的优势就是,当你在阅读自己六个月之前写的脚本程序的时候,不会把自己搞得一 头雾水,也不需要借助参考手册才能读懂自己的软件。 1.3.9 健壮性 没有什么能够比允许程序员在错误发生的时候根据出错条件提供处理机制更有效的了。针 对错误,Python 提供了“安全合理”的退出机制,让程序员能掌控局面。一旦你的 Python 由 于错误崩溃,解释程序就会转出一个“堆栈跟踪”,那里面有可用到的全部信息,包括你程序 崩溃的原因以及是那段代码(文件名、行数、行数调用等等)出错了。这些错误被称为异常。 如果在运行时发生这样的错误,Python 使你能够监控这些错误并进行处理。 这些异常处理可以采取相应的措施,例如解决问题、重定向程序流、执行清除或维护步骤、 正常关闭应用程序、亦或干脆忽略掉。无论如何,这都可以有效的缩减开发周期中的调试环节。 Python 的健壮性对软件设计师和用户而言都是大有助益的。一旦某些错误处理不当,Python 也 还能提供一些信息,作为某个错误结果而产生的堆栈追踪不仅可以描述错误的类型和位置,还 能指出代码所在模块。 1.3.10 高效的快速原型开发工具 我们之前已经提到了 Python 是多么的易学易读。但是,你或许要问了,BASIC也是如此啊, Python 有什么出类拔萃的呢?与那些封闭僵化的语言不同,Python有许多面向其他系统的接口, 它的功能足够强大,本身也足够强壮,所以完全可以使用 Python 开发整个系统的原型。显然, 传统的编译型语言也能实现同样的系统建模,但是Python 工程方面的简洁性让我们可以在同样 的时间内游刃有余的完成相同的工作。此外,大家已经为 Python 开发了为数众多的扩展库,所 以无论你打算开发什么样的应用程序,都可能找到先行的前辈。你所要做的全部事情,就是来 个“即插即用”(当然,也要自行配置一番)!只要你能想得出来,Python 模块和包就能帮你 实现。Python 标准库是很完备的,如果你在其中找不到所需,那么第三方模块或包就会为你完 成工作提供可能。 Edit By Vheavens  Edit By Vheavens                                1.3.11 内存管理器 C 或者 C++最大的弊病在于内存管理是由开发者负责的。所以哪怕是对于一个很少访问、修 改和管理内存的应用程序,程序员也必须在执行了基本任务之外履行这些职责。这些加诸在开 发者身上的没有必要的负担和责任常常会分散精力。 在 Python 中,由于内存管理是由 Python 解释器负责的,所以开发人员就可以从内存事务 中解放出来,全神贯注于最直接的目标,仅仅致力于开发计划中首要的应用程序。这会使错误 更少、程序更健壮、开发周期更短。 1.3.12 解释性和(字节)编译性 Python 是一种解释型语言,这意味着开发过程中没有了编译这个环节。一般来说,由于不 是以本地机器码运行,纯粹的解释型语言通常比编译型语言运行的慢。然而,类似于Java,Python 实际上是字节编译的,其结果就是可以生成一种近似机器语言的中间形式。这不仅改善了Python 的性能,还同时使它保持了解释型语言的优点。 核心笔记:文件扩展名 Python 源文件通常用.py 扩展名。当源文件被解释器加载或者显式地进行字节码编译的时 候会被编译成字节码。由于调用解释器的方式不同,源文件会被编译成带有.pyc 或.pyo 扩展 名的文件,你可以在第 12 章“模块”学到更多的关于扩展名的知识。 1.4 下载和安装 Python 得到所有 Python 相关软件最直接的方法就是去访问它的网站(http://python.org)。为 了方便读者,你也可以访问本书的网站(http://corepython.com)并点击左侧的“Download Python”链接——我们在表格中罗列了当前针对大多数平台的 Python 版本,当然,这还是主要 集中在“三巨头”身上:Unix,Win32 和 MacOS X。 正如我们在前面 1.3.5 小节中提到的,Python 的可应用平台非常广泛。我们可以将其划分 成如下的几大类和可用平台: z 所有 Unix 衍生系统(Linux,MacOS X,Solaris,FreeBSD 等等) z Win32 家族(Windows NT,2000,XP 等等) z 早期平台:MacOS 8/9,Windows 3.x,DOS,OS/2,AIX z 掌上平台(掌上电脑/移动电话):Nokia Series 60/SymbianOS,Windows CE/Pocket Edit By Vheavens  Edit By Vheavens                                PC,Sharp Zaurus/arm-linux,PalmOS z 游戏控制台:Sony PS2,PSP,Nintendo GameCube z 实时平台:VxWorks,QNX z 其他实现版本:Jython,IronPython,stackless z 其他 Python 大部分的最近版本都只是针对“三巨头”的。实际上,最新的 Linux 和 MacOS X 版 本都已经安装好了 Python——你只需查看一下是哪个版本。尽管其他平台只能找到相对较早的 2.x 对应版本,但是就1.5 版而言这些版本也有了显著的改进。一些平台有其对应二进制版本, 可以直接安装,另外一些则需要在安装前手工编译。 Unix 衍生系统(Linux,MacOS X,Solaris,FreeBSD 等等) 正如前文所述,基于 Unix 的系统可能已经安装了 Python。最好的检查方法就是通过命令 行运行 Python,查看它是否在搜索路径中而且运行正常。只需输入: myMac:~ wesley$ python Python 2.4 (#4, Mar 19 2005, 03:25:10) [GCC 3.3 20030304 (Apple Computer, Inc. build 1671)] on darwin Type "help", "copyright", "credits" or "license" for more information. ">>>" If starting Python fails, it doesn’t mean it’s not installed, just that it’s not in your path. Hunt around for it, and if you’re unsuccessful, try building it manu- ally, which isn’t very difficult (see “Build It Yourself” on the next page). If you’re using certain versions of Linux, you can get the binary or source RPMs. Windows/DOS 系统 首先从前文提到的 python.org 或是 corepython.com 网站下载 msi 文件(例如, python-2.5.msi),之后执行该文件安装 Python。如果你打算开发 Win32 程序,例如使用 COM 或 MFC,或者需要 Win32 库,强烈建议下载并安装 Python 的 Windows 扩展。之后你就可以 通过 DOS 命令行窗口或者 IDLE 和 Pythonwin 中的一个来运行 Python 了,IDLE 是 Python 缺 省的 IDE(Integrated Development Environment,集成开发环境),而 Pythonwin 则来自 Windows 扩展模块。 Edit By Vheavens  Edit By Vheavens                                自己动手编译 Python 对绝大多数其它平台 , 下载 .tgz 文件, 解压缩这些文件, 然后执行以下操作以编译 Python: 1. ./configure 2. make 3. make install Python 通常被安装在固定的位置,所以你很容易就能找到。如今,在系统上安装多种版本 的 Python 已经是司空见惯的事情了。虽然容易找到二进制执行文件,你还是要设置好库文件的 安装位置。 在 Unix 中,可执行文件通常会将 Python 安装到/usr/local/bin 子目录下,而库文件则通 常安装在/usr/local/lib/python2.x 子目录下,其中的 2.x 是你正在使用的版本号。MacOS X 系统中,Python 则安装在/sw/bin 以及/或者 /usr/local/bin 子目录下。而库文件则在 /sw/lib,/usr/local/lib, 以及/或者 /Library/Frameworks/Python.framework/Versions 子 目录下。 在 Windows 中,默认的安装地址是 C:\Python2x。请避免将其安装在 C:\Program Files 目录下。是的,我们知道这是通常安装程序的文件夹。但是 DOS 是不支持“Program Files” 这样的长文件名的,它通常会被用“Progra~1”这个别名代替。这有可能给程序运行带来一些 麻烦,所以最好尽量避免。所以,听我的,将 Python 安装在 C:\Python 目录下,这样标准库文 件就会被安装在 C:\Python\Lib 目录下。 1.5 运行 Python 有三种不同的办法来启动 Python。最简单的方式就是交互式的启动解释器,每次输入一行 Python 代码来执行。另外一种启动 Python 的方法是运行 Python 脚本。这样会调用相关的脚本 解释器。最后一种办法就是用集成开发环境中的图形用户界面运行 Python。集成开发环境通常 整合了其他的工具,例如集成的调试器、文本编辑器,而且支持各种像 CVS 这样的源代码版本 控制工具。 1.5.1 命令行上的交互式解释器 在命令行上启动解释器,你马上就可以开始编写 Python 代码。在 Unix, DOS或其它提供命 令行解释器或 shell 窗口的系统中,都可以这么做。学习 Python 的最好方法就是在交互式解 Edit By Vheavens  Edit By Vheavens                                释器中练习。在你需要体验 Python 的一些特性时, 交互式解释器也非常有用。 Unix 衍生系统(Linux,MacOS X,Solaris,FreeBSD 等等) 要访问 Python, 除非你已经将 Python 所在路径添加到系统搜索路径之中, 否则就必须 输入 Python 的完整路径名才可以启动 Python。Python 一般安装在 /usr/bin 或/usr/local/bin 子目录中。 我们建议读者把 Python(python 执行文件,或 Jython 执行文件——如果你想使用 Java 版 的解释器的话)添加到你的系统搜索路径之中, 这样你只需要输入解释器的名字就可以启动 Python 解释器了,而不必每次都输入完整路径。 要将 Python 添加到搜索路径中, 只需要检查你的登录启动脚本, 找到以 set path 或 PATH= 指令开始,后面跟着一串目录的那行, 然后添加解释器的完整路径。所有事情都做完之 后, 更新一下 shell 路径变量。现在在 Unix 提示符(根据 shell 的不同可能是'%’或 '$') 处键入 python(或 jython)就可以启动解释器了, 如下所示: $ python Python 启动成功之后, 你会看到解释器启动信息,表明 Python 的版本号及平台信息, 最后显示解释器提示符 ">>>"等待你输入 Python 命令。 Windoes/DOS 环境 为了把 Python 添加到搜索路径中,你需要编辑 C:\autoexec.bat 文件并将完整的 Python 安装路径添加其中。这通常是 C:\Python 或 C:\Program Files \Python ( 或是 “Program Files”在 DOS 下的简写名字 C:\Progra~1\Python) 要想在 DOS 中将 Python 添加到搜索路径中去, 需要编辑C:\autoexec.bat 文件,把Python 的安装目录添加上去。一般是 C:\Python 或 C:\Program Files\Python(或者它在DOS 中的简写 名字 C:\Progra~1\Python).在一个 DOS 窗口中(它可以是纯 DOS 环境或是在 Windows 中的启动 的一个 DOS 窗口)启动 Python 的命令与 Unix 操作系统是一样的 都是 “python”:它们唯一 的区别在于提示符不同, DOS 中是 C:\> 如下图所示: 图 1-1 在一个 UNIX(MacOS X)环境中启动 Python 时的屏幕画面。 Edit By Vheavens  Edit By Vheavens                                C:\> python 命令行选项 当从命令行启动 Python 的时候,可以给解释器一些选项。这里有部分选项可供选择: -d 提供调试输出 -O 生成优化的字节码(生成 .pyo 文件) -S 不导入 site 模块以在启动时查找 Python 路径 -v 冗余输出(导入语句详细追踪) -m mod 将一个模块以脚本形式运行 -Q opt 除法选项(参阅文档) -c cmd 运行以命令行字符串形式提交的 Python 脚本 file 从给定的文件运行 Python 脚本(参阅后文) 图 1-2 在一个 DOS/命令行 窗口启动 Python Edit By Vheavens  Edit By Vheavens                                Figure 1–2 Starting Python in a DOS/command window 1.5.2 从命令行启动脚本 Unix 衍生系统(Linux,MacOS X,Solaris,FreeBSD 等等) 不管哪种 Unix 平台, Python 脚本都可以象下面这样,在命令行上通过解释器执行: $ python script.py Python 脚本使用扩展名 .py, 上面的例子也说明了这一点。 Unix 平台还可以在不明确指 定 Python 解释器的情况下,自动执行 Python 解释器。如果你使用的是类 Unix 平台, 你可以 在你的脚本的第一行使用 shell 魔术字符串(“sh-bang”) : #!/usr/local/bin/python 在 #!之后写上 Python 解释器的完整路径, 我们前面曾经提到,Python 解释器通常安装 在 /usr/local/bin 或 /usr/bin 目录下. 如果 Python 没有安装到那里, 你就必须确认你的 Python 解释器确实位于你指定的路径。错误的路径将导致出现类似于”找不到命令“的错误信 息 有一个更好的方案, 许多 Unix 系统有一个命令叫 env, 位于 /bin 或 /usr/bin 中。它 会帮你在系统搜索路径中找到 python 解释器。 如果你的系统拥有 env, 你的启动行就可以改 为下面这样: Edit By Vheavens  Edit By Vheavens                                #!/usr/bin/env python 或者, 如果你的 env 位于 /bin 的话, #!/bin/env python 当你不能确定 Python 的具体路径或者 Python 的路径经常变化时(但不能挪到系统搜索路 径之外), env 就非常有用。当你在你的脚本首行书写了合适的启动指令之后, 这个脚本就 能够直接执行。当调用脚本时, 会先载入Python 解释器, 然后运行你的脚本。我们刚才提到, 这样就不必显式的调用 Python 解释器了, 而你只需要键入脚本的文件名: $ script.py 注意, 在键入文件名之前, 必须先将这个文件的属性设置为可以执行。在文件列表中, 你的文件应该将它设置为自己拥有 rwx 权限。如果在确定 Python 安装路径,或者改变文件权 限,或使用 chmod 命令时遇到困难, 请和系统管理员一道检查一下。 Windows/DOS 环境 DOS 命令窗口不支持自动执行机制, 不过至少在 WinXP 当中, 它能象在 Windows 中一样 做到通过输入文件名执行脚本: 这就是“文件类型”接口。这个接口允许 Windows 根据文件扩 展名识别文件类型, 从而调用相应的程序来处理这个文件。举例来说, 如果你安装了带有 PythonWin 的 Python, 双击一个带有 .py 扩展名的 Python 脚本就会自动调用 Python 或 PythonWin IDE(如果你安装了的话)来执行你的脚本。 运行以下命令就和双击它的效果一样: C:\> script.py 这样无论是基于 Unix 操作系统还是 Win32 操作系统都可以无需在命令行指定 Python 解释器的情况下运行脚本,但是如果调用脚本时,得到类似“命令无法识别”之类的错误提示 信息,你也总能正确处理。 1.5.3 集成开发环境 Edit By Vheavens  Edit By Vheavens                                你也可以从图形用户界面环境运行 Python,你所需要的是支持 Python 的 GUI 程序。如 果你已经找到了一个,很有可能它恰好也是集成开发环境。集成开发环境不仅仅是图形接口, 通常会带有源代码编辑器、追踪和排错工具。 Unix 衍生系统(Linux,MacOS X,Solaris,FreeBSD 等等) IDLE 可以说是 Unix 平台下 Python 的第一个集成开发环境(IDE)。最初版本的 IDLE 也是 贵铎·范·罗萨姆开发的,在 Python1.5.2 中, 它首次露面。IDLE 代表的就是 IDE, 只不过 多了一个“L”。我猜测, IDLE是借用了“蒙提·派森”一个成员的名字 [译注 1]...嗯...... IDLE 基于 Tkinter, 要运行它的话你的系统中必须先安装 Tcl/Tk . 目前的Python 发行版都带 有一个迷你版的 Tcl/Tk 库, 因此就不再需要 Tcl/Tk 的完整安装了。 如果你已经在系统中安装好了 Python, 或者你有一个 Python RPM包, 可是它并没有包 含 IDLE 或 Tkinter, 那在你尝试 IDLE 之前, 必须先将这两样东西安装好。(如果你需要, 确实有一个独立的 Tkinter RPM 包可以供你下载, 以便和 Python 一起工作)如果你是自己编 译的 Python, 而且有 Tk 库可用, 那 Tkinter 会自动编译进 Python, 而且 Tkinter 和 IDLE 也会随 Python 的安装而安装。 如果你打算运行 IDLE, 就必须找到你的标准库安装位置: /usr/local/lib/python2.x/idlelib/idle.py. 如 果 你 是 自 己 编 译 Python, 你 会 在 /usr/local/bin 目录中发现一个名为 idle 的脚本, 这样你就可以在shell 命令行中直接运行 idle. 图 1-3 是类 Unix 系统下的 IDLE 界面。MacOS X 是一个非常类似Unix(基于 mach 内核, BSD 服务)的操作系统。 在 MacOS X 下,Python 可以用传统的 Unix 编译工具编译。MacOS X 发行版自带一个编译好的 Python 解释器, 不过并没有任何一个面向 Mac 的特殊工具。(比如 GNU readline, IDE 等等)。 当然也没有 Tkinter 和 IDLE。 你可能会打算自己下载并编译一个出来, 不过要小心一点, 有时你新安装的 Python 会与 Apple 预装的版本混淆在一起。认真一点没有坏处。你也可以通过 Fink/Finkcommander 和 DarwinPorts 得到 MacOS X 版的 Python: http://fink.sourceforge.net/ http://darwinports.org Edit By Vheavens  Edit By Vheavens                                如果要得到最新 Mac 版 Python 的组建和信息, 请访问: http://undefined.org/python http://pythonmac.org/packages 图 1-3 在 Unix 中启动 IDLE 另一个选择是从 Python 站点下载 MacOS X 的通用二进制包。这个磁盘映像文件(DMG)要求 操作系统版本至少为 10.3.9, 它适用于基于 PowerPC 和 Intel 硬件的 Mac 机器。 Windows 环境 PythonWin 是 Python 的第一个 Windows 接口,并且还是个带有图形用户界面的集成开发 环境。PythonWin 发行版本中包含 Windows API和 COM 扩展。PythonWin 本身是针对 MFC 库编写 的,它可以作为开发环境来开发你自己的 Windows 应用程序。你可以从下面给出的网页中下载 并安装它。 PythonWin 通常被安装在和 Python 相同的目录中,在它自己的安装目录 C:\Python2x\Lib\site-packages\pythonwin 中 有 可 执 行 的 启 动 文 件 pythonwin.exe 。 PythonWin 拥有一个带有颜色显示的编辑器、一个新的增强版排错器、交互 shell 窗口、COM 扩 Edit By Vheavens  Edit By Vheavens                                展和更多的有用特性。如图1–4 就是运行在 Windows 上的 PythonWin 集成开发环境的屏幕截图。 图1–4 Windows环境中的PythonWin 你可以在下面由马克·哈蒙德(Mark Hammond)维护的网站中找到更多的关于 PythonWin 和 Python 针对 Windowns 的扩展(也被称作“win32all”): http://starship.python.net/crew/mhammond/win32/ http://sourceforge.net/projects/pywin32/ http://starship.python.net/crew/mhammond/win32/ http://sourceforge.net/projects/pywin32/ IDLE 也有 Windows 平台版本,这是由Tcl/ Tk和 Python/ Tkinter的跨平台性特点决定的, 它看上去很像 Unix 平台下的版本,如图 1–5 所示。 Edit By Vheavens  Edit By Vheavens                                在 Windows 平台下,IDLE 可以在 Python 编译器通常所在的目录 C:\Python2x 下的子目录 Lib\idlelib 中找到。从 DOS 命令行窗口中启动 IDLE,请调用 idle.py。你也可以从 Windows 环境中调用 idle.py,但是会启动一个不必要的 DOS 窗口。取而代之的方法是双击 idle.pyw, 以.pyw 作为扩展名的文件不会通过打开 DOS 命令行窗口来运行脚本。事实上你可以在桌面上创 建一个到 C:\Python2x\Lib\idlelib\idle.pyw 的快捷方式,然后双击启动就可以了,简单吧! Figure 1–5 Starting IDLE in Windows 1.5.4 其它的集成开发环境和执行环境 很多的软件开发专家事实上会选择在他们喜欢的文本编辑器中编写代码,比如 vi(m) 或者 emacs。除了这些和上面提到到的集成开发环境,还有大量的开源和商业的集成开发环境,下面 是个简短的列表: 开源 z IDLE (在 Python 发行版中自带) Edit By Vheavens  Edit By Vheavens                                http://python.org/idle/ z PythonWin + Win32 Extensions http://starship.python.net/crew/skippy/win32 z IPython (增强的交互式 Python) http://ipython.scipy.org z IDE Studio (IDLE 以及更多) http://starship.python.net/crew/mike/Idle z Eclipse http://pydev.sf.net http://eclipse.org/ 商业 z WingWare 开发的 WingIDE Python 集成开发环境 http://wingware.com/ z ActiveState 开 发 的 Komodo 集 成 开 发 环 境 http://activestate.com/Products/Komodo 通用集成开发环境列表 http://wiki.python.org/moin/IntegratedDevelopmentEnvironments 核心提示:运行本书中的代码实例 在本书中,你会发现很多的 Python 脚本样例,可以从本书的网站上下载。但是当你运行它 们的时候,请记住这些代码是设计用来从命令行(DOS 命令行窗口或 Unix shell)或者集成开 发环境执行的。如果你在使用 Win32 系统,双击 Python 程序会打开 DOS 窗口,但是在脚本执行 完毕后就会关闭,所以你可能看不到输出结果。如果你遇到了这种情况,就直接打开DOS 窗口, 从命令行中运行相关的脚本,或者在集成开发环境中执行脚本。另外一种办法,就是在脚本的 最后一行后面添加 raw_input()语句,这样就可以保持窗口开着,直到你按下回车键才关闭。 1.6 Python 文档 Edit By Vheavens  Edit By Vheavens                                Python 文档可以在很多地方找到。最便捷的方式就是从 Python 网站查看在线文档。如果 你没上网,并且使用的是 Win32 系统,那么在 C:\Python2x\Doc\目录下会找到一个名为 Python2x.chm 的离线帮助文档。它使用IE 接口,所以你实际上是使用网页浏览器来查看文档。 其他的离线文档包括 PDF 和 PostScript (PS)文件。最后,如果你下载了 Python 发行版,你会 得到 LaTeX 格式的源文件。 在本书的网站中,我们创建了一个包括绝大多数 Python 版 本 的文档,只要访问 http://corepython.com,单击左侧的“Documentation”就可以了。 1.7 比较 Python(Python 与其他语言的比较) Python 已经和很多语言比较过了。一个原因就是Python 提供了很多其他语言拥有的特性。 另外一个原因就是 Python 本身也是由诸多其他语言发展而来的,这包括 ABC、Modula-3、C、 C++、Algol-68、SmallTalk、Unix shell 和其他的脚本语言等等。Python 就是”浓缩的精华 “:范·罗萨姆研究过很多语言,从中吸收了许多觉得不错的特性,并将它们溶于一炉。 然而,往往因为Python 是一门解释型语言,你会发现大多数的比较是在Perl、Java、Tcl, 还有 JavaScript 之间进行的。Perl 是另外一种脚本语言,远远超越了标准的 shell 脚本。像 Python 一样,Perl 赋予了你所有编程语言的功能特性,还有系统调用能力。 Perl 最大的优势在于它的字符串模式匹配能力,其提供了一个十分强大的正则表达式匹配 引擎。这使得 Perl 实际上成为了一种用于过滤、识别和抽取字符串文本的语言,而且它一直是 开发 Web 服务器端 CGI(common gateway interface,通用网关接口)网络程序的最流行的语言。 Python 的正则表达式引擎很大程度上是基于 Perl 的。 然而,Perl 语言的晦涩和对符号语法的过度使用,让解读变得很困难。这些语法令初学者 不得精要,为他们的学习带来了不小的阻碍。Perl 的这些额外的“特色”使得完成同一个任务 会有多个方法,进而引起了开发者之间的分歧和内讧。最后,通常当你想阅读几个月前写的Perl 脚本的时候都不得不求助参考书。 Python 也经常被拿来和 Java 作对比,因为他们都有类似的面向对象的特性和语法。Java 的语法尽管比 C++简单的多,但是依旧有些繁琐,尤其是当你想完成一个小任务的时候。Python 的简洁比纯粹的使用 Java 提供了更加快速的开发环境。在 Python 和 Java 的关系上,一个非常 重大的革命就是 Jython 的开发。Jython 是一个完全用 Java 开发的 Python 解释器,现在可以 Edit By Vheavens  Edit By Vheavens                                在只有 Java 虚拟机的环境中运行 Python 程序。我们会在后面的章节中简单讲述Jython 的更多 优点,但是现在就可以告诉你:在 Jython 的脚本环境中,你可以熟练地处理 Java 对象,Java 可以和 Python 对象进行交互,你可以访问自己的 Java 标准类库,就如同 Java 一直是 Python 环境的一部分一样。 现在,由于 Rails 项目的流行,Python 也经常被拿来和 Ruby 进行比较。就像前面我们提 到的,Python 是多种编程范式的混合,它不像 Ruby 那样完全的面向对象,也没有像 Smalltalk 那样的块,而这正是Ruby 最引人注目的特性。Python有一个字节码解释器,而Ruby 没有。Python 更加易读,而 Ruby 事实上可以看作是面向对象的 Perl。相对于 Rails,Python 有几个自己的 Web 应用框架,比如 Django 和 Turbogears 这两个项目。 Tcl 是另一种可以与 Python 相提并论的脚本语言。Tcl是最易于使用的脚本语言之一, 程 序员很容易像访问系统调用一样对 Tcl 语言进行扩展。Tcl 直到今天仍然很流行, 与 Python 相比, 它或许有更多局限性(主要是因为它有限的几种数据类型), 不过它也拥有和 Python 一样的通过扩展超越其原始设计的能力。更重要的是, Tcl 通常总是和它的图形工具包 Tk 一 起工作, 一起协同开发图形用户界面应用程序。因为它非常流行, 所以 Tk 已经被移植到 Perl(Perl/Tk)和 Python(Tkinter)中. 同样有一个有争议的观点,那就是与 Tcl 相比, 因为 Python 有类, 模块及包的机制,所以写起大程序来更加得心应手。 Python 有一点点函数化编程(functional programming ,FP)结构,这使得它有点类似 List 或 Scheme 语言。尽管Python 不是传统的函数化编程语言, 但它持续的从Lisp 和 haskell 语言中借用一些有价值的特性。举例来说, 列表解析就是一个广受欢迎的来自 Haskell 世界的 特性, 而 Lisp 程序员在遇到 lambda, map, filter 和 reduce 时也会感到异常亲切。 JavaScript 是另外一种非常类似 Python 的面向对象脚本语言。优秀的JavaScript 程序员 学起 Python 来易如反掌。 聪慧的读者会注意到 JavaScript 是基于原型系统的, 而 Python 则 遵循传统的面向对象系统, 这使得二者的类和对象有一些差异。 下面列出了有关 Python 与其它语言进行比较的网页: Perl http://www2.linuxjournal.com/article/3882 http://llama.med.harvard.edu/~fgibbons/PerlPythonPhrasebook.html Edit By Vheavens  Edit By Vheavens                                http://aplawrence.com/Unixart/pythonvsperl.html http://pleac.sf.net/pleac_python http://www.garshol.priv.no/download/text/perl.html Java http://dirtsimple.org/2004/12/python-is-not-java.html http://twistedmatrix.com/users/glyph/rant/python-vs-java.html http://netpub.cstudies.ubc.ca/oleary/python/python_java_comparison.php Lisp http://strout.net/python/pythonvslisp.html http://norvig.com/python-lisp.html Ruby http://blog.ianbicking.org/ruby-python-power.html http://www.rexx.com/~oinkoink/Ruby_v_Python.html http://dev.rubycentral.com/faq/rubyfaq-2.html Perl, C++ http://strombergers.com/python/ Perl, Java, C++ http://furryland.org/~mikec/bench/ C++, Java, Ruby http://dmh2000.com/cjpr Perl, Java, PHP, Tcl http://www-128.ibm.com/developerworks/linux/library/l-python101.html http://www-128.ibm.com/developerworks/linux/library/l-script-survey/ C, C++, Java, Perl, Rexx, Tcl Edit By Vheavens  Edit By Vheavens                                http://www.ubka.uni-karlsruhe.de/indexer-vvv/ira/2000/5 你可以在下面的网址中看到更多 Python 与其他的语言的比较: http://www.python.org/doc/Comparisons.html 1.8 其它实现 标准版本的 Python 是用 C 来编译的, 又叫 CPython. 除此之外, 还有一些其它的 Python 实现。我们将在下面讲述些实现, 除了本书中提到的这些实现以外, 下面的网址还有更多的 实现版本: http://python.org/dev/implementations.html Java 我们在上一节中曾经提到, 还有一个可以用的 Python 解释器是完全由 Java 写成的, 名 为 Jython。 尽管两种解释器之间存在一些细微的差别, 但是它们非常接近, 而且启动环 境也完全相同。那 Jython 又有哪些优势呢? Jython... z 只要有 Java 虚拟机, 就能运行 Jython z 拥有访问 Java 包与类库的能力 z 为 Java 开发环境提供了脚本引擎 z 能够很容易的测试 Java 类库 z 提供访问 Java 原生异常处理的能力 z 继承了 JavaBeans 特性和内省能力 z 鼓励 Python 到 Java 的开发(反之亦然) z GUI 开发人员可以访问 Java 的 AWT/Swing 库 z 利用了 Java 原生垃圾收集器(CPython 未实现此功能) 对 Jython 进行详细论述, 超出了本文的范围。 不过网上有非常多的Jython 信息。Jython 目前仍然在不断开发之中, 不时会增加新的特性。你可以通过访问 Jython 的网站得到更多有 用的信息: Edit By Vheavens  Edit By Vheavens                                http://jython.org .NET/Mono 现在已经有一个名为 IronPython 的 Python 实现. 它是用 C# 语言完成的. 它适用的环 境是 .NET 和 Mono. 你可以在一个 .NET 应用程序中整合 IronPython 解释器来访问 .NET 对象. IronPython 的扩展可以用 C#或 VB.NET 语言编写. 除此之外, 还有一种名为 Boo 的 .NET/Mono 语言. 你可以在下面的网址获得更多关于 IronPython 和 Boo 语言的信息. http://codeplex.com/Wiki/View.aspx?ProjectName=IronPython http://boo.codehaus.org/ Stackless CPython 的一个局限就是每个 Python 函数调用都会产生一个 C 函数调用. (从计算机科学的角度来说, 我 们在讨论栈帧). 这意味着同时产生的函数调用是有限制的, 因此 Python 难以实现用户级的线程库和复杂递 归应用. 一旦超越这个限制, 程序就会崩溃. 你可以通过使用一个 “stackless” 的 Python 实现来突破 这个限制, 一个 C 栈帧可以拥有任意数量的 Python 栈帧. 这样你就能够拥有几乎无穷的函数调用, 并能 支持巨大数量的线程. 这个 Python 实现的名字就叫…….Stackless(嘿嘿, 很惊讶吗?!) Stackless 唯一的问题就是它要对现有的 CPython 解释器做重大修改. 所以它几乎是一 个独立的分支. 另一个名为 Greenlets 的项目也支持微线程, 它是一个标准的 C 扩展, 因此 不需要对标准 Python 解释器做任何修改. 通过以下网址你能了解更多信息: http://stackless.com http://codespeak.net/py/current/doc/greenlet.html 1.9 练习 1–1. 安装 Python。请检查 Python 是否已经安装到你的系统上,如果没有,请下载并 安装它! 1–2. 执行 Python。有多少种运行 Python 的不同方法?你喜欢哪一种?为什么? 1–3. Python 标准库。 (a)请找到系统中 Python 执行程序的安装位置和标准库模块的安装位置 (b)看看标准库里的一些文件,比如 string.py。这会帮助你适应阅读 Python 脚本。 1–4. I 交互执行。启动你的 Python 交互解释器。你可以通过输入完整的路径名来启动 它。当然,如果你已经在搜索路径中设置了它的位置,那么只输入它的名字(python 或者 python.exe)就行了。(你可以任选最适合你的的 Python 实现方式,例如:命令行、图形用户 Edit By Vheavens  Edit By Vheavens                                接口/集成开发环境、Jython、IronPython 或者 Stackless)启动界面看上去就像本章描述的一 样,一旦你看到>>>提示符,就意味着解释器准备好要接受你的 Python 命令了。 试着输入命令 print 'Hello World!' (然后按回车键),完成著名的 Hello World!程序, 然后退出解释器。在 Unix 系统中,按下 Ctrl+D 会发送 EOF 信号来中止 Python 解释器,在 DOS 系统中,使用的组合键是 Ctrl+Z。如果要从 Macintosh、PythonWin、以及 Windows 或 Unix 中 的 IDLE 这样的图形用户环境中退出,只要简单的关闭相关窗口就可以了。 1–5. 编写脚本。作为练习 1–4 的延续,创建“Hello World!”的 Python 脚本其实和 上面的交互性练习并不是一回事。如果你在使用 Unix 系统,尝试建立自动运行代码行,这样你 就可以在没有调用 Pyton 解释器的情况下运行程序了。 1–6. 编写脚本。使用 print 语句编写脚本在屏幕上显示你名字、年龄、最喜欢的颜色 和与你相关的一些事情(背景、兴趣、爱好等等)。 译注 1:蒙提·派森:Monty Python,也称“蒙地蟒蛇”。是英国的一个六人喜剧团 体,其七十年代的电视剧和八十年代的电影作品红极一时。贵铎·范·罗萨姆就是该团体的忠 实影剧迷,故而将本语言命名为Python。这里的 IDLE 指的是其成员艾瑞克·艾多(Eric Idle ) Edit By Vheavens  Edit By Vheavens                                Python起步 本章主题 z 介绍 z 输入/输出 z 注释 z 操作符 z 变量与赋值 z Python 类型 z 缩进 z 循环与条件 z 文件 z 错误 z 函数 z 类 z 模块 Edit By Vheavens  Edit By Vheavens                                本章将对 Python 的主要特性做一个快速介绍,这样你就可以借助以前的编程经验识别出 熟悉的语言结构,并立刻将 Python 付诸使用。虽然细节内容会在后续的章节中逐一讲解,但 是对整体的了解可以让你迅速融入到 Python 中。阅读本章的最好的方法就是在你的电脑上打 开 Python 解释器,尝试书中的示例, 当然你也可以随心所欲的自己做实验。 我们已经在第一章和练习 1–4 中介绍了如何启动 Python 解释器。在所有的交互示例中, 你会看到 Python 的主提示符( >>> )和次提示符( ... )。主提示符是解释器告诉你它在等待你 输入下一个语句,次提示符告诉你解释器正在等待你输入当前语句的其它部分。 Python 有两种主要的方式来完成你的要求:语句和表达式(函数、算术表达式等)。相信 大部分读者已经了解二者的不同,但是不管怎样,我们还是再来复习一下。语句使用关键字来 组成命令,类似告诉解释器一个命令。你告诉 Python 做什么,它就为你做什么,语句可以有输 出,也可以没有输出。下面我们先用 print 语句完成程序员们老生常谈第一个编程实例,Hello World: >>> print 'Hello World!' Hello World! Edit By Vheavens  Edit By Vheavens                                而表达式没有关键字。它们可以是使用数学运算符构成的算术表达式,也可以是使用括号 调用的函数。它们可以接受用户输入,也可以不接受用户输入,有些会有输出,有些则没有。 (在 Python 中未指定返回值的函数会自动返回 None,等价于NULL)下面举一个例子,函数 abs() 接受一个数值输入,然后输出这个数值的绝对值: >>> abs(-4) 4 >>> abs(4) 4 本章中我们将分别介绍语句和表达式。我们先来研究 print 语句。 2.1 程序输出,print 语句及“Hello World!” 有些语言, 比如 C, 通过函数输出数据到屏幕,例如函数 printf()。然而在 Python 和大 多数解释执行的脚本语言,使用语句进行输出。很多的 shell 脚本语言使用 echo 命令来输出程 序结果。 核心笔记:在交互式解释器中显示变量的值 通常当你想看变量内容时,你会在代码中使用 print 语句输出。不过在交互式解释器中, 你可以用 print 语句显示变量的字符串表示,或者仅使用变量名查看该变量的原始值。 在下面的例子中,我们把一个字符串赋值给变量 myString,先用 print 来显示变量的内容, 之后用变量名称来显示。 >>> myString = 'Hello World!' >>> print myString Hello World! >>> myString 'Hello World!' 注意:在仅用变量名时,输出的字符串是被用单引号括起来了的。这是为了让非字符串对 象也能以字符串的方式显示在屏幕上--即它显示的是该对象的字符串表示,而不仅仅是字符 串本身。引号表示你刚刚输入的变量的值是一个字符串。等你对 Python 有了较深入的了解之后, 你就知道 print 语句调用 str()函数显示对象,而交互式解释器则调用 repr()函数来显示对象。 2.2 程序输入和 raw_input()内建函数 Edit By Vheavens  Edit By Vheavens                                下划线(_)在解释器中有特别的含义,表示最后一个表达式的值。所以上面的代码执行之后, 下划线变量会包含字符串: >>> _ Hello World! Python 的 print 语句,与字符串格式运算符( % )结合使用,可实现字符串替换功能,这 一点和 C 语言中的 printf()函数非常相似: >>> print "%s is number %d!" % ("Python", 1) Python is number 1! %s 表示由一个字符串来替换,而%d表示由一个整数来替换,另外一个很常用的就是%f, 它 表示由一个浮点数来替换。我们会在本章中看到更多类似的例子。Python 非常灵活,所以即使 你将数字传递给 %s,也不会像其他要求严格的语言一样引发严重后果。参阅章节 6.4.1 以了解 更多关于字符串格式运算符的信息。Print 语句也支持将输出重定向到文件。这个特性是从 Python2.0 开始新增的。符号 >> 用来重定向输出,下面这个例子将输出重定向到标准错误输 出: import sys print >> sys.stderr, 'Fatal error: invalid input!' import sys print >> sys.stderr, 'Fatal error: invalid input!' 下面是一个将输出重定向到日志文件的例子: logfile = open('/tmp/mylog.txt', 'a') print >> logfile, 'Fatal error: invalid input!' logfile.close() 2.2 程序输入和 内建函数raw_input() 从用户那里得到数据输入的最容易的方法是使用 raw_input()内建函数。 它读取标准输入, 并将读取到的数据赋值给指定的变量。 你可以使用 int() 内建函数将用户输入的字符串转换 为整数。 >>> user = raw_input('Enter login name: ') Enter login name: root Edit By Vheavens  Edit By Vheavens                                >>> print 'Your login is:', user Your login is: root 上面这个例子只能用于文本输入。 下面是输入一个数值字符串(并将字符串转换为整数) 的例子: >>> num = raw_input('Now enter a number: ') Now enter a number: 1024 >>> print 'Doubling your number: %d' % (int(num) * 2) Doubling your number: 2048 内建函数 int()将数值字符串转换成整数值,这样才可以对它进行数学运算。参阅章节 6.5.3 以了解更多有关内建函数 raw_input()的知识。 核心笔记:从交互式解释器中获得帮助 在学习 Python 的过程中,如果需要得到一个生疏函数的帮助,只需要对它调用内建函数 help()。通过用函数名作为 help()的参数就能得到相应的帮助信息: >>> help(raw_input) Help on built-in function raw_input in module __builtin__: raw_input(...) raw_input([prompt]) -> string 从标准输入读取一个字符串并自动删除串尾的换行字符。 如果用入键入了 EOF 字符(Unix: Ctrl+D, Windows: Ctrl+Z+回车), 则引发 EOFError, 在 Unix 平台, 只要可用,就使用 GNU readline 库。如果提供提示字符串参数, 则显示该字符串并自动删去字符串末尾的换行字符。 (上面一行是 help(raw_input)的输出,译文是对其加以解释——译者注) 核心风格: 一直在函数外做用户交互操作 新手在需要显示信息或得到用户输入时, 很容易想到使用 print 语句和 raw_input()内 建函数。不过我们在此建议函数应该保持其清晰性, 也就是它只应该接受参数,返回结果。从 用户那里得到需要的数据, 然后调用函数处理, 从函数得到返回值,然后显示结果给用户。 这 样你就能够在其它地方也可以使用你的函数而不必担心自定义输出的问题。这个规则的一个例 Edit By Vheavens  Edit By Vheavens                                外是,如果函数的基本功能就是为了得到用户输出, 或者就是为了输出信息,这时在函数体使 用 print 语句或 raw_input() 也未尝不可。更重要的, 将函数分为两大类, 一类只做事, 不 需要返回值(比如与用户交互或设置变量的值), 另一类则执行一些运算,最后返回结果。如 果输出就是函数的目的,那么在函数体内使用 print 语句也是可以接受的选择。 2.3 注释 和大部分脚本及 Unix-shell 语言一样,Python 也使用 # 符号标示注释,从 # 开始,直 到一行结束的内容都是注释。 >>> # one comment ... print 'Hello World!' # another comment Hello World! 有一种叫做文档字符串的特别注释。你可以在模块、类或者函数的起始添加一个字符串, 起到在线文档的功能,这是 Java 程序员非常熟悉的一个特性。 def foo(): "This is a doc string." return True 与普通注释不同,文档字符串可以在运行时访问,也可以用来自动生成文档。 2.4 运算符 和其他绝大多数的语言一样,Python 中的标准算术运算符以你熟悉的方式工作 + - * / // % ** 加、减、乘、除和取余都是标准运算符。Python有两种除法运算符,单斜杠用作传统除法, 双斜杠用作浮点除法(对结果进行四舍五入)。传统除法是指如果两个操作数都是整数的话, 它将执行是地板除(取比商小的最大整数)(关于“地板除”请参考第 5 章——译者注),而浮 点除法是真正的除法,不管操作数是什么类型,浮点除法总是执行真正的除法。你可以在第五 章(数字)学到更多有关传统除法、真正的除法及浮点除法的知识。 还有一个乘方运算符, 双星号(**)。尽管我们一直强调这些运算符的算术本质, 但是请 注意对于其它数据类型,有些运算符是被重载了,比如字符串和列表。。让我们看一个例子: Edit By Vheavens  Edit By Vheavens                                >>> print -2 * 4 + 3 ** 2 就象你看到的, 运算符的优先级和你想象的一样: + 和 - 优先级最低, *, /, //, % 优先级较高, 单目运算符 + 和 - 优先级更高, 乘方的优先级最高。(3 ** 2) 首先求值, 然 后是 (-2 * 4), 然后是对两个结果进行求和。 Python 当然也有标准比较运算符, 比较运算根据表达式的值的真假返回布尔值: < <= > >= == != <> 试一试,看看这些比较运算会得到什么结果: >>> 2 < 4 True >>> 2 == 4 False >>> 2 > 4 False >>> 6.2 <= 6 False >>> 6.2 <= 6.2 True >>> 6.2 <= 6.20001 True Python 目前支持两种“不等于”比较运算符, != 和 <> , 分别是 C 风格和 ABC/Pascal 风格。目前后者慢慢地被淘汰了, 所以我们推荐使用前者。 Python 也提供了逻辑运算符: and or not 使用逻辑运算符可以将任意表达式连接在一起,并得到一个布尔值: >>> 2 < 4 and 2 == 4 False >>> 2 > 4 or 2 < 4 True >>> not 6.2 <= 6 True >>> 3 < 4 < 5 True Edit By Vheavens  Edit By Vheavens                                最后一个例子在其他语言中通常是不合法的,不过 Python 支持这样的表达式, 既简洁又 优美。它实际上是下面表达式的缩写: >>> 3 < 4 and 4 < 5 参阅章节 4.5 种可以得到更多有关 Python 运算符的信息。 核心风格: 合理使用括号增强代码的可读性,在很多场合使用括号都是一个好主意,而没 用括号的话,会使程序得到错误结果,或使代码可读性降低,引起阅读者困惑。。括号在Python 语言中不是必须存在的, 不过为了可读性, 使用括号总是值得的。任何维护你代码的人会感 谢你, 在你再次阅读自己的代码时,你也会感谢你自己。 2.5 变量和赋值 Python 中变量名规则与其它大多数高级语言一样, 都是受 C 语言影响(或者说这门语言 本身就是 C 语言写成的)。变量名仅仅是一些字母开头的标识符--所谓字母开头--意指大 写或小写字母,另外还包括下划线( _ ). 其它的字符可以是数字,字母, 或下划线。Python 变量名是大小写敏感的, 也就是说变量 "cAsE" 与 "CaSe" 是两个不同的变量。 Python 是动态类型语言, 也就是说不需要预先声明变量的类型。 变量的类型和值在赋值 那一刻被初始化。变量赋值通过等号来执行。 >>> counter = 0 >>> miles = 1000.0 >>> name = 'Bob' >>> counter = counter + 1 >>> kilometers = 1.609 * miles >>> print '%f miles is the same as %f km' % (miles, kilometers) 1000.000000 miles is the same as 1609.000000 km 上面是五个变量赋值的例子。第一个是整数赋值,第二个是浮点数赋值,第三个是字符串 Edit By Vheavens  Edit By Vheavens                                赋值,第四个是对一个整数增 1, 最后一个是浮点乘法赋值。 Python 也支持增量赋值,也就是运算符和等号合并在一起, 看下面的例子: n = n * 10 将上面的例子改成增量赋值方式就是: n *= 10 Python 不支持 C 语言中的自增 1 和自减 1 运算符, 这是因为 + 和 - 也是单目运算符, Python 会将 --n 解释为-(-n) 从而得到 n , 同样 ++n 的结果也是 n. 2.6 数字 Python 支持五种基本数字类型,其中有三种是整数类型。 z int (有符号整数) z long (长整数) z bool (布尔值) z float (浮点值) z complex (复数) 下面是一些例子: Python 中有两种有趣的类型是 Python 的长整型和复数类型。请不要将 Python 的长整数 与 C 语言的长整数混淆。Python的长整数所能表达的范围远远超过 C 语言的长整数, 事实上, Python 长整数仅受限于用户计算机的虚拟内存总数。如果你熟悉 Java, Python 的长整数类似 于 Java 中的 BigInteger 类型。 Edit By Vheavens  Edit By Vheavens                                从长远来看, 整型与长整型正在逐步统一为一种整数类型。从 Python2.3 开始,再也不会 报整型溢出错误, 结果会自动的被转换为长整数。在未来版本的 Python 中, 两种整数类型将 会无缝结合, 长整数后缀 “L”也会变得可有可无。 布尔值是特殊的整数。 尽管布尔值由常量 True 和 False 来表示, 如果将布尔值放到一 个数值上下文环境中(比方将 True 与一个数字相加), True 会被当成整数值 1, 而 False 则会被当成整数值 0。 复数(包括-1 的平方根, 即所谓的虚数)在其它语言中通常不被直接 支持(一般通过类来实现)。 其实还有第六种数字类型, decimal, 用于十进制浮点数。不过它并不是内建类型, 你 必须先导入 decimal 模块才可以使用这种数值类型。 由于需求日渐强烈, Python 2.4 增加 了这种类型。举例来说, 由于在二进制表示中有一个无限循环片段,数字 1.1 无法用二进制浮 点数精确表示。因此, 数字 1.1 实际上会被表示成: >>> 1.1 1.1000000000000001 >>> print decimal.Decimal('1.1') 1.1 第五章详细将介绍所有的数字类型 2.7 字符串 Python 中字符串被定义为引号之间的字符集合。Python 支持使用成对的单引号或双引号, 三引号(三个连续的单引号或者双引号)可以用来包含特殊字符。使用索引运算符( [ ] )和切 片运算符( [ : ] )可以得到子字符串。字符串有其特有的索引规则:第一个字符的索引是 0, 最后一个字符的索引是 -1 加号( + )用于字符串连接运算,星号( * )则用于字符串重复。下面是几个例子: >>> pystr = 'Python' >>> iscool = 'is cool!' >>> pystr[0] 'P' Edit By Vheavens  Edit By Vheavens                                >>> pystr[2:5] 'tho' >>> iscool[:2] 'is' >>> iscool[3:] 'cool!' >>> iscool[-1] '!' >>> pystr + iscool 'Pythonis cool!' >>> pystr + ' ' + iscool 'Python is cool!' >>> pystr * 2 'PythonPython' >>> '-' * 20 '--------------------' >>> pystr = '''python ... is cool''' >>> pystr 'python\nis cool' >>> print pystr python is cool >>> 你可以在第六章学到更多有关字符串的知识。 2.8 列表和元组 可以将列表和元组当成普通的“数组”,它能保存任意数量任意类型的 Python 对象。和数 组一样,通过从 0 开始的数字索引访问元素,但是列表和元组可以存储不同类型的对象。 列表和元组有几处重要的区别。列表元素用中括号( [ ])包裹,元素的个数及元素的值可 以改变。元组元素用小括号(( ))包裹,不可以更改(尽管他们的内容可以)。元组可以看成是 只读的列表。通过切片运算( [ ] 和 [ : ] )可以得到子集,这一点与字符串的使用方法一样。 Edit By Vheavens  Edit By Vheavens                                >>> aList = [1, 2, 3, 4] >>> aList [1, 2, 3, 4] >>> aList[0] 1 >>> aList[2:] [3, 4] >>> aList[:3] [1, 2, 3] >>> aList[1] = 5 >>> aList [1, 5, 3, 4] 元组也可以进行切片运算,得到的结果也是元组(不能被修改): >>> aTuple = ('robots', 77, 93, 'try') >>> aTuple ('robots', 77, 93, 'try') >>> aTuple[:3] ('robots', 77, 93) >>> aTuple[1] = 5 Traceback (innermost last): File "", line 1, in ? TypeError: object doesn't support item assignment 你可以在第六章学到更多有关列表、元组以及字符串的知识。 2.9 字典 字典是 Python 中的映射数据类型,工作原理类似 Perl 中的关联数组或者哈希表,由键- 值(key-value)对构成。几乎所有类型的 Python 对象都可以用作键,不过一般还是以数字或者 字符串最为常用。 值可以是任意类型的 Python 对象,字典元素用大括号({ })包裹。 Edit By Vheavens  Edit By Vheavens                                >>> aDict = {'host': 'earth'} # create dict >>> aDict['port'] = 80 # add to dict >>> aDict {'host': 'earth', 'port': 80} >>> aDict.keys() ['host', 'port'] >>> aDict['host'] 'earth' >>> for key in aDict: ... print key, aDict[key] ... host earth port 80 在第七章中会详细讲解字典。 2.10 代码块及缩进对齐 代码块通过缩进对齐表达代码逻辑而不是使用大括号,因为没有了额外的字符,程序的可 读性更高。而且缩进完全能够清楚地表达一个语句属于哪个代码块。当然,代码块也可以只有 一个语句组成。 对一个 Python 初学者来说, 仅使用缩进可能令他诧异。 人们通常竭力避免改变, 因此 对那些使用大括号很多年的人来说, 初次使用纯缩进来表示逻辑也许会多少感到有些不够坚定。 (不用大括号?到底成不成啊?)。然而回想一下, python 有两大特性, 一是简洁,二是可 读性好。如果你实在讨厌使用缩进作为代码分界, 我们希望你从现在开始,半年后再来看一下 这种方式。也许你会发现生活中没有大括号并不会象你想像的那么糟糕。 2.11 if 语句 标准 if 条件语句的语法如下: if expression: if_suite 如果表达式的值非 0 或者为布尔值 True, 则代码组 if_suite 被执行; 否则就去执行下一 条语句。 代码组是一个 Python 术语, 它由一条或多条语句组成,表示一个子代码块。Python Edit By Vheavens  Edit By Vheavens                                与其它语言不同, 条件条达式并不需要用括号括起来。 if x < .0: print '”x” must be atleast 0!' Python 当然也支持 else 语句, 语法如下: if expression: if_suite else: else_suite Python 还支持 elif (意指 “else-if ”)语句,语法如下: if expression1: if_suite elif expression2: elif_suite else: else_suite 在本书写作之时, 正在进行一个关于是否需要增加 switch/case 语句的讨论, 不过目前 并没有什么实质性的进展。 在将来版本的 Python 语言当中, 也非常有可能看到这样的“动 物”。 这个例子似乎有点奇怪、让人觉得困惑, 但是因为有了 Python 干净的语法, if-elif-else 语句并不象别人说的那么丑陋(以致不能让人接受)。如果你非要避免写一堆 if-elif-else 语句, 另一种变通的解决方案是使用 for 循环(参阅 2.13)来迭代你可能的 "cases"列表。 在第 8 章你可以学到更多有关 if, elif, else 条件语句的知识。 2.12 while 循环 标准 while 条件循环语句的语法类似 if. 再说一次, 要使用缩进来分隔每个子代码块。 while expression: while_suite Edit By Vheavens  Edit By Vheavens                                语句 while_suite 会被连续不断的循环执行, 直到表达式的值变成 0 或 False; 接着 Python 会执行下一句代码。 类似 if 语句, Python 的 while 语句中的条件表达式也不需要用 括号括起来。 >>> counter = 0 >>> while counter < 3: ... print 'loop #%d' % (counter) ... counter += 1 loop #0 loop #1 loop #2 while 循环和马上就要讲到的 for 循环会在第 8 章的循环一节进行详细讲解。 2.13 for 循环和 range()内建函数 Python 中的 for 循环与传统的 for 循环(计数器循环)不太一样, 它更象 shell 脚本里 的 foreach 迭代。Python 中的 for 接受可迭代对象(例如序列或迭代器)作为其参数,每次 迭代其中一个元素。 >>> print 'I like to use the Internet for:' I like to use the Internet for: >>> for item in ['e-mail', 'net-surfing', 'homework', 'chat']: ... print item ... e-mail net-surfing homework chat 上面例子的输出如果能在同一行就会美观许多。print 语句默认会给每一行添加一个换行 符。只要在 print 语句的最后添加一个逗号(,), 就可以改变它这种行为。 print 'I like to use the Internet for:' for item in ['e-mail', 'net-surfing', 'homework', 'chat']: Edit By Vheavens  Edit By Vheavens                                print item, print 上面的代码还添加了一个额外的没有任何参数的 print 语句, 它用来输出一个换行符。 否则, 提示信息就会立刻出现在我们的输出之后。 下面是以上代码的输出: I like to use the Internet for: e-mail net-surfing homework chat 为了输出清晰美观, 带逗号的 print 语句输出的元素之间会自动添加一个空格。通过指定 输出格式, 程序员可以最大程度的控制输出布局, 也不用担心这些自动添加的空格。它也可 以将所有数据放到一处输出--只需要将数据放在格式化运算符右侧的元组或字典中。 >>> who = 'knights' >>> what = 'Ni!' >>> print 'We are the', who, 'who say', what, what, what, what We are the knights who say Ni! Ni! Ni! Ni! >>> print 'We are the %s who say %s' % \ ... (who, ((what + ' ') * 4)) We are the knights who say Ni! Ni! Ni! Ni! 使用字符串格式运算符还允许我们做一些字符串输出之前的整理工作, 就象你在刚才的例 子中看到的一样。 通过演示一个让 Python for 循环更象传统循环(换言之, 计数循环)的示例, 我们来 结束对循环的介绍。因为我们不能改变 for 循环的行为(迭代一个序列), 我们可以生成一 个数字序列。 这样, 尽管我们确实是在迭代一个序列, 但是它至少展示的是递增计数的效果。 >>> for eachNum in [0, 1, 2]: ... print eachNum ... 0 1 2 在这个循环中, eachNum 包含的整数值可以用于显示, 也可以用于计算。因为我们要使 Edit By Vheavens  Edit By Vheavens                                用的数值范围可能会经常变化,Python 提供了一个 range()内建函数来生成这种列表。它正好 能满足我们的需要, 接受一个数值范围, 生成一个列表。 >>> for eachNum in range(3): ... print eachNum ... 0 1 2 对字符串来说, 很容易迭代每一个字符。 >>> foo = 'abc' >>> for c in foo: ... print c ... a b c range()函数经常和 len()函数一起用于字符串索引。 在这里我们要显示每一个元素及其 索引值: >>> foo = 'abc' >>> for i in range(len(foo)): ... print foo[i], '(%d)' % i ... a (0) b (1) c (2) 不过, 这些循环有一个约束, 你要么循环索引, 要么循环元素。这导致了 enumerate() 函数的推出(Python2.3 新增)。 它同时做到了这两点: >>> for i, ch in enumerate(foo): ... print ch, '(%d)' % i ... a (0) b (1) c (2) Edit By Vheavens  Edit By Vheavens                                2 .14 列表解析 这是一个让人欣喜的术语, 表示你可以在一行中使用一个for 循环将所有值放到一个列表 当中: >>> squared = [x ** 2 for x in range(4)] >>> for i in squared: ... print i 0 1 4 9 列表解析甚至能做更复杂的事情, 比如挑选出符合要求的值放入列表: >>> sqdEvens = [x ** 2 for x in range(8) if not x % 2] >>> >>> for i in sqdEvens: ... print i 0 4 16 36 2.15 文件和内建函数 open() 、file() 在你已经习惯一门语言的语法之后, 文件访问是相当重要的一环。在一些工作做完之后, 将它保存到持久存储是很重要的。 如何打开文件 handle = open(file_name, access_mode = 'r') file_name 变量包含我们希望打开的文件的字符串名字, access_mode 中 'r' 表示读取, 'w' 表示写入, 'a' 表示添加。其它可能用到的标声还有 '+' 表示读写, 'b'表示二进制访 问. 如果未提供 access_mode , 默认值为 'r'。如果 open() 成功, 一个文件对象句柄会被 返回。所有后续的文件操作都必须通过此文件句柄进行。当一个文件对象返回之后, 我们就可 以访问它的一些方法, 比如 readlines() 和 close().文件对象的方法属性也必须通过句点属 性标识法访问(参阅下面的核心笔记) Edit By Vheavens  Edit By Vheavens                                核心笔记:什么是属性? 属性是与数据有关的项目, 属性可以是简单的数据值, 也可以是可执行对象, 比如函数 和方法。哪些对象拥有属性呢? 很多。 类, 模块, 文件还有复数等等对象都拥有属性。 我如何访问对象属性? 使用句点属性标识法。 也就是说在对象名和属性名之间加一个句 点: object.attribute 下面有一些代码, 提示用户输入文件名, 然后打开一个文件, 并显示它的内容到屏幕上: filename = raw_input('Enter file name: ') fobj = open(filename, 'r') for eachLine in fobj: print eachLine, fobj.close() 我们的代码没有用循环一次取一行显示, 而是做了点改变。我们一次读入文件的所有行, 然后关闭文件, 再迭代每一行输出。这样写代码的好处是能够快速完整的访问文件。内容输出 和文件访问不必交替进行。这样代码更清晰, 而且将不相关的任务区分开来。需要注意的一点 是文件的大小。 上面的代码适用于文件大小适中的文件。对于很大的文件来说, 上面的代码 会占用太多的内存, 这时你最好一次读一行。(下一节有一个好例子) 我们的代码中另一个有趣的语句是我们又一次在 print 语句中使用逗号来抑制自动生成 的换行符号。 为什么要这样做?因为文件中的每行文本已经自带了换行字符, 如果我们不抑 制 print 语句产生的换行符号, 文本在显示时就会有额外的空行产生。 file()内建函数是最近才添加到 Python 当中的。它的功能等同于 open(), 不过 file() 这个名字可以更确切的表明它是一个工厂函数。(生成文件对象)类似 int()生成整数对象, dict()生成字典对象。在第 9 章, 我们详细介绍文件对象, 及它们的内建方法属性, 以及如 何访问本地文件系统。 请参考第 9 章以了解详细信息。 2.16 错误和异常 编译时会检查语法错误, 不过 Python 也允许在程序运行时检测错误。当检测到一个错误, Python 解释器就引发一个异常, 并显示异常的详细信息。程序员可以根据这些信息迅速定位 问题并进行调试, 并找出处理错误的办法。 要给你的代码添加错误检测及异常处理, 只要将它们封装在 try-except 语句当中。 try 之后的代码组, 就是你打算管理的代码。 except 之后的代码组, 则是你处理错误的代码。 Edit By Vheavens  Edit By Vheavens                                try: filename = raw_input('Enter file name: ') fobj = open(filename, 'r') for eachLine in fobj: print eachLine, fobj.close() except IOError, e: print 'file open error:', e 程序员也可以通过使用 raise 语句故意引发一个异常。在第 10 章你可以学到更多有关 Python 异常的知识。 2.17 函数 类似其它的语言, Python 中的函数使用小括号( () )调用。函数在调用之前必须先定义。 如果函数中没有 return 语句, 就会自动返回 None 对象。 Python 是通过引用调用的。 这意味着函数内对参数的改变会影响到原始对象。不过事实 上只有可变对象会受此影响, 对不可变对象来说, 它的行为类似按值调用。 如何定义函数 def function_name([arguments]): "optional documentation string" function_suite 定义一个函数的语法由 def 关键字及紧随其后的函数名再加上该函数需要的几个参数组 成。函数参数(比较上面例子中的 arguments)是可选的, 这也是为什么把它们放到中括号中 的原因。(在你的代码里千万别写上中括号!)这个语句由一个冒号(:)结束(与 if 和 while 语句的结束方式一样), 之后是代表函数体的代码组, 下面是一个简短的例子: def addMe2Me(x): 'apply + operation to argument' return (x + x) 这个函数, 干的是“在我的值上加我”的活。它接受一个对象, 将它的值加到自身, 然 后返回和。对于数值类型参数, 它的结果是显而易见的, 不过我要在这里指出, 加号运算符 Edit By Vheavens  Edit By Vheavens                                几乎与所有数据类型工作。换句话说, 几乎所有的标准数据类型都支持 + 运算符, 不管是数 值相加还是序列合并。 如何调用函数 >>> addMe2Me(4.25) 8.5 >>> >>> addMe2Me(10) 20 >>> >>> addMe2Me('Python') 'PythonPython' >>> >>> addMe2Me([-1, 'abc']) [-1, 'abc', -1, 'abc'] Python 语言中调用函数与在其它高级语言中一样, 函数名加上函数运算符, 一对小括 号。括号之间是所有可选的参数。即使一个参数也没有, 小括号也不能省略。注意一下, + 运 算符在非数值类型中如何工作。 默认参数 函数的参数可以有一个默认值, 如果提供有默认值,在函数定义中, 参数以赋值语句的 形式提供。事实上这仅仅是提供默认参数的语法,它表示函数调用时如果没有提供这个参数, 它 就取这个值做为默认值。 >>> def foo(debug=True): ... 'determine if in debug mode with default argument' ... if debug: ... print 'in debug mode' ... print 'done' ... >>> foo() in debug mode done >>> foo(False) done Edit By Vheavens  Edit By Vheavens                                在上面的例子里, debug 参数有一个默认值 True. 如果我们没有传递参数给函数 foo(), debug 自动拿到一个值, True. 在第二次调用 foo()时, 我们故意传递一个参数 False 给 foo(), 这样, 默认参数就没有被使用。 函数拥有的特性远比我们在这里介绍的多, 请阅读 第 11 章以了解更详细的函数的信息。 2.18 类 类是面向对象编程的核心, 它扮演相关数据及逻辑的容器角色。它们提供了创建“真实” 对象(也就是实例)的蓝图。因为Python 并不强求你以面向对象的方式编程(与Java 不同), 此 刻你也可以不学习类。 不过我们还是在这儿放了些例子, 以方便感兴趣的读者浏览。 如何定义类 class ClassName(base_class[es]): "optional documentation string" static_member_declarations method_declarations 使用 class 关键字定义类。 可以提供一个可选的父类或者说基类; 如果没有合适的基类, 那就使用 object 作为基类。class 行之后是可选的文档字符串, 静态成员定义, 及方法定 义。 class FooClass(object): """my very first class: FooClass""" version = 0.1 # class (data) attribute def __init__(self, nm='John Doe'): """constructor""" self.name = nm # class instance (data) attribute print 'Created a class instance for', nm def showname(self): """display instance attribute and class name""" print 'Your name is', self.name print 'My name is', self.__class__.__name__ def showver(self): """display class(static) attribute""" print self.version # references FooClass.version def addMe2Me(self, x): # does not use 'self' """apply + operation to argument""" return x + x Edit By Vheavens  Edit By Vheavens                                在上面这个类中, 我们定义了一个静态变量 version, 它将被所有实例及四个方法共享, __init__(), showname(), showver(), 及熟悉的 addMe2Me(). 这些 show*()方法并没有做什 么有用的事情, 仅仅输出对应的信息。 __init__() 方法有一个特殊名字, 所有名字开始和 结束都有两个下划线的方法都是特殊方法。 当一个类实例被创建时, __init__() 方法会自动执行, 在类实例创建完毕后执行, 类 似构建函数。__init__() 可以被当成构建函数, 不过不象其它语言中的构建函数, 它并不创 建实例--它仅仅是你的对象创建后执行的第一个方法。它的目的是执行一些该对象的必要的初 始化工作。通过创建自己的 __init__() 方法, 你可以覆盖默认的 __init__()方法(默认的 方法什么也不做),从而能够修饰刚刚创建的对象。在这个例子里, 我们初始化了一个名为 name 的类实例属性(或者说成员)。这个变量仅在类实例中存在, 它并不是实际类本身的一部分。 __init__()需要一个默认的参数, 前一节中曾经介绍过。毫无疑问,你也注意到每个方法都有 的一个参数, self. 什么是 self ? 它是类实例自身的引用。其他语言通常使用一个名为 this 的标识符。 如何创建类实例 >>> foo1 = FooClass() Created a class instance for John Doe 屏幕上显示的字符串正是自动调用 __init__() 方法的结果。当一个实例被创建, __init__()就会被自动调用。不管这个__int__()是自定义的还是默认的。 创建一个类实例就像调用一个函数, 它们确实拥有一样的语法。它们都是可调用对象。类 实例使用同样的函数运算符调用一个函数或方法。既然我们成功创建了第一个类实例, 那现在 来进行一些方法调用: >>> foo1.showname() Your name is John Doe My name is __main__.FooClass >>> >>> foo1.showver() 0.1 >>> print foo1.addMe2Me(5) 10 >>> print foo1.addMe2Me('xyz') Edit By Vheavens  Edit By Vheavens                                xyzxyz 每个方法的调用都返回我们期望的结果。比较有趣的数据是类名字。在showname()方法中, 我们显示 self.__class__.__name__ 变量的值。对一个实例来说, 这个变量表示实例化它的 类的名字。(self.__class__引用实际的类)。在我们的例子里, 创建类实例时我们并未传递 名字参数, 因此默认参数 'John Doe' 就被自动使用。在我们下一个例子里, 我们将指定一 个参数。 >>> foo2 = FooClass('Jane Smith') Created a class instance for Jane Smith >>> foo2.showname() Your name is Jane Smith My name is FooClass 第十三章将详细介绍 Python 类和类实例。 2.19 模块 模块是一种组织形式, 它将彼此有关系的 Python 代码组织到一个个独立文件当中。 模块可以包含可执行代码, 函数和类或者这些东西的组合。 当你创建了一个 Python 源文件,模块的名字就是不带 .py 后缀的文件名。一个模块创 建之后, 你可以从另一个模块中使用 import 语句导入这个模块来使用。 如何导入模块 import module_name 如何访问一个模块函数或访问一个模块变量 一旦导入完成, 一个模块的属性(函数和变量)可以通过熟悉的 .句点属性标识法访问。 module.function() module.variable 现在我们再次提供 Hello World! 例子, 不过这次使用 sys 模块中的输出函数。 >>> import sys Edit By Vheavens  Edit By Vheavens                                >>> sys.stdout.write('Hello World!\n') Hello World! >>> sys.platform 'win32' >>> sys.version '2.4.2 (#67, Sep 28 2005, 10:51:12) [MSC v.1310 32 bit (Intel)]' 这些代码的输出与我们使用 print 语句完全相同。 唯一的区别在于这次调用了标准输出 的 write()方法,而且这次需要显式的在字符串中提供换行字符, 不同于print 语句, write() 不会自动在字符串后面添加换行符号。 关于模块和导入, 你可以在第 12 章中得到更多有用的信息。在那里会详细介绍本章上面 所有提到的主题,希望我们提供的快速入门能达到你迅速使用 Python 开始工作的目标。 核心笔记:什么是“PEP”? 在本书中你会经常看到 PEP 这个字眼。 一个 PEP 就是一个 Python 增强提案(Python Enhancement Proposal), 这也是在新版 Python 中增加新特性的方式。 从初学者的角度看, 它们是一些高级读物, 它们不但提供了新特性的完整描述, 还有添加这些新特性的理由, 如 果需要的话, 还会提供新的语法、 技术实现细节、向后兼容信息等等。在一个新特性被整合 进 Python 之前, 必须通过 Python 开发社区, PEP 作者及实现者, 还有 Python 的创始人, Guido van Rossum(Python 终身的仁慈的独裁者)的一致同意。PEP1 阐述了 PEP 的目标及书写指南。 在 PEP0 中可以找到所有的 PEP。 PEP 索引的网址是: http://python.org/dev/peps. 2.20 实用的函数 本章中, 我们用到了很多实用的内建函数。我们在表 2.1 中总结了这些函数, 并且提供 了一些其它的有用函数。(注意我们并没有提供完整的使用语法,仅提供了我们认为可能对你 有用的部分) 表 2.1 对新 Python 程序员有用的内建函数 函数 描述 dir([obj]) 显示对象的属性,如果没有提供参数, 则显示全局变量的名字 Edit By Vheavens  Edit By Vheavens                                help([obj]) 以一种整齐美观的形式 显示对象的文档字符串, 如果没有提供任何参 数, 则会进入交互式帮助。 int(obj) 将一个对象转换为整数 len(obj) 返回对象的长度 open(fn, mode) 以 mode('r' = 读, 'w'= 写)方式打开一个文件名为 fn 的文件 range([[start,]stop[,step]) 返回一个整数列表。起始值为 start, 结束值为 stop - 1; start 默认值为 0, step默认值为1。 raw_input(str) 等待用户输入一个字符串, 可以提供一个可选的参数 str 用作提示信 息。 str(obj) 将一个对象转换为字符串 type(obj) 返回对象的类型(返回值本身是一个 type 对象!) 2.21 练习 2–1. 变量, print 和字符串格式化运算符。启动交互式解释器。给一些变量赋值(字 符串,数值等等)并通过输入变量名显示它们的值。再用 print 语句做同样的事。这二者有 何区别? 也尝试着使用字符串格式运算符 %, 多做几次, 慢慢熟悉它。 2–2. P程序输出, 阅读下面的 Python 脚本: #!/usr/bin/env python 1 + 2 * 4 (a) 你认为这段脚本是用来做什么的? (b) 你认为这段脚本会输出什么? (c) 输入以上代码,并保存为脚本,然后运行它。它所做的与你的预期一样吗?为什么一 样/不一样? (d) 这段代 独码单执 行和在交互解释器中执行有何不同?试一下,然后写出结果 (e) 如何改 个进这 脚本, 以便它能和你想像的一样工作? 2–3. 数值和运算符 启动交互解释器,使用 Python 两个数对值(任意类型)进行加、减、乘、除运算。然后使 用取余运算符来得到两个数相除的余数, 最后使用乘方运算符求 A 数的 B 次方。 2–4. 使用raw_input()函数得到用户输入 Edit By Vheavens  Edit By Vheavens                                (a) 创建一段脚本使用 raw_input() 内建函数从用户输入得到一个字符串,然后显示 个这 用户刚刚键入的字符串。 (b) 添加一段类似的代码,不 过这次输入的是数值。将输入数据转换为一个数值对象,(使 用 int()或其它数值转换函数) 并将 个这值显示给用户看。(注意,如果你用的是早于 1.5 的版 本,你需要使用 string.ato*() 函数执行 种这转换) 2–5. 循环和数字 分别使用 while 和 for 创建一个循环: (a) 写一个 while 循环,输出整数从 0 到 10。(要确保是从 0 到 10, 而不是从 0到9或 从 1 到 10) (b) 做同 (a) 一样的事, 不过这次使用 range() 内建函数。 2–6. 条件判断 判断一个数是正数,还是 数负 , 或者等于 0. 开始先用固定的数值,然 后修改你的代码支持用户输入数值再进行判断。 2–7. 循环和字串 从用户那里接受一个字符串输入,然后逐字符显示该字符串。先用 while 循 环实现,然后再用 for 循环实现。 2–8. 循环和运算符 创建一个包含五个固定数值的列表或元组,输出他们的和。然后修 改你的代码为接受用户输入数值。 分别使用 while 和 for 循环实现。 2–9. 循环和运算符 创建一个包含五个固定数值的列表或元组,输出他们的平均值。本练习的难 点之一是通过除法得到平均值。 你会发现整数除会截去小数,因 此你必须使用浮点除以得到更 精确的结果。 float()内建函数可以帮助你实现这一功能。 2–10. 带循环和条件判断的用户输入 使用 raw_input()函数来提示用户输入一个 1 和 100 之间的 数,如果用户输入的数满足 个条这 件,显示成功并退出。否则显示一个错误信息然后再次提示 用户输入数值,直到满足条件为止。 2–11. 带文本菜单的程序 写一个带文本菜单的程序,菜单项如下(1)取五个数的和 (2) 取五个 数的平均值....(X)退出。由用户做一个选择,然后执行相应的功能。当用户选择退出时程序 结束。 个这 程序的有用之处在于用户在功能之间切换不需要一遍一遍的重新启你动 的脚本。(这 开对发人员测试自己的程序也会大有用处) 2–12. dir()内建函数 Edit By Vheavens  Edit By Vheavens                                (a) 启动 Python 交互式解释器, 通过直接键入dir()回车以执行 dir()内建函数。你看到 什么? 显示你看到的每一个列表元素的值,记下实际值和你想像的值 (b) 你会问, dir()函数是干什么的?我们已经知道在 dir 后边加上一对括号可以执行dir() 内建函数, 如果不加括号会如何? 试一试。 解释器返回 你给 什么信息? 你个认为这 信息表 示什么意思 ? (c) type() 内建函数接收任意的 Python 对象做 参数并为 返回他们的类型。 在解释器中键 入 type(dir), 看看你得到的是什么? (d) 本练习的最后一部分, 我 来瞧们 一瞧 Python的文档字符串。 通过 dir.__doc__ 可 以访问 dir()内建函数的文档字符串。print dir.__doc__可以显示 个这 字符串的内容。 许多内建 函数,方法,模块及模 属块 性都有相应的文档字符串。我们希望你在你的代码中也要 写书 文档 字符串, 它会对使用这些代码的人提供及时方便的帮助。 2–13. 利用 dir() 找出 sys 模块中更多的东西。 (a) 启动Python 交互解释器, 执行 dir()函数, 然后键入 import sys 以导入 sys 模块。 再次执行 dir()函数以确认 sys 模块被正确的导入。 然后执行 dir(sys) , 你就可以看到 sys 模块的所有属性了。 (b) 显示 sys 模块的版本号属性及平台变量。记住在属性名前一定要加 sys. ,这表示 个属这 性是 sys 模块的。其中 version 变量保存着你使用的 Python 解释器版本, platform 属性则包含你运行Python时使用的计算机平台信息。 (c) 最后, 调用 sys.exit() 函数。 这是一种热键之外的另一种退出 Python 解释器的方 式 。 2–14. 重写 2.4 小节中 print 语句里的算术表达式, 试着在 个这 表达式中添加合适的 括号以便它能正常工作。 2–15. 元素排序 (a)让用户输入三个数 并将值 分 将它别们保存到 3 个不同的变量中。不使用列表或排序算法, 自己写代 来码对这三个数由小到大排序。(b)修改(a)的解决方案,使之从大到小排序 2–16. 文件 键入2.15节的文件显示的代码, 然后运行它, 看看能否在你的系统上正常工作,然后试 一下其它的输入文件。 Edit By Vheavens  Edit By Vheavens                                Python基础  本章主题 z 语句和语法 z 变量赋值 z 标识符和关键字 z 基本风格指南 z 内存管理 z 第一个 Python 程序 Edit By Vheavens  Edit By Vheavens                                我们下一个目标是了解基本的 Python 语法,介绍一些基本的编程风格,之后简要介绍一 下标识符、变量和关键字。我们也会讨论变量占用的内存是如何分配和回收的。最后,我们会 给出一个较大的 Python 样例程序,让你实际体验一下这些特性。无须担心,在你畅游 Python 的过程中有很多救生员在保护着你。 3.1 语句和语法 Python 语句中有一些基本规则和特殊字符: z 井号(#)表示之后的字符为 Python 注释 z 换行 (\n) 是标准的行分隔符(通常一个语句一行) z 反斜线 ( \ ) 继续上一行 z 分号 ( ; )将两个语句连接在一行中 z 冒号 ( : ) 将代码块的头和体分开 z 语句(代码块)用缩进块的方式体现 z 不同的缩进深度分隔不同的代码块 z Python 文件以模块的形式组织 3.1.1 注释( # ) Edit By Vheavens  Edit By Vheavens                                首要说明的事情是:尽管 Python 是可读性最好的语言之一,这并不意味着程序员在代码中 就可以不写注释。和很多 Unix 脚本类似,Python 注释语句从 # 字符开始,注释可以在一行 的任何地方开始,解释器会忽略掉该行 # 之后的所有内容。要正确的使用注释。 3.1.2 继续( \ ) Python 语句,一般使用换行分隔,也就是说一行一个语句。一行过长的语句可以使用反斜 杠( \ ) 分解成几行,如下例: # check conditions if (weather_is_hot == 1) and \ (shark_warnings == 0): send_goto_beach_mesg_to_pager() 有两种例外情况一个语句不使用反斜线也可以跨行。在使用闭合操作符时,单一语句可以 跨多行,例如:在含有小括号、中括号、花括号时可以多行书写。另外就是三引号包括下的字 符串也可以跨行书写。如下例: # display a string with triple quotes print'''hi there, this is a long message for you that goes over multiple lines... you will find out soon that triple quotes in Python allows this kind of fun! it is like a day on the beach!''' # set some variables go_surf, get_a_tan_while, boat_size, toll_money = (1,'windsurfing', 40.0, -2.00) 如果要在使用反斜线换行和使用括号元素换行作一个选择,我们推荐使用括号,这样可读 性会更好。 3.1.3 多个语句构成代码组(:): 缩进相同的一组语句构成一个代码块,我们称之代码组。像 if、while、def 和 class 这样 的复合语句,首行以关键字开始,以冒号( : )结束,该行之后的一行或多行代码构成代码组。 我们将首行及后面的代码组称为一个子句(clause)。 Edit By Vheavens  Edit By Vheavens                                3.1.4 代码组由不同的缩进分隔 我们在章节 2.10 中曾提到,Python 使用缩进来分隔代码组。代码的层次关系是通过同样 深度的空格或制表符缩进体现的。同一代码组的代码行必须严格左对齐(左边有同样多的空格 或同样多的制表符),如果不严格遵守这个规则,同一组的代码就可能被当成另一个组,甚至 会导致语法错误。 核心风格:缩进四个空格宽度,避免使用制表符 对一个初次使用空白字符作为代码块分界的人来说,遇到的第一个问题是:缩进多大宽度 才合适?两个太少,六到八个又太多,因此我们推荐使用四个空格宽度。需要说明一点,不同 的文本编辑器中制表符代表的空白宽度不一,如果你的代码要跨平台应用,或者会被不同的编 辑器读写,建议你不要使用制表符。使用空格或制表符这两种风格都得到了 Python 创始人 Guido van Rossum 的支持,并被收录到 Python 代码风格指南文档。在本章第 3.4 小节中你会看到同 样的建议。 随着缩进深度的增加,代码块的层次也在加深,没有缩进的代码块是最高层次的,别称做 脚本的“main”部分。 使用缩进对齐这种方式组织代码,不但代码风格优雅,而且也大大提高了代码的可读性。 而且它有效的避免了"悬挂 else"(dangling-else)问题,和未写大括号的单一子句问题。(如 果 C 语言中 if 语句没写大括号,而后面却跟着两个缩近的语句,这会造成不论条件表达式是 否成立,第二个语句总会执行。这种问题很难调试,不知道困惑了多少程序员。) 最后一点,由于 Python 只使用缩进方式表达代码块逻辑,因此“神圣的大括号战争”永远 不会发生在 Python 身上。C、C++和 Java 语言中,开始大括号可以在第一行的尾部,也可以在 第二行的头部,也可以在第二行空几格后开始,这就造成不同的人选择不同的风格,于是你就 会看到大括号战争的场景了。 3.1.5 同一行书写多个语句(;) 分号( ; )允许你将多个语句写在同一行上,语句之间用分号隔开,而这些语句也不能在这 行开始一个新的代码块。这里有一个例子: Edit By Vheavens  Edit By Vheavens                                import sys; x = 'foo'; sys.stdout.write(x + '\n') 必须指出一点, 同一行上书写多个语句会大大降低代码的可读性,Python 虽然允许但不 提倡你这么做。 3.1.6 模块 每一个 Python 脚本文件都可以被当成是一个模块。模块以磁盘文件的形式存在。当一个模 块变得过大,并且驱动了太多功能的话,就应该考虑拆一些代码出来另外建一个模块。模块里 的代码可以是一段直接执行的脚本,也可以是一堆类似库函数的代码,从而可以被别的模块导 入(import)调用。记得我们在上一章中曾提到过,模块可以包含直接运行的代码块、类定义、 函数定义或这几者的组合。 3.2 变量赋值 本节主题是变量赋值。我们将在 3.3 小节中讨论什么样的标识符才是合法的变量名。 赋值运算符 Python 语言中, 等号(=)是主要的赋值运算符。(其他的是增量赋值运算符,参见下节) anInt = -12 aString = 'cart' aFloat = -3.1415 * (5.0 ** 2) anotherString = 'shop' + 'ping' aList = [3.14e10, '2nd elmt of a list', 8.82-4.371j] 注意,赋值并不是直接将一个值赋给一个变量, 尽管你可能根据其它语言编程经验认为应 该如此。在 Python 语言中,对象是通过引用传递的。在赋值时,不管这个对象是新创建的,还 是一个已经存在的,都是将该对象的引用(并不是值)赋值给变量。如果此刻你还不是 100%理 解清楚,也不用着急。 在本章的后面部分,我们还会再讨论这个话题, 现在你只需要有这么 一个概念即可。 同样的, 如果你比较熟悉C, 你会知道赋值语句其实是被当成一个表达式(可以返回值)。 Edit By Vheavens  Edit By Vheavens                                不过这条并不适合于 Python, Python 的赋值语句不会返回值。类似下面的语句在 Python 中是 非法的: >>> x = 1 >>> y = (x = x + 1) # assignments not expressions! File "", line 1 y = (x = x + 1) ^ SyntaxError: invalid syntax 链式赋值没问题, 看(本章稍后部分会给出更多的例子): >>> y = x = x + 1 >>> x, y (2, 2) 增量赋值 从 Python 2.0 开始, 等号可以和一个算术运算符组合在一起, 将计算结果重新赋值给 左边的变量。这被称为增量赋值, 类似下面这样的语句: x = x + 1 现在可以被写成: x += 1 增量赋值通过使用赋值运算符,将数学运算隐藏在赋值过程当中。如果您用过 C、C++或者 Java,会觉得下面的运算符很熟悉。 += -= *= /= %= **= <<= >>= &= ^= |= 增量赋值相对普通赋值不仅仅是写法上的改变,最有意义的变化是第一个对象(我们例子 中的 A)仅被处理一次。可变对象会被就地修改(无修拷贝引用), 不可变对象则和 A = A + Edit By Vheavens  Edit By Vheavens                                B 的结果一样(分配一个新对象),我们前面提到过,有一个例外就是 A 仅被求值一次。 >>> m = 12 >>> m %= 7 >>> m 5 >>> m **= 2 >>> m 25 >>> aList = [123, 'xyz'] >>> aList += [45.6e7] >>> aList [123, 'xyz', 456000000.0] Python 不支持类似 x++ 或 --x 这样的前置/后置自增/自减运算。 多重赋值 >>> x = y = z = 1 >>> x 1 >>> y 1 >>> z 1 在上面的例子中,一个值为 1 的整数对象被创建,该对象的同一个引用被赋值给 x、y 和 z 。也就是将一个对象赋给了多个变量。当然,在 Python 当中,将多个对象赋给多个变量也是 可以的。 “多元”赋值 另一种将多个变量同时赋值的方法我们称为多元赋值(multuple)。这不是官方 Python 术 语, 而是我们将 "mul-tuple"连在一起自造的。因为采用这种方式赋值时, 等号两边的对象 都是元组(我们在 2.8 节讲过元组是一种 Python 基本数据类型)。 >>> x, y, z = 1, 2, 'a string' >>> x Edit By Vheavens  Edit By Vheavens                                1 >>> y 2 >>> z 'a string' 在上面的例子里, 两个整数对象(值分别为1和2)及一个字符串对象, 被分别赋值给 x, y 和 z。通常元组需要用圆括号(小括号)括起来,尽管它们是可选的。我们建议总是加上 圆括号以使得你的代码有更高的可读性。 >>> (x, y, z) = (1, 2, 'a string') 在其它类似 C 的语言中, 如果你要交换两个值, 你会想到使用一个临时变量比如 tmp 来 临时保存其中一个值: /* C 语言中两个变量交换 */ tmp = x; x = y; y = tmp; 在上面的 C 代码片段中,变量 x 和变量 y 的值被互相交换。 临时变量 tmp 用于在将 y 赋 值给 x 前先保存 x 的值。将 y 的值赋给 x 之后, 才可以将保存在 tmp 变量中的 x 的值赋给 y。 Python 的多元赋值方式可以实现无需中间变量交换两个变量的值。 # swapping variables in Python >>> x, y = 1, 2 >>> x 1 >>> y 2 >>> x, y = y, x >>> x 2 >>> y 1 显然, Python 在赋值之前已经事先对x和y的新值做了计算。 Edit By Vheavens  Edit By Vheavens                                3.3 标识符 标识符是电脑语言中允许作为名字的有效字符串集合。其中,有一部分是关键字,构成语 言的标识符。这样的标识符是不能做它用的标识符的,否则会引起语法错误(SyntaxError 异 常)。 Python 还有称为 built-in 标识符集合,虽然它们不是保留字,但是不推荐使用这些特别 的名字(见 3.3.3)。 3.3.1 合法的 Python 标识符 Python 标识符字符串规则和其他大部分用 C 编写的高级语言相似: z 第一个字符必须是字母或下划线(_) z 剩下的字符可以是字母和数字或下划线 z 大小写敏感 标识符不能以数字开头;除了下划线,其他的符号都不允许使用。处理下划线最简单的方 法是把它们当成字母字符。大小写敏感意味着标识符foo 不同于 Foo,而这两者也不同于FOO。 3.3.2 关键字 Python 的关键字列在表 3.1 中。一般来说,任何语言的关键字应该保持相对的稳定,但是 因为 Python 是一门不断成长和进化的语言,关键字列表和 iskeyword()函数都放入了 keyword 模块以便查阅。 Edit By Vheavens  Edit By Vheavens                                a. 从 Python1.4 开始关键字 access 就被废除了 b. Python2.6 时加入 c. Python1.5 时加入 d. Python2.3 时加入 e. Python2.4 中非关键字常量 3.3.3 内建 除了关键字之外,Python 还有可以在任何一级代码使用的“内建”的名字集合,这些名字 可以由解释器设置或使用。虽然 built-in 不是关键字,但是应该把它当作“系统保留字”,不 做他用。然而,有些情况要求覆盖(也就是:重定义,替换)它们。Python 不支持重载标识符, 所以任何时刻都只有一个名字绑定。 我们还可以告诉高级读者 built-in 是__builtins__模块的成员,在你的程序开始或在交互 解释器中给出>>>提示之前,由解释器自动导入的。把它们看成适用在任何一级 Python 代码的 全局变量。 3.3.4 专用下划线标识符 Python 用下划线作为变量前缀和后缀指定特殊变量。稍后我们会发现,对于程序来说,其 中的有些变量是非常有用的,而其他的则是未知或无用的。这里对 Python 中下划线的特殊用法 Edit By Vheavens  Edit By Vheavens                                做了总结: z _xxx 不用'from module import *'导入 z __xxx__系统定义名字 z __xxx 类中的私有变量名 核心风格:避免用下划线作为变量名的开始 因为下划线对解释器有特殊的意义,而且是内建标识符所使用的符号,我们建议程序员避 免用下划线作为变量名的开始。一般来讲,变量名_xxx 被看作是“私有的”,在模块或类外不 可以使用。当变量是私有的时候,用_xxx 来表示变量是很好的习惯。因为变量名__xxx__对 Python 来说有特殊含义,对于普通的变量应当避免这种命名风格。 3.4 基本风格指南 注释 注释对于自己和后来人来说都是非常重要的,特别是对那些很久没有被动过的代码而言, 注释更显得有用了。既不能缺少注释,也不能过度使用注释。尽可能使注释简洁明了,并放在 最合适的地方。这样注释便为每个人节省了时间和精力。记住,要确保注释的准确性。 文档 Python 还提供了一个机制,可以通过__doc__特别变量,动态获得文档字串。在模块,类 声明,或函数声明中第一个没有赋值的字符串可以用属性 obj.__doc__来进行访问,其中 obj 是一个模块,类,或函数的名字。这在运行时刻也可以运行。 缩进 因为缩进对齐有非常重要的作用,您得考虑用什么样的缩进风格才让代码容易阅读。在选 择要空的格数的时候,常识也起着非常大的作用。 1 或 2 可能不够,很难确定代码语句属于哪个块 8 至 10 可能太多,如果代码内嵌的层次太多,就会使得代码很难阅读。四个空格非常的 流行,更不用说 Python 的创造者也支持这种风格。五和六个也不坏,但是文本编辑器通常不支 持这样的设置,所以也不经常使用。三个和七个是边界情况。 Edit By Vheavens  Edit By Vheavens                                当使用制表符 Tab 的时候,请记住不同的文本编辑器对它的设置是不一样。推荐您不要 使用 Tab,如果您的代码会存在并运行在不同的平台上,或者会用不同的文本编辑器打开,推 荐您不要使用 Tab。 选择标识符名称 好的判断也适用于选择标志符名称,请为变量选择短而意义丰富的标识符。虽然变量名的 长度对于今天的编程语言不再是一个问题,但是使用简短的名字依然是个好习惯,这个原则同 样使用于模块(Python 文件)的命名。 Python 风格指南 Guido van Rossum 在多年前写下Python 代码风格指南。目前它已经被至少三个PEP 代替: 7(C 代码风格指南)、8(Python 代码风格指南)和 257(文档字符串规范)。这些 PEP 被归 档、维护并定期更新。 渐渐的,你会听到“Pythonic”这个术语,它指的是以 Python 的方式去编写代码、组织 逻辑,及对象行为。更多时间过后,你才会真正理解它的含义。PEP 20 写的是 Python 之禅, 你可以从那里开始你探索“Pythonic”真正含义的旅程。如果你不能上网,但想看到这篇诗句, 那就从你的 Python 解释器输入 import this 然后回车。下面是一些网上资源: www.Python.org/doc/essays/styleguide.html www.Python.org/dev/peps/pep-0007/ www.Python.org/dev/peps/pep-0008/ www.Python.org/dev/peps/pep-0020/ www.Python.org/dev/peps/pep-0257/ 3.4.1 模块结构和布局 用模块来合理组织你的 Python 代码是简单又自然的方法。你应该建立一种统一且容易阅读 的结构,并将它应用到每一个文件中去。下面就是一种非常合理的布局: # (1) 起始行(Unix) # (2) 模块文档 Edit By Vheavens  Edit By Vheavens                                # (3) 模块导入 # (4) 变量定义 # (5) 类定义 # (6) 函数定义 # (7) 主程序 图 3–1 一个典型模块的内部结构图解。 (1) 起始行 通常只有在类 Unix 环境下才使用起始行,有起始行就能够仅输入脚本名字来执行脚本,无 需直接调用解释器。 (2)模块文档 简要介绍模块的功能及重要全局变量的含义,模块外可通过 module.__doc__ 访问这些内 容。 (3)模块导入 导入当前模块的代码需要的所有模块;每个模块仅导入一次(当前模块被加载时);函数 内部的模块导入代码不会被执行, 除非该函数正在执行。 (4)变量定义 这里定义的变量为全局变量,本模块中的所有函数都可直接使用。从好的编程风格角度说, 除非必须,否则就要尽量使用局部变量代替全局变量,如果坚持这样做,你的代码就不但容易 维护,而且还可以提高性能并节省内存。 (5)类定义语句 所有的类都需要在这里定义。当模块被导入时 class 语句会被执行, 类也就会被定义。类 的文档变量是 class.__doc__。 (6)函数定义语句 此处定义的函数可以通过 module.function()在外部被访问到,当模块被导入时 def 语句 会被执行, 函数也就都会定义好,函数的文档变量是 function.__doc__。 (7) 主程序 无论这个模块是被别的模块导入还是作为脚本直接执行,都会执行这部分代码。通常这里 不会有太多功能性代码,而是根据执行的模式调用不同的函数。 Edit By Vheavens  Edit By Vheavens                                Figure 3–1 Typical Python file structure 推荐代码风格:主程序调用 main()函数 主程序代码通常都和你前面看到的代码相似,检查 __name__ 变量的值然后再执行相应的 调用(参阅下一页的核心笔记)。主程序中的代码通常包括变量赋值, 类定义和函数定义,随 后检查__name__来决定是否调用另一个函数(通常调用 main()函数)来完成该模块的功能。主 程序通常都是做这些事。(我们上面的例子中使用 test()而不是 main()是为了避免你在读到核 心笔记前感到迷惑。) 不管用什么名字,我们想强调一点那就是:这儿是放置测试代码的好地 方。我们在 3.4.2 小节中曾经说过,大部分的 Python 模块都是用于导入调用的,直接运行模块 应该调用该模块的回归测试代码。 很多项目都是一个主程序,由它导入所有需要的模块。所以请记住,绝大部分的模块创建 的目的是为了被别人调用而不是作为独立执行的脚本。我们也很可能创建一个Python 库风格的 Edit By Vheavens  Edit By Vheavens                                模块,这种模块的创建目的就是为了被其他模块调用。总之,只有一个模块,也就是包含主程 序的模块会被直接执行,或由用户通过命令行执行,或作为批处理执行, 或由 Unix cron任务 定时执行,或通过 Web 服务器调用,或通过 GUI 执行。 时刻记住一个事实,那就是所有的模块都有能力来执行代码。最高级别的 Python 语句-- 也就是说, 那些没有缩进的代码行在模块被导入时就会执行, 不管是不是真的需要执行。由 于有这样一个“特性”,比较安全的写代码的方式就是除了那些真正需要执行的代码以外, 几 乎所有的功能代码都在函数当中。再说一遍, 通常只有主程序模块中有大量的顶级可执行代码, 所有其它被导入的模块只应该有很少的顶级执行代码,所有的功能代码都应该封装在函数或类 当中。 (参阅核心笔记了解更多信息) 核心笔记:__name__ 指示模块应如何被加载 由于主程序代码无论模块是被导入还是被直接执行都会运行, 我们必须知道模块如何决定 运行方向。一个应用程序可能需要导入另一个应用程序的一个模块,以便重用一些有用的代码 (否则就只能用拷贝粘贴那种非面向对象的愚蠢手段)。这种情况下,你只想访问那些位于其 它应用程序中的代码,而不是想运行那个应用程序。因此一个问题出现了,“Python 是否有 一种方法能在运行时检测该模块是被导入还是被直接执行呢?” 答案就是......(鼓声雷 动).....没错! __name__ 系统变量就是正确答案。 如果模块是被导入, __name__ 的值为模块名字 如果模块是被直接执行, __name__ 的值为 '__main__' 3.4.2 在主程序中书写测试代码 优秀的程序员和软件工程师,总是会为我们的应用程序提供一组测试代码或者简单教程。 对那些仅仅为了让别的程序导入而创建的模块来说, Python 有效的简化了这个任务。这些模 块理论上永远不会被直接执行, 那么,在这个模块被直接执行时进行系统测试岂不妙哉? 设 置起来难吗? 一点儿也不。 测试代码仅当该文件被直接执行时运行, 也就是说不是被别的模块导入时。上文及核心笔 记中提到如何判断一个模块是被直接运行还是被导入。我们应该利用 __name__ 变量这个有利 条件。将测试代码放在一个或者叫main(), 或者叫 test()(或者你随便取个啥名字)的函数中, Edit By Vheavens  Edit By Vheavens                                如果该模块是被当成脚本运行, 就调用这个函数。 这些测试代码应该随着测试条件及测试结果的变更及时修改, 每次代码更新都应该运行这 些测试代码,以确认修改没有引发新问题。只要坚持这样做,你的代码就会足够健壮,更不用 提验证和测试新特性和更新了。 在主程序中放置测试代码是测试模块的简单快捷的手段。Python 标准库中还提供了 unittest 模块, 有时候它被称为 PyUnit, 是一个测试框架。如何使用 unittest 超出了本书 的范围, 不过当需要对一个大系统的组件进行正规系统的回规测试时,它就会派上用场。 3.5 内存管理 到现在为止, 你已经看了不少 Python 代码的例子。我们本节的主题是变量和内存管理的 细节, 包括: z 变量无须事先声明 z 变量无须指定类型 z 程序员不用关心内存管理 z 变量名会被“回收” z del 语句能够直接释放资源 3.5.1 变量定义 大多数编译型语言,变量在使用前必须先声明,其中的 C 语言更加苛刻:变量声明必须位 于代码块最开始,且在任何其他语句之前。其它语言,像 C++和 Java,允许“随时随地”声明 变量,比如,变量声明可以在代码块的中间,不过仍然必须在变量被使用前声明变量的名字和 类型。在 Python 中,无需此类显式变量声明语句,变量在第一次被赋值时自动声明。和其他大 多数语言一样,变量只有被创建和赋值后才能被使用。 >>> a Traceback (innermost last): File "", line 1, in ? NameError: a Edit By Vheavens  Edit By Vheavens                                变量一旦被赋值,您就可以通过变量名来访问它。 >>> x = 4 >>> y = 'this is a string' >>> x 4 >>> y 'this is a string' 3.5.2 动态类型 还要注意一点,Python中不但变量名无需事先声明,而且也无需类型声明。Python语言中, 对象的类型和内存占用都是运行时确定的。尽管代码被编译成字节码,Python 仍然是一种解释 型语言。在创建--也就是赋值时,解释器会根据语法和右侧的操作数来决定新对象的类型。 在对象创建后,一个该对象的应用会被赋值给左侧的变量。 3.5.3 内存分配 作为一个负责任的程序员,我们知道在为变量分配内存时,是在借用系统资源,在用完之 后, 应该释放借用的系统资源。Python 解释器承担了内存管理的复杂任务, 这大大简化了应 用程序的编写。你只需要关心你要解决的问题,至于底层的事情放心交给 Python 解释器去做就 行了。 3.5.4 引用计数 要保持追踪内存中的对象, Python 使用了引用计数这一简单技术。也就是说 Python 内部 记录着所有使用中的对象各有多少引用。你可以将它想像成扑克牌游戏“黑杰克”或“21点”。 一个内部跟踪变量,称为一个引用计数器。至于每个对象各有多少个引用, 简称引用计数。当 对象被创建时, 就创建了一个引用计数, 当这个对象不再需要时, 也就是说, 这个对象的 引用计数变为 0 时, 它被垃圾回收。(严格来说这不是 100%正确,不过现阶段你可以就这么 认为) 增加引用计数 Edit By Vheavens  Edit By Vheavens                                当对象被创建并(将其引用)赋值给变量时,该对象的引用计数就被设置为 1。 当同一个对象(的引用)又被赋值给其它变量时,或作为参数传递给函数, 方法或类实例 时, 或者被赋值为一个窗口对象的成员时,该对象的一个新的引用,或者称作别名,就被创建 (则该对象的引用计数自动加 1)。 图 3–2 有两个引用的同一对象 请看以下声明: x = 3.14 y = x 语句 x=3.14 创建了一个浮点数对象并将其引用赋值给 x。 x 是第一个引用, 因此,该 对象的引用计数被设置为 1。语句 y=x 创建了一个指向同一对象的别名 y(参阅图 3-2)。事 实上并没有为 Y 创建一个新对象, 而是该对象的引用计数增加了 1 次(变成了 2)。这是对象 引用计数增加的方式之一。还有一些其它的方式也能增加对象的引用计数, 比如该对象作为参 数被函数调用或这个对象被加入到某个容器对象当中时。 总之,对象的引用计数在 z 对象被创建 x = 3.14 z 或另外的别名被创建 y = x z 或被作为参数传递给函数(新的本地引用) foobar(x) Edit By Vheavens  Edit By Vheavens                                z 或成为容器对象的一个元素 myList = [123, x, 'xyz'] 下面让我们来看一下引用计数是如何变少的。 减少引用计数 当对象的引用被销毁时,引用计数会减小。最明显的例子就是当引用离开其作用范围时, 这种情况最经常出现在函数运行结束时,所有局部变量都被自动销毁,对象的引用计数也就随 之减少。 当变量被赋值给另外一个对象时,原对象的引用计数也会自动减 1: foo = 'xyz' bar = foo foo = 123 当字符串对象"xyz"被创建并赋值给 foo 时, 它的引用计数是 1. 当增加了一个别名 bar 时, 引用计数变成了 2. 不过当 foo 被重新赋值给整数对象 123 时, xyz 对象的引用计数自 动减 1,又重新变成了 1. 其它造成对象的引用计数减少的方式包括使用 del 语句删除一个变量(参阅下一节), 或 者当一个对象被移出一个窗口对象时(或该容器对象本身的引用计数变成了0 时)。 总结一下, 一个对象的引用计数在以下情况会减少: z 一个本地引用离开了其作用范围。比如 foobar()(参见上一下例子)函数结束时。 z 对象的别名被显式的销毁。 del y # or del x z 对象的一个别名被赋值给其它的对象 x = 123 z 对象被从一个窗口对象中移除 myList.remove(x) z 窗口对象本身被销毁 del myList # or goes out-of-scope Edit By Vheavens  Edit By Vheavens                                参阅 11.8 了解更多变量作用范围的信息。 del 语句 Del 语句会删除对象的一个引用,它的语法是: del obj1[, obj2[,... objN]] 例如,在上例中执行 del y 会产生两个结果: z 从现在的名字空间中删除 y z x 的引用计数减一 引申一步, 执行 del x 会删除该对象的最后一个引用, 也就是该对象的引用计数会减为 0, 这会导致该对象从此“无法访问”或“无法抵达”。 从此刻起, 该对象就成为垃圾回收 机制的回收对象。 注意任何追踪或调试程序会给一个对象增加一个额外的引用, 这会推迟该 对象被回收的时间。 3.5.5 垃圾收集 不再被使用的内存会被一种称为垃圾收集的机制释放。象上面说的, 虽然解释器跟踪对象 的引用计数, 但垃圾收集器负责释放内存。垃圾收集器是一块独立代码, 它用来寻找引用计 数为 0 的对象。它也负责检查那些虽然引用计数大于 0 但也应该被销毁的对象。 特定情形会导 致循环引用。 一个循环引用发生在当你有至少两个对象互相引用时, 也就是说所有的引用都消失时, 这 些引用仍然存在, 这说明只靠引用计数是不够的。Python 的垃圾收集器实际上是一个引用计 数器和一个循环垃圾收集器。 当一个对象的引用计数变为 0,解释器会暂停,释放掉这个对象 和仅有这个对象可访问(可到达)的其它对象。作为引用计数的补充, 垃圾收集器也会留心被 分配的总量很大(及未通过引用计数销毁的那些)的对象。 在这种情况下, 解释器会暂停下 来, 试图清理所有未引用的循环。 3.6 第一个 Python 程序 Edit By Vheavens  Edit By Vheavens                                我们已经熟悉了语法、代码风格、变量赋值及内存分配,现在来看一点略微复杂的代码。 这个例子中还有你不熟悉(我们还未讲到的)的 Python 结构,不过我们相信因为 Python 非常 的简单和优雅,你一定可以弄懂每一行代码的用途。 我们将要介绍两段处理文本文件的相关脚本。首先, makeTextFile.py, 创建一个文本文 件。 它提示用户输入每一行文本, 然后将结果写到文件中。另一个 readTextFile.py 读取并 显示该文本文件的内容。 研究一下这两段代码, 看看他们是如何工作的。 例 3.1 创建文件(makeTextFile.py) 这个脚本提醒用户输入一个(尚不存在的)文件名, 然后由用户输入该文件的每一行。最 后, 将所有文本写入文本文件。 1 #!/usr/bin/env python 2 3'makeTextFile.py -- create text file' 4 5import os 6 ls = os.linesep 7 8# get filename 9 while True: 10 11 if os.path.exists(fname): 12 print "ERROR: '%s' already exists" % fname 13 else: 14 break 15 16 # get file content (text) lines 17 all = [] 18 print "\nEnter lines ('.' by itself to quit).\n" 19 20 # loop until user terminates input 21 while True: 22 entry = raw_input('> ') 23 if entry == '.': 24 break 25 else: Edit By Vheavens  Edit By Vheavens                                26 all.append(entry) 27 28 # write lines to file with proper line-ending 29 fobj = open(fname, 'w') 30 fobj.writelines(['%s%s' % (x, ls) for x in all]) 31 fobj.close() 32 print 'DONE!' 第 1–3 行 UNIX 启动行之后是模块的文档字符串。应该坚持写简洁并有用的文档字符串。这里我们写 的有点短,不过对这段代码已经够用。(建议读者看一下标准库中 cgi 模块的文档字符串,那 是一个很好的示例) 第 5–6 行 之后我们导入 os 模块, 在第 6 行我们为 os.linesep 属性取了一个新别名。这样做一方 面可以缩短变量名, 另一方面也能改善访问该变量的性能。 核心技巧:使用局部变量替换模块变量 类似 os.linesep 这样的名字需要解释器做两次查询:(1)查找 os 以确认它是一个模块, (2)在这个模块中查找 linesep 变量。因为模块也是全局变量, 我们多消耗了系统资源。如 果你在一个函数中类似这样频繁使用一个属性,我们建议你为该属性取一个本地变量别名。 变 量查找速度将会快很多--在查找全局变量之前, 总是先查找本地变量。 这也是一个让你的 程序跑的更快的技巧: 将经常用到的模块属性替换为一个本地引用。代码跑得更快,而也不用 老是敲那么长的变量名了。在我们的代码片段中,并没有定义函数,所以不能给你定义本地别 名的示例。不过我们有一个全局别名,至少也减少了一次名字查询 第 8–14 行 显然这是一个无限循环, 也就是说除非我们在 while 语句体提供 break 语句, 否则它会 一直循环下去。 while 语句根据后面的表达式决定是否进行下一次循环, 而 True 则确保它一直循环下去。 第 10-14 行提示用户输入一个未使用的文件名。 raw_input() 内建函数接受一个“提示 字符串”参数,作为对用户的提示信息。raw_input()返回用户输入的字符串,也就是为 fname Edit By Vheavens  Edit By Vheavens                                赋值。 如果用户不小心输入了一个已经存在的文件的名字,我们要提示这个用户重新输入另一 个名字。 os.path.exists() 是 os 模块中一个有用的函数, 帮助我们确认这一点。 当有输 入一个不存在的文件名时, os.path.exists() 才会返回 False, 这时我们中断循环继续下面 的代码。 第 16–26 行 这部分代码提供用户指令,引导用户输入文件内容,一次一行。我们在第十七行初始化了 列表 all,它用来保存每一行文本。第21 行开始另一个无限循环, 提示用户输入每一行文本, 一行仅输入一个句点 '.' 表示输入结束。 23-26 行的 if-else 语句判断是否满足结束条件 以中止循环(行 24), 否则就再添加新的一行。 第 28–32 行 现在所有内容都保存在内存当中, 我们需要将它们保存到文件。 第 29 行打开文件准备进 行写操作,第 30 行将内存中的内容逐行写入文件。每个文件都需要一个行结束符(或文件结束 字符)。 第 30 行的结构称为列表解析, 它做以下事: 对我们文件的每一行, 根据程序运行 平台添加一个合适的行结束符。 '%s%s' 为每一行添加行结束符,(x, ls)表示每一行及其行 结束符, 对Unix 平台,是'\n', 对DOS 或 win32 平台,则是 '\r\n'。通过使用 os.lineseq , 我们不必关心程序运行在什么平台,也不必要根据不同的平台决定使用哪种行结束符。 文件 对象的 writelines() 方法接收包含行结束符的结果列表,并将它写入文件 不错吧。现在来看一下如何查看刚刚创建的文件。出于这个目的,我们创建了第二个Python 脚本, readTextFile.py。你会看到,它比 makeTextFile.py 短的多。创建一个文件的复杂度 总是比读取它要大。你可能感兴趣的、有新意的一点在于异常处理的出现。 第 1–3 行 和前面一样, 是 Unix 启动行及模块文档字符串。 第 5–7 行 不同于 makeTextFil.py, 我们在这个例子中不再关心用户是否输入合适的文件名。 Example 3.2 File Read and Display (readTextFile.py) Edit By Vheavens  Edit By Vheavens                                1 #!/usr/bin/env Python 2 3 'readTextFile.py -- read and display text file' 4 5 # get filename 6 fname = raw_input('Enter filename: ') 7 print 8 9 # attempt to open file for reading 10 try: 11 fobj = open(fname, 'r') 12 except IOError, e: 13 print "*** file open error:", e 14 else: 15 # display contents to the screen 16 for eachLine in fobj: 17 print eachLine, 18 fobj.close() 换句话说, 我们在其它地方进行验证工作(如果需要)。第 7 行打印一个空行以便将提 示信息和文件内容分隔开来。 第 9–18 行 脚本的剩余部分展示了一种新的 Python 结构, try-except-else 语句。try 子句是一段 我们希望监测错误的代码块。 在第 10-11 行代码,我们尝试打开用户输入的文件。except 子 句是我们处理错误的地方。在 12-13 行,我们检查 open() 是否失败-通常是 IOError 类型 的错误。 最后,14-18 行的 else 子句在 try 代码块运行无误时执行。我们在这儿将文件的每一行 显示在屏幕上。注意由于我们没有移除代表每行结束的行结束符,我们不得不抵制 print 语句 自动生成的行结束符 --通过在 print 语句的最后加一个逗号可以达到这一目的。 第18 行关 闭文件,从而结束这段脚本。 最后要讲的一点是关于使用 os.path.exists() 和异常处理:一般程序员倾向于使用前者, 因为有一个现成的函数可以检查错误条件 -- 并且很简单, 这是个布尔函数, 它会告你“是” 还是“不是”。 (注意,这个函数内可能已经有异常处理代码)。那你为什么还要重新发明一 Edit By Vheavens  Edit By Vheavens                                个轮子来干同样一件事?异常处理最适用的场合,是在没有合适的函数处理异常状况的时候。 这时程序员必须识别这些非正常的错误,并做出相应处理。对我们的例子来说, 我们能够通过 检查文件是否存在来避免异常发生, 不过因为有可能因为其它原因造成文件打开失败,比如缺 少权限,网络驱动器突然连接失败等等。从更安全的角度来说, 就不应该使用类似 os.path.exists() 之类的函数而是使用异常处理, 尤其是在没有合适函数的情况下更应如此。 你会在第 9 章中找到更多文件系统函数的例子, 在第10 章则有更多关于异常处理的知识。 3.7 相关模块和开发工具 Python 代码风格指南(PEP8), Python 快速参考和 Python 常见问答都是开发者很重要的 “工具”。另外, 还有一些模块会帮助你成为一个优秀的 Python 程序员。 z Debugger: pdb z Logger: logging z Profilers: profile, hotshot, cProfile 调试模块 pdb 允许你设置(条件)断点,代码逐行执行,检查堆栈。它还支持事后调试。 logging 模块是在 Python2.3 中新增的, 它定义了一些函数和类帮助你的程序实现灵活 的日志系统。共有五级日志级别: 紧急, 错误,警告,信息和调试。 历史上,因为不同的人们为了满足不同的需求重复实现了很多性能测试器,Python 也有好 几个性能测试模块。 最早的 Python profile 模块是 Python 写成的,用来测试函数的执行时 间,及每次脚本执行的总时间,既没有特定函数的执行时间也没有被包含的子函数调用时间。 在三个 profile 模块中,它是最老的也是最慢的,尽管如此, 它仍然可以提供一些有价值的性 能信息。 hotshot 模块是在 Python2.2 中新增的,它的目标是取代 profile 模块, 它修复了 profile 模块的一些错误, 因为它是用 C 语言写成,所以它有效的提高了性能。 注意 hotshot 重点解决了性能测试过载的问题, 但却需要更多的时间来生成结果。Python2.5 版修复了 hotshot 模块的一个关于时间计量的严重 bug。 cProfile 模块是 Python2.5 新增的, 它用来替换掉已经有历史的 hotshot 和 profile 模 块。被作者确认的它的一个较明显的缺点是它需要花较长时间从日志文件中载入分析结果, 不 支持子函数状态细节及某些结果不准。它也是用 C 语言来实现的。 Edit By Vheavens  Edit By Vheavens                                3.8 练习 3–1. 标识符。为什么 Python 中不需要变量名和变量类型声明? 3–2. 标识符。为什么 Python 中不需要声明函数类型? 3–3. 标识符。为什么应当避免在变量名的开始和和结尾使用双下划线? 3–4. 语句。在 Python 中一行可以书写多个语句吗? 3–5. 语句。在 Python 中可以将一个语句分成多行书写吗? 3–6. 变量赋值 (a)赋值语句 x, y, z = 1, 2, 3 会在 x、y、z 中分别赋什么值? (b)执行 z, x, y = y, z, x 后,x、y、z 中分别含有什么值? 3–7. 标识符。下面哪些是 Python 合法的标识符?如果不是,请说明理由!在合法的标 识符中,哪些是关键字? 下面的问题涉及了 makeTextFile.py 和 readTextFile.py 脚本。 3–8. Python 代码。将脚本拷贝到您的文件系统中,然后修改它。可以添加注释,修改 提示符(‘>’太单调了)等等,修改这些代码,使它看上去更舒服。 3–9. 移植。 如果你在不同类型的计算机系统中分别安装有 Python, 检查一下, os.linesep 的值是否有不同。 记下操作系统的类型以及 linesep 的值。 3–10. 异常。使用类似 readTextFile.py 中异常处理的方法取代 readTextFile.py makeTextFile.py 中 对 os.path.exists()的调用。反过来, 用 os.path.exists() 取 代 readTextFile.py 中的异常处理方法。 3–11. 字符串格式化 不再抑制 readTextFile.py 中 print 语句生成的 NEWLINE 字符,修改你的 代码, 在显示一行之前删除每行末尾的空白。这样, 你就可以移除 print 语句末尾的逗号了。 提示: 使用字符串对象的 strip()方法 Edit By Vheavens  Edit By Vheavens                                3–12. 合并源文件。将两段程序合并成一个,给它起一个你喜欢的名字,比方 readNwriteTextFiles.py。让用户自己选择是创建还是显示一个文本文件。 3–13. 添加新功能。将你上一个问题改造好的 readNwriteTextFiles.py 增加一个新功 能:允许用户编辑一个已经存在的文本文件。 你可以使用任何方式,无论是一次编辑一行,还 是一次编辑所有文本。需要提醒一下的是, 一次编辑全部文本有一定难度,你可能需要借助 GUI 工具包或一个基于屏幕文本编辑的模块比如 curses 模块。要允许用户保存他的修改(保存到 文件)或取消他的修改(不改变原始文件),并且要确保原始文件的安全性(不论程序是否正 常关闭)。 Edit By Vheavens  Edit By Vheavens                                Python 对象 本章主题 z Python 对象 z 内建类型 z 标准类型运算符 z 值的比较 z 对象身份比较 z 布尔类型 z 标准类型内建函数 z 标准类型总览 z 各种类型 z 不支持的类型 4 Edit By Vheavens  Edit By Vheavens                                我们现在来学习 Python 语言的核心部分。首先我们来了解什么是 Python 对象,然后讨论 最常用的内建类型,接下来我们讨论标准类型运算符和内建函数,之后给出对标准类型的不同 分类方式。这有助于我们更好的理解他们如何工作。最后我们提一提 Python 目前还不支持的 类型(这对那些有其他高级语言经验的人会有所帮助)。 4.1 Python 对象 Python 使用对象模型来存储数据。构造任何类型的值都是一个对象。尽管 Python 通常当 成一种“面向对象的编程语言”,但你完全能够写出不使用任何类和实例的实用脚本。不过 Python 的对象语法和架构鼓励我们使用这些特性,下面让我们仔细研究一下 Python 对象。 所有的 Python 对像都拥有三个特性:身份,类型和值。 身份: 每一个对象都有一个唯一的身份标识自己,任何对象的身份可以使用内建函数id()来得到。 这个值可以被认为是该对象的内存地址。您极少会用到这个值,也不用太关心它究竟是什么。 类型 对象的类型决定了该对象可以保存什么类型的值,可以进行什么样的操作,以及遵循什么 Edit By Vheavens  Edit By Vheavens                                样的规则。您可以用内建函数 type()查看 Python 对象的类型。因为在 Python 中类型也是对象 (还记得我们提到 Python 是面向对象的这句话吗?),所以 type()返回的是对象而不是简单的 字符串。 值 对象表示的数据项 上面三个特性在对象创建的时候就被赋值,除了值之外,其它两个特性都是只读的。对于 新风格的类型和类, 对象的类型也是可以改变的,不过对于初学者并不推荐这样做。 如果对象支持更新操作,那么它的值就可以改变,否则它的值也是只读的。对象的值是否 可以更改被称为对象的可改变性(mutability),我们会在后面的小节 4.7 中讨论这个问题。只 要一个对象还没有被销毁, 这些特性就一直存在。 Python 有一系列的基本(内建)数据类型,必要时也可以创建自定义类型来满足你的应用 程序的需求。绝大多数应用程序通常使用标准类型,对特定的数据存储则通过创建和实例化类 来实现。 4.1.1 对象属性 某些 Python 对象有属性、值或相关联的可执行代码,比如方法(method)。Python用点(.) 标记法来访问属性。属性包括相应对象的名字等等,在章节 2.14 的备注中曾做过介绍。最常用 的属性是函数和方法,不过有一些 Python 类型也有数据属性。含有数据属性的对象包括(但不 限于):类、类实例、模块、复数和文件。 4.2 标准类型 z 数字(分为几个子类型,其中有三个是整型) z 整型 z 布尔型 z 长整型 z 浮点型 z 复数型 z 字符串 Edit By Vheavens  Edit By Vheavens                                z 列表 z 元组 z 字典 在本书中,我们把标准类型也称作“基本数据类型”,因为这些类型是Python 内建的基本 数据类型,我们会在第 5、6 和 7 章详细介绍它们。 4.3 其他内建类型 z 类型 z Null 对象 (None) z 文件 z 集合/固定集合 z 函数/方法 z 模块 z 类 这些是当你做 Python 开发时可能会用到的一些数据类型。我们在这里讨论 Type 和 None 类型的使用,除此之外的其他类型将在其他章节中讨论。 4.3.1 类型对象和 type 类型对象 在本章我们要讨论所有的 Python 类型,虽然看上去把类型本身也当成对象有点特别,我们 还是要在这里提一提。你一定还记得,对象的一系列固有行为和特性(比如支持哪些运算,具 有哪些方法)必须事先定义好。从这个角度看,类型正是保存这些信息的最佳位置。描述一种 类型所需要的信息不可能用一个字符串来搞定,所以类型不能是一个简单的字符串,这些信息 不能也不应该和数据保存在一起, 所以我们将类型定义成对象。 下面我们来正式介绍内建函数 type()。通过调用type()函数你能够得到特定对象的类型 信息: >>> type(42) 我们仔细研究一下这个例子,请注意看 type 函数有趣的返回值。我们得到一个简洁的 Edit By Vheavens  Edit By Vheavens                                输出结果。不过你应当意识到它并不是一个简简单单的告诉你 42 是个整数这样 的字符串。您看到的实际上是一个类型对象,碰巧它输出了一个字符串来告诉你 它是个 int 型对象。 现在你该问自己了,那么类型对象的类型是什么?来, 我们试验一下: >>> type(type(42)) 没错,所有类型对象的类型都是 type,它也是所有 Python 类型的根和所有 Python 标准类 的默认元类(metaclass)。你现在有点搞不明白,没关系,随着我们逐步深入的学习类和类型, 你就会慢慢理解。 随着 Python 2.2 中类型和类的统一,类型对象在面向对象编程和日常对象使用中扮演着 更加重要的角色。从现在起, 类就是类型,实例是对应类型的对象。 4.3.2 None, Python 的 Null 对象 Python 有一个特殊的类型,被称作 Null 对象或者 NoneType,它只有一个值,那就是 None。 它不支持任何运算也没有任何内建方法。如果非常熟悉 C 语言,和 None 类型最接近的 C 类型就 是 void,None 类型的值和 C 的 NULL 值非常相似(其他类似的对象和值包括 Perl 的 undef 和 Java 的 void 类型与 null 值)。 None 没有什么有用的属性,它的布尔值总是 False。 核心笔记:布尔值 所有标准对象均可用于布尔测试,同类型的对象之间可以比较大小。每个对象天生具有布 尔 True 或 False 值。空对象、值为零的任何数字或者 Null 对象 None 的布尔值都是 False。 下列对象的布尔值是 False。 z None z False (布尔类型) z 所有的值为零的数: Edit By Vheavens  Edit By Vheavens                                z 0 (整型) z (浮点型) z 0L (长整型) z 0.0+0.0j (复数) z "" (空字符串) z [] (空列表) z () (空元组) z {} (空字典) 值不是上面列出来的任何值的对象的布尔值都是 True,例如 non-empty、 non-zero 等等。 用户创建的类实例如果定义了 nonzero(__nonzero__())或 length(__len__())且值为 0,那 么它们的布尔值就是 False。 4.4 内部类型 z 代码 z 帧 z 跟踪记录 z 切片 z 省略 z Xrange 我们在这里简要介绍一下这些内部类型,一般的程序员通常不会直接和这些对象打交道。 不过为了这一章的完整性,我们还是在这里介绍一下它们。请参阅源代码或者 Python 的内部文 档和在线文档获得更详尽的信息。 你如果对异常感到迷惑的话,可以告诉你它们是用类来实现的,在老版本的 Python 中,异 常是用字符串来实现的。 4.4.1 代码对象 代码对象是编译过的 Python 源代码片段,它是可执行对象。通过调用内建函数 compile() 可以得到代码对象。代码对象可以被 exec 命令或 eval()内建函数来执行。在第 14 章将详细 研究代码对象。 Edit By Vheavens  Edit By Vheavens                                代码对象本身不包含任何执行环境信息, 它是用户自定义函数的核心, 在被执行时动态 获得上下文。(事实上代码对象是函数的一个属性)一个函数除了有代码对象属性以外,还有一 些其它函数必须的属性,包括函数名,文档字符串,默认参数,及全局命名空间等等。 4.4.2 帧对象 帧对象表示 Python 的执行栈帧。帧对象包含Python 解释器在运行时所需要知道的所有信 息。它的属性包括指向上一帧的链接,正在被执行的代码对象(参见上文),本地及全局名字空 间字典以及当前指令等。每次函数调用产生一个新的帧,每一个帧对象都会相应创建一个 C 栈 帧。用到帧对象的一个地方是跟踪记录对象(参见下一节) 4.4.3 跟踪记录 对象 当你的代码出错时, Python 就会引发一个异常。如果异常未被捕获和处理, 解释器就会 退出脚本运行,显示类似下面的诊断信息: Traceback (innermost last): File "", line N?, in ??? ErrorName: error reason 当异常发生时,一个包含针对异常的栈跟踪信息的跟踪记录对象被创建。如果一个异常有 自己的处理程序,处理程序就可以访问这个跟踪记录对象。 4.4.4 切片对象 当使用 Python 扩展的切片语法时,就会创建切片对象。扩展的切片语法允许对不同的索引 切片操作,包括步进切片, 多维切片,及省略切片。多维切片语法是 sequence[start1 : end1, start2 : end2], 或使用省略号, sequence[...,start1 : end1 ]. 切片对象也可以由内建 函数 slice()来生成。步进切片允许利用第三个切片元素进行步进切片,它的语法为 sequence[起始索引 : 结束索引 : 步进值]。Python 很早就支持扩展步进切片语法了,但直到 Python2.3 以前都必须依靠 C API 或 Jython 才能工作。 下面是几个步进切片的例子: >>> foostr = 'abcde' >>> foostr[::-1] 'edcba' >>> foostr[::-2] Edit By Vheavens  Edit By Vheavens                                'eca' >>> foolist = [123, 'xba', 342.23, 'abc'] >>> foolist[::-1] ['abc', 342.23, 'xba', 123] 4.4.5 省略对象 省略对象用于扩展切片语法中,起记号作用。 这个对象在切片语法中表示省略号。类似 Null 对象 None, 省略对象有一个唯一的名字 Ellipsis, 它的布尔值始终为 True. 4.4.6 XRange 对象 调用内建函数 xrange() 会生成一个 Xrange 对象,xrange()是内建函数 range()的兄弟版 本, 用于需要节省内存使用或 range()无法完成的超大数据集场合。在第 8 章你可以找到更多 关于 range() 和 xrange() 的使用信息。 4.5 标准类型运算符 4.5.1 对象值的比较 比较运算符用来判断同类型对象是否相等,所有的内建类型均支持比较运算,比较运算返 回布尔值 True 或 False。如果你正在使用的是早于 Python2.3 的版本,因为这些版本还没有 布尔类型,所以会看到比较结果为整型值 1 (代表 True)或 0 (代表 False)。 注意,实际进行的比较运算因类型而异。换言之,数字类型根据数值的大小和符号比较, 字符串按照字符序列值进行比较,等等。 >>> 2 == 2 True >>> 2.46 <= 8.33 True >>> 5+4j >= 2-3j True >>> 'abc' == 'xyz' False >>> 'abc' > 'xyz' False Edit By Vheavens  Edit By Vheavens                                >>> 'abc' < 'xyz' True >>> [3, 'abc'] == ['abc', 3] False >>> [3, 'abc'] == [3, 'abc'] True 不同于很多其它语言,多个比较操作可以在同一行上进行,求值顺序为从左到右。 >>> 3 < 4 < 7 # same as ( 3 < 4 ) and ( 4 < 7 ) True >>> 4 > 3 == 3 # same as ( 4 > 3 ) and ( 3 == 3 ) True >>> 4 < 3 < 5 != 2 < 7 False 我们会注意到比较操作是针对对象的值进行的,也就是说比较的是对象的数值而不是对象 本身。在后面的部分我们会研究对象身份的比较。 表 4.1 标准类型值比较运算符 运算符 功能 expr1 < expr2 expr1 小于 expr2 expr1 > expr2 expr1 大于 expr2 expr1 <= expr2 expr1 小于等于 expr2 expr1 >= expr2 expr1 大于等于 expr2 expr1 == expr2 expr1 等于 expr2 expr1 != expr2 expr1 不等于 expr2 (C 风格) expr1 <> expr2 expr1 不等于 expr2 (ABC/Pascal 风格) 注: 未来很有可能不再支持 <> 运算符,建议您一直使用 != 运算符。 4.5.2 对象身份比较 Edit By Vheavens  Edit By Vheavens                                作为对值比较的补充,Python 也支持对象本身的比较。对象可以被赋值到另一个变量(通 过引用)。因为每个变量都指向同一个(共享的)数据对象,只要任何一个引用发生改变,该对 象的其它引用也会随之改变。 为了方便大家理解,最好先别考虑变量的值,而是将变量名看作对象的一个链接。让我们 来看以下三个例子: 例 1: foo1 和 foo2 指向相同的对象 foo1 = foo2 = 4.3 当你从值的观点看这条语句时, 它表现的只是一个多重赋值,将 4.3 这个值赋给了 foo1 和 foo2 这两个变量。这当然是对的, 不过它还有另一层含义。 事实是一个值为 4.3 的数字对 象被创建,然后这个对象的引用被赋值给foo1 和 foo2, 结果就是 foo1 和 foo2 指向同一个对 象。图 4-1 演示了一个对象两个引用。 图 4–1 foo1 和 foo2 指向相同的对象 例 2: foo1 和 foo2 指向相同的对象 foo1 = 4.3 foo2 = foo1 这个例子非常类似上一个,一个值为 4.3 的数值对象被创建,然后赋给一个变量, 当执行 foo2 = foo1 时, foo2 被指向 foo1 所指向的同一个对象, 这是因为 Python 通过传递引用来 处理对象。foo2 就成为原始值 4.3 的一个新的引用。 这样 foo1 和 foo2 就都指向了同一个对 象。示意图也和图 4-1 一样。 例 3: foo1 和 foo2 指向不同的对象 Edit By Vheavens  Edit By Vheavens                                foo1 = 4.3 foo2 = 1.3 + 3.0 这个例子有所不同。首先一个数字对象被创建,然后赋值给 foo1. 然后第二个数值对象被 创建并赋值给 foo2. 尽管两个对象保存的是同样大小的值,但事实上系统中保存的都是两个独 立的对象,其中 foo1 是第一个对象的引用, foo2 则是第二个对象的引用。图 4-2 演示给我 们这里有两个不同的对象,尽管这两个对象有同样大小的数值。 我们为什么在示意图中使用盒 子?没错,对象就象一个装着内容的盒子。当一个对象被赋值到一个变量,就象在这个盒子上 贴了一个标签,表示创建了一个引用。每当这个对象有了一个新的引用,就会在盒子上新贴一 张标签。当一个引用被销毁时, 这个标签就会被撕掉。当所有的标签都被撕掉时, 这个盒子 就会被回收。那么,Python 是怎么知道这个盒子有多少个标签呢? 图 4–2 foo1 和 foo2 指向不同的对象 每个对象都天生具有一个计数器,记录它自己的引用次数。这个数目表示有多少个变量指 向该对象。这也就是我们在第三章3.5.5-3.5.7 小节提到的引用计数。Python提供了 is 和 is not 运算符来测试两个变量是否指向同一个对象。象下面这样执行一个测试 a is b 这个表达式等价于下面的表达式 id(a) == id(b) 对象身份比较运算符拥有同样的优先级,表 4.2 列出了这些运算符。在下面这个例子里, 我们创建了一个变量,然后将第二个变量指向同一个对象。 >>> a = [ 5, 'hat', -9.3] >>> b = a >>> a is b True >>> a is not b False Edit By Vheavens  Edit By Vheavens                                >>> >>> b = 2.5e-5 >>> b 2.5e-005 >>> a [5, 'hat', -9.3] >>> a is b False >>> a is not b True is 与 not 标识符都是 Python 关键字。 表 4.2 标准类型对象身份比较运算符 运算符 功能 obj1 is obj2 obj1 和 obj2 是同一个对象 obj1 is not obj2 obj1 和 obj2 不是同一个对象 核心提示:实践 在上面的例子中,您会注意到我们使用的是浮点数而不是整数。为什么这样?整数对象和 字符串对象是不可变对象,所以Python 会很高效的缓存它们。这会造成我们认为Python 应该 创建新对象时,它却没有创建新对象的假象。看下面的例子: >>> a = 1 >>> id(a) 8402824 >>> b = 1 >>> id(b) 8402824 >>> >>> c = 1.0 >>> id(c) 8651220 >>> d = 1.0 >>> id(d) 8651204 在上面的例子中,a 和 b 指向了相同的整数对象,但是 c 和 d 并没有指向相同的浮点数 对象。如果我们是纯粹主义者,我们会希望 a 与 b 能和 c 与 d 一样,因为我们本意就是为 了创建两个整数对象,而不是像 b = a 这样的结果。 Edit By Vheavens  Edit By Vheavens                                Python 仅缓存简单整数,因为它认为在 Python 应用程序中这些小整数会经常被用到。当 我们在写作本书的时候,Python 缓存的整数范围是(-1, 100),不过这个范围是会改变的,所 以请不要在你的应用程序使用这个特性。 Python 2.3 中决定,在预定义缓存字符串表之外的字符串,如果不再有任何引用指向它, 那这个字符串将不会被缓存。也就是说, 被缓存的字符串将不会象以前那样永生不灭,对象回 收器一样可以回收不再被使用的字符串。从 Python 1.5 起提供的用于缓存字符的内建函数 intern() 也已经不再推荐使用, 即将被废弃。 4.5.3 布尔类型 布尔逻辑运算符 and, or 和 not 都是 Python 关键字,这些运算符的优先级按从高到低 的顺序列于表 4.3. not 运算符拥有最高优先级,只比所有比较运算符低一级。 and 和 or 运 算符则相应的再低一级。 表 4.3 标准类型布尔运算符 运算符 功能 not expr expr 的逻辑非 (否) expr1 and expr2 expr1 和 expr2 的逻辑与 expr1 or expr2 expr1 和 expr2 的逻辑或 >>> x, y = 3.1415926536, -1024 >>> x < 5.0 True >>> not (x < 5.0) False >>> (x < 5.0) or (y > 2.718281828) True >>> (x < 5.0) and (y > 2.718281828) False >>> not (x is y) True 前面我们提到过 Python 支持一个表达式进行多种比较操作, 其实这个表达式本质上是由 多个隐式的 and 连接起来的多个表达式。 >>> 3 < 4 < 7 # same as "( 3 < 4 ) and ( 4 < 7 )" True Edit By Vheavens  Edit By Vheavens                                4.6 标准类型内建函数 除了这些运算符, 我们刚才也看到, Python提供了一些内建函数用于这些基本对象类型: cmp(), repr(), str(), type(), 和等同于 repr()函数的单反引号(``) 运算符。 表 4.4 标准类型内建函数 函数 功能 cmp(obj1, obj2) 比较 obj1 和 obj2, 根据比较结果返回整数 i: i < 0 if obj1 < obj2 i > 0 if obj1 > obj2 i == 0 if obj1 == obj2 repr(obj) 或 `obj` 返回一个对象的字符串表示 str(obj) 返回对象适合可读性好的字符串表示 type(obj) 得到一个对象的类型,并返回相应的 type 对象 4.6.1 type() 我们现在来正式介绍 type()。在 Python2.2 以前, type() 是内建函数。不过从那时起, 它变成了一个“工厂函数”。在本章的后面部分我们会讨论工厂函数, 现在你仍然可以将type() 仅仅当成一个内建函数来看。 type() 的用法如下: type(object) type() 接受一个对象做为参数,并返回它的类型。它的返回值是一个类型对象。 >>> type(4) # int type >>> >>> type('Hello World!') # string type >>> >>> type(type(4)) # type type Edit By Vheavens  Edit By Vheavens                                在上面的例子里, 我们通过内建函数 type() 得到了一个整数和一个字符串的类型;为了 确认一下类型本身也是类型, 我们对 type()的返回值再次调用 type(). 注意 type()有趣的 输出, 它看上去不象一个典型的 Python 数据类型, 比如一个整数或一个字符串,一些东西被 一个大于号和一个小号包裹着。这种语法是为了告诉你它是一个对象。每个对象都可以实现一 个可打印的字符串表示。不过并不总是这样, 对那些不容易显示的对象来说, Python 会以一 个相对标准的格式表示这个对象,格式通常是这种形式: , 以 这种形式显示的对象通常会提供对象类别,对象 id 或位置, 或者其它合适的信息。 4.6.2 cmp() 内建函数 cmp()用于比较两个对象 obj1 和 obj2, 如果 obj1 小于 obj2, 则返回一个负整 数,如果 obj1 大于 obj2 则返回一个正整数, 如果 obj1 等于 obj2, 则返回 0。它的行为非常 类似于 C 语言的 strcmp()函数。比较是在对象之间进行的,不管是标准类型对象还是用户自定 义对象。如果是用户自定义对象, cmp()会调用该类的特殊方法__cmp__()。在第 13 章会详细 介绍类的这些特殊方法。下面是几个使用 cmp()内建函数的对数值和字符串对象进行比较的例 子。 >>> a, b = -4, 12 >>> cmp(a,b) -1 >>> cmp(b,a) 1 >>> b = -4 >>> cmp(a,b) 0 >>> >>> a, b = 'abc', 'xyz' >>> cmp(a,b) -23 >>> cmp(b,a) 23 >>> b = 'abc' >>> cmp(a,b) 0 在后面我们会研究 cmp()用于其它对象的比较操作。 Edit By Vheavens  Edit By Vheavens                                4.6.3 str()和 repr() (及 `` 运算符) 内建函数 str() 和 repr() 或反引号运算符(``) 可以方便的以字符串的方式获取对象的 内容、类型、数值属性等信息。str()函数得到的字符串可读性好, 而 repr()函数得到的字符 串通常可以用来重新获得该对象, 通常情况下 obj == eval(repr(obj)) 这个等式是成立的。 这两个函数接受一个对象做为其参数, 返回适当的字符串。在下面的例子里, 我们会随机取 一些 Python 对象来查看他们的字符串表示。 >>> str(4.53-2j) '(4.53-2j)' >>> >>> str(1) '1' >>> >>> str(2e10) '20000000000.0' >>> >>> str([0, 5, 9, 9]) '[0, 5, 9, 9]' >>> >>> repr([0, 5, 9, 9]) '[0, 5, 9, 9]' >>> >>> `[0, 5, 9, 9]` '[0, 5, 9, 9]' 尽管 str(),repr()和``运算在特性和功能方面都非常相似, 事实上 repr() 和 `` 做的 是完全一样的事情,它们返回的是一个对象的“官方”字符串表示, 也就是说绝大多数情况下 可以通过求值运算(使用 eval()内建函数)重新得到该对象,但 str()则有所不同。str() 致力 于生成一个对象的可读性好的字符串表示,它的返回结果通常无法用于 eval()求值, 但很适 合用于 print 语句输出。需要再次提醒一下的是, 并不是所有 repr()返回的字符串都能够用 eval()内建函数得到原来的对象: >>> eval(`type(type))`) File "", line 1 eval(`type(type))`) ^ SyntaxError: invalid syntax Edit By Vheavens  Edit By Vheavens                                也就是说 repr() 输出对 Python 比较友好, 而 str()的输出对人比较友好。虽然如此, 很多情况下这三者的输出仍然都是完全一样的。 核心笔记:为什么我们有了 repr()还需要``? 在 Python 学习过程中,你偶尔会遇到某个运算符和某个函数是做同样一件事情。之所以如 此是因为某些场合函数会比运算符更适合使用。举个例子, 当处理类似函数这样的可执行对象 或根据不同的数据项调用不同的函数处理时,函数就比运算符用起来方便。另一个例子就是双 星号(**)乘方运算和 pow()内建函数,x ** y 和 pow(x,y) 执行的都是x的y次方。 译者注:事实上 Python 社区目前已经不鼓励继续使用``运算符。 4.6.4 type() 和 isinstance() Python 不支持方法或函数重载, 因此你必须自己保证调用的就是你想要的函数或对象。 (参阅 Python 常见问答 4.75 节)。幸运的是, 我们前面 4.3.1 小节提到的 type()内建函数可 以帮助你确认这一点。一个名字里究竟保存的是什么?相当多,尤其是这是一个类型的名字时。 确认接收到的类型对象的身份有很多时候都是很有用的。为了达到此目的,Python 提供了一 个内建函数 type(). type()返回任意 Python 对象对象的类型,而不局限于标准类型。让我们 通过交互式解释器来看几个使用 type()内建函数返回多种对象类型的例子: >>> type('') >>> >>> s = 'xyz' >>> type(s) >>> >>> type(100) >>> type(0+0j) >>> type(0L) >>> type(0.0) >>> >>> type([]) >>> type(()) Edit By Vheavens  Edit By Vheavens                                >>> type({}) >>> type(type) >>> >>> class Foo: pass # new-style class ... >>> foo = Foo() >>> class Bar(object): pass # new-style class ... >>> bar = Bar() >>> >>> type(Foo) >>> type(foo) >>> type(Bar) >>> type(bar) Python2.2 统一了类型和类, 如果你使用的是低于 Python2.2 的解释器,你可能看到不一 样的输出结果。 >>> type('') >>> type(0L) >>> type({}) >>> type(type) >>> >>> type(Foo) # assumes Foo created as in above >>> type(foo) # assumes foo instantiated also 除了内建函数 type(), 还有一个有用的内建函数叫 isinstance(). 我们会在第13 章(面 Edit By Vheavens  Edit By Vheavens                                向对象编程)正式研究这个函数,不过在这里我们还是要简要介绍一下你如何利用它来确认一 个对象的类型。 举例 在例 4.1 中我们提供了一段脚本来演示在运行时环境使用 isinstance() 和 type()函数。 随后我们讨论 type()的使用以及怎么将这个例子移植为改用 isinstance()。 运行 typechk.py, 我们会得到以下输出: -69 is a number of type: int 9999999999999999999999 is a number of type: long 98.6 is a number of type: float (-5.2+1.9j) is a number of type: complex xxx is not a number at all!! 例 4.1 检查类型(typechk.py) 函数 displayNumType() 接受一个数值参数,它使用内建函数 type()来确认数值的类型 (或不是一个数值类型)。 1 #!/usr/bin/env python 2 3def displayNumType(num): 4 print num, 'is', 5 if isinstance(num, (int, long, float, complex)): 6 print 'a number of type:', type(num).__name__ 7 else: 8 print 'not a number at all!!' 9 10 displayNumType(-69) 11 displayNumType(9999999999999999999999L) 12 displayNumType(98.6) 13 displayNumType(-5.2+1.9j) 14 displayNumType('xxx') Edit By Vheavens  Edit By Vheavens                                例子进阶 原始 这个完成同样功能的函数与本书的第一版中的例子已经大不相同: def displayNumType(num): print num, "is", if type(num) == type(0): print 'an integer' elif type(num) == type(0L): print 'a long' elif type(num) == type(0.0): print 'a float' elif type(num) == type(0+0j): print 'a complex number' else: print 'not a number at all!!' 由于 Python 奉行简单但是比较慢的方式,所以我们必须这么做,看一眼我们原来的条件 表达式: if type(num) == type(0)... 减少函数调用的次数 如果我们仔细研究一下我们的代码,会看到我们调用了两次 type()。要知道每次调用函数 都会付出性能代价, 如果我们能减少函数的调用次数, 就会提高程序的性能。 利用在本章我们前面提到的 types 模块, 我们还有另一种比较对象类型的方法,那就是 将检测得到的类型与一个已知类型进行比较。如果这样, 我们就可以直接使用 type 对象而不 用每次计算出这个对象来。那么我们现在修改一下代码,改为只调用一次 type()函数: >>> import types >>> if type(num) == types.IntType... 对象值比较 VS 对象身份比较 在这一章的前面部分我们讨论了对象的值比较和身份比较, 如果你了解其中的关键点,你 就会发现我们的代码在性能上还不是最优的.在运行时期,只有一个类型对象来表示整数类型. 也就是说,type(0),type(42),type(-100) 都是同一个对象: (types.IntType 也 是这个对象) Edit By Vheavens  Edit By Vheavens                                如果它们是同一个对象, 我们为什么还要浪费时间去获得并比较它们的值呢(我们已经知 道它们是相同的了!)? 所以比较对象本身是一个更好地方案.下面是改进后的代码: if type(num) is types.IntType... # or type(0) 这样做有意义吗? 我们用对象身份的比较来替代对象值的比较。如果对象是不同的,那意 味着原来的变量一定是不同类型的。(因为每一个类型只有一个类型对象),我们就没有必要去 检查(值)了。 一次这样的调用可能无关紧要,不过当很多类似的代码遍布在你的应用程序中的 时候,就有影响了。 减少查询次数 这是一个对前一个例子较小的改进,如果你的程序像我们的例子中做很多次比较的话,程 序的性能就会有一些差异。为了得到整数的对象类型,解释器不得不首先查找 types 这个模块 的名字,然后在该模块的字典中查找 IntType。通过使用 from-import,你可以减少一次查询: from types import IntType if type(num) is IntType... 惯例和代码风格 Python2.2 对类型和类的统一导致 isinstance()内建函数的使用率大大增加。我们将在 第 13 章(面向对象编程)正式介绍 isinstance(),在这里我们简单浏览一下。 这个布尔函数接受一个或多个对象做为其参数,由于类型和类现在都是一回事, int 现在 既是一个类型又是一个类。我们可以使用 isinstance() 函数来让我们的 if 语句更方便,并具 有更好的可读性。 if isinstance(num, int)... 在判断对象类型时也使用 isinstance() 已经被广为接受, 我们上面的 typechk.py 脚本 最终与改成了使用 isinstance() 函数。值得一提的是, isinstance()接受一个类型对象的元 组做为参数, 这样我们就不必像使用 type()时那样写一堆 if-elif-else 判断了。 4.6.5 Python 类型运算符和内建函数总结 表 4.5 列出了所有运算符和内建函数,其中运算符顺序是按优先级从高到低排列的。同一 种灰度的运算符拥有同样的优先级。注意在 operator 模块中有这些(和绝大多数 Python)运算 Edit By Vheavens  Edit By Vheavens                                符相应的同功能的函数可供使用。 表 4.5 标准类型运算符和内建函数 Operator/Function  Description  Resulta  String    ‘‘  String representation  st Built‐in functions    cmp(obj1, obj2) Compares two objects  in repr(obj)  String representation  st str(obj)  String representation  st type(obj)  Determines object type  typ Value comparisons    <  Less than  boo >  Greater than  boo <=  Less than or equal to  boo >=  Greater than or equal to  boo ==  Equal to  boo !=  Not equal to  boo <>  Not equal to  boo Object comparisons   is  The same as  boo is not  Not the same as  boo Boolean operators    not  Logical negation  boo and  Logical conjunction  boo or  Logical disjunction  boo 布尔比较总是返回 True 或 False Edit By Vheavens  Edit By Vheavens                                4.7 类型工厂函数 Python 2.2 统一了类型和类, 所有的内建类型现在也都是类, 在这基础之上, 原来的 所谓内建转换函数象 int(), type(), list() 等等, 现在都成了工厂函数。 也就是说虽然他 们看上去有点象函数, 实质上他们是类。当你调用它们时, 实际上是生成了该类型的一个实 例, 就象工厂生产货物一样。 下面这些大家熟悉的工厂函数在老的 Python 版里被称为内建函数: z int(), long(), float(), complex() z str(), unicode(), basestring() z list(), tuple() z type() 以前没有工厂函数的其他类型,现在也都有了工厂函数。除此之外,那些支持新风格的类 的全新的数据类型,也添加了相应的工厂函数。下面列出了这些工厂函数: z dict() z bool() z set(), frozenset() z object() z classmethod() z staticmethod() z super() z property() z file() 4.8 标准类型的分类 如果让我们最啰嗦的描述标准类型,我们也许会称它们是 Python 的“基本内建数据对象原 始类型”。 z “基本”,是指这些类型都是Python 提供的标准或核心类型。 z “内建”,是由于这些类型是Python 默认就提供的 z “数据”,因为他们用于一般数据存储 z “对象”,因为对象是数据和功能的默认抽象 Edit By Vheavens  Edit By Vheavens                                z “原始”,因为这些类型提供的是最底层的粒度数据存储 z “类型”,因为他们就是数据类型 不过, 上面这些描述实际上并没有告诉你每个类型如何工作以及它们能发挥什么作用。事 实上, 几个类型共享某一些的特性,比如功能的实现手段, 另一些类型则在访问数据值方面 有一些共同之处。我们感兴趣的还有这些类型的数据如何更新以及它们能提供什么样的存储。 有三种不同的模型可以帮助我们对基本类型进行分类,每种模型都展示给我们这些类型之间的 相互关系。这些模型可以帮助我们更好的理解类型之间的相互关系以及他们的工作原理。 4.8.1 存储模型 我们对类型进行分类的第一种方式, 就是看看这种类型的对象能保存多少个对象。Python 的类型, 就象绝大多数其它语言一样,能容纳一个或多个值。一个能保存单个字面对象的类型 我们称它为原子或标量存储,那些可容纳多个对象的类型,我们称之为容器存储。(容器对象有 时会在文档中被称为复合对象,不过这些对象并不仅仅指类型,还包括类似类实例这样的对象) 容器类型又带来一个新问题,那就是它是否可以容纳不同类型的对象。所有的 Python 容器对 象都能够容纳不同类型的对象。表 4.6 按存储模型对 Python 的类型进行了分类。 字符串看上去像一个容器类型,因为它“包含”字符(并且经常多于一个字符),不过由 于 Python 并没有字符类型(参见章节 4.8),所以字符串是一个自我包含的文字类型。 表 4.6 以存储模型为标准的类型分类 存储模型 分类 Python 类型 标量/原子类型 数值(所有的数值类型),字符串(全部是文字) 容器类型 列表、元组、字典 4.8.2 更新模型 另一种对标准类型进行分类的方式就是, 针对每一个类型问一个问题:“对象创建成功之 后,它的值可以进行更新吗?” 在前面我们介绍 Python 数据类型时曾经提到,某些类型允许 他们的值进行更新,而另一些则不允许。可变对象允许他们的值被更新,而不可变对象则不允 Edit By Vheavens  Edit By Vheavens                                许他们的值被更改。表 4.7 列出了支持更新和不支持更新的类型。 看完这个表之后,你可能马上冒出一个问题:“等等,你说数值和字符串对象是不可改变的? 看看下面的例子!”: x = 'Python numbers and strings' x = 'are immutable?!? What gives?' i = 0 i = i + 1 “在我看来, 这可不象是不可变对象的行为!” 没错,是这样,不过你还没有搞清楚幕后 的真相。上面的例子中,事实上是一个新对象被创建,然后它取代了旧对象。就是这样,请多 读一遍这段。 新创建的对象被关联到原来的变量名, 旧对象被丢弃,垃圾回收器会在适当的时机回收这 些对象。你可以通过内建函数 id()来确认对象的身份在两次赋值前后发生了变化。 表 4.7 以更新模型为标准的类型分类 更新模型 分类 Python 类型 可变类型 列表, 字典 不可变类型 数字、字符串、元组 下面我们在上面的例子里加上 id()调用, 就会清楚的看到对象实际上已经被替换了: >>> x = 'Python numbers and strings' >>> print id(x) 16191392 >>> x = 'are immutable?!? What gives?' >>> print id(x) 16191232 >>> i = 0 >>> print id(i) 7749552 >>> i = i + 1 >>> print id(i) Edit By Vheavens  Edit By Vheavens                                7749600 你看到的身份数字很可能和我不同,每次执行这些数字也会不同,这是正常的。这个数字 与该对象当时分配的内存地址密切相关。因此不同的机器, 不同的执行时间都会生成不同的对 象身份。另一类对象, 列表可以被修改而无须替换原始对象, 看下面的例子: >>> aList = ['ammonia', 83, 85, 'lady'] >>> aList ['ammonia', 83, 85, 'lady'] >>> >>> aList[2] 85 >>> >>> id(aList) 135443480 >>> >>> aList[2] = aList[2] + 1 >>> aList[3] = 'stereo' >>> aList ['ammonia', 83, 86, 'stereo'] >>> >>> id(aList) 135443480 >>> >>> aList.append('gaudy') >>> aList.append(aList[2] + 1) >>> aList ['ammonia', 83, 86, 'stereo', 'gaudy', 87] >>> >>> id(aList) 135443480 注意列表的值不论怎么改变, 列表的 ID 始终保持不变。 4.8.3 访问模型 尽管前面两种模型分类方式在介绍 Python 时都很有用,它们还不是区分数据类型的首要模 型。对这种目的,我们使用访问模型。也就是说根据访问我们存储的数据的方式对数据类型进 行分类。在访问模型中共有三种访问方式:直接存取,顺序,和映射。表 4.8 按访问方式对数 据类型进行了分类。 Edit By Vheavens  Edit By Vheavens                                对非容器类型可以直接访问。所有的数值类型都归到这一类。 序列类型是指容器内的元素按从 0 开始的索引顺序访问。一次可以访问一个元素或多个元 素, 也就是大家所了解的切片(slice)。字符串, 列表和元组都归到这一类。我们前面提到过, Python 不支持字符类型,因此,虽然字符串是简单文字类型,因为它有能力按照顺序访问子字 符串,所以也将它归到序列类型。 映射类型类似序列的索引属性,不过它的索引并不使用顺序的数字偏移量取值, 它的元素 无序存放, 通过一个唯一的key 来访问, 这就是映射类型, 它容纳的是哈希键-值对的集合。 我们在以后的章节中将主要使用访问模型,详细介绍各种访问模型的类型, 以及某个分类 的类型之间有哪些相同之处(比如运算符和内建函数), 然后讨论每种 Python 标准类型。所有 类型的特殊运算符,内建函数, 及方法都会在相应的章节特别说明。 为什么要对同样的数据类型再三分类呢?首先, 我们为什么要分类? 因为 Python 提供 了高级的数据结构,我们需要将那些原始的类型和功能强大的扩展类型区分开来。另一个原因 就是这有助于搞清楚某种类型应该具有什么行为。举例来说,如果我们基本上不用问自己“列 表和元组有什么区别?”或“什么是可变类型和不可变类型?”这些问题的时候,我们也就达 到了目的。最后,某些分类中的所有类型具有一些相同的特性。一个优秀的工匠应该知道他或 她的工具箱里都有哪些宝贝。 表 4.7 以访问模型为标准的类型分类 访问模型 分类 Python 类型 直接访问 数字 顺序访问 字符串、列表、元组 映射访问 字典 表 4.9 标准类型分类 Edit By Vheavens  Edit By Vheavens                                数据类型  存储模型  更新模型  访问模型l  数字  Scalar  不可更改  直接访问   字符串  Scalar  不可更改  顺序访问  列表  Container  可更改  顺序访问  元组  Container  不可更改  顺序访问  字典  Container  可更改  映射访问  另一个问题就是, “为什么要用这么多不同的模型或从不同的方面来分类?” 所有这些 数据类型看上去是很难分类的。它们彼此都有着错综复杂的关系,所有类型的共同之处最好能 揭示出来,而且我们还想揭示每种类型的独到之处。没有两种类型横跨所有的分类。(当然,所 有的数值子类型做到了这一点, 所以我们将它们归纳到一类当中)。最后,我们确信搞清所有 类型之间的关系会对你的开发工作有极大的帮助。你对每种类型的了解越多,你就越能在自己 的程序中使用恰当的类型以达到最佳的性能。 我们提供了一个汇总表(表 4.9)。表中列出了所有的标准类型, 我们使用的三个模型, 以及每种类型归入的分类。 4.9 不支持的类型 在我们深入了解各个标准类型之前,我们在本章的结束列出Python 目前还不支持的数据类 型。 char 或 byte Python 没有 char 或 byte 类型来保存单一字符或 8 比特整数。你可以使用长度为 1 的字 符串表示字符或 8 比特整数。 指针 Python 替你管理内存,因此没有必要访问指针。在 Python 中你可以使用 id()函数得到一 Edit By Vheavens  Edit By Vheavens                                个对象的身份号, 这是最接近于指针的地址。因为你不能控制这个值,所以其实没有太大意义。 其实在 Python 中, 一切都是指针。 int vs short vs long Python 的普通整数相当于标准整数类型,不需要类似 C 语言中的 int, short, long 这三 种整数类型。事实上 Python 的整数实现等同于 C 语言的长整数。 由于 Python 的整型与长整型 密切融合, 用户几乎不需要担心什么。 你仅需要使用一种类型, 就是 Python 的整型。即便 数值超出整型的表达范围, 比如两个很大的数相乘, Python 会自动的返回一个长整数给你而 不会报错。 float VS double C 语言有单精度和双精度两种浮点类型。 Python 的浮点类型实际上是 C 语言的双精度浮 点类型。 Python认为同时支持两种浮点类型的好处与支持两种浮点类型带来的开销不成比例, 所以 Python 决定不支持单精度浮点数。对那些宁愿放弃更大的取值范围而需要更高精确度的 用户来说, Python 还有一种十进制浮点数类型 Decimal, 不过你必须导入 decimal 模块才可 以使用它。浮点数总是不精确的。Decimals 则拥有任意的精度。在处理金钱这类确定的值时, Decimal 类型就很有用。 在处理重量,长度或其它度量单位的场合, float 足够用了。 4.10 练习 4–1. Python 对象。与所有 Python 对象有关的三个属性是什么?请简单的描述一下。 4–2. 类型。不可更改(immutable)指的是什么?Python 的哪些类型是可更改的 (mutable),哪些不是? 4–3. 类型。哪些 Python 类型是按照顺序访问的,它们和映射类型的不同是什么? 4–4. type()。内建函数 type()做什么?type()返回的对象是什么? 4–4. str() 和 repr()。内建函数str()与 repr()之间的不同是什么?哪一个等价于反 引号(``)运算符?。 4–6. 对象相等。您认为type(a) == type(b)和type(a) is type(b)之间的不同是什么? 为什么会选择后者?函数 isinstance()与这有什么关系? 4–7. 内建函数 dir()。在第二章的几个练习中,我们用内建函数 dir()做了几个实验, 它接受一个对象,然后给出相应的属性。请对 types 模块做相同的实验。记下您熟悉的类型, 包括您对这些类型的认识,然后记下你还不熟悉的类型。在学习 Python 的过程中,你要逐步将 “不熟悉”的类型变得“熟悉”起来。 Edit By Vheavens  Edit By Vheavens                                4–8. 列表和元组。列表和元组的相同点是什么?不同点是什么? 4–9. 练习,给定以下赋值: a = 10 b = 10 c = 100 d = 100 e = 10.0 f = 10.0 请问下面各表达式的输出是什么?为什么? (a) a is b (b) c is d (c) e is f Edit By Vheavens  Edit By Vheavens                                数字 本章主题 z 数的简介 z 整型 z 布尔型 z 标准的整型 z 长整型 z 浮点型实数 z 复数 z 操作符 z 内建函数 z 其它数字类型 z 相关模块 Edit By Vheavens  Edit By Vheavens                                本章的主题是 Python 中的数字。我们会详细介绍每一种数字类型,它们适用的各种运算 符, 以及用于处理数字的内建函数。在本章的末尾, 我们简单介绍了几个标准库中用于处理 数字的模块。 5.1 数字类型 数字提供了标量贮存和直接访问。它是不可更改类型,也就是说变更数字的值会生成新的 对象。当然,这个过程无论对程序员还是对用户都是透明的,并不会影响软件的开发方式。 Python 支持多种数字类型:整型、长整型、布尔型、双精度浮点型、十进制浮点型和复数。 如何创建数值对象并用其赋值 (数字对象) 创建数值对象和给变量赋值一样同样简单: anInt = 1 aLong = -9999999999999999L aFloat = 3.1415926535897932384626433832795 Edit By Vheavens  Edit By Vheavens                                aComplex = 1.23+4.56J 如何更新数字对象 通过给数字对象(重新)赋值, 您可以“更新”一个数值对象。我们之所以给更新这两个 字加上引号, 是因为实际上你并没有更新该对象的原始数值。这是因为数值对象是不可改变对 象。Python 的对象模型与常规对象模型有些不同。你所认为的更新实际上是生成了一个新的数 值对象,并得到它的引用。 在学习编程的过程中, 我们一直接受这样的教育, 变量就像一个盒子, 里面装着变量的 值。在 Python 中, 变量更像一个指针指向装变量值的盒子。 对不可改变类型来说, 你无法 改变盒子的内容, 但你可以将指针指向一个新盒子。每次将另外的数字赋给变量的时候,实际 上创建了一个新的对象并把它赋给变量.(不仅仅是数字,对于所有的不可变类型,都是这么回 事) anInt += 1 aFloat = 2.718281828 如何删除数字对象 按照 Python 的法则, 你无法真正删除一个数值对象, 你仅仅是不再使用它而已。如果你 实际上想删除一个数值对象的引用, 使用 del 语句(参见 3.5.6 小节)。 删除对象的引用之 后, 你就不能再使用这个引用(变量名), 除非你给它赋一个新值。如果试图使用一个已经被 删除的对象引用, 会引发 NameError 异常。 del anInt del aLong, aFloat, aComplex 好了, 既然你已经了解如何创建和更新数值对象, 那么来看下 Python 的四种主要数字类 型。 5.2 整型 Python 有几种整数类型。布尔类型是只有两个值的整型。常规整型是绝大多数现代系统都 能识别的整型。Python 也有长整数类型。然而,它表示的数值大小远超过 C 语言的长整数 。 下面我们先来了解一下这些类型,然后再来研究那些用于Python 整数类型的运算符和内建函数。 Edit By Vheavens  Edit By Vheavens                                5.2.1 布尔型 Python 从版本 2.3 开始支持布尔类型。该类型的取值范围只有两个值,也就是布尔值True 和布尔值 False。我们会在本章的末尾一节 5.7.1 详细讲解布尔对象。 5.2.2 标准整数类型 Python 的标准整数类型是最通用的数字类型。在大多数 32 位机器上,标准整数类型的取 值范围是-231 到 231-1,也就是-2,147,483,648 到 2,147,483,647。如果在 64 位机器上使 用 64 位编译器编译 Python,那么在这个系统上的整数将是 64 位。下面是一些 Python 标准整 数类型对象的例子: 0101 84 -237 0x80 017 -680 -0X92 Python 标准整数类型等价于 C 的(有符号)长整型。整数一般以十进制表示,但是Python 也支持八进制或十六进制来表示整数。如果八进制整数以数字“0”开始, 十六进制整数则以 “0x” 或“0X” 开始。 5.2.3 长整型 关于 Python 长整数类型我们必须要提的是, 请不要将它和 C 或其它编译型语言的长整数 类型混淆。那些语言的长整数典型的取值范围是 32 位或 64 位。Python 的长整数类型能表达的 数值仅仅与你的机器支持的(虚拟)内存大小有关, 换句话说, Python 能轻松表达很大很大很 大的整数。 长整数类型是标准整数类型的超集, 当你的程序需要使用比标准整数类型更大的整数时, 长整数类型就有用武之地了。在一个整数值后面加个 L(大写或小写都可以),表示这个整数是 长整数。这个整数可以是十进制,八进制, 或十六进制。下面是一些长整数的例子: 16384L -0x4E8L 017L -2147483648l 052144364L 299792458l 0xDECADEDEADBEEFBADFEEDDEAL -5432101234L Edit By Vheavens  Edit By Vheavens                                核心风格:用大写字母 “L”表示长整数 尽管 Python 也支持用小写字母 L 标记的长整型,但是我们郑重推荐您仅使用大写的 “L”, 这样能有效避免数字 1 和小写 L 的混淆。Python 在显示长整数类型数值的时候总是用大写“L ”, 目前整型和长整型正在逐渐缓慢的统一,您只有在对长整数调用 repr()函数时才有机会看到 “L”,如果对长整数对象调用 str()函数就看不到 L 。举例如下: >>> aLong = 999999999l >>> aLong 999999999L >>> print aLong 999999999 5.2.4 整型和长整型的统一 这两种整数类型正在逐渐统一为一种。在 Python 2.2以前,标准整数类型对象超出取值范 围会溢出(比如上面提到的大于 232 的数),但是从 Python2.2 以后就再也没有这样的错误了。 Python 2.1 >>> 9999 ** 8 Traceback (most recent call last): File "", line 1, in ? OverflowError: integer exponentiation Python 2.2 >>> 9999 ** 8 99920027994400699944002799920001L 移除这个错误是第一步。 下一步修改位移位; 左移比特导致出界(导致 0 值)在过去是 经常可能发生的事; >>> 2 << 32 0 Edit By Vheavens  Edit By Vheavens                                在 Python2.3 中, 这个操作产生一个警告, 不过在 2.4 版里移除了这个 Warning, 并且 这步操作生成了一个真正的长整数。 Python 2.3 >>> 2 << 32 __main__:1: FutureWarning: x<>> 2 << 32 8589934592L 不远的将来,至少普通用户会几乎感觉不到长整型的存在。必要时整型会悄悄自动转换为 长整型。当然,那些要调用 C 的人仍然可以继续使用这两种整数类型, 因为 C 代码必须区分不 同的整数类型。如果你想详细了解标准整型与长整型整合的信息,请阅读 PEP237. 5.3 双精度浮点数 Python 中的浮点数类似 C 语言中的 double 类型, 是双精度浮点数,可以用直接的十进制 或科学计数法表示。每个浮点数占8 个字节(64比特),完全遵守IEEE754 号规范(52M/11E/1S), 其中 52 个比特用于表示底,11 个比特用于表示指数(可表示的范围大约是正负 10 的 308.25 次方), 剩下的一个比特表示符号。这看上去相当完美,然而,实际精度依赖于机器架构和创 建 Python 解释器的编译器。 浮点数值通常都有一个小数点和一个可选的后缀 e(大写或小写,表示科学计数法)。在 e 和指数之间可以用正(+)或负(-)表示指数的正负(正数的话可以省略符号)。下面是一些典 型的浮点数值的例子: 0.0 -777. 1.6 -5.555567119 96e3 * 1.0 4.3e25 9.384e-23 -2.172818 float(12) 1.000000001 3.1416 4.2E-10 -90. 6.022e23 -1.609E-19 Edit By Vheavens  Edit By Vheavens                                5.4 复数 在很久以前,数学家们被下面这样的等式困扰。 x2 = -1 这是因为任何实数(无论正数还是负数)乘以自己总是会得到一个非负数。一个数怎么可 以乘以自己却得到一个负数?没有这样的实数存在。就这样, 直到 18 世纪, 数学家们发明了 一个虚拟的数 i (或者叫 j,看你读的是哪本教科书了) j= 基于这个特殊的数(或者称之为概念),数学从此有了一个新的分支。现在虚数已经广泛应 用于数值和科学计算应用程序中。一个实数和一个虚数的组合构成一个复数。一个复数是一对 有序浮点数(x, y)。表示为 x + yj, 其中 x 是实数部分,y 是虚数部分。 渐渐的复数在日常运算,机械,电子等行业获得了广泛的应用。由于一些研究人员不断的重 复制造用于复数运算的工具, 在很久以前的 Python1.4 版本里,复数终于成为一个真正的 Python 数据类型。 下面是 Python 语言中有关复数的几个概念: z 虚数不能单独存在,它们总是和一个值为 0.0 的实数部分一起来构成一个复数。 z 复数由实数部分和虚数部分构成 z 表示虚数的语法: real+imagj z 实数部分和虚数部分都是浮点数 z 虚数部分必须有后缀j或J。 下面是一些复数的例子: 64.375+1j 4.23-8.5j 0.23-8.55j 1.23e-045+6.7e+089j 6.23+1.5j -1.23-875J 0+1j 9.80665-8.31441J -.0224+0j 5.4.1 复数的内建属性 复数对象拥有数据属性(参见 4.1.1 节), 分别为该复数的实部和虚部。复数还拥有 conjugate 方法, 调用它可以返回该复数的共轭复数对象。(两头牛背上的架子称为轭,轭使 Edit By Vheavens  Edit By Vheavens                                两头牛同步行走。共轭即为按一定的规律相配的一对——译者注) 表 5.1 复数属性 属性 描述 num.real 该复数的实部 num num.imag 该复数的虚部 num.conjugate() 返回该复数的共轭复数 >>> aComplex = -8.333-1.47j >>> aComplex (-8.333-1.47j) >>> aComplex.real -8.333 >>> aComplex.imag -1.47 >>> aComplex.conjugate() (-8.333+1.47j) 表 5.1 描述了复数的所有属性 5.5 运算符 数值类型可进行多种运算。从标准运算符到数值运算符,甚至还有专门的整数运算符。 5.5.1 混合模式运算符 也许你还记得, 过去将两个数相加时, 你必须努力保证操作数是合适的类型。自然而然的, 加法总是使用 + 号, 然而在计算机语言看来这件事没那么简单,因为数字又有很多不同的类 型。 当两个整数相加时, + 号表示整数加法, 当两个浮点数相加时, + 表示浮点数加法, 依 此类推。在 Python 中, 甚至非数字类型也可以使用 + 运算符。举例来说, 字符串 A + 字符 串 B 并不表示加法操作, 它表示的是把这两个字符串连接起来, 生成一个新的字符串。关键 之处在于支持 + 运算符的每种数据类型, 必须告诉 Python, + 运算符应该如何去工作。 这 也体现了重载概念的具体应用。 Edit By Vheavens  Edit By Vheavens                                虽然我们不能让一个数字和一个字符串相加, 但 Python 确实支持不同的数字类型相加。 当一个整数和一个浮点数相加时, 系统会决定使用整数加法还是浮点数加法(实际上并不存在 混合运算)。Python 使用数字类型强制转换的方法来解决数字类型不一致的问题, 也就是说它 会强制将一个操作数转换为同另一个操作数相同的数据类型。这种操作不是随意进行的, 它遵 循以下基本规则。 首先,如果两个操作数都是同一种数据类型,没有必要进行类型转换。仅当两个操作数类 型不一致时, Python 才会去检查一个操作数是否可以转换为另一类型的操作数。如果可以, 转换它并返回转换结果。由于某些转换是不可能的,比如果将一个复数转换为非复数类型, 将 一个浮点数转换为整数等等,因此转换过程必须遵守几个规则。 要将一个整数转换为浮点数,只要在整数后面加个 .0 就可以了。 要将一个非复数转换为 复数,则只需要要加上一个 “0j” 的虚数部分。这些类型转换的基本原则是: 整数转换为浮 点数, 非复数转换为复数。 在 Python 语言参考中这样描述 coerce() 方法: z 如果有一个操作数是复数, 另一个操作数被转换为复数。 z 否则,如果有一个操作数是浮点数, 另一个操作数被转换为浮点数。 z 否则, 如果有一个操作数是长整数,则另一个操作数被转换为长整数; z 否则,两者必然都是普通整数,无须类型转换。(参见下文中的示意图) 图 5-1 的流程图阐释了强制转换的规则。数字类型之间的转换是自动进行的,程序员无须 自己编码处理类型转换。不过在确实需要明确指定对某种数据类型进行特殊类型转换的场合, Python 提供了 coerce() 内建函数来帮助你实现这种转换。(见5.6.2 小节) Edit By Vheavens  Edit By Vheavens                                图 5-1 数值类型转换 下面演示一下 Python 的自动数据类型转换。为了让一个整数和一个浮点数相加, 必须使 二者转换为同一类型。因为浮点数是超集,所以在运算开始之前, 整数必须强制转换为一个浮 点数,运算结果也是浮点数: >>> 1 + 4.5 5.5 Edit By Vheavens  Edit By Vheavens                                5.5.2 标准类型运算符 第四章中讲到的标准运算符都可以用于数值类型。上文中提到的混合模式运算问题, 也就 是不同数据类型之间的运算, 在运算之前,Python内部会将两个操作数转换为同一数据类型。 下面是一些数字标准运算的例子: >>> 5.2 == 5.2 True >>> -719 >= 833 False >>> 5+4e >= 2-3e True >>> 2 < 5 < 9 # same as ( 2 < 5 ) and ( 5 < 9 ) True >>> 77 > 66 == 66 # same as ( 77 > 66 ) and ( 66 == 66 ) True >>> 0. < -90.4 < 55.3e2 != 3 < 181 False >>> (-1 < 1) or (1 < -1) True 5.5.3 算术运算符 Python 支持单目运算符正号(+)和负号(-), 双目运算符, +,-,*,/,%,还有 ** , 分别表示加法,减法, 乘法, 除法, 取余, 和幂运算。从 Python2.2 起,还增加了一种新 的整除运算符 // 。 除法 拥有 C 背景的程序员一定熟悉传统除法――也就是说, 对整数操作数,会执行“地板除” (floor,取比商小的最大整数。例如 5 除以 2 等于 2.5,其中“2”就称为商的“地板”,即“ 地 板除”的结果。本书中使用“地板除”的说法是为了沿用原作者的风格,译者注)。对浮点操作 数会执行真正的除法。然而,对第一次学编程的人或者那些依赖精确计算的人来说,可能就需 要多次调整代码才能得到自己想要的结果。 在未来的 Python 版本中,Python 开发小组已经决定改变 / 运算符的行为。/ 的行为将变 更为真正的除法, 会增加一种新的运算来表示地板除。 下面我们总结一下 Python 现在的除法 规则, 以及未来的除法规则: Edit By Vheavens  Edit By Vheavens                                传统除法 如果是整数除法, 传统除法会舍去小数部分,返回一个整数(地板除)。如果操作数之一 是浮点数,则执行真正的除法。包括Python 语言在内的很多语言都是这种行为。看下面的例子: >>> 1 / 2 # perform integer result (floor) # 地板除 0 >>> 1.0 / 2.0 # returns actual quotient#真正除法 0.5 真正的除法 除法运算总是返回真实的商, 不管操作数是整数还是浮点数。在未来版本的 Python 中, 这将是除法运算的标准行为。现阶段通过执行 from __future__ import division 指令, 也 可以做到这一点。 >>> from __future__ import division >>> >>> 1 / 2 # returns real quotient 0.5 >>> 1.0 / 2.0 # returns real quotient 0.5 地板除 从 Python 2.2开始, 一个新的运算符 // 已经被增加进来, 以执行地板除: // 除法不 管操作数何种数值类型,总是舍去小数部分,返回数字序列中比真正的商小的最接近的数字。 >>> 1 // 2 # floors result, returns integer # 地板除, 返回整数 0 >>> 1.0 // 2.0 # floors result, returns float # 地板除, 返回浮点数 0.0 >>> -1 // 2 # move left on number line# 返回比 –0.5 小的整数, 也就是 -1 -1 Edit By Vheavens  Edit By Vheavens                                关于除法运算的变更, 支持的人和反对的人几乎一样多。有些人认为这种变化是错误的, 有些人则不想修改自己的现有代码,而剩下的人则想要真正的除法。 之所以会有这种变化是因为 Python 的核心开发团队认为 Python 的除法运算从一开始就设 计失误。特别是, 随着 Python 的逐渐发展, 它已经成为那些从未接触过地板除的人们的首选 学习语言。Python 语言的发明人 范•罗萨姆 在他的 《Python 2.2 新增功能》一文中讲到: def velocity(distance, totalTime): rate = distance / totalTime 你可能会说, 只要有一个参数为浮点数这个函数就能正常工作。像上面提到的那样,要确 保 它 能正常 工 作需要 强 制将参 数 转换为 浮 点类型 , 也就是 rate = float(distance) / float(totalTime)。将来除法将变更为真正的除法,上面的代码可以无需更改正常工作。需要 地板除的地方只需要改变为两个连续的 除号。 是的, 代码会受到一些影响, Python 团队已经创作了一系列脚本帮助你转换旧代码, 以确保它能适应新的除法行为。而且对那些强烈需要某种除法行为的人来说, Python 解释器 提供了 Qdivision_style 启动参数。 -Qnew 执行新的除法行为, -Qold 则执行传统除法行 为(默认为 Qold)。你也可以帮助你的用户使用-Qwarn 或 –Qwarnall 参数度过过渡时期。 关于这次变化的详细信息可以参考 PEP238。如果你对这场论战感兴趣,也可以翻阅 2001 年的 comp.lang.python 归档。 表 5.2 总结了除法运算符在不同 Python 版本中的行为差异。 取余 整数取余相当容易理解, 浮点数取余就略复杂些。 表 5.2 除法操作符的行为 Edit By Vheavens  Edit By Vheavens                                商取小于等于精确值的最大整数的乘积之差. 即: x - (math.floor(x/y) * y) 或者 对于复数,取余的定义类似于浮点数,不同之处在于商仅取其实数部分,即: x - (math.floor((x/y).real) * y)。 幂运算 幂运算操作符和一元操作符之间的优先级关系比较特别: 幂运算操作符比其左侧操作数 的一元操作符优先级低,比 起右侧操作数的一元操作符的优先级高,由于这个特性你会在算术运算符表中找到两个 ** .下面举几个例子: >>> 3 ** 2 9 >>> -3 ** 2 # ** 优先级高于左侧的 - -9 >>> (-3) ** 2 # 加括号提高 -的优先级 9 >>> 4.0 ** -1.0 # ** 优先级低于右侧的 - 0.25 第 2 种情况下解释器先计算 3**2 再取其相反数,我们需要给"-3"加上括号来得到我们希 Edit By Vheavens  Edit By Vheavens                                望的结果。最后一个例子,结果是 4**(-1),这是按照规定的优先级获得的结果. >>> 4 ** -1 Traceback (innermost last): File "", line 1, in ? ValueError: integer to the negative power 总结 表 5.3 总结了所有的算术运算符, 从上到下, 计算优先级依次降低。 这里列出的所有 运算符都比即将在 5.5.4 小节讲到的位运算符优先级高。 表 5.3 算术运算符    算术去处符         功能  表达式1 表达式2 结果 +expr 结果符号不变  -expr  对结果符号取负  表达式1 表达式2 结果 expr1 * expr2 表达式1 乘 表达式2 expr1 / expr2 表达式1 除以 表达式2(传统除或真正除)   expr1 // expr2 表达式1 地板除以 表达式2 expr1 % expr2 表达式1 对表达式2 取余 expr1 + expr2 表达式1 加 表达式2 expr1 - expr2 表达式1 减 表达式2 注:** 运算符优先级高于单目运算符 下面是更多 Python 数值运算的例子: >>> -442 - 77 -519 >>> Edit By Vheavens  Edit By Vheavens                                >>> 4 ** 3 64 >>> >>> 4.2 ** 3.2 98.7183139527 >>> 8 / 3 2 >>> 8.0 / 3.0 2.66666666667 >>> 8 % 3 2 >>> (60. - 32.) * ( 5. / 9. ) 15.5555555556 >>> 14 * 0x04 56 >>> 0170 / 4 30 >>> 0x80 + 0777 639 >>> 45L * 22L 990L >>> 16399L + 0xA94E8L 709879L >>> -2147483648L - 52147483648L -54294967296L >>> 64.375+1j + 4.23-8.5j (68.605-7.5j) >>> 0+1j ** 2 # same as 0+(lj**2) (-1+0j) >>> 1+1j ** 2 # same as 1+(lj**2) 0j >>> (1+1j) ** 2 2j 注意指数运算符的优先级高于连接实部和虚部的+号运算符。就上面最后一个例子来说, 我 们人为的加上了括号,这就改变运算顺序, 从而得到我们想要的结果。 5.5.4 *位运算符(只适用于整数) Edit By Vheavens  Edit By Vheavens                                Python 整数支持标准位运算:取反(~),按位 与(&), 或(|) 及 异或(^) 及左移(<<)和右 移(>>)。Python 这样处理位运算: z 负数会被当成正数的 2 进制补码处理。 z 左移和右移 N 位等同于无溢出检查的2的N次幂运算: 2**N。 z 对长整数来说, 位运算符使用一种经修改的 2 进制补码形式,使得符号位可以无限的 向左扩展。 取反(~)运算的优先级与数字单目运算符相同, 是所有位操作符中优先级最高的一个。 左 移和右移运算的优先级次之,但低于加减法运算。与, 或, 异或 运算优先级最低。所有位运 算符按优先级高低列在表 5.4 中。 表 5.4 整型位运算符 位运算符  功能  ~num  单目运算,对数的每一位取反。结果为  num1 << num2  Num1 左移 num2 位  num1 >> num2  Num1 右移 num2 位  num1 & num2 num1 与 num2 按位 与 num1 ^ num2 num1 异或 num2 num1 | num2 num1 与 num2 按位 或 下面是几个使用整数 30(011110),45(101101),60(111100)进行位运算的例子: >>> 30 & 45 12 Edit By Vheavens  Edit By Vheavens                                >>> 30 | 45 63 >>> 45 & 60 44 >>> 45 | 60 61 >>> ~30 -31 >>> ~45 -46 >>> 45 << 1 90 >>> 60 >> 2 15 >>> 30 ^ 45 51 5.6 内建函数与工厂函数 5.6.1 标准类型函数 在上一章中, 我们介绍了 cmp(), str() 和 type() 内建函数。 这些函数可以用于所有 的标准类型。对数字对象来说, 这些函数分别比较两个数的大小, 将数字转换为字符串, 以 及返回数字对象的类型。 >>> cmp(-6, 2) -1 >>> cmp(-4.333333, -2.718281828) -1 >>> cmp(0xFF, 255) 0 >>> str(0xFF) '255' >>> str(55.3e2) '5530.0' >>> type(0xFF) >>> type(98765432109876543210L) Edit By Vheavens  Edit By Vheavens                                >>> type(2-1j) 5.6.2 数字类型函数 Python 现在拥有一系列针对数字类型的内建函数。一些函数用于数字类型转换, 另一些 则执行一些常用运算。 转换工厂函数 函数 int(), long(), float() 和 complex() 用来将其它数值类型转换为相应的数值类型。 从 Python 1.5 版本开始, 这些函数也接受字符串参数, 返回字符串所表示的数值。从 Python 1.6 版开始,int() 和 long() 在转换字符串时,接受一个进制参数。如果是数字类型之间的 转换,则这个进制参数不能使用。 从 Python2.2 起, 有了第五个内建函数 bool()。它用来将整数值1和0转换为标准布尔 值 True 和 False. 从 Python2.3 开始, Python 的标准数据类型添加了一个新成员:布尔 (Boolean)类型。从此 true 和 false 现在有了常量值即 True 和 False(不再是1和0)。 要了解布尔类型的更多信息, 参阅 5.7.1 小节。 另外, 由于 Python 2.2对类型和类进行了整合(这里指 Python 的传统风格类和新风格类 ——译者注), 所有这些内建函数现在都转变为工厂函数。我们曾经在第四章介绍过工厂函数, 所谓工厂函数就是指这些内建函数都是类对象, 当你调用它们时,实际上是创建了一个类实例。 不过不用担心, 这些函数的使用方法并没有什么改变。 下面是一些使用内建函数的示例: >>> int(4.25555) 4 >>> long(42) 42L >>> float(4) 4.0 >>> complex(4) (4+0j) >>> Edit By Vheavens  Edit By Vheavens                                >>> complex(2.4, -8) (2.4-8j) >>> >>> complex(2.3e-10, 45.3e4) (2.3e-10+453000j) 表 5.5 数值工厂函数总结 类(工厂函数)                操作  bool(obj) b  返回obj对象的布尔值,也就是 obj.__nonzero__()方法的返回值 int(obj, base=10) 返回一个字符串或数值对象的整数表 示,  类似string.atoi();从Python 1.6起, 引入了可选的进制参数。 long(obj, base=10) 返回一个字符或数据对象的长整数表 示,类似string.atol(),  从Python1.6起,  引入了可选的进制参数 float(obj) 返回一个字符串或数据对象的浮点数 表示,类似string.atof() complex(str) or complex(real, imag=0.0) 返回一个字符串的复数表示,或 者根据给定的实数(及一个可选 的虚数部分)生成一个复数对象。 a. 在 Python2.3 之前, 这些都是内建函数 b. Python2.2 中新增的内建函数,在 Python2.3 中改变为工厂函数 功能函数 Python 有五个运算内建函数用于数值运算: abs(), coerce(), divmod(), pow(), pow() 和 round()。我们将对这些函数逐一浏览,并给出一些有用的例子: Edit By Vheavens  Edit By Vheavens                                abs()返回给定参数的绝对值。如果参数是一个复数, 那么就返回math.sqrt(num.real2 + num.imag2)。下面是几个 abs()函数的示例: >>> abs(-1) 1 >>> abs(10.) 10.0 >>> abs(1.2-2.1j) 2.41867732449 >>> abs(0.23 - 0.78) 0.55 函数 coerce(),尽管从技术上讲它是一个数据类型转换函数,不过它的行为更像一个运算 符,因此我将它放到了这一小节。在 5.5.1 小节,我们讨论了 Python 如何执行数值类型转换。 函数 coerce()为程序员提供了不依赖 Python 解释器, 而是自定义两个数值类型转换的方法。 对一种新创建的数值类型来说, 这个特性非常有用。函数 coerce()仅回一个包含类型转换完 毕的两个数值元素的元组。下面是几个例子: >>> coerce(1, 2) (1, 2) >>> >>> coerce(1.3, 134L) (1.3, 134.0) >>> >>> coerce(1, 134L) (1L, 134L) >>> >>> coerce(1j, 134L) (1j, (134+0j)) >>> >>> coerce(1.23-41j, 134L) ((1.23-41j), (134+0j)) divmod()内建函数把除法和取余运算结合起来, 返回一个包含商和余数的元组。对整数来 说,它的返回值就是地板除和取余操作的结果。对浮点数来说, 返回的商部分是 math.floor(num1/num2),对复数来说, 商部分是 ath.floor((num1/num2).real)。 >>> divmod(10,3) (3, 1) >>> divmod(3,10) Edit By Vheavens  Edit By Vheavens                                (0, 3) >>> divmod(10,2.5) (4.0, 0.0) >>> divmod(2.5,10) (0.0, 2.5) >>> divmod(2+1j, 0.5-1j) (0j, (2+1j)) 函数 pow() 和双星号 (**) 运算符都可以进行指数运算。不过二者的区别并不仅仅在于 一个是运算符,一个是内建函数。 在 Python 1.5 之前,并没有 ** 运算符。内建函数 pow()还接受第三个可选的参数,一个 余数参数。如果有这个参数的, pow() 先进行指数运算,然后将运算结果和第三个参数进行取 余运算。这个特性主要用于密码运算,并且比 pow(x,y) % z 性能更好, 这是因为这个函数的 实现类似于 C 函数 pow(x,y,z)。 >>> pow(2,5) 32 >>> >>> pow(5,2) 25 >>> pow(3.141592,2) 9.86960029446 >>> >>> pow(1+1j, 3) (-2+2j) 内建函数 round()用于对浮点数进行四舍五入运算。它有一个可选的小数位数参数。如果 不提供小数位参数, 它返回与第一个参数最接近的整数(但仍然是浮点类型)。第二个参数告 诉 round 函数将结果精确到小数点后指定位数。 >>> round(3) 3.0 >>> round(3.45) 3.0 >>> round(3.4999999) 3.0 >>> round(3.4999999, 1) 3.5 >>> import math Edit By Vheavens  Edit By Vheavens                                >>> for eachNum in range(10): ... print round(math.pi, eachNum) ... 3.0 3.1 3.14 3.142 3.1416 3.14159 3.141593 3.1415927 3.14159265 3.141592654 3.1415926536 >>> round(-3.5) -4.0 >>> round(-3.4) -3.0 >>> round(-3.49) -3.0 >>> round(-3.49, 1) -3.5 值得注意的是 round() 函数是按四舍五入的规则进行取整。也就是 round(0.5)得到 1, round(-0.5)得到-1。猛一看 int(), round(), math.floor() 这几个函数好像做的是同一件 事, 很容易将它们弄混,是不是?下面列出它们之间的不同之处: z 函数 int()直接截去小数部分。(返回值为整数) z 函数 floor()得到最接近原数但小于原数的整数。(返回值为浮点数) z 函数 round()得到最接近原数的整数。(返回值为浮点数) z 的例子用四个正数和四个负数作为这三个函数的参数,将返回结果列在一起做个比较。 (为了便于比较,我们将 int()函数的返回值也转换成了浮点数)。 >>> import math >>> for eachNum in (.2, .7, 1.2, 1.7, -.2, -.7, -1.2, -1.7): ... print "int(%.1f)\t%+.1f" % (eachNum, float(int(each- Num))) ... print "floor(%.1f)\t%+.1f" % (eachNum, Edit By Vheavens  Edit By Vheavens                                ... math.floor(eachNum)) ... print "round(%.1f)\t%+.1f" % (eachNum, round(eachNum)) ... print '-' * 20 ... int(0.2) +0.0 floor(0.2) +0.0 round(0.2) +0.0 -------------------- int(0.7) +0.0 floor(0.7) +0.0 round(0.7) +1.0 -------------------- int(1.2) +1.0 floor(1.2) +1.0 round(1.2) +1.0 -------------------- int(1.7) +1.0 floor(1.7) +1.0 round(1.7) +2.0 -------------------- int(-0.2) +0.0 floor(-0.2) -1.0 round(-0.2) +0.0 -------------------- int(-0.7) +0.0 floor(-0.7) -1.0 round(-0.7) -1.0 -------------------- int(-1.2) -1.0 floor(-1.2) -2.0 round(-1.2) -1.0 -------------------- int(-1.7) -1.0 floor(-1.7) -2.0 round(-1.7) -2.0 表 5.6 数值运算函数一览 表 5.6 数值运算内建函数 Edit By Vheavens  Edit By Vheavens                                  函数                功能  abs(num) 返回 num 的绝对值 coerce(num1, num2) 将num1和num2转换为同一类型,然后以一个  元组的形式 返回。 divmod(num1, num2) 除法-取余运算的结合。返回一个元组(num1/num2,num1 %  num2)。对浮点数和复数的商进行下舍入(复数仅取实    数部分的商)  pow(num1, num2, mod=1) 取 num1 的 num2次方,如果提供 mod参数,则计算结果 再对mod进行取余运算  round(flt, ndig=0) 接受一个浮点数  flt  并对其四舍五入,保存  ndig位小数。 若不提供ndig  参数,则默认小数点后0位。    round()仅用于浮点数。(译者注:整数也可以,  不过并没有什么 实际意义)  5.6.3 仅用于整数的函数 除了适应于所有数值类型的内建函数之外,Python还提供一些仅适用于整数的内建函数(标 准整数和长整数)。这些函数分为两类,一类用于进制转换,另一类用于ASCII 转换。 进制转换函数 前面我们已经看到,除了十进制标准,Python 整数也支持八进制和 16 进制整数。 除此之 外, Python还提供了两个内建函数来返回字符串表示的 8 进制和 16 进制整数。它们分别是 oct() 和 hex()。它们都接受一个整数(任意进制的)对象,并返回一个对应值的字符串对象。下面 是几个示例: >>> hex(255) '0xff' Edit By Vheavens  Edit By Vheavens                                >>> hex(23094823l) '0x1606627L' >>> hex(65535*2) '0x1fffe' >>> >>> oct(255) '0377' >>> oct(23094823l) '0130063047L' >>> oct(65535*2) '0377776' ASCII 转换函数 Python 也提供了 ASCII(美国标准信息交换码)码与其序列值之间的转换函数。每个字符 对应一个唯一的整数(0-255)。对所有使用ASCII 表的计算机来说, 这个数值是不变的。这 保证了不同系统之间程序行为的一致性。函数chr()接受一个单字节整数值,返回一个字符串, 其值为对应的字符。函数 ord()则相反,它接受一个字符,返回其对应的整数值。 >>> ord('a') 97 >>> ord('A') 65 >>> ord('0') 48 >>> chr(97) 'a' >>> chr(65L) 'A' >>> chr(48) '0' 表 5.7 列出了用于整数类型的所有内建函数 表 5.7 仅适用于整数的内建函数 函数           操作  Edit By Vheavens  Edit By Vheavens                                hex(num) 将数字转换成十六进制数并以字符串形式返回  oct(num) 将数字转换成八进制数并以字符串形式返回  chr(num) 将ASCII值的数字转换成ASCII字符,范围只 能是0 <= num <= 255。  ord(chr) 接受一个  ASCII  或  Unicode  字符(长度为1的字符串),返回相应的ASCII  或Unicode  值。  unichr(num) 接受Unicode码值,返回  其对应的Unicode字符。所接受的码值范围依赖于 你的Python是构建于UCS‐2还是UCS‐4。  5.7 其他数字类型 5.7.1 布尔“数” 从 Python2.3 开始,布尔类型添加到了Python 中来。尽管布尔值看上去是“True” 和“False, 但是事实上是整型的子类,对应与整数的 1 和 0。下面是有关布尔类型的主要概念: z 有两个永不改变的值 True 或 False。 z 布尔型是整型的子类,但是不能再被继承而生成它的子类。 z 没有__nonzero__()方法的对象的默认值是 True。 z 对于值为零的任何数字或空集(空列表、空元组和空字典等)在 Python 中的布尔值都 是 False。 z 在数学运算中,Boolean 值的 True 和 False 分别对应于 1 和 0。 z 以前返回整数的大部分标准库函数和内建布尔型函数现在返回布尔型。 z True 和 False 现在都不是关键字,但是在 Python 将来的版本中会是。 所有 Python 对象都有一个内建的 True 或 False 值,对内建类型来说,这个值究竟是 True 还是 False 请参阅章节 4.3.2 中的核心备注。下面是使用内建类型布尔值的一些例子: Edit By Vheavens  Edit By Vheavens                                # intro >>> bool(1) True >>> bool(True) True >>> bool(0) False >>> bool('1') True >>> bool('0') True >>> bool([]) False >>> bool ( (1,) ) True # 使用布尔数 >>> foo = 42 >>> bar = foo < 100 >>> bar True >>> print bar + 100 101 >>> print '%s' % bar True >>> print '%d' % bar 1 # 无 __nonzero__() >>> class C: pass >>> c = C() >>> >>> bool(c) True >>> bool(C) True # 重载 __nonzero__() 使它返回 False >>> class C: ... def __nonzero__(self): ... return False ... >>> c = C() >>> bool(c) False >>> bool(C) True # 哦,别这么干!! (无论如何不要这么干!) >>> True, False = False, True >>> bool(True) False Edit By Vheavens  Edit By Vheavens                                >>> bool(False) True 你可以在 Python 文档和 PEP 285 看到有关布尔类型的知识。 5.7.2 十进制浮点数 从 Python2.4 起(参阅 PEP327)十进制浮点制成为一个 Python 特性。这主要是因为下面 的语句经常会让一些编写科学计算或金融应用程序的程序员抓狂: >>> 0.1 0.1000000000000001 为什么会这样?这是因为语言绝大多数 C 语言的双精度实现都遵守 IEEE 754 规范,其中 52 位用于底。因此浮点值只能有 52 位精度,类似这样的值的二进制表示只能象上面那样被截 断。0.1 的二进制表示是 0.11001100110011 . . . 因为最接近的二进制表示就是.0001100110011...或 1/16 +1/32 + 1/256 + . . . 你可以看到,这些片断不停的重复直到舍入出错。如果我们使用十进制来做同样的事情, 感觉就会好很多,看上去会有任意的精度。注意下面,你不能混用十进制浮点数和普通的浮点 数。你可以通过字符串或其它十进制数创建十进制数浮点数。你必须导入 decimal 模块以便 使用 Decimal 类: >>> from decimal import Decimal >>> dec = Decimal(.1) Traceback (most recent call last): File "", line 1, in ? File "/usr/local/lib/python2.4/decimal.py", line 523, in __new__ raise TypeError("Cannot convert float to Decimal. " + TypeError: Cannot convert float to Decimal. First convert the float to a string >>> dec = Decimal('.1') >>> dec Decimal("0.1") >>> print dec 0.1 >>> dec + 1.0 Traceback (most recent call last): File "", line 1, in ? File "/usr/local/lib/python2.4/decimal.py", line 906, in __add__ other = _convert_other(other) File "/usr/local/lib/python2.4/decimal.py", line 2863, in _convert_other Edit By Vheavens  Edit By Vheavens                                raise TypeError, "You can interact Decimal only with int, long or Decimal data types." TypeError: You can interact Decimal only with int, long or Decimal data types. >>> >>> dec + Decimal('1.0') Decimal("1.1") >>> print dec + Decimal('1.0') 1.1 你可以从 Python 文档中读取相关的 PEP 以了解十进制数。值得庆幸的是,十进制数和其 它数值类型一样, 可以使用同样的算术运算符。由于十进制数本质上是一种用于数值计算的特 殊类, 我们在本章的剩余部分将不再专门讲解十进制数。 5.8 相关模块 在 Python 标准库中有不少专门用于处理数值类型对象的模块,它们增强并扩展了内建函数 的功能和数值运算的功能。 表 5.8 列出了几个比较核心的模块。要详细了解这些模块,请参阅 这些模块的文献或在线文档。 对高级的数字科学计算应用来说,你会对著名的第三方包 Numeric(NumPy) 和SciPy 感兴 趣。关于这两个包的详细请访问下面的网址。 http://numeric.scipy.org/ http://scipy.org/ 表 5.8 数字类型相关模块 模块          介绍  decimal 十进制浮点运算类  Decimal  array 高效数值数组(字符,整数,浮点数等等)  math/cmath 标准C库数学运算函数。常规数学运算在match模块, 复数运算在cmath模块  operator 数字运算符的函数实现。比如 tor.sub(m,n)等价 Edit By Vheavens  Edit By Vheavens                                于 m - n random 多种伪随机数生成器 核心模块: random 当你的程序需要随机数功能时,random 模块就能派上用场。该模块包含多个伪随机数发生 器,它们均以当前的时间戳为随机数种子。这样只要载入这个模块就能随时开始工作。下面列 出了该模块中最常用的函数: 两个整数参数,返回二者之间的随机整数 randrange() 它接受和 range()函数一样的参数, 随机返回 range([start,]stop[,step])结果的一项 uniform() 几乎和 randint()一样,不过它返回的是二者之间的一个浮点数(不包括范围 上限)。 random() 类似 uniform() 只不过下限恒等于 0.0,上限恒等于 1.0 choice() 随机返回给定序列(关于序列,见第六章)的一个元素 到这儿,我们的 Python 数值类型之旅就该结束了。 表 5.9 总结了数值类型的所有内建函数和运算符。 Edit By Vheavens  Edit By Vheavens                                运算符/内建函数  描述  整型   长整型   浮点型  复数   结果          Table 5.9 Operators and Built-in Functions for All Numeric Types Edit By Vheavens  Edit By Vheavens                                (continued) Operator/  Built‐in  Description  Int    Long Float  Complex  Resulta    +  Addition  • •  •  • number  加法            -  Subtraction  • •  •  • number  减法            <<  Bit left shift  ••  int/lo 位左移     >>  Bit right shift  ••  int/lo 位右移     &  Bitwise AND  • •  int/lo 按位与运算      ^  Bitwise XOR  ••  int/lo 按位异或运算    |  Bitwise OR  •ˇ •ˇˇ int/lo   按位或运算        a. 结果为 number 表示可以为所有四种数值类型,可能与操作数相同 b.   与单目运算符有特殊关系,参阅 5.5.3 小节和表 5.2 c. 单目运算符 5.9 练习 本章的练习可以先通过应用程序的形式实现。一旦功能齐备并且调试通过, 建议读者将自 己的代码功能用函数封装起来,以便 在后面的练习中重用代码。关于编程风格我在这儿提醒一 下,最好不要在函数内使用 print 语句输出信息,而是通过 return 语句返回必要的值。 这 样调用函数的代码就可以自己处理显示方式。这样你的代码就适应性更广,更便于重用。 Edit By Vheavens  Edit By Vheavens                                5-1 整形 讲讲 Python 普通整型和长整型的区别 5-2 运算符 (a) 写一个函数,计算并返回两个数的乘积 (b) 写一段代码调用这个函数,并显示它的结果 5-3 标准类型运算符. 写一段脚本,输入一个测验成绩,根据下面的标准,输出他的评分 成绩(A-F)。 A: 90–100 B: 80–89 C: 70–79 D: 60–69 F: <60 5-4 取余。判断给定年份是否是闰年。使用下面的公式: 一个闰年就是指它可以被 4 整除,但不能被 100 整除, 或者它既可以被 4 又可以被 100 整 除。比如 1992,1996 和 2000 年是闰年,但 1967 和 1900 则不是闰年。下一个是闰年的整世 纪是 2400 年。 5-5 取余。取一个任意小于 1 美元的金额,然后计算可以换成最少多少枚硬币。硬币有 1 美分,5 美分,10 美分,25 美分四种。1 美元等于 100 美分。举例来说,0.76 美元换算结果 应该是 3 枚 25 美分,1 枚 1 美分。类似 76 枚 1 美分,2 枚 25 美分+2 枚 10 美分+1 枚 5 美分+1 枚 1 美分这样的结果都是不符合要求的。 5-6 算术。写一个计算器程序 你的代码可以接受这样的表达式,两个操作数加一个运算符: N1 运算符 N2. 其中 N1 和 N2 为整数或浮点数,运算符可以是+, -, *, /, %, ** 分别表示 加法,减法, 乘法, 整数除,取余和幂运算。计算这个表达式的结果,然后显示出来。提示: 可以使用字符串方法 split(),但不可以使用内建函数 eval(). 5-7 营业税。随意取一个商品金额,然后根据当地营业税额度计算应该交纳的营业税。 5-8 几何。计算面积和体积: (a) 正方形 和 立方体 (b) 圆 和 球 5–9. 数值形式 回答下面关于数值格式的问题: (a) 为什么下面的例子里 17+32 等于 49, 而 017+32 等于 47, 017+032 等于 41? >>> 17 + 32 Edit By Vheavens  Edit By Vheavens                                49 >>> 017+ 32 47 >>> 017 + 032 41 (b)为什么下面这个表达式我们得到的结果是 134L 而不是 1342 ? >>> 56l + 78l 134L 5-10 转换。写一对函数来进行华氏度到摄氏度的转换。转换公式为 C = (F - 32) * (5 / 9) 应该在这个练习中使用真正的除法, 否则你会得到不正确的结果。 5-11 取余。 (a) 使用循环和算术运算,求出 0-20 之间的所有偶数 (b) 同上,不过这次输出所有的奇数 (c) 综合 (a) 和 (b), 请问辨别奇数和偶数的最简单的方法是什么? (d) 使用(c)的成果,写一个函数,检测一个整数能否被另一个整数整除。 先要求用户输 入两个数,然后你的函数判断两者是否有整除关系,根据判断结果分别返回 True 和 False; 5-12 系统限制。写一段脚本确认一下你的 Python 所能处理的整数,长整数,浮点数和复 数的范围。 5-13 转换。写一个函数把由小时和分钟表示的时间转换为只用分钟表示的时间。 5-14 银行利息。写一个函数,以定期存款利率为参数, 假定该账户每日计算复利,请计 算并返回年回报率。 5–15. 最大公约数和最小公倍数。请计算两个整数的最大公约数和最小公倍数。 5-16 家庭财务。给定一个初始金额和月开销数, 使用循环,确定剩下的金额和当月的支 出数, 包括最后的支出数。 Payment() 函数会用到初始金额和月额度, 输出结果应该类似下 面的格式(例子中的数字仅用于演示): Enter opening balance:100.00 Edit By Vheavens  Edit By Vheavens                                Enter monthly payment: 16.13 Amount Remaining Pymt# Paid Balance ----- ------ --------- 0 $ 0.00 $100.00 1 $16.13 $ 83.87 2 $16.13 $ 67.74 3 $16.13 $ 51.61 4 $16.13 $ 35.48 5 $16.13 $ 19.35 6 $16.13 $ 3.22 7 $ 3.22 $ 0.00 5-17 随机数。熟读随机数模块然后解下面的题: 生成一个有 N 个元素的由随机数 n 组成的列表, 其中 N 和 n 的取值范围分别为: (1 < N <= 100), (0 <= n <= 231 -1)。然后再随机从这个列表中取 N (1 <= N <= 100)个随机数 出来, 对它们排序,然后显示这个子集。 Edit By Vheavens  Edit By Vheavens                                序列:  字符串、列表和元 组  本章主题 z 序列简介 z 字符串 z 列表 z 元组 Edit By Vheavens  Edit By Vheavens                                接下来我们要研究这样一些 Python 的类型,它们的成员有序排列的,并且可以通过下标 偏移量访问到它的一个或者几个成员,这类Python 类型统称为序列,包括下面这些:字符串(普 通字符串和 unicode 字符串),列表,和元组类型。 因为这些类型其实都是由一些成员共同组成的一个序列整体,所以我们把它们统称为序列, 比如说,一个字符串是由一些字符(尽管 Python 并没有显式的定义字符这个类型)组成的序列, 那么“Hello”这个字符串的第一个字符就是“H",第二个字符就是‘e’……,同样的,列表类 型和元组类型就是其他一些 Python 对象所组成的序列。 首先我们来熟悉一下适用于所有序列类型的操作符和内建函数(BIFs), z 简介 z 操作符 z 内建函数 z 内建函数(如果可用) z 特性(如果可用) z 相关模块(如果可用) 在本章的末尾我们会给出一个对于所有序列类型都适用的操作符和函数的参考图表,现 Edit By Vheavens  Edit By Vheavens                                在让我们概略看一下这些内容. N == 序列的长度 == len(sequence) Figure 6-1 有多少可以保存并可以被访问的序列元素 6.1 序列 序列类型有着相同的访问模式:它的每一个元素可以通过指定一个偏移量的方式得到。而 多个元素可以通过切片操作的方式一次得到,切片操作会在接下来的内容中讲到。下标偏移量 是从 0 开始到 总元素数-1 结束 -- 之所以要减一是因为我们是从 0 开始计数的。图 6-1 阐述 了序列的元素是如何存储的。 6.1.1 标准类型操作符 标准类型操作符(参见 4.5 节)一般都能适用于所有的序列类型。当然,如果作复合类型的 对象比较的话,这样说可能需要有所保留,不过其他的操作绝对是完全适用的。 6.1.2 序列类型操作符 表 6.1 列出了对所有序列类型都适用的操作符。操作符是按照优先级从高到底的顺序排列 的。 成员关系操作符 (in, not in) 成员关系操作符使用来判断一个元素是否属于一个序列的。比如对字符串类型来说,就是 判断一个字符是否属于这个字符串,对和元组类型来说,就代表了一个对象是否属于该对象序 列。in/not in 操作符的返回值一般来讲就是True/False,满足成员关系就返回 True,否则返 回 False。该操作符的语法如下: obj [not] in sequence Edit By Vheavens  Edit By Vheavens                                表 6.1 序列类型操作符 序列操作符 作用 seq[ind] 获得下标为 ind 的元素 seq[ind1:ind2] 获得下标从 ind1 到 ind2 间的元素集合 seq * expr 序列重复 expr 次 seq1 + seq2 连接序列 seq1 和 seq2 obj in seq 判断 obj 元素是否包含在 seq 中 obj not in seq 判断 obj 元素是否不包含在 seq 中 连接操作符( + ) 这个操作符允许我们把一个序列和另一个相同类型的序列做连接。语法如下: sequence1 + sequence2 该表达式的结果是一个包含 sequence1 和 sequence2 的内容的新序列.注意,这种方式看 起来似乎实现了把两个序列内容合并的概念,但是这个操作不是最快或者说最有效的。对字符 串来说,这个操作不如把所有的子字符串放到一个列表或可迭代对象中,然后调用一个 join 方法来把所有的内容连接在一起节约内存;类似地,对列表来说,我们推荐读者用列表类型的 extend()方法来把两个或者多个列表对象合并.当你需要简单地把两个对象的内容合并,或者说 不能依赖于可变对象的那些没有返回值(实际上它返回一个 None)的内建方法来完成的时候时, 连接操作符还是很方便的一个选择。下面的切片操作可以视作这些情况的例子。 重复操作符 ( * ) 当你需要需要一个序列的多份拷贝时,重复操作符非常有用,它的语法如下: sequence * copies_int copies_int 必须是一个整数(1.6 节里面有讲到,不能是长整数).像连接操作符一样,该操 作符返回一个新的包含多份原对象拷贝的对象。 切片操作符 ( [], [:], [::] ) 简单地讲,所谓序列类型就是包含一些顺序排列的对象的一个结构.你可以简单的用方括号 加一个下标的方式访问它的每一个元素,或者通过在方括号中用冒号把开始下标和结束下标分 Edit By Vheavens  Edit By Vheavens                                开的方式来访问一组连续的元素.下面我们将详细的讲解提到的这两种方式.序列类型是其元素 被顺序放置的一种数据结构类型,这种方式允许通过指定下标的方式来获得某一个数据元素,或 者通过指定下标范围来获得一组序列的元素.这种访问序列的方式叫做切片,我们通过切片操作 符就可以实现我们上面说到的操作。访问某一个数据元素的语法如下: sequence[index] sequence 是序列的名字,index 是想要访问的元素对应的偏移量.偏移量可以是正值,范围 从 0 到偏移最大值(比序列长度少一),用 len()函数(下一节会讲),可以得到序列长度,实际 的范围是 0 <= inde <= len(sequece)-1 .另外,也可以使用负索引,范围是 -1 到序列的负 长度,-len(sequence), -len(sequence) <= index <= -1.正负索引的区别在于正索引以序列 的开始为起点,负索引以序列的结束为起点.试图访问一个越界的索引会引发一个如下的异常: >>> names = ('Faye', 'Leanna', 'Daylen') >>> print names[4] Traceback (most recent call last): File "", line 1, in ? IndexError: tuple index out of range 因为 Python 是面向对象的,所以你可以像下面这样直接访问一个序列的元素(不用先把它 赋值给一个变量): >>> print ('Faye', 'Leanna', 'Daylen')[1] Leanna 这个特性在你调用一个返回值是序列类型的函数,并且你只对返回的序列中的一个或某几 个元素感兴趣时特别有用. 那么我们如何才能一次得到多个元素呢?其实这跟访问某一个单一元素一样简单,只要简 单的给出开始和结束的索引值,并且用冒号分隔就可以了,其语法如下: sequence[starting_index:ending_index] 通过这种方式我们可以得到从起始索引到结束索引(不包括结束索引对应的元素)之间的 一"片"元素.起始索引和结束索引都是可选的,如果没有提供或者用None 作为索引值,切片操作 会从序列的最开始处开始,或者直到序列的最末尾结束.在图 6-2 和 6-6 里面,我们以一个长度 为 5 的序列为例,分别讲解了这几种切片方式。 Edit By Vheavens  Edit By Vheavens                                图 6–2 Entire sequence: sequence or sequence[:] 图 6-3 序列切片操作: sequence[0:3]或者 sequence[:3] 用步长索引来进行扩展的切片操作 序列的最后一个切片操作是扩展切片操作,它多出来的第三个索引值被用做步长参数。你 可以把这个参数看成跟内建函数 range()里面的步长参数或者类似于 C/C++,Perl,PHP 和 Java 语言里面 for 语句中的步长参数一样来理解。 Python 的虚拟机里面其实很早就有了扩展切片 Edit By Vheavens  Edit By Vheavens                                操作,只不过以前需要通过扩展的方式来使用。Jython 也支持这个语法(以前叫 JPython) 图 6-4 序列切片操作: sequence[2:5] 图 6-5 序列切片操作: sequence[1:3] long before version 2.3 of the C interpreter gave everyone else access to it. 以下是几个例子: >>> s = 'abcdefgh' >>> s[::-1] # 可以视作"翻转"操作 'hgfedcba' Edit By Vheavens  Edit By Vheavens                                >>> s[::2] # 隔一个取一个的操作 'aceg' 图 6-6 序列切片操作: sequence[3] 切片索引的更多内容 切片索引的语法要比简单的单一元素索引灵活的多。开始和结束素引值可以超过字符串的 长度。换句话说,起始索引可以小于 0,而对于结束索引,即使索引值为 100 的元素并不存在也 不会报错,简单地说,即使用 100 来作为一个长度不到 100 的序列的结束索引也不会有什么问 题,例子如下: >>> ('Faye', 'Leanna', 'Daylen')[-100:100] ('Faye', 'Leanna', 'Daylen') 有这么一个问题:有一个字符串,我们想通过一个循环按照这样的形式显示它:每次都把 位于最后的一个字符砍掉,下面是实现这个要求的一种方法: >>> s = 'abcde' >>> i = -1 >>> for i in range(-1, -len(s), -1): ... print s[:i] ... abcd abc Edit By Vheavens  Edit By Vheavens                                ab a 可是,该如何在第一次迭代的时候显示整个字符串呢?是否有一种方法可以不用在整个循 环之前加入一个额外的 print 语句呢?我们该如何定义一个索引,来代表整个的序列呢?事实 上在个以负数作为索引的例子里是没有一个真正能解决这个问题的方法的,因为-1 已经是“最 小”的索引了.我们不可能用 0 来作为索引值,因为这会切片到第一个元素之前而什么都不会显 示: >>> s[:0] '' 我们的方案是使用另一个小技巧:用 None 作为索引值,这样一来就可以满足你的需要,比 如说,在你想用一个变量作为索引来从第一个到遍历最后一个元素的时候: >>> s = 'abcde' >>> for i in [None] + range(-1, -len(s), -1): ... print s[:i] ... abcde abcd abc ab a 现在这个程序符合我们的要求了。在进行下面的内容之前,必须指出,似乎还可以先创建 一个只包含 None 的列表,然后用 extend()函数把 range()的输出添加到这个列表,或者先建立 range()输出组成的列表然后再把 None 插入到这个列表的最前面,然后对这个列表进行遍历, 但是可变对象的内建函数 extend()根本就没有返回值,所以这个方法是行不通的: >>> for i in [None].extend(range(-1, -len(s), -1)): ... print s[:i] ... Traceback (most recent call last): File "", line 1, in ? TypeError: iteration over non-sequence Edit By Vheavens  Edit By Vheavens                                这个错误发生的原因是[None].extend(...)函数返回 None , None 既不是序列类型也不是 可迭代对象. 这种情况下使用上面提到的的列表连接操作来实现是唯一不需要添加额外代码的 方法. 6.1.3 内建函数(BIFs) 在讲解序列类型的内建函数之前,有一点需要说明,序列本身就内含了迭代的概念,之所以 会这样,是因为迭代这个概念就是从序列,迭代器,或者其他支持迭代操作的对象中泛化得来 的。由于 Python 的 for 循环可以遍历所有的可迭代类型,在(非纯序列对象上)执行 for 循环时 就像在一个纯序列对象上执行一样。而且Python 的很多原来只支持序列作为参数的内建函数现 在也开始支持迭代器或者或类迭代器了.我们把这些类型统称为"可迭代对象".在这一章里我们 会详细的讨论跟序列关系紧密的内建函数(BIF). 在第八章"条件判断和循环"里面将讨论针对" 在循环中迭代"这种情况的内建函数(BIF). 类型转换 内建函数 list(),str()和 tuple()被用做在各种序列类型之间转换。你可以把它们理解成 其他语言里面的类型转换,但是并没有进行任何的转换。这些转换实际上是工厂函数(在第 4 章介绍),将对象作为参数,并将其内容(浅)拷贝到新生成的对象中.表6.2 列出了适用于序 列类型的转换函数。 表 6.2 序列类型转换工厂函数 函数 含义 list(iter) 把可迭代对象转换为列表 str(obj) 把obj 对象转换成字符串(对象的字符串表示法) unicode(obj) 把对象转换成 Unicode 字符串(使用默认编码) basestring() 抽象工厂函数,其作用仅仅是为 str 和 unicode 函数提供父类,所以不能被 实例化,也不能被调用(详见第 6.2 节) tuple(iter) 把一个可迭代对象转换成一个元组对象 Edit By Vheavens  Edit By Vheavens                                我们又用了一次“转换”这个词。不过,为什么 Python 里面不简单地把一个对象转换成另 一个对象呢?回过头看一下第 4 章就会知道,一旦一个 Python 的对象被建立,我们就不能更改 其身份或类型了.如果你把一个列表对象传给 list()函数,便会创建这个对象的一个浅拷贝, 然后将其插入新的列表中。同样地,在做连接操作和重复操作时,我们也会这样处理。 所谓浅拷贝就是只拷贝了对对象的索引,而不是重新建立了一个对象!如果你想完全的拷 贝一个对象(包括递归,如果你的对象是一个包含在容器中的容器),你需要用到深拷贝,关于浅 拷贝和深拷贝的更多信息会在本章的末尾讲到。 str()函数在需要把一个对象的可打印信息输出时特别有用,不仅仅是对序列类型,对其他 类型的对象同样如此.Unicode()是 str()函数的 unicode 版本,它跟 str()函数基本一样.list() 和 tuple()函数在列表类型和元组类型的互换时非常有用.不过,虽然这些函数也适用于string 类型(因为 string 类型也是序列的一种),但是在 string 类型上应用 tuple()和 list()函数却得 不到我们通常希望的结果. Operational Python 为序列类型提供以下可操作 BIFs(见表 6.3).注意,len(),reversed()和 sum()函 数只能接受序列类型对象作为参数,而剩下的则还可以接受可迭代对象做为参数,另外,max() 和 min()函数也可以接受一个参数列表. 表 6.3 序列类型可用的内建函数 函数名 功能 enumerate(iter) a 接受一个可迭代对象作为参数,返回一个 enumerate 对象(同 时也是一个迭代器),该对象生成由 iter 每个元素的 index 值 和 item 值组成的元组(PEP 279) len(seq) 返回 seq 的长度 max(iter,key=None) or max(arg0,arg1...,key=None)b 返回 iter 或(arg0,arg1,...)中的最大值,如果指定了key, 这个 key 必须是一个可以传给 sort()方法的,用于比较的回 调函数. min(iter, key=None) or min(arg0, arg1.... key=None)b 返回 iter 里面的最小值;或者返回(arg0,arg2,...)里面 Edit By Vheavens  Edit By Vheavens                                的最小值;如果指定了 key,这个 key 必须是一个可以传给 sort()方法的,用于比较的回调函数. reversed(seq)c 接受一个序列作为参数,返回一个以逆序访问的迭代器(PEP 322) sorted(iter, func=None, key=None, reverse=False)c 接受一个可迭代对象作为参数,返回一个有序的列表;可选参数 func,key 和 reverse 的含义跟 list.sort()内建函数的参数含义一 样. sum(seq, init=0)a 返回 seq 和可选参数 init 的总和,其效果等同于 reduce(operator.add,seq,init) zip([it0, it1,... itN])d 返回一个列表,其第一个元素是 it0,it1,...这些元素的第 一个元素组成的一个元组,第二个...,类推. a. Python2.3 新增 b. 从 Python2.5 开始支持关键字参数 c. Python2.4 开始支持 d. Python2.0 加入,Python2.4 加强 我们将分别在每个序列的章节里面提供使用这些函数的例子. 6.2 字符串 字符串类型是 Python 里面最常见的类型.我们可以简单地通过在引号间包含字符的方式 创建它.Python 里面单引号和双引号的作用是相同的,这一点 Python 不同于其他类 Shell 的脚 本语言,在这些脚本语言中,通常转义字符仅仅在双引号字符串中起作用,在单一号括起的字 符串中不起作用。Python 用"原始字符串"操作符来创建直接量字符串,所以再做区分就没什么 意义了。其他的语言,比如 C 语言里面用单引号来标示字符,双引号标示字符串,而在 Python 里面没有字符这个类型.这可能是双引号和单引号在 Python 里面被视作一样的的另一个原因. 几乎所有的 Python 应用程序都会某种方式用到字符串类型.字符串是一种直接量或者说是一种 标量,这意味着 Python 解释器在处理字符串时是把它作为单一值并且不会包含其他 Python 类型 的。字符串是不可变类型,就是说改变一个字符串的元素需要新建一个新的字符串.字符串是由 独立的字符组成的,并且这些字符可以通过切片操作顺序地访问。 Edit By Vheavens  Edit By Vheavens                                根据在 2.2 章节里面对类型和类的概念进行的统一,Python 实际上有 3 类字符串.通常意 义的字符串(str)和 Unicode 字符串(unicode)实际上都是抽象类 basestring 的子类.这个 basestring 是不能实例化的,如果你试图实例化一个 basestring 类,你会得到以下报错信息: >>> basestring('foo') Traceback (most recent call last): File "", line 1, in TypeError: The basestring type cannot be instantiated 字符串的创建和赋值 创建一个字符串就像使用一个标量一样简单,当然你也可以把 str()作为工厂方法来创建 一个字符串并把它赋值给一个变量: >>> aString = 'Hello World!' # 使用单引号 >>> anotherString = "Python is cool!" # 使用双引号 >>> print aString # print 不带引号的 Hello World! >>> anotherString # 不是进行 print 操作,带有引号 'Python is cool!' >>> s = str(range(4)) # 把一个列表转换成一个字符串 >>> s '[0, 1, 2, 3]' 如何访问字符串的值(字符和子串) Python 里面没有字符这个类型,而是用长度为 1 的字符串来表示这个概念,当然,这其实也 是一个子串。用方括号加一个或者多于一个索引的方式来获得子串: >>> aString = 'Hello World!' >>> aString[0] 'H' >>> aString[1:5] 'ello' >>> aString[6:] 'World!' 如何改变字符串 Edit By Vheavens  Edit By Vheavens                                你可以通过给一个变量赋值(或者重赋值)的方式“更新”一个已有的字符串.新的值可能 与原有值差不多,也可能跟原有串完全不同。 >>> aString = aString[:6] + 'Python!' >>> aString 'Hello Python!' >>> aString = 'different string altogether' >>> aString 'different string altogether' 跟数字类型一样,字符串类型也是不可变的,所以你要改变一个字符串就必须通过创建一 个新串的方式来实现。也就是说你不能只改变一个字符串的一个字符或者一个子串,然而,通 过拼凑一个旧串的各个部分来得到一个新串是被允许的,正如上面你看到的那样. 如何删除字符和字符串 再重复一遍,字符串是不可变的,所以你不能仅仅删除一个字符串里的某个字符,你能做的 是清空一个空字符串,或者是把剔除了不需要的部分后的字符串组合起来形成一个新串。假设 你想要从"Hello World!"里面删除小写的'l' >>> aString = 'Hello World!' >>> aString = aString[:3] + aString[4:] >>> aString 'Helo World!' 通过赋一个空字符串或者使用 del 语句来清空或者删除一个字符串: >>> aString = '' >>> aString '' >>> del aString 在大部分应用程序里,没有必要显式的删除字符串。定义这个字符串的代码最终会结束, 那时 Python 会自动释放这些字符串. 6.3 字符串和操作符 6.3.1 标准类型操作符 Edit By Vheavens  Edit By Vheavens                                在第 4 章里面,我们介绍了一些适用于包括标准类型在内的大部分对象的操作符,在这里 再看一下这些其中的一些操作符是怎样作用于字符串类型的,下面是几个简单的例子: >>> str1 = 'abc' >>> str2 = 'lmn' >>> str3 = 'xyz' >>> str1 < str2 True >>> str2 != str3 True >>> str1 < str3 and str2 == 'xyz' False 在做比较操作的时候,字符串是按照 ASCII 值的大小来比较的. 6.3.2 序列操作符 切片( [ ] 和 [ : ] ) 在早先地 6.1.1 章节里面我们展示了如何访问序列类型的一个或一组元素,接下来我们会 把这些知识应用到字符串类型上,着重考察以下的操作: z 正向索引 z 反向索引 z 默认索引 接下来以字符串'abcd'为例子.表里面分别列出了使用正索引和负索引来定位字符的情况. 可以用长度操作符来确认该字符串的长度是 4: >>> aString = 'abcd' >>> len(aString) 4 Edit By Vheavens  Edit By Vheavens                                正向索引时,索引值开始于0,结束于总长度减 1(因为我们是从 0 开始索引的).本例中最后 一个索引是: final_index = len(aString) - 1 = 4 - 1 = 3 在这个范围内,我们可以访问任意的子串。用一个参数来调用切片操作符结果是一个单一 字符,而使用一个数值范围(用':')作为参数调用切片操作的参数会返回一串连续地字符.再强 调一遍,对任何范围[start:end],我们可以访问到包括start 在内到 end(不包括 end)的所有字 符,换句话说,假设 x 是[start:end]中的一个索引值,那么有: start<= x < end. >>> aString[0] 'a' >>> aString[1:3] 'bc' >>> aString[2:4] 'cd' >>> aString[4] Traceback (innermost last): File "", line 1, in ? IndexError: string index out of range 使用不在允许范围(本例中是 0 到 3)内的索引值会导致错误。上面的 aString[2:4]却并没 有出错,那是因为实际上它返回的是索引值2和3的值。但是直接拿 4 作为索引访问是不被允 许的。 在进行反向索引操作时,是从-1 开始,向字符串的开始方向计数,到字符串长度的负数为 索引的结束。最末一个索引(也就是第一个字符)是这样定位的: final_index = -len(aString) = -4 >>> aString[-1] 'd' >>> aString[-3:-1] 'bc' >>> aString[-4] 'a' Edit By Vheavens  Edit By Vheavens                                如果开始索引或者结束索引没有被指定,则分别以字符串的第一个和最后一个索引值为 默认值。 >>> aString[2:] 'cd' >>> aString[1:] 'bcd' >>> aString[:-1] 'abc' >>> aString[:] 'abcd' 注意:起始/结束索引都没有指定的话会返回整个字符串. 成员操作符(in ,not in) 成员操作符用于判断一个字符或者一个子串(中的字符)是否出现在另一个字符串中。出现 则返回 True,否则返回 False.注意,成员操作符不是用来判断一个字符串是否包含另一个字符 串的,这样的功能由 find()或者 index()(还有它们的兄弟:rfind()和 rindex())函数来完成 下面是一些字符串和成员操作符的例子. 在 Python2.3 以前,in(和 not in)操作符只允许用来判断一个单个字符是否属于一个字 符串,就像下面第二个例子那样.2.3 以后这个限制去掉了,所有的字符串都可以拿来判断. >>> 'bc' in 'abcd' True >>> 'n' in 'abcd' False >>> 'nm' not in 'abcd' True 在例 6.1 里面,我们会用到下面这些 string 模块预定义的字符串: >>> import string >>> string.ascii_uppercase 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' >>> string.ascii_lowercase 'abcdefghijklmnopqrstuvwxyz' >>> string.ascii_letters 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' Edit By Vheavens  Edit By Vheavens                                >>> string.digits '0123456789' 例 6.1 是一个用来检查 Python 有效标识符的小脚本,名字是 idcheck.py.我们知道, Python 标识符必须以字母或下划线开头,后面跟字母,下划线或者数字. 例 6.1 标识符检查 (idcheck.py) 标识符合法性检查,首先要以字母或者下划线开始,后面要跟字母,下划线或者或数字. 这个小例子只检查长度大于等于 2 的标识符. 1 #!usr/bin/env python 2 3 import string 4 5 alphas = string.letters + '_' 6 nums = string.digits 7 8 print 'Welcome to the Identifier Checker v1.0' 9 print 'Testees must be at least 2 chars long.' 10 inp = raw_input('Identifier to test? ') 11 12 if len(myInput) > 1: 13 14 if myInput[0] not in alphas: 15 print '''invalid: first symbol must be 16 alphabetic''' 17 else: 18 for otherChar in myInput[1:]: 19 20 if otherChar not in alphas + nums: 21 print '''invalid: remaining 22 symbols must be alphanumeric''' 23 break Edit By Vheavens  Edit By Vheavens                                24 else: 25 print "okay as an identifier" 这个例子还展示了字符串连接符( + )的使用,本章的后面会讲到字符串连接符.运行几次 后得到下面的输出: $ python idcheck.py Welcome to the Identifier Checker v1.0 Testees must be at least 2 chars long. Identifier to test? counter okay as an identifier $ $ python idcheck.py Welcome to the Identifier Checker v1.0 Testees must be at least 2 chars long. Identifier to test? 3d_effects invalid: first symbol must be alphabetic 让我们逐行解释这个应用程序 。 3-6 行 导入 string 模块并且预定义了两个字符串,用于后面的判断. 8-12 行 输出提示信息,第 12 行的 if 语句过滤掉长度小于 2 的标识符或者候选标识符. 14-16 行 检查第一个符号是不是字母或下划线,如果不是,输出结果并退出。 17-18 行 否则,从第二个字符开始到最后一个字符,循环检查剩余的字符. 20-23 行 检查剩余的符号是否都是字母,下划线或者数字.注意我们是如何使用连接操作符来创建合 法字符集合的。只要发现一个非法字符,就显示结果并通过 break 语句退出。 Edit By Vheavens  Edit By Vheavens                                核心提示: 性能 一般来说,从性能的的角度来考虑,把重复操作作为参数放到循环里面进行是非常低效的. while i < len(myString): print 'character %d is:', myString[i] 上面的循环操作把大把的时间都浪费到了重复计算字符串 myString 的长度上了.每次循环 迭代都要运行一次这个函数.如果把这个值做一次保存,我们就可以用更为高效的方式重写我们 的循环操作. length = len(myString) while i < length: print 'character %d is:', myString[i] 这个方法同样适用于上面的例 6.1 for otherChar in myInput[1:]: if otherChar not in alphas + nums: : 第 18 行的 for 循环包含了一个 if 语句,在这个 if 里面执行了合并两个字符串的操作。 被合并的这两个字符串从始至终就没变过,而每次都会重新进行一次计算.如果先把这两个字 符串存为一个新字符串,我们就可以直接引用这个字符串而不用进行重复计算了。 alphnums = alphas + nums for otherChar in myInput[1:]: if otherChar not in alphnums: : 24-25 行 或许现在就向你展示 for-else 循环语句有点儿早,可是我们必需先看一看这个语句(在第 8 章有详细的介绍).for 循环的 else 语句是一个可选项,它只在for 循环完整的结束,没有遇到 break 时执行。在我们的例子中,如果所有的符号都检查合格,那么我们就得到了一个合法的 标识符,程序会返回一个这样的结果,然后执行完毕。 其实,这段程序并不是完美的,一个问题就是标识符的长度必须大于1.我们的程序几乎是, 但还并没有真正定义出 Python 标识符的范围,Python 标识符长度可以是 1.另一个问题是这段 程序并没有考虑到 Python 的关键字,而这些都是作为保留字,不允许用做标识符的.我们把这两 个问题作为课后练习留给读者(见练习 6-2)。 Edit By Vheavens  Edit By Vheavens                                连接符( + ) 运行时刻字符串连接 我们可以通过连接操作符来从原有字符串获得一个新的字符串.我们已经在前面的例 6-1 里面见识过连接符了,下面是一些更多的例子: >>> 'Spanish' + 'Inquisition' 'SpanishInquisition' >>> >>> 'Spanish' + ' ' + 'Inquisition' 'Spanish Inquisition' >>> >>> s = 'Spanish' + ' ' + 'Inquisition' + ' Made Easy' >>> s 'Spanish Inquisition Made Easy' >>> >>> import string >>> string.upper(s[:3] + s[20]) # archaic (see below) 'SPAM' 最后一个例子展示了用一个字符串 s 的两个切片来构成一个新串的操作,从"Spanish"里 面切出"Spa"加上从"Made"里面切出来的"M".将抽取出来字符串切片连接后作为参数传给了 string.upper()方法,该方法负责把字符串的所有字符都变为大写。String 模块的方法是在 Python1.6 里面添加进来的,所以这个操作也可以用最后一个字符串的一个单一方法调用来完 成(见下面的例子)。现在已经没有必要导入 string 模块了,除非你需要访问该模块自己定义的 字符串常量。注意:虽然对初学者来说string 模块的方式更便于理解,但出于性能方面的考虑, 我们还是建议你不要用 string 模块。原因是 Python 必须为每一个参加连接操作的字符串分配 新的内存,包括新产生的字符串。取而代之,我们推荐你像下面介绍的那样使用字符串格式化 操作符(%),或者把所有的字符串放到一个列表中去,然后用一个 join()方法来把它们连接在 一起。 >>> '%s %s' % ('Spanish', 'Inquisition') Edit By Vheavens  Edit By Vheavens                                'Spanish Inquisition' >>> >>> s = ' '.join(('Spanish', 'Inquisition', 'Made Easy')) >>> s 'Spanish Inquisition Made Easy' >>> >>> # no need to import string to use string.upper(): >>> ('%s%s' % (s[:3], s[20])).upper() 'SPAM' 编译时字符串连接 上面的语法在运行时字符串连接的加法操作,这个用法是非常标准的。Python 中还有一种 并不是经常用到,更像是一种程序员的习惯用法的语法.Python 的语法允许你在源码中把几个 字符串连在一起写,以此来构建新字符串: >>> foo = "Hello" 'world!' >>> foo 'Helloworld!' 通过这种方法,你可以把长的字符串分成几部分来写,而不用加反斜杠。如上所示, 你可以在一行里面混用两种分号。这种写法的好处是你可以把注释也加进来,如下: >>> f = urllib.urlopen('http://' # protocol ... 'localhost' # hostname ... ':8000' # port ... '/cgi-bin/friends2.py') # file 如你所想,下面就是 urlopen()方法所得到的真实输入: >>> 'http://' 'localhost' ':8000' '/cgi-bin/friends2.py' 'http://localhost:8000/cgi-bin/friends2.py' 普通字符串转化为 Unicode 字符串 如果把一个普通字符串和一个 Unicode 字符串做连接处理,Python 会在连接操作前先把普 通字符串转化为 Unicode 字符串: Edit By Vheavens  Edit By Vheavens                                >>> 'Hello' + u' ' + 'World' + u'!' u'Hello World!' 重复操作符( * ) 重复操作符创建一个包含了原有字符串的多个拷贝的新串: >>> 'Ni!' * 3 'Ni!Ni!Ni!' >>> >>> '*'*40 '****************************************' >>> >>> print '-' * 20, 'Hello World!', '-' * 20 -------------------- Hello World! -------------------- >>> who = 'knights' >>> who * 2 'knightsknights' >>> who 'knights' 像其他的标准操作符一样,原变量是不被修改的,就像上面最后一个例子所示。 6.4 只适用于字符串的操作符 6.4.1 格式化操作符( % ) Python 风格的字符串格式化操作符。只适用于字符串类型,非常类似于 C 语言里面的 printf()函数的字符串格式化,甚至所用的符号都一样,都用百分号(%),并且支持所有printf() 式的格式化操作.语法如下: 左边的 format_string 里面同通常会在 printf()函数的第一个参数里面见到的一样:包含% Edit By Vheavens  Edit By Vheavens                                的格式化字符串.表 6.4 列出了可用的各种符号.arguments_to_convert 参数是你要转化、显示 的变量,对应于你送给 prinf 的其他参数. 表 6.4 字符串格式化符号 格式化字符 转换方式 %c 转换成字符(ASCII 码值,或者长度为一的字符串) %ra 优先用 repr()函数进行字符串转换 %s 优先用 str()函数进行字符串转换 %d / %i 转成有符号十进制数 %ub 转成无符号十进制数 %ob 转成无符号八进制数 %xb/%Xb (Unsigned)转成无符号十六进制数(x/X 代表转换后的十六进制字符的大 小写) %e/%E 转成科学计数法(e/E 控制输出 e/E) %f/%F 转成浮点数(小数部分自然截断) %g/%G %e 和%f/%E 和%F 的简写 %% 输出% a. Python2.0 新增;而且好像只有 Python 里面有. b. Python2.4 里面%u/%o/%x/%X 在遇到负数的时候会返回一个有符号字符串 Python 支持两种格式的输入参数。第一种是元组(见 2.8 节,6.15 节),这基本上是一种的 C printf()风格的转换参数集; Python 支持的第二种形式是字典形式(详见第 7 章).字典其实是一个哈希键-值对的集合。 这种形式里面,key 是作为格式字符串出现,相对应的 value 值作为参数在进行转化时提 供给格式字符串. 格式字符串既可以跟 print 语句一起用来向终端用户输出数据,又可以用来合并字符串形 成新字符串,而且还可以直接显示到 GUI(Graphical User Interface)界面上去. 其他的格式字符和方法见表 6.5 Edit By Vheavens  Edit By Vheavens                                表 6.5 格式化操作符辅助指令 符号 作用 * 定义宽度或者小数点精度 - 用做左对齐 + 在正数前面显示加号( + ) 在正数前面显示空格 # 在八进制数前面显示零('0'),在十六进制前面显示'0x'或者'0X'(取决于 用的是'x'还是'X') 0 显示的数字前面填充‘0’而不是默认的空格 % '%%'输出一个单一的'%' (var) 映射变量(字典参数) m.n m 是显示的最小总宽度,n 是小数点后的位数(如果可用的话) 以下是一些使用格式字符串的例子: 十六进制输出: >>> "%x" % 108 '6c' >>> >>> "%X" % 108 '6C' >>> >>> "%#X" % 108 '0X6C' >>> >>> "%#x" % 108 '0x6c' 浮点数和科学记数法形式输出: >>> >>> '%f' % 1234.567890 Edit By Vheavens  Edit By Vheavens                                '1234.567890' >>> >>> '%.2f' % 1234.567890 '1234.57' >>> >>> '%E' % 1234.567890 '1.234568E+03' >>> >>> '%e' % 1234.567890 '1.234568e+03' >>> >>> '%g' % 1234.567890 '1234.57' >>> >>> '%G' % 1234.567890 '1234.57' >>> >>> "%e" % (1111111111111111111111L) '1.111111e+21' 整数和字符串输出: >>> "%+d" % 4 '+4' >>> >>> "%+d" % -4 '-4' >>> >>> "we are at %d%%" % 100 'we are at 100%' >>> >>> 'Your host is: %s' % 'earth' 'Your host is: earth' >>> Edit By Vheavens  Edit By Vheavens                                >>> 'Host: %s\tPort: %d' % ('mars', 80) 'Host: mars Port: 80' >>> >>> num = 123 >>> 'dec: %d/oct: %#o/hex: %#X' % (num, num, num) 'dec: 123/oct: 0173/hex: 0X7B' >>> >>> "MM/DD/YY = %02d/%02d/%d" % (2, 15, 67) 'MM/DD/YY = 02/15/67' >>> >>> w, p = 'Web', 'page' >>> 'http://xxx.yyy.zzz/%s/%s.html' % (w, p) 'http://xxx.yyy.zzz/Web/page.html' 上面的例子都是使用的元组类型的参数作转换.下面我们将把字典类型的参数提供给格式 化操作符. >>> 'There are %(howmany)d %(lang)s Quotation Symbols' % \ ... {'lang': 'Python', 'howmany': 3} 'There are 3 Python Quotation Symbols' 令人称奇的调试工具 字符串格式化操作符不仅很酷,易用,上手快,而且是一个非常有用的调试工具。事实上, 所有的 Python 对象都有一个字符串表示形式 (通过 repr()函数,'' 或 str()函数来展现).print语句自动为每个对象调用 str()函数. 更好的是,在定义自己的对象时,你可以利用"钩子"为你的对象创建字符串表达形式. 这样, repr(),str()或`` 或者 print 被调用时,就可以获得一个适当的字符串描述信息.即使在坏 的不能再坏的情况下,repr()或者 str()也不能显示一个对象的信息时,Pythonic 方式的默认 做法最起码能给你返回想如下格式的信息: <... something that is useful ...>. Edit By Vheavens  Edit By Vheavens                                6.4.2 字符串模板: 更简单的替代品 字符串格式化操作符是 Python 里面处理这类问题的主要手段,而且以后也是如此。然而它 也不是完美的,其中的一个缺点是它不是那么直观,尤其对刚从 C/C++转过来的 Python 新手来 说更是如此,即使是现在使用字典形式转换的程序员也会偶尔出现遗漏转换类型符号的错误, 比如说,用了%(lang)而不是正确的%(lang)s.为了保证字符串被正确的转换,程序员必须明确 的记住转换类型参数,比如到底是要转成字符串,整数还是其他什么类型. 新式的字符串模板的优势是不用去记住所有的相关细节的,而是像现在 shell 风格的脚本 语言里面那样使用美元符号($). 由于新式的字符串 Template 对象的引进使得 string 模块又重新活了过来,Template 对象 有两个方法,substitute()和 safe_substitute().前者更为严谨,在 key 缺少的情况下它会报一 个 KeyError 的异常出来,而后者在缺少 key 时,直接原封不动的把字符串显示出来. >>> from string import Template >>> s = Template('There are ${howmany} ${lang} Quotation Symbols') >>> >>> print s.substitute(lang='Python', howmany=3) There are 3 Python Quotation Symbols >>> >>> print s.substitute(lang='Python') Traceback (most recent call last): File "", line 1, in ? File "/usr/local/lib/python2.4/string.py", line 172, in substitute return self.pattern.sub(convert, self.template) File "/usr/local/lib/python2.4/string.py", line 162, in convert val = mapping[named] KeyError: 'howmany' >>> >>> print s.safe_substitute(lang='Python') There are ${howmany} Python Quotation Symbols 新式的字符串模板是从 Python2.4 开始加入的,更多信息请查阅 Python 类库手册和 PEP Edit By Vheavens  Edit By Vheavens                                292. 6.4.3 原始字符串操作符( r/R ) 关于原始字符串的目的,在 Python1.5 里面已经有说明,是为了对付那些在字符串中出现 的特殊字符(下面的小节会介绍这些特殊字符)。在原始字符串里,所有的字符都是直接按照字 面的意思来使用,没有转义特殊或不能打印的字符。 原始字符串的这个特性让一些工作变得非常的方便,比如正则表达式的创建(详见文档的 re 模块).正则表达式是一些定义了高级搜索匹配方式的字符串,通常是由代表字符,分组、匹配信 息、变量名、和字符类等的特殊符号组成。正则表达式模块已经包含了足够用的符号。但当你 必须插入额外的符号来使特殊字符表现的像普通字符的时候,你就陷入了“字符数字”的泥潭! 这时原始字符串就会派上用场了. 除了原始字符串符号(引号前面的字母"r")以外,原始字符串跟普通字符串有着几乎完全相 同的语法. 这个'r'可以是小写也可以是大写,唯一的要求是必须紧靠在第一个引号前. 在三个例子的第一个例子里面,我们需要一个反斜杠加一个'n'来而不是一个换行符.: >>> '\n' '\n' >>> print '\n' >>> r'\n' '\\n' >>> print r'\n' \n 接下来的例子里,我们打不开我们的 README 文件了,为什么?因为'\t'和'\r'被当成 不在我们的文件名中的特殊符号,但它们实际上文件路径的中 4 个独立的字符. >>> f = open('C:\windows\temp\readme.txt', 'r') Traceback (most recent call last): Edit By Vheavens  Edit By Vheavens                                File "", line 1, in ? f = open('C:\windows\temp\readme.txt', 'r') IOError: [Errno 2] No such file or directory: 'C:\\win- dows\\temp\readme.txt' >>> f = open(r'C:\windows\temp\readme.txt', 'r') >>> f.readline() 'Table of Contents (please check timestamps for last update!)\n' >>> f.close() 最后我们要找一对原始的\n 字符而不是换行。为了找到它,我们使用了一个简单的正则表 达式,它的作用是查找通常被用来表示空白字符的反斜线-字符对(backslash-character pairs)。 >>> import re >>> m = re.search('\\[rtfvn]', r'Hello World!\n') >>> if m is not None: m.group() ... >>> m = re.search(r'\\[rtfvn]', r'Hello World!\n') >>> if m is not None: m.group() ... '\\n' 6.4.4 Unicode 字符串操作符( u/U ) Unocide 字符串操作符,大写的(U)和小写的(u)是在 Python1.6 中 和 Unicode 字符串一 起被引入的. 它用来把标准字符串或者是包含 Unicode 字符的字符串转换成完全地 Unicode 字 符串对象。关于 Unicode 字符串的进一步信息在 6.7.4 节有详细介绍.另外,字符串方法(见 6.6 节)和正则表达式引擎也支持 Unicode.下面是几个例子: u'abc' U+0061 U+0062 U+0063 u'\u1234' U+1234 u'abc\u1234\n' U+0061 U+0062 U+0063 U+1234 U+0012 Unicode 操作符也可以接受原始 Unicode 字符串, 只要我们将 Unicode 操作符和前面讨论 Edit By Vheavens  Edit By Vheavens                                过的原始字符串操作符连接在一起就可以了. 注意:Unicode 操作符必须出现在原始字符串操作 符前面. ur'Hello\nWorld!' 6.5 内建函数 6.5.1 标准类型函数 cmp() 同比较操作符一样,内建的 cmp()函数也根据字符串的 ASCII 码值进行比较. >>> str1 = 'abc' >>> str2 = 'lmn' >>> str3 = 'xyz' >>> cmp(str1, str2) -11 >>> cmp(str3, str1) 23 >>> cmp(str2, 'lmn') 0 6.5.2 序列类型函数 len() >>> str1 = 'abc' >>> len(str1) 3 >>> len('Hello World!') 12 正如您期望的那样,内建函数 len()返回字符串的字符数. max() and min() >>> str2 = 'lmn' Edit By Vheavens  Edit By Vheavens                                >>> str3 = 'xyz' >>> max(str2) 'n' >>> min(str3) 'x' 虽然 max()和 min()函数对其他的序列类型可能更有用,但对于 string 类型它们能很好地 运行,返回最大或者最小的字符,(按照 ASCII 码值排列),下面是几个例子: >>> min('ab12cd') '1' >>> min('AB12CD') '1' >>> min('ABabCDcd') 'A' enumerate() >>> s = 'foobar' >>> for i, t in enumerate(s): ... print i, t ... 0 f 1 o 2 o 3 b 4 a 5 r zip() >>> s, t = 'foa', 'obr' >>> zip(s, t) [('f', 'o'), ('o', 'b'), ('a', 'r')] Edit By Vheavens  Edit By Vheavens                                6.5.3 字符串类型函数 raw_input() 内建的 raw_input()函数使用给定字符串提示用户输入并将这个输入返回,下面是一个使 用 raw_input()的例子: >>> user_input = raw_input("Enter your name: ") Enter your name: John Doe >>> >>> user_input 'John Doe' >>> >>> len(user_input) 8 Python 里面没有 C 风格的结束字符 NUL,你输入多少个字符,len()函数的返回值就是多少. str() and unicode() str()和 unicode()函数都是工厂函数,就是说产生所对应的类型的对象.它们接受一个任 意类型的对象,然后创建该对象的可打印的或者Unicode 的字符串表示. 它们和 basestring 都 可以作为参数传给 isinstance()函数来判断一个对象的类型. >>> isinstance(u'\0xAB', str) False >>> not isinstance('foo', unicode) True >>> isinstance(u'', basestring) True >>> not isinstance('foo', basestring) False chr(), unichr(), and ord() chr()函数用一个范围在 range(256)内的(就是0到255)整数做参数,返回一个对应的字 Edit By Vheavens  Edit By Vheavens                                符.unichr()跟它一样,只不过返回的是 Unicode 字符,这个从 Python2.0 才加入的 unichr() 的参数范围依赖于你的 Python 是如何被编译的.如果是配置为 USC2 的 Unicode,那么它的允许 范 围 就 是 range(65536)或者说 0x0000-0xFFFF;如果配置为 UCS4 ,那么这个值应该是 range(1114112)或者 0x000000-0x110000.如果提供的参数不在允许的范围内,则会报一个 ValueError 的异常。 ord()函数是 chr()函数(对于 8 位的 ASCII 字符串)或 unichr()函数(对于 Unicode 对象) 的配对函数,它以一个字符(长度为 1 的字符串)作为参数,返回对应的 ASCII 数值,或者 Unicode 数值,如果所给的 Unicode 字符超出了你的 Python 定义范围,则会引发一个 TypeError 的异常。 >>> chr(65) 'A' >>> ord('a') 97 >>> unichr(12345) u'\u3039' >>> chr(12345) Traceback (most recent call last): File "", line 1, in ? chr(12345) ValueError: chr() arg not in range(256) >>> ord(u'\ufffff') Traceback (most recent call last): File "", line 1, in ? ord(u'\ufffff') TypeError: ord() expected a character, but string of length 2 found >>> ord(u'\u2345') 9029 6.6 字符串内建函数 字符串方法是从 Python1.6 到 2.0 慢慢加进来的--它们也被加到了 Jython 中.这些方法实 现了 string 模块中的大部分方法,表 6.6 列出了目前字符串内建支持的方法,所有这些方法都 包含了对 Unicode 的支持,有一些甚至是专门用于 Unicode 的. 表 6.6 字符串类型内建方法 Edit By Vheavens  Edit By Vheavens                                方法 描述 string.capitalize() 把字符串的第一个字符大写 string.center(width) 返回一个原字符串居中,并使用空格填充至长度 width 的新字符 串 string.count(str, beg=0, end=len(string)) 返回 str 在 string 里面出现的次数,如果 beg 或者 end 指定则 返回指定范围内 str 出现的次数 string.decode(encoding='UTF-8', errors='strict') 以 encoding 指定的编码格式解码 string,如果出错默认报一个 ValueError 的 异 常 , 除 非 errors 指 定 的 是 'ignore' 或 者 'replace' string.encode(encoding='UTF-8', errors='strict')a 以 encoding 指定的编码格式编码 string,如果出错默认报一个 ValueError 的异常,除非 errors 指定的是'ignore'或者'replace' string.endswith(obj, beg=0, end=len(string))b,e 检查字符串是否以 obj 结束,如果 beg 或者 end 指定则检查指 定的范围内是否以 obj 结束,如果是,返回 True,否则返回 False. string.expandtabs(tabsize=8)把字符串 string 中的 tab 符号转为空格,默认的空 格数 tabsize 是 8. string.find(str, beg=0, end=len(string)) 检测 str 是否包含在 string 中,如果 beg 和 end 指定范围, 则检查是否包含在指定范围内,如果是返回开始的索引值,否则 返回-1 string.index(str, beg=0, end=len(string)) 跟 find()方法一样,只不过如果 str 不在 string 中会报一个异常. string.isalnum()a, b, c R 如果 string 至少有一个字符并且所有字符都是字母或数字则返 回 True,否则返回 False string.isalpha()a, b, c 如果 string 至少有一个字符并且所有字符都是字母则返回 True, 否则返回 False string.isdecimal()b, c, d 如果 string 只包含十进制数字则返回 True 否则返回 False. string.isdigit()b, c 如果 string 只包含数字则返回 True 否则返回 False. string.islower()b, c 如果 string 中包含至少一个区分大小写的字符,并且所有这些(区分 大小写的)字符都是小写,则返回 True,否则返回 False Edit By Vheavens  Edit By Vheavens                                string.isnumeric()b, c, d 如果 string 中只包含数字字符,则返回 True,否则返回 False string.isspace()b, c 如果 string 中只包含空格,则返回 True,否则返回 False. string.istitle()b, c 如果 string 是标题化的(见 title())则返回 True,否则返回 False string.isupper()b, c 如果 string 中包含至少一个区分大小写的字符,并且所有这些(区分 大小写的)字符都是大写,则返回 True,否则返回 False string.join(seq) Merges (concatenates)以string 作为分隔符,将 seq 中所有的元素 (的字符串表示)合并为一个新的字符串 string.ljust(width)返回一个原字符串左对齐,并使用空格填充至长度 width 的新字符串 string.lower() 转换 string 中所有大写字符为小写. string.lstrip() 截掉string 左边的空格 string.partition(str)e 有点像 find()和 split()的结合体,从 str 出现的第一个位置起, 把 字 符 串 string 分 成 一 个 3 元 素 的 元 组 (string_pre_str,str,string_post_str),如果 string 中不包含 str 则 string_pre_str == string. string.replace(str1, str2, num=string.count(str1))把 string 中的 str1 替换成 str2,如果 num 指定, 则替换不超过 num 次. string.rfind(str, beg=0,end=len(string))类似于 find()函数,不过是从右边开始查 找. string.rindex( str, beg=0,end=len(string)) 类似于 index(),不过是从右边开始. string.rjust(width)返回一个原字符串右对齐,并使用空格填充至长度 width 的新字符串 string.rpartition(str)e 类似于 partition()函数,不过是从右边开始查找. string.rstrip() 删除string 字符串末尾的空格. string.split(str="", num=string.count(str)) 以str 为分隔符切片 string,如果 num 有指定值,则仅分隔 num 个子字符串 string.splitlines(num=string.count('\n'))b, c 按照行分隔,返回一个包含各行作为元素 的列表,如果 num 指定则仅切片 num 个 行. string.startswith(obj, beg=0,end=len(string))b, e 检查字符串是否是以 obj 开头,是则 返回 True,否则返回 False。如果 beg 和 end 指定值,则在指定范围内 Edit By Vheavens  Edit By Vheavens                                检查. string.strip([obj]) 在 string 上执行 lstrip()和 rstrip() string.swapcase() 翻转 string 中的大小写 string.title()b, c 返回"标题化"的 string,就是说所有单词都是以大写开始,其余 字母均为小写(见 istitle()) string.translate(str, del="") 根据str 给出的表(包含 256 个字符)转换 string 的字符, 要过滤掉的字符放到 del 参数中 string.upper() 转换 string 中的小写字母为大写 string.zfill(width) 返回长度为 width 的字符串,原字符串string 右对齐,前面填充 0 a.Python1.6 中只适用于 Unicode 字符串,2.0 中适用于所有字符串 b. 1.5.2 版本中 string 模块没有该方法 c. 在 Jython2.1 有 d. 仅对 Unicode 字符串有效, e.Python2.5 或者以上版本 几个使用字符串方法的例子: >>> quest = 'what is your favorite color?' >>> quest.capitalize() 'What is your favorite color?' >>> >>> quest.center(40) ' what is your favorite color? ' >>> >>> quest.count('or') 2 >>> >>> quest.endswith('blue') False >>> >>> quest.endswith('color?') True >>> >>> quest.find('or', 30) -1 Edit By Vheavens  Edit By Vheavens                                >>> >>> quest.find('or', 22) 25 >> >>> quest.index('or', 10) 16 >>> >>> ':'.join(quest.split()) 'what:is:your:favorite:color?' >>> quest.replace('favorite color', 'quest') >>> 'what is your quest?' >>> >>> quest.upper() 'WHAT IS YOUR FAVORITE COLOR?' 上面最复杂的例子是有 split()和 join()函数的那个.首先我们在 string 上调用 split() 函数,没有用参数,也就是说以空格作为分隔符分隔字符串,然后我们以这个包含单词的列表 做参数调用 join()方法把这些单词用一个新的分隔符冒号重新串在一起,注意,我们首先用 split()函数把 string 切片成一个列表,然后我们在字符串':'上应用 join()方法把这个列表 重新连接成一个字符串. 6.7 字符串的独特特性 6.7.1 特殊字符串和控制字符 像其他高级语言和脚本语言一样,一个反斜线加一个单一字符可以表示一个特殊字符,通常 是一个不可打印的字符,这就是我们上面讨论的特殊字符,如果这些特殊字符是包含在一个原 始字符串中的,那么它就失去了转义的功能. 除了通常用的特殊字符,比如换行符(\n),tab 符(\t)之外,也可以直接用 ASCII 码值来标 示特殊字符:\000 或者\xXX,分别对应字符的八进制和十六进制 ASCII 码值,下面分别是十进 制,八进制和十六进制的 0,65,和 255: ASCII ASCII ASCII Decimal 0 65 255 Edit By Vheavens  Edit By Vheavens                                Octal \000 \101 \177 Hexadecimal \x00 \x41 \xFF 特殊字符,包括反斜杠转义的那些都可以像普通字符一样存储到 Python 的字符串中. 跟 C 字符串的另一个不同之处是 Python 的字符串并不是以 NUL(\000)作为结束符的.NUL 跟 其他的反斜杠转义字符没什么两样.事实上,一个字符串中不仅可以出现 NUL 字符,而且还可以 出现不止一次,在字符串的任意位置都可以。表 6.7 列出了被大部分 Python 版本支持的转义字 符. 如上所述,就像使用连字符来让一行的内容持续到下一行一样,可以用显式定义八进制 或者十六进制的 ASCII 码的方式定义特殊字符,合法的 ASCII 码值范围是从 0 到 255(八进制的 是 0177,十六进制是 0XFF). Table 6.7 反斜杠开头的转义字符 /X 八进制 十进制 十六进制 字符 说明 \0 000 0 0x00 NUL 空字符 Nul \a 007 7 0x07 BEL 响铃字符 \b 010 8 0x08 BS 退格 \t 011 9 0x09 HT 横向制表符 \n 012 10 0x0A LF 换行 \v 013 11 0x0B VT 纵向制表符 \f 014 12 0x0C FF 换页 \r 015 13 0x0D CR 回车 \e 033 27 0x1B ESC 转义 \" 042 34 0x22 " 双引号 \' 047 39 0x27 ' 单引号 \\ 134 92 0x5C \ 反斜杠 \OOO 八进制值(范围是 000 到 0177) \xXX x 打头的十六进制值(范围是 0x00 到 0xFF) \ 连字符,将本行和下一行的内容连接起来. Edit By Vheavens  Edit By Vheavens                                控制字符的一个作用是用做字符串里面的定界符,在数据库或者web 应用中,大多数的可 打印字符都是被允许用在数据项里面的,就是说可打印的字符不适合做定界符. 用可打印的字符串比如冒号(:)来作定界符,将会很难分辨一个字符到底是数据还是定 界符.而且还会限定你能用在数据项里面的字符数量,而这不是你想要的. 一个通常的解决方案是,使用那些不经常使用的,不可打印的 ASCII 码值来作为定界符, 它们是非常完美的定界符,这样一来诸如冒号这样的可打印字符就可以解脱出来用在数据项中 了. 6.7.2 三引号 虽然你可以用单引号或者双引号来定义字符串,但是如果你需要包含诸如换行符这样的特 殊字符时,单引号或者双引号就不是那么方便了。Python 的三引号就是为了解决这个问题的, 它允许一个字符串跨多行,字符串中可以包含换行符、制表符以及其他特殊字符. 三引号的语法是一对连续的单引号或者双引号(通常都是成对的用): >>> hi = '''hi there''' >>> hi # repr() 'hi\nthere' >>> print hi # str() hi there 三引号让程序员从引号和特殊字符串的泥潭里面解脱出来,自始至终保持一小块字符串的 格式是所谓的 WYSIWYG(所见即所得)格式的。 一个典型的用例是,当你需要一块 HTML 或者 SQL 时,这时用字符串组合,特殊字符串转义 将会非常的繁琐. errHTML = ''' Friends CGI Demo

ERROR

%s

Edit By Vheavens  Edit By Vheavens                                ''' cursor.execute(''' CREATE TABLE users ( login VARCHAR(8), uid INTEGER, prid INTEGER) ''') 6.7.3 字符串不变性 在第 4.7.2 节里面,我们讨论了字符串是一种不可变数据类型,就是说它的值是不能被改 变或修改的。这就意味着如果你想修改一个字符串,或者截取一个子串,或者在字符串的末尾 连接另一个字符串等等,你必须新建一个字符串。 这听起来要比实际情况复杂.因为 Python 替你管理内存,你根本不需要知道到底发生了什 么,每次你修改一个字符串或者做一些改变字符串内容的操作时,Python 都会自动为你分配 一个新串.在下面的例子里面,Python 分别为"abc"和"def"分配了空间,当进行连接操作时, Python 自动为新的字符串"abcdef"分配了空间. >>> 'abc' + 'def' 'abcdef' 给变量赋值是没什么不同: >>> s = 'abc' >>> s = s + 'def' >>> s 'abcdef' 上面的例子里,看起来是我们先把"abc"赋给了 s,然后在 s 的末尾添加了"def".这样看起 来字符串似乎是可变的,其实事实是在"s+'def""这个操作进行的时候,新建了一个新字符串, 然后这个新的对象被赋给了 s,原来的字符串'abc'被析构掉了. Edit By Vheavens  Edit By Vheavens                                我们可以用 id()函数来更明显的显示出来到底发生了什么.复习一下,id()函数返回一个 对象的身份,这个概念有点类似于"内存地址"。 >> s = 'abc' >>> >>> id(s) 135060856 >>> >>> s += 'def' >>> id(s) 135057968 注意修改前后的身份是不同的.另一个测试是针对字符串的一个字符或者一个子串所做的 修改.我们现在将展示对字符串的一个字符或者一片字符的改动都是不被允许的: >>> s 'abcdef' >>> >>> s[2] = 'C' Traceback (innermost last): File "", line 1, in ? AttributeError: __setitem__ >>> >>> s[3:6] = 'DEF' Traceback (innermost last): File "", line 1, in ? AttributeError: __setslice__ 两个操作都抛出了异常.为了实现要求,我们需要用现有字符串的子串来构建一个新串,然 后把这个新串赋给原来的变量: >>> s 'abcdef' >>> >>> s = '%sC%s' % (s[0:2], s[3:]) >>> s Edit By Vheavens  Edit By Vheavens                                'abCdef' >>> >>> s[0:3] + 'DEF' 'abCDEF' 对像字符串这样的不可变对象,我们探究了它在赋值操作中所为左值的限制,左值必须是一 个完整的对象,比如说一个字符串对象,不能是字符串的一部分.对赋值操作的右值没有这个限 制. 6.8 Unicode 从 Python1.6 起引进的 Unicode 字符串支持,是用来在多种双字节字符的格式、编码进行转 换的,其中包括一些对这类字符串的操作管理功能。内建的字符串和正则表达式对Unicode 字符 串的支持,再加上 string 模块的辅助,Python 已经可以应付大部分应用对 Unicode 的存储、 访问、操作的需要了。我们会尽最大的努力把 Python 对 Unicode 的支持说清楚,但在这之前, 让我们先讨论一些基本的术语,然后问一下自己,到底什么是 Unicode? 6.8.1 术语 Table 6.8 Unicode 术语 名词 意思 ASCII 美国标准信息交换码 BMP 基本多文种平面(第零平面) BOM 字节顺序标记(标识字节顺序的字符) CJK/CJKV 中文-日文-韩文(和越南语)的缩写 Code point 类似于 ASCII 值,代表 Unicode 字符的值,范围在 range(1114112)或者说 0x000000 到 0x10FFFF. Octet 八位二进制数的位组 UCS 通用字符集 UCS2 UCS 的双字节编码方式(见 UTF-16) UCS4 UCS 的四字节编码方式. UTF Unicode 或者 UCS 的转换格式. UTF-8 八位 UTF 转换格式(无符号字节序列, 长度为一到四个字节) Edit By Vheavens  Edit By Vheavens                                UTF-16 16 位 UTF 转换格式(无符号字节序列,通常是 16 位长[两个字节],见 UCS2) 6.8.2 什么是 Unicode? Unicode 是计算机可以支持这个星球上多种语言的秘密武器.在 Unicode 之前,用的都是 ASCII,ASCII 码非常简单,每个英文字符都是以七位二进制数的方式存贮在计算机内,其范围 是 32 到 126.当用户在文件中键入一个大写字符 A 时,计算机会把 A 的 ASCII 码值 65 写入磁盘,然后当计算机读取该文件时,它会首先把 65 转化成字符 A 然后显示到屏幕上. ASCII 编码的文件小巧易读。一个程序只需简单地把文件的每个字节读出来,把对应的数 值转换成字符显示出来就可以了.但是 ASCII 字符只能表示 95 个可打印字符.后来的软件厂商把 ASCII 码扩展到了 8 位,这样一来它就可以多标识 128 个字符,可是 223 个字符对需要成千上万 的字符的非欧洲语系的语言来说仍然太少 Unicode 通过使用一个或多个字节来表示一个字符的方法突破了 ASCII 的限制. 在这样机 制下, Unicode 可以表示超过 90,000 个字符. 6.8.3 你是怎么用 Unicode 的? 早先,Python 只能处理 8 位的 ASCII 值,字符串就是简单的数据类型,为了处理一个字符 串,用户必须首先创建一个字符串,然后把它作为参数传给string 模块的一个函数来处理.2000 年,Python1.6(和 2.0)版释出,Unicode 第一次在 Python 里面得到了支持. 为了让 Unicode 和 ASCII 码值的字符串看起来尽可能的相像,Python 的字符串从原来的简 单数据类型改成了真正的对象.ASCII 字符串成了 StringType,而 Unicode 字符串成了 UnicodeType 类型.它们的行为是非常相近的.string 模块里面都有相应的处理函数.string 模 块已经停止了更新,只保留了 ASCII 码的支持,string 模块已经不推荐使用,在任何需要跟 Unicode 兼容的代码里都不要再用该模块,Python 保留该模块仅仅是为了向后兼容。 Python 里面处理 Unicode 字符串跟处理 ASCII 字符串没什么两样.Python 把硬编码的字符 串叫做字面上的字符串,默认所有字面上的字符串都用 ASCII 编码,可以通过在字符串前面加一 个'u'前缀的方式声明 Unicode 字符串,这个'u'前缀告诉Python 后面的字符串要编码成 Unicode 字符串 . Edit By Vheavens  Edit By Vheavens                                >>> "Hello World" # ASCII string >>> u"Hello World" # Unicode string 内建的 str()函数和 chr()函数并没有升级成可以处理 Unicode.它们只能处理常规的 ASCII 编码字符串,如果一个 Unicode 字符串被作作为参数传给了 str()函数,它会首先被转换 成 ASCII 字符串然后在交给 str()函数.如果该 Unicode 字符串中包含任何不被 ASCII 字符串支 持的字符,会导致 str()函数报异常.同样地,chr()函数只能以 0 到 255 作为参数工作.如果你 传给它一个超出此范围的值(比如说一个 Unicode 字符),它会报异常. 新的内建函数 unicode()和 unichar()可以看成 Unicode 版本的 str()和 chr().Unicode() 函数可以把任何 Python 的数据类型转换成一个 Unicode 字符串,如果是对象,并且该对象定义 了__unicode__()方法,它还可以把该对象转换成相应的 Unicode 字符串.具体内容见 6.1.3 和 6.5.3 章节. 6.8.4 Codecs 是什么? codec 是 COder/DECoder 的首字母组合.它定义了文本跟二进制值的转换方式,跟 ASCII 那 种用一个字节把字符转换成数字的方式不同,Unicode 用的是多字节.这导致了 Unicode 支持多 种不同的编码方式.比如说 codec 支持的四种耳熟能详的编码方式是 :ASCII,ISO 8859-1/Latin-1,UTF-8 和 UTF-16. 中最著名的是 UTF-8 编码,它也用一个字节来编码 ASCII 字符,这让那些必须同时处理ASCII 码和 Unicode 码文本的程序员的工作变得非常轻松,因为 ASCII 字符的 UTF-8 编码跟 ASCII 编 码完全相同。 UTF-8 编码可以用 1 个到 4 个字节来表示其他语言的字符,CJK/East 这样的东亚文字一般都 是用 3 个字节来表示,那些少用的、特殊的、或者历史遗留的字符用 4 个字节来表示.这给那些 需要直接处理 Unicode 数据的程序员带来了麻烦,因为他们没有办法按照固定长度逐一读出各 个字符.幸运的是我们不需要掌握直接读写 Unicode 数据的方法,Python 已经替我们完成了相 关细节,我们无须为处理多字节字符的复杂问题而担心.Python 里面的其他编码不是很常用, 事实上,我们认为大部分的 Python 程序员根本就用不着去处理其他的编码,UTF-16 可能是个 例外. UTF-16 可能是以后大行其道的一种编码格式,它容易读写,因为它把所有的字符都是用单 Edit By Vheavens  Edit By Vheavens                                独的一个 16 位字,两个字节来存储的,正因为此,这两个字节的顺序需要定义一下,一般的 UTF-16 编码文件都需要一个 BOM(Byte Order Mark),或者你显式地定义 UTF-16-LE(小端)或 者 UTF-16-BE(大端)字节序. 从技术上讲,UTF-16 也是一种变长编码,但它不是很常用(人们一般不会知道或者根本不 在意除了基本多文种平面 BMP 之外到底使用的是那种平面),尽管如此,UTF-16 并不向后兼容 ASCII,因此,实现它的程序很少,因为大家需要对 ASCII 进行支持。 6.8.5 编码解码 Unicode 支持多种编码格式,这为程序员带来了额外的负担,每当你向一个文件写入字符 串的时候,你必须定义一个编码(encoding 参数)用于把对应的 Unicode 内容转换成你定义的格 式,Python 通过 Unicode 字符串的 encode()函数解决了这个问题,该函数接受字符串中的字符 为参数,输出你指定的编码格式的内容。 所以,每次我们写一个Unicode 字符串到磁盘上我们都要用指定的编码器给他"编码"一下, 相应地,当我们从这个文件读取数据时,我们必须"解码"该文件,使之成为相应的Unicode 字符 串对象. 简单的例子 下面的代码创建了一个 Unicode 字符串,用 UTF-8 编码器将它编码,然后写入到一个文件中 去.接着把数据从文件中读回来,解码成 Unicode 字符串对象.最后,打印出 Unicode 字符串,用 以确认程序正确地运行. 逐行解释 第 1-7 行 像通常一样,首先定义了 doc 字符串和用以表示解码器的常量,还有用以存储字符串的文 件名. 第 9-19 行 我们创建了一个 Unicode 字符串,用我们指定的编码格式对其进行编码,然后把它写入到文 Edit By Vheavens  Edit By Vheavens                                件中去,(9-13 行),接着我们把内容从文件中重新读出来, 解码,显示到屏幕上,输出的时候去 掉 print 的自动换行,因为我们已经在字符串中写了一个换行符(15-19 行). 例 6.2 简单 Unicode 字符串例子(uniFile.py) 这个简单的例子中,我们把一个 Unicode 字符串写入到磁盘文件,然后再把它读出并显示 出来。写入的时候用 UTF-8 编码,读出也一样,用 UTF-8. 1 #!/usr/bin/env python 2 ''' 3 An example of reading and writing Unicode strings:Writes 4 a Unicode string to a file in utf-8 and reads itback in. 5 ''' 6 CODEC = 'utf-8' 7 FILE = 'unicode.txt' 8 9 hello_out = u"Hello world\n" 10 bytes_out = hello_out.encode(CODEC) 11 f = open(FILE, "w") 12 f.write(bytes_out) 13 f.close() 14 15 f = open(FILE, "r") 16 bytes_in = f.read() 17 f.close() 18 hello_in = bytes_in.decode(CODEC) 19 print hello_in, 运行该程序,我们得到如下的输出: $ unicode_example.py Hello World 在文件系统中也会发现一个叫 unicode.txt 的文件,里面包含跟输出的内容一致的数据. $ cat unicode.txt Hello World! Edit By Vheavens  Edit By Vheavens                                简单 Web 例子 在第 20 章 Web 编程里面我们展示了一个简单的在 CGI 应用中使用 Unicode 的例子. 6.8.6 把 Unicode 应用到实际应用中 这些处理 Unicode 字符串的例子简单到让人感到有点假,事实上,只要你遵守以下的规则, 处理 Unicode 就是这么简单: z 程序中出现字符串时一定要加个前缀 u. z 不要用 str()函数,用 unicode()代替. z 不要用过时的 string 模块 -- 如果传给它的是非 ASCII 字符,它会把一切搞砸。 z 不到必须时不要在你的程序里面编解码 Unicod 字符.只在你要写入文件或数据库或者 网络时,才调用encode()函数;相应地,只在你需要把数据读回来的时候才调用decode() 函数. 这些规则可以规避 90%由于 Unicode 字符串处理引起的 bug.现在的问题是剩下的 10%的问 题却让你处理不了,幸亏 Python 提供了大量的模块、库来替你处理这些问题.它们可以让你用 10 行 Python 语句写出其他语言需要 100 行语句才能完成的功能,但是相应地,对 Unicode 支 持的质量也完全取决于这些模块、库. Python 标准库里面的绝大部分模块都是兼容 Unicode 的.除了 pickle 模块!pickle模块只 支持 ASCII 字符串。如果你把一个 Unicode 字符串交给 pickle 模块来 unpickle,它会报异常. 你必须先把你的字符串转换成 ASCII 字符串才可以.所以最好是避免基于文本的 pickle 操作. 幸运地是现在二进制格式已经作为 pickle 的默认格式了,pickle 的二进制格式支持不错.这点 在你向数据库里面存东西是尤为突出,把它们作为 BLOB 字段存储而不是作为 TEXT 或者 VARCHAR 字段存储要好很多.万一有人把你的字段改成了 Unicode 类型,这可以避免 pickle 的崩溃. 如果你的程序里面用到了很多第三方模块,那么你很可能在各个模块统一使用 Unicode 通 讯方面遇到麻烦,Unicode 还没成为一项必须的规定,在你系统里面的第三方模块(包括你的应 用要面对的平台\系统)需要用相同的 Unicode 编码,否则,可能你就不能正确的读写数据. 作为一个例子,假设你正在构建一个用数据库来读写 Unicode 数据的 Web 应用.为了支持 Edit By Vheavens  Edit By Vheavens                                Unicode,你必须确保以下方面对 Unicode 的支持: z 数据库服务器(MySQL,PostgreSQL,SQL Server,等等) z 数据库适配器(MySQLdb 等等) z Web 开发框架(mod_python,cgi,Zope,Plane,Django 等等) 数据库方面最容易对付,你只要确保每张表都用 UTF-8 编码就可以了。 数据库适配器可能有点麻烦,有些适配器支持 Unicode 有些不支持,比如说 MySQLdb,它并 不是默认就支持 Unicode 模式,你必须在 connect()方法里面用一个特殊的关键字 use_unicode 来确保你得到的查询结果是 Unicode 字符串. mod_python 里 面 开 启 对 Unicode 的支持相当简单,只要在 request 对 象 里 面 把 text-encoding 一项设成"utf-8"就行了,剩下的 mod_python 都会替你完成,Zope 等其他复杂 的系统可能需要更多的工作来支持 Unicode. 6.8.7 从现实中得来的教训 失误 #1: 你必须在一个极有限的时间内写出一个大型的应用,而且需要其他语言的支持, 但是产品经理并没有明确定义这一点。你并没有考虑Unicode 的兼容,直到项目快要结束... , 这时候再添加 Unicode 的支持几乎不太可能,不是吗? 结果 #1: 没能预测到最终用户对其他语言界面的需求,在集成他们用的面向其他语种的应 用时又没有使用 Unicode 支持.更新整个系统既让让人觉得枯燥和更是浪费时间。 失误 #2:在源码中到处使用 string 模块或者 str()和 chr()函数. 结果 #2:通过全局的查找替换把 str()和 chr()替换成 unicode()和 unichr(),但是这样一 来很可能就不能再用 pickle 模块,要用只能把所有要 pickle 处理的数据存成二进制形式,这 样一来就必须修改数据库的结构,而修改数据库结构就意味着全部推倒重来. 失误 #3: 不能确定所有的辅助系统都完全地支持 Unicode. 结果 #3: 不得不去为那些系统打补丁,而其中有些系统可能你根本就没有源码.修复对 Unicode 支持的 bug 可能会降低代码的可靠性,而且非常有可能引入新的 bug. 总结: 使应用程序完全支持 Unicode,兼容其他的语言本身就是一个工程. Edit By Vheavens  Edit By Vheavens                                它需要详细的考虑、计划.所有涉及到的软件、系统都需要检查,包括 Python 的标准库和其 他将要用到的第三方扩展模块.你甚至有可能需要组建一个经验丰富的团队来专门负责国际化 (I18N)问题. 6.8.8 Python 的 Unicode 支持 内建的 unicode()函数 Unicode 的工厂方法,同 Unicode 字符串操作符(u / U)的工作方式很类似,它接受一个 string 做参数,返回一个 Unicode 字符串. 内建的 decode()/encode()方法 decode()和 encode()内建函数接受一个字符串做参数返回该字符串对应的解码后/编码后 的字符串.decode()和 encode()都可以应用于常规字符串和 Unicode 字符串.decode()方法是在 Python2.2 以后加入的. Unicode 类型 Unicode 字符串对象是 basestring 的子类、用Unicode()工厂方法或直接在字符串前面加 一个 u 或者 U 来创建实例.支持 Unicode 原始字符串,只要在你的字符串前面加一个 ur 或者 UR 就可以了. Unicode 序数 标准内建函数 ord()工作方式相同,最近已经升级到可以支持 Unicode 对象了。内建的 unichr()函数返回一个对应的 Unicode 字符(需要一个 32 位的值);否则就产生一个 ValueError 异常. 强制类型转换 混合类型字符串操作需要把普通字符串转换成 Unicode 对象. 异常 UnicodeError 异常是在 exceptions 模块中定义的,ValueError 的子类.所有关于 Unicode 编解码的异常都要继承自 UnicodeError.详见 encode()函数. Edit By Vheavens  Edit By Vheavens                                标准编码 表 6.9 简洁地列出了 Python 中常用的编码方式.更详细、完全的列表见 Python 的文档,下 面是它的链接: http://docs.python.org/lib/standard-encodings.html RE 引擎对 Unicode 的支持 正则表达式引擎需要 Unicode 支持.详见 6.9 节的 re 模块. 表 6.9 常用 Unicode 编辑码 编码 描述 utf-8 变量长度为 8 的编码(默认编码) utf-16 变量长度为 16 的编码(大/小端) utf-16-le 小端 UTF-16 编码 utf-16-be 大端 UTF-16 编码 ascii 7-bit 7 位 ASCII 码表 iso-8859-1 ISO 8859-1 (Latin-1) 码表 unicode-escape (定义见 Python Unicode 构造函数) raw-unicode-escape (定义见 Python Unicode 构造函数) native Python 用的内部格式 字符串格式化操作符 对于 Python 的格式化字符串的操作符,%s 把 Python 字符串中的 Unicode 对象执行了 str(u)操作,所以,输出的应该是 u.encode(默认编码).如果格式化字符串是 Unicode 对象,所 有的参数都将首先强制转换成 Unicode 然后根据对应的格式串一起进行格式转换.数字首先被 转换成普通字符串,然后在转换成 Unicode.Python 字符串通过默认编码格式转化成 Unicode.Unicode 对象不变,所有其他格式字符串都需要像上面这样转化,下面是例子: u"%s %s" % (u"abc", "abc")   u"abc abc" 6.9 相关模块 Edit By Vheavens  Edit By Vheavens                                表 6.10 列出了 Python 标准库里面与字符串有关的主要模块. Table 6.10 与字符串类型有关的模块 模块 描述 string 字符串操作相关函数和工具,比如 Template 类. re 正则表达式:强大的字符串模式匹配模块 struct 字符串和二进制之间的转换 c/StringIO 字符串缓冲对象,操作方法类似于 file 对象. base64 Base 16,32,64 数据编解码 codecs 解码器注册和基类 crypt 进行单方面加密 diffliba 找出序列间的不同 hashlibb 多种不同安全哈希算法和信息摘要算法的 API hmac HMAC 信息鉴权算法的 Python 实现 md5d RSA 的 MD5 信息摘要鉴权 rotor 提供多平台的加解密服务 shad NIAT 的安全哈希算法 SHA stringprepe 提供用于 IP 协议的 Unicode 字符串 textwrape 文本打包和填充 unicodedata Unicode 数据库 a. Python2.1 新加 b.Python2.5 新加 c. Python2.2 新加 d. Python2.5 的 hashlib 中废除 e. Python2.3 新加 核心模块: re 正则表达式(RE)提供了高级的字符串模式匹配方案.通过描述这些模式的语法,你可以像使 用“过滤器”一样高效地查找传进来的文本。这些过滤器允许你基于自定义的模式字符串抽取 匹配模式、执行查找-替换或分割字符串. Edit By Vheavens  Edit By Vheavens                                Python1.5 中加入的 re 模块代替了早期的 regex 和 regsub 模块,全面采用了 Perl 正则表 达式语法,使得 Python 在对正则表达式的支持方面前进了一大步. Python1.6 里面重写了正则 表达式引擎(SRE),增加了对 Unicode 字符串的支持并对性能进行了重大的升级.SRE 引擎取代了 原有正则表达式的模块下的 PCRE 引擎. 该模块中包含的关键函数有:compile() - 将一个 RE 表达式编译成一个可重用的 RE 对 象;match() - 试图从字符串的开始匹配一个模式; search() - 找出字符串中所有匹配的项;sub() - 进行查找替换操作。其中的一些函数返 回匹配到的对象,你可以通过组匹配来访问(如果找到的话)。15 章的整章内容都是讲述正则 表达式。 6.10 字符串关键点总结 一些引号分隔的字符 你可以把字符串看成是 Python 的一种数据类型,在 Python 单引号或者双引号之间的字符数 组或者是连续的字符集合.在 Python 中最常用两个引号是单引号(')和双引号(")。字符串 的实际内容是这些单引号(')或者双引号(")之间的字符,不包括引号本身. 可以用两种引号来创建字符串是很有益处的,因为是当你的字符串中包含单引号时,如果 用单引号创建字符串,那么字符串中的双引号就不需要转义。反之亦然. 不可分字符类型 字符串是唯一的字面上的字符序列类型.不过,字符本身并不是一种类型,所以,字符串是字 符存储操作的最基本单位.字符应该视为长度为 1 的字符串. 字符串格式化操作符 ( % )提供类似于 printf()那样的功能. 字符串格式化操作符(见 6.4.1 节)提供了一种基于多种输入类型的创建自定义字符串的灵 活方式.它也提供了类似于 C/C++世界里的格式化操作的接口. 三引号 在 6.7.2 节里面,我们介绍了三引号,在三引号字符串中可以包含诸如换行回车或者 tab 键 这样的特殊字符.三引号字符串是用两边各三个单引号(''')或者两边各三个双引号(""")来定 义的. Edit By Vheavens  Edit By Vheavens                                原始字符串对每个特殊字符串都使用它的原意 第 6.4.2 节中,我们讲述了原始字符串,并且讨论了它们并不通过反斜线转义特殊字符的特 性.这个特性使得原始字符串非常适用于那些需要字符串原意的场合,比如在定义一个正则表达 式时. Python 字符串不是通过 NUL 或者'\0'来结束的 C 编程的一个主要问题是你访问了一个字符串后面的本不属于你的空间,这种情况发生在你 没有在字符串末尾添加终结符,NUL 或者'\0'(ASCII 值为 0)的时候.Python 不仅为你自动管理内 存,而且也把 C 的这个负担或者说是小麻烦去掉了.Python 中的字符串不是以 NUL 结束的,所以 你不需要为是否已经添加终结符担心.字符串中只包含你所定义的东西,没有别的. 6.11 列表 像字符串类型一样,列表类型也是序列式的数据类型,可以通过下标或者切片操作来访问 某一个或者某一块连续的元素.然而,相同的方面也就这些,字符串只能由字符组成,而且是不 可变的(不能单独改变它的某个值),而列表则是能保留任意数目的 Python 对象的灵活的容器。 就像我们将要看到的例子中所示,创建列表非常简单,向列表中添加元素也是如此. 列表不仅可以包含 Python 的标准类型,而且可以用用户定义的对象作为自己的元素.列表 可以包含不同类型的对象,而且要比 C 或者 Python 自己的数组类型(包含在 array 扩展包中)都 要灵活.因为数组类型所有的元素只能是一种类型.列表可以执行 pop,empt,sort,reverse 等操 作.列表也可以添加或者减少元素.还可以跟其他的列表结合或者把一个列表分成几个.可以对 单独一个元素或者多个元素执行 insert,update,或者 remove 操作. 元组类型在很多操作上都跟列表一样,许多用在列表上的例子在元组上照样能跑,我们有一 节内容专门讲解元组类型.它们的主要不同在于元组是不可变的,或者说是只读的,所以那些用 于更新列表的操作,比如用切片操作来更新一部分元素的操作,就不能用于元组类型. 如何创建列表类型数据并给它赋值 创建一个列表就像给一个变量赋值一样的简单.你手工写一个列表(空的或者有值的都行) 然后赋给一个变量,列表是由方括号([])来定义的,当然,你也可以用工厂方法来创建它. Edit By Vheavens  Edit By Vheavens                                >>> aList = [123, 'abc', 4.56, ['inner', 'list'], 7-9j] >>> anotherList = [None, 'something to see here'] >>> print aList [123, 'abc', 4.56, ['inner', 'list'], (7-9j)] >>> print anotherList [None, 'something to see here'] >>> aListThatStartedEmpty = [] >>> print aListThatStartedEmpty [] >>> list('foo') ['f', 'o', 'o'] 如何访问列表中的值 列表的切片操作就像字符串中一样;切片操作符([])和索引值或索引值范围一起使用 >>> aList[0] 123 >>> aList[1:4] ['abc', 4.56, ['inner', 'list']] >>> aList[:3] [123, 'abc', 4.56] >>> aList[3][1] 'list' 如何更新列表 你可以通过在等号的左边指定一个索引或者索引范围的方式来更新一个或几个元素,你也 可以用 append()方法来追加元素到列表中去. >>> aList [123, 'abc', 4.56, ['inner', 'list'], (7-9j)] >>> aList[2] 4.56 >>> aList[2] = 'float replacer' >>> aList [123, 'abc', 'float replacer', ['inner', 'list'], (7-9j)] >>> >>> anotherList.append("hi, i'm new here") Edit By Vheavens  Edit By Vheavens                                >>> print anotherList [None, 'something to see here', "hi, i'm new here"] >>> aListThatStartedEmpty.append('not empty anymore') >>> print aListThatStartedEmpty ['not empty anymore'] 如何删除列表中的元素或者列表(本身) 要删除列表中的元素,如果你确切的知道要删除元素的素引可以用 del 语句,否则可以用 remove()方法. >>> aList [123, 'abc', 'float replacer', ['inner', 'list'], (7-9j)] >>> del aList[1] >>> aList [123, 'float replacer', ['inner', 'list'], (7-9j)] >>> aList.remove(123) >>> aList ['float replacer', ['inner', 'list'], (7-9j)] 你还可以通过 pop()方法来删除并从列表中返回一个特定对象. 一般来说,程序员不需要去删除一个列表对象。列表对象出了作用域(比如程序结束,函数调 用完成等等)后它会自动被析构,但是如果你想明确的删除一整个列表,你可以用 del 语句: del aList 6.12 操作符 6.12.1 标准类型操作符 在第 4 章里,我们介绍了一些适用于包括标准类型在内的大部分对象的操作符,现在我们来 看一下这些操作符如何作用在列表上: >>> list1 = ['abc', 123] >>> list2 = ['xyz', 789] Edit By Vheavens  Edit By Vheavens                                >>> list3 = ['abc', 123] >>> 1ist1 < list2 True >>> list2 < list3 False >>> list2 > list3 and list1 == list3 True 在使用比较操作符时,比较数字和字符串是很明了的,但是用在列表上时就不是那么简单了, 列表比较操作有些狡猾,但是合乎逻辑.比较列表时也是用的内建的 cmp()函数,基本的比较逻辑 是这样的:两个列表的元素分别比较,直到有一方的元素胜出,比如我们上面的例子,'abc'和 'xyz'的比较直接决定了比较结果,在'abc'<'xyz'时,list1=list3,元组类型在 进行比较操作时跟列表遵循相同的逻辑. 6.12.2 序列类型操作符 切片([] 和[:]) 列表的切片操作跟字符串的切片操作很像,不过列表的切片操作返回的是一个对象或者是 几个对象的集合,而不是像字符串那样,返回一个字符或者一个子串.我们定义以下几个列表用 来做例子: >>> num_list = [43, -1.23, -2, 6.19e5] >>> str_list = ['jack', 'jumped', 'over', 'candlestick'] >>> mixup_list = [4.0, [1, 'x'], 'beef', -1.9+6j] 列表的切片操作也遵从正负索引规则,也有开始索引值,结束索引值,如果这两个值为空,默 认也会分别指到序列的开始和结束位置. >>> num_list[1] -1.23 >>> >>> num_list[1:] [-1.23, -2, 619000.0] >>> >>> num_list[2:-1] Edit By Vheavens  Edit By Vheavens                                [-2] >>> >>> str_list[2] 'over' >>> str_list[:2] ['jack', 'jumped'] >>> >>> mixup_list [4.0, [1, 'x'], 'beef', (-1.9+6j)] >>> mixup_list[1] [1, 'x'] 跟字符串类型只能用字符为元素不同,列表类型的元素可以是另一个序列类型,这就意味着 你在列表的元素上也可以使用所有的序列操作符或者在其之上执行序列类型内建的各种操作. 在下面的例子中,我们将会展示,不仅可以在一个切片操作的结果之上再进行切片,而且还可以 改变这个切片的结果,即使新对象的类型跟原对象不同也可以.你会注意到,这跟多维数组有一 些类似. >>> mixup_list[1][1] 'x' >>> mixup_list[1][1] = -64.875 >>> mixup_list [4.0, [1, -64.875], 'beef', (-1.9+6j)] 这时用 num_list 来做的另一个例子: >>> num_list [43, -1.23, -2, 6.19e5] >>> >>> num_list[2:4] = [16.0, -49] >>> >>> num_list [43, -1.23, 16.0, -49] >>> >>> num_list[0] = [65535L, 2e30, 76.45-1.3j] Edit By Vheavens  Edit By Vheavens                                >>> >>> num_list [[65535L, 2e+30, (76.45-1.3j)], -1.23, 16.0, -49] 注意在最后一个例子中,我们是如何把列表的单一元素替换成一个列表.在列表中进行诸如 remove,add,和 replace 的操作是多么的自由了吧!还有一点要注意,如果你想以子列表的形式 得到一个列表中的一个切片,那需要确保在赋值时等号的左边也是一个列表而不是一个列表的 元素. 成员关系操作( in ,not in) 列表中(同样适用于元组),我们可以检查一个对象是否是一个列表(或者元组)的成员. >>> mixup_list [4.0, [1, 'x'], 'beef', (-1.9+6j)] >>> >>> 'beef' in mixup_list True >>> >>> 'x' in mixup_list False >>> >>> 'x' in mixup_list[1] True >>> num_list [[65535L, 2e+030, (76.45-1.3j)], -1.23, 16.0, -49] >>> >>> -49 in num_list True >>> >>> 34 in num_list False >>> >>> [65535L, 2e+030, (76.45-1.3j)] in num_list True 注 意 ,'x'并不属于 mixup_list,因为'x'本身并不是 mixup_list 的 一个成员,而是 mixup_list[1]的,mixup_list[1]也是一个列表类型.成员关系操作运算同样适用于元组类型. Edit By Vheavens  Edit By Vheavens                                连接接操作符( + ) 连接操作符允许我们把多个列表对象合并在一起.注意,列表类型的连接操作也只能在同类 型之间进行,换句话说,你不能把两个不同类型的对象连接在一起,即便他们都是序列类型也不 行. >>> num_list = [43, -1.23, -2, 6.19e5] >>> str_list = ['jack', 'jumped', 'over', 'candlestick'] >>> mixup_list = [4.0, [1, 'x'], 'beef', -1.9+6j] >>> >>> num_list + mixup_list [43, -1.23, -2, 619000.0, 4.0, [1, 'x'], 'beef', (-1.9+6j)] >>> >>> str_list + num_list ['jack', 'jumped', 'over', 'candlestick', 43, -1.23, -2, 619000.0] 在 6.23 节里面我们会讲到,从 Python1.5.2 起,我们可以用 extend()方法来代替连接操作 符把一个列表的内容添加到另一个中去.使用 extend()方法比连接操作的一个优点是它实际上 是把新列表添加到了原有的列表里面,而不是像连接操作那样新建一个列表。list.extend() 方法也被用来做复合赋值运算,也就是 Python2.0 中添加的替换连接操作(+=). 必须指出,连接操作符并不能实现向列表中添加新元素的操作.在接下来的例子中,我们展 示了一个试图用连接操作向列表中添加新元素报错的例子. >>> num_list + 'new item' Traceback (innermost last): File "", line 1, in ? TypeError: illegal argument type for built-in operation 这个例子之所以是错误的,是因为我们在连接操作符的左右两边使用了不同类型的值,列表 类型 + 字符串类型这样的操作是非法的.显然,我们的初衷是把一个字符串作为一个新元素添 加到列表中去,不过我们的方法不正确.幸运的是,我们有一个正确的方法: 使用内建函数 append() (我们会在 6.13 节里面正是地介绍 append()和其他内建函数) Edit By Vheavens  Edit By Vheavens                                >>> num_list.append('new item') 重复操作符( * ) 重复操作符可能更多的应用在字符串类型中,不过,列表和元组跟字符串同属序列类型,所 以需要的时候也可以使用这一操作. >>> num_list * 2 [43, -1.23, -2, 619000.0, 43, -1.23, -2, 619000.0] >>> >>> num_list * 3 [43, -1.23, -2, 619000.0, 43, -1.23, -2, 619000.0, 43, -1.23, -2, 619000.0] Python2.0 起,也开始支持复合赋值运算: >>> hr = '-' >>> hr *= 30 >>> hr '------------------------------' 6.12.3 列表类型操作符和列表解析 其实 Python 中没有专门用于列表类型的操作符.列表可以使用大部分的对象和序列类型的 操作符.此外,列表类型有属于自己的方法.列表才有的构建--列表解析.这种方法是结合了列表 的方括弧和 for 循环,在逻辑上描述要创建的列表的内容.我们在第八章讨论列表解析,这里仅 仅向本章其他地方所做的那样,展示一个简单的例子: >>> [ i * 2 for i in [8, -2, 5] ] [16, -4, 10] >>> [ i for i in range(8) if i % 2 == 0 ] [0, 2, 4, 6] 6.13 内建函数 Edit By Vheavens  Edit By Vheavens                                6.13.1 标准类型函数 cmp() 在 4.6.1 章节里,我们通过比较数字和字符串介绍了内建 cmp()函数.但我们还不知道 cmp() 函数是如何跟其他的比如列表和元组类型合作的,这些类型不仅含有数字和字符串,而且还有列 表,元组,字典之类的其他对象,甚至可以是用户自定义的对象.这种情况下 cmp()函数是如何工 作的呢? >>> list1, list2 = [123, 'xyz'], [456, 'abc'] >>> cmp(list1, list2) -1 >>> >>> cmp(list2, list1) 1 >>> list3 = list2 + [789] >>> list3 [456, 'abc', 789] >>> >>> cmp(list2, list3) -1 如果我们比较的是两个同类的对象,比较操作是非常直观的.比如数字和字符串,直接比较 它们的值就行了。对于序列类型,比较操作稍微有点复杂了,但是方式上有相似 Python 在两个 对象基本不能比较的时候尽量做出公平的结果,比如当两个对象没有关系时或者两种类型根本 就没有用于比较的函数时,这时 Python 只能根据"逻辑"来做出结论. 除了这种极端的情况之外,安全又健全的比较方法是如果有不相等的情况出现,比较操作就 结束.这种算法是如何工作的呢?像我们前面简短的提到过的,列表的元素是可以无限迭代的.如 果它的元素都是相同类型,则用标准的比较方法来作比较.否则,如果要比较的元素类型不一致, 就像我们前面提到过的那样,如果比较的对象不一致,那么要得到一个准确的或者说绝对的比较 结果就有些冒险. 当我们较 list1 和 list2 时,list1 和 list2 进行逐项比较.第一个比较操作发生在两个列 表的第一个元素之间,比如说,123 跟 456 比较,因为 123<456,所以 list1 被认为小于 list2. Edit By Vheavens  Edit By Vheavens                                如果比较的值相等,那么两个序列的下一个值继续比较,直到不相等的情况出现,或者到达 较短的一个序列的末尾,在这种情况下,长的序列被认为是"较大"的.这就是为什么上面的 list2>> len(num_list) 4 >>> >>> len(num_list*2) 8 max() and min() max()和 min()函数在字符串操作里面用处不大,因为它们能对字符串做的只能是找出字符 Edit By Vheavens  Edit By Vheavens                                串中"最大"和"最小"的字符(按词典序),而对列表和元组来说,它们被定义了更多的用处.比如 对只包含数字和字符串对象的列表,max()和 min()函数就非常有用,重申一遍,混合对象的结构 越复杂返回的结构准确性就越差.然而,在有些情况下(虽然很少),这样的操作可以返回你需要 的结果.我们展示了一些使用上面定义好的列表的例子. >>> max(str_list) 'park' >>> max(num_list) [65535L, 2e+30, (76.45-1.3j)] >>> min(str_list) 'candlestick' >>> min(num_list) -49 sorted() and reversed() >>> s = ['They', 'stamp', 'them', 'when', "they're", 'small'] >>> for t in reversed(s): ... print t, ... small they're when them stamp They >>> sorted(s) ['They', 'small', 'stamp', 'them', "they're", 'when'] 初学者使用字符串,应该注意是如何把单引号和双引号的使用矛盾和谐掉.同时还要注意字 符串排序使用的是字典序,而不是字母序(字母'T'的 ASCII 码值要比字母'a'的还要靠前) enumerate() and zip() >>> albums = ['tales', 'robot', 'pyramid'] >>> for i, album in enumerate(albums): ... print i, album ... Edit By Vheavens  Edit By Vheavens                                0 tales 1 robot 2 pyramid >>> >>> fn = ['ian', 'stuart', 'david'] >>> ln = ['bairnson', 'elliott', 'paton'] >>> >>> for i, j in zip(fn, ln): ... print ('%s %s' % (i,j)).title() ... Ian Bairnson Stuart Elliott David Paton sum() >>> a = [6, 4, 5] >>> reduce(operator.add, a) 15 >>> sum(a) 15 >>> sum(a, 5) 20 >>> a = [6., 4., 5.] >>> sum(a) 15.0 list() and tuple() list()函数和 tuple()函数接受可迭代对象(比如另一个序列)作为参数,并通过浅拷贝数据 来创建一个新的列表或者元组.虽然字符串也是序列类型的,但是它们并不是经常用于 list()和 tuple(). 更多的情况下,它们用于在两种类型之间进行转换,比如你需要把一个已有的元组转 成列表类型的(然后你就可以修改它的元素了),或者相反. Edit By Vheavens  Edit By Vheavens                                >>> aList = ['tao', 93, 99, 'time'] >>> aTuple = tuple(aList) >>> aList, aTuple (['tao', 93, 99, 'time'], ('tao', 93, 99, 'time')) >>> aList == aTuple False >>> anotherList = list(aTuple) >>> aList == anotherList True >>> aList is anotherList False >>> [id(x) for x in aList, aTuple, anotherList] [10903800, 11794448, 11721544] 正如我们在本章的开头所讨论的,无论 list()还是 tuple()都不可能做完全的转换(见 6.1.2 节).也就是说,你传给 tuple()的一个列表对象不可能变成一个元组,而你传给 list()的 对象也不可能真正的变成一个列表.虽然前后两个对象(原来的和新的对象)有着相同的数据集 合(所以相等 == ),但是变量指向的却不是同一个对象了(所以执行 is 操作会返回 false).还 要注意,即使它们的所有的值都相同,一个列表也不可能"等于"一个元组. 6.13.3 列表类型内建函数 如果你不考虑 range()函数的话,Python 中没有特定用于列表的内建函数.range()函数接 受一个数值作为输入,输出一个符合标准的列表.第 8 章里面详细讨论了 range()函数.列表类型 对象可以使用大多数的对象和序列的内建函数,并且,列表对象有属于它们自己的方法. 6.14 列表类型的内建函数 Python 中的列表类型有自己的方法.我们会在第 13 章面向对象编程里面正式而详细的介绍 方法这一概念,现在你只需要把方法视为特定对象的函数或者过程就好.本节讨论的方法就像内 建的函数一样,除了它们只对列表类型进行操作之外.因为这些函数涉及到对列表更改(或者说 更新),所以它们都不适应于元组. Edit By Vheavens  Edit By Vheavens                                你可以重温以下我们前面讲到的用点号的方式访问对象的属性:object.attribute.列表的 方法也是这样:list.method().我们用点号来访问一个对象的属性(在这里是一个函数),然后用 函数操作符( () )来调用这个方法. 我们可以在一个列表对象上应用 dir()方法来得到它所有的方法和属性: >>> dir(list) # or dir([]) ['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__delslice__', '__doc__', '__eq__', '__ge__', '__getattribute__', '__getitem__', '__getslice__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__setslice__', '__str__', 'append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort'] 表 6.11 列出了目前列表类型支持的所有方法,稍后我们给出使用这些方法的例子. 表 6.11 列表类型内建函数 List Method Operation list.append(obj) 向列表中添加一个对象 obj list.count(obj) 返回一个对象obj 在列表中出现的次数 list.extend(seq)a 把序列 seq 的内容添加到列表中 list.index(obj, i=0, j=len(list)) 返回 list[k] == obj 的 k 值,并且 k 的范围在 i<=k>> music_media = [45] >>> music_media [45] >>> >>> music_media.insert(0, 'compact disc') >>> music_media ['compact disc', 45] >>> >>> music_media.append('long playing record') >>> music_media ['compact disc', 45, 'long playing record'] >>> >>> music_media.insert(2, '8-track tape') >>> music_media ['compact disc', 45, '8-track tape', 'long playing record'] 在前面的例子中,我们用一个元素初始化了一个列表,然后当向列表插入元素,或在尾部追 加新的元素后,都会去检查这个列表.现在确认一下一个值是否在我们的列表中,并看看如何找 出元素在列表中的索引值.我们用 in 操作符和 index()方法实现这两个需求. >>> 'cassette' in music_media False >>> 'compact disc' in music_media True >>> music_media.index(45) 1 >>> music_media.index('8-track tape') 2 Edit By Vheavens  Edit By Vheavens                                >>> music_media.index('cassette') Traceback (innermost last): File "", line 0, in ? ValueError: list.index(x): x not in list 噢!最后一个例子怎么出错了?呃,看起来用 index()来检查一个元素是否存在于一个 list 中并不是个好主意,因为我们出错了.应该先用 in 成员关系操作符(或者是 not in)检查一下,然 后在用 index()找到这个元素的位置。我们可以把最后几个对 index()调用放到一个单独的 for 循环里面,像这样: for eachMediaType in (45, '8-track tape', 'cassette'): if eachMediaType in music_media: print music_media.index(eachMediaType) 这个方案避免了我们上面犯的错误,因为在确认一个元素属于该列表之前 index()方法是不 会被调用的.稍后我们将会发现该如何处理这种错误,而不是这样的一出错,程序就崩溃了。 接下来我们测试 sort()和 reverse()方法,它们会把列表中的元素排序,然后翻转. >>> music_media ['compact disc', 45, '8-track tape', 'long playing record'] >>> music_media.sort() >>> music_media [45, '8-track tape', 'compact disc', 'long playing record'] >>> music_media.reverse() >>> music_media ['long playing record', 'compact disc', '8-track tape', 45] 核心笔记:那些可以改变对象值的可变对象的方法是没有返回值的! Python 初学者经常会陷入一个误区:调用一个方法就返回一个值.最明显的例子就是 sort(): >>> music_media.sort()# 没有输出? >>> 在使用可变对象的方法如 sort(),extend()和 reverse()的时候要注意,这些操作会在列表 中原地执行操作,也就是说现有的列表内容会被改变,但是没有返回值!是的,与之相反,字符串 方法确实有返回值: Edit By Vheavens  Edit By Vheavens                                >>> 'leanna, silly girl!'.upper() 'LEANNA, SILLY GIRL!' 温习一下,字符串是不可变的 -- 不可变对象的方法是不能改变它们的值的,所以它们必须 返回一个新的对象.如果你确实需要返回一个对象,那么我们建议你看一下 Python2.4 以后加入 的 reversed()和 sorted()内建函数. 它们像列表的方法一样工作,不同的是它们可以用做表达式,因为它们返回一个对象.同时 原来的那个列表还是那个列表,没有改变,而你得到的是一个新的对象. 回到 sort()方法,它默认的排序算法是归并排序(或者说"timsort")的衍生算法,时间复杂 度是 O(lg(n!)).关于这个算法我们不做进一步的讲解,可以通过源码查看它们的详情 -- Objects/listobject.c,还有算法描述: Objects/listsort.txt. extend()方法接受一个列表的内容然后把它的所有元素追加到另一个列表中去: >>> new_media = ['24/96 digital audio disc', 'DVD Audio disc', 'Super Audio CD'] >>> music_media.extend(new_media) >>> music_media ['long playing record', 'compact disc', '8-track tape', 45, '24/96 digital audio disc', 'DVD Audio disc', 'Super Audio CD'] 从 2.2 开始,extend()方法的参数支持任何可迭代对象,在 2.2 之前,它的参数必须是序列对 象,而在 1.6 之前它的参数必须是列表对象.通过可迭代对象(而不是一个序列对象),你能做更 多有趣的事情,比如: >>> motd = [] >>> motd.append('MSG OF THE DAY') >>> f = open('/etc/motd', 'r') >>> motd.extend(f) >>> f.close() >>> motd ['MSG OF THE DAY', 'Welcome to Darwin!\n'] Edit By Vheavens  Edit By Vheavens                                1.5.2 中加入的 pop()方法会从列表中把最后的或指定的元素返回调用者.我们会在 6.15.1 节和练习中看到 pop()方法, 6.15 列表的特殊特性 6.15.1 用列表构建其他数据结构 列表有容器和可变的特性,这使得它非常灵活,用它来构建其他的数据结构不是件难事.我 们马上能想到的是堆栈和队列. 堆栈 堆栈是一个后进先出(LIFO)的数据结构,其工作方式就像自助餐厅里面用于放盘子的弹簧 支架.把盘子想像成对象,第一个离开堆栈的是你最后放上的那个.在栈上"push"元素是个常用 术语,意思是把一个对象添加到堆栈中.反之,要删除一个元素,你可以把它"pop"出堆栈,例 6.3 展示了一个菜单驱动的程序,它实现了一个简单的、用于存储字符串的堆栈. 逐行解释 1-3 行 一开始是 Unix 的起始行,然后我们初始化堆栈(其实是个列表). 例 6.3 用列表模拟堆栈(stack.py) 这个简单的脚本把列表做为堆栈用于存储和取回输入的字符串,这个菜单驱动驱动的程序 仅使用了列表的 append()和 pop()方法. 1 #!/usr/bin/env python 2 3 stack = [] 4 5 def pushit(): 6 stack.append(raw_input('Enter new string: ').strip()) 7 8 def popit(): 9 if len(stack) == 0: Edit By Vheavens  Edit By Vheavens                                10 print 'Cannot pop from an empty stack!' 11 else: 12 print 'Removed [', ‘stack.pop()‘, ']' 13 14 def viewstack(): 15 print stack # calls str() internally 16 17 CMDs = {'u': pushit, 'o': popit, 'v': viewstack} 18 19 def showmenu(): 20 pr = """ 21 p(U)sh 22 p(O)p 23 (V)iew 24 (Q)uit 25 26 Enter choice: """ 27 28 while True: 29 while True: 30 try: 31 choice = raw_input(pr).strip()[0].lower() 32 except (EOFError,KeyboardInterrupt,IndexError): 33 choice = 'q' 34 35 print '\nYou picked: [%s]' % choice 36 if choice not in 'uovq': 37 print 'Invalid option, try again' 38 else: 39 break 40 41 if choice == 'q': 42 break Edit By Vheavens  Edit By Vheavens                                43 CMDs[choice]() 44 45 if __name__ == '__main__': 46 showmenu() 5-6 行 pushit()函数添加一个元素(通过提示由用户输入)到堆栈中. 8-12 行 popit()函数从堆栈中移除一个元素(最新的那个).试图从一个空的堆栈中移除元素会引 发一个错误.这种情况下,用户会得到一个警告提示.当一个元素从堆栈中pop 出来时,用户可以 看到到底是哪个元素被移除了.我们用反单引号(`)来代替 repr()函数,,把字符串的内容用引号 括起来显示而不是单单显示字符串的内容. 14-15 行 viewstack()方法显示堆栈现有的内容. Line 17 虽然我们下一章才会正式讲解字典类型,但是这里我们还是希望给你展示一个小例子,一 个包含命令的矢量(CMDs).这个字典的内容是前面定义的三个"动作"函数,它们可以通过字母进 行访问,用户必须输入这些字母来执行相应的命令.比如说,要进栈一个字符串,用户就必须输入 'u',那么字母'u'是如何从字典里面访问到 pushit()函数的呢?在第 43 行执行了选择的函数. 19-43 行 整个菜单驱动的应用都是由 showmenu()函数控制的.它首先向用户提供一个选单,如果用户 输入了合法选项就调用相应的函数.我们还没有详细的涉及到异常的处理,try-except 语句,但 本节里面的代码允许用户输入^D(EOF,产生一个 EOF 错误)或者^C(中断退出,产生一个 KeyboardInterrupt 异常),这两种操作在我们的脚本里面都会得到处理,结果等同于用户输入 'q'退出应用程序.这是对 Python 异常处理特性的一次应用,说明了 Python 的异常处理机制是多 么方便.外循环用来执行用户输入的指令直到用户退出应用,内循环提示用户输入一个合法的命 令项. 45-46 行 Edit By Vheavens  Edit By Vheavens                                如果调用文件,这部分的代码就会启动程序.如果该脚本只是被作为一个模块导入,则仅仅 是导入定义的函数和变量,而菜单也就不会显示.关于第 45 行和 __name__ 变量,请查阅第 3.4.1 节 . 下面简单的执行了一下该脚本: $ stack.py p(U)sh p(O)p (V)iew (Q)uit Enter choice: u You picked: [u] Enter new string: Python p(U)sh p(O)p (V)iew (Q)uit Enter choice: u You picked: [u] Enter new string: is p(U)sh p(O)p (V)iew (Q)uit Enter choice: u You picked: [u] Enter new string: cool! Edit By Vheavens  Edit By Vheavens                                p(U)sh p(O)p (V)iew (Q)uit Enter choice: v You picked: [v] ['Python', 'is', 'cool!'] p(U)sh p(O)p (V)iew (Q)uit Enter choice: o You picked: [o] Removed [ 'cool!' ] p(U)sh p(O)p (V)iew (Q)uit Enter choice: o You picked: [o] Removed [ 'is' ] p(U)sh p(O)p (V)iew (Q)uit Enter choice: o You picked: [o] Removed [ 'Python' ] p(U)sh p(O)p (V)iew (Q)uit Edit By Vheavens  Edit By Vheavens                                Enter choice: o You picked: [o] Cannot pop from an empty stack! p(U)sh p(O)p (V)iew (Q)uit Enter choice: ^D You picked: [q] 队列 队列是一种先进先出(FIFO)的数据类型,它的工作原理类似于超市中排队交钱或者银行里 面的排队,队列里的第一个人首先接受服务( 满心想第一个出去).新的元素通过"入队"的方式添加进队列的末尾,"出队"就是从队列的 头部删除.下面的例子里面展示了这种操作,我们把上面的堆栈的例子进行了改造,用列表实现 了一个简单的队列. 例 6.4 把列表用做队列(queue.py) 这个例子中,我们把列表用做队列来存储和取回菜单驱动应用里面输入的字符串,只用到了 列表的 append()和 pop()方法. 1 #!/usr/bin/env python 2 3 queue = [] 4 5 def enQ(): 6 queue.append(raw_input('Enter new string: ').strip()) 7 8 def deQ(): 9 if len(queue) == 0: 10 print 'Cannot pop from an empty queue!' 11 else: Edit By Vheavens  Edit By Vheavens                                12 print 'Removed [', ‘queue.pop(0)‘, ']' 13 14 def viewQ(): 15 print queue # calls str() internally 16 17 CMDs = {'e': enQ, 'd': deQ, 'v': viewQ} 18 19 def showmenu(): 20 pr = """ 21 (E)nqueue 22 (D)equeue 23 (V)iew 24 (Q)uit 25 26 Enter choice: """ 27 28 while True: 29 while True: 30 try: 31 choice = raw_input(pr).strip()[0].lower() 32 except (EOFError,KeyboardInterrupt,IndexError): 33 choice = 'q' 34 35 print '\nYou picked: [%s]' % choice 36 if choice not in 'devq': 37 print 'Invalid option, try again' 38 else: 39 break 40 41 if choice == 'q': 42 break 43 CMDs[choice]() 44 Edit By Vheavens  Edit By Vheavens                                45 if __name__ == '__main__': 46 showmenu() 逐行解释 该脚本跟上面的 stack.py 非常相似,所以我们只讲解一下有显著不同的行: 1-7 行 定义了几个后面脚本要用到的常量. Lines 5–6 enQ()方法跟 pushit()方法非常相近,只不过名字改变了. 8-12 行 两个脚本的主要差别就在于此,deQ()函数不像 popit()函数那样把列表的最后一个元素 弹出来,而是第一个元素. 17,21-24,36 行 选项改变了,所以我们也需要重写原来的提示信息和输入检查. 还是在这里列举一些输出: $ queue.py (E)nqueue (D)equeue (V)iew (Q)uit Enter choice: e You picked: [e] Enter new queue element: Bring out (E)nqueue Edit By Vheavens  Edit By Vheavens                                (D)equeue (V)iew (Q)uit Enter choice: e You picked: [e] Enter new queue element: your dead! (E)nqueue (D)equeue (V)iew (Q)uit Enter choice: v You picked: [v] ['Bring out', 'your dead!'] (E)nqueue (D)equeue (V)iew (Q)uit Enter choice: d You picked: [d] Removed [ 'Bring out' ] (E)nqueue (D)equeue (V)iew (Q)uit Edit By Vheavens  Edit By Vheavens                                Enter choice: d You picked: [d] Removed [ 'your dead!' ] (E)nqueue (D)equeue (V)iew (Q)uit Enter choice: d You picked: [d] Cannot dequeue from empty queue! (E)nqueue (D)equeue (V)iew (Q)uit Enter choice: ^D You picked: [q] 6.16 元组 实际上元组是跟列表非常相近的另一种容器类型.元组和列表看起来不同的一点是元组用 的是圆括号而列表用的是方括号。而功能上,元组和列表相比有一个很重要的区别,元组是一种 不可变类型.正因为这个原因,元组能做一些列表不能做的事情... 用做一个字典的 key.另外当 处理一组对象时,这个组默认是元组类型. 通常情况下,我们会先介绍可用于大部分对象的操作符和内建函数,然后是介绍针对序列类 型的, 最后是总结一下仅适用于元组类型的操作符和内建函数.不过,由于元组类型跟列表类型 有着如此多的共同之处,按照这种讲法我们会重复非常多的上一节的内容.为了避免太多重复信 息,我们会讲解元组和列表在应用于每一组操作符和内建函数上时的区别,然后讨论一下元组的 不变性以及其他独特的特性. Edit By Vheavens  Edit By Vheavens                                如何创建一个元组并给它赋值 创建一个元组并给他赋值实际上跟创建一个列表并给它赋值完全一样,除了一点,只有一个 元素的元组需要在元组分割符里面加一个逗号(,)用以防止跟普通的分组操作符混淆.不要忘了 它是一个工厂方法! >>> aTuple = (123, 'abc', 4.56, ['inner', 'tuple'], 7-9j) >>> anotherTuple = (None, 'something to see here') >>> print aTuple (123, 'abc', 4.56, ['inner', 'tuple'], (7-9j)) >>> print anotherTuple (None, 'something to see here') >>> emptiestPossibleTuple = (None,) >>> print emptiestPossibleTuple (None,) >>> tuple('bar') ('b', 'a', 'r') 如何访问元组中的值 元组的切片操作跟列表一样,用方括号作为切片操符([]),里面写上索引值或者索引范围. >>> aTuple[1:4] ('abc', 4.56, ['inner', 'tuple']) >>> aTuple[:3] (123, 'abc', 4.56) >>> aTuple[3][1] 'tuple' 如何更新元组 跟数字和字符串一样,元组也是不可变类型,就是说你不能更新或者改变元组的元素,在 6.2 和 6.3.2 节里面,我们是通过现有字符串的片段再构造一个新字符串的方式解决的,对元组同样 Edit By Vheavens  Edit By Vheavens                                需要这样. >>> aTuple = aTuple[0], aTuple[1], aTuple[-1] >>> aTuple (123, 'abc', (7-9j)) >>> tup1 = (12, 34.56) >>> tup2 = ('abc', 'xyz') >>> tup3 = tup1 + tup2 >>> tup3 (12, 34.56, 'abc', 'xyz') 如何移除一个元组的元素以及元组本身 删除一个单独的元组元素是不可能的,当然,把不需要的元素丢弃后, 重新组成一个元组是 没有问题的. 要显示地删除一整个元组,只要用 del 语句减少对象引用计数.当这个引用计数达到 0 的时 候,该对象就会被析构.记住,大多数时候,我们不需要显式的用 del 删除一个对象,一出它的作 用域它就会被析构,Python 编程里面用到显式删除元组的情况非常之少. del aTuple 6.17 元组操作符和内建函数 6.17.1 标准类型操作符,序列类型操作符和内建函数. 元组的对象和序列类型操作符还有内建函数跟列表的完全一样.你仍然可以对元组进行切 片操作,合并操作,以及多次拷贝一个元组,还可以检查一个对象是否属于一个元组,进行元组之 间的比较等. 创建,重复,连接操作 >>> t = (['xyz', 123], 23, -103.4) >>> t Edit By Vheavens  Edit By Vheavens                                (['xyz', 123], 23, -103.4) >>> t * 2 (['xyz', 123], 23, -103.4, ['xyz', 123], 23, -103.4) >>> t = t + ('free', 'easy') >>> t (['xyz', 123], 23, -103.4, 'free', 'easy') 成员关系操作,切片操作 >>> 23 in t True >>> 123 in t False >>> t[0][1] 123 >>> t[1:] (23, -103.4, 'free', 'easy') 内建函数 >>> str(t) (['xyz', 123], 23, -103.4, 'free', 'easy') >>> len(t) 5 >>> max(t) 'free' >>> min(t) -103.4 >>> cmp(t, (['xyz', 123], 23, -103.4, 'free', 'easy')) 0 >>> list(t) [['xyz', 123], 23, -103.4, 'free', 'easy'] 操作符 >>> (4, 2) < (3, 5) Edit By Vheavens  Edit By Vheavens                                False >>> (2, 4) < (3, -1) True >>> (2, 4) == (3, -1) False >>> (2, 4) == (2, 4) True 6.17.2 元组类型操作符和内建函数,内建方法 像列表一样 元组也没有它自己专用的运算符和内建函数.上一节中描述的列表方法都跟列 表对象的可变性有关,比如说排序,替换,添加等等,因为元组是不可变的,所以这些操作对元组 来说就是多余的,这些方法没有被实现. 6.18 元组的特殊特性. 6.18.1 不可变性给元组带来了什么影响? 是的,我们在好多地方使用到了"不可变性"这个单词,除了这个词的计算机学科定义和实现, 从应用的角度来考虑,这个词的底线是什么?一个数据类型成为不可变的到底意味着什么? 在三个标准不可变类型里面--数字,字符串和元组字符串--元组是受到影响最大的,一个数 据类型是不可变的,简单来讲,就意味着一旦一个对象被定义了,它的值就不能再被更新,除非重 新创建一个新的对象.对数字和字符串的影响不是很大,因为它们是标量类型,当它们代表的值 改变时,这种结果是有意义的,是按照你所想要的方式进行访问的,而对于元组,事情就不是 这样了。 因为元组是容器对象,很多时候你想改变的只是这个容器中的一个或者多个元素,不幸的 是这是不可能的,切片操作符不能用作左值进行赋值。这和字符串没什么不同,切片操作只能 用于只读的操作。 不可变并不是坏事,比如我们把数据传给一个不了解的 API 时,可以确保我们的数据不会 被修改。同样地,如果我们操作从一个函数返回的元组,可以通过内建 list()函数把它转换成 一个列表. Edit By Vheavens  Edit By Vheavens                                6.18.2 元组也不是那么“不可变” 虽然元组是被定义成不可变的,但这并不影响它的灵活性。元组并不像我们想的那么不可 变,这是什么意思?其实元组几个特定的行为让它看起来并不像我们先前声称的那么不可变. 比如说,既然我们可以把字符串组合在一起形成一个大字符串。那么把元组组合在一起形 成一个大的元组也没什么不对,所以,连接操作可用,这个操作一点都没有改变那些小元组。 我们所作的是把它们的元素结合在一起.这里有几个例子: >>> s = 'first' >>> s = s + ' second' >>> s 'first second' >>> >>> t = ('third', 'fourth') >>> t ('third', 'fourth') >>> >>> t = t + ('fifth', 'sixth') >>> t ('third', 'fourth', 'fifth', 'sixth') 同样的概念也适用于重复操作。重复操作只不过是多次复制同样的元素,再有,我们前面 提到过可以用一个简单的函数调用把一个元组变成一个可变的列表。我们的最后一个特性可能 会吓到你。你可以“修改”特定的元组元素,哇!这意味着什么? 虽然元组对象本身是不可变的,但这并不意味着元组包含的可变对象也不可变了。 >>> t = (['xyz', 123], 23, -103.4) >>> t (['xyz', 123], 23, -103.4) >>> t[0][1] 123 >>> t[0][1] = ['abc', 'def'] >>> t Edit By Vheavens  Edit By Vheavens                                (['xyz', ['abc', 'def']], 23, -103.4) 在上面的例子中,虽然t 是一个元组类型变量,但是我们设法通过替换它的第一个元素(一 个列表对象)的项来“改变”了它。我们替换了 t[0][1],原来是个整数,我们把它替换成了一 个列表对象 ['abc','def'].虽然我们只是改变了一个可变对象,但在某种意义上讲,我们也“改 变”了我们的元组类型变量。 6.18.3 默认集合类型 所有的多对象的,逗号分隔的,没有明确用符号定义的,比如说像用方括号表示列表和用 圆括号表示元组一样,等等这些集合默认的类型都是元组,下面是一个简单的示例: >>> 'abc', -4.24e93, 18+6.6j, 'xyz' ('abc', -4.24e+093, (18+6.6j), 'xyz') >>> >>> x, y = 1, 2 >>> x, y (1, 2) 所有函数返回的多对象(不包括有符号封装的)都是元组类型。注意,有符号封装的多对 象集合其实是返回的一个单一的容器对象,比如: def foo1(): : return obj1, obj2, obj3 def foo2(): : return [obj1, obj2, obj3] def foo3(): : return (obj1, obj2, obj3) Edit By Vheavens  Edit By Vheavens                                上面的例子中,foo1()返回 3 个对象,默认的作为一个包含 3 个对象的元组类型,foo2() 返回一个单一对象,一个包含 3 个对象的列表,还有 foo3()返回一个跟 foo1()相同的对象.唯一 不同的是这里的元组是显式定义的. 为了避免令人讨厌的副作用,建议总是显式的用圆括号表达式表示元组或者创建一个元组. >>> 4, 2 < 3, 5 # int, comparison, int (4, True, 5) >>> (4, 2) < (3, 5) # tuple comparison False 在第一个例子中小于号的优先级高于逗号,2<3 的结果成了元组变量的第二个元素,适当 的封装元组就会得到希望得到的结果. 6.18.4 单元素元组 曾经试过创建一个只有一个元素的元组?你在列表上试过,它可以完成,但是无论你怎么 在元组上试验,你都不能得到想要的结果。 >>> ['abc'] ['abc'] >>> type(['abc']) # a list >>> >>> ('xyz') 'xyz' >>> type(('xyz')) # a string, not a tuple 或许你忘记了圆括号被重载了,它也被用作分组操作符。由圆括号包裹的一个单一元素首 先被作为分组操作,而不是作为元组的分界符。一个变通的方法是在第一个元素后面添一个逗 号(,)来表明这是一个元组而不是在做分组操作. >>> ('xyz',) Edit By Vheavens  Edit By Vheavens                                ('xyz',) 6.18.5 字典的关键字 不可变对象的值是不可改变的。这就意味着它们通过 hash 算法得到的值总是一个值。这是 作为字典键值的一个必备条件。在下一章节里面我们会讨论到,键值必须是可哈希的对象,元 组变量符合这个标准,而列表变量就不行。 核心笔记:列表 VS 元组 一个经常会被问到的问题是,"为什么我们要区分元组和列表变量?"这个问题也可以被表 述为“我们真的需要两个相似的序列类型吗?”,一个原因是在有些情况下,使用其中的一种类 型要优于使用另一种类型。 最好使用不可变类型变量的一个情况是,如果你在维护一些敏感的数据,并且需要把这些 数据传递给一个并不了解的函数(或许是一个根本不是你写的 API),作为一个只负责一个软件 某一部分的工程师,如果你确信你的数据不会被调用的函数篡改,你会觉得安全了许多。 一个需要可变类型参数的例子是,如果你在管理动态数据集合时。你需要先把它们创建出 来,逐渐地或者不定期的添加它们,或者有时还要移除一些单个的元素。这是一个必须使用可 变类型对象的典型例子。幸运的是,通过内建的 list()和 tuple()转换函数,你可以非常轻松 的在两者之间进行转换. list()和 tuple()函数允许你用一个列表来创建一个元组,反之亦然.如果你有一个元组变 量,但你需要一个列表变量因为你要更新一下它的对象,这时 list()函数就是你最好的帮手.如 果你有一个列表变量,并且想把它传递给一个函数,或许一个 API,而你又不想让任何人弄乱你 的数据,这时 tuple()函数就非常有用。 6.19 相关模块 表 6.12 列出了与序列类型相关的关键模块,这个列表包含了前面我们间接提到的数组模块, 它就像列表类型,不过它要求所有的元素都是同一类型。copy 模块(可以参考下面的 6.20 节) 负责处理对象的浅拷贝和深拷贝。 Table 6.12 与序列类型相关的模块 模块 内容 Edit By Vheavens  Edit By Vheavens                                数组 一种受限制的可变序列类型,要求所有的元素必须都是相同的类型。 copy 提供浅拷贝和深拷贝的能力(详见 6.20) operator 包含函数调用形式的序列操作符,比如 operator.concat(m,n)就相当于连 接操作(m+n)。 re Perl 风格的正则表达式查找(和匹配);见第 15 章 StringIO/ cStringIO 把长字符串作为文件来操作,比如 read(),seek()函数等,C 版的更快一些, 但是它不能被继承. Textwrapa 用作包裹/填充文本的函数,也有一个类 types 包含 Python 支持的所有类型 collectionsb 高性能容器数据类型 a. Python2.3 新加 b. Python2.4 新加 operator 模块除了提供与数字操作符相同的功能外,还提供了与序列类型操作符相同的 功能.types 模块是代表 python 支持的全部类型的 type 对象的引用。最后,UserList 模块包 含了 list 对象的完全的类实现。因为 Python 类型不能作为子类,所以这个模块允许用户获得 类似 list 的类,也可以派生出新的类或功能。如果你熟悉面向对象编程的话,我们强烈推荐你 阅读第 13 章 6.20 拷贝 Python 对象 浅拷贝和深拷贝 在前面的 3.5 节里面我们讲过对象赋值实际上是简单的对象引用。也就是说当你创建一个 对象,然后把它赋给另一个变量的时候,Python 并没有拷贝这个对象,而是拷贝了这个对象的 引用。 比如,假设你想创建一对小夫妻的通用档案,名为 person.然后你分别为他俩拷贝一份。 在下面的例子中,我们展示了两种拷贝对象的方式,一种使用了切片操作,另一种用了工厂方 法,为了区分出三个不同的对象,我们使用 id()内建函数来显示每个对象的标识符。(我们还 可以用 is 操作符来做相同的事情) Edit By Vheavens  Edit By Vheavens                                >>> person = ['name', ['savings', 100.00]] >>> hubby = person[:] # slice copy >>> wifey = list(person) # fac func copy >>> [id(x) for x in person, hubby, wifey] [11826320, 12223552, 11850936] 为他们创建了初始有$100 的个人存款帐户。用户名改为定制的名字。但是,当丈夫取走$50 后,他的行为影响到了他妻子的账户,虽然我们进行了分开的拷贝作(当然,前提是我们希望他 们每个人都拥有自己单独的帐号,而不是一个单一的联合帐号。)为什么会这样呢? >>> hubby[0] = 'joe' >>> wifey[0] = 'jane' >>> hubby, wifey (['joe', ['savings', 100.0]], ['jane', ['savings', 100.0]]) >>> hubby[1][1] = 50.00 >>> hubby, wifey (['joe', ['savings', 50.0]], ['jane', ['savings', 50.0]]) 原因是我们仅仅做了一个浅拷贝。对一个对象进行浅拷贝其实是新创建了一个类型跟原对 象一样,其内容是原来对象元素的引用,换句话说,这个拷贝的对象本身是新的,但是它的内容不 是.序列类型对象的浅拷贝是默认类型拷贝,并可以以下几种方式实施:(1)完全切片操作[:],(2) 利用工厂函数,比如 list(),dict()等,(3)使用 copy 模块的 copy 函数. 你的下一个问题可能是:当妻子的名字被赋值,为什么丈夫的名字没有受到影响?难道它们 的名字现在不应该都是'jane'了吗?为什么名字没有变成一样的呢?怎么会是这样呢?这是因为 在这两个列表的两个对象中,第一个对象是不可变的(是个字符串类型),而第二个是可变的(一 个列表).正因为如此,当进行浅拷贝时,字符串被显式的拷贝,并新创建了一个字符串对象,而列 表元素只是把它的引用复制了一下,并不是它的成员.所以改变名字没有任何问题,但是更改他 们银行账号的任何信息都会引发问题.现在,让我们分别看一下每个列表的元素的对象 ID 值,注 意,银行账号对象是同一个对象,这也是为什么对一个对象进行修改会影响到另一个的原因.注 意在我们改变他们的名字后,新的名字字符串是如何替换原有'名字'字符串的. Edit By Vheavens  Edit By Vheavens                                BEFORE: >>> [id(x) for x in hubby] [9919616, 11826320] >>> [id(x) for x in wifey] [9919616, 11826320] AFTER: >>> [id(x) for x in hubby] [12092832, 11826320] >>> [id(x) for x in wifey] [12191712, 11826320] 假设我们要给这对夫妻创建一个联合账户,那这是一个非常棒的方案,但是,如果需要的是 两个分离账户,就需要作些改动了.要得到一个完全拷贝或者说深拷贝--创建一个新的容器对象, 包含原有对象元素(引用)全新拷贝的引用--需要 copy.deepcopy()函数.我们使用深拷贝来重 写整个例子. >>> person = ['name', ['savings', 100.00]] >>> hubby = person >>> import copy >>> wifey = copy.deepcopy(person) >>> [id(x) for x in person, hubby, wifey] [12242056, 12242056, 12224232] >>> hubby[0] = 'joe' >>> wifey[0] = 'jane' >>> hubby, wifey (['joe', ['savings', 100.0]], ['jane', ['savings', 100.0]]) >>> hubby[1][1] = 50.00 >>> hubby, wifey (['joe', ['savings', 50.0]], ['jane', ['savings', 100.0]]) 这就是我们想要的方式,作为验证,让我们确认一下所有四个对象都是不同的. >>> [id(x) for x in hubby] [12191712, 11826280] Edit By Vheavens  Edit By Vheavens                                >>> [id(x) for x in wifey] [12114080, 12224792] 以下有几点关于拷贝操作的警告。第一,非容器类型(比如数字,字符串和其他"原子"类型的 对象,像代码,类型和 xrange 对象等)没有被拷贝一说,浅拷贝是用完全切片操作来完成的.第二, 如果元组变量只包含原子类型对象,对它的深拷贝将不会进行.如果我们把账户信息改成元组类 型,那么即便按我们的要求使用深拷贝操作也只能得到一个浅拷贝: >>> person = ['name', ('savings', 100.00)] >>> newPerson = copy.deepcopy(person) >>> [id(x) for x in person, newPerson] [12225352, 12226112] >>> [id(x) for x in person] [9919616, 11800088] >>> [id(x) for x in newPerson] [9919616, 11800088] 核心模块: copy 我们刚才描述的浅拷贝和深拷贝操作都可以在 copy 模块中找到.其实 copy 模块中只有两 个函数可用:copy()进行浅拷贝操作,而 deepcopy()进行深拷贝操作. 6.21 序列类型小结 序列类型为数据的顺序存储提供了几种机制.字符串是最常用的数据载体,无论是用于给用 户显示,存贮到硬盘,通过网络传输,还是作为一个多源信息的容器.列表和元组提供了容器存储 能力,允许简单的操作和访问多个对象,无论它们是 Python 的对象还是用户自定义的对象.单一 元素或一组元素可以通过持续有序地索引偏移进行切片操作来访问.总之,这些数据类型为你的 Python 开发环境提供了灵活而易用的存贮工具.我们用表 6.13--序列类型的操作符,内建函数 和方法的摘要列表来总结本章. Edit By Vheavens  Edit By Vheavens                                Table 6.13 序列类型操作符,内建函数和方法                   Edit By Vheavens  Edit By Vheavens                                                        Edit By Vheavens  Edit By Vheavens    6.22 练习 6–1. 字符串.string 模块中是否有一种字符串方法或者函数可以帮我鉴定一下一个字符串 是否是另一个大字符串的一部分? 6–2. 字符串标识符.修改例 6-1 的 idcheck.py 脚本,使之可以检测长度为一的标识符,并且 可以识别 Python 关键字,对后一个要求,你可以使用 keyword 模块(特别是 keyword.kelist)来帮你. 6–3. 排序 (a) 输入一串数字,从大到小排列之. (b) 跟a 一样,不过要用字典序从大到小排列之. 6–4. 算术. 更新上一章里面你的得分测试练习方案,把测试得分放到一个列表中去.你的代 码应该可以计算出一个平均分,见练习 2-9 和练习 5-3. 6–5. 字符串 (a)更新你在练习 2-7 里面的方案,使之可以每次向前向后都显示一个字符串的一个字符. (b)通过扫描来判断两个字符串是否匹配(不能使用比较操作符或者 cmp()内建函数)。附加题: Edit By Vheavens  Edit By Vheavens  在你的方案里加入大小写区分. (c)判断一个字符串是否重现(后面跟前面的一致).附加题:在处理除了严格的回文之外,加入对 例如控制符号和空格的支持。 (d)接受一个字符,在其后面加一个反向的拷贝,构成一个回文字符串. 6–6. 字符串.创建一个 string.strip()的替代函数:接受一个字符串,去掉它前面和后面的 空格(如果使用 string.*strip()函数那本练习就没有意义了) 6–7. 调试.看一下在例 6.5 中给出的代码(buggy.py) (a)研究这段代码并描述这段代码想做什么.在所有的(#)处都要填写你的注释. (b)这个程序有一个很大的问题,比如输入 6,12,20,30,等它会死掉,实际上它不能处理任何的偶 数,找出原因. (c)修正(b)中提出的问题. 6–8. 列表.给出一个整数值,返回代表该值的英文,比如输入89 返回"eight-nine"。附加题: 能够返回符合英文语法规则的形式,比如输入“89”返回“eighty-nine”。本练习中的值限定在家 0 到 1,000. 6–9. 转换.为练习 5-13 写一个姊妹函数, 接受分钟数, 返回小时数和分钟数. 总时间不 变,并且要求小时数尽可能大. 6–10.字符串.写一个函数,返回一个跟输入字符串相似的字符串,要求字符串的大小写反转. 比如,输入"Mr.Ed",应该返回"mR.eD"作为输出. Example 6.4 有 bug 的程序(buggy.py) 这是一个用于练习 6-7 的程序,判断这个程序是干什么的,在"#"处添加你的注释,找出其中的错 误,并修改之. 1 #!/usr/bin/env python 2 3 # 4 num_str = raw_input('Enter a number: ') 5 6 # 7 num_num = int(num_str) 8 9# 10 fac_list = range(1, num_num+1) 11 print "BEFORE:", 'fac_list' Edit By Vheavens  Edit By Vheavens  12 13 # 14 i = 0 15 16 # 17 while i < len(fac_list): 18 19 # 20 if num_num % fac_list[i] == 0: 21 del fac_list[i] 22 23 # 24 i = i + 1 25 26 # 27 print "AFTER:", 'fac_list' 6–11.转换 (a)创建一个从整数到 IP 地址的转换程序,如下格式: WWW.XXX.YYY.ZZZ. (b)更新你的程序,使之可以逆转换. 6–12.字符串 (a)创建一个名字为 findchr()的函数,函数声明如下: def findchr(string, char) findchr()要在字符串 string 中查找字符 char,找到就返回该值的索引,否则返回-1.不能用 string.*find()或者 string.*index()函数和方法 (b)创建另一个叫 rfindchr()的函数,查找字符 char 最后一次出现的位置.它跟 findchr()工作 类似,不过它是从字符串的最后开始向前查找的. (c)创建第三个函数,名字叫 subchr(),声明如下: def subchr(string, origchar, newchar) subchr()跟 findchr()类似,不同的是,如果找到匹配的字符就用新的字符替换原先字符.返回 修改后的字符串. 6–13.字符串.string 模块包含三个函数,atoi(),atol(),和 atof(),它们分别负责把字符串转 换成整数,长整型,和浮点型数字.从 Python1.5 起,Python 的内建函数 int(),long(),float()也可以 做相同的事了, complex()函数可以把字符串转换成复数.(然而 1,5 之前,这些转换函数只能工作于 数字之上) string 模块中并没有实现一个 atoc()函数,那么你来实现一个,atoc(),接受单个字符串做参 数输入,一个表示复数的字符串,例如,'-1.23e+4-5.67j',返回相应的复数对象.你不能用 eval()函 数,但可以使用 complex()函数,而且你只能在如下的限制之下使用 complex():complex(real,imag) Edit By Vheavens  Edit By Vheavens  的 real 和 imag 都必须是浮点值. 6–14.随机数.设计一个"石头,剪子,布"游戏,有时又叫"Rochambeau",你小时候可能玩过,下面 是规则.你和你的对手,在同一时间做出特定的手势,必须是下面一种手势:石头,剪子,布.胜利者从 下面的规则中产生,这个规则本身是个悖论. (a) the paper covers the rock, 布包石头. (b)石头砸剪子, (c)剪子剪破布.在你的计算机版本中,用户输入她/他的选项,计算机找一个随机选项,然后由你 的程序来决定一个胜利者或者平手.注意:最好的算法是尽量少的使用 if 语句. 6–15.转换 (a)给出两个可识别格式的日期,比如 MM/DD/YY 或者 DD/MM/YY 格式,计算出两个日期间的天 数. (b)给出一个人的生日,计算从此人出生到现在的天数,包括所有的闰月. (c)还是上面的例子,计算出到此人下次过生日还有多少天. 6–16.矩阵.处理矩阵M和N的加和乘操作. 6–17.方法.实现一个叫 myPop()的函数,功能类似于列表的 pop()方法,用一个列表作为输入, 移除列表的最新一个元素,并返回它. 6–18. zip() 内建函数 在6.13.2 节里面关于 zip()函数的例子中,zip(fn,ln)返回的是什么? 6–19.多列输出.有任意项的序列或者其他容器,把它们等距离分列显示.由调用者提供数据和 输出格式.例如,如果你传入 100 个项并定义 3 列输出,按照需要的模式显示这些数据.这种情况下,应 该是两列显示 33 个项,最后一列显示 34 个.你可以让用户来选择水平排序或者垂直排序. Edit By Vheavens  Edit By Vheavens  映射和集合类型  本章主题 z 映射类型: 字典 z 操作符 z 内建函数 z 内建方法 z 字典的键 z 集合类型 z 操作符 z 内建函数 z 内建方法 z 相关模块 Edit By Vheavens  Edit By Vheavens  本章中,我们来讨论 Python 语言中的映射类型和集合类型。和前面的章节一样,我们首先做一 个介绍,然后在来讨论可用操作符,工厂函数、内建函数(BIF)和方法。然后我们再来看看每种数据 类型的详细用法。 7.1 映射类型:字典 字典是 Python 语言中唯一的映射类型。映射类型对象里哈希值(键) 和指向的对象(值)是一对 多的关系。 它们与 Perl 中的哈希类型(译者注:又称关联数组)相似,通常被认为是可变的哈希表。 一个字典对象是可变的,它是一个容器类型,能存储任意个数的 Python 对象,其中也包括其他容器 类型。字典类型和序列类型容器类(列表、元组)的区别是存储和访问数据的方式不同。序列类型只 用数字类型的键(从序列的开始起按数值顺序索引)。映射类型可以用其他对象类型做键;一般最常 见的是用字符串做键(keys)。和序列类型的键不同,映射类型的键(keys)直接,或间接地和存储的 数据值相关联。但因为在映射类型中,我们不再用"序列化排序"的键(keys),所以映射类型中的数据 是无序排列的。 显然,这并不影响我们使用映射类型,因为映射类型不要求用数字值做索引以从一个容器中获 取对应的数据项。你可以用键(key)直接 "映射" 到值, 这就是为什么叫映射类型(“mapping type”) 的原因。映射类型通常被称做哈希表的原因是字典对象就是哈希类型的。字典是 Python 中最强大的 数据类型之一。 核心笔记:什么是哈希表?它们与字典的关系是什么? 序列类型用有序的数字键做索引将数据以数组的形式存储。一般,索引值与所存储的数据毫无 关系。还可以用另一种方式来存储数据:基于某种相关值,比如说一个字符串。我们在日常生活中 Edit By Vheavens  Edit By Vheavens  一直这么做。你把人们的电话号码按照他们的姓记录在电话簿上,你按照时间在日历或约会簿上添 加事件,等等。在这些例子中,你的键(key)就是和数据项相关的值。 哈希表是一种数据结构:它按照我们所要求的去工作。哈希表中存储的每一条数据,叫做一个 值(value),是根据与它相关的一个被称作为键(key)的数据项进行存储的。键和值合在一起被称为 “键-值 对”(key-value pairs)。 哈希表的算法是获取键,对键执行一个叫做哈希函数的操作, 并根据计算的结果,选择在数据结构的某个地址中来存储你的值。任何一个值存储的地址皆取决于 它的键。正因为这种随意性,哈希表中的值是没有顺序的。你拥有的是一个无序的数据集。 你所能获得的有序集合只能是字典中的键的集合或者值的集合。方法 Keys() 或 values() 返回 一个列表,该列表是可排序的。 你还可以用 items()方法得到包含键、值对的元组的列表来排序。 由于字典本身是哈希的,所以是无序的。 哈希表一般有很好的性能, 因为用键查询相当快。 Python 的字典是作为可变的哈希表实现的。如果你熟悉 Perl 的话, 就可以发现字典与 Perl 中 的"关系数组"或散列相似。 现在我们就来研究 Python 字典。一个字典条目的语法格式是 键:值。 而且,多条字典条目 被包含在( { } ) 里。 如何创建字典和给字典赋值 创建字典只需要把字典赋值给一个变量,不管这个字典是否包含元素: >>> dict1 = {} >>> dict2 = {'name': 'earth', 'port': 80} >>> dict1, dict2 ({}, {'port': 80, 'name': 'earth'}) 从 Python 2.2版本起, 可以用工厂方法 dict() 来创建字典。 当我们详细讨论 dict()的时候 会看到更多的例子,现在来看一个小例子: >>> fdict = dict((['x', 1], ['y', 2])) >>> fdict {'y': 2, 'x': 1} 从 Python 2.3 版本起, 可以用一个很方便的内建方法 fromkeys() 来创建一个"默认"字典, 字 典中元素具有相同的值 (如果没有给出, 默认为 None): >>> ddict = {}.fromkeys(('x', 'y'), -1) >>> ddict {'y': -1, 'x': -1} Edit By Vheavens  Edit By Vheavens  >>> >>> edict = {}.fromkeys(('foo', 'bar')) >>> edict {'foo': None, 'bar': None} 如何访问字典中的值 要想遍历一个字典(一般用键), 你只需要循环查看它的键, 像这样: >>> dict2 = {'name': 'earth', 'port': 80} >>> >>>> for key in dict2.keys(): ... print 'key=%s, value=%s' % (key, dict2[key]) ... key=name, value=earth key=port, value=80 从 Python 2.2 开始, 你可以不必再用 keys()方法获取供循环使用的键值列表了。 可以 用迭代器来轻松地访问类序列对象(sequence-like objects),比如字典和文件。只需要用字 典的名字就可以在 for 循环里遍历字典。 >>> dict2 = {'name': 'earth', 'port': 80} >>> >>>> for key in dict2: ... print 'key=%s, value=%s' % (key, dict2[key]) ... key=name, value=earth key=port, value=80 要得到字典中某个元素的值, 可以用你所熟悉的字典键加上中括号来得到: >>> dict2['name'] 'earth' >>> >>> print 'host %s is running on port %d' % \ ... (dict2['name'], dict2['port']) host earth is running on port 80 字典 dict1 是空的,字典 dict2 有两个数据元素。字典 dict2 的键是 'name' 和 'port',它们 对应的值分别是'earth' 和 80。就像你看到的,通过键'name'可以得到字典中的元素的值。 Edit By Vheavens  Edit By Vheavens  如果我们想访问该字典中的一个数据元素,而它在这个字典中没有对应的键,将会产生一个错 误: >>> dict2['server'] Traceback (innermost last): File "", line 1, in ? KeyError: server 在这个例子中,我们试图获得字典中'server'键所对应的值。你从上面的代码知道,'server' 这个键并不存在。检查一个字典中是否有某个键的最好方法是用字典的 has_key()方法, 或者另一 种比较好的方法就是从 2.2 版本起用的,in 或 not in 操作符。 has_key() 方法将会在未来的 Python 版本中弃用,所以用 in 或 not in 是最好的方法。 下面我们将介绍字典所有的方法。方法 has_key()和 in 以及 not in 操作符都是布尔类型的。 对于前两者而言,如果字典中有该键就返回真(True),否则返回假(False)。(Python 2.3版本以前, 没有布尔常量,为真时返回 1,假时返回 0。) >>> 'server' in dict2 # 或 dict2.has_key('server') False >>> 'name' in dict # 或 dict2.has_key('name') True >>> dict2['name'] 'earth' 一个字典中混用数字和字符串的例子: >>> dict3 = {} >>> dict3[1] = 'abc' >>> dict3['1'] = 3.14159 >>> dict3[3.2] = 'xyz' >>> dict3 {3.2: 'xyz', 1: 'abc', '1': 3.14159} 除了逐一地添加每个键-值对外,我们也可以给 dict3 整体赋值。 dict3 = {3.2: 'xyz', 1: 'abc', '1': 3.14159} 如果事先已经知道所有的数据就可以用键-值对来创建一个字典(这是显而易见的)。通过字典 dict3 的示例说明你可以采用各种类型的数据作为字典的键。如果我们被问到是否可以改变某个字典 Edit By Vheavens  Edit By Vheavens  值的键(key) 时,你可能会说,“不”,对吗? 为什么在执行中字典中的键不允许被改变呢?你这样想就会明白: 比方说, 你创建了一个字 典,字典中包含一个元素(一个键和一个值)。可能是由于某个变量的改变导致键发生了改变。这时 候你如果用原来的键来取出字典里的数据,会得到 KeyError(因为键的值已经改变了),现在你没办 法从字典中获取该值了,因为键本身的值发生了变化。由于上面的原因,字典中的键必须是可哈希 的, 所以数字和字符串可以作为字典中的键, 但是列表和其他字典不行。(见 7.5.2 小节 字典的 键必须是可哈希的) 如何更新字典 你可以通过以下几种方式对一个字典做修改:添加一个新数据项或新元素(即,一个键-值对); 修改一个已存在的数据项;或删除一个已存在的数据项(下面有关于数据项删除操作的详细讲述). >>> dict2['name'] = 'venus' # 更新已有条目 >>> dict2['port'] = 6969 # 更新已有条目 >>> dict2['arch'] = 'sunos5'# 增加新条目 >>> >>> print 'host %(name)s is running on port %(port)d' %dict2 host venus is running on port 6969 如果字典中该键已经存在,则字典中该键对应的值将被新值替代。上面的 print 语句展示了另 一种在字典中使用字符串格式符( %)的方法。用字典参数可以简化 print 语句,因为这样做你只须 用到一次该字典的名字,而不用在每个元素出现的时候都用元组参数表示。 你也可以用内建方法 update()将整个字典的内容添加到另一个字典。我们将在 7.4 节介绍此 方法。 如何删除字典元素和字典 删除整个字典的操作不常见。通常,你删除字典中的单个元素或是清除整个字典的内容。但是, 如果你真想"删除"一个字典,用 del 语句 (介绍见小节 3.5.5)。 以下是删除字典和字典元素的例 子。 del dict2['name'] # 删除键为“name”的条目 dict2.clear() # 删除 dict2 中所有的条目 del dict2 # 删除整个 dict2 字典 dict2.pop('name') # 删除并返回键为“name”的条目 Edit By Vheavens  Edit By Vheavens  核心笔记:避免使用内建对象名字作为变量的标识符 如果在 Python 2.3 前,你已经开始使用 Python,你可能用 dict 作为一个字典的标识符。但是, 因为 dict() 现在已成为 Python 的类型和工厂方法,重载 dict()会给你带来麻烦和潜在的 bugs。 编译器允许你做这样的重载,它认为你是聪明的,知道自己正在做什么!小心。请不要用 dict, list, file, bool, str, input, len 这样的内建类型为变量命名。 7.2 映射类型操作符 字典可以和所有的标准类型操作符一起工作,但却不支持像拼接(concatenation)和重复 (repetition)这样的操作。这些操作对序列有意义,可对映射类型行不通。在接下来的两小节里, 我们将向你讲述字典中的操作符。 7.2.1 标准类型操作符 标准类型操作符已在第四章介绍。 下面是一些使用操作符的简单示例: >>> dict4 = {'abc': 123} >>> dict5 = {'abc': 456} >>> dict6 = {'abc': 123, 98.6: 37} >>> dict7 = {'xyz': 123} >>> dict4 < dict5 True >>> (dict4 < dict6) and (dict4 < dict7) True >>> (dict5 < dict6) and (dict5 < dict7) True >>> dict6 < dict7 False 字典是如何比较的呢? 与列表和元组一样,这个过程比数字和字符串的比较更复杂些。详细 算法请见第 7.3.1 小节。 7.2.2 映射类型操作符 字典的键查找操作符([ ]) 键查找操作符是唯一仅用于字典类型的操作符,它和序列类型里单一元素的切片(slice)操作符 很相象。对序列类型来说,用索引做唯一参数或下标(subscript)以获取一个序列中某个元素的值。 Edit By Vheavens  Edit By Vheavens  对字典类型来说,是用键(key)查询(字典中的元素),所以键是参数(argument), 而不是一个索引 (index)。键查找操作符既可以用于给字典赋值,也可以用于从字典中取值: d[k] = v 通过键'k',给字典中某元素赋值'v' d[k] 通过键'k',查询字典中某元素的值 (键)成员关系操作( in ,not in) 从 Python 2.2 起,程序员可以不用 has_key()方法,而用 in 和 not in 操作符来检查某个键 是否存在于字典中: >>> 'name' in dict2 True >>> 'phone' in dict2 False 7.3 映射类型的内建函数和工厂函数 7.3.1 标准类型函数[type()、str()和 cmp()] 如你所料,对一个字典调用 type()工厂方法,会返回字典类型, “”. 调用 str()工厂方法将返回该字典的字符串表示形式,这些都很容易理解。 在前面的三个章节里,我们已经讲述了用 cmp() 内建函数来操作数字、字符串、列表和元组。 那么字典又是如何比较的呢? 字典是通过这样的算法来比较的: 首先是字典的大小,然后是键,最 后是值。可是,用 cmp() 做字典的比较一般不是很有用。 接下来的小节里,将进一步详细说明字典比较的算法,但这部分是高层次的阅读内容,可以跳 过,因为字典的比较不是很有用也不常见。 *字典比较算法 接下来的例子中,我们建立两个字典进行比较,然后慢慢修改,来看看这些修改对它们之间的 比较带来的影响: >>> dict1 = {} >>> dict2 = {'host': 'earth', 'port': 80} >>> cmp(dict1, dict2) -1 Edit By Vheavens  Edit By Vheavens  >>> dict1['host'] = 'earth' >>> cmp(dict1, dict2) -1 在第一个比较中,dict1 比 dict2 小,因为 dict2 有更多元素(2 个 vs. 0 个)。在向dict1 添 加一个元素后,dict1 仍然比 dict2 小 (2 vs. 1),虽然添加的元素在 dict2 中也存在。 >>> dict1['port'] = 8080 >>> cmp(dict1, dict2) 1 >>> dict1['port'] = 80 >>> cmp(dict1, dict2) 0 在向 dict1 中添加第二个元素后,两个字典的长度相同,所以用键比较大小。这时键相等,则 通过它们的值比较大小。键 'host'的值相同,对于键 'port',dict1中值比 dict2 中的值大(8080 vs. 80)。当把 dict2 中'port'的值设成和 dict1 中的值一样,那么两个字典相等:它们有相同的大小、 相同的键、相同的值,所以 cmp() 返回值是 0。 {译者注: 原文有误:dict2 is deemed larger because its value is greater than that of dict1’s 'port' key (8080 vs. 80). 应为: dict1 is deemed larger because its value is greater than that of dict2’s 'port' key (8080 vs. 80). } >>> dict1['prot'] = 'tcp' >>> cmp(dict1, dict2) 1 >>> dict2['prot'] = 'udp' >>> cmp(dict1, dict2) -1 当向两个字典中的任何一个添加新元素时,这个字典马上会成为大的那个字典,就像例子中的 dict1 一样。向 dict2 添加键-值对后,因为两个字典的长度又相等了,会继续比较它们的键和值。 >>> cdict = {'fruits':1} >>> ddict = {'fruits':1} >>> cmp(cdict, ddict) 0 >>> cdict['oranges'] = 0 >>> ddict['apples'] = 0 >>> cmp(cdict, ddict) 14 Edit By Vheavens  Edit By Vheavens  上面的例子表明 cmp()可以返回除-1,0,1 外的其他值。算法按照以下的顺序。 (1)比较字典长度 如果字典的长度不同,那么用 cmp(dict1, dict2) 比较大小时,如果字典 dict1 比 dict2 长, cmp()返回正值,如果 dict2 比 dict1 长,则返回负值。也就是说,字典中的键的个数越多,这个 字典就越大,即: len(dict1) > len(dict2) ==> dict1 > dict2 (2)比较字典的键 如果两个字典的长度相同,那就按字典的键比较;键比较的顺序和 keys()方法返回键的顺序相 同。 (注意: 相同的键会映射到哈希表的同一位置,这保证了对字典键的检查的一致性。) 这时, 如果两个字典的键不匹配时,对这两个(不匹配的键)直接进行比较。当 dict1 中第一个不同的键大 于 dict2 中第一个不同的键,cmp()会返回正值。 (3)比较字典的值 如果两个字典的长度相同而且它们的键也完全匹配,则用字典中每个相同的键所对应的值进行 比较。一旦出现不匹配的值,就对这两个值进行直接比较。若 dict1 比 dict2 中相同的键所对应的 值大,cmp()会返回正值。 (4) Exact Match 到此为止,即,每个字典有相同的长度、相同的键、每个键也对应相同的值,则字典完全匹配, 返回 0 值。 图 7-1 说明了上述字典比较的算法 Edit By Vheavens  Edit By Vheavens  图 7-1 字典是如何进行比较的 7.3.2 映射类型相关的函数 dict() 工厂函数被用来创建字典。如果不提供参数,会生成空字典。当容器类型对象做为一个参数传 递给方法 dict() 时很有意思。如果参数是可以迭代的,即,一个序列,或是一个迭代器,或是一个 支持迭代的对象,那每个可迭代的元素必须成对出现。在每个值对中,第一个元素是字典的键、第 二个元素是字典中的值。见 Python 文档里关于 dict()的例子: >>> dict(zip(('x', 'y'), (1, 2))) {'y': 2, 'x': 1} >>> dict([['x', 1], ['y', 2]]) {'y': 2, 'x': 1} >>> dict([('xy'[i-1], i) for i in range(1,3)]) {'y': 2, 'x': 1} 如果输入参数是(另)一个映射对象,比如,一个字典对象,对其调用 dict()会从存在的字典里 复制内容来生成新的字典。新生成的字典是原来字典对象的浅复制版本, 它与用字典的内建方法 copy() 生成的字典对象是一样的。但是从已存在的字典生成新的字典速度比用 copy()方法慢,我们 推荐使用 copy()。 从 Python 2.3 开始,调用dict()方法可以接受字典或关键字参数字典(函数操作符,第 11 章): >>> dict(x=1, y=2) {'y': 2, 'x': 1} >>> dict8 = dict(x=1, y=2) >>> dict8 {'y': 2, 'x': 1} >>> dict9 = dict(**dict8) >>> dict9 {'y': 2, 'x': 1} 我们提醒读者 dict9 的例子只作为了解 dict()方法的用途,它不是现实中的例子。使用下面这 些行的方法更聪明(效率更好): >>> dict9 = dict8.copy() Edit By Vheavens  Edit By Vheavens  >>> dict9 {'y': 2, 'x': 1} len() 内建函数 len()很灵活。它可用在序列、映射类型和集合上(在本章的后面我们会看到)。对字典 调用 len(),它会返回所有元素(键-值对)的数目: >>> dict2 = {'name': 'earth', 'port': 80} >>> dict2 {'port': 80, 'name': 'earth'} >>> len(dict2) 2 我们前面提到字典中的元素是没有顺序的。从上面的例子中可以看到,dict2 的元素显示的顺序 和输入时的顺序正相反。 hash() 内建函数 hash()本身并不是为字典设计的方法,但它可以判断某个对象是否可以做一个字典的 键。将一个对象作为参数传递给 hash(), 会返回这个对象的哈希值。 只有这个对象是可哈希的, 才可作为字典的键 (函数的返回值是整数,不产生错误或异常)。 如果用比较操作符来比较两个数值,发现它们是相等的,那么即使二者的数据类型不同, 它 们也会得到相同的哈希值。 如果非可哈希类型作为参数传递给 hash()方法,会产生 TypeError 错误(因此,如果使用这样的 对象作为键给字典赋值时会出错): >>> hash([]) Traceback (innermost last): File "", line 1, in ? TypeError: list objects are unhashable >>> >>> dict2[{}] = 'foo' Traceback (most recent call last): File "", line 1, in ? TypeError: dict objects are unhashable 在表 7.1 中,我们列出以下三个映射类型的相关函数。 表 7.1 映射类型的相关函数 Edit By Vheavens  Edit By Vheavens  函数 操作 dict([container]) 创建字典的工厂函数。如果提供了容器类(container) , 就 用其中的条目填充字典,否则就创建一个空字典。 len(mapping) 返回映射的长度(键-值对的个数) hash(obj) 返回 obj 的哈希值 7.4 映射类型内建方法 字典提供了大量方法来帮你做事情,见表 7.2. 下面,我们说明字典的一些很常见的方法。在上面的例子里,我们已经看到 has_key() 和它的 替代方法 in 和 not in。如我们在 7.1 小节看到,试图查找一个字典里没有的键值会产生 KeyError 异常。 基本的字典方法关注他们的键和值。它们有:keys()方法,返回一个列表,包含字典中所有的 键,values()方法,返回一个列表,包含字典中所有的值,items(), 返回一个包含所有(键, 值)元 组的列表。这些方法在不按任何顺序遍历字典的键或值时很有用。 >>> dict2.keys() ['port', 'name'] >>> >>> dict2.values() [80, 'earth'] >>> >>> dict2.items() [('port', 80), ('name', 'earth')] >>> >>> for eachKey in dict2.keys(): ... print 'dict2 key', eachKey, 'has value', dict2[eachKey] ... dict2 key port has value 80 dict2 key name has value earth keys()方法很有用,它返回一个包含字典中所有键的列表,此方法可以与 for 循环一起使用来 获取字典中的值。 表 7.2 字典类型方法 方法名字 操作 dict.cleara() 删除字典中所有元素 dict.copya() 返回字典(浅复制)的一个副本 dict.fromkeysc(seq, Edit By Vheavens  Edit By Vheavens  val=None) c 创建并返回一个新字典,以 seq 中的元素做该字典的键,val 做该字 典中所有键对应的初始值(如果不提供此值,则默认为 None) dict.get(key, default=None)a 对字典 dict 中的键 key,返回它对应的值 value,如果字典中不存在此 键,则返回 default 的值(注意,参数 default 的默认值为 None) dict.has_key(key) 如果键(key)在字典中存在,返回True,否则返回False. 在 Python2.2 版本引入 in 和 not in 后,此方法几乎已废弃不用了,但仍提供一个 可工作的接口。 dict.items() 返回一个包含字典中(键, 值)对元组的列表 dict.keys() 返回一个包含字典中键的列表 dict.iter()d 方法 iteritems(), iterkeys(), itervalues()与它们对应的非迭代方法 一样,不同的是它们返回一个迭代子,而不是一个列表。 dict.popc(key [, default]) c 和方法 get()相似,如果字典中 key 键存在,删除并返回 dict[key], 如果 key 键不存在,且没有给出 default 的值,引发 KeyError 异常。 dict.setdefault(key, default=None)e 和方法 set()相似,如果字典中不存在 key 键,由 dict[key]=default 为 它赋值。 dict.update(dict2)a 将字典 dict2 的键-值对添加到字典 dict dict.values() 返回一个包含字典中所有值的列表 a. Python 1.5 新增 b. 关于深复制和浅复制的详细信息请参见 6.19 节. c. Python 2.3 新增 d. Python 2.2 新增 e. Python 2.0 新增 但是,它返回的元素是没有顺序的(和哈希表中的键(keys)一样),我们通常希望它们能按某种 方式排序。 在 Python 2.4 版本以前,你只能调用字典的keys()方法获得键的列表,然后调用列表的sort() 方法得到一个有序可遍历的列表。现在特别为迭代子设计了一个名为 sorted()的内建函数,它返回 一个有序的迭代子: >>> for eachKey in sorted(dict2): ... print 'dict2 key', eachKey, 'has value', dict2[eachKey] ... dict2 key name has value earth dict2 key port has value 80 Edit By Vheavens  Edit By Vheavens  update()方法可以用来将一个字典的内容添加到另外一个字典中。字典中原有的键如果与新添 加的键重复,那么重复键所对应的原有条目的值将被新键所对应的值所覆盖。原来不存在的条目则 被添加到字典中。clear()方法可以用来删除字典中的所有的条目。 >>> dict2= {'host':'earth', 'port':80} >>> dict3= {'host':'venus', 'server':'http'} >>> dict2.update(dict3) >>> dict2 {'server': 'http', 'port': 80, 'host': 'venus'} >>> dict3.clear() >>> dict3 {} copy() 方法返回一个字典的副本。注意这只是浅复制。关于浅复制和深复制请阅读小节 6.19。 最后要说明,get()方法和键查找(key-lookup)操作符( [ ] )相似,不同的是它允许你为不存在的 键提供默认值。如果该键不存在,也未给出它的默认值,则返回 None。此方法比采用键查找 (key-lookup)更灵活,因为你不必担心因键不存在而引发异常。 >>> dict4 = dict2.copy() >>> dict4 {'server': 'http', 'port': 80, 'host': 'venus'} >>> dict4.get('host') 'venus' >>> dict4.get('xxx') >>> type(dict4.get('xxx')) >>> dict4.get('xxx', 'no such key') 'no such key' setdefault()是自 2.0 才有的内建方法, 使得代码更加简洁,它实现了常用的语法: 检查字典 中是否含有某键。 如果字典中这个键存在,你可以取到它的值。 如果所找的键在字典中不存在, 你可以给这个键赋默认值并返回此值。这正是执行 setdefault()方法的目的: >>> myDict = {'host': 'earth', 'port': 80} >>> myDict.keys() ['host', 'port'] >>> myDict.items() [('host', 'earth'), ('port', 80)] >>> myDict.setdefault('port', 8080) 80 Edit By Vheavens  Edit By Vheavens  >>> myDict.setdefault('prot', 'tcp') 'tcp' >>> myDict.items() [('prot', 'tcp'), ('host', 'earth'), ('port', 80)] 前面,我们曾简要介绍过 fromkeys()方法,下面是更多的示例: >>> {}.fromkeys('xyz') {'y': None, 'x': None, 'z': None} >>> >>> {}.fromkeys(('love', 'honor'), True) {'love': True, 'honor': True} 目前,keys(), items(), 和 values()方法的返回值都是列表。数据集如果很大会导致很难 处理,这也正是 iteritems(), iterkeys(), 和 itervalues() 方法被添加到 Python 2.2 的主要原 因。这些函数与返回列表的对应方法相似,只是它们返回惰性赋值的迭代器,所以节省内存。未来 的 Python 版本中,甚至会更灵活,那时这些方法将会返回强大的对象,暂叫做视图(views)。视图 (views)是访问容器对象的接口集。举例来说,你可以从一个视图(views)中删除某个字典的键,从 而改变某个字典。 7.5 字典的键 字典中的值没有任何限制。 他们可以是任意 Python 对象,即,从标准对象到用户自定义对象 皆可。但是字典中的键是有类型限制的。 7.5.1 不允许一个键对应多个值 你必须明确一条原则:每个键只能对应一个项。也就是说,一键对应多个值是不允许的。(像列 表、元组和其他字典这样的容器对象是可以的。) 当有键发生冲突(即,字典键重复赋值),取最后(最 近)的赋值。 >>> dict1 = {' foo':789, 'foo': 'xyz'} >>> dict1 {'foo': 'xyz'} >>> >>> dict1['foo'] = 123 >>> dict1 {'foo': 123} Edit By Vheavens  Edit By Vheavens  Python 并不会因字典中的键存在冲突而产生一个错误,它不会检查键的冲突是因为,如果真这 样做的话,在每个键-值对赋值的时候都会做检查,这将会占用一定量的内存。在上面的例子里,键 'foo'被列出两次,Python 从左到右检查键-值对。首先值 789 被赋值(给键'foo'所对应的值),然后 又很快被字符串'xyz'替代。当给字典中一个不存在的键赋值时,键和值会被创建和添加,但如果该 键已经存在(键冲突),那此键所对应的值将被替换。上面例子中,键 'foo' 所对应的值被替换了两 次;最后的赋值语句,值 123 代替了值'xyz'。 7.5.2 键必须是可哈希的 我们在小节 7.1 说过,大多数 Python 对象可以作为键;但它们必须是可哈希的对象。像列表和 字典这样的可变类型,由于它们不是可哈希的,所以不能作为键。 所有不可变的类型都是可哈希的,因此它们都可以做为字典的键。一个要说明的是问题是数字: 值相等的数字表示相同的键。换句话来说,整型数字 1 和 浮点数 1.0 的哈希值是相同的,即它们 是相同的键。 同时,也有一些可变对象(很少)是可哈希的,它们可以做字典的键,但很少见。举一个例子, 一个实现了__hash__() 特殊方法的类。因为__hash__()方法返回一个整数,所以仍然是用不可变 的值(做字典的键)。 为什么键必须是可哈希的?解释器调用哈希函数,根据字典中键的值来计算存储你的数据的位 置。如果键是可变对象,它的值可改变。如果键发生变化,哈希函数会映射到不同的地址来存储数 据。如果这样的情况发生,哈希函数就不可能可靠地存储或获取相关的数据。选择可哈希的键的原 因就是因为它们的值不能改变。(此问题在 Python FAQ 中也能找到答案) 我们知道数字和字符串可以被用做字典的键,但元组又怎么样呢?我们知道元组是不可变的, 但在小节 6.17.2, 我们提示过它们也可能不是一成不变的。用元组做有效的键,必须要加限制:元 组中只包括像数字和字符串这样的不可变参数,才可以作为字典中有效的键。 我们用一个程序(userpw.py 例 7.1), 来为本章关于字典的讲述做个小结。这个程序是用于管 理用户名和密码的模拟登录数据系统。脚本接受新用户的信息: 示例 7.1 字典示例(userpw.py) 这个程序管理用于登录系统的用户信息:登录名字和密码。登录用户帐号建立后,已存在用户 可以用登录名字和密码重返系统。新用户不能用别人的登录名建立用户帐号。 1 #!/usr/bin/env python 2 Edit By Vheavens  Edit By Vheavens  3 db = {} 4 5 def newuser(): 6 prompt = 'login desired: ' 7 while True: 8 name = raw_input(prompt) 9 if db.has_key(name): 10 prompt = 'name taken, try another: ' 11 continue 12 else: 13 break 14 pwd = raw_input('passwd: ') 15 db[name] = pwd 16 17 def olduser(): 18 name = raw_input('login: ') 19 pwd = raw_input('passwd: ') 20 passwd = db.get(name) 21 if passwd == pwd: 22 print 'welcome back', name 23 else: 24 print 'login incorrect' 25 26 def showmenu(): 27 prompt = """ 28 (N)ew User Login 29 (E)xisting User Login 30 (Q)uit Example 7.1 Dictionary Example (userpw.py) (continued) 31 32 Enter choice: """ 33 34 done = False 35 while not done: 36 37 chosen = False 38 while not chosen: 39 try: Edit By Vheavens  Edit By Vheavens  40 choice = raw_input(prompt).strip()[0].lower() 41 except (EOFError, KeyboardInterrupt): 42 choice = 'q' 43 print '\nYou picked: [%s]' % choice 44 if choice not in 'neq': 45 print 'invalid option, try again' 46 else: 47 chosen = True 49 done = True 50 newuser() 51 olduser() 52 53 if __name__ == '__main__': 54 showmenu() 他们提供登录名和密码。帐号建立后,已存在用户可用登录名和正确的密码重返系统。新用户 不能用别人的登录名建立帐号。 逐行解释 Lines 1–3 在 Unix 初始行后,我们用一个空用户数据库初始化程序。 因为我们没有把数据存储在任何地 方,每次程序执行时都会新建一个用户数据库。 Lines 5–15 newuser() 函数用来建立新用户。它检查名字是否已经存在,如果证实是一个新名字,将要求 用户输入他或她的密码 (我们这个简单的程序没有加密),用户的密码被存储在字典里,以他们的名 字做字典中的键。 Lines 17–24 olduser()函数处理返回的用户。如果用户用正确的用户名和密码登录,打出欢迎信息。否则通 知用户是无效登录并返回菜单。我们不会采用一个无限循环来提示用户输入正确的密码,因为用户 可能会无意进入错误的菜单选项。 Lines 26–51 真正控制这个脚本的是 showmenu()函数,它显示给用户一个友好界面。提示信息被包括在三引 号里("""), 这样做是因为提示信息跨多行,而且比单行包含'\n'符号的字符串更容易处理。菜单显 示后,它等待用户的有效输入,然后根据菜单选项选择操作方式。try-expect 语句和上章 stack.py queue.py 例子里的一样(见 小节 6.14.1). Edit By Vheavens  Edit By Vheavens  Lines 53–54 如果这个脚本被直接执行(不是通过 import 方式),这行代码会调用 showmenu()函数运行程序。 这是我们的脚本运行结果: $ userpw.py (N)ew User Login (E)xisting User Login (Q)uit Enter choice: n You picked: [n] login desired: king arthur passwd: grail (N)ew User Login (E)xisting User Login (Q)uit Enter choice: e You picked: [e] login: sir knight passwd: flesh wound login incorrect (N)ew User Login (E)xisting User Login (Q)uit Enter choice: e You picked: [e] login: king arthur passwd: grail welcome back king Arthur (N)ew User Login (E)xisting User Login (Q)uit Edit By Vheavens  Edit By Vheavens  Enter choice: ^D You picked: [q] 7.6 集合类型 数学上, 把 set 称做由不同的元素组成的集合,集合(set)的成员通常被称做集合元素(set elements)。Python把这个概念引入到它的集合类型对象里。集合对象是一组无序排列的可哈希的值。 是的,集合成员可以做字典中的键。数学集合转为Python 的集合对象很有效,集合关系测试和union、 intersection 等操作符在 Python 里也同样如我们所预想地那样工作。 和其他容器类型一样,集合支持用 in 和 not in 操作符检查成员, 由 len() 内建函数得到集 合的基数(大小), 用 for 循环迭代集合的成员。但是因为集合本身是无序的,你不可以为集合创建 索引或执行切片(slice)操作,也没有键(keys)可用来获取集合中元素的值。 集合(sets)有两种不同的类型,可变集合(set) 和 不可变集合(frozenset)。如你所想,对可 变集合(set),你可以添加和删除元素,对 不可变集合(frozenset)则不允许这样做。请注意,可变 集合(set)不是可哈希的,因此既不能用做字典的键也不能做其他集合中的元素。不可变集合 (frozenset)则正好相反,即,他们有哈希值,能被用做字典的键或是作为集合中的一个成员。 集合(Sets)最早出现在 Python2.3 版本中,通过集合(sets)模块来创建,并通过 ImmutableSet 类和 Set 类进行访问。而后来,大家都认为把它们作为内建的数据类型是个更好的主意,因此这些 类被用 C 重写改进后包含进 Python2.4。关于集合类型和这些类改进的更多内容,可阅读此文获得详 情:PEP 218,链接地址: http://python.org/peps/pep-0218.html. 虽然现在集合类型已经是 Python 的基本数据类型了,但它经常会以用户自定义类的形式出现在 各种 Python 程序中,就像复数一样(复数从 Python1.4 版本起成为 python 的一个数据类型),这样 重复的劳动已数不胜数了。在现在的 Python 版本之前,(即使集合类型对许多人的程序来说并不是 最理想的数据结构,)许多人仍然试图给列表和字典这样的 Python 标准类型添加集合功能,这样可 以把它们作为真正集合类型的代理来使用。因此现在的使用者有包括“真正”集合类型在内的多种 选择。 在我们详细讲述 Python 的集合对象之前,我们必须理解 Python 中的一些数学符号 (见表 7.3), 这样对术语和功能有一个清晰的了解。 表 7.3 集合操作符和关系符号 Edit By Vheavens  Edit By Vheavens  数学符号 Python 符号 说明 7.6 集合类型 如何创建集合类型和给集合赋值 集合与列表( [ ] )和字典( { } ) 不同,没有特别的语法格式。列表和字典可以分别用他们自 己的工厂方法 list() 和 dict() 创建,这也是集合被创建的唯一方法 - 用集合的工厂方法 set() 和 frozenset(): >>> s = set('cheeseshop') >>> s set(['c', 'e', 'h', 'o', 'p', 's']) >>> t = frozenset('bookshop') >>> t frozenset(['b', 'h', 'k', 'o', 'p', 's']) >>> type(s) >>> type(t) >>> len(s) 6 >>> len(s) == len(t) Edit By Vheavens  Edit By Vheavens  True >>> s == t False 如何访问集合中的值 你可以遍历查看集合成员或检查某项元素是否是一个集合中的成员: >>> 'k' in s False >>> 'k' in t True >>> 'c' not in t True >>> for i in s: ... print i ... c e h o p s 如何更新集合 用各种集合内建的方法和操作符添加和删除集合的成员: >>> s.add('z') >>> s set(['c', 'e', 'h', 'o', 'p', 's', 'z']) >>> s.update('pypi') >>> s set(['c', 'e', 'i', 'h', 'o', 'p', 's', 'y', 'z']) >>> s.remove('z') >>> s set(['c', 'e', 'i', 'h', 'o', 'p', 's', 'y']) >>> s -= set('pypi') >>> s Edit By Vheavens  Edit By Vheavens  set(['c', 'e', 'h', 'o', 's']) 我们之前提到过,只有可变集合能被修改。试图修改不可变集合会引发异常。 >>> t.add('z') Traceback (most recent call last): File "", line 1, in ? AttributeError: 'frozenset' object has no attribute 'add' 如何删除集合中的成员和集合 前面我们看到如何删除集合成员。如果如何删除集合本身,可以像删除任何 Python 对象一样, 令集合超出它的作用范围,或调用del 将他们直接清除出当前的名字空间。如果它的引用计数为零, 也会被标记以便被垃圾回收。 >>> del s >>> 7.7 集合类型操作符 7.7.1 标准类型操作符(所有的集合类型) 成员关系 (in, not in) 就序列而言,Python 中的 in 和 not in 操作符决定某个元素是否是一个集合中的成员。 >>> s = set('cheeseshop') >>> t = frozenset('bookshop') >>> 'k' in s False >>> 'k' in t True >>> 'c' not in t True 集合等价/不等价 Edit By Vheavens  Edit By Vheavens  等价/不等价被用于在相同或不同的集合之间做比较。两个集合相等是指,对每个集合而言,当 且仅当其中一个集合中的每个成员同时也是另一个集合中的成员。 你也可以说每个集合必须是另一个集合的一个子集, 即,s <= t 和 s >= t 的值均为真(True), 或(s <= t and s>= t) 的值为真(True)。集合等价/不等价与集合的类型或集合成员的顺序无关, 只与集合的元素有关。 >>> s == t False >>> s != t True >>> u = frozenset(s) >>> s == u True >>> set('posh') == set('shop') True 子集/超集 Sets 用 Python 的比较操作符检查某集合是否是其他集合的超集或子集。“小于”符号( <, <= ) 用来判断子集,“大于”符号( >, >= )用来判断超集。 “小于” 和 “大于”意味着两个集合在比较时不能相等。等于号允许非严格定义的子集和超 集。 Sets 支持严格( < )子集和非严格 ( <= ) 子集, 也支持严格( > )超集和非严格 ( >= ) 超集。只有当第一个集合是第二个集合的严格子集时,我们才称第一个集合“小于”第二个集合, 同理,只有当第一个集合是第二个集合的严格超集时,我们才称第一个集合“大于”第二个集合。 >>> set('shop') < set('cheeseshop') True >>> set('bookshop') >= set('shop') True 7.7.2 集合类型操作符(所有的集合类型) 联合( | ) 联合(union)操作和集合的 OR(又称可兼析取(inclusive disjunction))其实是等价的,两个集 合的联合是一个新集合,该集合中的每个元素都至少是其中一个集合的成员,即,属于两个集合其 中之一的成员。联合符号有一个等价的方法,union(). Edit By Vheavens  Edit By Vheavens  >>> s | t set(['c', 'b', 'e', 'h', 'k', 'o', 'p', 's']) 交集( & ) 你可以把交集操作比做集合的 AND(或合取)操作。两个集合的交集是一个新集合,该集合中的每 个元素同时是两个集合中的成员,即,属于两个集合的成员。交集符号有一个等价的方法, intersection(). >>> s & t set(['h', 's', 'o', 'p'] 差补/相对补集( – ) 两个集合(s 和 t)的差补或相对补集是指一个集合 C,该集合中的元素,只属于集合 s,而不属 于集合 t。差符号有一个等价的方法,difference(). >>> s - t set(['c', 'e']) 对称差分( ^ ) 和其他的布尔集合操作相似,对称差分是集合的XOR(又称”异或“ (exclusive disjunction)). 两个集合(s 和 t)的对称差分是指另外一个集合 C,该集合中的元素,只能是属于集合 s 或者集合 t 的成员,不能同时属于两个集合。对称差分有一个等价的方法,symmetric_difference(). >>> s ^ t set(['k', 'b', 'e', 'c']) 混合集合类型操作 上面的示例中,左边的 s 是可变集合,而右边的 t 是一个不可变集合. 注意上面使用集合操作 运算符所产生的仍然是可变集合,但是如果左右操作数的顺序反过来,结果就不一样了: >>> t | s frozenset(['c', 'b', 'e', 'h', 'k', 'o', 'p', 's']) >>> t ^ s frozenset(['c', 'b', 'e', 'k']) >>> t - s frozenset(['k', 'b']) 如果左右两个操作数的类型相同,既都是可变集合或不可变集合, 则所产生的结果类型是相同 的,但如果左右两个操作数的类型不相同(左操作数是 set,右操作数是 frozenset,或相反情况), Edit By Vheavens  Edit By Vheavens  则所产生的结果类型与左操作数的类型相同,上例中可以证明这一点。还要注意,加号不是集合类 型的运算符: >>> v = s + t Traceback (most recent call last): File "", line 1, in ? TypeError: unsupported operand type(s) for +: 'set' and 'set' >>> v = s | t >>> v set(['c', 'b', 'e', 'h', 'k', 'o', 'p', 's']) >>> len(v) 8 >>> s < v True 7.7.3 集合类型操作符(仅适用于可变集合) (Union) Update ( |= ) 这个更新方法从已存在的集合中添加(可能多个)成员,此方法和 update()等价. >>> s = set('cheeseshop') >>> u = frozenset(s) >>> s |= set('pypi') >>> s set(['c', 'e', 'i', 'h', 'o', 'p', 's', 'y']) 保留/交集更新( &= ) 保留(或交集更新)操作保留与其他集合的共有成员。此方法和 intersection_update()等价. >>> s = set(u) >>> s &= set('shop') >>> s set(['h', 's', 'o', 'p']) 差更新 ( –= ) 对集合 s 和 t 进行差更新操作 s-=t,差更新操作会返回一个集合,该集合中的成员是集合 s 去 除掉集合 t 中元素后剩余的元素。此方法和 difference_update()等价. Edit By Vheavens  Edit By Vheavens  >>> s = set(u) >>> s -= set('shop') >>> s set(['c', 'e']) 对称差分更新( ^= ) 对集合s和t进行对称差分更新操作(s^=t),对称差分更新操作会返回一个集合,该集合中的成 员仅是原集合 s 或仅是另一集合 t 中的成员。此方法和 symmetric_difference_update()等价. >>> s = set(u) >>> t = frozenset('bookshop') >>> s ^= t >>> s set(['c', 'b', 'e', 'k']) 7.8 内建函数 7.8.1 标准类型函数 len() 把集合作为参数传递给内建函数 len(),返回集合的基数(或元素的个数)。 >>> s = set(u) >>> s set(['p', 'c', 'e', 'h', 's', 'o']) >>> len(s) 6 7.8.2 集合类型工厂函数 set() and frozenset() set()和 frozenset()工厂函数分别用来生成可变和不可变的集合。如果不提供任何参数,默认 会生成空集合。如果提供一个参数,则该参数必须是可迭代的,即,一个序列,或迭代器,或支持 迭代的一个对象,例如:一个文件或一个字典。 Edit By Vheavens  Edit By Vheavens  >>> set() set([]) >>> set([]) set([]) >>> set(()) set([]) >>> set('shop') set(['h', 's', 'o', 'p']) >>> >>> frozenset(['foo', 'bar']) frozenset(['foo', 'bar']) >>> >>> f = open('numbers', 'w') >>> for i in range(5): ... f.write('%d\n' % i) ... >>> f.close() >>> f = open('numbers', 'r') >>> set(f) set(['0\n', '3\n', '1\n', '4\n', '2\n']) >>> f.close() 7.9 集合类型内建方法 7.9.1 方法(所有的集合方法) 我们已看到很多和内建方法等价的操作符,表 7.4 做了小结: 内建方法 copy() 没有等价的操作符。和同名的字典方法一样,copy()方法比用像 set(), frozenset(), 或 dict()这样的工厂方法复制对象的副本要快。 表 7.4 集合类型方法 方法名称 操作 s.issubset(t) 如果s是t的子集,则返回 True,否则返回 False s.issuperset(t) 如果t是s的超集,则返回 True,否则返回 False s.union(t) 返回一个新集合,该集合是s和t的并集 s.intersection(t) 返回一个新集合,该集合是s和t的交集 Edit By Vheavens  Edit By Vheavens  s.difference(t) 返回一个新集合,该集合是s 的成员,但不是 t 的成员 s.symmetric_difference(t) 返回一个新集合,该集合是 s 或 t 的成员,但不是s和t共有的 成员 s.copy() 返回一个新集合,它是集合s 的浅复制 7.9.2 方法(仅适用于可变集合) 表 7.5 总结了所有可变集合的内建方法,和上面的方法相似,我们已经看过许多和它们等价的 操作符。 新的方法有 add(), remove(), discard(), pop(), clear(). 这些接受对象的方法,参数必 须是可哈希的。 7.9.3 操作符和内建方法比较 像你看到的, 很多内建的方法几乎和操作符等价。我们说"几乎等价",意思是它们间是有一个 重要区别: 当用操作符时,操作符两边的操作数必须是集合。 在使用内建方法时,对象也可以是 迭代类型的。为什么要用这种方式来实现呢? Python 的文档里写明: 采用易懂的 set('abc').intersection('cbs') 可以避免用 set('abc') [and] 'cbs' 这样容易出错的构建方 法。 表 7.5 可变集合类型的方法 方法名 操作 s.update(t) 用 t 中的元素修改 s, 即,s 现在包含s或t的成员 s.intersection_update(t) s 中的成员是共同属于s和t的元素。 s.difference_update(t) s 中的成员是属于 s 但不包含在 t 中的元素 s.symmetric_difference_update(t) s 中的成员更新为那些包含在s或t中,但不 是 s 和 t 共有的元素 s.add(obj) 在集合 s 中添加对象 obj s.remove(obj) 从集合 s 中删除对象 obj;如果 obj 不是集合 s 中的元素(obj not in s),将引发 KeyError 错误 s.discard(obj) 如果 obj 是集合 s 中的元素,从集合 s 中删除对象 obj; s.pop() 删除集合 s 中的任意一个对象,并返回它 s.clear() 删除集合 s 中的所有元素 7.10 操作符、函数/方法 Edit By Vheavens  Edit By Vheavens  集合类型总结表 表 7.6 中,我们总结了所有的集合类型的操作符、函数和方法 7.11 相关模块 集合(set)模块从 2.3 版本引进,可继承 Set 或 ImmuteablSet 来生成子类。虽然从 Python2.4 起使用集合类型,但是集合模块不会弃用。 表 7.6 集合类型操作符、函数和方法 函数/方法名 等价运算符 说明 所有集合类型 len(s) 集合基数: 集合 s 中元素的个数 set([obj]) 可变集合工厂函数; obj必须是支持迭代的,由 obj 中 的元素创建集合,否则创建一个空集合 frozenset([obj]) 不可变集合工厂函数; 执行方式和 set()方法相同, 但它返回的是不可变集合 obj in s 成员测试:obj 是s 中的一个元素吗? obj not in s 非成员测试:obj 不是 s 中的一个元素吗? s == t 等价测试: 测试s和t是否具有相同的元素? s != t 不等价测试: 与==相反 s < t (严格意义上)子集测试; s != t 而且 s 中 所 有 的元素都是 t 的成员 s.issubset(t) s <= t 子集测试(允许不严格意义上的子集): s中所有的元素 都是 t 的成员 s > t (严格意义上)超集测试: s != t 而且 t 中所有的元素 都是 s 的成员 s.issuperset(t) s >= t 超集测试(允许不严格意义上的超集): t 中所有的元素 都是 s 的成员 s.union(t) s | t 合并操作:s 或 t 中的元素 s.intersec- tion(t) s & t 交集操作:s和 t 中的元素 s.difference(t) s - t 差分操作: s 中的元素,而不是 t 中的元素 s.symmetric_difference(t)s ^ t 对称差分操作:s 或 t 中的元素,但不是s和t共有 的元素 s.copy() 复制操作:返回s 的(浅复制)副本 Table 7.6 集合类型,函数和方法(继续) Edit By Vheavens  Edit By Vheavens  函数/方法名字 操作符 等价描述 仅用于可变集合 s.update(t) s |= t (Union) 修改操作: 将 t 中的成员添加 s s.intersection_update(t) s &= t 交集修改操作: s 中仅包括s和t中共有的成员 s.difference_update(t) s -= t 差修改操作: s 中包括仅属于 s 但不属于 t 的成员 s.symmetric_ difference_ update(t) s ^= t 对称差分修改操作: s 中包括仅属于 s 或仅属于 t 的 成员 s.add(obj) 加操作: 将 obj 添加到 s s.remove(obj) 删除操作: 将 obj 从 s 中删除;如果 s 中不存在 obj,将引发 KeyError s.discard(obj) 丢弃操作: remove() 的 友 好 版 本 - 如 果 s 中存在 obj, 从 s 中删除它 s.pop() Pop 操作: 移除并返回 s 中的任意一个元素 s.clear() 清除操作: 移除 s 中的所有元素 以下是一些你可能认为有用的在线参考文章: http://en.wikipedia.org/wiki/Set http://www.geocities.com/basicmathsets/set.html http://www.math.uah.edu/stat/foundations/Sets.xhtml 7.12 练习 7–1. 字典方法。哪个字典方法可以用来把两个字典合并到一起? 7–2. 字典的键。我们知道字典的值可以是任意的 Python 对象,那字典的键又如何呢?请试 着将除数字和字符串以外的其他不同类型的对象作为字典的键,看一看,哪些类型可以,哪些不行? 对那些不能作字典的键的对象类型,你认为是什么原因呢? 7–3. 字典和列表的方法。 (a) 创建一个字典,并把这个字典中的键按照字母顺序显示出来。 (b) 现在根据已按照字母顺序排序好的键,显示出这个字典中的键和值。 (c)同(b),但这次是根据已按照字母顺序排序好的字典的值,显示出这个字典中的键和值。(注 意:对字典和哈希表来说,这样做一般没有什么实际意义,因为大多数访问和排序(如果需要)都是 基于字典的键,这里只把它作为一个练习。) 7-4. 建立字典。给定两个长度相同的列表,比如说,列表[1, 2, 3,...]和['abc', 'def', 'ghi',...],用这两个列表里的所有数据组成一个字典,像这样:{1:'abc', 2: 'def', 3: 'ghi',...} Edit By Vheavens  Edit By Vheavens  7–5. userpw2.py. 下面的问题和例题 7.1 中管理名字-密码的键值对数据的程序有关。 (a)修改那个脚本,使它能记录用户上次的登录日期和时间(用 time 模块),并与用户密码一起 保存起来。程序的界面有要求用户输入用户名和密码的提示。无论户名是否成功登录,都应有提示, 在户名成功登录后,应更新相应用户的上次登录时间戳。如果本次登录与上次登录在时间上相差不 超过 4 个小时,则通知该用户: “You already logged in at: .” (b) 添加一个“管理”菜单,其中有以下两项:(1)删除一个用户 (2)显示系统中所有用户的名 字和他们的密码的清单。 (c) 口令目前没有加密。请添加一段对口令加密的代码(请参考crypt, rotor, 或其它加密模块) (d) 为程序添加图形界面,例如,用 Tkinter 写。 (e) 要求用户名不区分大小写。 (f) 加强对用户名的限制,不允许符号和空白符。 (g)合并“新用户”和“老用户”两个选项。如果一个新用户试图用一个不存在的用户名登录, 询问该用户是否是新用户,如果回答是肯定的,就创建该帐户。否则,按照老用户的方式登录。 7-6. 列表和字典。创建一个简单的股票证券投资数据系统。其中应至少包含四项数据:股市 行情显示器符号,所持有的股票,购买价格及当前价位 - 你可以随意添加其他数据项,比如收益率, 52 周最高指数、最低指数,等等。 用户每次输入各列的数据构成一个输出行。每行数据构成一个列表。还有一个总列表,包括了 所有行的数据。数据输入完毕后,提示用户选择一列数据项进行排序。把该数据项抽取出来作为字 典的键,字典的值就是该键对应行的值的列表。提醒读者:被选择用来排序的数据项必须是非重复 的键,否则就会丢失数据,因为字典不允许一个键有多个值。 你还可以选择其他计算输出,比如,盈亏比率,目前证券资产价值等。 7-7. 颠倒字典中的键和值。用一个字典做输入,输出另一个字典,用前者的键做值,前者的 值做键。 7-8. 人力资源。创建一个简单的雇员姓名和编号的程序。让用户输入一组雇员姓名和编号。 你的程序可以提供按照姓名排序输出的功能,雇员姓名显示在前面,后面是对应的雇员编号。附加 题:添加一项功能,按照雇员编号的顺序输出数据。 7-9. 翻译 (a) 编写一个字符翻译程序(功能类似于 Unix 中的 tr 命令)。我们将这个函数叫做 tr(),它有 三个字符串做参数: 源字符串、目的字符串、基本字符串,语法定义如下: def tr(srcstr, dststr, string) srcstr 的内容是你打算“翻译”的字符集合,dsrstr 是翻译后得到的字符集合,而 string 是 你打算进行翻译操作的字符串。举例来说,如果 srcstr == 'abc', dststr == 'mno', string == 'abcdef', 那么 tr()的输出将是'mnodef'. 注意这里 len(srcstr) == len(dststr). 在这个练习里,你可以使用内建函数 chr() 和 ord(), 但它们并不一定是解决这个问题所必不 可少的函数。 (b) 在这个函数里增加一个标志符参数,来处理不区分大小写的翻译问题。 Edit By Vheavens  Edit By Vheavens  (c)修改你的程序,使它能够处理删除字符的操作。字符串srcstr 中不能够映射到字符串 dststr 中字符的多余字符都将被过滤掉。换句话说,这些字符没有映射到 dststr 字符串中的任何字符,因 此就从函数返回的字符里被过滤掉了。举例来说:如果 srcstr == 'abcdef', dststr == 'mno', string == 'abcdefghi', 那么 tr()将输出'mnoghi'. 注意这里 len(srcstr) >= len(dststr). 7–10. 加密。 (a) 用上一个练习的思路编写一个"rot13"翻译器。"rot13"是一个古老而又简单的加密方法, 它把字母表中的每个字母用其后的第 13 个字母来代替。字母表中前半部分字母将被映射到后半部分, 而后半部分字母将被映射到前半部分,大小写保持不变。举例来说,'a'将被替换为'n','X'将被替 换为'K'; 数字和符号不进行翻译。 (b)在你的解决方案的基础上加一个应用程序,让它提示用户输入准备加密的字符串(这个算法 同时也可以对加密后的字符串进行解密),如下所示: % rot13.py Enter string to rot13: This is a short sentence. Your string to en/decrypt was: [This is a short sentence.]. The rot13 string is: [Guvf vf n fubeg fragrapr.]. % % rot13.py Enter string to rot13: Guvf vf n fubeg fragrapr. Your string to en/decrypt was: [Guvf vf n fubeg fragrapr.]. The rot13 string is: [This is a short sentence.]. 7–11. 定义。什么组成字典中合法的键? 举例说明字典中合法的键和非法的键。 7-12. 定义。 (a)在数学上,什么是集合? (b)在 Python 中,关于集合类型的定义是什么? 7–13. 随机数。修改练习 5-17 的代码:使用 random 模块中的 randint()或 randrange()方 法生成一个随机数集合:从 0 到 9(包括 9)中随机选择,生成1到10个随机数。这些数字组成集合 A(A 可以是可变集合,也可以不是)。同理,按此方法生成集合 B。每次新生成集合A和B后,显示 结果 A | B 和 A & B 7–14. 用户验证。修改前面的练习,要求用户输入 A | B 和A & B的结果,并告诉用户他(或 她)的答案是否正确,而不是将 A | B 和A & B 的结果直接显示出来。如果用户回答错误,允许他(或 她)修改解决方案,然后重新验证用户输入的答案。如果用户三次提交的答案均不正确,程序将显示 正确结果。 附加题:运用你关于集合的知识,创建某个集合的潜在子集,并询问用户此潜在子集是否真是 该集合的子集,要求和主程序一样有显示更正和答案的功能。 Edit By Vheavens  Edit By Vheavens  7–15. 编写计算器。 这个练习取材于 http://math.hws.edu/ 在线免费 Java 教材中的练习 12.2。编写一个程序允许用户选择两个集合:A 和 B, 及运算操作符。例如,in, not in, &, |, ^, <, <=, >, >=, ==, !=, 等. (你自己定义集合的输入语法,它们并不一定要像 Java 示例中那样用方括 号括住。)解析输入的字符串,按照用户选择的运算进行操作。你写的程序代码应该比 Java 版本的 该程序更简洁。 Edit By Vheavens  Edit By Vheavens  条件和循环  本章主题 z if 语句 z else 语句 z elif 语句 z 条件表达式 z while 语句 z for 语句 z break 语句 z continue 语句 z pass 语句 z else 语句 (再看) z Iterators 迭代器 z 列表解析(List Comprehensions) z 生成器表达式(Generator Expressions ) Edit By Vheavens  Edit By Vheavens  本章的主要内容是 Python 的条件和循环语句以及与它们相关的部分. 我们会深入探讨 if , while , for 以及与他们相搭配的 else , elif , break , continue 和 pass 语句. 8.1 if 语句 Python 中的 if 子句看起来十分熟悉. 它由三部分组成: 关键字本身, 用于判断结果真假的 条件表达式, 以及当表达式为真或者非零时执行的代码块. if 语句的语法如下: if expression: expr_true_suite if 语句的 expr_true_suite 代码块只有在条件表达式的结果的布尔值为真时才执行, 否则将 继续执行紧跟在该代码块后面的语句. 8.1.1 多重条件表达式 单个 if 语句可以通过使用布尔操作符 and , or 和 not 实现多重判断条件或是否定判断条件. if not warn and (system_load >= 10): print "WARNING: losing resources" Edit By Vheavens  Edit By Vheavens  warn += 1 8.1.2 单一语句的代码块 如果一个复合语句(例如 if 子句, while 或 for 循环)的代码块仅仅包含一行代码, 那么它可 以和前面的语句写在同一行上: if make_hard_copy: send_data_to_printer() 上边这样的单行语句是合法的, 尽管它可能方便, 但这样会使得代码更难阅读, 所以我们推 荐将这行代码移到下一行并合理地缩进. 另外一个原因就是如果你需要添加新的代码, 你还是得把 它移到下一行. 8.2 else 语句 和其他语言一样, Python 提供了与 if 语句搭配使用的 else 语句. 如果 if 语句的条件表达式的结果布尔值为假, 那么程序将执行 else 语句后的代码. 它的语 法你甚至可以猜到: if expression: expr_true_suite else: expr_false_suite 这里是样例代码: if passwd == user.passwd: ret_str = "password accepted" id = user.id valid = True else: ret_str = "invalid password entered... try again!" valid = False 8.2.1 避免“悬挂 else” Python 使用缩进而不是用大括号标记代码块边界的设计, 不仅帮助强化了代码的正确性, 而 且还暗中帮助程序员避免了语法上正确的代码中存在潜在的问题. 其中一个问题就是(臭名)昭著的 "悬挂 else (dangling else)"问题, 一种语义错觉. Edit By Vheavens  Edit By Vheavens  我们在这里给出一段 C 代码来说明我们的例子( K&R 和其他的编程教材也给出过): /* dangling-else in C */ if (balance > 0.00) if (((balance - amt) > min_bal) && (atm_cashout() == 1)) printf("Here's your cash; please take all bills.\n"); else printf("Your balance is zero or negative.\n"); 问题是: else 属于哪个 if ? 在 C 语言中, 规则是 else 与最近的 if 搭配. 所以我们上 面的例子中, else 虽然是想和外层的 if 搭配, 但是事实上 else 属于内部的 if ,因为 C 编译器 会忽略额外的空白. 结果, 如果你的 balance 是正数但小于最小值, 你将得到错误的输出, 程序 会显示你的 balance 是零或者为负数. 由于这个例子很简单, 所以解决这个问题并不难, 但是如果是大块的代码嵌入到了类似这样 的框架中, 那么发现并改正程序中的错误需要耗费很多精力. Python 设置的护栏不仅阻止你掉下悬 崖, 而且会带你离开危险的境地. 在 Python 中相同的例子对应如下的两种代码(只有一种是正确 的): if balance > 0.00: if balance - amt > min_bal and atm_cashout(): print "Here's your cash; please take all bills." else: print 'Your balance is zero or negative.' 或者是: if balance > 0.00: if balance - amt > min_bal and atm_cashout(): print "Here's your cash; please take all bills." else: print 'Your balance is zero or negative.' Python 的缩进使用强制使代码正确对齐, 让程序员来决定 else 属于哪一个 if . 限制您的选 择从而减少了不确定性, Python 鼓励您第一次就写出正确的代码. 在 Python 中制造出“悬挂 else” 问题是不可能的, 而且, 由于大括号不再被使用, Python 代码更易读懂. Edit By Vheavens  Edit By Vheavens  8.3 elif (即 else-if )语句 elif 是 Python 的 else-if 语句, 它检查多个表达式是否为真, 并在为真时执行特定代码块 中的代码. 和 else 一样, elif 声明是可选的, 然而不同的是, if 语句后最多只能有一个 else 语句, 但可以有任意数量的 elif 语句. if expression1: expr1_true_suite elif expression2: expr2_true_suite elif expressionN: exprN_true_suite else: none_of_the_above_suite switch/case 语句的替代品么? 在将来的某天, Python 可能会支持 switch /case 语句, 但是你完全可以用其他的 Python 结构来模拟它. 在 Python 中, 大量的 if-elif 语句并不难阅读: if user.cmd == 'create': action = "create item" elif user.cmd == 'delete': action = 'delete item' elif user.cmd == 'update': action = 'update item' else: action = 'invalid choice... try again!' 上面的语句完全可以满足我们的需要, 不过我们还可以用序列和成员关系操作符来简化它: if user.cmd in ('create', 'delete', 'update'): action = '%s item' % user.cmd else: Edit By Vheavens  Edit By Vheavens  action = 'invalid choice... try again!' 另外我们可以用 Python 字典给出更加优雅的解决方案, 我们将在第七章 "映射和集合类型" 中介绍字典. msgs = {'create': 'create item', 'delete': 'delete item', 'update': 'update item'} default = 'invalid choice... try again!' action = msgs.get(user.cmd, default) 众所周知, 使用映射对象(比如字典)的一个最大好处就是它的搜索操作比类似 if-elif-else 语句或是 for 循环这样的序列查询要快很多. 8.4 条件表达式(即"三元操作符") 如果你来自 C/C++ 或者是 Java 世界, 那么你很难忽略的一个事实就是 Python 在很长的一 段时间里没有条件表达式(C ? X : Y), 或称三元运算符. ( C 是条件表达式; X 是 C 为 True 时 的结果, Y 是 C 为 False 时的结果) 贵铎·范·罗萨姆一直拒绝加入这样的功能, 因为他认为应 该保持代码简单, 让程序员不轻易出错. 不过在十年多后, 他放弃了, 主要是因为人们试着用 and 和 or 来模拟它, 但大多都是错误的. 根据 FAQ , 正确的方法(并不唯一)是 (C and [X] or [Y])[0] . 唯一的问题是社区不同意这样的语法. (你可以看一看 PEP 308, 其 中有不同的方案.) 对于 Python 的这一问题,人们表达了极大的诉求. 贵铎·范·罗萨姆最终选择了一个最被看好(也是他最喜欢)的方案, 然后把它运用于标准库中 的一些模块. 根据 PEP , "这个评审通过考察大量现实世界的案例, 包含不同的应用, 以及由不同 程序员完成的代码." 最后 Python 2.5 集成的语法确定为: X if C else Y . 有了三元运算符后你就只需要一行完成条件判断和赋值操作, 而不需要像下面例子中的 min() 那样,使用 if-else 语句实现对数字x和y的操作: >>> x, y = 4, 3 >>> if x < y: ... smaller = x ... else: ... smaller = y ... >>> smaller 3 Edit By Vheavens  Edit By Vheavens  在 2.5 以前的版本中, Python 程序员最多这样做(其实是一个 hack ): >>> smaller = (x < y and [x] or [y])[0] >>> smaller 3 ``` 在 2.5 和更新的版本中, 你可以使用更简明的条件表达式: >>> smaller = x if x < y else y >>> smaller 3 8.5 while 语句 Python 的 while 是本章我们遇到的第一个循环语句. 事实它上是一个条件循环语句. 与 if 声明相比, 如果 if 后的条件为真, 就会执行一次相应的代码块. 而 while 中的代码块会一直循 环执行, 直到循环条件不再为真. 8.5.1 一般语法 while 循环的语法如下: while expression: suite_to_repeat while 循环的 suite_to_repeat 子句会一直循环执行, 直到 expression 值为布尔假. 这种 类型的循环机制常常用在计数循环中, 请参见下节中例子. 8.5.2 计数循环 count = 0 while (count < 9): print 'the index is:', count count += 1 这里的代码块里包含了 print 和自增语句, 它们被重复执行, 直到 count 不再小于 9 . 索引 count 在每次迭代时被打印出来然后自增 1 . 在 Python 解释器中输入这些代码我们将得到这样的 Edit By Vheavens  Edit By Vheavens  结果: >>> count = 0 >>> while (count < 9): ... print 'the index is:', count ... count += 1 ... the index is: 0 the index is: 1 the index is: 2 the index is: 3 the index is: 4 the index is: 5 the index is: 6 the index is: 7 the index is: 8 8.5.3 无限循环 你必须小心地使用 while 循环, 因为有可能 condition 永远不会为布尔假. 这样一来循环 就永远不会结束. 这些"无限"的循环不一定是坏事, 许多通讯服务器的客户端/服务器系统就是通 过它来工作的. 这取决于循环是否需要一直执行下去, 如果不是, 那么这个循环是否会结束; 也就 是说, 条件表达式会不会计算后得到布尔假? while True: handle, indata = wait_for_client_connect() outdata = process_request(indata) ack_result_to_client(handle, outdata) 例如上边的代码就是故意被设置为无限循环的,因为 True 无论如何都不会变成 False. 这是因 为服务器代码是用来等待客户端(可能通过网络)来连接的. 这些客户端向服务器发送请求, 服务器 处理请求. 请求被处理后, 服务器将向客户端返回数据, 而此时客户端可能断开连接或是发送另一个请求. 对于服务器而言它已经完成了对这个客户端的任务, 它会返回最外层循环等待下一个连接. 在第 16 章, "网络编程" 和第 17 章节 "Internet 客户端编程" 里你将了解关于如何处理客户端/服务 器的更多信息. Edit By Vheavens  Edit By Vheavens  8.6 for 语句 Python 提供给我们的另一个循环机制就是 for 语句. 它提供了 Python 中最强大的循环结构. 它可以遍历序列成员, 可以用在 列表解析 和 生成器表达式中, 它会自动地调用迭代器的 next() 方法, 捕获 StopIteration 异常并结束循环(所有这一切都是在内部发生的). 如果你刚刚接触 Python 那么我们要告诉你, 在以后你会经常用到它的. 和传统语言(例如 C/C++ , Fortran, 或者 Java )中的 for 语句不同, Python 的 for 更像是 shell 或是脚本语言中的 foreach 循环. 8.6.1 一般语法 for 循环会访问一个可迭代对象(例如序列或是迭代器)中的所有元素, 并在所有条目都处理过 后结束循环. 它的语法如下: for iter_var in iterable: suite_to_repeat 每次循环, iter_var 迭代变量被设置为可迭代对象(序列, 迭代器, 或者是其他支持迭代的对 象)的当前元素, 提供给 suite_to_repeat 语句块使用. 8.6.2 用于序列类型 本节中, 我们将学习用 for 循环迭代不同的序列对象. 样例将涵盖字符串, 列表, 以及元组. >>> for eachLetter in 'Names': ... print 'current letter:', eachLetter ... current letter: N current letter: a current letter: m current letter: e current letter: s 当迭代字符串时, 迭代变量只会包含一个字符(长度为 1 的字符串). 但这并不常用。在字符串 里中查找字符时, 程序员往往使用 in 来测试成员关系, 或者使用 string 模块中的函数以及字符 串方法来检查子字符串. 看到单个的字符在一种情况下有用,即在通过 print 语句调试for 循环中的序列时, 如果你在 应该看到字符串的地方发现的却是单个的字符, 那么很有可能你接受到的是一个字符串, 而不是对 Edit By Vheavens  Edit By Vheavens  象的序列. 迭代序列有三种基本方法: 通过序列项迭代 >>> nameList = ['Walter', "Nicole", 'Steven', 'Henry'] >>> for eachName in nameList: ... print eachName, "Lim" ... Walter Lim Nicole Lim Steven Lim Henry Lim 在上面的例子中, 我们迭代一个列表. 每次迭代, eacgName 变量都被设置为列表中特定某个元 素, 然后我们在代码块中打印出这个变量. ===通过序列索引迭代=== 另个方法就是通过序列的索引来迭代: >>> nameList = ['Cathy', "Terry", 'Joe', 'Heather', 'Lucy'] >>> for nameIndex in range(len(nameList)): ... print "Liu,", nameList[nameIndex] ... Liu, Cathy Liu, Terry Liu, Joe Liu, Heather Liu, Lucy 我们没有迭代元素, 而是通过列表的索引迭代. 这里我们使用了内建的 len() 函数获得序列长度, 使用 range() 函数(我们将在下面详细讨 论它)创建了要迭代的序列. >>> len(nameList) 5 >>> range(len(nameList)) Edit By Vheavens  Edit By Vheavens  [0, 1, 2, 3, 4] 使用 range() 我们可以得到用来迭代 nameList 的索引数列表; 使用切片/下标操作符( [ ] ), 就可以访问对应的序列对象. 如果你对性能有所了解的话, 那么毫无疑问你会意识到 直接迭代序列要比通过索引迭代快. 如果你不明白, 那么你可以仔细想想. (参见练习 8-13). ===使用项和索引迭代=== 两全其美的办法是使用内建的 enumerate() 函数, 它是 Python 2.3 的新增内容. 代码如下: >>> nameList = ['Donn', 'Shirley', 'Ben', 'Janice', ... 'David', 'Yen', 'Wendy'] >>> for i, eachLee in enumerate(nameList): ... print "%d %s Lee" % (i+1, eachLee) ... 1 Donn Lee 2 Shirley Lee 3 Ben Lee 4 Janice Lee 5 David Lee 6 Yen Lee 7 Wendy Lee 8.6.3 用于迭代器类型 用 for 循环访问迭代器和访问序列的方法差不多. 唯一的区别就是 for 语句会为你做一些额 外的事情. 迭代器并不代表循环条目的集合. 迭代器对象有一个 next() 方法, 调用后返回下一个条目. 所有条目迭代完后, 迭代器引发一 个 StopIteration 异常告诉程序循环结束. for 语句在内部调用 next() 并捕获异常. 使用迭代器做 for 循环的代码与使用序列条目几乎完全相同. 事实上在大多情况下, 你无法 分辨出你迭代的是一个序列还是迭代器, 因此,这就是为什么我们在说要遍历一个迭代器时,实际 上可能我们指的是要遍历一个序列,迭代器,或是一个支持迭代的对象(它有 next()方法)。 Edit By Vheavens  Edit By Vheavens  8.6.4 range() 内建函数 我们前面介绍 Python 的 for 循环的时候提到过它是一种迭代的循环机制. Python 同样提供 一个工具让我们在传统的伪条件设置下使用 for 声明, 例如从一个数字开始计数到另外个数字, 一旦到达最后的数字或者某个条件不再满足就立刻退出循环. 内建函数 range() 可以把类似 foreach 的 for 循环变成你更加熟悉的语句. 例如从 0 到 10 计数, 或者从 10 到 100 一次递增 5 . === range() 的完整语法=== Python 提供了两种不同的方法来调用 range() . 完整语法要求提供两个或三个整数参数: range(start, end, step =1) range() 会返回一个包含所有 k 的列表, 这里 start <= k < end , 从 start 到 end , k 每 次 递增 step . step 不可以为零,否则将发生错误. >>> range(2, 19, 3) [2, 5, 8, 11, 14, 17] 如果只给定两个参数,而省略 step, step 就使用默认值 1 . >>> range(3, 7) [3, 4, 5, 6] 我们来看看解释器环境下的例子 >>> for eachVal in range(2, 19, 3): ... print "value is:", eachVal ... value is: 2 value is: 5 value is: 8 value is: 11 value is: 14 value is: 17 我们的循环从 2 "数" 到 19 , 每次递增 3 . 如果你对 C 熟悉的话, 你会发现,range()的参 数与 C 的 for 循环变量有着直接的关系: Edit By Vheavens  Edit By Vheavens  /* equivalent loop in C */ for (eachVal = 2; eachVal < 19; eachVal += 3) { printf("value is: %d\n", eachVal); } 虽然看起来像是一个条件循环(检查 eachVal< 19 ), 但实际上是 range() 先用我们指定的条 件生成一个列表, 然后把列表用于这个 for 语句. ===range() 简略语法=== range() 还有两种简略的语法格式: range(end) range(start, end) 我们在第 2 章看到过最短的语法 接受一个值, start 默认为 0 , step 默认为 1 , 然后 range()返回从 0 到 end 的数列. >>> range(5) [0, 1, 2, 3, 4] range() 的中型版本和完整版本几乎完全一样, 只是 step 使用默认值 1 . 现在我们在 Python 解释器中试下这条语句: >>> for count in range(2, 5): ... print count ... 2 3 4 核心笔记: 为什么 range() 不是只有一种语法? 你已经知道了 range() 的所有语法, 有些人可能会问一个挑剔的问题, 为什么不把这两种语 法合并成一个下面这样的语法? range(start=0, end, step =1) # 错误 这个语法不可以使用两个参数调用. 因为 step 要求给定 start . 换句话说, 你不能只传递 end 和 step 参数. 因为它们会被解释器误认为是 start 和 end . Edit By Vheavens  Edit By Vheavens  8.6.5 xrange() 内建函数 xrange() 类似 range() , 不过当你有一个很大的范围列表时, xrange() 可能更为适合, 因为 它不会在内存里创建列表的完整拷贝. 它只被用在 for 循环中, 在 for 循环外使用它没有意义。 同样地, 你可以想到, 它的性能远高出 range(), 因为它不生成整个列表。在 Python 的将来版本 中, range() 可能会像 xrange() 一样, 返回一个可迭代对象(不是列表也不是一个迭代器) - 它会 像前边一章讨论的那样. 8.6.6 与序列相关的内建函数 sorted(), reversed(), enumerate(), zip() 下边是使用循环相关和序列相关函数的例子. 为什么它们叫"序列相关"呢? 是因为其中两个函 数( sorted() 和 zip() )返回一个序列(列表), 而另外两个函数( reversed() 和 enumerate() ) 返回迭代器(类似序列) >>> albums = ('Poe', 'Gaudi', 'Freud', 'Poe2') >>> years = (1976, 1987, 1990, 2003) >>> for album in sorted(albums): ... print album, ... Freud Gaudi Poe Poe2 >>> >>> for album in reversed(albums): ... print album, ... Poe2 Freud Gaudi Poe >>> >>> for i, album in enumerate(albums): ... print i, album ... 0 Poe 1 Gaudi 2 Freud 3 Poe2 >>> >>> for album, yr in zip(albums, years): ... print yr, album ... Edit By Vheavens  Edit By Vheavens  1976 Poe 1987 Gaudi 1990 Freud 2003 Poe2 我们已经涵盖了 Python 中的所有循环语句, 下面我们看看循环相关的语句, 包括用于放弃循 环的 break 语句, 和立即开始下一次迭代的 continue 语句. 8.7 break 语句 Python 中的 break 语句可以结束当前循环然后跳转到下条语句, 类似 C 中的传统 break . 常用在当某个外部条件被触发(一般通过 if 语句检查), 需要立即从循环中退出时. break 语句可 以用在 while 和 for 循环中. count = num / 2 while count > 0: if num % count == 0: print count, 'is the largest factor of', num break count -= 1 上边这段代码用于寻找给定数字 num 的最大约数. 我们迭代所有可能的约数, count 变量依次 递减, 第一个能整除 num 的就是我们要找的最大约数,找到后就不再再继续找了, 使用 break 语 句退出循环. phone2remove = '555-1212' for eachPhone in phoneList: if eachPhone == phone2remove: print "found", phone2remove, '... deleting' deleteFromPhoneDB(phone2remove) break 这里的 break 语句用于打断列表的迭代. 目的是为了找到列表中的目标元素, 如果找到, 则 把它从数据库里删除然后退出循环. 8.8 continue 语句 核心笔记: continue 语句 Edit By Vheavens  Edit By Vheavens  不管是 Python, C, Java 还是其它任何支持 continue 语句的结构化语言中, 一些初学者有这样 的一个误解:continue 语句"立即启动循环的下一次迭代". 实际上, 当遇到 continue 语句时, 程 序会终止当前循环,并忽略剩余的语句, 然后回到循环的顶端. 在开始下一次迭代前,如果是条件循 环, 我们将验证条件表达式.如果是迭代循环,我们将验证是否还有元素可以迭代. 只有在验证成功 的情况下, 我们才会开始下一次迭代. Python 里的 continue 语句和其他高级语言中的传统 continue 并没有什么不同. 它可以被 用在 while 和 for 循环里. while 循环是条件性的, 而 for 循环是迭代的, 所以 continue 在开 始下一次循环前要满足一些先决条件(前边的核心笔记中强调的), 否则循环会正常结束. valid = False count = 3 while count > 0: input = raw_input("enter password") # check for valid passwd for eachPasswd in passwdList: if input == eachPasswd: valid = True break if not valid: # (or valid == 0) print "invalid input" count -= 1 continue else: break 这里例子结合使用了 while , for , if , break 以及 continue , 用来验证用户输入. 用 户有三次机会来输入正确的密码, 如果失败, 那么 valid 变量将仍为一个布尔假( 0 ), 然后我们 可以采取必要的操作阻止用户猜测密码. 8.9 pass 语句 Python 还提供了 pass 语句( C 中没有提供对应的语句). Python 没有使用传统的大括号来标 记代码块, 有时,有些地方在语法上要求要有代码, 而Python 中没有对应的空大括号或是分号( ; ) 来表示 C 语言中的 "不做任何事" , 如果你在需要子语句块的地方不写任何语句, 解释器会提示你 语法错误. 因此, Python 提供了 pass 语句, 它不做任何事情 - 即 NOP , ( No OPeration , 无 操作) 我们从汇编语言中借用这个概念. pass 同样也可作为开发中的小技巧, 标记你后来要完成的 代码, 例如这样: Edit By Vheavens  Edit By Vheavens  def foo_func(): pass 或是 if user_choice == 'do_calc': pass else: pass 这样的代码结构在开发和调试时很有用, 因为编写代码的时候你可能要先把结构定下来,但你 不希望它干扰其他已经完成的代码. 在不需要它做任何事情地方, 放一个 pass 将是一个很好的主 意. 另外它在异常处理中也被经常用到, 我们将在第 10 章中详细介绍; 比如你跟踪到了一个非致 命的错误, 不想采取任何措施(你只是想记录一下事件或是在内部进行处理). 8.10 再谈 else 语句 在 C (以及大多其他语言中), 你不会在条件语句范围外发现 else 语句, 但 Python 不同, 你可以在 while 和 for 循环中使用 else 语句. 它们是怎么工作的呢? 在循环中使用时, else 子句只在循环完成后执行, 也就是说 break 语句也会跳过 else 块. 展示 while 语句中 else 用法的一个例子就是寻找一个数的最大约数. 我们已经实现了完成 这个任务的函数, 使用 while 循环和 else 语句. Example 8.1 (maxFact.py) 利用这个语法完成 了 showMaxFactor() 函数. Example 8.1 while-else Loop Example (maxFact.py) 这个程序显示出 10 到 20 中的数字的最大约数. 该脚本也会提示这个数是否为素数. 1 #!/usr/bin/env python 2 3 def showMaxFactor(num): 4 count = num / 2 5 while count > 1: 6 if num % count == 0: 7 print 'largest factor of %d is %d' % \ 8 (num, count) 9 break 10 count -= 1 Edit By Vheavens  Edit By Vheavens  11 else: 12 print num, "is prime" 13 14 for eachNum in range(10, 21): 15 showMaxFactor(eachNum) showMaxFactor() 函数中第 3 行的循环从 amount 的一半开始计数(这样就可以检查这个数是否 可以被 2 整除, 如果可以,那就找到了最大的约数). 然后循环每次递减 1 (第 10 行), 直到发现 约数(第 6-9 行). 如果循环递减到 1 还没有找到约数, 那么这个数一定是素数. 11-12 行的 else 子句负责处理这样的情况. 程序的主体( 14-15 行)用数字参数调用 showMaxFactor() . 执行该程序将得到这样的输出: largest factor of 10 is 5 11 is prime largest factor of 12 is 6 13 is prime largest factor of 14 is 7 largest factor of 15 is 5 largest factor of 16 is 8 17 is prime largest factor of 18 is 9 19 is prime largest factor of 20 is 10 同样地, for 循环也可以有 else 用于循环后处理(post-processing). 它和 while 循环中的 else 处理方式相同. 只要 for 循环是正常结束的(不是通过 break ), else 子句就会执行. 我们在 8.5.3 已经见过这样的例子 表 8.1 条件及循环语句中的辅助语句总结 a. pass 在任何需要语句块(一个或多个语句)的地方都可以使用(例如 elif , else , clasa , Edit By Vheavens  Edit By Vheavens  def , try , except , finally ). 8.11 迭代器和 iter() 函数 8.11.1 什么是迭代器? 迭代器是在版本 2.2 被加入 Python 的, 它为类序列对象提供了一个类序列的接口. 我们在 前边的第 6 章已经正式地介绍过序列. 它们是一组数据结构,你可以利用它们的索引从 0 开始一直 "迭代" 到序列的最后一个条目. 用"计数"的方法迭代序列是很简单的. Python 的迭代无缝地支持 序列对象, 而且它还允许程序员迭代非序列类型, 包括用户定义的对象. 迭代器用起来很灵巧, 你可以迭代不是序列但表现出序列行为的对象, 例如字典的 key , 一个 文件的行, 等等. 当你使用循环迭代一个对象条目时, 你几乎不可能分辨出它是迭代器还是序列. 你不必去关注这些, 因为 Python 让它象一个序列那样操作. 8.11.2 为什么要迭代器? 援引 PEP (234) 中对迭代器的定义: z 提供了可扩展的迭代器接口. z 对列表迭代带来了性能上的增强. z 在字典迭代中性能提升. z 创建真正的迭代接口, 而不是原来的随机对象访问. z 与所有已经存在的用户定义的类以及扩展的模拟序列和映射的对象向后兼容 z 迭代非序列集合(例如映射和文件)时, 可以创建更简洁可读的代码. 8.11.3 如何迭代? 根本上说, 迭代器就是有一个 next() 方法的对象, 而不是通过索引来计数. 当你或是一个循 环机制(例如 for 语句)需要下一个项时, 调用迭代器的 next() 方法就可以获得它. 条目全部取 出后, 会引发一个 StopIteration 异常, 这并不表示错误发生, 只是告诉外部调用者, 迭代完成. 不过, 迭代器也有一些限制. 例如你不能向后移动, 不能回到开始, 也不能复制一个迭代器. 如果你要再次(或者是同时)迭代同个对象, 你只能去创建另一个迭代器对象. 不过, 这并不糟糕, Edit By Vheavens  Edit By Vheavens  因为还有其他的工具来帮助你使用迭代器. reversed() 内建函数将返回一个反序访问的迭代器. enumerate() 内建函数同样也返回迭代器. 另外两个新的内建函数, any() 和 all() , 在 Python 2.5 中新增, 如果迭代器中某个/所有条目 的值都为布尔真时,则它们返回值为真. 本章先前部分我们展示了如何在 for 循环中通过索引或是 可迭代对象来遍历条目. 同时 Python 还提供了一整个 itertools 模块, 它包含各种有用的迭代 器. 8.11.4 使用迭代器 ===序列=== 正如先前提到的, 迭代 Python 的序列对象和你想像的一样: >>> myTuple = (123, 'xyz', 45.67) >>> i = iter(myTuple) >>> i.next() 123 >>> i.next() 'xyz' >>> i.next() 45.67 >>> i.next() Traceback (most recent call last): File "", line 1, in ? StopIteration 如果这是一个实际应用程序, 那么我们需要把代码放在一个 try-except 块中. 序列现在会自 动地产生它们自己的迭代器, 所以一个 for 循环: for i in seq: do_something_to(i) 实际上是这样工作的: fetch = iter(seq) while True: try: i = fetch.next() except StopIteration: Edit By Vheavens  Edit By Vheavens  break do_something_to(i) 不过, 你不需要改动你的代码, 因为 for 循环会自动调用迭代器的 next() 方法(以及监视 StopIteration 异常). ===字典=== 字典和文件是另外两个可迭代的 Python 数据类型. 字典的迭代器会遍历它的键(keys). 语句 for eachKey in myDict.keys() 可以缩写为 for eachKey in myDict , 例如: >>> legends = { ('Poe', 'author'): (1809, 1849, 1976), ... ('Gaudi', 'architect'): (1852, 1906, 1987), ... ('Freud', 'psychoanalyst'): (1856, 1939, 1990) ... } ... >>> for eachLegend in legends: ... print 'Name: %s\tOccupation: %s' % eachLegend ... print ' Birth: %s\tDeath: %s\tAlbum: %s\n' \ ... % legends[eachLegend] ... Name: Freud Occupation: psychoanalyst Birth: 1856 Death: 1939 Album: 1990 Name: Poe Occupation: author Birth: 1809 Death: 1849 Album: 1976 Name: Gaudi Occupation: architect Birth: 1852 Death: 1906 Album: 1987 另外, Python 还引进了三个新的内建字典方法来定义迭代: myDict.iterkeys() (通过 keys 迭 代), myDict.itervalues() (通过 values 迭代), 以及 myDicit.iteritems() (通过 key/value 对 来迭代). 注意, in 操作符也可以用于检查字典的 key 是否存在 , 之前的布尔表达式 myDict.has_key(anyKey) 可以被简写为 anyKey in myDict . ===文件=== 文件对象生成的迭代器会自动调用 readline() 方法. 这样, 循环就可以访问文本文件的所有 行 . 程 序 员 可 以 使 用 更 简 单 的 for eachLine in myFile 替 换 for eachLine in myFile.readlines() : Edit By Vheavens  Edit By Vheavens  >>> myFile = open('config-win.txt') >>> for eachLine in myFile: ... print eachLine, # comma suppresses extra \n ... [EditorWindow] font-name: courier new font-size: 10 >>> myFile.close() 8.11.5 可变对象和迭代器 记住,在迭代可变对象的时候修改它们并不是个好主意. 这在迭代器出现之前就是一个问题. 一个流行的例子就是循环列表的时候删除满足(或不满足)特定条件的项: for eachURL in allURLs: if not eachURL.startswith('http://'): allURLs.remove(eachURL) # YIKES!! 除列表外的其他序列都是不可变的, 所以危险就发生在这里. 一个序列的迭代器只是记录你 当前到达第多少个元素, 所以如果你在迭代时改变了元素, 更新会立即反映到你所迭代的条目上. 在迭代字典的 key 时, 你绝对不能改变这个字典. 使用字典的 keys() 方法是可以的, 因为 keys() 返回一个独立于字典的列表. 而迭代器是与实际对象绑定在一起的, 它将不会继续执行下 去: >>> myDict = {'a': 1, 'b': 2, 'c': 3, 'd': 4} >>> for eachKey in myDict: ... print eachKey, myDict[eachKey] ... del myDict[eachKey] ... a 1 Traceback (most recent call last): File "", line 1, in ? RuntimeError: dictionary changed size during iteration 这样可以避免有缺陷的代码. 更多有关迭代器的细节请参阅 PEP 234 . 8.11.6 如何创建迭代器 对一个对象调用 iter() 就可以得到它的迭代器. 它的语法如下: Edit By Vheavens  Edit By Vheavens  iter(obj) iter(func, sentinel ) 如果你传递一个参数给 iter() , 它会检查你传递的是不是一个序列, 如果是, 那么很简单: 根据索引从 0 一直迭代到序列结束. 另一个创建迭代器的方法是使用类, 我们将在第 13 章详细 介绍, 一个实现了 __iter__() 和 next() 方法的类可以作为迭代器使用. 如果是传递两个参数给 iter() , 它 会重复地调用 func , 直到迭代器的下个值等于 sentinel . 8.12 列表解析 列表解析( List comprehensions, 或缩略为 list comps ) 来自函数式编程语言 Haskell . 它 是一个非常有用, 简单, 而且灵活的工具, 可以用来动态地创建列表. 它在 Python 2.0 中被加入. 在第 11 章, 函数中, 我们将讨论 Python 早就支持的函数式编程特性, 例如 lambda , map() , 以及 filter() 等, 这些存在于 Python 中已经很长时间了, 但通过列表解析 , 它们可以被简化 为一个列表解析式子. map() 对所有的列表成员应用一个操作, filter() 基于一个条件表达式过 滤列表成员. 最后, lambda 允许你快速地创建只有一行的函数对象. 你不需要现在就去掌握这些, 在本节中你将看到它们出现在例子里, 因为我们需要讨论列表解析的优势. 首先让我们看看列表 解析的语法: [expr for iter_var in iterable] 这个语句的核心是 for 循环, 它迭代 iterable 对象的所有条目. 前边的 expr 应用于序列 的每个成员, 最后的结果值是该表达式产生的列表. 迭代变量并不需要是表达式的一部分. 这里用到了第 11 章的一些代码. 它有一个计算序列成员的平方的 lambda 函数表达式: >>> map(lambda x: x ** 2, range(6)) [0, 1, 4, 9, 16, 25] 我们可以使用下面这样的列表解析来替换它: >>> [x ** 2 for x in range(6)] [0, 1, 4, 9, 16, 25] 在新语句中, 只有一次函数调用( range() ), 而先前的语句中有三次函数调用(range() , map() , 以及 lambda ). 你也可以用括号包住表达式, 象 [(x ** 2) for x in range(6)] 这样, 更 Edit By Vheavens  Edit By Vheavens  便于阅读. 列表解析的表达式可以取代内建的 map() 函数以及 lambda , 而且效率更高. 结合 if 语句,列表解析还提供了一个扩展版本的语法: [expr for iter_var in iterable if cond_expr] 这个语法在迭代时会过滤/捕获满足条件表达式 cond_expr 的序列成员. 回想下 odd() 函数, 它用于判断一个数值对象是奇数还是偶数(奇数返回 1 , 偶数返回 0 ): def odd(n): return n % 2 我们可以借用这个函数的核心操作, 使用 filter() 和 lambda 挑选出序列中的奇数: >>> seq = [11, 10, 9, 9, 10, 10, 9, 8, 23, 9, 7, 18, 12, 11, 12] >>> filter(lambda x: x % 2, seq) [11, 9, 9, 9, 23, 9, 7, 11] 和先前的例子一样, 即使不用 filter() 和 lambda,我们同样可以使用列表解析来完成操作, 获得想要的数字: >>> [x for x in seq if x % 2] [11, 9, 9, 9, 23, 9, 7, 11] 我们使用更多实用的例子结束这节. ===矩阵样例=== 你需要迭代一个有三行五列的矩阵么? 很简单: >>> [(x+1,y+1) for x in range(3) for y in range(5)] [(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5)] ===磁盘文件样例=== 假设我们有如下这样一个数据文件 hhga.txt , 需要计算出所有非空白字符的数目: And the Lord spake, saying, "First shalt thou take out the Holy Pin. Then shalt thou count to three, Edit By Vheavens  Edit By Vheavens  no more, no less. Three shall be the number thou shalt count, and the number of the counting shall be three. Four shalt thou not count, nei- ther count thou two, excepting that thou then proceed to three. Five is right out. Once the number three, being the third number, be reached, then lobbest thou thy Holy Hand Grenade of Antioch towards thy foe, who, being naughty in My sight, shall snuff it." 我们已经知道可以通过 for line in data 迭代文件内容, 不过, 除了这个, 我们还可以把每 行分割( split )为单词, 然后我们可以像这样计算单词个数: >>> f = open('hhga.txt', 'r') >>> len([word for line in f for word in line.split()]) 91 快速地计算文件大小 import os >>> os.stat('hhga.txt').st_size 499L 假定文件中至少有一个空白字符, 我们知道文件中有少于 499 个非空字符. 我们可以把每个 单词的长度加起来, 得到和. >>> f.seek(0) >>> sum([len(word) for line in f for word in line.split()]) 408 这里我们用 seek() 函数回到文件的开头, 因为迭代器已经访问完了文件的所有行. 一个清晰 明了的列表解析完成了之前需要许多行代码才能完成的工作! 如你所见, 列表解析支持多重嵌套 for 循环以及多个 if 子句. 完整的语法可以在官方文档中找到. 你也可以在 PEP 202 中找到更多 关于列表解析的资料. 8.13 生成器表达式 生成器表达式是列表解析的一个扩展. 在 Python 2.0 中我们加入了列表解析, 使语言有了一 次革命化的发展, 提供给用户了一个强大的工具, 只用一行代码就可以创建包含特定内容的列表. 你可以去问一个有多年 Python 经验的程序员是什么改变了他们编写 Python 程序的方式, 那么列 Edit By Vheavens  Edit By Vheavens  表解析一定会是最多的答案. 另个在 Python 版本 2.2 时被加入的另一个重要特性是生成器. 生成器是特定的函数, 允许 你返回一个值, 然后"暂停"代码的执行, 稍后恢复. 我们将在第 11 章中讨论生成器. 列表解析的一个不足就是必要生成所有的数据, 用以创建整个列表. 这可能对有大量数据的迭 代器有负面效应. 生成器表达式通过结合列表解析和生成器解决了这个问题. 生成器表达式在 Python 2.4 被引入, 它与列表解析非常相似,而且它们的基本语法基本相同; 不过它并不真正创建数字列表, 而是返回一个生成器,这个生成器在每次计算出一个条目后,把这 个条目“产生”(yield)出来. 生成器表达式使用了"延迟计算"(lazy evaluation), 所以它在使用 内存上更有效. 我们来看看它和列表解析到底有多相似: 列表解析: [expr for iter_var in iterable if cond_expr] 生成器表达式: (expr for iter_var in iterable if cond_expr) 生成器并不会让列表解析废弃, 它只是一个内存使用更友好的结构, 基于此, 有很多使用生 成器地方. 下面我们提供了一些使用生成器表达式的例子, 最后例举一个冗长的样例, 从它你可 以感觉到 Python 代码在这些年来的变化. ===磁盘文件样例=== 在前边列表解析一节, 我们计算文本文件中非空白字符总和. 最后的代码中, 我们展示了如何 使用一行列表解析代码做所有的事. 如果这个文件的大小变得很大, 那么这行代码的内存性能会很 低, 因为我们要创建一个很长的列表用于存放单词的长度. 为了避免创建庞大的列表, 我们可以使用生成器表达式来完成求和操作. 它会计算每个单词的 长度然后传递给 sum() 函数(它的参数不仅可以是列表,还可以是可迭代对象,比如生成器表达式). 这样, 我们可以得到优化后的代码(代码长度, 还有执行效率都很高效): >>> sum(len(word) for line in data for word in line.split()) 408 我们所做的只是把方括号删除: 少了两字节, 而且更节省内存 ... 非常地环保! === 交叉配对例子 === Edit By Vheavens  Edit By Vheavens  生成器表达式就好像是懒惰的列表解析(这反而成了它主要的优势). 它还可以用来处理其他列 表或生成器, 例如这里的 rows 和 cols : rows = [1, 2, 3, 17] def cols(): # example of simple generator yield 56 yield 2 yield 1 不需要创建新的列表, 直接就可以创建配对. 我们可以使用下面的生成器表达式: x_product_pairs = ((i, j) for i in rows for j in cols()) 现在我们可以循环 x_product_pairs , 它会懒惰地循环 rows 和 cols : >>> for pair in x_product_pairs: ... print pair ... (1, 56) (1, 2) (1, 1) (2, 56) (2, 2) (2, 1) (3, 56) (3, 2) (3, 1) (17, 56) (17, 2) (17, 1) === 重构样例 === 我们通过一个寻找文件最长的行的例子来看看如何改进代码. 在以前, 我们这样读取文件: f = open('/etc/motd', 'r') longest = 0 while True: linelen = len(f.readline().strip()) Edit By Vheavens  Edit By Vheavens  if not linelen: break if linelen > longest: longest = linelen f.close() return longest 事实上, 这还不够老. 真正的旧版本 Python 代码中, 布尔常量应该写是整数 1 , 而且我们应 该使用 string 模块而不是字符串的 strip() 方法: import string : len(string.strip(f.readline())) 从那时起, 我们认识到如果读取了所有的行, 那么应该尽早释放文件资源. 如果这是一个很多 进程都要用到的日志文件, 那么理所当然我们不能一直拿着它的句柄不释放. 是的, 我们的例子是 用来展示的, 但是你应该得到这个理念. 所以读取文件的行的首选方法应该是这样: f = open('/etc/motd', 'r') longest = 0 allLines = f.readlines() f.close() for line in allLines: linelen = len(line.strip()) if linelen > longest: longest = linelen return longest 列表解析允许我们稍微简化我们代码, 而且我们可以在得到行的集合前做一定的处理. 在下段 代码中, 除了读取文件中的行之外,我们还调用了字符串的 strip() 方法处理行内容. f = open('/etc/motd', 'r') longest = 0 allLines = [x.strip() for x in f.readlines()] f.close() for line in allLines: linelen = len(line) if linelen > longest: longest = linelen return longest Edit By Vheavens  Edit By Vheavens  然而, 两个例子在处理大文件时候都有问题, 因为 readlines() 会读取文件的所有行. 后来 我们有了迭代器, 文件本身就成为了它自己的迭代器, 不需要调用 readlines() 函数. 我们已经 做到了这一步, 为什么不去直接获得行长度的集合呢(之前我们得到的是行的集合)? 这样, 我们就 可以使用 max() 内建函数得到最长的字符串长度: f = open('/etc/motd', 'r') allLineLens = [len(x.strip()) for x in f] f.close() return max(allLineLens) 这里唯一的问题就是你一行一行迭代 f 的时候, 列表解析需要文件的所有行读取到内存中, 然后生成列表. 我们可以进一步简化代码: 使用生成器表达式替换列表解析, 然后把它移到 max() 函数里, 这样, 所有的核心部分只有一行: f = open('/etc/motd', 'r') longest = max(len(x.strip()) for x in f) f.close() return longest 最后, 我们可以去掉文件打开模式(默认为读取), 然后让 Python 去处理打开的文件. 当然, 文件用于写入的时候不能这么做, 但这里我们不需要考虑太多: return max(len(x.strip()) for x in open('/etc/motd')) 我们走了好长一段路. 注意,即便是这只有一行的 Python 程序也不是很晦涩. 生成器表达式 在 Python 2.4 中被加入, 你可以在 PEP 289 中找到更多相关内容. 8.14 R 相关模块 Python 2.2 引进了迭代器, 在下一个发行 (2.3) 中, itertools 模块被加入, 用来帮助那些 发现迭代器威力但又需要一些辅助工具的开发者. 有趣的是如果你阅读关于 itertools 中实用程 序的文档, 你会发现生成器. 所以在迭代器和生成器间有一定的联系. 你可以在第 11 章 "函数" 中了解更多. 8.15 练习 8–1. 条件语句. 请看下边的代码 Edit By Vheavens  Edit By Vheavens  # statement A if x > 0: # statement B pass elif x < 0: # statement C pass else: # statement D pass # statement E (a)如果 x< 0 , 上面哪个语句(A, B, C, D, E)将被执行 (b)如果 x== 0 , 上面哪个居于将被执行? (c)如果 x> 0 , 上面哪个语句将被执行? 8–2. 循环. 编写一个程序, 让用户输入三个数字: (f)rom, (t)o, 和 (i)ncrement . 以 i 为步长, 从 f 计数到 t , 包括 f 和 t . 例如, 如果输入的是 f == 2, t == 26, i == 4 , 程序 将输出 2, 6, 10, 14, 18, 22, 26. 8–3. range() . 如果我们需要生成下面的这些列表, 分别需要在 range() 内建函数中提 供那些参数? (a) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] (b) [3, 6, 9, 12, 15, 18] (c) [-20, 200, 420, 640, 860] 8–4. 素数. 我们在本章已经给出了一些代码来确定一个数字的最大约数或者它是否是一个 素数. 请把相关代码转换为一个返回值为布尔值的函数,函数名为 isprime() . 如果输入的是一个 素数, 那么返回 True , 否则返回 False . 8–5. 约数. 完成一个名为 getfactors() 的函数. 它接受一个整数作为参数, 返回它所有 约数的列表, 包括 1 和它本身, 8–6. 素因子分解. 以刚才练习中的 isprime() 和 getfactors() 函数为基础编写一个函 数, 它接受一个整数作为参数, 返回该整数所有素数因子的列表. 这个过程叫做求素因子分解, 它 输出的所有因子之积应该是原来的数字. 注意列表里可能有重复的元素. 例如输入 20 , 返回结果 应该是 [2, 2, 5] . 8–7. 全数. 完全数被定义为这样的数字: 它的约数(不包括它自己)之和为它本身. 例如: 6 的约数是 1, 2, 3, 因为 1 + 2 + 3 = 6 , 所以 6 被认为是一个完全数. 编写一个名为 isperfect() Edit By Vheavens  Edit By Vheavens  的函数, 它接受一个整数作为参数, 如果这个数字是完全数, 返回 1 ; 否则返回 0 . 8–8. 阶乘. 一个数的阶乘被定义为从 1 到该数字所有数字的乘积. N 的阶乘简写为 N! . 写一个函数, 指定 N, 返回 N! 的值. 8–9. Fibonacci 数列. Fibonacci 数列形如 1, 1, 2, 3, 5, 8, 13, 21, 等等. 也就是说, 下一个值是序列中前两个值之和. 写一个函数, 给定 N , 返回第 N 个 Fibonacci 数字. 例如, 第 1 个 Fibonacci 数字是 1 , 第 6 个是 8 . 8–10. 文本处理. 统计一句话中的元音, 辅音以及单词(以空格分割)的个数. 忽略元音和 辅音的特殊情况, 如 "h", "y", "qu" 等. 附加题: 编写处理这些特殊情况的代码. 8–11. 文本处理. 要求输入一个姓名列表,输入格式是“Last Name, First Name,” 即 姓, 逗号, 名. 编写程序处理输入, 如果用户输入错误, 比如“First Name Last Name,” , 请纠正这 些错误, 并通知用户. 同时你还需要记录输入错误次数. 当用户输入结束后, 给列表排序, 然后以 "姓 , 名" 的顺序显示. 输入输出示例(你不需要完全按照这里里例子完成): % nametrack.py Enter total number of names: 5 Please enter name 0: Smith, Joe Please enter name 1: Mary Wong >> Wrong format... should be Last, First. >> You have done this 1 time(s) already. Fixing input... Please enter name 2: Hamilton, Gerald Please enter name 3: Royce, Linda Please enter name 4: Winston Salem >> Wrong format... should be Last, First. >> You have done this 2 time(s) already. Fixing input... The sorted list (by last name) is: Hamilton, Gerald Royce, Linda Salem, Winston Smith, Joe Wong, Mary 8–12. (整数)位操作. 编写一个程序, 用户给出起始和结束数字后给出一个下面这样的表格, 分别显示出两个数字间所有整数的十进制, 二进制, 八进制和十六进制表示. 如果字符是可打印的 ASCII 字符, 也要把它打印出来, 如果没有一个是可打印字符, 就省略掉 ASCII 那一栏的表头. Edit By Vheavens  Edit By Vheavens  请参考下面的输入输出格式: 8–13. 程序执行性能. 在 8.5.2 节里, 我们介绍了两种基本的迭代序列方法: (1) 通过序列 项, 以及 (2) 通过序列索引遍历. 该小节的末尾我们指出后一种方法在序列很长的时候性能不佳. (在我的系统下, 性能差了将近两倍[83%]) 你认为它的原因是什么? Edit By Vheavens  Edit By Vheavens  文件和输入输出            本章主题 z 文件对象 z 文件内建函数 z 文件内建方法 z 文件内建属性 z 标准文件 z 命令行参数 z 文件系统 z 文件执行 z 持久存储 z 相关模块 Edit By Vheavens  Edit By Vheavens  本章将深入介绍 Python 的文件处理和相关输入输出能力. 我们将介绍文件对象(它的内建函 数, 内建方法和属性), 标准文件, 同时讨论文件系统的访问方法, 文件执行, 最后 简洁地涉及持 久存储和标准库中与文件有关的模块. 9.1 文件对象 文件对象不仅可以用来访问普通的磁盘文件, 而且也可以访问任何其它类型抽象层面上的"文 件". 一旦设置了合适的"钩子", 你就可以访问具有文件类型接口的其它对象, 就好像访问的是普 通文件一样. 随着你使用 Python 经验的增长. 您会遇到很多处理"类文件"对象的情况. 有很多这样的例子, 例如实时地"打开一个 URL"来读取 Web 页面,在另一个独立的进程中执行一个命令进行通讯, 就好 像是两个同时打开的文件, 一个用于读取, 另个用于写入. 内建函数 open() 返回一个文件对象(参见下一小节), 对该文件进行后继相关的操作都要用到 它. 还有大量的函数也会返回文件对象或是类文件( file-like )对象. 进行这种抽象处理的主要原 因是许多的输入/输出数据结构更趋向于使用通用的接口. 这样就可以在程序行为和实现上保持一 致性. 甚至像 Unix 这样的操作系统把文件作为通信的底层架构接口. 请记住, 文件只是连续的字 节序列. 数据的传输经常会用到字节流, 无论字节流是由单个字节还是大块数据组成. 9.2 文件内建函数[open()和 file()] Edit By Vheavens  Edit By Vheavens  作为打开文件之门的"钥匙", 内建函数 open() [以及 file() ]提供了初始化输入/输出(I/O) 操作的通用接口. open() 内建函数成功打开文件后时候会返回一个文件对象, 否则引发一个错误. 当操作失败, Python 会产生一个 IOError 异常 - 我们会在下一章讨论错误和异常的处理. 内建函 数 open() 的基本语法是: file_object = open(file_name, access_mode='r', buffering=-1) file_name 是包含要打开的文件名字的字符串, 它可以是相对路径或者绝对路径. 可选变量 access_mode 也是一个字符串, 代表文件打开的模式. 通常, 文件使用模式 'r', 'w', 或是 'a' 模式来打开, 分别代表读取, 写入和追加. 还有个 'U' 模式, 代表通用换行符支持(见下). 使用 'r' 或 'U' 模式打开的文件必须是已经存在的. 使用 'w' 模式打开的文件若存在则首 先清空, 然后(重新)创建. 以 'a' 模式打开的文件是为追加数据作准备的, 所有写入的数据都将 追加到文件的末尾. 即使你 seek 到了其它的地方. 如果文件不存在, 将被自动创建, 类似以 'w' 模式打开文件. 如果你是一个 C 程序员, 就会发现这些也是 C 库函数 fopen() 中使用的模式. 其它 fopen() 支持的模式也可以工作在 Python 的 open() 下. 包括 '+' 代表可读可写, 'b' 代表二进制模式访问. 关于 'b' 有一点需要说明, 对于所有 POSIX 兼容的 Unix 系统(包括 Linux)来说, 'b'是可由可无的, 因为它们把所有的文件当作二进制文件, 包括文本文件. 下面是 从 Linux 手册的 fopen() 函数使用中摘录的一段, Python 语言中的 open() 函数就是从它衍生 出的: 指示文件打开模式的字符串中也可以包含字符 "b" , 但它不能做为第一个字符出现.这样做的 目的是为了严格地满足 ANSI C3.159-1989 (即 ANSI C)中的规定; 事实上它没有任何效果, 所有 POSIX 兼容系统, 包括 Linux , 都会忽略 "b" (其它系统可能会区分文本文件和二进制文件, 如果 你要处理一个二进制文件, 并希望你的程序可以移植到其它非 Unix 的环境中, 加上"b" 会是不错的 主意)。 你可以在表 9.1 中找到关于文件访问模式的详细列表, 包括 'b' 的使用 - 如果你选择使用 它的话. 如果没有给定 access_mode , 它将自动采用默认值 'r' . 另外一个可选参数 buffering 用于指示访问文件所采用的缓冲方式. 其中 0 表示不缓冲, 1 表示只缓冲一行数据, 任何其它大于 1 的值代表使用给定值作为缓冲区大小. 不提供该参数或者 给定负值代表使用系统默认缓冲机制, 既对任何类电报机( tty )设备使用行缓冲, 其它设备使用正 常缓冲. 一般情况下使用系统默认方式即可. 表 9.1 文件对象的访问模式 文件模式 操作 r 以读方式打开 Edit By Vheavens  Edit By Vheavens  rU 或 Ua 以读方式打开, 同时提供通用换行符支持 (PEP 278) w 以写方式打开 (必要时清空) a 以追加模式打开 (从 EOF 开始, 必要时创建新文件) r+ 以读写模式打开 w+ 以读写模式打开 (参见 w ) a+ 以读写模式打开 (参见 a ) rb 以二进制读模式打开 wb 以二进制写模式打开 (参见 w ) ab 以二进制追加模式打开 (参见 a ) rb+ 以二进制读写模式打开 (参见 r+ ) wb+ 以二进制读写模式打开 (参见 w+ ) ab+ 以二进制读写模式打开 (参见 a+ ) a. Python 2.3 中新增 这里是一些打开文件的例子: fp = open('/etc/motd') # 以读方式打开 fp = open('test', 'w') # 以写方式打开 fp = open('data', 'r+') # 以读写方式打开 fp = open(r'c:\io.sys', 'rb') # 以二进制读模式打开 9.2.1 工厂函数 file() 在 Python 2.2 中,类型和类被统一了起来,这时,加入了内建函数 file(). 当时, 很多的内 建类型没有对应的内建函数来创建对象的实例。例如 dict(), bool(), file(), 等等, 然而,另一 些却有对应的内建函数, 例如 list(), str(), 等等. open() 和 file() 函数具有相同的功能, 可以任意替换. 您所看到任何使用 open() 的地方, 都可以使用 file() 替换它. 可以预见, 在 将来的 Python 版本中, open() 和 file() 函数会同时存在, 完成相同的功能. 一般说来, 我们建议使用 open() 来读写文件, 在您想说明您在处理文件对象时使用 file() , 例 如 if instance(f, file) . 9.2.2 通用换行符支持(UNS) 在下一个核心笔记中, 我们将介绍如何使用 os 模块的一些属性来帮助你在不同平台下访问文 件, 不同平台用来表示行结束的符号是不同的, 例如 \n, \r, 或者 \r\n . 所以, Python 的解释 Edit By Vheavens  Edit By Vheavens  器也要处理这样的任务, 特别是在导入模块时分外重要。 你难道不希望 Python 用相同的方式处理 所有文件吗? 这就是 UNS 的关键所在, 作为 PEP 278 的结果, Python 2.3 引入了 UNS. 当你使用 'U' 标志 打开文件的时候, 所有的行分割符(或行结束符, 无论它原来是什么)通过 Python 的输入方法(例 如 read*() )返回时都会被替换为换行符 NEWLINE(\n). ('rU' 模式也支持 'rb' 选项) . 这个特性 还支持包含不同类型行结束符的文件. 文件对象的 newlines 属性会记录它曾“看到的”文件的行结 束符. 如果文件刚被打开, 程序还没有遇到行结束符, 那么文件的 newlines 为 None .在第一行被读 取后, 它被设置为第一行的结束符. 如果遇到其它类型的行结束符, 文件的 newlines 会成为一个 包含每种格式的元组. 注意 UNS 只用于读取文本文件. 没有对应的处理文件输出的方法. 在编译 Python 的时候,UNS 默认是打开的. 如果你不需要这个特性, 在运行 configure 脚本 时,你可以使用 --without-universal-newlines 开关关闭它. 如果你非要自己处理行结束符, 请 查阅核心笔记,使用 os 模块的相关属性. 9.3 文件内建方法 open() 成功执行并返回一个文件对象之后, 所有对该文件的后续操作都将通过这个"句柄"进 行. 文件方法可以分为四类: 输入, 输出, 文件内移动, 以及杂项操作. 所有文件对象的总结被 列在了表 9.3 . 我们现在来讨论每个类的方法. 9.3.1 输入 read() 方法用来直接读取字节到字符串中, 最多读取给定数目个字节. 如果没有给定 size 参数(默认值为 -1)或者 size 值为负, 文件将被读取直至末尾. 未来的某个版本可能会删除此方 法. readline() 方法读取打开文件的一行(读取下个行结束符之前的所有字节). 然后整行,包括行 结束符,作为字符串返回. 和 read() 相同, 它也有一个可选的 size 参数, 默认为 -1, 代表读至 行结束符. 如果提供了该参数, 那么在超过 size 个字节后会返回不完整的行. readlines() 方法并不像其它两个输入方法一样返回一个字符串. 它会读取所有(剩余的)行然 后把它们作为一个字符串列表返回. 它的可选参数 sizhint 代表返回的最大字节大小. 如果它大 于 0 , 那么返回的所有行应该大约有 sizhint 字节(可能稍微大于这个数字, 因为需要凑齐缓冲区 大小). Edit By Vheavens  Edit By Vheavens  Python 2.1 中加入了一个新的对象类型用来高效地迭代文件的行: xreadlines 对象(可以在 xreadlines 模块中找到). 调用 file.xreadlines() 等价于 xreadlines.xreadlines(file). xreadlines() 不是一次性读取取所有的行, 而是每次读取一块, 所以用在 for 循环时可以减少对 内存的占用. 不过, 随着 Python 2.3 中迭代器和文件迭代的引入, 没有必要再使用 xreadlines() 方法, 因为它和使用 iter(file) 的效果是一样的, 或者在 for 循环中, 使用 for eachLine in file 代替它. 它来得容易,去得也快。 另个废弃的方法是 readinto() , 它读取给定数目的字节到一个可写的缓冲器对象, 和废弃的 buffer() 内建函数返回的对象是同个类型. (由于 buffer() 已经不再支持, 所以 readinto() 被 废弃.) 9.3.2 输出 write() 内建方法功能与 read() 和 readline() 相反. 它把含有文本数据或二进制数据块的 字符串写入到文件中去. 和 readlines() 一样,writelines() 方法是针对列表的操作, 它接受一个字符串列表作为参 数, 将它们写入文件. 行结束符并不会被自动加入, 所以如果需要的话, 你必须在调用 writelines()前给每行结尾加上行结束符. 注意这里并没有 "writeline()" 方法, 因为它等价于使用以行结束符结尾的单行字符串调用 write() 方法. 核心笔记:保留行分隔符 当使用输入方法如 read() 或者 readlines() 从文件中读取行时, Python 并不会删除行结束 符. 这个操作被留给了程序员. 例如这样的代码在 Python 程序中很常见: f = open('myFile', 'r') data = [line.strip() for line in f.readlines()] f.close() 类似地, 输出方法 write() 或 writelines() 也不会自动加入行结束符. 你应该在向文件写 入数据前自己完成: 9.3.3 文件内移动 seek() 方法(类似 C 中的 fseek() 函数)可以在文件中移动文件指针到不同的位置. offset 字节代表相对于某个位置偏移量. 位置的默认值为 0 , 代表从文件开头算起(即绝对偏移量), 1 代 表从当前位置算起, 2 代表从文件末尾算起. 如果你是一个 C 程序员,并且使用过了 fseek() , 那 Edit By Vheavens  Edit By Vheavens  么,0, 1, 2 分别对应着常量 SEEK_SET, SEEK_CUR, 以及 SEEK_END. 当人们打开文件进行读写操 作的时候就会接触到 seek()方法。 text() 方法是对 seek() 的补充; 它告诉你当前文件指针在文件中的位置 - 从文件起始算起, 单位为字节. 9.3.4 文件迭代 一行一行访问文件很简单: for eachLine in f: : 在这个循环里, eachLine 代表文本文件的一行(包括末尾的行结束符),你可以使用它做任何想 做的事情. 在 Python 2.2 之前, 从文件中读取行的最好办法是使用 file.readlines() 来读取所有数据, 这样程序员可以尽快释放文件资源. 如果不需要这样做, 那么程序员可以调用 file.readline() 一次读取一行. 曾有一段很短的时间, file.xreadlines() 是读取文件最高效的方法. 在 Python 2.2 中, 我们引进了迭代器和文件迭代, 这使得一切变得完全不同, 文件对象成为 了它们自己的迭代器, 这意味着用户不必调用 read*() 方法就可以在 for 循环中迭代文件的每一行. 另外我们也可以使用迭代器的 next 方法, file.next() 可以用来读取文件的下一行. 和其它迭代 器一样, Python 也会在所有行迭代完成后引发 StopIteration 异常. 所以请记得, 如果你见到这样的代码, 这是"完成事情的老方法", 你可以安全地删除对 readline() 的调用. for eachLine in f.readline(): : 文件迭代更为高效, 而且写(和读)这样的 Python 代码更容易. 如果你是 Python 新人, 那 么请使用这些新特性, 不必担心它们过去是如何. 9.3.5 其它 close() 通过关闭文件来结束对它的访问. Python 垃圾收集机制也会在文件对象的引用计数降 至零的时候自动关闭文件. 这在文件只有一个引用时发生, 例如 fp = open(...), 然后 fp 在原文 Edit By Vheavens  Edit By Vheavens  件显式地关闭前被赋了另一个文件对象. 良好的编程习惯要求在重新赋另个文件对象前关闭这个文 件. 如果你不显式地关闭文件, 那么你可能丢失输出缓冲区的数据. fileno() 方法返回打开文件的描述符. 这是一个整数, 可以用在如 os 模块( os.read() )的 一些底层操作上. 调用 flush() 方法会直接把内部缓冲区中的数据立刻写入文件, 而不是被动地等待输出缓冲 区被写入. isatty() 是一个布尔内建函数, 当文件是一个类 tty 设备时返回 True , 否则返回 False . truncate() 方法将文件截取到当前文件指针位置或者到给定 size , 以字节为单位. 9.3.6 文件方法杂项 我们现在重新实现第二章中的第一个文件例子: filename = raw_input('Enter file name: ') f = open(filename, 'r') allLines = f.readlines() f.close() for eachLine in allLines: print eachLine, # suppress print’s NEWLINE 我们曾经介绍过这个程序. 与大多数标准的文件访问方法相比, 它的不同在于它读完所有的行 才开始向屏幕输出数据. 很明显如果文件很大, 这个方法并不好. 这时最好还是回到最可靠的方法: 使用文件迭代器, 每次只读取和显示一行: filename = raw_input('Enter file name: ') f = open(filename, 'r') for eachLine in f: print eachLine, f.close() 核心笔记: 行分隔符和其它文件系统的差异 操作系统间的差异之一是它们所支持的行分隔符不同. 在 POSIX (Unix 系列或 Mac OS X)系统 上, 行分隔符是 换行符 NEWLINE ( \n ) 字符. 在旧的 MacOS 下是 RETURN ( \r ) , 而 DOS 和 Wind32 系统下结合使用了两者 ( \r\n ). 检查一下你所使用的操作系统用什么行分隔符。 另个不同是路径分隔符(POSIX 使用 "/", DOS 和 Windows 使用 "\", 旧版本的 MacOS 使用 ":"), 它用来分隔文件路径名, 标记当前目录和父目录. 当我们创建要跨这三个平台的应用的时候, 这些差异会让我们感觉非常麻烦(而且支持的平台 越多越麻烦)。幸运的是 Python 的 os 模块设计者已经帮我们想到了这些问题. os 模块有五个很 有用的属性. 它们被列在了表 9.2 中. Edit By Vheavens  Edit By Vheavens  Table 9.2 有助于跨平台开发的 os 模块属性 os 模块属性 描述 linesep 用于在文件中分隔行的字符串 sep 用来分隔文件路径名的字符串 pathsep 用于分隔文件路径的字符串 curdir 当前工作目录的字符串名称 pardir (当前工作目录的)父目录字符串名称 不管你使用的是什么平台, 只要你导入了 os 模块, 这些变量自动会被设置为正确的值, 减少 了你的麻烦. 还要提醒大家的是: print 语句默认在输出内容末尾后加一个换行符, 而在语句后加一个逗号 就可以避免这个行为. readline() 和 readlines() 函数不对行里的空白字符做任何处理(参见本章 练习), 所以你有必要加上逗号. 如果你省略逗号, 那么显示出的文本每行后会有两个换行符, 其 中一个是输入是附带的, 另个是 print 语句自动添加的. 文件对象还有一个 truncate() 方法, 它接受一个可选的 size 作为参数. 如果给定, 那么文 件将被截取到最多 size 字节处. 如果没有传递 size 参数, 那么默认将截取到文件的当前位置. 例如, 你刚打开了一个文件, 然后立即调用 truncate() 方法, 那么你的文件(内容)实际上被删除, 这时候你是其实是从 0 字节开始截取的( tell() 将会返回这个数值 ). 在学习下一小节之前, 我们再来看两个例子, 第一个展示了如何输出到文件, 第二个展示了 文件的输出和输入, 以及用于文件定位的 seek() 和 tell() 方法的使用. filename = raw_input('Enter file name: ') fobj = open(filename, 'w') while True: aLine = raw_input("Enter a line ('.' to quit): ") if aLine != ".": fobj.write('%s%s' % (aLine, os.linesep) else: break fobj.close() 这里我们每次从用户接收一行输入, 然后将文本保存到文件中. 由于 raw_input()不会保留用 户 输 入 的 换 行 符 , 调 用 write() 方 法 时 必 须 加 上 换 行 符 。 而 且 , 在 键 盘 上 很 难 输 入 一 个 EOF(end-of-file)字符,所以,程序使用句号( . )作为文件结束的标志, 当用户输入句号后会自动 结束输入并关闭文件. 第二个例子以可读可写模式创建一个新的文件(可能是清空了一个现有的文件). 在向文件写入 数据后, 我们使用 seek() 方法在文件内部移动, 使用 tell() 方法展示我们的移动过程. Edit By Vheavens  Edit By Vheavens  >>> f = open('/tmp/x', 'w+') >>> f.tell() 0 >>> f.write('test line 1\n') # 加入一个长为 12 的字符串 [0-11] >>> f.tell() 12 >>> f.write('test line 2\n') # 加入一个长为 12 的字符串 [12-23] >>> f.tell() # 告诉我们当前的位置 24 >>> f.seek(-12, 1) # 向后移 12 个字节 >>> f.tell() # 到了第二行的开头 12 >>> f.readline() 'test line 2\012' >>> f.seek(0, 0) # 回到最开始 >>> f.readline() 'test line 1\012' >>> f.tell() # 又回到了第二行 12 >>> f.readline() 'test line 2\012' >>> f.tell() # 又到了结尾 24 >>> f.close() # 关闭文件 表 9.3 文件对象的内建方法列表 文件对象的方法 操作 file.close() 关闭文件 file.fileno() 返回文件的描述符(file descriptor ,FD, 整数值) file.flush() 刷新文件的内部缓冲区 file.isatty() 判断 file 是否是一个类 tty 设备 file.nexta() 返回文件的下一行(类似于 file.readline() ), 或在没有其它行时 引发 StopIteration 异常 file.read(size=-1) 从文件读取 size 个字节, 当未给定 size 或给定负值的时候, 读 取剩余的所有字节, 然后作为字符串返回 file.readintob(buf, size) 从文件读取 size 个字节到 buf 缓冲器(已不支持) file.readline(size=-1) 从文件中读取并返回一行(包括行结束符), 或返回最大 size Edit By Vheavens  Edit By Vheavens  个字符 file.readlines(sizhint=0) 读取文件的所有行并作为一个列表返回(包含所有的行结束 符); 如果给定 sizhint 且大于 0 , 那么将返回总和大约为 sizhint 字节的行(大小由缓冲器容量的下一个值决定)( 比 如说缓冲器的大小只能为 4K 的倍数,如果 sizhint 为 15k,则 最后返回的可能是 16k———译者按) file.xreadlinesc() 用于迭代, 可以替换 readlines() 的一个更高效的方法 file.seek(off, whence=0) 在文件中移动文件指针, 从 whence ( 0 代表文件其始, 1 代 表当前位置, 2 代表文件末尾)偏移 off 字节 file.tell() 返回当前在文件中的位置 file.truncate(size=file.tell()) 截取文件到最大 size 字节, 默认为当前文件位置 file.write(str) 向文件写入字符串 file.writelines(seq) 向文件写入字符串序列 seq ; seq 应该是一个返回字符串的 可迭代对象; 在 2.2 前, 它只是字符串的列表 a. Python 2.2 中新增 b. Python 1.5.2 中新增, 不再支持 c. Python 2.1 中新增, 在 Python 2.3 中废弃 9.4 文件内建属性 文件对象除了方法之外,还有一些数据属性. 这些属性保存了文件对象相关的附加数据, 例如 文件名(file.name ), 文件的打开模式 ( file.mode ), 文件是否已被关闭 ( file.closed), 以及 一个标志变量, 它可以决定使用 print 语句打印下一行前是否要加入一个空白字符 ( file.softspace ). 表 9.4 列出了这些属性并做了简短说明。 表 9.4 文件对象的属性 文件对象的属性 描述 file.closed True 表示文件已经被关闭, 否则为 False file.encodinga 文件所使用的编码 - 当 Unicode 字符串被写入数据时, 它们将自动使 用 file.encoding 转换为字节字符串; 若 file.encoding 为 None 时使 用系统默认编码 file.mode 文件打开时使用的访问模式 file.name 文件名 file.newlinesa 未读取到行分隔符时为 None , 只有一种行分隔符时为一个字符串, 当 文件有多种类型的行结束符时,则为一个包含所有当前所遇到的行结束 符的列表 file.softspace 为 0 表示在输出一数据后,要加上一个空格符,1 表示不加。这个属性 Edit By Vheavens  Edit By Vheavens  一般程序员用不着,由程序内部使用。 a. New in Python 2.3. 9.5 标准文件 一般说来, 只要你的程序一执行, 那么你就可以访问三个标准文件. 它们分别是标准输入(一 般是键盘), 标准输出(到显示器的缓冲输出)和标准错误(到屏幕的非缓冲输出). (这里所说的"缓冲 "和"非缓冲"是指 open() 函数的第三个参数.) 这些文件沿用的是 C 语言中的命名, 分别为 stdin , stdout 和 stderr . 我们说"只要你的程序一执行就可以访问这三个标准文件", 意思是这 些文件已经被预先打开了, 只要知道它们的文件句柄就可以随时访问这些文件. Python 中可以通过 sys 模块来访问这些文件的句柄. 导入 sys 模块以后, 就可以使用 sys.stdin , sys.stdout 和 sys.stderr 访问. print 语句通常是输出到 sys.stdout ; 而内建 raw_input() 则通常从 sys.stdin 接受输入. 记得 sys.* 是文件, 所以你必须自己处理好换行符. 而 print 语句会自动在要输出的字符串 后加上换行符。 9.6 命令行参数 sys 模块通过 sys.argv 属性提供了对命令行参数的访问。 命令行参数是调用某个程序时除程 序名以外的其它参数. 这样命名是有历史原因的, 在一个基于文本的环境里(比如 UNIX 操作系统 的 shell 环境或者 DOS-shell ), 这些参数和程序的文件名一同被输入的. 但在 IDE 或者 GUI 环 境中可能就不会是这样了, 大多 IDE 环境都提供一个用来输入"命令行参数"的窗口; 这些参数最 后会像命令行上执行那样被传递给程序. 熟悉 C 语言的读者可能会问了, "argc 哪去了?" argc 和 argv 分别代表参数个数(argument count)和参数向量(argument vector). argv 变量代表一个从命令行上输入的各个参数组成的字符 串数组; argc 变量代表输入的参数个数. 在 Python 中, argc 其实就是 sys.argv 列表的长度, 而该列表的第一项 sys.argv[0] 永远是程序的名称. 总结如下: z sys.argv 是命令行参数的列表 z len(sys.argv) 是命令行参数的个数(也就是 argc) 我们来创建这个名为 argv.py 的测试程序: Edit By Vheavens  Edit By Vheavens  import sys print 'you entered', len(sys.argv), 'arguments...' print 'they were:', str(sys.argv) 下面是该脚本程序运行的输出: $ argv.py 76 tales 85 hawk you entered 5 arguments... they were: ['argv.py', '76', 'tales', '85', 'hawk'] 命令行参数有用吗? Unix 操作系统中的命令通常会接受输入, 执行一些功能, 然后把结果作为 流输出出来. 这些输出的结果还可能被作为下一个程序的输入数据, 在完成了一些其它处理后, 再 把新的输出送到下一个程序, 如此延伸下去. 各个程序的输出一般是不保存的, 这样可以节省大量 的磁盘空间, 各个程序的输出通常使用"管道"实现到下个程序输入的转换. 这是通过向命令行提供数据或是通过标准输入实现的. 当一个程序显示或是发送它的输出到标 准输出文件时, 内容就会出现在屏幕上 - 除非该程序被管道连接到下一个程序, 那么此时程序的 标准输出就成为下个程序的标准输入. 你现在明白了吧? 命令行参数使程序员可以在启动一个程序的时候对程序行为做出选择. 在大多情况下, 这些 执行操作都不需要人为干预, 通过批处理执行. 命令行参数配合程序选项可以实现这样的处理功 能. 让计算机在夜里有空闲时完成一些需要大量处理的工作. Python 还提供了两个模块用来辅助处理命令行参数. 其中一个(最原始的)是 getopt 模块, 它更简单些, 但是不是很精细. 而 Python 2.3 引入的 optparse 模块提供了一个更强大的工具, 而且它更面向对象. 如果你只是用到一些简单的选项, 我们推荐 getopt , 但如果你需要提供复杂 的选项, 那么请参阅 optparse . 9.7 文件系统 对文件系统的访问大多通过 Python 的 os 模块实现. 该模块是 Python 访问操作系统功能的主 要接口. os 模块实际上只是真正加载的模块的前端, 而真正的那个"模块"明显要依赖与具体的操作 系统. 这个"真正"的模块可能是以下几种之一: posix (适用于 Unix 操作系统), nt (Win32), mac(旧版本的 MacOS), dos (DOS), os2 (OS/2), 等. 你不需要直接导入这些模块. 只要导入 os 模 块, Python 会为你选择正确的模块, 你不需要考虑底层的工作. 根据你系统支持的特性, 你可能无 法访问到一些在其它系统上可用的属性. Edit By Vheavens  Edit By Vheavens  除了对进程和进程运行环境进行管理外, os 模块还负责处理大部分的文件系统操作, 应用程序 开发人员可能要经常用到这些. 这些功能包括删除/重命名文件, 遍历目录树, 以及管理文件访问 权限等. 表 9.5 列出 os 模块提供的一些常见文件或目录操作函数. 另一个模块 os.path 可以完成一些针对路径名的操作. 它提供的函数可以完成管理和操作文 件路径名中的各个部分, 获取文件或子目录信息, 文件路径查询等操作. 表 9.6 列出了 os.path 中的几个比较常用的函数. 这两个模块提供了与平台和操作系统无关的统一的文件系统访问方法. 例 9.1 (ospathex.py) 展示了 os 和 os.path 模块中部分函数的使用. 表 9.5 os 模块的文件/目录访问函数 函数 描述 文件处理 mkfifo()/mknod()a 创建命名管道/创建文件系统节点 remove()/unlink() Delete file 删除文件 rename()/renames()b 重命名文件 *statc() 返回文件信息 symlink() 创建符号链接 utime() 更新时间戳 tmpfile() 创建并打开('w+b')一个新的临时文件 walk()a 生成一个目录树下的所有文件名 目录/文件夹 chdir()/fchdir()a 改变当前工作目录/通过一个文件描述符改变当前工作目录 chroot()d 改变当前进程的根目录 listdir() 列出指定目录的文件 getcwd()/getcwdu()a 返回当前工作目录/功能相同, 但返回一个 Unicode 对象 mkdir()/makedirs() 创建目录/创建多层目录 rmdir()/removedirs() 删除目录/删除多层目录 访问/权限 access() 检验权限模式 chmod() 改变权限模式 chown()/lchown()a 改变 owner 和 group ID/功能相同, 但不会跟踪链接 umask() 设置默认权限模式 文件描述符操作 open() 底层的操作系统 open (对于文件, 使用标准的内建 open() 函数) read()/write() 根据文件描述符读取/写入数据 dup()/dup2() 复制文件描述符号/功能相同, 但是是复制到另一个文件描述符 设备号 makedev()a 从 major 和 minor 设备号创建一个原始设备号 Edit By Vheavens  Edit By Vheavens  major()a /minor()a 从原始设备号获得 major/minor 设备号 a. New in Python 2.3. b. New in Python 1.5.2. c. Includes stat(), lstat(), xstat(). d. New in Python 2.2. 表 9.6 os.path 模块中的路径名访问函数 函数 描述 分隔 basename() 去掉目录路径, 返回文件名 dirname() 去掉文件名, 返回目录路径 join() 将分离的各部分组合成一个路径名 split() 返回 (dirname(), basename()) 元组 splitdrive() 返回 (drivename, pathname) 元组 splitext() 返回 (filename, extension) 元组 信息 getatime() 返回最近访问时间 getctime() 返回文件创建时间 getmtime() 返回最近文件修改时间 getsize() 返回文件大小(以字节为单位) 查询 exists() 指定路径(文件或目录)是否存在 isabs() 指定路径是否为绝对路径 isdir() 指定路径是否存在且为一个目录 isfile() 指定路径是否存在且为一个文件 islink() 指定路径是否存在且为一个符号链接 ismount() 指定路径是否存在且为一个挂载点 samefile() 两个路径名是否指向同个文件 例 9.1 os 和 os.path 模块例子(ospathex.py) 这段代码练习使用一些 os 和 os.path 模块中的功能. 它创建一个文本文件, 写入少量数据, 然后重命名, 输出文件内容. 同时还进行了一些辅助性的文件操作, 比如遍历目录树和文件路径名 处理. 1 #!/usr/bin/env python 2 3 import os Edit By Vheavens  Edit By Vheavens  4 for tmpdir in ('/tmp', r'c:\temp'): 5 if os.path.isdir(tmpdir): 6 break 7 else: 8 print 'no temp directory available' 9 tmpdir = '' 10 11 if tmpdir: 12 os.chdir(tmpdir) 13 cwd = os.getcwd() 14 print '*** current temporary directory' 15 print cwd 16 17 print '*** creating example directory...' 18 os.mkdir('example') 19 os.chdir('example') 20 cwd = os.getcwd() 21 print '*** new working directory:' 22 print cwd 23 print '*** original directory listing:' 24 print os.listdir(cwd) 25 26 print '*** creating test file...' 27 fobj = open('test', 'w') 28 fobj.write('foo\n') 29 fobj.write('bar\n') 30 fobj.close() 31 print '*** updated directory listing:' 32 print os.listdir(cwd) 33 34 print "*** renaming 'test' to 'filetest.txt'" 35 os.rename('test', 'filetest.txt') 36 print '*** updated directory listing:' 37 print os.listdir(cwd) 38 39 path = os.path.join(cwd, os.listdir (cwd)[0]) 40 print '*** full file pathname' 41 print path 42 print '*** (pathname, basename) ==' 43 print os.path.split(path) Edit By Vheavens  Edit By Vheavens  44 print '*** (filename, extension) ==' 45 print os.path.splitext(os.path.basename(path)) 46 47 print '*** displaying file contents:' 48 fobj = open(path) 49 for eachLine in fobj: 50 print eachLine, 51 fobj.close() 52 53 print '*** deleting test file' 54 os.remove(path) 55 print '*** updated directory listing:' 56 print os.listdir(cwd) 57 os.chdir(os.pardir) 58 print '*** deleting test directory' 59 os.rmdir('example') 60 print '*** DONE' os 的子模块 os.path 更多用于文件路径名处理. 比较常用的属性列于表 9.6 中. 在 Unix 平台下执行该程序, 我们会得到如下输出: $ ospathex.py *** current temporary directory /tmp *** creating example directory... *** new working directory: /tmp/example *** original directory listing: [] *** creating test file... *** updated directory listing: ['test'] *** renaming 'test' to 'filetest.txt' *** updated directory listing: ['filetest.txt'] *** full file pathname: /tmp/example/filetest.txt *** (pathname, basename) == ('/tmp/example', 'filetest.txt') *** (filename, extension) == Edit By Vheavens  Edit By Vheavens  ('filetest', '.txt') *** displaying file contents: foo bar *** deleting test file *** updated directory listing: [] *** deleting test directory *** DONE 在 DOS 窗口下执行这个例子我们会得到非常相似的输出: C:\>python ospathex.py *** current temporary directory c:\windows\temp *** creating example directory... *** new working directory: c:\windows\temp\example *** original directory listing: [] *** creating test file... *** updated directory listing: ['test'] *** renaming 'test' to 'filetest.txt' *** updated directory listing: ['filetest.txt'] *** full file pathname: c:\windows\temp\example\filetest.txt *** (pathname, basename) == ('c:\\windows\\temp\\example', 'filetest.txt') *** (filename, extension) == ('filetest', '.txt') *** displaying file contents: foo bar *** deleting test file *** updated directory listing: [] *** deleting test directory *** DONE 这里就不逐行解释这个例子了, 我们把这个留给读者做练习. 下面我们来看看一个类似的交互 式例子(包括错误), 我们会把代码分成几个小段, 然后依次进行讲解. Edit By Vheavens  Edit By Vheavens  >>> import os >>> os.path.isdir('/tmp') True >>> os.chdir('/tmp') >>> cwd = os.getcwd() >>> cwd '/tmp' 代码的第一部分导入了 os 模块(同时也包含 os.path 模块). 然后检查并确认 '/tmp' 是一 个合法的目录, 并切换到这个临时目录开始我们的工作. 之后我们用 getcwd() 方法确认我们当前 位置. >>> os.mkdir('example') >>> os.chdir('example') >>> cwd = os.getcwd() >>> cwd '/tmp/example' >>> >>> os.listdir() # oops, forgot name Traceback (innermost last): File "", line 1, in ? TypeError: function requires at least one argument >>> >>> os.listdir(cwd) # that's better :) [] 接下来, 我们在临时目录里创建了一个子目录, 然后用 listdir() 方法确认目录为空(因为我 们刚创建它). 第一次调用 listdir() 调用时出现的问题是因为我们没有传递要列目录的路径名. 我们马上在第二次调用时修正了这个失误. >>> fobj = open('test', 'w') >>> fobj.write('foo\n') >>> fobj.write('bar\n') >>> fobj.close() >>> os.listdir(cwd) ['test'] 这里我们创建了一个有两行内容的 test 文件, 之后列目录确认文件被成功创建. >>> os.rename('test', 'filetest.txt') Edit By Vheavens  Edit By Vheavens  >>> os.listdir(cwd) ['filetest.txt'] >>> >>> path = os.path.join(cwd, os.listdir(cwd)[0]) >>> path '/tmp/example/filetest.txt' >>> >>> os.path.isfile(path) True >>> os.path.isdir(path) False >>> >>> os.path.split(path) ('/tmp/example', 'filetest.txt') >>> >>> os.path.splitext(os.path.basename(path)) ('filetest', '.ext') 这一段代码使用了 os.path 的一些功能, 包括我们之前看到过的 join(), isfile(), isdir(), split(), basename(), 以及 splitext() . 我们还调用了 os 下的 rename() 函数. 接下来, 我们 显示文件的内容, 之后, 删除之前创建的文件和目录: >>> fobj = open(path) >>> for eachLine in fobj: ... print eachLine, ... foo bar >>> fobj.close() >>> os.remove(path) >>> os.listdir(cwd) [] >>> os.chdir(os.pardir) >>> os.rmdir('example') 核心模块: os (和 os.path ) 从上面这些长篇讨论可以看出, os 和 os.path 模块提供了访问计算机文件系统的不同方法. 我们在本章学习的只是文件访问方面, 事实上 os 模块可以完成更多工作. 我们可以通过它管理进 程环境, 甚至可以让一个 Python 程序直接与另外一个执行中的程序"对话". 你很快就会发现自己 离不开这个模块了. 更多关于 os 模块的内容请参阅第 14 章. 9.8 文件执行 Edit By Vheavens  Edit By Vheavens  无论你只是想简单地运行一个操作系统命令, 调用一个二进制可执行文件, 或者其它类型的脚 本(可能是 shell 脚本, Perl, 或是 Tcl/Tk), 都需要涉及到运行系统其它位置的其它文件. 尽管 不经常出现,但是有时甚至会需要启动另外一个 Python 解释器.我们将把这部分内容留到第 14 章 去讨论. 如果读者有兴趣了解如何启动其它程序,以及如何与它们进行通讯, 或者是 Python 执行 环境的一般信息, 都可以在 14 章里找到答案. 9.9 永久存储模块 在本书的很多练习里, 都需要用户输入数据. 这可能需要用户多次输入重复的数据. 尤其是如 果你要输入大批数据供以后使用时, 你肯定会厌烦这样做. 这就是永久储存大显身手的地方了, 它 可以把用户的数据归档保存起来供以后使用, 这样你就可以避免每次输入同样的信息. 在简单的磁 盘文件已经不能满足你的需要, 而使用完整的关系数据库管理系统(relational database management systems 即 RDBMS) 又有些大材小用时, 简单的永久性储存就可以发挥它的作用. 大部 分永久性储存模块是用来储存字符串数据的, 但是也有方法来归档 Python 对象. 9.9.1 pickle 和 marshal 模块 Python 提供了许多可以实现最小化永久性储存的模块. 其中的一组( marshal 和 pickle )可 以用来转换并储存 Python 对象. 该过程将比基本类型复杂的对象转换为一个二进制数据集合, 这样就可以把数据集合保存起来或通过网络发送, 然后再重新把数据集合恢复原来的对象格式. 这个过程也被称为数据的扁平化, 数据的序列化, 或者数据的顺序化. 另外一些模块 (dbhash/bsddb, dbm, gdbm, dumbdbm 等)以及它们的"管理器"( anydbm )只提供了 Python 字 符串的永久性储存. 而最后一个模块( shelve ) 则两种功能都具备. 我们已经提到 marshal 和 pickle 模块都可以对 Python 对象进行储存转换. 这些模块本身 并没有提供"永久性储存"的功能, 因为它们没有为对象提供名称空间, 也没有提供对永久性储存对 象的并发写入访问( concurrent write access ). 它们只能储存转换 Python 对象, 为保存和传输 提供方便. 数据储存是有次序的(对象的储存和传输是一个接一个进行的). marshal 和 pickle 模 块的区别在于 marshal 只能处理简单的 Python 对象(数字, 序列, 映射, 以及代码对象), 而 pickle 还可以处理递归对象, 被不同地方多次引用的对象, 以及用户定义的类和实例. pickle 模 块还有一个增强的版本叫 cPickle , 使用 C 实现了相关的功能. 9.9.2 DBM 风格的模块 *db* 系列的模块使用传统的 DBM 格式写入数据, Python 提供了 DBM 的多种实现: dbhash/bsddb, dbm, gdbm, 以及 dumbdbm 等. 你可以随便按照你的爱好使用, 如果你不确定 的话, 那么最好使用 anydbm 模块, 它会自动检测系统上已安装的 DBM 兼容模块, 并选择"最好" Edit By Vheavens  Edit By Vheavens  的一个. dumbdbm 模块是功能最少的一个, 在没有其它模块可用时, anydbm 才会选择它. 这些模块 为用户的对象提供了一个命名空间, 这些对象同时具备字典对象和文件对象的特点. 不过不足之处 在于它们只能储存字符串, 不能对 Python 对象进行序列化. 9.9.3 shelve 模块 最后, 我们来看一个更为完整的解决方案, shelve 模块. shelve 模块使用 anydbm 模块寻找 合适的 DBM 模块, 然后使用 cPickle 来完成对储存转换过程. shelve 模块允许对数据库文件进行 并发的读访问, 但不允许共享读/写访问. 这也许是我们在 Python 标准库里找到的最接近于永久 性储存的东西了. 可能有一些第三方模块实现了"真正"的永久性储存. 图 9-1 展示了储存转换模 块与永久性储存模块之间的关系, 以及为何 shelve 对象能成为两者的最好的选择的. 图 9-1 用于序列化和永久性储存的 Python 模块 Edit By Vheavens  Edit By Vheavens  核心模块: pickle 和 cPickle 你可以使用 pickle 模块把 Python 对象直接保存到文件里, 而不需要把它们转化为字符串, 也不用底层的文件访问操作把它们写入到一个二进制文件里. pickle 模块会创建一个 Python 语言 专用的二进制格式, 你不需要考虑任何文件细节, 它会帮你干净利索地完成读写对象操作, 唯一需 要的只是一个合法的文件句柄. pickle 模块中的两个主要函数是 dump() 和 load() . dump() 函数接受一个文件句柄和一个 数据对象作为参数, 把数据对象以特定格式保存到给定文件里. 当我们使用 load() 函数从文件中 取出已保存的对象时, pickle 知道如何恢复这些对象到它们本来的格式. 我们建议你看一看 pickle 和更"聪明"的 shelve 模块, 后者提供了字典式的文件对象访问功能, 进一步减少了程序 员的工作. cPickle 是 pickle 的一个更快的 C 语言编译版本. 9.10 相关模块 还有大量的其它模块与文件和输入/输出有关, 它们中的大多数都可以在主流平台上工作. 表 9.7 列出了一些文件相关的模块 表 9.7 文件相关模块 模块 内容 base64 提供二进制字符串和文本字符串间的编码/解码操作 binascii 提供二进制和 ASCII 编码的二进制字符串间的编码/解码操作 bz2a 访问 BZ2 格式的压缩文件 csva 访问 csv 文件(逗号分隔文件) filecmpb 用于比较目录和文件 fileinput 提供多个文本文件的行迭代器 getopt/optparsea 提供了命令行参数的解析/处理 glob/fnmatch 提供 Unix 样式的通配符匹配的功能 gzip/zlib 读写 GNU zip( gzip) 文件(压缩需要 zlib 模块) shutil 提供高级文件访问功能 c/StringIO 对字符串对象提供类文件接口 tarfilea 读写 TAR 归档文件, 支持压缩文件 tempfile 创建一个临时文件(名) uu 格式的编码和解码 zipfilec 用于读取 ZIP 归档文件的工具 a. New in Python 2.3. b. New in Python 2.0. c. New in Python 1.6. fileinput 模块遍历一组输入文件, 每次读取它们内容的一行, 类似 Perl 语言中的不带参数 Edit By Vheavens  Edit By Vheavens  的 "<>" 操作符. 如果没有明确给定文件名, 则默认从命令行读取文件名. glob 和 fnmatch 模块提供了老式 Unix shell 样式文件名的模式匹配, 例如使用星号( * )通 配符代表任意字符串, 用问号( ? )匹配任意单个字符. 核心提示: 使用 os.path.expanduser() 的波浪号 ( ~ ) 进行扩展 虽然 glob 和 fnmatch 提供了 Unix 样式的模式匹配, 但它们没有提供对波浪号(用户目录) 字符, ~ 的支持. 你可以使用 os.path.expanduser() 函数来完成这个功能, 传递一个带波浪号的 目录, 然后它会返回对应的绝对路径. 这里是两个例子, 分别运行在 Unix 和 Win32 环境下: >>> os.path.expanduser('~/py') '/home/wesley/py' >>> os.path.expanduser('~/py') 'C:\\Documents and Settings\\wesley/py' 另外 Unix 家族系统还支持 "~user" 这样的用法, 表示指定用户的目录. 还有, 注意 Win32 版本函数没有使用反斜杠来分隔目录路径. gzip 和 zlib 模块提供了对 zlib 压缩库直接访问的接口. gzip 模块是在 zlib 模块上编写 的, 不但实现了标准的文件访问, 还提供了自动的 gzip 压缩/解压缩. bz2 类似于 gzip , 用于 操作 bzip 压缩的文件. 程序员可以通过 1.6 中新增的 zipfile 模块创建, 修改和读取 zip 归档文件. ( tarfile 文件实现了针对 tar 归档文件的相同功能 ). 在 2.3 版本中, Python 加入了导入归档 zip 文件 中模块的功能. 更多细节请参阅 12.5.7 小节. shutil 模块提供高级的文件访问功能, 包括复制文件, 复制文件的访问权限, 递归地目录树 复制, 等等. tempfile 模块用于生成临时文件(名). 在关于字符串一章中, 我们介绍了 StringIO 模块(和它的 C 语言版本 cStringIO ), 并且介 绍了它是如何在字符串对象顶层加入文件操作接口的. 这个接口包括文件对象的所有标准方法. 我们在前面永久性储存一节( 9.9 节) 中介绍的模块还有文件和字典对象混合样式的例子. 其它的 Python 类文件对象还有网络和文件 socket 对象( socket 模块), 用于管道连接的 popen*() 文件对象( os 和 popen2 模块), 用于底层文件访问的 fdopen() 文件对象(os模块), 通 过 URL ( Uniform Resource Locator 统一资源定位器)建立的到指定 web 服务器的网络连接 ( urllib 模块)等. 需要注意的是并非所有的标准文件方法都能在这些对象上实现, 同样的,这些 对象也提供了一些普通文件没有的功能. Edit By Vheavens  Edit By Vheavens  具体内容请参考这些模块的相关文档. 你可以在下边这些地址中找到关于 file()/open() , 文 件, 文件对象的更多信息. http://docs.python.org/lib/built-in-funcs.html http://docs.python.org/lib/bltin-file-objects.html http://www.python.org/doc/2.3/whatsnew/node7.html http://www.python.org/doc/peps/pep-0278/ 9.11 练习 9–1. 文件过滤. 显示一个文件的所有行, 忽略以井号( # )开头的行. 这个字符被用做 Python , Perl, Tcl, 等大多脚本文件的注释符号. 附加题: 处理不是第一个字符开头的注释. 9–2. 文件访问. 提示输入数字 N 和文件 F, 然后显示文件 F 的前 N 行. 9–3. 文件信息. 提示输入一个文件名, 然后显示这个文本文件的总行数. 9–4. 文件访问. 写一个逐页显示文本文件的程序. 提示输入一个文件名, 每次显示文本 文件的 25 行, 暂停并向用户提示"按任意键继续.", 按键后继续执行. 9–5. 考试成绩. 改进你的考试成绩问题(练习 5 -3 和 6-4), 要求能从多个文件中读入考 试成绩. 文件的数据格式由你自己决定. 9–6. 文件比较. 写一个比较两个文本文件的程序. 如果不同, 给出第一个不同处的行号和 列号. 9–7. 解析文件. Win32 用户: 创建一个用来解析 Windows .ini 文件的程序. POSIX 用户: 创建一个解析 /etc/serves 文件的程序. 其它平台用户: 写一个解析特定结构的系统配置文件的 程序. 9–8. 模块研究. 提取模块的属性资料. 提示用户输入一个模块名(或者从命令行接受输入). 然后使用 dir() 和其它内建函数提取模块的属性, 显示它们的名字, 类型, 值. 9–9. Python 文档字符串. 进入 Python 标准库所在的目录. 检查每个 .py 文件看是否有 __doc__ 字符串, 如果有, 对其格式进行适当的整理归类. 你的程序执行完毕后, 应该会生成一个 漂亮的清单. 里边列出哪些模块有文档字符串, 以及文档字符串的内容. 清单最后附上那些没有文 档字符串模块的名字. 附加题: 提取标准库中各模块内全部类(class)和函数的文档. Edit By Vheavens  Edit By Vheavens  9–10. 家庭理财. 创建一个家庭理财程序. 你的程序需要处理储蓄, 支票, 金融市场, 定 期存款等多种帐户. 为每种帐户提供一个菜单操作界面, 要有存款, 取款, 借, 贷等操作. 另外还 要提供一个取消操作选项. 用户退出这个程序时相关数据应该保存到文件里去(出于备份的目的, 程序执行过程中也要备份.) 9–11. Web 站点地址. a) 编写一个 URL 书签管理程序. 使用基于文本的菜单, 用户可以添加, 修改或者删除书签数 据项. 书签数据项中包含站点的名称, URL 地址, 以及一行简单说明(可选). 另外提供检索功能, 可以根据检索关键字在站点名称和 URL 两部分查找可能的匹配. 程序退出时把数据保存到一个磁 盘文件中去; 再次执行时候加载保存的数据. b)改进 a) 的解决方案, 把书签输出到一个合法且语法正确的 HTML 文件(.html 或 htm )中, 这样用户就可以使用浏览器查看自己的书签清单. 另外提供创建"文件夹"功能, 对相关的书签进行 分组管理. 附加题: 请阅读 Python 的 re 模块了解有关正则表达式的资料, 使用正则表达式对用户输入 的 URL 进行验证. 9–12. 用户名和密码. 回顾练习 7-5 , 修改代码使之可以支持"上次登录时间". 请参阅 time 模块中的文档了解如 何记录用户上次登录的时间. 另外提供一个"系统管理员", 它可以导出所有用户的用户名, 密码 (如果想要的话,你可以把密码加密), 以及"上次登录时间". a) 数据应该保存在磁盘中, 使用冒号( : )分割, 一次写入一行, 例如 "joe:boohoo:953176591.145", 文件中数据的行数应该等于你系统上的用户数. b) 进一步改进你的程序, 不再一次写入一行, 而使用 pickle 模块保存整个数据对象. 请参 阅 pickle 模块的文档了解如何序列化/扁平化对象, 以及如何读写保存的对象. 一般来说, 这个 解决方案的代码行数要比 a) 的少. c) 使用 shelve 模块替换 pickle 模块, 由于可以省去一些维护代码,这个解决方案的代码比 b) 的更少. 9–13. 命令行参数 a) 什么是命令行参数, 它们有什么用? b) 写一个程序, 打印出所有的命令行参数. 9–14. 记录结果. 修改你的计算器程序(练习 5-6 )使之接受命令行参数. 例如: $ calc.py 1 + 2 只输出计算结果. 另外, 把每个表达式和它的结果写入到一个磁盘文件中. 当使用下面的命令 时: $ calc.py print Edit By Vheavens  Edit By Vheavens  会把记录的内容显示到屏幕上, 然后重置文件. 这里是样例展示: $ calc.py 1 + 2 3 $ calc.py 3 ^ 3 27 $ calc.py print 1 + 2 3 3 ^ 3 27 $ calc.py print $ 附加题: 处理输入时候的注释. 9–15. 复制文件. 提示输入两个文件名(或者使用命令行参数). 把第一个文件的内容复制 到第二个文件中去. 9–16. 文本处理. 人们输入的文字常常超过屏幕的最大宽度. 编写一个程序, 在一个文本 文件中查找长度大于 80 个字符的文本行. 从最接近 80 个字符的单词断行, 把剩余文件插入到 下一行处. 程序执行完毕后, 应该没有超过 80 个字符的文本行了. 9–17. 文本处理. 创建一个原始的文本文件编辑器. 你的程序应该是菜单驱动的, 有如下 这些选项: 1) 创建文件(提示输入文件名和任意行的文本输入), 2) 显示文件(把文件的内容显示到屏幕), 3) 编辑文件(提示输入要修改的行, 然后让用户进行修改), 4) 保存文件, 以及 5) 退出. 9–18. 搜索文件. 提示输入一个字节值(0 - 255)和一个文件名. 显示该字符在文件中出现 的次数. 9–19. 创建文件. 创建前一个问题的辅助程序. 创建一个随机字节的二进制数据文件, 但 某一特定字节会在文件中出现指定的次数. 该程序接受三个参数: 1) 一个字节值( 0 - 255 ), 2) 该字符在数据文件中出现的次数, 以及 3) 数据文件的总字节长度. Edit By Vheavens  Edit By Vheavens  你的工作就是生成这个文件, 把给定的字节随机散布在文件里, 并且要求保证给定字符在文件 中只出现指定的次数, 文件应精确地达到要求的长度. 9–20. 压缩文件. 写一小段代码, 压缩/解压缩 gzip 或 bzip 格式的文件. 可以使用命令 行下的 gzip 或 bzip2 以及 GUI 程序 PowerArchiver , StuffIt , 或 WinZip 来确认你的 Python 支持这两个库. 9–21. ZIP 归档文件. 创建一个程序, 可以往 ZIP 归档文件加入文件, 或从中提取文件, 有可能的话, 加入创建 ZIP 归档文件的功能. 9–22. ZIP 归档文件. unzip -l 命令显示出的 ZIP 归档文件很无趣. 创建一个 Python 脚本 lszip.py , 使它可以显示额外信息: 压缩文件大小, 每个文件的压缩比率(通过比较压缩 前后文件大小), 以及完成的 time.ctime() 时间戳, 而不是只有日期和 HH:MM . 提示: 归档文件的 date_time 属性并不完整, 无法提供给 time.mktime() 使用....这由你自 己决定. 9–23. TAR 归档文件. 为 TAR 归档文件建立类似上个问题的程序. 这两种文件的不同之处 在于 ZIP 文件通常是压缩的, 而 TAR 文件不是, 只是在 gzip 和 bzip2 的支持下才能完成压缩 工作. 加入任意一种压缩格式支持. 附加题: 同时支持 gzip 和 bzip2 . 9–24. 归档文件转换. 参考前两个问题的解决方案, 写一个程序, 在 ZIP (.zip) 和 TAR/gzip (.tgz/.tar.gz) 或 TAR/bzip2 (.tbz/.tar.bz2) 归档文件间移动文件. 文件可能是已经 存在的, 必要时请创建文件. 9–25. 通用解压程序. 创建一个程序, 接受任意数目的归档文件以及一个目标目录做为参数. 归档文件格式可以是 .zip, .tgz, .tar.gz, .gz, .bz2, .tar.bz2, .tbz 中的一种或几种. 程序 会把第一个归档文件解压后放入目标目录, 把其它归档文件解压后放入以对应文件名命名的目录下 (不包括扩展名). 例如输入的文件名为 header.txt.gz 和 data.tgz ,目录为 incoming , header.txt 会被解压到 incoming 而 data.tgz 中的文件会被放入 incoming/data . Edit By Vheavens  Edit By Vheavens  错误和异常 本章主题 z 什么是异常? z Python 中的异常 z 探测和处理异常 z 上下文管理 z 引发异常 z 断言 z 标准异常 z 创建异常 z 相关模块 Edit By Vheavens  Edit By Vheavens  程序员的一生中, 错误几乎每天都在发生. 在过去的一个时期, 错误要么对程序(可能还有机 器)是致命的, 要么产生一大堆无意义的输出, 无法被其他计算机或程序识别, 连程序远自己也可 能搞不懂它的意义. 一旦出现错误, 程序就会终止执行, 直到错误被修正, 程序重新执行. 所以, 人们需要一个"柔和"的处理错误的方法, 而不是终止程序. 同时, 程序本身也在不断发展, 并不是 每个错误都是致命的, 即使错误发生, 编译器或是在执行中的程序也可以提供更多更有用的诊断 信息, 帮助程序员尽快解决问题. 然而, 错误毕竟是错误, 一般都是停止编译或执行后才能去解 决它. 一小段代码只能让程序终止执行, 也许还能打印出一些模糊的提示. 当然, 这一切都是在 异常和异常处理出现之前的事了. 虽然目前还没有讨论到 Python 中的类和面向对象编程(OOP), 但我们这里要介绍的许多概念 已经涉及了类和类实例.[脚注 1] 我们提供了一小节介绍如何创建自定义的异常类. ------------------------------------ 1 . 从 Python 1.5 开始, 所有的标准异常都使用类来实现. 如果你对类, 实例, 以及其他面 向对象相关术语不太了解, 请参阅第 13 章 ---------------------------- 本章将介绍什么是异常, 异常处理, 以及 Python 对异常的支持. 我们还会介绍如何在代码里 生成异常. 最后, 我们会涉及如何创建自定义的异常类. 10.1 什么是异常 Edit By Vheavens  Edit By Vheavens  10.1.1 错误 在深入介绍异常之前, 我们来看看什么是错误. 从软件方面来说, 错误是语法或是逻辑上的. 语法错误指示软件的结构上有错误, 导致不能被解释器解释或编译器无法编译. 这些错误必须在程 序执行前纠正. 当程序的语法正确后, 剩下的就是逻辑错误了. 逻辑错误可能是由于不完整或是不合法的输入 所致; 在其他情况下, 还可能是逻辑无法生成, 计算, 或是输出结果需要的过程无法执行. 这些错 误通常分别被称为域错误和范围错误. 当 Python 检测到一个错误时, 解释器就会指出当前流已经无法继续执行下去. 这时候就出现 了异常. 10.1.2 异常 对异常的最好描述是: 它是因为程序出现了错误而在正常控制流以外采取的行为. 这个行为又 分为两个阶段: 首先是引起异常发生的错误, 然后是检测(和采取可能的措施)阶段. 第一个阶段是在发生了一个异常条件(有时候也叫做例外的条件)后发生的. 只要检测到错误 并且意识到异常条件, 解释器会引发一个异常. 引发也可以叫做触发, 引发或者生成. 解释器通 过它通知当前控制流有错误发生. Python 也允许程序员自己引发异常. 无论是 Python 解释器还是 程序员引发的, 异常就是错误发生的信号. 当前流将被打断, 用来处理这个错误并采取相应的操作. 这就是第二阶段. 对异常的处理发生在第二阶段. 异常引发后, 可以调用很多不同的操作. 可以是忽略错误(记 录错误但不采取任何措施, 采取补救措施后终止程序), 或是减轻问题的影响后设法继续执行程序. 所有的这些操作都代表一种继续, 或是控制的分支. 关键是程序员在错误发生时可以指示程序如何 执行. 你可能已经得出这样一个结论: 程序运行时发生的错误主要是由于外部原因引起的, 例如非法 输入或是其他操作失败等等. 这些因素并不在程序员的直接控制下, 而程序员只能预见一部分错误, 编写常见的补救措施代码. 类似 Python 这样支持引发和处理异常(这更重要)的语言, 可以让开发人员可以在错误发生时 更直接地控制它们. 程序员不仅仅有了检测错误的能力, 还可以在它们发生时采取更可靠的补救措 施. 由于有了运行时管理错误的能力, 应用程序的健壮性有了很大的提高. 异常和异常处理并不是什么新概念. 它们同样存在于 Ada, Modula-3, C++, Eiffel, 以及 Java Edit By Vheavens  Edit By Vheavens  中. 异常的起源可以追溯到处理系统错误和硬件中断这类异常的操作系统代码. 在 1965 年左右, PL/1 作为第一个支持异常的主要语言出现, 而异常处理是作为一个它提供的软件工具. 和其他支 持异常处理的语言类似, Python 采用了 "try/尝试" 块和 "catching/捕获" 块的概念, 而且它在 异常处理方面更有"纪律性". 我们可以为不同的异常创建不同的处理器, 而不是盲目地创建一个 "catch-all/捕获所有"的代码. 10.2 Python 中的异常 在先前的一些章节里你已经执行了一些代码, 你一定遇到了程序"崩溃"或因未解决的错误而终 止的情况. 你会看到"traceback/跟踪返回"消息, 以及随后解释器向你提供的信息, 包括错误的名 称, 原因, 以及发生错误的行号. 不管你是通过 Python 解释器执行还是标准的脚本执行, 所有的 错误都符合相似的格式, 这提供了一个一致的错误接口. 所有错误, 无论是语意上的还是逻辑上的, 都是由于和 Python 解释器不相容导致的, 其后果就是引发异常. 我们来看几个异常的例子. NameError: 尝试访问一个未申明的变量 >>> foo Traceback (innermost last): File "", line 1, in ? NameError: name 'foo' is not defined NameError 表示我们访问了一个没有初始化的变量. 在 Python 解释器的符号表没有找到那个 另人讨厌的变量. 我们将在后面的两章讨论名称空间, 现在大家可以认为它们是连接名字和对象的 "地址簿"就可以了. 任何可访问的变量必须在名称空间里列出. 访问变量需要由解释器进行搜索, 如果请求的名字没有在任何名称空间里找到, 那么将会生成一个 NameError 异常. ZeroDivisionError: 除数为零 >>> 1/0 Traceback (innermost last): File "", line 1, in ? ZeroDivisionError: integer division or modulo by zero 我们边的例子使用的是整数, 但事实上, 任何数值被零除都会导致一个 ZeroDivisionError 异常. SyntaxError: Python 解释器语法错误 >>> for File "", line 1 Edit By Vheavens  Edit By Vheavens  for ^ SyntaxError: invalid syntax SyntaxError 异常是唯一不是在运行时发生的异常. 它代表 Python 代码中有一个不正确的结 构, 在它改正之前程序无法执行. 这些错误一般都是在编译时发生, Python 解释器无法把你的脚本 转化为 Python 字节代码. 当然这也可能是你导入一个有缺陷的模块的时候. IndexError:请求的索引超出序列范围 >>> aList = [] >>> aList[0] Traceback (innermost last): File "", line 1, in ? IndexError: list index out of range IndexError 在你尝试使用一个超出范围的值索引序列时引发. KeyError:请求一个不存在的字典关键字 >>> aDict = {'host': 'earth', 'port': 80} >>> print aDict['server'] Traceback (innermost last): File "", line 1, in ? KeyError: server 映射对象, 例如字典, 是依靠关键字(keys)访问数据值的. 如果使用错误的或是不存在的键请 求字典就会引发一个 KeyError 异常. IOError: 输入/输出错误 >>> f = open("blah") Traceback (innermost last): File "", line 1, in ? IOError: [Errno 2] No such file or directory: 'blah' 类似尝试打开一个不存在的磁盘文件一类的操作会引发一个操作系统输入/输出(I/O)错误. 任 何类型的 I/O 错误都会引发 IOError 异常. AttributeError: 尝试访问未知的对象属性 >>> class myClass(object): ... pass ... >>> myInst = myClass() >>> myInst.bar = 'spam' >>> myInst.bar Edit By Vheavens  Edit By Vheavens  'spam' >>> myInst.foo Traceback (innermost last): File "", line 1, in ? AttributeError: foo 在我们的例子中, 我们在 myInst.bar 储存了一个值, 也就是实例 myInst 的 bar 属性. 属 性被定义后, 我们可以使用熟悉的点/属性操作符访问它, 但如果是没有定义属性, 例如我们访问 foo 属性, 将导致一个 AttributeError 异常. 10.3 检测和处理异常 异常可以通过 try 语句来检测. 任何在 try 语句块里的代码都会被监测, 检查有无异常发 生. try 语句有两种主要形式: try-except 和 try-finally . 这两个语句是互斥的, 也就是说你 只能使用其中的一种. 一个 try 语句可以对应一个或多个 except 子句, 但只能对应一个 finally 子句, 或是一个 try-except-finally 复合语句. 你可以使用 try-except 语句检测和处理异常. 你也可以添加一个可选的 else 子句处理没 有探测到异常的时执行的代码. 而 try-finally 只允许检测异常并做一些必要的清除工作(无论 发生错误与否), 没有任何异常处理设施. 正如你想像的,复合语句两者都可以做到. 10.3.1 try-except 语句 try-except 语句(以及其更复杂的形式)定义了进行异常监控的一段代码, 并且提供了处理异 常的机制. 最常见的 try-except 语句语法如下所示. 它由 try 块和 except 块 (try_suite 和 except_suite )组成, 也可以有一个可选的错误原因. try: try_suite # watch for exceptions here 监控这里的异常 except Exception[, reason]: except_suite # exception-handling code 异常处理代码 我们用一个例子说明这一切是如何工作的. 我们将使用上边的 IOError 例子, 把我们的代码 封装在 try-except 里, 让代码更健壮: Edit By Vheavens  Edit By Vheavens  >>> try: ... f = open('blah', 'r') ... except IOError, e: ... print 'could not open file:', e ... could not open file: [Errno 2] No such file or directory 如你所见, 我们的代码运行时似乎没有遇到任何错误. 事实上我们在尝试打开一个不存在的文 件时仍然发生了 IOError . 有什么区别么? 我们加入了探测和错误错误的代码. 当引发 IOError 异常时, 我们告诉解释器让它打印出一条诊断信息. 程序继续执行, 而不像以前的例子那样被"轰 出来" - 异常处理小小地显了下身手. 那么在代码方面发生了什么呢? 在程序运行时, 解释器尝试执行 try 块里的所有代码, 如果代码块完成后没有异常发生, 执 行流就会忽略 except 语句继续执行. 而当 except 语句所指定的异常发生后, 我们保存了错误的 原因, 控制流立即跳转到对应的处理器( try 子句的剩余语句将被忽略), 本例中我们显示出一个包 含错误原因的错误信息. 在我们上边的例子中, 我们只捕获 IOError 异常. 任何其他异常不会被我们指定的处理器捕 获. 举例说, 如果你要捕获一个 OSError , 你必须加入一个特定的异常处理器. 我们将在本章后 面详细地介绍 try-except 语法. 核心笔记: 忽略代码, 继续执行, 和向上移交 try 语句块中异常发生点后的剩余语句永远不会到达(所以也永远不会执行). 一旦一个异常被 引发, 就必须决定控制流下一步到达的位置. 剩余代码将被忽略, 解释器将搜索处理器, 一旦找到, 就开始执行处理器中的代码. 如果没有找到合适的处理器, 那么异常就向上移交给调用者去处理, 这意味着堆栈框架立即回 到之前的那个. 如果在上层调用者也没找到对应处理器, 该异常会继续被向上移交, 直到找到合适 处理器. 如果到达最顶层仍然没有找到对应处理器, 那么就认为这个异常是未处理的, Python 解释 器会显示出跟踪返回消息, 然后退出. 10.3.2 封装内建函数 我们现在给出一个交互操作的例子 - 从最基本的错误检测开始, 然后逐步改进它, 增强代码 的健壮性. 这里的问题是把一个用字符串表示的数值转换为正确的数值表示形式, 而且在过程中要 检测并处理可能的错误. float() 内建函数的基本作用是把任意一个数值类型转换为一个浮点数. 从 Python 1.5 开始, float() 增加了把字符串表示的数值转换为浮点数的功能, 没必要使用 string 模块中的 atof() 函数. 如果你使用的老版本的 Python , 请使用 string.atof() 替换这里的 float() . Edit By Vheavens  Edit By Vheavens  >>> float(12345) 12345.0 >>> float('12345') 12345.0 >>> float('123.45e67') 1.2345e+069 不幸的是, float() 对输入很挑剔: >>> float('foo') Traceback (innermost last): File "", line 1, in ? float('foo') ValueError: invalid literal for float(): foo >>> >>> float(['this is', 1, 'list']) Traceback (innermost last): File "", line 1, in ? float(['this is', 1, 'list']) TypeError: float() argument must be a string or a number 从上面的错误我们可以看出, float() 对不合法的参数很不客气. 例如, 如果参数的类型正确 (字符串), 但值不可转换为浮点数, 那么将引发 ValueError 异常, 因为这是值的错误. 列表也 是不合法的参数, 因为他的类型不正确, 所以, 引发一个 TypeError 异常. 我们的目标是"安全地"调用 float() 函数, 或是使用一个"安全的方式" 忽略掉错误, 因为它 们与我们转换数值类型的目标没有任何联系, 而且这些错误也没有严重到要让解释器终止执行. 为 了实现我们的目的, 这里我们创建了一个"封装"函数, 在 try-except 的协助下创建我们预想的环 境, 我们把他叫做 safe_float() . 在第一次改进中我们搜索并忽略 ValueError , 因为这是最常 发生的. 而 TypeError 并不常见, 我们一般不会把非字符串数据传递给 float(). def safe_float(obj): try: return float(obj) except ValueError: pass 我们采取的第一步只是"止血". 在上面的例子中, 我们把错误"吞了下去". 换句话说, 错误会 被探测到, 而我们在 except 从句里没有放任何东西(除了一个 pass , 这是为了语法上的需要.), 不进行任何处理, 忽略这个错误. Edit By Vheavens  Edit By Vheavens  这个解决方法有一个明显的不足, 它在出现错误的时候没有明确地返回任何信息. 虽然返回了 None(当函数没有显式地返回一个值时, 例如没有执行到 return object 语句函数就结束了, 它就 返回 None), 我们并没有得到任何关于出错信息的提示. 我们至少应该显式地返回 None , 来使代 码更容易理解: def safe_float(obj): try: retval = float(obj) except ValueError: retval = None return retval 注意我们刚才做的修改, 我们只是添加了一个局部变量. 在设计良好的应用程序接口 (Application Programmer Interface, API)时, 返回值可以更灵活. 你可以在文档中这样写, 如果 传递给 safe_float() 合适的参数, 它将返回一个浮点数; 如果出现错误, 将返回一个字符串说明 输入数据有什么问题. 我们按照这个方案再修改一次代码, 如下所示: def safe_float(obj): try: retval = float(obj) except ValueError: retval = 'could not convert non-number to float' return retval 这里我们只是把 None 替换为一个错误字符串. 下面我们试试这个函数看看它表现如何: >>> safe_float('12.34') 12.34 >>> safe_float('bad input') 'could not convert non-number to float' 我们有了一个好的开始 - 现在我们已经可以探测到非法的字符串输入了, 可如果传递的是一 个非法的对象, 还是会"受伤": >>> safe_float({'a': 'Dict'}) Traceback (innermost last): File "", line 3, in ? retval = float(obj) TypeError: float() argument must be a string or a number Edit By Vheavens  Edit By Vheavens  我们暂时只是指出这个缺点, 在进一步改进程序之前, 首先来看看 try-except 的其他灵活的 语法, 特别是 except 语句, 它有好几种变化形式. 10.3.3 带有多个 except 的 try 语句 在本章的前边, 我们已经介绍了 except 的基本语法: except Exception[, reason]: suite_for_exception_Exception 这种格式的 except 语句指定检测名为 Exception 的异常. 你可以把多个 except 语句连接 在一起, 处理一个 try 块中可能发生的多种异常, 如下所示: except Exception1[, reason1]: suite_for_exception_Exception1 except Exception2[, reason2]: suite_for_exception_Exception2 : 同样, 首先尝试执行 try 子句, 如果没有错误, 忽略所有的 except 从句继续执行. 如果 发生异常, 解释器将在这一串处理器(except 子句)中查找匹配的异常. 如果找到对应的处理器, 执行流将跳转到这里. 我们的 safe_float() 函数已经可以检测到指定的异常了. 更聪明的代码能够处理好每一种异 常. 这就需要多个 except 语句, 每个 except 语句对应一种异常类型. Python 支持把 except 语 句串连使用 我们将分别为每个异常类型分别创建对应的错误信息, 用户可以得到更详细的关于错 误的信息: def safe_float(obj): try: retval = float(obj) except ValueError: retval = 'could not convert non-number to float' except TypeError: retval = 'object type cannot be converted to float' return retval 使用错误的参数调用这个函数, 我们得到下面的输出结果: Edit By Vheavens  Edit By Vheavens  >>> safe_float('xyz') 'could not convert non-number to float' >>> safe_float(()) 'argument must be a string' >>> safe_float(200L) 200.0 >>> safe_float(45.67000) 45.67 10.3.4 处理多个异常的 except 语句 我们还可以在一个 except 子句里处理多个异常. except 语句在处理多个异常时要求异常被放 在一个元组里: except (Exception1, Exception2)[, reason]: suite_for_Exception1_and_Exception2 上边的语法展示了如何处理同时处理两个异常. 事实上 except 语句可以处理任意多个异常, 前提只是它们被放入一个元组里 , 如下所示: except (Exc1[, Exc2[, ... ExcN]])[, reason]: suite_for_exceptions_Exc1_to_ExcN 如果由于其他原因, 也许是内存规定或是设计方面的因素, 要求 safe_float() 函数中的所有 异常必须使用同样的代码处理, 那么我们可以这样满足需求: def safe_float(obj): try: retval = float(obj) except (ValueError, TypeError): retval = 'argument must be a number or numeric string' return retval 现在, 错误的输入会返回相同的字符串: >>> safe_float('Spanish Inquisition') 'argument must be a number or numeric string' >>> safe_float([]) Edit By Vheavens  Edit By Vheavens  'argument must be a number or numeric string' >>> safe_float('1.6') 1.6 >>> safe_float(1.6) 1.6 >>> safe_float(932) 932.0 10.3.5 捕获所有异常 使用前一节的代码, 我们可以捕获任意数目的指定异常, 然后处理它们. 如果我们想要捕获所 有的异常呢? 当然可以! 自版本 1.5 后, 异常成为类, 实现这个功能的代码有了很大的改进. 也 因为这点(异常成为类),我们现在有一个异常继承结构可以遵循. 如果查询异常继承的树结构, 我们会发现 Exception 是在最顶层的, 所以我们的代码可能看 起来会是这样: try: : except Exception, e: # error occurred, log 'e', etc. 另一个我们不太推荐的方法是使用 裸 except 子句: try: : except: # error occurred, etc. 这个语法不如前个 "Pythonic" . 虽然这样的代码捕获大多异常, 但它不是好的 Python 编程 样式. 一个主要原因是它不会考虑潜在的会导致异常的主要原因. 我们的 catch-all 语句可能不 会如你所想的那样工作, 它不会调查发生了什么样的错误, 如何避免它们. 我们没有指定任何要捕获的异常 - 这不会给我们任何关于可能发生的错误的信息. 另外它会 捕获所有异常, 你可能会忽略掉重要的错误, 正常情况下这些错误应该让调用者知道并做一定处理. 最后, 我们没有机会保存异常发生的原因. 当然, 你可以通过 sys.exc_info() 获得它, 但这样你 就不得不去导入 sys 模块, 然后执行函数 - 这样的操作本来是可以避免的, 尤其当我们需要立即 告诉用户为什么发生异常的时候.在 Python 的未来版本中很可能不再支持裸 except 子句. (参见 “核心风格”) Edit By Vheavens  Edit By Vheavens  关于捕获所有异常, 你应当知道有些异常不是由于错误条件引起的. 它们是 SystemExit 和 KeyboardInterupt . SystemExit 是由于当前 Python 应用程序需要退出, KeyboardInterupt 代表 用户按下了 CTRL-C (^C) , 想要关闭 Python . 在真正需要的时候, 这些异常却会被异常处理捕获. 一个典型的迂回工作法代码框架可能会是这样: try: : except (KeyboardInterupt, SystemExit): # user wants to quit raise # reraise back to caller except Exception: # handle real errors 关于异常的一部分内容在 Python 2.5 有了一些变化. 异常被迁移到了 new-style class 上, 启用了一个新的"所有异常的母亲", 这个类叫做 BaseException , 异常的继承结构有了少许调整, 为了让人们摆脱不得不除创建两个处理器的惯用法. KeyboardInterrupt 和 SystemExit 被从 Exception 里移出, 和 Exception 平级: - BaseException |- KeyboardInterrupt |- SystemExit |- Exception |- (all other current built-in exceptions) 所有当前内建异常 你可以在表 10.2 找到整个异常继承结构(变化前后). 这样, 当你已经有了一个 Exception 处理器后, 你不必为这两个异常创建额外的处理器. 代 码将会是这样: try: : except Exception, e: # handle real errors 如果你确实需要捕获所有异常, 那么你就得使用新的 BaseException : try: : except BaseException, e: Edit By Vheavens  Edit By Vheavens  # handle all errors 当然, 也可以使用不被推荐的裸 except 语句. 核心风格: 不要处理并忽略所有错误 Python 提供给程序员的 try-except 语句是为了更好地跟踪潜在的错误并在代码里准备好处 理异常的逻辑. 这样的机制在其他语言(例如 C ) 是很难实现的. 它的目的是减少程序出错的次数 并在出错后仍能保证程序正常执行. 作为一种工具而言, 只有正确得当地使用它, 才能使其发挥作 用. 一个不正确的使用方法就是把它作为一个大绷带"绑定"到一大片代码上. 也就是说把一大段程 序(如果还不是整个程序源代码的话)放入一个 try 块中, 再用一个通用的 except 语句 "过滤" 掉任何致命的错误, 忽略它们. # this is really bad code try: large_block_of_code # bandage of large piece of code except Exception: # same as except: pass # blind eye ignoring all errors 很明显, 错误无法避免, try-except 的作用是提供一个可以提示错误或处理错误的机制, 而不 是一个错误过滤器. 上边这样的结构会忽略许多错误, 这样的用法是缺乏工程实践的表现, 我们 不赞同这样做. 底线: 避免把大片的代码装入 try-except 中然后使用 pass 忽略掉错误. 你可以捕获特定 的异常并忽略它们, 或是捕获所有异常并采取特定的动作. 不要捕获所有异常,然后忽略掉它们. 10.3.6 异常参数 异常也可以有参数, 异常引发后它会被传递给异常处理器. 当异常被引发后参数是作为附加帮 助信息传递给异常处理器的. 虽然异常原因是可选的, 但标准内建异常提供至少一个参数, 指示异 常原因的一个字符串. 异常的参数可以在处理器里忽略, 但 Python 提供了保存这个值的语法. 我们已经在上边接触 到相关内容: 要想访问提供的异常原因, 你必须保留一个变量来保存这个参数. 把这个参数放在 except 语句后, 接在要处理的异常后面. except 语句的这个语法可以被扩展为: # single exception except Exception[, reason]: suite_for_Exception_with_Argument # multiple exceptions except (Exception1, Exception2, ..., ExceptionN)[, reason]: Edit By Vheavens  Edit By Vheavens  suite_for_Exception1_to_ExceptionN_with_Argument reason 将会是一个包含来自导致异常的代码的诊断信息的类实例. 异常参数自身会组成一个 元组,并存储为类实例(异常类的实例)的属性. 上边的第一种用法中, reason 将会是一个 Exception 类的实例. 对于大多内建异常, 也就是从 StandardError 派生的异常, 这个元组只包含一个指示错误原 因的字符串. 一般说来, 异常的名字已经是一个满意的线索了, 但这个错误字符串会提供更多的信 息. 操作系统或其他环境类型的错误, 例如 IOError , 元组中会把操作系统的错误编号放在错误字 符串前. 无论 reason 只包含一个字符串或是由错误编号和字符串组成的元组, 调用 str(reason) 总 会返回一个良好可读的错误原因. 不要忘记 reason 是一个类实例 - 这样做你其实是调用类的特 殊方法 __str__() . 我们将在第 13 章探索面向对象编程中的这些特殊方法. 唯一的问题就是某些第三方或是其他外部库并不遵循这个标准协议. 我们推荐你在引发你自己 的异常时遵循这个标准(参见核心风格笔记). 核心风格: 遵循异常参数规范 当你在自己的代码中引发内建(built-in)的异常时, 尽量遵循规范, 用和已有 Python 代码 一致错误信息作为传给异常的参数元组的一部分. 简单地说, 如果你引发一个 ValueError , 那么 最好提供和解释器引发 ValueError 时一致的参数信息, 如此类推. 这样可以在保证代码一致性, 同时也能避免其他应用程序在使用你的模块时发生错误. 如下边的例子, 它传参给内建 float 函数一个无效的对象, 引发 TypeError 异常: >>> try: ... float(['float() does not', 'like lists', 2]) ... except TypeError, diag:# capture diagnostic info ... pass ... >>> type(diag) >>> >>> print diag float() argument must be a string or a number 我们首先在一个 try 语句块中引发一个异常,随后简单的忽略了这个异常,但保留了错误的信 息。调用内置的 type()函数,我们可以确认我们的异常对象的确是 TypeError 异常类的实例。最后 我们对异常诊断参数调用 print 以显示错误。 Edit By Vheavens  Edit By Vheavens  为了获得更多的关于异常的信息,我们可以调用该实例的 __class__ 属性,它标示了实例是从 什么类实例化而来. 类对象也有属性, 比如文档字符串(documentation string)和进一步阐明错误 类型的名称字符串: >>> diag # exception instance object >>> diag.__class__ # exception class object >>> diag.__class__.__doc__ # exception class documentation string 'Inappropriate argument type.' >>> diag.__class__.__name__ # exception class name 'TypeError' 我们会在第 13 章"类和面向对象编程"发现, __class__ 属性存在于所有的类实例中,而__doc__ 类属性存在于所有的定义了文档字符串的类中. 我们现在再次来改进我们的 saft_float()以包含异常参数,当 float()发生异常时传给解释器. 在前一次改进中,我们在一句话中同时捕获了 ValueError 和 TypeError 异常以满足某些需求.但还 是有瑕疵,那个解决方案中没有线索表明是哪一种异常引发了错误.它仅仅是返回了一个错误字符 串指出有无效的参数.现在,通过异常参数,可以改善这种状况. 因为每一个异常都将生成自己的异常参数,如果我们选择用这个字符串来而不是我们自定义的 信 息 , 可 以 提 供 一 个 更 好 的 线 索 来 指 出 问 题 . 下 面 的 代 码 片 段 中 , 我 们 用 字 符 串 化 (string representation)的异常参数来替换单一的错误信息. def safe_float(object): try: retval = float(object) except (ValueError, TypeError), diag: retval = str(diag) return retval 在此基础上运行我们的新代码,当我们提供 sofe_float()的参数给不恰当时,虽然还是只有一条 捕获语句,但是可以获得如下(不同的)信息. >>> safe_float('xyz') 'invalid literal for float(): xyz' >>> safe_float({}) 'object can't be converted to float' Edit By Vheavens  Edit By Vheavens  10.3.7 在应用使用我们封装的函数 我们将在一个迷你应用中特地的使用这个函数.它将打开信用卡交易的数据文件 (carddata.txt),加载所有的交易,包括解释的字符串.下面是一个示例的 carddate.txt 文件: % cat carddata.txt # carddata.txt previous balance 25 debits 21.64 541.24 25 credits -25 -541.24 finance charge/late fees 7.30 5 我们的程序,cardrun.py,见示例 10.1 示例 10.1 信用卡交易系统(cardrun.py) 我们用 safe_float()来处理信用卡交易文件,将其作为字符串读入.并用一个日志文件跟踪处理 进程. 1 #!/usr/bin/env python 2 3 def safe_float(obj): 4 'safe version of float()' 5 try: 6 retval = float(obj) 7 except (ValueError, TypeError), diag: 8 retval = str(diag) 9 return retval 10 11 def main(): 12 'handles all the data processing' 13 log = open('cardlog.txt', 'w') 14 try: Edit By Vheavens  Edit By Vheavens  15 ccfile = open('carddata.txt', 'r') 16 except IOError, e: 17 log.write('no txns this month\n') 18 log.close() 19 return 20 21 txns = ccfile.readlines() 22 ccfile.close() 23 total = 0.00 24 log.write('account log:\n') 25 26 for eachTxn in txns: 27 result = safe_float(eachTxn) 28 if isinstance(result, float): 29 total += result 30 log.write('data... processed\n') 31 else: 32 log.write('ignored: %s' % result) 33 print '$%.2f (new balance)' % (total) 34 log.close() 35 36 if __name__ == '__main__': 37 main() 逐行解读 行 3-9 这段代码是 safe_float()函数的主体 行 11-34 我们应用的核心部分有 3 个主要任务 (1)读入信用卡的数据文件 (2)处理输入 (3)显示结果 行 14-22 从文件中提取数据.你可以看到这里的文件打开被置于 try-except 语句段中. 同时还有一个处理的日志文件.在我们的例子中,我们假设这个日志文件可以不出错的打开.你 可以看到我们的处理进程伴随着这个日志文件.如果信用卡的数据文件不能够被访问,我们可以假设 Edit By Vheavens  Edit By Vheavens  该月没有信用卡交易(行 16 - 19). 数据被读入 txns(transactions 交易)列表,随后在 26-32 行遍历它.每次调用 safe_float()后, 我们用内建的 isinstance 函数检查结果类型.在我们例子中,我们检查 safe_float 是返回字符串还 是浮点数.任何字符串都意味着错误,表明该行不能转换为数字,同时所有的其他数字可以作为浮点 数累加入 total.在 main()函数的尾行会显示最终生成的余额. 行 36-37 这两行通常表明"仅在非导入时启动"的功能.运行我们程序,可以得到如下的输出 $ cardrun.py $ 58.94 (new balance) 我们再看看 log 文件(cardlog.txt),我们可以看到在处理完 carddata.txt 中的交易后有其有如 下的记录条目: $ cat cardlog.txt account log: ignored: invalid literal for float(): # carddata.txt ignored: invalid literal for float(): previous balance data... processed ignored: invalid literal for float(): debits data... processed data... processed data... processed ignored: invalid literal for float(): credits data... processed 10.3.8 else 子句 我们已经看过 else 语句段配合其他的 Python 语句,比如条件和循环.至于 try-except 语句段, 它的功能和你所见过的其他 else 没有太多的不同:在 try 范围中没有异常被检测到时,执行 else 子 句. 在 else 范围中的任何代码运行前,try 范围中的所有代码必须完全成功(也就是,结束前没有引发 异常).下面是用 Python 伪代码写的简短例子. import 3rd_party_module Edit By Vheavens  Edit By Vheavens  log = open('logfile.txt', 'w') try: 3rd_party_module.function() except: log.write("*** caught exception in module\n") else: log.write("*** no exceptions caught\n") log.close() 在前面的例子中,我们导入了一个外部的模块然后测试是否有错误.用一个日志文件来确定这个 第三方模块是有无缺陷.根据运行时是否引发异常,我们将在日志中写入不同的消息. 10.3.9 finally 子句 finally 子句是无论异常是否发生,是否捕捉都会执行的一段代码.你可以将 finally 仅仅配合 try 一起使用,也可以和 try-except(else 也是可选的)一起使用.独立的 try-finally 将会在下一章 介绍,我们稍后再来研究. 从 Python 2.5 开始,你可以用 finally 子句(再一次)与 try-except 或 try-except-else 一起 使用.之所以说是"再一次"是因为无论你相信与否,这并不是一个新的特性.回顾 Python 初期,这个特 性在早已存在,但是在 Python 0.9.6(1992 四月)中被移除.那时,这样可以简化字节码的生成,并方便 解析,另外就是范·罗萨姆认为一个标准化的 try-except(-else)-finally 无论如何不会太流行.然 而,十年时间改变了一切! 下面是 try-except-else-finally 语法的示例: try: A except MyException: B else: C finally: D 等价于 Python 0.9.6 至 2.4.x 中如下的写法: try: try: A Edit By Vheavens  Edit By Vheavens  except MyException: B else: C finally: D 当然,无论如何,你都可以有不止一个的 except 子句,但最少有一个 except 语句,而 else 和 finally 都是可选的.A,B,C 和 D 是程序(代码块).程序会按预期的顺序执行.(注意:可能的顺序是 A-C-D[正常]或 A-B-D[异常]).无论异常发生在 A,B,和/或 C 都将执行 finally 块.旧式写法依然有效, 所以没有向后兼容的问题. 10.3.10 try-finally 语句 另一种使用 finally 的方式是 finally 单独和 try 连用.这个 try-finally 语句和 try-except 区别在于它不是用来捕捉异常的.作为替代,它常常用来维持一致的行为而无论异常是否发生.我们 得知无论 try 中是否有异常触发,finally 代码段都会被执行 try: try_suite finally: finally_suite #无论如何都执行 当在 try 范围中产生一个异常时,(这里)会立即跳转到 finally 语句段.当 finally 中的所有代 码都执行完毕后,会继续向上一层引发异常. 因而常常看到嵌套在 try-except 中的 try-finally 语句.当在读取 carddata.txt 中文本时可能 引发异常,我们可以在 cardrun.py 的这一处添加 try-finally 语句段来改进代码.在当前示例 10.1 的代码中,我们在读取阶段没有探测到错误(通过 readlines()) try: ccfile = open('carddata.txt') except IOError: log.write('no txns this month\n') txns = ccfile.readlines() ccfile.close() 但有很多原因会导致 readlines()失败,其中一种就是 carddata.txt 存在于网络(或软盘)上,但 是变得不能读取.无论怎样,我们可以把这一小段读取数据的代码整个放入 try 子句的范围中: Edit By Vheavens  Edit By Vheavens  try: ccfile = open('carddata.txt', 'r') txns = ccfile.readlines() ccfile.close() except IOError: log.write('no txns this month\n') 我们所做的一切不过是将 readline()和 close()方法调用都移入了 try 语句段.尽管我们代码变 得更加的健壮了,但还有改进的空间.注意如果按照这样的顺序发生错误:打开成功,但是出于一些原 因 readlines()调用失败,异常处理会去继续执行 except 中的子句,而不去尝试关闭文件.难道没有一 种好的方式来关闭文件而无论错误是否发生?我们可以通过 try-finally 来实现: ccfile = None try: try: ccfile = open('carddata.txt', 'r') txns = ccfile.readlines() except IOError: log.write('no txns this month\n') finally: if ccfile: ccfile.close() 代码片段会尝试打开文件并且读取数据.如果在其中的某步发生一个错误,会写入日志,随后文 件被正确的关闭.如果没有错误发生,文件也会被关闭.(同样的功能可以通过上面标准化的 try-except-finally 语句段实现).另一种可选的实现切换了 try-except 和 try-finally 包含的方式, 如: ccfile = None try: try: ccfile = open('carddata.txt', 'r') txns = ccfile.readlines() finally: if ccfile: ccfile.close() except IOError: log.write('no txns this month\n') Edit By Vheavens  Edit By Vheavens  代码本质上干的是同一种工作,除了一些小小的不同.最显著的是关闭文件发生在异常处理器将 错误写入日志之前.这是因为 finally 会自动的重新引发异常. 一个这样写的理由是如果在 finally 的语句块内发生了一个异常,你可以创建一个同现有的异常 处理器在同一个(外)层次的异常处理器来处理它.这样,从本质上来说,就可以同时处理在原始的 try 语句块和 finally 语句块中发生的错误.这种方法唯一的问题是,当 finally 语句块中的确发生异常 时,你会丢失原来异常的上下文信息,除非你在某个地方保存了它. 反对这种写法的一个理由是:在很多情况下,异常处理器需要做一些扫尾工作,而如果你在异常 处理之前,用 finally 语句块中释放了某些资源,你就不能再去做这项工作了.简单的说,finally 语句 块并不是如你所想的是"最终的(final)"了. 一个最终的注意点:如果 finally 中的代码引发了另一个异常或由于 return,break,continue 语 法而终止,原来的异常将丢失而且无法重新引发. 10.3.11 try-except-else-finally:厨房一锅端 我们综合了这一章目前我们所见过的所有不同的可以处理异常的语法样式: try: try_suite except Exception1: suite_for_Exception1 except (Exception2, Exception3, Exception4): suite_for_Exceptions_2_3_and_4 except Exception5, Argument5: suite_for_Exception5_plus_argument except (Exception6, Exception7), Argument67: suite_for_Exceptions6_and_7_plus_argument except: suite_for_all_other_exceptions else: no_exceptions_detected_suite Edit By Vheavens  Edit By Vheavens  finally: always_execute_suite 回顾上面的,finally 子句和 try-except 或 try-except-else 联合使用是 Python 2.5 的"新"有 的.这一节最重要的是无论你选择什么语法,你至少要有一个 except 子句,而 else 和 finally 都是可 选的. 10.4 上下文管理 10.4.1 with 语句 如上所述的标准化的 try-except 和 try-finally 可以使得程序更加"Pythonic",其含义是,在许 多的其他特性之外,更加写地轻松,读地自在.Python 对隐藏细节已经做了大量的工作,因此需要你操 心的仅是如何解决你所遇到问题.(你能假想移植一个复杂的 Python 应用到 C++或 Java 吗?!?) 另一个隐藏低层次的抽象的例子是 with 语句,它在 Python 2.6 中正式启用.(Python2.5 尝试性 的引入了 with, 并对使用 with 作为标识符的应用程序发出这样的警告 - 在 Python 2.6 中,with 将会成为关键字. 如果你想在 Python 2.5 使用 with 语句, 你必须用 from __future__ import with_statement 来导入它.) 类似 try-except-finally , with语句也是用来简化代码的,这与用 try-except 和 try-finally 所想达到的目的前后呼应.try-except 和 try-finally 的一种特定的配合用法是保证共享的资源的 唯一分配,并在任务结束的时候释放它.比如文件(数据,日志,数据库等等),线程资源,简单同步,数 据库连接,等等. with 语句的目标就是应用在这种场景. 然而,with 语句的目的在于从流程图中把 try,except 和 finally 关键字和资源分配释放相关 代码统统去掉, 而不是像 try-except-finally 那样仅仅简化代码使之易用. with 语法的基本用法 看上去如下: with context_expr [as var]: with_suite 看起来如此简单,但是其背后还有一些工作要做.这并不如看上去的那么容易,因为你不能对 Python 的任意符号使用 with 语句.它仅能工作于支持上下文管理协议(context management protocol)的对象.这显然意味着只有内建了"上下文管理"的对象可以和 with 一起工作.我们过一会 再来阐明它的含义. 现在,正如一个新的游戏硬件,每当有一个新的特性推出时,第一时间总有人开发出相应的新游 戏,从而你打开盒子就可以开始玩了.类似,目前已经有了一些支持该协议的对象.下面是第一批成员 的简短列表: Edit By Vheavens  Edit By Vheavens  z file z decimal.Context z thread.LockType z threading.Lock z threading.RLock z threading.Condition z threading.Semaphore z threading.BoundedSemaphore 既然 file 是上面的列表上的第一个也是最易于演示的,下面就给出一段和 with 一起使用的代码 片段. with open('/etc/passwd', 'r') as f: for eachLine in f: # ...do stuff with eachLine or f... 这个代码片段干了什么呢...嗯,这是 Python,因而你很可能的已经猜到了.它会完成准备工作, 比如试图打开一个文件,如果一切正常,把文件对象赋值给 f.然后用迭代器遍历文件中的每一行,当 完成时,关闭文件.无论的在这一段代码的开始,中间,还是结束时发生异常,会执行清理的代码,此 外文件仍会被自动的关闭. 因为已经从你手边拿走了一堆细节,所以实际上只是进行了两层处理: 第一,发生用户层 —— 和 in 类似,你所需要关心的只是被使用的对象 第二,在对象层.既然这个对象支持上下文管理协议,它干的也就是"上下文管理". 10.4.2 *上下文管理协议 除非你打算自定义可以和 with 一起工作的类,比如:别的程序员会在他们的设计的应用中使用你 的对象. 绝大多数 Python 程序员仅仅需要使用 with 语句,可以跳过这一节. 我们不打算在这里对上下文管理做深入且详细的探讨,但会介绍兼容协议所必须的对象类型与 功能,使其能和 with 一起工作. 前面,我们在例子中描述了一些关于协议如何和文件对象协同工作.让我们在此进一步地研究. 上下文表达式(context_expr),上下文管理器 Edit By Vheavens  Edit By Vheavens  当 with 语句执行时,便执行上下文符号(译者注:就是 with 与 as 间内容)来获得一个上下文管理 器.上下文管理器的职责是提供一个上下文对象.这是通过调用__context__()方法来实现的.该方法 返回一个上下文对象,用于在 with 语句块中处理细节.有点需要注意的是上下文对象本身就可以是上 下文管理器.所以 context_expr 既可以为一个真正的上下文管理器,也可以是一个可以自我管理的上 下文对象.在后一种情况时,上下文对象仍然有__context__()方法,返回其自身,如你所想. 上下文对象,with 语句块 一旦我们获得了上下文对象,就会调用它的__enter()__方法.它将完成 with 语句块执行前的所 有准备工作.你可以注意到在上面的 with 行的语法中有一个可选的 as 声明变量跟随在 context_expr 之后.如果提供提供了变量,以__enter()__返回的内容来赋值;否则,丢弃返回值.在我们的文件对象 例子中,上下文对象的__enter()__返回文件对象并赋值给 f. 现在,执行了 with 语句块.当 with 语句块执行结束,无论是"和谐地"还是由于异常,都会调用上 下文对象的__exit()__方法.__exit__()有三个参数.如果 with 语句块正常结束,三个参数全部是 None.如果发生异常,三个参数的值的分别等于调用 sys.exc_info()函数(见 10.12)返回的三个值:类 型(异常类),值(异常实例),和回溯(traceback),相应的回溯对象. 你可以自己决定如何在__exit__()里面处理异常.惯例是当你处理完异常时不返回任何值,或 返回 None,或返回其他布尔值为 False 对象.这样可以使异常抛给你的用户来处理.如果你明确的想 屏蔽这个异常,返回一个布尔为 True 的值.如果没有发生异常或你在处理异常后返回 True,程序会继 续执行 with 子句后的下一段代码. 因为上下文管理器主要作用于共享资源,你可以想象到__enter()__和__exit()__方法基本是干 的需要分配和释放资源的低层次工作,比如: 数据库连接,锁分配,信号量加减,状态管理,打开/关闭文件,异常处理,等等. 为 了 帮 助 你 编 写 对 象 的 上 下 文 管 理 器 , 有 一 个 contextlib 模 块 , 包 含 了 实 用 的 functions/decorators,你可以用在你的函数/对象上而不用去操心关于类或 __context__(),__enter()__,__enter()__,__exit()__这些方法的实现. 想了解更多关于上下文管理器的信息,查看官方的 Python 文档的 with 语法和 contextlib 模块, 类的指定方法(与 with 和 contexts 相关的),PEP 343,和“What’s New in Python 2.5(Python 2.5 的更新)”的文档. 10.5 *字符串作为异常 早在 Python 1.5 前,标准的异常是基于字符串实现的.然而,这样就限制了异常之间不能有相互 的关系.这种情况随着异常类的来临而不复存在.到 1.5 为止,所有的标准异常都是类了.程序员还是 Edit By Vheavens  Edit By Vheavens  可以用字符串作为自己的异常的,但是我们建议从现在起使用异常类. 为了向后兼容性,还是可以启用基于字符串的异常.从命令行以-X 为参数启动 Python 可以提供你 字符串方式的标准异常.从 Python1.6 起这个特性被视为废弃的. Python 2.5开始处理向来不赞成使用的字符串异常.在 2.5 中,触发字符串异常会导致一个警告. 在 2.6,捕获字符串异常会导致一个警告.由于它很少被使用而且已经被废弃,我们将不再在本书范围 内考虑字符串异常并且已经去除相关文字.(在本书的早期的版本中你会找到这些.)唯一也是最后的 中肯警告是:你可能用到仍然使用着字符串异常的外部或第三方的模块.字符串异常总而言之是一个 糟糕的想法.读者可以回想,有着拼写错误的 Linux RPM 异常如在眼前. 10.6 触发异常 到目前为止,我们所见到的异常都是由解释器引发的.由于执行期间的错误而引发.程序员在编 写 API 时也希望在遇到错误的输入时触发异常,为此,Python 提供了一种机制让程序员明确的触发异 常:这就是 raise 语句. 10.6.1 raise 语句 语法与惯用法 raise 语句对所支持是参数十分灵活,对应到语法上就是支持许多不同的格式.rasie 一般的用法 是: raise [SomeException [, args [, traceback]]] 第一个参数,SomeExcpetion,是触发异常的名字.如果有,它必须是一个字符串,类或实例(详见 下文).如果有其他参数(arg 或 traceback),就必须提供 SomeExcpetion.Python 所有的标准异常见表 10.2. 第二个符号为可选的 args(比如参数,值),来传给异常.这可以是一个单独的对象也可以是一个 对象的元组.当异常发生时,异常的参数总是作为一个元组传入.如果 args 原本就是元组,那么就将其 传给异常去处理;如果 args 是一个单独的对象,就生成只有一个元素的元组(就是单元素元组).大多 数情况下,单一的字符串用来指示错误的原因.如果传的是元组,通常的组成是一个错误字符串,一个 错误编号,可能还有一个错误的地址,比如文件,等等. 最后一项参数,traceback,同样是可选的(实际上很少用它),如果有的话,则是当异常触发时新 生成的一个用于异常-正常化(exception—normally)的追踪(traceback)对象.当你想重新引发异常 Edit By Vheavens  Edit By Vheavens  时,第三个参数很有用(可以用来区分先前和当前的位置).如果没有这个参数,就填写 None. 最常见的用法为 SomeException 是一个类.不需要其他的参数,但如果有的话,可以是一个单一对 象参数,一个参数的元组,或一个异常类的实例.如果参数是一个实例,可以由给出的类及其派生类实 例化(已存在异常类的子集).若参数为实例,则不能有更多的其他参数. 更多的特殊/少见的惯用法 当参数是一个实例的时候会发生什么呢? 该实例若是给定异常类的实例当然不会有问题, 然而, 如果该实例并非这个异常类或其子类的实例时, 那么解释器将使用该实例的异常参数创建一个给定 异常类的新实例. 如果该实例是给定异常类子类的实例, 那么新实例将作为异常类的子类出现, 而 不是原来的给定异常类. 如果 raise 语句的额外参数不是一个实例——作为替代,是一个单件(singleton)或元组—那么, 将用这些作为此异常类的初始化的参数列表.如果不存在第二个参数或是 None,则参数列表为空. 如果 SomeException 是一个实例,我们就无需对什么进行实例化了.这种情况下,不能有额外的参 数或只能是 None. 异常的类型就是实例的类;也就是说,等价于触发此类异常,并用该实例为参数:比如 raise instance.__class__,instance. 我们建议用异常类,不赞成用字符串异常.但如果用字符串作为 SomeException,那么会触发一 个用字符串标识的异常,还有一个可选的参量(args)作参数. 最后,这种不含任何参数的 raise 语句结构是在 Python1.5 中新引进的,会引发当前代码块(code block)最近触发的一个异常.如果之前没有异常触发,会因为没可以有重新触发的异常而生成一个 TypeError 异常. 由于 raise 有许多不同格式有效语法(比如:SomeException 可以是类,实例或一个字符串),我们 提供表 10.1 来阐明 rasie 的不同用法. 表 10.1 raise 语句的用法 rasie 语法 描述 raise exclass 触发一个异常,从 exclass 生成一个实例(不含任何异常参数) raise exclass() 同上,除了现在不是类;通过函数调用操作符(function calloperator: "()")作用于类名生成一个新的 exclass 实例,同样也没有异常参数 raise exclass, args 同上,但同时提供的异常参数 args,可以是一个参数也可以元组 raise exclass(args) 同上 raise exclass,args, tb 同上,但提供一个追踪(traceback)对象 tb 供使用 Edit By Vheavens  Edit By Vheavens  raise exclass,instance 通过实例触发异常(通常是 exclass 的实例);如果实例是 exclass 的子类实例,那么这个新异常的类型会是子类的类型(而不是 exclass);如果实例既不是 exclass 的实例也不是 exclass 子类的 实例,那么会复制此实例为异常参数去生成一个新的 exclass 实例. raise instance 通过实例触发异常:异常类型是实例的类型;等价于 raise instance.__class__, instance (同上). raise string (过时的) 触发字符串异常 raise string, args 同上,但触发伴随着 args raise string, args, tb 同上,但提供了一个追踪(traceback)对象 tb 供使用 raise (1.5 新增)重新触发前一个异常,如果之前没有异常,触发 TypeError. 10.7 断言 断言是一句必须等价于布尔真的判定;此外,发生异常也意味着表达式为假.这些工作类似于 C 语 言预处理器中 assert 宏,但在 Python 中它们在运行时构建(与之相对的是编译期判别). 如果你刚刚接触断言这个概念,无妨.断言可以简简单单的想象为 raise-if 语句(更准确的说是 raise-if-not 语句).测试一个表达式,如果返回值是假,触发异常. 断言通过 assert 语句实现,在 1.5 版中引入. 10.7.1 断言语句 断言语句等价于这样的 Python 表达式,如果断言成功不采取任何措施(类似语句),否则触发 AssertionError(断言错误)的异常.assert 的语法如下: assert expression[, arguments] 下面有一些演示 assert 用法的语句: assert 1 == 1 assert 2 + 2 == 2 * 2 assert len(['my list', 12]) < 10 assert range(3) == [0, 1, 2] AssertionError 异常和其他的异常一样可以用 try-except 语句块捕捉,但是如果没有捕捉,它将 终止程序运行而且提供一个如下的 traceback: Edit By Vheavens  Edit By Vheavens  >>> assert 1 == 0 Traceback (innermost last): File "", line 1, in ? AssertionError 如同先前章节我们研究的 raise 语句,我们可以提供一个异常参数给我们的 assert 命令: >>> assert 1 == 0, 'One does not equal zero silly!' Traceback (innermost last): File "", line 1, in ? AssertionError: One does not equal zero silly! 下面是我们如何用 try-except 语句捕获 AssertionError 异常: try: assert 1 == 0, 'One does not equal zero silly!' except AssertionError, args: print '%s: %s' % (args.__class__.__name__, args) 从命令行执行上面的代码会导致如下的输出: AssertionError: One does not equal zero silly! 为了让你更加了解 assert 如何运作,想象一下断言语句在 Python 中如何用函数实现.可以像下 面这样: def assert(expr, args=None): if __debug__ and not expr: raise AssertionError, args 此处的 if 语句检查 assert 的语法是否合适,也就是 expr 必须是一个表达式.我们比较 expr 的类型和真正的表达式来确认.函数的第二部分对表达式求值然后根据结果选择性的引发异常.内建 的变量__debug__在通常情况下为 True,如果开启优化后为 False(命令行选项-O)(Python 2.2 后为 布尔值 True 和 False.) 10.8 标准异常 表 10.2 列出了所有的 Python 当前的标准异常集,所有的异常都是内建的. 所以它们在脚本启动 前或在互交命令行提示符出现时已经是可用的了. 表 10.2 Python 内建异常 Edit By Vheavens  Edit By Vheavens  异常名称 描述 BaseExceptiona 所有异常的基类 SystemExitb python 解释器请求退出 KeyboardInterruptc 用户中断执行(通常是输入^C) Exceptiond 常规错误的基类 StopIteratione 迭代器没有更多的值 GeneratorExita 生成器(generator)发生异常来通知退出 SystemExith Python 解释器请求退出 StandardErrorg 所有的内建标准异常的基类 ArithmeticErrord 所有数值计算错误的基类 FloatingPointErrord 浮点计算错误 OverflowError 数值运算超出最大限制 ZeroDivisionError 除(或取模)零 (所有数据类型) AssertionErrord 断言语句失败 AttributeError 对象没有这个属性 EOFError 没有内建输入,到达 EOF 标记 EnvironmentErrord 操作系统错误的基类 IOError 输入/输出操作失败 OSErrord 操作系统错误 WindowsErrorh Windows 系统调用失败 ImportError 导入模块/对象失败 KeyboardInterruptf 用户中断执行(通常是输入^C) LookupErrord 无效数据查询的基类 IndexError 序列中没有没有此索引(index) KeyError 映射中没有这个键 MemoryError 内存溢出错误(对于 Python 解释器不是致命的) NameError 未声明/初始化对象 (没有属性) UnboundLocalErrorh 访问未初始化的本地变量 ReferenceErrore 弱引用(Weak reference)试图访问已经垃圾回收了的对象 RuntimeError 一般的运行时错误 NotImplementedErrord 尚未实现的方法 SyntaxError Python 语法错误 IndentationErrorg 缩进错误 TabErrorg Tab 和空格混用 SystemError 一般的解释器系统错误 TypeError 对类型无效的操作 ValueError 传入无效的参数 UnicodeErrorh Unicode 相关的错误 UnicodeDecodeErrori Unicode 解码时的错误 UnicodeEncodeErrori Unicode 编码时错误 Edit By Vheavens  Edit By Vheavens  UnicodeTranslateErrorf Unicode 转换时错误 Warningj 警告的基类 DeprecationWarningj 关于被弃用的特征的警告 FutureWarningi 关于构造将来语义会有改变的警告 OverflowWarningk 旧的关于自动提升为长整型(long)的警告 PendingDeprecationWarningi 关于特性将会被废弃的警告 RuntimeWarningj 可疑的运行时行为(runtime behavior)的警告 SyntaxWarningj 可疑的语法的警告 UserWarningj 用户代码生成的警告 a. Python2.5 新增 b. 在 Python2.5 前,Exception 的子类 SystemExit c.在 Python2.5 前,StandardError 的子类 KeyboardInterrupt d.Python1.5 新增,用基于类的异常来替代字符串 e.Python2.2 新增 f.Python1.6 新增 g.Python2.0 新增 h.Python1.6 新增 i.Python2.3 新增 j.Python2.1 新增 k.Python2.2 新增,但在 Python2.4 时移除 所有的标准/内建异常都是从根异常派生的.目前,有 3 个直接从 BaseException 派生的异常子 类:SystemExit,KeyboardInterrupt 和 Exception.其他的所有的内建异常都是 Exception 的子类.表 10.2 中的每一层缩进都代表一次异常类的派生. 到了 Python2.5,所有的异常的都是新风格(new-style)的类,并且最终都是 BaseException 的子 类.在这一版中,SystemExit 和 KeyboardInterrupt 从 Exception 的继承中移到 BaseException 的继 承下.这样可以允许如 except Exception 的语句捕获所有非控制程序退出的异常. 从 Python1.5 到 Python2.4.x,异常是标准的类,在这之前,他们是字符串.从 Python2.5 开始,不 再支持构建基于字符串的异常并且被正式的弃用,也就是说你不能再触发一个字符串异常了.在 2.6, 你将不能捕获他们.还有一个要求就是所有新的异常最终都是 BaseException 的子类,以便于他们有 一个统一的接口.这将从 Python2.7 开始,并在余下的 Python2.x 发布版中延续. 10.9 *创建异常 尽管标准异常集包含的内容已经相当广泛,你还是可以创建自己的异常.一种情况是你想在特定 的标准异常和模块异常中添加额外的信息.我们将介绍两个例子,都与 IOError 有关.IOError 是一个 Edit By Vheavens  Edit By Vheavens  用于输入/输出的通用异常,可能在无效的文件访问或其他形式的通信中触发.假如我们想要更加明 确的标明问题的来源,比如:对于文件错误,我们希望有行为类似 IOError 的一个 FileError 异常,但 是名字表明是在执行文件操作. 我们将查看的另一个异常与套接字(socket)网络编程有关.socket 模块生成的异常叫 socket.error,不是内建的异常.它从通用 Exception 类派生.然而 socket.error 这个异常的宗旨和 IOError 很类似,所以我们打算定义一个新的从 IOError 派生的 NetworkError 的异常,但是其包含了 socket.error 提供的信息. 如同类和面向对象编程,我们暂时不会正式介绍网络编程,如果你需要的话可以跳到 16 章. 我们现在给出一个叫做 myexc.py 的模块和我们自定义的新异常 FileError 与 NetworkError.代 码如 10.2. 例:10.2 创建异常(myexc.py) 此模块定义了两个新的异常,FileError 和 NetworkError,也重新实现了一个诊断版的 open()[myopen()]和 socket.connect()[myconnect()].同时包含了一个测试函数[test()],当直接 运行文件时执行. 1 #!/usr/bin/env python 2 3 import os, socket, errno, types, tempfile 4 5 class NetworkError(IOError): 6 pass 7 8 class FileError(IOError): 9 pass 10 11 def updArgs(args, newarg=None): 12 if isinstance(args, IOError): 13 myargs = [] 14 myargs.extend([arg for arg in args]) 15 else: 16 myargs = list(args) 17 18 if newarg: 19 myargs.append(newarg) 20 Edit By Vheavens  Edit By Vheavens  21 return tuple(myargs) 22 23 def fileArgs(file, mode, args): 24 if args[0] == errno.EACCES and \ 25 'access' in dir(os): 26 perms = '' 27 permd = { 'r': os.R_OK, 'w': os.W_OK, 28 'x': os.X_OK} 29 pkeys = permd.keys() 30 pkeys.sort() 31 pkeys.reverse() 32 33 for eachPerm in 'rwx': 34 if os.access(file, permd[eachPerm]): 35 perms += eachPerm 36 else: 37 perms += '-' 38 39 if isinstance(args, IOError): 40 myargs = [] 41 myargs.extend([arg for arg in args]) 42 else: 43 myargs = list(args) 44 45 myargs[1] = "'%s' %s (perms: '%s')" % \ 46 (mode, myargs[1], perms) 47 48 myargs.append(args.filename) 49 50 else: 51 myargs = args 52 53 return tuple(myargs) 54 55 def myconnect(sock, host, port): 56 try: 57 sock.connect((host, port)) 58 59 except socket.error, args: 60 myargs = updArgs(args) # conv inst2tuple Edit By Vheavens  Edit By Vheavens  61 if len(myargs) == 1: # no #s on some errs 62 myargs = (errno.ENXIO, myargs[0]) 63 64 raise NetworkError, \ 65 updArgs(myargs, host + ':' + str(port)) 66 67 def myopen(file, mode='r'): 68 try: 69 fo = open(file, mode) 70 except IOError, args: 71 raise FileError, fileArgs(file, mode, args) 72 73 return fo 74 75 def testfile(): 76 77 file = mktemp() 78 f = open(file, 'w') 79 f.close() 80 81 for eachTest in ((0, 'r'), (0100, 'r'), 82 (0400, 'w'), (0500, 'w')): 83 try: 84 os.chmod(file, eachTest[0]) 85 f = myopen(file, eachTest[1]) 86 87 except FileError, args: 88 print "%s: %s" % \ 89 (args.__class__.__name__, args) 90 else: 91 print file, "opened ok... perm ignored" 92 f.close() 93 94 os.chmod(file, 0777)# enable all perms 95 os.unlink(file) 96 97 def testnet(): 98 s = socket.socket(socket.AF_INET, 99 socket.SOCK_STREAM) 100 Edit By Vheavens  Edit By Vheavens  101 for eachHost in ('deli', 'www'): 102 try: 103 myconnect(s, 'deli', 8080) 104 except NetworkError, args: 105 print "%s: %s" % \ 106 (args.__class__.__name__, args) 107 108 if __name__ == '__main__': 109 testfile() 110 testnet() 行 1-3 模块的开始部分是 Unix 启动脚本和 socket,os,errno,types 和 tempfile 模块的导入. 行 5-9 无论你是否相信,这 5 行代码定义了我们的新异常.不是仅仅一个,而是两个.除了将要介绍的一 个新功能,创建一个新的异常仅需要从一个已经存在的异常类派生一个出子类.本例中,这个基类是 IOError. 我们也可以从 IOError 的基类 EnvironmentError 派生,但我们想明确表明我们的异常是 I/O 相关的. 我们选择 IOError 是因为它提供了两个参数,一个错误编号和一个错误字符串.文件相关[用 open()]的 IOError 异常甚至支持大部分异常没有的第三个参数,那个可以是文件名.我们将对这个在 主要元组之外的,名字叫"filename"的参数执行一些特定的操作. 行 11-21 updArgs()函数的全部意图就是"更新"异常的参数.我们这里的意思是原来的异常提供给我们一 个参数集.我们希望获取这些参数并让其成为我们新的异常的一部分,可能是嵌入或添加第三个参数 (如果没有传入,什么也不添加—None 是其默认值,我们下一章将会学习).我们的目标是提供更多的 细节信息给用户,这样当问题发生时能够尽快的捕捉到. Lines 23–53 函数 fileArgs()仅在 myopen()中使用(如下).实际上,我们寻找表示"permissiondenied.(没有 权限.)"的错误 EACCES.其他所有的 IOError 异常我们将不加修改(行 54-55)的传递.如果你对 ENXIO, EACCES 和其他的系统错误号感到好奇,你可以从 Unix 系统下/usr/include/sys/errno.h 或 Windows 系统下 Visula C++的 C:\Msdev\include\Errno.h 文件来对它们刨根究底. 在第 27 行,我们也确认了我们当前使用的机器支持 os.access()函数,它用来检查对任意一个特 定文件你所拥有的权限.除非我们收到权限错误同时也能够检查我们拥有的权限,否则我们什么不做. 当一切完毕,我们设置一个字典来帮助构建表示我们对文件所拥有的权限的字符串. Edit By Vheavens  Edit By Vheavens  Unix 文件系统清晰标明用户(user),组(group,可以有多个用户属于一个组)和其他(other,不是 所有者,也不和所有者同组的用户)对文件的读,写,执行(‘r’, ‘w’, ‘x’)的权限. Windows 支持这些权限中的一部分.现在可以来构建权限字符串了.如果对文件有某种权限,字符 串中就有相应的字母,否则用'-'替代..比如,字符串'rw-'标明你可以对其进行读/写访问.如果字符 串是'r-x',你仅可以对其进行读和执行操作;'---'标示没有任何权限. 当权限字符串构建完成后,我们创建了一个临时的参数列表.我们随后更改了错误字符串使之包 含权限字符串.(标准的 IOError 异常并没有提供权限字符串相关信息). 如果系统并没有告诉你具有 什么权限才能来解决这个问题,而只是显示"Permission denied(没有权限)" 这个错误信息,这似 乎是很愚蠢。 当然这是出于安全的考虑. 当入侵者没有权限访问某个数据的时候, 最好不要让他们 看到这个文件的权限是什么。不过, 我们的例子仅仅是一个练习, 所以我们可以暂时"违背安全"信 条。 问题的关键在于确认调用 os.chmod() 函数,它能够按照你的意愿来修改文件的权限。 最后一件事情我们把文件名加入参数列表,并以元组形式返回参数. 行 55-65 我们新的 myconnect()函数仅仅是简单的对套接字的函数 conect()进行包装当网络连接失败时 提供一个 IOError 类型的异常.和一般的 socket.error 不一样,我们还提供给程序员主机名和端口 号. 对于刚刚接触网络编程的,主机名和端口号可以想象为当你联系某人时的区号和电话号.在这 个例子中,我们试着去连接一个在远程主机上运行的程序,可能是某种服务. 因此我们需要知道主机 名和服务器监听的端口. 当失败发生时,错误号和错误字符很有帮助,但是如果结合更精确的主机-端口会更有帮助,因为 这一对可能是由某个数据库或名称服务动态生成或重新获得.这些值由我们版本的 connect()加入. 另一种情形是无法找到主机,socket.error 异常没有直接提供的错误号,我们为了遵循 IOError 协议, 提供了一个错误号-错误字符串对,我们查找最接近的错误号.我们选用的 ENXIO. 行 67-73 类似同类 myconnect(),myopen()也封装了已经存在的一些代码.这里,我们用的是 open()函数. 我们仅仅捕捉 IOError 异常.所有的其他都忽略并传给下一层(因为没有与他们相关的处理器).一旦 捕捉到 IOError 我们引发我们自己的异常并通过 fileArgs()返回值来定制参数. 行 75-95 我们首先测试文件,这里使用 testfile()函数.开始之前,我们需要新建一个测试文件,以便我们 可以手工的修改其权限来造成权限错误.这个 tempfile 模块包含了创建临时文件文件名和临时文件 的代码.当前我们仅仅需要文件名,然后用 myopen()函数来创建一个空的文件.注意,如果此次产生了 错误,我们不会捕获,我们的程序将致命的终止——测试程序当我们连文件都无法创建时不会继续. Edit By Vheavens  Edit By Vheavens  我们的测试用了 4 种不同的权限配置.零标示没有任何权限,0100 表示仅能执行,0400 表示只 读,0500 表示只可读或执行(0400+0100).在所有的情况下,我们试图用一种无效的方式打开文 件.os.chmod()被用来改变文件的权限(注意:这些权限有前导的零,表明他们是八进制[基数 8]数) 如果发生错误,我们希望可以显示诊断的信息,类似 Python 解释器捕获异常时所做的那样. 这就 是给出异常名和紧跟其后的异常的参数.__class__属性表示实例化该实例的类对象. 比在此显示完 整的类名(myexc.FileError)更好的做法是通过类对象的__name__属性来显示类名(FileError),这 也是异常未被捕获时你在解释器所见到的.随后是我们在封装函数中辛辛苦苦聚到一起的参数. 如果文件被打开成功,也就是权限由于某种原因被忽略.我们通过诊断信息指明并关闭文件.当 所有的测试都完成时,我们对文件开启所有的权限然后用 os.unlink()移除(os.remove()等价于 os.unlink()). 行 97-106 下一段代码(testnet())测试了我们的网络异常.套接字是一个用来与其他主机建立连接的通信 端点.我们创建一个套接字,然后用它连接一个没有接受我们连接的服务器的主机和一个不存在于我 们网络的主机. 行 108-110 我们希望仅在直接调用我们脚本时执行 test*()函数,此处的代码完成了该功能.大多数脚本用 同样的格式给出了这段文本. 在 Unix 系的机器上运行这段脚本,我们得到了如下的输出: $myexc.py FileError: [Errno 13] 'r' Permission denied (perms: '---'): '/usr/tmp/@18908.1' FileError: [Errno 13] 'r' Permission denied (perms: '--x'): '/usr/tmp/@18908.1' FileError: [Errno 13] 'w' Permission denied (perms: 'r--'): '/usr/tmp/@18908.1' FileError: [Errno 13] 'w' Permission denied (perms: 'r-x'): '/usr/tmp/@18908.1' NetworkError: [Errno 146] Connection refused: 'deli:8080' NetworkError: [Errno 6] host not found: 'www:8080' 在 Win32 的机器上有些许的不同: D:\python> python myexc.py C:\WINDOWS\TEMP\~-195619-1 opened ok... perms ignored C:\WINDOWS\TEMP\~-195619-1 Edit By Vheavens  Edit By Vheavens  opened ok... perms ignored FileError: [Errno 13] 'w' Permission denied (perms: 'r-x'): 'C:\\WINDOWS\\TEMP\\~-195619-1' FileError: [Errno 13] 'w' Permission denied (perms: 'r-x'): 'C:\\WINDOWS\\TEMP\\~-195619-1' NetworkError: [Errno 10061] winsock error: 'deli:8080' NetworkError: [Errno 6] host not found: 'www:8080' 你可以看到 Windows 不支持文件的读权限,这就是前两次尝试文件打开成功的原因.在你的机器 和操作系统上的结果可能会大相径庭。 10.10 为什么用异常(现在)? 毫无疑问,错误的存在会伴随着软件的存在.区别在于当今快节奏的计算世界, 我们的执行环境 已经改变, 所以我们需要改变错误处理, 以准确反映我们软件的开发环境. 就现今应用来说, 普遍 的是自洽(self-contained)的图形用户界面(GUIs)或是客户机/服务器体系, 例如 Web. 在应用层处理错误的能力近来变得更为重要, 用户已不再是应用程序的的唯一的直接运行者. 随着互联网和网上电子商业应用越来越普及, web服务器将成为应用软件的主要客户. 这意味着应用 程序再也不能只是直接的失败或崩溃, 因为如果这样, 系统错误导致浏览器的错误, 这反过来又 会让用户沮丧. 失去眼球意味着失去广告收入和和潜在的大量无可挽回的生意. 如果错误的确发生了, 它们一般都归因于用户输入的数据无效. 运行环境必须足够强健,来处 理应用级别的错误,并提供用户级别的错误信息.就服务器而言,这必须转化为一个"非错误" . 因为 应用必须要成功的完成, 即使所做的不过是返回一个错误的信息, 向用户是提供一个有效的超文本 标记语言(HTML)的网页指明错误. 如果你不清楚我在说什么, 那个一个简单的网页浏览器窗口,用大而黑的字体写到"内部服务器 错误"是否更耳熟?用一个弹出式窗口宣告"文件中没有数据"的致命错误如何?作为一个用户, 这 些词语对你有意义吗?没有, 当然没有(除非你是一个互联网软件工程师), 至于对普通用户来说, 这些是无休止的混乱和挫折感的来源. 这些错误导致在执行的程序时的失败. 应用不论是返回无效 的超文本传输协议( http)数据还是致命地终止, 都会导致Web 服务器举手投降, 说: "我放弃" ! 这种类型的执行错误不应该被允许, 无论情况如何. 随着系统变得更加复杂, 又牵涉到更多的 新手用户, 要采取额外的措施, 确保用户平滑的学到应用经验. 即使面对一个错误, 应用应该成功 的中止, 不至于灾难性的影响其执行环境. Python 异常处理促使成熟和正确的编程. 10.11 到底为什么要异常? 如果上文的动机不够充分, 试想 Python 编程没有程序级的异常处理. 第一件事需要担心的是客 Edit By Vheavens  Edit By Vheavens  户端程序员在自己的代码中遗忘控制. 举例来说, 如果你创造了一个交互的应用程序分配并使用了 大量的资源, 如果一个用户击中 Ctrl+C 或其他键盘中断, 应用程序将不会有机会执行清理工作, 可 能导致数据丢失或数据损坏. 此外, 也没有机制来给出可选的行为, 诸如提示用户, 以确认他们真 的是想退出或是他们意外的按下了 Ctrl 键. 另一个缺点就是函数必须重写来为错误的情形返回一个"特殊"的值, 如:None. 程序员要负责 检查每一个函数调用的返回值. 这可能是个麻烦, 因为你可能不得不检查返回值, 这和没有发生错 误时你期待结果也许不是同一类型的对象. 什么,你的函数要把 None 作为一个有效的数值返回?那 么, 你将不得不拿出另一个返回值, 也许是负数.我们也许并不需要提醒你, 在 Python 的环境下负 数下可能是有效的, 比如作为一个序列的索引. 作为一个写应用程序接口( API )的程序员, 你不 得不为每个一个用户输入可能遇到的返回错误写文档. 同时, 我们难以(而且乏味)在多层次的代 码中以传播错误(和原因). 没有一个简单的传播方法像异常一样做到这一点. 因为错误的数据需要在调用层次中向上转发, 但在前进的道路上可能被曲解. 一个不相干的错误可能会被宣布为起因,而实际上它与原始问题完 全无关.在一层一层的传递中,我们失去了对原始错误封装和保管的能力,更不用说完全地失去我们 原本关心的数据的踪影!异常不仅简化代码, 而且简化整个错误管理体系 --- 它不该在应用开发中 如此重要角色;而有了 Python 的异常处理能力, 也的确没有必要了. 10.12 异常和 sys 模块 另一种获取异常信息的途径是通过 sys 模块中 exc_info()函数. 此功能提供了一个 3 元组 (3-tuple)的信息, 多于我们单纯用异常参数所能获得. 让我们看看如何用 sys.exc_info() : >>> try: ... float('abc123') ... except: ... import sys ... exc_tuple = sys.exc_info() ... >>> print exc_tuple (, , ) >>> >>> for eachItem in exc_tuple: ... print eachItem ... exceptions.ValueError invalid literal for float(): abc123 Edit By Vheavens  Edit By Vheavens  我们从 sys.exc_info()得到的元组中是: z exc_type: 异常类 z exc_value: 异常类的实例 z exc_traceback: 追踪(traceback)对象 我们所熟悉的前两项:实际的异常类, 和这个异常类的实例(和在上一节我们讨论的异常参数 是一样的) . 第三项, 是一个新增的追踪(traceback)对象. 这一对象提供了的发生异常的上下文. 它包含诸如代码的执行帧,异常发生时的行号等信息. 在旧版本中的 Python 中, 这三个值分别存在于 sys 模块, 为 sys.exc_type , sys.exc_value , sys.exc_traceback . 不幸的是, 这三者是全局变量而不是线程安全的. 我们建议亡羊补牢, 用 sys.exc_info()来代替. 在未来版本 Python 中,所有这三个变量都将被逐步停用,并最终移除. 10.13 相关模块 表 10.3 此章的相关模块 表 10.3 异常相关的标准库 模块 描述 exceptions 内建异常(永远不用导入这个模块) contextliba 为使用 with 语句的上下文对象工具 sys 包含各种异常相关的对象和函数(见 sys.ex*) a. Python2.5 新增 10.14 练习 10–1. 引发异常. 以下的哪个因素会在程序执行时引发异常? 注意这里我们问的并不是异 常的原因. a) 用户 b) 解释器 c) 程序 d) 以上所有 e) 只有 b) 和 c) f) 只有 a) 和 c) Edit By Vheavens  Edit By Vheavens  10–2. 引发异常. 参考上边问题的列表, 哪些因素会在执行交互解释器时引发异常? 10–3. 关键字. 用来引发异常的关键字有那些? 10–4. 关键字. try-except 和 try-finally 有什么不同? 10–5. 异常. 下面这些交互解释器下的 Python 代码段分别会引发什么异常(参阅表 10.2 给出的内建异常清单): (a) >>> if 3 < 4 then: print '3 IS less than 4!' (b) >>> aList = ['Hello', 'World!', 'Anyone', 'Home?'] >>> print 'the last string in aList is:', aList[len(aList)] (c) >>> x (d) >>> x = 4 % 0 (e) >>> import math >>> i = math.sqrt(-1) 10–6. 改进的 open(). 为内建的 open() 函数创建一个封装. 使得成功打开文件后, 返 回文件句柄; 若打开失败则返回给调用者 None , 而不是生成一个异常. 这样你打开文件时就不需 要额外的异常处理语句. 10–7. 异常. 下面两段 Python 伪代码 a) 和 b) 有什么区别? 考虑语句 A 和 B 的上下 文环境. (这么细致的区别要感谢 Guido ) (a) try: statement_A except . . .: . . . else: statement_B (b) try: statement_A statement_B except . . .: . . . 10–8. 改进的 raw_input() . 本章的开头, 我们给出了一个"安全"的 float() 函数, 它建立在内建函数 float() 上, 可以检测并处理 float() 可能会引发的两种不同异常. 同样, raw_input() 函数也可能会生成两种异常, EOFError (文件末尾 EOF, 在 Unix 下是由于按下了 Ctrl+D 在 Dos 下 是因 为 Ctrl+Z)或是 KeyboardInterrupt (取消输入, 一般是由于按下了 Edit By Vheavens  Edit By Vheavens  Ctrl+C). 请创建一个封装函数 safe_input() , 在发生异常时返回 None . 10–9. 改进的 math.sqrt(). math 模块包含大量用于处理数值相关运算的函数和常量. 不 幸的是, 它不能识别复数, 所以我们创建了 cmath 模块来支持复数相关运算. 请创建一个 safe_sqrt() 函数, 它封装 math.sqrt() 并能处理负值, 返回一个对应的复数. Edit By Vheavens  Edit By Vheavens  函数和函数式编程 章节主题 z 什么是函数 z 调用函数 z 创建函数 z 传入函数 z 形参 z 变长参数 z 函数式编程 z 变量的作用域 z 递归 z 生成器 Edit By Vheavens  Edit By Vheavens  在第二章,我们引入了函数,并介绍了函数的创建和调用。这一章,我们将在前面内容的基础 上,详细的讲解函数的方方面面。除了预期特性之外, Python 中的函数还支持多种调用方式以及参 数类型并实现了一些函数式编程接口。最后我们将以对 Python 变量的作用域和递归函数的讨论来结 束本章的学习. 11.1 什么是函数? 函数是对程序逻辑进行结构化或过程化的一种编程方法。能将整块代码巧妙地隔离成易于管理 的小块,把重复代码放到函数中而不是进行大量的拷贝--这样既能节省空间,也 有助于保持一致性,因为你只需改变单个的拷贝而无须去寻找再修改大量复制代码的拷贝。 Python 中函数的基础部分与你熟悉的其他的语言没有什么不同.本章开始,我们先回顾一下函数基础, 然后将着重介绍 python 函数的其他特性. 函数可以以不同的形式出现。下面简单展示了一些创建、使用,或者引用函数的方法。 declaration/definition def foo(): print 'bar' function object/reference foo function call/invocation foo() 11.1.1 函数 vs 过程 我们经常拿函数和过程比较。两者都是可以被调用的实体,但是传统意义上的函数或者“黑盒”, 可能不带任何输入参数,经过一定的处理,最后向调用者传回返回值。其中一些函数则是布尔类型 Edit By Vheavens  Edit By Vheavens  的, 返回一个“是“或者“否“的回答,更确切地说,一个非零或者零值。而过程是简单,特殊, 没有返回值的函数。从后面内容你会看到,python 的过程就是函数,因为解释器会隐式地返回默认 值 None 11.1.2.返回值与函数类型 函数会向调用者返回一个值, 而实际编程中大偏函数更接近过程,不显示地返回任何东西。把 过程看待成函数的语言通常对于“什么都不返回”的函数设定了特殊的类型或者值的名字。这些函 数在 c 中默认为“void"的返回类型,意思是没有值返回。 在 python 中, 对应的返回对象类型是 none。 下面 hello()函数的行为就像一个过程,没有返回值。如果保存了返回值,该值为 None: >>> def hello(): ... print 'hello world' >>> >>> res = hello() hello world >>> res >>> print res None >>> type(res) 另外,与其他大多数的语言一样,python 里的函数可以返回一个值或者对象。只是在返回一个容 器对象的时候有点不同,看起来像是能返回多个对象。好比说,你不能拿着大量零散的商品离开百 货店,但是你可以将它们放在一个购物袋里,然后带着这个袋子从商店走出去,合理合法。 def foo(): return ['xyz', 1000000, -98.6] def bar(): return 'abc', [42, 'python'], "Guido" foo()函数返回一个列表,bar()函数返回一个元组。由于元组语法上不需要一定带上圆括号, 所 以让人真的以为可以返回多个对象。如果我们要恰当地给这个元组加上括号, bar()的定义看起来 会是这样: def bar(): Edit By Vheavens  Edit By Vheavens  return ('abc', [4-2j, 'python'], "Guido") 从返回值的角度来考虑, 可以通过很多方式来存储元组。接下来的 3 种保存返回值的方式是等 价的 >>> aTuple = bar() >>> x, y, z = bar() >>> (a, b, c) = bar() >>> >>> aTuple ('abc', [(4-2j), 'python'], 'Guido') >>> x, y, z ('abc', [(4-2j), 'python'], 'Guido') >>> (a, b, c) ('abc', [(4-2j), 'python'], 'Guido') 在对 x,y,z 和 a,b,c 的赋值中,根据值返回的顺序, 每个变量会接收到与之对应的返回值。而 aTuple 直接获得函数隐式返回的整个元组。回想一下,元组既可以被分解成为单独的变量,也可以直 接用单一变量对其进行引用。(参见6.18.3) 简而言之,当没有显式地返回元素或者如果返回 None 时, python 会返回一个 None.那么调用 者接收的就是 python 返回的那个对象,且对象的类型仍然相同。如果函数返回多个对象,python 把 他们聚集起来并以一个元组返回。是的,尽管我们声称 python 比诸如 c 那样只允许一个返回值的语 言灵活的多,但是老实说,python 也遵循了相同的传统。只是让程序员误以为可以返回多个对象。 表 11.1 返回值及其类型 表 11.1 总结了从一个函数中返回的元素的数目,以及 python 实际返回的对象。 Edit By Vheavens  Edit By Vheavens  许多静态类型的语言主张一个函数的类型就是其返回值的类型。在 python 中, 由于 python 是 动态地确定类型而且函数能返回不同类型的值,所以没有进行直接的类型关联。因为重载并不是语 言特性,程序员需要使用 type()这个内建函数作为代理,来处理有着不同参数类型的函数的多重声 明以模拟类 C 语言的函数重载(以参数不同选择函数的多个原型)。 11.2 调用函数 11.2.1.函数操作符 同大多数语言相同,我们用一对圆括号调用函数。实际上,有些人认为(())是一个双字符操作 符。正如你可能意识到的,任何输入的参数都必须放置在括号中。作为函数声明的一部分,括号也 会用来定义那些参数。虽然我们没有正式地学习类和面向对象编程,但你将会发现在 python 中,函 数的操作符同样用于类的实例化。 11.2.2.关键字参数 关键字参数的概念仅仅针对函数的调用。这种理念是让调用者通过函数调用中的参数名字来区 分参数。这样规范允许参数缺失或者不按顺序,因为解释器能通过给出的关键字来匹配参数的值。 举个简单的例子,比如有一个函数 foo(),伪代码如下: def foo(x): foo_suite # presumably does some processing with 'x' 标准调用 foo():foo(42) foo('bar') foo(y) 关键字调用 foo():foo(x=42) foo(x='bar') foo(x=y) 再举个更实际的例子, 假设你有一个函数叫做 net_conn(),需要两个参数 host 和 port: def net_conn(host, port): net_conn_suite 只要按照函数声明中参数定义的顺序,输入恰当的参数,自然就可以调用这个函数: net_conn('kappa', 8080) host 参数得到字符串'kappa',port 参数得到整数 8080.当然也可以不按照函数声明中的参数顺 序输入,但是要输入相应的参数名,如下例: Edit By Vheavens  Edit By Vheavens  net_conn(port=8080, host='chino') 当参数允许"缺失“的时候,也可以使用关键字参数.这取决于函数的默认参数, 我们将在下一 小节对它进行介绍。 11.2.3.默认参数 默认参数就是声明了默认值的参数。因为给参数赋予了默认值,所以, 在函数调用时,不向该 参数传入值也是允许的。我们将在 11.5.2 章对默认参数进行更全面的介绍。 11.2.4.参数组 Python 同样允许程序员执行一个没有显式定义参数的函数,相应的方法是通过一个把元组(非 关键字 参数)或字典(关键字参数)作为参数组传递给函数。我们将在本章中讨论这两种形式。基本 上,你可以将所有参数放进一个元组或者字典中,仅仅用这些装有参数的容器来调用一个函数,而 不必显式地将它们放在函数调用中: func(*tuple_grp_nonkw_args, **dict_grp_kw_args) 其中的 tuple_grp_nonkw_args 是以元组形式体现的非关键字参数组, dict_grp_kw_args是装有 关键字参数的字典。正如我们已经提到的,我们将在这章对这两者进行全面介绍,现在你只需知道, 存在这样的特性允许你把变量放在元组和/或者字典里,并在没有显式地对参数进行逐个声明的情况 下,调用函数。 实际上,你也可以给出形参!这些参数包括标准的位置参数和关键字参数,所以在 python 中允 许的函数调用的完整语法为: func(positional_args, keyword_args, *tuple_grp_nonkw_args, **dict_grp_kw_args) 该语法中的所有的参数都是可选的---从参数传递到函数的过程来看,在单独的函数调用时,每 个参数都是独立的。这可以有效地取代 apply()内建函数。(Prior to Python 1.6, such argument objects could only be passed to apply() with the function object for invocation.)(在 Python 1.6 版本之前,这样的参数对象只能通过 apply()函数来调用)。 例子 Edit By Vheavens  Edit By Vheavens  在子 11.1 里的数学游戏中,我们用函数调用转换来生成一个有两个子项的参数列表,并把这个 列表发送给合的适算术函数.(我们也会指出在原来版本中哪些地方会用到 apply()) easyMath.py 程序是一个儿童算术游戏,可以随机选择算术加减法。我们通过函数 add(),sub() 等价+-运算符,这两者都可以在 operator 模块中找到。接着我们生成一个参数列表(该列表只有 2 个参数, 因为这些是二元运算符/运算)。接着选择任意的数字作为算子。因为我们没打算在这个程 序的基础版本中支持负数,所以我们将两个数字的列表按从大到小的顺序排序,然后用这个参数列 表和随机选择的算术运算符去调用相对应的函数,最后获得问题的正确解答。 例子 11.1 算术游戏(easyMath.py) 随机选择数字以及一个算术函数, 显示问题, 以及验证结果. 在 3 次错误的尝试以后给出结果, 等到用户输入一个正确的答案后便会继续运行. 1 #!/usr/bin/env python 2 3 from operator import add, sub 4 from random import randint, choice 5 6 ops = {'+': add, '-': sub} 7 MAXTRIES = 2 8 9 def doprob(): 10 op = choice('+-') 11 nums = [randint(1,10) for i in range(2)] 12 nums.sort(reverse=True) 13 ans = ops[op](*nums) 14 pr = '%d %s %d = ' % (nums[0], op, nums[1]) 15 oops = 0 16 while True: 17 try: 18 if int(raw_input(pr)) == ans: 19 print 'correct' 20 break 21 if oops == MAXTRIES: 22 print 'answer\n%s%d'%(pr, ans) 23 else: 24 print 'incorrect... try again' 25 oops += 1 Edit By Vheavens  Edit By Vheavens  26 except (KeyboardInterrupt, \ 27 EOFError, ValueError): 28 print 'invalid input... try again' 29 30 def main(): 31 while True: 32 doprob() 33 try: 34 opt = raw_input('Again? [y]').lower() 35 if opt and opt[0] == 'n': 36 break 37 except (KeyboardInterrupt, EOFError): 38 break 39 40 if __name__ == '__main__': 41 main() 逐行解释 Lines 1– 4 我们的代码从通常的 unix 启动行开始,接着从 operator 和 random 模块中,导入我们会用到 的函数。 Lines 6–7 在这个应用程序中我们用的全局变量有:一个包含了运算符和与其相关联的函数的集合(字典), 一个决定在给出正解之前,用户有多少次机会尝试给出答案的整型变量。函数字典的键值是运算符 的符号,程序通过查字典找到合适的算术函数。 Lines 9–28 doprob()函数是应用程序的核心引擎。该函数随机选择一个操作并生成两个操作数,同时为了 避免减法问题中的负数问题,将这两个算子按大到下进行排序。然后用这些值调用一个数学函数, 计算出正确的解。接着用一个等式来提示用户输入并给用户三次机会来输入一个正确的答案。 第十行用了 random.choice()函数。它用于获取一个序列----我们案例中运算符号的字符串-- 并随机返回其中的元素。 第 11 行用了一个列表解析来随机地给我们的练习选择两个数。这个例子非常的简单以至于我们 可以仅仅用两次 randint()来获得我们的操作数, 比如, nums = [randint(1,10), randint(1,10)], 但是为了让你能看看列表解析的又一个例子,我们没有这样做,而且使用列表解析更易于扩展和升 级,比如获得更多的数,这与我们使用循环来代替剪切和粘贴的原因相似。 Edit By Vheavens  Edit By Vheavens  第 12 行只能在 python2.4 以及更新的版本中运行,因为 list.sort()方法原本不支持倒转的标志 位。如果你使用的是更早一点的 python 版本,你要么: z 增加一个反序的比较函数来获得倒转的排序,如:lambda x, y: cmp(y, x), 或者 z 在 nums.sort()后调用 nums.reverse() 如果你之前没有看见过 lambda,不用害怕。我们会在这章对 lambda 进行详述,而现在,你可以 认为它是一个单行的匿名函数。 如果你正使用 1.6 以前的 python,那第 13 行是可能会用到 apply()。对合适运算函数的调用要 这样写 apply(ops[op],nums),而不是 ops[op](*nums) 16-28 行描述了用来处理有效和无效输入的控制循环。while 循环是无限循环,直到有正确答案 输入或者允许尝试的次数(我们的程序中设定为 3 次)被耗尽才终止运行。这允许程序接受不合法 的输入,比如非数字或者各种键盘的控制字符。一旦用户超过了尝试最大的次数,程序就会给出答案 并“强制“用户给出正确的答案,只有给出答案,程序才会向下进行。 Lines 30–41 程序的主入口是 main(),如果直接运行脚本,程序将自顶向下的运行。如果被作为模块导入, 导入者要么调用 doprob()函数来开始执行,要么调用 main()来进入程序控制。main()简单地调用 doprob()使用户与脚本的主要功能进行交互, 并负责提示用户退出或者尝试下一个问题。 因为数值和运算符都是随机选择的,每次运行 easyMath.py 的结果应该都是不一样的。这是我 们今天的得到的(噢,你的答案也可能不一样!!!!): $ easyMath.py 7 - 2 = 5 correct Again? [y] 7 * 6 = 42 correct Again? [y] 7 * 3 = 20 incorrect... try again 7 * 3 = 22 incorrect... try again 7 * 3 = 23 sorry... the answer is 7 * 3 = 21 7 * 3 = 21 correct Again? [y] 7 - 5 = 2 correct Again? [y] n Edit By Vheavens  Edit By Vheavens  11.3 创建函数 11.3.1. def 语句 函数是用 def 语句来创建的,语法如下: def function_name(arguments): "function_documentation_string" function_body_suite 标题行由 def 关键字,函数的名字,以及参数的集合(如果有的话)组成。def 子句的剩余部分 包括了一个虽然可选但是强烈推荐的文档字串,和必需的函数体。在本书中我们已经看到很多函数 的声明,这又是一个: def helloSomeone(who): 'returns a salutory string customized with the input' return "Hello " + str(who) 11.3.2.声明与定义比较 在某些编程语言里, 函数声明和函数定义区分开的。一个函数声明包括提供对函数名,参数的 名字(传统上还有参数的类型),但不必给出函数的任何代码,具体的代码通常属于函数定义的范畴。 在声明和定义有区别的语言中,往往是因为函数的定义可能和其声明放在不同的文件中。python 将这两者视为一体,函数的子句由声明的标题行以及随后的定义体组成的。 11.3.3 前向引用 和其他高级语言类似,Python 也不允许在函数未声明之前,对其进行引用或者调用. 我们下面给出几个例子来看一下: def foo(): print 'in foo()' bar() 如果我们调用函数 foo(),肯定会失败,因为函数 bar()还没有声明: >>> foo() Edit By Vheavens  Edit By Vheavens  in foo() Traceback (innermost last): File "", line 1, in ? File "", line 3, in foo NameError: bar 我们现在定义函数 bar(),在函数 foo()前给出 bar()的声明: def bar(): print 'in bar()' def foo(): print 'in foo()' bar() 现在我们可以安全的调用 foo(),而不会出现任何问题: >>> foo() in foo() in bar() 事实上,我们甚至可以在函数 bar()前定义函数 foo(): def foo(): print 'in foo()' bar() def bar(): print 'in bar()' 太神奇了,这段代码可以非常好的运行,不会有前向引用的问题: >>> foo() in foo() in bar() 这段代码是正确的因为即使(在foo()中)对bar()进行的调用出现在 bar()的定义之前,但foo() 本身不是在 bar()声明之前被调用的。换句话说,我们声明foo(),然后再声明bar(),接着调用foo(), 但是到那时,bar()已经存在了,所以调用成功。 注意 foo()在没有错误的情况下成功输出了'in foo()'。名字错误是当访问没有初始化的标识符 时才产生的异常 Edit By Vheavens  Edit By Vheavens  11.3.4.函数属性 在这一章中,我们稍后将对命名空间进行简短的讨论,尤其是它们与变量作用域的关系。在下 一章中会有对命名空间的更深入的探讨,然而,这里我们只是想要指出python 名字空间的基本特征。 你可以获得每个 pyhon 模块,类,和函数中任意的名字空间。你可以在模块 foo 和 bar 里都有 名为 x 的一个变量,,但是在将这两个模块导入你的程序后,仍然可以使用这两个变量。所以,即使 在两个模块中使用了相同的变量名字,这也是安全的,因为句点属性标识对于两个模块意味了不同 的命名空间,比如说,在这段代码中没有名字冲突: import foo, bar print foo.x + bar.x 函数属性是 python 另外一个使用了句点属性标识并拥有名字空间的领域。(更多关于名字空间 将在本章的稍后部分以及第 12 章关于 python 的模块中进行讨论) def foo(): 'foo() -- properly created doc string' def bar(): pass bar.__doc__ = 'Oops, forgot the doc str above' bar.version = 0.1 上面的 foo()中,我们以常规地方式创建了我们的文档字串,比如, 在函数声明后第一个没有 赋值的字串。当声明bar()时,我们什么都没做,仅用了句点属性标识来增加文档字串以及其他属性。 我们可以接着任意地访问属性。下面是一个使用了交互解释器的例子。(你可能已经发现,用内建函 数 help()显示会比用__doc__属性更漂亮,但是你可以选择你喜欢的方式) >>> help(foo) Help on function foo in module __main__: foo() foo() -- properly created doc string >>> print bar.version 0.1 >>> print foo.__doc__ Edit By Vheavens  Edit By Vheavens  foo() -- properly created doc string >>> print bar.__doc__ Oops, forgot the doc str above 注意我们是如何在函数声明外定义一个文档字串。然而我们仍然可以就像平常一样,在运行时 刻访问它。然而你不能在函数的声明中访问属性。换句话说,在函数声明中没有'self‘这样的东西 让你可以进行诸如__dict__['version'] = 0.1 的赋值。这是因为函数体还没有被创建,但之后你有 了函数对象,就可以按我们在上面描述的那样方法来访问它的字典。另外一个自由的名字空间! 函数属性是在 2.1 中添加到 python 中的,你可以在 PEP232 中阅读到更多相关信息。 11.3.5 内部/内嵌函数 在函数体内创建另外一个函数(对象)是完全合法的。这种函数叫做内部/内嵌函数。因为现在 python 支持静态地嵌套域(在 2.1 中引入但是到 2.2 时才是标准),内部函数实际上很有用的。内嵌 函数对于较老的 python 版本没有什么意义,那些版本中只支持全局和一个局部域。那么如何去创造 一个内嵌函数呢? 最明显的创造内部函数的方法是在外部函数的定义体内定义函数(用 def 关键字),如在: def foo(): def bar(): print 'bar() called' print 'foo() called' bar() foo() bar() 我们将以上代码置入一个模块中,如 inner.py,然后运行,我们会得到如下输出: foo() called bar() called Traceback (most recent call last): File "inner.py", line 11, in ? bar() NameError: name 'bar' is not defined 内部函数一个有趣的方面在于整个函数体都在外部函数的作用域(即是你可以访问一个对象的 区域;稍后会有更多关于作用域的介绍)之内。如果没有任何对 bar()的外部引用,那么除了在函数 体内,任何地方都不能对其进行调用,这就是在上述代码执行到最后你看到异常的原因 另外一个函数体内创建函数对象的方式是使用 lambda 语句。我们会在稍后的 11.7.1 小节进行 Edit By Vheavens  Edit By Vheavens  讲述。如果内部函数的定义包含了在外部函数里定义的对象的引用(这个对象甚至可以是在外部函 数之外),内部函数会变成被称为闭包(closure)的特别之物。在接下来的 11.8.4 小节,我们将对 闭包进行更多的学习。在下一小节中,我们将介绍装饰器,但是例子程序也包含了闭包的预览。 11.3.6 *函数(与方法)装饰器 装饰器背后的主要动机源自 python 面向对象编程。装饰器是在函数调用之上的修饰。这些修饰 仅是当声明一个函数或者方法的时候,才会应用的额外调用。 装饰器的语法以@开头,接着是装饰器函数的名字和可选的参数。紧跟着装饰器声明的是被修饰 的函数,和装饰函数的可选参数。装饰器看起来会是这样: @decorator(dec_opt_args) def func2Bdecorated(func_opt_args): : 那么装饰器语法如何(以及为什么)产生的呢?装饰器背后的灵感是什么?唔,当静态方法和 类方法在 2.2 时被加入到 python 中的时候,实现方法很笨拙: class MyClass(object): def staticFoo(): : staticFoo = staticmethod(staticFoo) : (要澄清的是对于那个发行版本,这不是最终的语法)在这个类的声明中,我们定义了叫 staticFoo()的方法。现在因为打算让它成为静态方法,我们省去它的 self 参数,而你会在 12 章中 看到,self 参数在标准的类方法中是必需的。接着用 staticmethod()内建函数来将这个函数“转化 “为静态方法,但是在 def staticFoo()后跟着 staticFoo = staticmethod (sta- ticFoo)显得有 多么的臃肿。使用装饰器,你现在可以用如下代码替换掉上面的: class MyClass(object): @staticmethod def staticFoo(): : 此外,装饰器可以如函数调用一样“堆叠“起来,这里有一个更加普遍的例子,使用了多个装 饰器: Edit By Vheavens  Edit By Vheavens  @deco2 @deco1 def func(arg1, arg2, ...): pass 这和创建一个组合函数是等价的。 def func(arg1, arg2, ...): pass func = deco2(deco1(func)) 函数组合用数学来定义就像这样: (g · f)(x) = g(f(x))。对于在 python 中的一致性 @g @f def foo(): : ......与 foo=g(f(foo))相同 有参数和无参数的装饰器 是的,装饰器语法一开始有点让你犯迷糊,但是一旦你适应了,唯一会困扰你的就是什么时候 使用带参数的装饰器。没有参数的情况,一个装饰器如: @deco def foo(): pass ....非常的直接 foo = deco(foo) 跟着是无参函数(如上面所见)组成。然而,带参数的装饰器 decomaker() @decomaker(deco_args) def foo(): pass . . . 需要自己返回以函数作为参数的装饰器。换句话说,decomaker()用 deco_args 做了些事并返回 函数对象,而该函数对象正是以 foo 作为其参数的装饰器。简单的说来: foo = decomaker(deco_args)(foo) 这里有一个含有多个装饰器的例子,其中的一个装饰器带有一个参数 Edit By Vheavens  Edit By Vheavens  @deco1(deco_arg) @deco2 def func(): pass This is equivalent to:这等价于: func = deco1(deco_arg)(deco2(func)) 我们希望如果你明白这里的这些例子,那么事情就变得更加清楚了。下面我们会给出简单实用 的脚本,该脚本中装饰器不带任何参数。例子 11.8 就是含有无参装饰器的中间脚本。 那么什么是装饰器? 现在我们知道装饰器实际就是函数。我们也知道他们接受函数对象。但它们是怎样处理那些函 数的呢?一般说来,当你包装一个函数的时候,你最终会调用它。最棒的是我们能在包装的环境下 在合适的时机调用它。我们在执行函数之前,可以运行些预备代码,如 post-morrem 分析,也可以在 执行代码之后做些清理工作。所以当你看见一个装饰器函数的时候,很可能在里面找到这样一些代 码,它定义了某个函数并在定义内的某处嵌入了对目标函数的调用或者至少一些引用。从本质上看, 这些特征引入了 java 开发者称呼之为 AOP(Aspect Oriented Programming,面向方面编程)的概念。 你可以考虑在装饰器中置入通用功能的代码来降低程序复杂度。例如,可以用装饰器来: z 引入日志 z 增加计时逻辑来检测性能 z 给函数加入事务的能力 对于用 python 创建企业级应用,支持装饰器的特性是非常重要的。你将会看到上面的条例与我 们下面的例子有非常紧密地联系,这在例 11.2 中也得到了很好地体现。 修饰符举例 下面我们有个极其简单的例子,但是它应该能让你开始真正地了解装饰器是如何工作的。这个 例子通过显示函数执行的时间"装饰"了一个(没有用的)函数。这是一个"时戳装饰",与我们在 16 章讨论的时戳服务器非常相似。 例子 11.2 使用函数装饰器的例子(deco.py) 这个装饰器(以及闭包)示范表明装饰器仅仅是用来“装饰“(或者修饰)函数的包装,返回一 个修改后的函数对象,将其重新赋值原来的标识符,并永久失去对原始函数对象的访问。 1 #!/usr/bin/env python 2 3 from time import ctime, sleep 4 Edit By Vheavens  Edit By Vheavens  5 def tsfunc(func): 6 def wrappedFunc(): 7 print '[%s] %s() called' % ( 8 ctime(), func.__name__) 9 return func() 10 return wrappedFunc 11 12 @tsfunc 13 def foo(): 14 pass 15 16 foo() 17 sleep(4) 18 19 for i in range(2): 20 sleep(1) 21 foo() 运行脚本,我们得到如下输出: [Sun Mar 19 22:50:28 2006] foo() called [Sun Mar 19 22:50:33 2006] foo() called [Sun Mar 19 22:50:34 2006] foo() called 逐行解释 5-10 行 在启动和模块导入代码之后, tsfunc()函数是一个显示何时调用函数的时戳的装饰器。它定义 了一个内部的函数 wrappedFunc(),该函数增加了时戳以及调用了目标函数。装饰器的返回值是一个 “包装了“的函数。 Lines 12–21 我们用空函数体(什么都不做)来定义了 foo()函数并用 tsfunc()来装饰。为证明我们的设想, 立刻调用它,然后等待四秒,然后再调用两次,并在每次调用前暂停一秒。 结果,函数立刻被调用,第一次调用后,调用函数的第二个时间点应该为 5(4+1),第三次的时 间应该大约为之后的 1 秒。这与上面看见的函数输出十分吻合。 你可以在 python langugae reference, python2.4 中“What’s New in Python 2.4”的文档 以及 PEP 318 中来阅读更多关于装饰器的内容。 Edit By Vheavens  Edit By Vheavens  11.4 传递函数 当学习一门如 C 的语言时,函数指针的概念是一个高级话题,但是对于函数就像其他对象的 python 来说就不是那么回事了.函数是可以被引用的(访问或者以其他变量作为其别名),也作为参 数传入函数,以及作为列表和字典等等容器对象的元素 函数有一个独一无二的特征使它同其他对象区分开来,那就是函数是可调用的。 举例来说,可以通过函数操作来调用他们。(在python 中有其他的可调用对象。更多信息,参 见 14 章)在以上的描述中,我们注意到可以用其他的变量来做作为函数的别名 因为所有的对象都是通过引用来传递的,函数也不例外。当对一个变量赋值时,实际是将相同 对象的引用赋值给这个变量。如果对象是函数的话,这个对象所有的别名都是可调用的。 >>> def foo(): ... print 'in foo()' ... >>> bar = foo >>> bar() in foo() 当我们把 foo 赋值给 bar 时,bar 和 foo 引用了同一个函数对象,所以能以和调用 foo()相同的 方式来调用 bar()。确定你明白"foo"(函数对象的引用)和"foo()"(函数对象的调用)的区别。 稍微深入下我们引用的例子,我们甚至可以把函数作为参数传入其他函数来进行调用。 >>> def bar(argfunc): ... argfunc() ... >>> bar(foo) in foo() 注意到函数对象 foo 被传入到 bar()中。bar()调用了 foo()(用局部变量 argfunc 来作为其别名 就如同在前面的例子中我们把 foo 赋给 bar 一样)。现在我们来研究下一个更加实际的例子, numconv.py,代码在例子 11.3 中给出 例 11.3 传递和调用(内建)函数 (numConv.py) Edit By Vheavens  Edit By Vheavens  一个将函数作为参数传递,并在函数体内调用这些函数,更加实际的例子。这个脚本用传入的 转换函数简单将一个序列的数转化为相同的类型。特别地,test()函数传入一个内建函数 int(), long(), 或者 float()来执行转换。 1 #!/usr/bin/env python 2 3 def convert(func, seq): 4 'conv. sequence of numbers to same type' 5 return [func(eachNum) for eachNum in seq] 6 7 myseq = (123, 45.67, -6.2e8, 999999999L) 8 print convert(int, myseq) 9 print convert(long, myseq) 10 print convert(float, myseq) 如果我们运行这个程序,我们将会得到如下输出: $ numconv.py [123, 45, -620000000, 999999999] [123L, 45L, -620000000L, 999999999L] [123.0, 45.67, -620000000.0, 999999999.0] 11.5 形式参数 python 函数的形参集合由在调用时要传入函数的所有参数组成,这参数与函数声明中的参数列 表精确的配对。这些参数包括了所有必要参数(以正确的定位顺序来传入函数的),关键字参数(以 顺序或者不按顺序传入,但是带有参数列表中曾定义过的关键字),以及所有含有默认值,函数调用 时不必要指定的参数。(声明函数时创建的)局部命名空间为各个参数值,创建了一个名字。一旦函 数开始执行,即能访问这个名字。 11.5.1 位置参数 这些我们都是熟悉的标准化参数。位置参数必须以在被调用函数中定义的准确顺序来传递。另 外,没有任何默认参数(见下一个部分)的话,传入函数(调用)的参数的精确的数目必须和声明 的数字一致。 >>> def foo(who): # defined for only 1 argument Edit By Vheavens  Edit By Vheavens  ... print 'Hello', who ... >>> foo() # 0 arguments... BAD Traceback (innermost last): File "", line 1, in ? TypeError: not enough arguments; expected 1, got 0 >>> >>> foo('World!') # 1 argument... WORKS Hello World! >>> >>> foo('Mr.', 'World!')# 2 arguments... BAD Traceback (innermost last): File "", line 1, in ? TypeError: too many arguments; expected 1, got 2 foo()函数有一个位置参数。那意味着任何对foo()的调用必须有唯一的一个参数,不多,不少。 否则你会频频看到 TypeError。看看,python 的错误是多么具有信息性的。作为一个普遍的规则, 无论何时调用函数,都必须提供函数的所有位置参数。可以不按位置地将关键字参数传入函数,给 出关键字来匹配其在参数列表中的合适的位置是被准予的(可以回顾 11.2.2 小节) 由于默认参数的特质,他们是函数调用的可选部分。 11.5.2.默认参数 对于默认参数如果在函数调用时没有为参数提供值则使用预先定义的的默认值。这些定义在函 数声明的标题行中给出。c++也支持默认参数,和 python 有同样的语法:参数名等号默认值。这个 从句法上来表明如果没有值传递给那个参数,那么这个参数将取默认值。 python 中用默认值声明变量的语法是所有的位置参数必须出现在任何一个默认参数之前。 def func(posargs, defarg1=dval1, defarg2=dval2,...): "function_documentation_string" function_body_suite 每个默认参数都紧跟着一个用默认值的赋值语句。如果在函数调用时没有给出值,那么这个赋 值就会实现。 为什么用默认参数? 默认参数让程序的健壮性上升到极高的级别,因为它们补充了标准位置参数没有提供的一些灵 活性。这种简洁极大的帮助了程序员。当少几个需要操心的参数时候,生活不再那么复杂。这在一 个程序员刚接触到一个 API 接口时,没有足够的知识来给参数提供更对口的值时显得尤为有帮助。 使用默认参数的概念与在你的电脑上安装软件的过程类似。一个人会有多少次选择默认安装而 不是自定义安装?我可以说可能几乎都是默认安装。这既方便,易于操作,又能节省时间。如果你 Edit By Vheavens  Edit By Vheavens  是那些总是选择自定义安装的顽固分子,请记着你只是少数人之一 另外一个让开发者受益的地方在于,使开发者更好地控制为顾客开发的软件。当提供了默认值 的时候,他们可以精心选择“最佳“的默认值,所以用户不需要马上面对繁琐的选项。随着时间流 逝,当用户对系统或者 api 越来越熟悉的时候,他们最终能自行给出参数值,便不再需要使用“学 步车“了 下面这个例子中默认参数派得上用场,并在日益增长的电子商务中多少有些用处 >>> def taxMe(cost, rate=0.0825): ... return cost + (cost * rate) ... >>> taxMe(100) 108.25 >>> >>> taxMe(100, 0.05) 105.0 在上面个例子中,taxMe()函数以一个项目的成本输入参数,计算出附加了销售税的销售价格。 成本是一个必需的参数,但税率是一个默认参数(在我们的例子中为 8.25%)。或许你是一个在线零 售商,生意上的大部分客户来自相同的州或者国家。不同地方税率的顾客期望看见他们与当地销售 税率相对应的购买价格总量。为了覆盖默认的税率,你所要做的就是提供一个参数值,比如在上面 的例子中的 taxMe(100,0.05)。通过指定 5%税率,你提供了一个参数作为税率参数,所以覆盖或者 说绕过了 0.0825 的默认值。 所有必需的参数都要在默认参数之前。为什么?简单说来就是因为它们是强制性的,但默认参 数不是。从句法构成上看,对于解释器来说,如果允许混合模式,确定什么值来匹配什么参数是不 可能的。如果没有按正确的顺序给出参数,就会产生一个语法错误。 >>> def taxMe2(rate=0.0825, cost): ... return cost * (1.0 + rate) ... SyntaxError: non-default argument follows default argument Edit By Vheavens  Edit By Vheavens  让我们再看下关键字参数,用我们的老朋友 net_conn() def net_conn(host, port): net_conn_suite 读者应该还记得,如果命名了参数,这里可以不按顺序给出参数。由于有了上述声明,我们可 以做出如下(规则的)位置或者关键字参数调用: z net_conn('kappa', 8000) z net_conn(port=8080, host='chino') 然而,如果我们将默认参数引入这个等式,情况就会不同,虽然上面的调用仍然有效。让我们 修改下 net_conn()的声明以使端口参数有默认值 80,再增加另外的名为 stype(服务器的类型)默认 值为‘tcp‘的参数: def net_conn(host, port=80, stype='tcp'): net_conn_suite 我们已经扩展了调用 net_conn()的方式。以下就是所有对 net_conn()有效的调用 z net_conn('phaze', 8000, 'udp') # no def args used z net_conn('kappa') # both def args used z net_conn('chino', stype='icmp') # use port def arg z net_conn(stype='udp', host='solo') # use port def arg z net_conn('deli', 8080) # use stype def arg z net_conn(port=81, host='chino') # use stype def arg 在上面所有的例子中,我们发现什么是一直不变的?唯一的必须参数,host。host没有默认值, 所以他必须出现在所有对 net_conn()的调用中。关键字参数已经被证明能给不按顺序的位置参数提 供参数,结合默认参数,它们同样也能被用于跳过缺失参数,上面例子就是极好的证据。 默认函数对象参数举例 我们现在将给出另外一个证明默认参数会让人受益的例子。grabWeb.py 脚本,在例子 11.4 中给 出,是一个主要目的是从互联网上抓取一个 Web 页面并暂时储存到一个本地文件中用于分析的简单 脚本。这类程序能用来测试 web 站点页面的完整性或者能监测一个服务器的负载(通过测量可链接 性或者下载速度)。process()函数可以做我们想要的任何事,表现出了无限种的用途。我们为这 个练习选择的用法是显示从 web 页面上获得的第一和最后的非空格行。虽然在现实中这个特别的例 Edit By Vheavens  Edit By Vheavens  子或许没有多少用处,但是你可以以这段代码为基础,举一反三。 例子 抓取网页 这段脚本下载了一个 web 页面(默认为本地的 www 服务器)并显示了 html 文件的第一个以及最 后一个非空格行。由于download()函数的双默认参数允许用不同的 urls 或者指定不同的处理函数来 进行覆盖,灵活性得倒了提高。 1 #!/usr/bin/env python 2 3 from urllib import urlretrieve 4 5 def firstNonBlank(lines): 6 for eachLine in lines: 7 if not eachLine.strip(): 8 continue 9 else: 10 return eachLine 11 12 def firstLast(webpage): 13 f = open(webpage) 14 lines = f.readlines() 15 f.close() 16 print firstNonBlank(lines), 17 lines.reverse() 18 print firstNonBlank(lines), 19 20 def download(url='http://www', 21 process=firstLast): 22 try: 23 retval = urlretrieve(url)[0] 24 except IOError: 25 retval = None 26 if retval: # do some processing 27 process(retval) 28 29 if __name__ == '__main__': 30 download() 在我们的环境下运行这个脚本会得到如下的输出,虽然你的内容是绝对不同的,因为你将浏览 Edit By Vheavens  Edit By Vheavens  一个完全不同的网页。 $ grabWeb.py 11.6 可变长度的参数 可能会有需要用函数处理可变数量参数的情况。这时可使用可变长度的参数列表。变长的参数 在函数声明中不是显式命名的,因为参数的数目在运行时之前是未知的(甚至在运行的期间,每次 函数调用的参数的数目也可能是不同的),这和常规参数(位置和默认)明显不同,常规参数都是在 函数声明中命名的。由于函数调用提供了关键字以及非关键字两种参数类型,python 用两种方法来 支持变长参数, 在 11.2.4 小节中,我们了解了在函数调用中使用*和**符号来指定元组和字典的元素作为非关 键字以及关键字参数的方法。在这个部分中,我们将再次使用相同的符号,但是这次在函数的声明 中,表示在函数调用时接收这样的参数。这语法允许函数接收在函数声明中定义的形参之外的参数。 11.6.1.非关键字可变长参数(元组) 当函数被调用的时候,所有的形参(必须的和默认的)都将值赋给了在函数声明中相对应的局 部变量。剩下的非关键字参数按顺序插入到一个元组中便于访问。可能你对 C 中的“varargs“很熟 悉(比如, va_list, va_arg,以及省略号[....])。Python 提供了与之相等的支持---迭代过所有的 元组元素和在 C 中用 va_arg 是相同的。对于那些不熟悉 C 或者"varargs"的人,这仅仅代表了在函 数调用时,接受一个不定(非固定)数目的参数。 可变长的参数元组必须在位置和默认参数之后,带元组(或者非关键字可变长参数)的函数普 遍的语法如下: def function_name([formal_args,] *vargs_tuple): "function_documentation_string" function_body_suite 星号操作符之后的形参将作为元组传递给函数,元组保存了所有传递给函数的"额外"的参数(匹 配了所有位置和具名参数后剩余的)。如果没有给出额外的参数,元组为空。 正如我们先前看见的,只要在函数调用时给出不正确的函数参数数目,就会产生一个 TypeError 异常。通过末尾增加一个可变的参数列表变量,我们就能处理当超出数目的参数被传入函数的情形, 因为所有的额外(非关键字)参数会被添加到变量参数元组。(额外的关键字参数需要关键字变量参 数[参见下一小节].)正如预料的那样,由于和位置参数必须放在关键字参数之前一样的原因,所有 Edit By Vheavens  Edit By Vheavens  的形式参数必须先于非正式的参数之前出现。 def tupleVarArgs(arg1, arg2='defaultB', *theRest): 'display regular args and non-keyword variable args' print 'formal arg 1:', arg1 print 'formal arg 2:', arg1 for eachXtrArg in theRest: print 'another arg:', eachXtrArg 我们现在调用这个函数来说明可变参数元组是如何工作的。 >>> tupleVarArgs('abc') formal arg 1: abc formal arg 2: defaultB >>> >>> tupleVarArgs(23, 4.56) formal arg 1: 23 formal arg 2: 4.56 >>> >>> tupleVarArgs('abc', 123, 'xyz', 456.789) formal arg 1: abc formal arg 2: 123 another arg: xyz another arg: 456.789 11.6.2.关键字变量参数(Dictionary) 在我们有不定数目的或者额外集合的关键字的情况中, 参数被放入一个字典中,字典中键为参 数名,值为相应的参数值。为什么一定要是字典呢?因为为每个参数-参数的名字和参数值--都是成 对给出---用字典来保存这些参数自然就最适合不过了。 这给出使用了变量参数字典来应对额外关键字参数的函数定义的语法: def function_name([formal_args,][*vargst,] **vargsd): function_documentation_string function_body_suite 为了区分关键字参数和非关键字非正式参数,使用了双星号(**)。 **是被重载了的以便不与 幂运算发生混淆。关键字变量参数应该为函数定义的最后一个参数,带**。我们现在展示一个如何 使用字典的例子: def dictVarArgs(arg1, arg2='defaultB', **theRest): 'display 2 regular args and keyword variable args' Edit By Vheavens  Edit By Vheavens  print 'formal arg1:', arg1 print 'formal arg2:', arg2 for eachXtrArg in theRest.keys(): print 'Xtra arg %s: %s' % \ (eachXtrArg, str(theRest[eachXtrArg])) 在解释器中执行这个代码,我们得到以下输出。 >>> dictVarArgs(1220, 740.0, c='grail') formal arg1: 1220 formal arg2: 740.0 Xtra arg c: grail >>> >>> dictVarArgs(arg2='tales', c=123, d='poe', arg1='mystery') formal arg1: mystery formal arg2: tales Xtra arg c: 123 Xtra arg d: poe >>> >>> dictVarArgs('one', d=10, e='zoo', men=('freud', 'gaudi')) formal arg1: one formal arg2: defaultB Xtra arg men: ('freud', 'gaudi') Xtra arg d: 10 Xtra arg e: zoo 关键字和非关键字可变长参数都有可能用在同一个函数中,只要关键字字典是最后一个参数并 且非关键字元组先于它之前出现,正如在如下例子中的一样: def newfoo(arg1, arg2, *nkw, **kw): display regular args and all variable args' print 'arg1 is:', arg1 print 'arg2 is:', arg2 for eachNKW in nkw: print 'additional non-keyword arg:', eachNKW for eachKW in kw.keys(): print "additional keyword arg '%s': %s" % \ (eachKW, kw[eachKW]) 在解释器中调用我们的函数,我们得到如下的输出: >>> newfoo('wolf', 3, 'projects', freud=90, gamble=96) Edit By Vheavens  Edit By Vheavens  arg1 is: wolf arg2 is: 3 additional non-keyword arg: projects additional keyword arg 'freud': 90 additional keyword arg 'gamble': 96 11.6.3 调用带有可变长参数对象函数 在上面的 11.2.4 部分中,我们介绍了在函数调用中使用*和**来指定参数集合。接下来带着对 函数接受变长参数的些许偏见,我们会向你展示更多那种语法的例子, 我们现在将用在前面部分定义的,我们的老朋友 newfoo(),来测试新的调用语法。我们第一个 对 newfoo()的调用将会使用旧风格的方式来分别列出所有的参数,甚至跟在所有形式参数之后的变 长参数: >>> newfoo(10, 20, 30, 40, foo=50, bar=60) arg1 is: 10 arg2 is: 20 additional non-keyword arg: 30 additional non-keyword arg: 40 additional keyword arg 'foo': 50 additional keyword arg 'bar': 60 我们现在进行相似的调用;然而,我们将非关键字参数放在元组中将关键字参数放在字典中, 而不是逐个列出变量参数: >>> newfoo(2, 4, *(6, 8), **{'foo': 10, 'bar': 12}) arg1 is: 2 arg2 is: 4 additional non-keyword arg: 6 additional non-keyword arg: 8 additional keyword arg 'foo': 10 additional keyword arg 'bar': 12 最终,我们将再另外进行一次调用,但是是在函数调用之外来创建我们的元组和字典。 >>> aTuple = (6, 7, 8) >>> aDict = {'z': 9} >>> newfoo(1, 2, 3, x=4, y=5, *aTuple, **aDict) arg1 is: 1 Edit By Vheavens  Edit By Vheavens  arg2 is: 2 additional non-keyword arg: 3 additional non-keyword arg: 6 additional non-keyword arg: 7 additional non-keyword arg: 8 additional keyword arg 'z': 9 additional keyword arg 'x': 4 additional keyword arg 'y': 5 注意我们的元组和字典参数仅仅是被调函数中最终接收的元组和字典的子集。额外的非关键字 值‘3’以及‘x’和‘y'关键字对也被包含在最终的参数列表中,而它们不是’*‘和’**‘的可变 参数中的元素。 之前的 1.6,过去变长对象只能通过 apply()函数传递给被调用函数。现在的调用语法已经可 以有效取代 apply()的使用。下面演示了如何使用了这些符号来把任意类型任意个数的参数传递给 任意函数对象。 函数式编程举例 函数式编程的另外一个有用的应用出现在调试和性能测量方面上。你正在使用需要每夜都被完 全测试或通过衰退,或需要给对潜在改善进行多次迭代计时的函数来工作。你所要做的就是创建一 个设置测试环境的诊断函数,然后对有疑问的地方,调用函数。因为系统应该是灵活的, 所以想 testee 函数作为参数传入。那么这样的函数对,timeit()和 testit(),可能会对如今的软件开发者 有帮助。 我们现在将展示这样的一个 testit()函数的例子的源代码(见例子 11.5)。我们将留下timeit() 函数作为读者的练习(见习题 11.12) 该模块给函数提供了一个执行测试的环境。testit()函数使用了一个函数和一些参数,然后在 异常处理的监控下,用给定的参数调用了那个函数。如果函数成功的完成, 会返回 True 和函数的 返回值给调用者。任何的失败都会导致 False 和异常的原因一同被返回。 (Exception 是所有运行时刻异常的根类:复习第 10 章以获得更详细的资料) Example 11.5 Testing Functions (testit.py) testit()用其参数地调用了一个给定的函数,成功的话,返回一个和那函数返回值打包的 True 的返回值,或者 False 和失败的原因。 1 #!/usr/bin/env python 2 3 def testit(func, *nkwargs, **kwargs): 4 5 try: Edit By Vheavens  Edit By Vheavens  6 retval = func(*nkwargs, **kwargs) 7 result = (True, retval) 8 except Exception, diag: 9 result = (False, str(diag)) 10 return result 11 12 def test(): 13 funcs = (int, long, float) 14 vals = (1234, 12.34, '1234', '12.34') 15 16 for eachFunc in funcs: 17 print '-' * 20 18 for eachVal in vals: 19 retval = testit(eachFunc, 20 eachVal) 21 if retval[0]: 22 print '%s(%s) =' % \ 23 (eachFunc.__name__, `eachVal`), retval[1] 24 else: 25 print '%s(%s) = FAILED:' % \ 26 (eachFunc.__name__, `eachVal`), retval[1] 27 28 if __name__ == '__main__': 29 test() 单元测试函数 test()在一个为 4 个数字的输入集合运行了一个数字转换函数的集合。为了确定 这样的功能性,在测试中有两个失败的案例。这里是运行脚本的输出: $ testit.py -------------------- int(1234) = 1234 int(12.34) = 12 int('1234') = 1234 int('12.34') = FAILED: invalid literal for int(): 12.34 -------------------- long(1234) = 1234L long(12.34) = 12L long('1234') = 1234L long('12.34') = FAILED: invalid literal for long(): 12.34 -------------------- float(1234) = 1234.0 Edit By Vheavens  Edit By Vheavens  float(12.34) = 12.34 float('1234') = 1234.0 float('12.34') = 12.34 11.7 函数式编程 Python 不是也不大可能会成为一种函数式编程语言,但是它支持许多有价值的函数式编程语言 构建。也有些表现得像函数式编程机制但是从传统上也不能被认为是函数式编程语言的构建。Python 提供的以 4 种内建函数和 lambda 表达式的形式出现 11.7.1.匿名函数与 lambda python 允许用 lambda 关键字创造匿名函数。匿名是因为不需要以标准的方式来声明,比如说, 使用 def 语句。(除非赋值给一个局部变量,这样的对象也不会在任何的名字空间内创建名字.)然而, 作为函数,它们也能有参数。一个完整的 lambda“语句”代表了一个表达式,这个表达式的定义体 必须和声明放在同一行。我们现在来演示下匿名函数的语法: lambda [arg1[, arg2, ... argN]]: expression 参数是可选的,如果使用的参数话,参数通常也是表达式的一部分。 核心笔记:lambda 表达式返回可调用的函数对象。 用合适的表达式调用一个 lambda 生成一个可以像其他函数一样使用的函数对象。它们可被传入 给其他函数,用额外的引用别名化,作为容器对象以及作为可调用的对象被调用(如果需要的话, 可以带参数)。当被调用的时候,如过给定相同的参数的话,这些对象会生成一个和相同表达式等价 的结果。它们和那些返回等价表达式计算值相同的函数是不能区分的。 在我们看任何一个使用 lambda 的例子之前,我们意欲复习下单行语句,然后展示下 lambda 表 达式的相似之处。 def true(): return True 上面的函数没有带任何的参数并且总是返回 True。python 中单行函数可以和标题写在同一行。 如果那样的话,我们重写下我们的 true()函数以使其看其来像如下的东西: def true(): return True 在整这个章节,我们将以这样的方式呈现命名函数,因为这有助于形象化与它们等价的 lamdba 表达式。至于我们的 true()函数,使用 lambda 的等价表达式(没有参数,返回一个 True)为: lambda :True Edit By Vheavens  Edit By Vheavens  命名的 true()函数的用法相当的明显,但 lambda 就不是这样。我们仅仅是这样用,或者我们 需要在某些地方用它进行赋值吗?一个 lambda 函数自己就是无目地服务,正如在这里看到的: >>> lambda :True at f09ba0> 在上面的例子中,我们简单地用 lambda 创建了一个函数(对象),但是既没有在任何地方保存 它,也没有调用它。这个函数对象的引用计数在函数创建时被设置为 True,但是因为没有引用保存下 来,计数又回到零,然后被垃圾回收掉。为了保留住这个对象,我们将它保存到一个变量中,以后 可以随时调用。现在可能就是一个好机会。 >>> true = lambda :True >>> true() True 这里用它赋值看起来非常有用。相似地,我们可以把 lambda 表达式赋值给一个如列表和元组的 数据结构,其中,基于一些输入标准,我们可以选择哪些函数可以执行,以及参数应该是什么。(在 下个部分中,我们将展示如何去使用带函数式编程构建的 lambda 表达式。 我们现在来设计一个带 2 个数字或者字符串参数,返回数字之和或者已拼接的字符串的函数。 我们先将展示一个标准的函数,然后再是其未命名的等价物。 def add(x, y): return x + y ? lambda x, y: x + y 默认以及可变的参数也是允许的,如下例所示: def usuallyAdd2(x, y=2): return x+y ? lambda x, y=2: x+y def showAllAsTuple(*z): return z ? lambda *z: z 上去是一回事,所以我们现在将通过演示如何能在解释器中尝试这种做法,来努力着让你相信: >>> a = lambda x, y=2: x + y >>> a(3) 5 >>> a(3,5) 8 >>> a(0) 2 >>> a(0,9) 9 >>> >>> b = lambda *z: z Edit By Vheavens  Edit By Vheavens  >>> b(23, 'zyx') (23, 'zyx') >>> b(42) (42,) 关于 lambda 最后补充一点:虽然看起来 lambdda 是一个函数的单行版本,但是它不等同于 c++ 的内联语句,这种语句的目的是由于性能的原因,在调用时绕过函数的栈分配。lambda 表达式运作 起来就像一个函数,当被调用时,创建一个框架对象。 11.7.2 内建函数 apply()、filter()、map()、reduce() 在这个部分中,我们将看看 apply(),filter(), map(), 以及 reduce()内建函数并给出一些如 何使用它们的例子。这些函数提供了在python 中可以找到的函数式编程的特征。正如你想像的一样, lambda 函数可以很好的和使用了这些函数的应用程序结合起来,因为它们都带了一个可执行的函数 对象,lambda 表达式提供了迅速创造这些函数的机制。 表 11.2 函数式编程的内建函数 内建函数 描述 apply(func[, nkw][, kw]) a 用可选的参数来调用 func,nkw 为非关键字参数,kw 关 键字参数;返回值是函数调用的返回值。 filter(func, seq)b 调用一个布尔函数 func 来迭代遍历每个 seq 中的元素; 返回一个 使 func 返回值为 ture 的元素的序列。 map(func, seq1[,seq2...])b 将函数 func 作用于给定序列(s)的每个元素,并用一个列表来提 供返回值;如果 func 为 None, func 表现为一个身份函数,返回 一个含有每个序列中元素集合的 n 个元组的列表。 reduce(func, seq[, init]) 将二元函数作用于 seq 序列的元素,每次携带一对(先前的结果 以及下一个序列元素),连续的将现有的结果和下雨给值作用在获 得的随后的结果上,最后减少我们的序列为一个单一的返回值;如 果初始值 init 给定,第一个比较会是 init 和第一个序列元素而不 是序列的头两个元素。 a. 可以有效的取代 1.6,在其后的 python 版本中逐渐淘汰。 b.由于在 python2.0 中,列表的综合使用的引入,部分被摈弃。 *apply() 正如前面提到的, 函数调用的语法, 现在允许变量参数的元组以及关键字可变参数的字典, 在 python1.6 中有效的摈弃了 apply()。 这个函数将来会逐步淘汰,在未来版本中最终会消失。 我们 在这里提及这个函数既是为了介绍下历史,也是出于维护具有 applay()函数的代码的目的。 Edit By Vheavens  Edit By Vheavens  filter() 在本章中我们研究的第二个内建函数是 filter()。想像下,去一个果园,走的时候带着一包你 从树上采下的苹果。 如果你能通过一个过滤器,将包裹中好的苹果留下,不是一件很令人开心的事 吗?这就是 filter()函数的主要前提。给定一个对象的序列和一个“过滤”函数,每个序列元素都 通过这个过滤器进行筛选, 保留函数返回为真的的对象。filter 函数为已知的序列的每个元素调用 给定布尔函数。每个 filter 返回的非零(true)值元素添加到一个列表中。返回的对象是一个从原 始队列中“过滤后”的队列 如果我们想要用纯 python 编写 filter(),它或许就像这样: def filter(bool_func, seq): filtered_seq = [] for eachItem in seq: if bool_func(eachItem): filtered_seq.append(eachItem) return filtered_seq 一种更好地理解 filter()的方法就是形象化其行为。 图 11-1 试着那样做。 Figure 11–1 How the filter() built-in function works 在图 11-1 中,我们观察到我们原始队列在顶端, 一个大小为 n 的队列,元素从 eq[0], seq[1], . . . seq[N-1]。每一次对bool_func()的调用,举例来说,bool_func(seq[1]), bool_func (seq[0])等等,每个为 True 或 False 的的返回值都会回现。(因为Boolean 函数的每个定义--确保 你的函数确实返回一个真或假)。如果bool_func()给每个序列的元返回一个真,那个元素将会被插 入到返回的序列中。当迭代整个序列已经完成, filter()返回一个新创建的序列。我们下面展示在 一个使用了 filer()来获得任意奇数的简短列表的脚本。该脚本产生一个较大的随机数集合,然后 过滤出所有的的偶数,留给我们一个需要的数据集。当一开始编写这个例子的时候,oddnogen.py 如 下所示: Edit By Vheavens  Edit By Vheavens  from random import randint def odd(n): return n % 2 allNums = [] for eachNum in range(9): allNums.append(randint(1, 99)) print filter(odd, allNums) 代码包括两个函数:odd(), 确定一个整数是奇数(真) 或者 偶数(假)Boolean 函数,以及 main(), 主要的驱动部件。main()的目的是来产生 10 个在 1 到 100 之间的随机数:然后调用filter() 来移除掉所有的偶数。最后,先显示出我们过滤列表的大小,然后是奇数的集合 导入和运行这个模块几次后,我们能得到如下输出: $ python oddnogen.py [9, 33, 55, 65] $ python oddnogen.py [39, 77, 39, 71, 1] $ python oddnogen.py [23, 39, 9, 1, 63, 91] $ python oddnogen.py [41, 85, 93, 53, 3] 第一次重构 在第二次浏览时,我们注意到 odd()是非常的简单的以致能用一个 lambda 表达式替换 from random import randint allNums = [] for eachNum in range(9): allNums.append(randint(1, 99)) print filter(lambda n: n%2, allNums) Refactoring Pass 2 我们已经提到 list 综合使用如何能成为 filter()合适的替代者,如下便是: from random import randint Edit By Vheavens  Edit By Vheavens  allNums = [] for eachNum in range(9): allNums.append(randint(1, 99)) print [n for n in allNums if n%2] Refactoring Pass 3 我们通过整合另外的列表解析将我们最后的列表放在一起,来进一步简化我们的代码。正如你 如下看到的一样, 由于列表解析灵活的语法,就不再需要一个暂时的变量了。(为了简单,我们用 一个较短的名字将 randint()倒入到我们的代码中) from random import randint as ri print [n for n in [ri(1,99) for i in range(9)] if n%2] 虽然比原来的长些, 但是这行扮演了该例子中核心部分的代码不再如其他人想的那么模糊不清。 map() map()内建函数与 filter()相似,因为它也能通过函数来处理序列。然而,不像filter(), map() 将函数调用“映射”到每个序列的元素上,并返回一个含有所有返回值的列表。 在最简单的形式中,map()带一个函数和队列, 将函数作用在序列的每个元素上, 然后创建 由每次函数应用组成的返回值列表。所以如果你的映射函数是给每个进入的数字加 2,并且你将这个 函数和一个数字的列表传给 map(),返回的结果列表是和原始集合相同的数字集合,但是每个数字 都加了 2. 如果我们要用 python 编写这个简单形式的 map()如何运作的, 它可能像在图 11-2 中阐释的 如下代码: def map(func, seq): mapped_seq = [] for eachItem in seq: mapped_seq.append(func(eachItem)) return mapped_seq Edit By Vheavens  Edit By Vheavens  Figure 11–2 How the map() built-in function works 我们可以列举一些简短的 lambda 函数来展示如何用 map()处理实际数据: >>> map((lambda x: x+2), [0, 1, 2, 3, 4, 5]) [2, 3, 4, 5, 6, 7] >>> >>> map(lambda x: x**2, range(6)) [0, 1, 4, 9, 16, 25] >>> [x+2 for x in range(6)] [2, 3, 4, 5, 6, 7] >>> >>>[x**2 for x in range(6)] [0, 1, 4, 9, 16, 25] 我们已经讨论了有时 map()如何被列表解析取代, 所以这里我们再分析下上面的两个例子。形 式更一般的 map()能以多个序列作为其输入。如果是这种情况, 那么 map()会并行地迭代每个序 列。在第一次调用时, map()会将每个序列的第一个元素捆绑到一个元组中, 将 func 函数作用到 map()上, 当 map()已经完成执行的时候,并将元组的结果返回到 mapped_seq 映射的,最终以 整体返回的序列上。图 11-2 阐释了一个 map()如何和单一的序列一起运行。如果我们用带有每个 序列有 N 个对象的 M 个序列来的 map(),我们前面的图表会转变成如图11-3 中展示的图表那样。 Edit By Vheavens  Edit By Vheavens  图 11-3 内建函数 map()如何和>1 的序列一起运作。 这里有些使用带多个序列的 map()的例子 >>> map(lambda x, y: x + y, [1,3,5], [2,4,6]) [3, 7, 11] >>> >>> map(lambda x, y: (x+y, x-y), [1,3,5], [2,4,6]) [(3, -1), (7, -1), (11, -1)] >>> >>> map(None, [1,3,5], [2,4,6]) [(1, 2), (3, 4), (5, 6)] 上面最后的例子使用了 map()和一个为 None 的函数对象来将不相关的序列归并在一起。这种思 想在一个新的内建函数,zip,被加进来之前的 python2.0 是很普遍的。而 zip 是这样做的: >>> zip([1,3,5], [2,4,6]) [(1, 2), (3, 4), (5, 6)] reduce() 函数式编程的最后的一部分是 reduce(),reduce使用了一个二元函数(一个接收带带两个值 作为输入,进行了一些计算然后返回一个值作为输出),一个序列,和一个可选的初始化器,卓有成 效地将那个列表的内容“减少”为一个单一的值,如同它的名字一样。在其他的语言中,这种概念 也被称作为折叠。 它通过取出序列的头两个元素,将他们传入二元函数来获得一个单一的值来实现。然后又用这 Edit By Vheavens  Edit By Vheavens  个值和序列的下一个元素来获得又一个值,然后继续直到整个序列的内容都遍历完毕以及最后的值 会被计算出来为止。 你可以尝试去形象化 reduce 如下面的等同的例子: reduce(func, [1, 2, 3]) = func(func(1, 2), 3) 有些人认为 reduce()合适的函数式使用每次只需要仅需要一个元素。在上面一开始的迭代中, 我们拿了两个元素因为我们没有从先前的值(因为我们没有任何先前的值)中获得的一个“结果”。 这就是可选初始化器出现的地方(参见下面的 init 变量)。如果给定初始化器, 那么一开始的迭代 会用初始化器和一个序列的元素来进行,接着和正常的一样进行。 如果我们想要试着用纯 python 实现 reduce(), 它可能会是这样: if init is None: # initializer? res = lseq.pop(0) # no else: res = init # yes for item in lseq: # reduce sequence res = bin_func(res, item) # apply function return res # return result 从概念上说这可能 4 个中最难的一个, 所以我们应该再次向你演示一个例子以及一个函数式 图表(见图 11-4)。reduce()的“hello world”是其一个简单加法函数的应用或在这章前面看到的 与之等价的 lamda z def mySum(x,y): return x+y z lambda x,y: x+y 给定一个列表, 我们可以简单地创建一个循环, 迭代地遍历这个列表,再将现在元素加到前 面元素的累加和上,最后当循环结束就能获得所有值的总和。 >>> def mySum(x,y): return x+y >>> allNums = range(5) # [0, 1, 2, 3, 4] >>> total = 0 Edit By Vheavens  Edit By Vheavens  图 11-4 reduce()内建函数是如何工作的。 >>> for eachNum in allNums: ... total = mySum(total, eachNum) ... >>> print 'the total is:', total the total is: 10 使用 lambda 和 reduce(),我们可以以一行代码做出相同的事情。 >>> print 'the total is:', reduce((lambda x,y: x+y), range(5)) the total is: 10 给出了上面的输入,reduce()函数运行了如下的算术操作。 ((((0 + 1) + 2) + 3) + 4) => 10 用 list 的头两个元素(0,1),调用 mySum()来得到 1,然后用现在的结果和下一个元素 2 来再 次调用 mySum(),再从这次调用中获得结果,与下面的元素 3 配对然后调用 mySum(),最终拿整个前 面的求和和 4 来调用 mySum()得到 10,10 即为最终的返回值。 11.7.3 偏函数应用 Edit By Vheavens  Edit By Vheavens  currying 的概念将函数式编程的概念和默认参数以及可变参数结合在一起。一个带 n 个参数, curried 的函数固化第一个参数为固定参数,并返回另一个带n-1 个参数函数对象,分别类似于LISP 的原始函数 car 和 cdr 的行为。Currying 能泛化成为偏函数应用(PFA), 这种函数将任意数量(顺 序)的参数的函数转化成另一个带剩余参数的函数对象。 在某种程度上,这似乎和不提供参数,就会使用默认参数情形相似。 在 PFA 的例子中, 参数 不需要调用函数的默认值,只需明确的调用集合。你可以有很多的偏函数调用,每个都能用不同的 参数传给函数,这便是不能使用默认参数的原因。 这个特征是在 python2.5 的时候被引入的,通过 functools 模块能很好的给用户调用。 简单的函数式例子 如何创建一个简单小巧的例子呢?我们来使用下两个简单的函数 add()和 mul(), 两者都来自 operator 模块。这两个函数仅仅是我们熟悉的+和*操作符的函数式接口,举例来说,add(x,y)与 x+y 一样。在我们的程序中,我们经常想要给和数字加一或者乘以 100 除了大量的,如 add(1,foo),add(1,bar),mul(100, foo), mul(100, bar)般的调用,拥 有已存在的并使函数调用简化的函数不是一件很美妙的事吗?举例来说,add1(foo), add1(bar), mul100,但是却不用去实现函数 add1()和 mul100()?哦,现在用 PFAs 你就可以这样做。你可以通 过使用 functional 模块中的 partial()函数来创建 PFA: >>> from operator import add, mul >>> from functools import partial >>> add1 = partial(add, 1) # add1(x) == add(1, x) >>> mul100 = partial(mul, 100) # mul100(x) == mul(100, x) >>> >>> add1(10) 11 >>> add1(1) 2 >>> mul100(10) 1000 >>> mul100(500) 50000 这个例子或许不能让你看到 PFAs 的威力,但是我们不得不从从某个地方开始。当调用带许多参 数的函数的时候,PFAs 是最好的方法。使用带关键字参数的 PFAs 也是较简单的, 因为能显示给出 特定的参数,要么作为 curried 参数,要么作为那些更多在运行时刻传入的变量, 并且我们不需担 心顺序。下面的一个例子来自 python 文档中关于在应用程序中使用,在这些程序中需要经常将二进 Edit By Vheavens  Edit By Vheavens  制(作为字符串)转换成为整数。 >>> baseTwo = partial(int, base=2) >>> baseTwo.__doc__ = 'Convert base 2 string to an int.' >>> baseTwo('10010') 18 这个例子使用了 int()内建函数并将 base 固定为 2 来指定二进制字符串转化。现在我们没有多 次用相同的第二参数(2)来调用int(),比如('10010', 2),相反,可以只用带一个参数的新baseTwo ()函数。接着给新的(部分)函数加入了新的文档并又一次很好地使用了“函数属性”(见上面的 11.3.4 部分),这是很好的风格。要注意的是这里需要关键字参数base 警惕关键字 如果你创建了不带 base 关键字的偏函数,比如, baseTwo- BAD = partial(int, 2),这可能 会让参数以错误的顺序传入 int(),因为固定参数的总是放在运行时刻参数的左边, 比如 baseTwoBAD(x) == int(2, x)。如果你调用它, 它会将 2 作为需要转化的数字,base 作为'10010' 来传入,接着产生一个异常: >>> baseTwoBAD = partial(int, 2) >>> baseTwoBAD('10010') Traceback (most recent call last): File "", line 1, in TypeError: an integer is required 由于关键字放置在恰当的位置, 顺序就得固定下来,因为,如你所知,关键字参数总是出现在 形参之后, 所以 baseTwo(x) == int(x, base=2). 简单 GUI 类的例子。 PFAs 也扩展到所有可调用的东西,如类和方法。一个使用 PFAs 的优秀的例子是提供了“部分 gui 模范化”。GUI 小部件通常有很多的参数,如文本,长度,最大尺寸, 背景和前景色,活动或者 非活动,等等。如果想要固定其中的一些参数, 如让所有的文本标签为蓝底白字, 你可以准确地 以 PFAs 的方式,自定义为相似对象的伪模板。 例 11.6 偏函数应用 GUI (ppfaGUI.py) 这是较有用的偏函数应用的例子,或者更准确的说,“部分类实例化” 。。。。为什么呢? 1 #!/usr/bin/env python 2 3 from functools import partial Edit By Vheavens  Edit By Vheavens  4 import Tkinter 5 6 root = Tkinter.Tk() 7 MyButton = partial(Tkinter.Button, root, 8 fg='white', bg='blue') 9 b1 = MyButton(text='Button 1') 10 b2 = MyButton(text='Button 2') 11 qb = MyButton(text='QUIT', bg='red', 12 command=root.quit) 13 b1.pack() 14 b2.pack() 15 qb.pack(fill=Tkinter.X, expand=True) 16 root.title('PFAs!') 17 root.mainloop() 在 7-8 行,我们给 Tkinter.Button 创建了"部分类实例化器”(因为那便是它的名字,而不是偏 函数),固定好父类的窗口参数然后是前景色和背景色。我们创建了两个按钮b1 和 b2 来与模板匹配, 只让文本标签唯一。quit 按钮(11-12 行)是稍微自定义过的,带有不同的背景色(红色, 覆盖了 默认的蓝色)并配置了一个回调的函数,当按钮被按下的时候,关闭窗口。(另外的的两个按钮没 有函数,当他们被按下的的时候) 没有 MyButton“模板”的话,你每次会不得不使用“完全”的语法(因为你仍然没有给全参数, 由于有大量你不传入的,含有默认]值的参数) b1 = Tkinter.Button(root, fg='white', bg='blue', text='Button 1') b2 = Tkinter.Button(root, fg='white', bg='blue', text='Button 2') qb = Tkinter.Button(root, fg='white', text='QUIT', bg='red', command=root.quit) 这就一个简单的 GUI 的截图: 当你的代码可以变得更紧凑和易读的时候,为什么要还有重复的做令人心烦的事?你能在 18 张 章找到更多关于 GUI 编程的资料, 在那我们着重描写了一个使用 PFAs 的例子。从你迄今为止看到 的内容中,可以发现,在以更函数化编程环境提供默认值方面,PFA带有模板以及“style-sheeting” 的感觉。你可以在 Python Library Reference,“What’s New in Python 2.5”文档和指定的 PEP309 Edit By Vheavens  Edit By Vheavens  里,关于 functools 模块的文档中阅读到更多关于 pfa 的资料。 11.8 变量作用域 标识符的作用域是定义为其声明在程序里的可应用范围, 或者即是我们所说的变量可见性。换 句话说,就好像在问你自己,你可以在程序里的哪些部分去访问一个制定的标识符。变量可以是局 部域或者全局域。 11.8.1 全局变量与局部变量 定义在函数内的变量有局部作用域,在一个模块中最高级别的变量有全局作用域。在编译器理 论里有名的“龙“书中,Aho, Sethi, 和 ULLman 以这种方法进行了总结。 “声明适用的程序的范围被称为了声明的作用域。在一个过程中,如果名字在过程的声明之内, 它的出现即为过程的局部变量;否则的话,出现即为非局部的“ 全局变量的一个特征是除非被删除掉,否则它们的存活到脚本运行结束,且对于所有的函数, 他们的值都是可以被访问的,然而局部变量,就像它们存放的栈,暂时地存在,仅仅只依赖于定义 它们的函数现阶段是否处于活动。当一个函数调用出现时,其局部变量就进入声明它们的作用域。 在那一刻,一个新的局部变量名为那个对象创建了,一旦函数完成,框架被释放,变量将会离开作 用域。 global_str = 'foo' def foo(): local_str = 'bar' return global_str + local_str 上面的例子中,global_str 是全局变量,而 local_str 是局部变量。foo()函数可以对全局和局 部变量进行访问,而代码的主体部分只能访问全局变量。 核心笔记:搜索标识符(aka 变量,名字,等等) 当搜索一个标识符的时候,python 先从局部作用域开始搜索。如果在局部作用域内没有找到那 个名字,那么就一定会在全局域找到这个变量否则就会被抛出 NameError 异常。 一个变量的作用域和它寄住的名字空间相关。我们会在 12 章正式介绍名字空间;对于现在只能 说子空间仅仅是将名字映射到对象的命名领域,现在使用的变量名字虚拟集合。作用域的概念和用 于找到变量的名字空间搜索顺序相关。当一个函数执行的时候,所有在局部命名空间的名字都在局 部作用域内。那就是当查找一个变量的时候,第一个被搜索的名字空间。如果没有在那找到变量的 话,那么就可能找到同名的全局变量。这些变量存储(搜索)在一个全局以及内建的名字空间,。 仅仅通过创建一个局部变量来“隐藏“或者覆盖一个全局变量是有可能的。回想一下,局部名 Edit By Vheavens  Edit By Vheavens  字空间是首先被搜索的,存在于其局部作用域。如果找到一个名字,搜索就不会继续去寻找一个全 局域的变量,所以在全局或者内建的名字空间内,可以覆盖任何匹配的名字。 同样,当使用全局变量同名的局部变量的时候要小心。如果在赋予局部变量值之前,你在函数 中(为了访问这个全局变量)使用了这样的名字,你将会得到一个异常(NAMEERROR 或者 Unbound- LocalError),而这取决于你使用的python 版本。 11.8.2. globa 语句 如果将全局变量的名字声明在一个函数体内的时候,全局变量的名字能被局部变量给覆盖掉。 这里有另外的例子,与第一个相似,但是该变量的全局和局部的特性就不是那么清晰了。 def foo(): print "\ncalling foo()..." bar = 200 print "in foo(), bar is", bar bar = 100 print "in __main__, bar is", bar foo() print "\nin __main__, bar is (still)", bar 得到如下输出: in __main__, bar is 100 calling foo()... in foo(), bar is 200 in __main__, bar is (still) 100 我们局部的 bar 将全局的 bar 推出了局部作用域。为了明确地引用一个已命名的全局变量,必 须使用 global 语句。global 的语法如下: global var1[, var2[, ... varN]]] 修改上面的例子,可以更新我们代码,这样我们便可以用全局版本的 is_this_global 而无须创 建一个新的局部变量。 >>> is_this_global = 'xyz' >>> def foo(): ... global is_this_global ... this_is_local = 'abc' ... is_this_global = 'def' Edit By Vheavens  Edit By Vheavens  ... print this_is_local + is_this_global ... >>> foo() abcdef >>> print is_this_global def 11.8.3.作用域的数字 python 从句法上支持多个函数嵌套级别,就如在 python2.1 中的,匹配静态嵌套的作用域。然 而,在 2.1 至前的版本中,最多为两个作用域:一个函数的局部作用域和全局作用域。虽然存在多 个函数的嵌涛,但你不能访问超过两个作用域。 def foo(): m = 3 def bar(): n = 4 print m + n print m bar() 虽然这代码在今天能完美的运行.... >>> foo() 3 7 . . .在 python2.1 之前执行它将会产生错误。 >>> foo() Traceback (innermost last): File "", line 1, in ? File "", line 7, in foo File "", line 5, in bar NameError: m 在函数 bar()内访问 foo()的局部变量 m 是非法的,因为m 是声明为 foo()的局部变量。从bar() 中可访问唯一的作用域为局部作用域和全局作用域。foo()的局部作用域没有包含在上面两个作用域 的列表中。注意'print m'语句的输出成功了,而而对 bar()的函数调用却失败了。幸运的是,由于 python 的现有嵌套作用语规则,今天就不存在这个问题了。 Edit By Vheavens  Edit By Vheavens  11.8.4 闭包 由于 python 的静态嵌套域,如我们早先看到的,定义内部函数变得很有用处。在下面的部分中, 我们将着重讨论作用域和 lambda,但是在 python2.1 之前,当作用域规改则变为今天这样之前,内 部函数也会遭受到相同的问题。如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的 变量进行引用,那么内部函数就被认为是 closure。定义在外部函数内的但由内部函数引用或者使用 的变量被称为自由变量。closures 在函数式编程中是一个重要的概念,Scheme 和 Haskell 便是函数 式编程中两种。Closures 从句法上看很简单(和内部函数一样简单)但是仍然很有威力。 闭包将内部函数自己的代码和作用域以及外部函数的作用结合起来。闭包的词法变量不属于全 局名字空间域或者局部的--而属于其他的名字空间,带着“流浪"的作用域。(注意这不同于对象因 为那些变量是存活在一个对象的名字空间但是闭包变量存活在一个函数的名字空间和作用域)那么 为什么你会想要用 closues? Closurs 对于安装计算,隐藏状态,以及在函数对象和作用域中随意地切换是很有用的。closurs 在 GUI 或者在很多 API 支持回调函数的事件驱动编程中是很有些用处的。以绝对相同的方式,应用 于获取数据库行和处理数据。回调就是函数。闭包也是函数,但是他们能携带一些额外的作用域。 它们仅仅是带了额外特征的函数……另外的作用域。 你可能会觉得闭包的使用和这章先前介绍的偏函数应用非常的相似,但是与闭包的使用相比, PFA 更像是 currying, 因为闭包和函数调用没多少相关,而是关于使用定义在其他作用域的变量。 简单的闭包的例子 下面是使用闭包简单的例子。我们会模拟一个计数器,同样也通过将整数包裹为一个列表的单 一元素来模拟使整数易变。 def counter(start_at=0): count = [start_at] def incr(): count[0] += 1 return count[0] return incr counter()做的唯一一件事就是接受一个初始化的的值来开始计数,并将该值赋给列表 count 唯 一一个成员。然后定义一个 incr()的内部函数。通过在内部使用变量 count,我们创建了一个闭包 因为它现在携带了整个 counter()作用域。incr()增加了正在运行的 count 然后返回它。然后最后的 魔法就是 counter()返回一个 incr,一个(可调用的)函数对象。如我们交互地运行这个函数,我 们将得到如下的输出---------注意这看起来和实例化一个 counter 对象并执行这个实例有多么相似: >>> count = counter(5) Edit By Vheavens  Edit By Vheavens  >>> print count() 6 >>> print count() 7 >>> count2 = counter(100) >>> print count2() 101 >>> print count() 8 有点不同的是我们能够做些原来需要我们写一个类做的事,并且不仅仅是要写,而且必需覆盖 掉这个类的__call__()特别方法来使他的实例可调用。这里我们能够使用一对函数来做这事。 现在,在很多情况下,类是最适合使用的。闭包更适合需要一个必需有自己的作用域的回调函 数情况,尤其是回调函数是很小巧而且简单的,通常也很聪明。跟平常一样,如果你使用了闭包, 对你的代码进行注释或者用文档字符串来解释你正做的事是很不错的主意 追踪闭包词法的变量 下面两个部分包含了给高级读者的材料……如果你愿意的话,你可以跳过去。我们将讨论如何 能使用函数的 func_closure 属性来追踪自由变量。这里有个显示追踪的代码片断。如果我们运行 这段代码,将得到如下输入: f2 closure vars: [''] f3 closure vars: ['', ''] 例子 11.7 追踪闭包变量(closureVars.py) 这个例子说明了如何能通过使用函数的 func_closure 属性来追踪闭包变量 1 #!/usr/bin/env python 2 3 output = '' Edit By Vheavens  Edit By Vheavens  4 w = x = y = z = 1 5 6 def f1(): 7 x = y = z = 2 8 9 def f2(): 10 y = z = 3 11 12 def f3(): 13 z = 4 14 print output % ('w', id(w), w) 15 print output % ('x', id(x), x) 16 print output % ('y', id(y), y) 17 print output % ('z', id(z), z) 18 19 clo = f3.func_closure 20 if clo: 21 print "f3 closure vars:", [str(c) for c in clo] 22 else: 23 print "no f3 closure vars" 24 f3() 25 26 clo = f2.func_closure 27 if clo: 28 print "f2 closure vars:", [str(c) for c in clo] 29 else: 30 print "no f2 closure vars" 31 f2() 32 33 clo = f1.func_closure 34 if clo: 35 print "f1 closure vars:", [str(c) for c in clo] 36 else: 37 print "no f1 closure vars" 38 f1() 逐行解释 Lines 1–4 这段脚本由创建模板来输出一个变量开始:它的名字,ID,以及值,然后设置变量 w,x,y 和 z。 Edit By Vheavens  Edit By Vheavens  我们定义了模板,这样便不需要多次拷贝相同输出格式的字符串 Lines 6–9, 26–31 f1()函数的定义包括创建一个局部变量 x,y 和 z,以及一个内部函数 f2()的定义。(注意所有的 局部变量遮蔽或者隐藏了对他们同名的全局变量的访问)。如果f2()使用了任何的定义在 f1()作用 域的变量,比如说,非全局的和非f2()的局部域的,那么它们便是自由变量,将会被f1.func_closure 追踪到。 Lines 9–10, 19–24 这几行实际上是对 f1()的拷贝,对 f2()做相同的事,定义了局部变量y和z,以及对一个内部 函数 f3().此外,这里的局部变量会遮蔽全局以及那些在中间局部化作用域的变量,比如,f1()的。 如果对于 f3()有任何的自由变量,他们会在这里显示出来。 毫无疑问,你会注意到对自由变量的引用是存储在单元对象里,或者简单的说,单元。这些东 西是什么呢?单元是在作用域结束后使自由变量的引用存活的一种基础方法。 举例来说,我们假设函数 f3()已经被传入到其他一些函数,这样便可在稍后,甚至是 f2()完成 之后,调用它。你不想要让 f2()的栈出现,因为即使我们仅仅在乎 f3()使用的自由变量,栈也会让 所有的 f2()'s 的变量保持存活。单元维持住自由变量以便 f2()的剩余部分能被释放掉。 Lines 12–17 这个部分描绘了 f3()的定义,创建一个局部的变量 z。接着显示 w,x,y,z,这 4 个变量从最内 部作用域逐步向外的追踪到的。在 f3(), f2(), 或者 f1()中都是找不到变量w 的,所以,这是个全 局变量。在f3()或者 f2()中,找不到变量x,所以来自f1()的闭包变量。相似地,y是一个来自 f2() 的闭包变量。最后,z 是 f3()的局部变量。 Lines 33–38 main()中剩余的部分尝试去显示 f1()的闭包变量,但是什么都不会发生因为在全局域和 f1()的 作用域之间没有任何的作用域---没有 f1()可以借用的作用域,因此不会创建闭包---所以第34 行的 条件表达式永远不会求得 True。这里的这段代码仅仅是有修饰的目的。 *高级闭包和装饰器的例子 回到 11.3.6 部分,我们看到了一个使用闭包和装饰器的简单例子,deco.py。接下来就是稍微 高级点的例子,来给你演示闭包的真正的威力。应用程序“logs"函数调用。用户选择是要在函数调 用之前或者之后,把函数调用写入日志。如果选择贴日志,执行时间也会显示出来。 例子 11.8 用闭包将函数调用写入日至。 Edit By Vheavens  Edit By Vheavens  这个例子演示了带参数的装饰器,该参数最终决定哪一个闭包会被用的。这也是闭包的威力的 特征。 1 #!/usr/bin/env python 2 3 from time import time 4 5 def logged(when): 6 def log(f, *args, **kargs): 7 print '''Called: 8 function: %s 9 args: %r 10 kargs: %r''' % (f, args, kargs) 11 12 def pre_logged(f): 13 def wrapper(*args, **kargs): 14 log(f, *args, **kargs) 15 return f(*args, **kargs) 16 return wrapper 17 18 def post_logged(f): 19 def wrapper(*args, **kargs): 20 now = time() 21 try: 22 return f(*args, **kargs) 23 finally: 24 log(f, *args, **kargs) 25 print "time delta: %s" % (time()-now) 26 return wrapper 27 28 try: 29 return {"pre": pre_logged, 30 "post": post_logged}[when] 31 except KeyError, e: 32 raise ValueError(e), 'must be "pre" or "post"' 33 34 @logged("post") 35 def hello(name): 36 print "Hello,", name 37 Edit By Vheavens  Edit By Vheavens  38 hello("World!") 如果执行这个脚本,你将会得到和下面相似的输出: $ funcLog.py Hello, World! Called: function: args: ('World!',) kargs: {} time delta: 0.000471115112305 逐行解释 Lines 5–10, 28–32 这段代码描绘了 logged()函数的核心部分,其职责就是获得关于何时函数调用应该被写入日志 的用户请求。它应该在目标函数被调用前还是之后呢?logged()有 3 个在它的定义体之内的助手内 部函数:log(),pre_logged()以及 post_logged()。log()是实际上做日志写入的函数。它仅仅是显 示标准输出函数的名字和参数。如果你愿意在“真实的世界中”使用该函数的话,你很有可能会把 输出写到一个文件,数据库,或者标准错误(sys.stderr)。logged()在 28-32 行的最后的部分实际 上是函数中非函数声明的最开始的代码。读取用户的选择然后返回*logged()函数中的一个便能用 目标函调用并包裹它。 Lines 12–26 pre_logged()和 post_logged()都会包装目标函数然后根据它的名字写入日志,比如,当目标函 数已经执行之后,post_loggeed()会将函数调用写入日志,而 pre_logged()则是在执行之前。 根据用户的选择,pre_logged()和 post_logged()其中之一会被返回。当这个装饰器被调用的时 候,首先对装饰器和其参数进行求值,比如 logged(什么时候)。然后返回的函数对象作为目标的函 数的参数进行调用,比如,pre_logged(f)或者 post_logged(f). 两个*logged()函数都包括了一个名为 wrapper()的闭包。当合适将其写入日志的时候,它便会 调用目标函数。这个函数返回了包裹好的函数对象,该对象随后将被重新赋值给原始的目标函数标 识符。 Lines 34–38 这段脚本的主要部分简单地装饰了 hello()函数并将用修改过的函数对象一起执行它。当你在 38 行调用 hello()的时候,它和你在 35 行创建的函数对象已经不是一回事了。34 行的装饰器用特殊 的装饰将原始函数对象进行了包裹并返回这个包裹后的 hello()版本。 11.8.5 作用域和 lambda Edit By Vheavens  Edit By Vheavens  python 的 lambda 匿名函数遵循和标准函数一样的作用域规则。一个 lambda 表达式定义了新的 作用域,就像函数定义,所以这个作用域除了局部 lambda/函数,对于程序其他部分,该作用域都是 不能对进行访问的。 那些声明为函数局部变量的 lambda 表达式在这个函数体内是可以访问的;然而,在 lambda 语 句中的表达式有和函数相同的作用域。你也可以认为函数和一个 lambda 表达式是同胞。 x = 10 def foo(): y = 5 bar = lambda :x+y print bar() 我们现在知道这段代码能很好的运行。 >>> foo() 15 .....然而,我们必须在回顾下过去,去看下原来的 python 版本中让代码运行必需的,一种极 其普遍的做法。在2.1 之前,我们将会得到一个错误,如同你在下面看到的一样,因为函数和lambda 都可访问全局变量,但两者都不能访问彼此的局部作用域。 >>> foo() Traceback (innermost last): File "", line 1, in ? File "", line 4, in foo File "", line 3, in NameError: y 在上面的例子中,虽然 lambda 表达式在 foo()的局部作用域中创建,但他仅仅只能访问两个作 用域:它自己的局部作用域和全局的作用域(同样见 Section 11.8.3).解决的方法是加入一个变量 作为默认参数,这样我们便能从外面的局部作用域传递一个变量到内部。在我们上面的例子中,我 们将 lambda 的那一行修改成这样: bar = lambda y=y: x+y 由于这个改变,程序能运行了。外部 y 的值会作为一个参数传入,成为局部的 y(lambda 函数 的局部变量)。你可以在所有你遇到的python 代码中看到这种普遍的做法;然而,这不表明存在改 变外部 y 值的可能性,比如: x = 10 Edit By Vheavens  Edit By Vheavens  def foo(): y = 5 bar = lambda y=y: x+y print bar() y = 8 print bar() 输出“完全错误“ >>> foo() 15 15 原因是外部 y 的值被传入并在 lambda 中“设置“,所以虽然其值在稍后改变了,但是 lambda 的定义没有变。那时唯一替代的方案就是在 lambda 表达式中加入对函数局部变量 y 进行引用的局部 变量 z。 x = 10 def foo(): y = 5 bar = lambda z:x+z print bar(y) y = 8 print bar(y) 为了获得正确的输出所有的一切都是必需的: >>> foo() 15 18 这同样也不可取因为现在所有调用 bar()的地方都必需改为传入一个变量。从 python2.1 开始, 在没有任何修改的情况下整个程序都完美的运行。 x = 10 def foo(): y = 5 bar = lambda :x+y print bar(y) y = 8 Edit By Vheavens  Edit By Vheavens  print bar(y) >>> foo() 15 18 正确的静态嵌套域(最后)被加入到 python 中,你会不高兴吗?许多老前辈一定不会。你可以 在 pep227 中阅读到更多关于这个重要改变的信息。 11.8.6 变量作用域和名字空间。 从我们在这章的学习中,我们可以看见任何时候,总有一个或者两个活动的作用域---不多,不 少。我们要么在只能访问全局作用域的模块的最高级,要么在一个我们能访问函数局部作用域和全 局作用域的函数体内执行。名字空间是怎么和作用域关联的呢? 从 11.8.1 小节的核心笔记中,我们也可以发现,在任何给定的时间,存在两个或者三个的活动 的名字空间。从函数内部,局部作用域包围了局部名字空间,第一个搜寻名字的地方。如果名字存 在的话,那么将跳过检查全局作用域(全局和内建的名字空间) 我们现在将给出例子 11.9,一个到处混合了作用域的脚本。我们将确定此程序输出作为练习留 给读者。 例子 11.9 变量作用域(scope.py) 局部变量隐藏了全局变量,正如在这个变量作用程序中显示的。程序的输出会是什么呢?(以 及为什么) 1 #!/usr/bin/env python 2 j, k = 1, 2 3 4 def proc1(): 5 6 j, k = 3, 4 7 print "j == %d and k == %d" % (j, k) 8 k = 5 9 10 def proc2(): 11 Edit By Vheavens  Edit By Vheavens  12 j = 6 13 proc1() 14 print "j == %d and k == %d" % (j, k) 15 16 17 k = 7 18 proc1() 19 print "j == %d and k == %d" % (j, k) 20 21 j = 8 22 proc2() 23 print "j == %d and k == %d" % (j, k) 12.3.1 小节有更多关于名字空间和变量作用域的信息。 11.9 *递归 如果函数包含了对其自身的调用,该函数就是递归的。根据 Aho, Sethi, 和Ullman, ”[a] 如 果一个新的调用能在相同过程中较早的调用结束之前开始,那么该过程就是递归的“ 递归广泛地应用于语言识别和使用递归函数的数学应用中。在本文的早先部分,我们第一次看 到了我们定义的阶乘函数 N! ? factorial(N) ? 1 * 2 * 3 ... * N 我们可以用这种方式来看阶乘: factorial(N) = N! = N * (N-1)! = N * (N-1) * (N-2)! : = N * (N-1) * (N-2) ... * 3 * 2 * 1 我们现在可以看到阶乘是递归的,因为 factorial(N) = N* factorial(N-1).换句话说,为了获 得 factorial(N)的值,需要计算 factorial(N-1).而且,为了找到 factorial(N-1),需要计算 factorial(N-2)等等。我们现在给出阶乘函数的递归版本。 def factorial(n): if n == 0 or n == 1: # 0! = 1! = 1 return 1 Edit By Vheavens  Edit By Vheavens  else: return (n * factorial(n-1)) 11.10 生成器 早先在第 8 章,我们讨论了迭代器背后的有效性以及它们如何给非序列对象一个像序列的迭代 器接口。这很容易明白因为他们仅仅只有一个方法,用于调用获得下个元素的 next() 然而,除非你实现了一个迭代器的类,迭代器真正的并没有那么“聪明“。难道调用函数还没 有强大到在迭代中以某种方式生成下一个值并且返回和 next()调用一样简单的东西?那就是生成器 的动机之一。 生成器的另外一个方面甚至更加强力.....协同程序的概念。协同程序是可以运行的独立函数调 用,可以暂停或者挂起,并从程序离开的地方继续或者重新开始。在有调用者和(被调用的)协同 程序也有通信。举例来说,当协同程序暂停的时候,我们能从其中获得一个中间的返回值,当调用 回到程序中时,能够传入额外或者改变了的参数,但仍能够从我们上次离开的地方继续,并且所有 状态完整。挂起返回出中间值并多次继续的协同程序被称为生成器,那就是 python 的生成器真正在 做的事。在 2.2 的时候,生成器被加入到 python 中接着在 2.3 中成为标准(见 PEP255),虽然之前 足够强大,但是在 Python2.5 的时候,得到了显著的提高(见 pep342)。这些提升让生成器更加接 近一个完全的协同程序,因为允许值(和异常)能传回到一个继续的函数中。同样地,当等待一个 生成器的时候,生成器现在能返回控制。在调用的生成器能挂起(返回一个结果)之前,调用生成 器返回一个结果而不是阻塞等待那个结果返回。让我们更进一步观察生成器自顶向下的启动. 什么是 python 式的生成器?从句法上讲,生成器是一个带 yield 语句的函数。一个函数或者子 程序只返回一次,但一个生成器能暂停执行并返回一个中间的结果----那就是yield 语句的功能, 返 回一个值给调用者并暂停执行。当生成器的 next()方法被调用的时候,它会准确地从离开地方继续 (当它返回[一个值以及]控制给调用者时) 当在 2.2 生成器被加入的时候,因为它引入了一个新的关键字,yield,为了向下兼容,你需要 从_future_模块中导入 generators 来使用生成器。从 2.3 开始,当生成器成为标准的时候,这就不 再是必需的了。 11.10.1.简单的生成器特性 与迭代器相似,生成器以另外的方式来运作:当到达一个真正的返回或者函数结束没有更多的 值返回(当调用 next()),一个 StopIteration 异常就会抛出。这里有个例子,简单的生成器: def simpleGen(): yield 1 yield '2 --> punch!' Edit By Vheavens  Edit By Vheavens  现在我们有自己的生成器函数,让我们调用他来获得和保存一个生成器对象(以便我们能调用它 的 next()方法从这个对象中获得连续的中间值) >>> myG = simpleGen() >>> myG.next() 1 >>> myG.next() '2 --> punch!' >>> myG.next() Traceback (most recent call last): File "", line 1, in ? myG.next() StopIteration 由于 python 的 for 循环有 next()调用和对 StopIteration 的处理,使用一个for 循环而不是手 动迭代穿过一个生成器(或者那种事物的迭代器)总是要简洁漂亮得多。 >>> for eachItem in simpleGen(): ... print eachItem ... 1 '2 --> punch!' 当然这是个挺傻的例子:为什么不对这使用真正的迭代器呢?许多动机源自能够迭代穿越序列, 而这需要函数威力而不是已经在某个序列中静态对象。 在接下来的例子中,我们将要创建一个带序列并从那个序列中返回一个随机元素的随机迭代器: from random import randint def randGen(aList): while len(aList) > 0: yield aList.pop(randint(0, len(aList))) 不同点在于每个返回的元素将从那个队列中消失,像一个 list.pop()和 random.choice()的结 合的归类。 >>> for item in randGen(['rock', 'paper', 'scissors']): ... print item ... scissors rock Edit By Vheavens  Edit By Vheavens  paper 在接下来的几章中,当我们谈到面向对象编程的时候,将看见这个生成器较简单(和无限)的 版本作为类的迭代器。在几章前的 8.12 小节中,我们讨论了生成器表达式的语法。使用这个语法返 回的对象是个生成器,但只以一个简单的形式,并允许使用过分简单化的列表解析的语法。 这些简单的例子应该让你有点明白生成器是如何工作的,但你或许会问。"在我的应用中,我可 以在哪使用生成器?“或许,你会问“最适合使用这些个强大的构建的地方在哪?“ 使用生成器最好的地方就是当你正迭代穿越一个巨大的数据集合,而重复迭代这个数据集合是 一个很麻烦的事,比如一个巨大的磁盘文件,或者一个复杂的数据库查询。对于每行的数据,你希 望执行非元素的操作以及处理,但当正指向和迭代过它的时候,你“不想失去你的地盘“。 你想要抓取一块数据,比如,将它返回给调用者来处理以及可能的对(另外一个)数据库的插 入,接着你想要运行一次 next()来获得下一块的数据,等等。状态在挂起和再继续的过程中是保留 了的,所以你会觉得很舒服有一个安全的处理数据的环境。没有生成器的话,你的程序代码很有可 能会有很长的函数,里面有一个很长的循环。当然,这仅仅是因为一个语言这样的特征不意味着你 需要用它。如果在你程序里没有明显适合的话,那就别增加多余的复杂性!当你遇到合适的情况时, 你便会知道什么时候生成器正是要使用的东西。 11.10.2 加强的生成器特性 在 python2.5 中,一些加强特性加入到生成器中,所以除了 next()来获得下个生成的值,用户 可以将值回送给生成器[send()],在生成器中抛出异常,以及要求生成器退出[close()] 由于双向的动作涉及到叫做 send()的代码来向生成器发送值(以及生成器返回的值发送回来), 现在 yield 语句必须是一个表达式,因为当回到生成器中继续执行的时候,你或许正在接收一个进 入的对象。下面是一个展示了这些特性的,简单的例子。我们用简单的闭包例子,counter: def counter(start_at=0): count = start_at while True: val = (yield count) if val is not None: count = val else: count += 1 生成器带有一个初始化的值,对每次对生成器[next()]调用以 1 累加计数。用户已可以选择重 置这个值,如果他们非常想要用新的值来调用 send()不是调用 next()。这个生成器是永远运行的, Edit By Vheavens  Edit By Vheavens  所以如果你想要终结它,调用 close()方法。如果我们交互的运行这段代码,会得到如下输出: >>> count = counter(5) >>> count.next() 5 >>> count.next() 6 >>> count.send(9) 9 >>> count.next() 10 >>> count.close() >>> count.next() Traceback (most recent call last): File "", line 1, in StopIteration 你可以在 PEP 的 255 和 342 中,以及给读者介绍 python2.2 中新特性的 linux 期刊文章中阅读 到更多关于生成器的资料: http://www.linuxjournal.com/article/5597 11.11 练习 11–1.参数。比较下面 3 个函数: def countToFour1(): for eachNum in range(5): print eachNum, def countToFour2(n): for eachNum in range(n, 5): print eachNum, def countToFour3(n=1): for eachNum in range(n, 5): print eachNum, 给定如下的输入直到程序输出,你认为什么会发生?向下表 11.2 填入输出。如果你认为给定的 输入会发生错误的话填入“ERROR"或者如果没有输出的话填入“NONE" Edit By Vheavens  Edit By Vheavens  11-2.函数。结合你对练习 5-2 的解,以便你创建一个带相同对数字并同时返回一它们之和以及 产物的结合函数。 表 11.2 问题 11-1 的输出图 11-3 函数。在这个练习中,我们将实现 max()和 min()内建函数。 (a) 写分别带两个元素返回一个较大和较小元素,简单的 max2()核 min2()函数。他们应该可以 用任意的 python 对象运作。举例来说,max2(4,8)和 min2(4,8)会各自每次返回8和4。 (b) 创建使用了在 a 部分中的解来重构 max()和 min()的新函数 my_max()和 my_min().这些函 数分别返回非空队列中一个最大和最小值。它们也能带一个参数集合作为输入。用数字和字符串来 测试你的解。 11–4. 返回值。给你在 5-13 的解创建一个补充函数。创建一个带以分为单位的总时间以及 返回一个以小时和分为单位的等价的总时间。 11–5. 默认参数。更新你在练习 5-7 中创建的销售税脚本以便让销售税率不再是函数输入的必要之物。 创建使用你地方税率的默认参数如果在调用的时候没有值传入。 11–6. 变长参数。下一个称为 printf()的函数。有一个值参数,格式字符串。剩下的就是根 据格式化字符串上的值,要显示在标准输出上的可变参数,格式化字符串中的值允许特别的字符串 格式操作指示符,如%d, %f, etc。提示:解是很琐碎的----无需实现字符串操作符功能性,但你需 要显示用字符串格式化操作(%) 11–7. 用 map() 进 行 函 数 式 编 程 。 给 定 一 对 同 一 大 小 的 列 表 , 如 [1 , 2 , 3] 和 ['abc','def','ghi',....],将两个标归并为一个由每个列表元素组成的元组的单一的表,以使我 们的结果看起来像这样:{[(1, 'abc'), (2, 'def'), (3, 'ghi'), ...}.(虽然这问题在本质上和 第六章的一个问题相似,那时两个解没有直接的联系)然后创建用 zip 内建函数创建另一个解。 Edit By Vheavens  Edit By Vheavens  11–8. 用 filer()进行函数式编程.使用练习 5-4 你给出的代码来决定闰年。更新你的代码一 边他成为一个函数如果你还没有那么做的话。然后写一段代码来给出一个年份的列表并返回一个只 有闰年的列表。然后将它转化为用列表解析。 11–9. 用 reduce()进行函数式编程。复习 11.7.2 部分,阐述如何用 reduce()数字集合的累 加的代码。修改它,创建一个叫 average()的函数来计算每个数字集合的简单的平均值。 11–10.用 filter()进行函数式编程。在 unix 文件系统中,在每个文件夹或者目录中都有两个 特别的文件:'.'表示现在的目录,'..'表示父目录。给出上面的知识,看下 os.listdir()函数的文 档并描述这段代码做了什么: files = filter(lambda x: x and x[0] != '.', os. listdir(folder)) 11–11.用 map()进行函数式编程。写一个使用文件名以及通过除去每行中所有排头和最尾的空 白来“清洁“文件。在原始文件中读取然后写入一个新的文件,创建一个新的或者覆盖掉已存在的。 给你的用户一个选择来决定执行哪一个。将你的解转换成使用列表解析。 11–12. 传递函数。给在这章中描述的 testit()函数写一个姊妹函数。timeit()会带一个函数 对象(和参数一起)以及计算出用了多少时间来执行这个函数,而不是测试执行时的错误。返回下 面的状态:函数返回值,消耗的时间。你可以用 time.clock()或者 time.time(),无论哪一个给你 提供了较高的精度。(一般的共识是在POSIX 上用 time.time(),在win32 系统上用 time.clock()) 注意:timeit()函数与 timeit 模块不相关(在 python2.3 中引入) 11–13.使用 reduce()进行函数式编程以及递归。在第 8 张中,我们看到 N 的阶乘或者 N!作为 从1到N所有数字的乘积。 (a) 用一分钟写一个带 x,y 并返回他们乘积的名为 mult(x,y)的简单小巧的函数。 (b)用你在 a 中创建 mult()函数以及 reduce 来计算阶乘。 (c)彻底抛弃掉 mult()的使用,用 lamda 表达式替代。 (d)在这章中,我们描绘了一个递归解决方案来找到N!用你在上面问题中完成的 timeit()函数, 并给三个版本阶乘函数计时(迭代的,reduce()以及递归) 11–14. 递归。我们也来看下在第八章中的 Fibonacci 数字。重写你先前计算 Fibonacci 数字 的解(练习 8-9)以便你可以使用递归。 11–15.递归。从写练习 6-5 的解,用递归向后打印一个字符串。用递归向前以及向后打印一个 字符串。 11–16. 更新 easyMath.py。这个脚本,如例子 11.1 描绘的那样,以入门程序来帮助年轻人强 化他们的数学技能。通过加入乘法作为可支持的操作来更进一步提升这个程序。额外的加分:也加 入除法;这比较难做些因为你要找到有效的整数除数。幸运的是,已经有代码来确定分子比分母大, 所以不需要支持分数。 Edit By Vheavens  Edit By Vheavens  11–17.定义 (a) 描述偏函数应用和 currying 之间的区别。 (b) 偏函数应用和闭包之间有什么区别? (c) 最后,迭代器和生成器是怎么区别开的? 11–18. 同步化函数调用。复习下第六章中当引入浅拷贝和深拷贝的时候,提到的丈夫和妻子 情形(6.20 小结)。他们共用了一个普通账户,同时对他们银行账户访问时会发生不利影响。 创建一个程序,让调用改变账户收支的函数必需同步。换句话说,在任意给定时刻只能有个 一进程或者线程来执行函数。一开始你试着用文件,但是一个真正的解决方法是用装饰器和在 threading 或者 mutex 模块中的同步指令。你看看第 17 张来获得更多的灵感。 Edit By Vheavens  Edit By Vheavens  模块 本章主题 z 什么是模块? z 模块和文件 z 命名空间 z 导入模块 z 导入模块属性 z 模块内建函数包 z 模块的其他特性 Edit By Vheavens  Edit By Vheavens  本章将集中介绍 Python 模块和如何把数据从模块中导入到编程环境中。同时也会涉及包的相 关概念。模块是用来组织 Python 代码的方法, 而包则是用来组织模块的。本章最后还会讨论一些 与模块有关的其他方面的问题。 12.1 什么是模块 模块支持从逻辑上组织 Python 代码。 当代码量变得相当大的时候, 我们最好把代码分成一 些有组织的代码段,前提是保证它们的彼此交互。 这些代码片段相互间有一定的联系, 可能是一个 包含数据成员和方法的类, 也可能是一组相关但彼此独立的操作函数。 这些代码段是共享的,所以 Python 允许 "调入" 一个模块, 允许使用其他模块的属性来利用之前的工作成果, 实现代码重用. 这个把其他模块中属性附加到你的模块中的操作叫做导入(import) 。那些自我包含并且有组织的代 码片断就是模块( module )。 12.2 模块和文件 如果说模块是按照逻辑来组织 Python 代码的方法, 那么文件便是物理层上组织模块的方法。 因此, 一个文件被看作是一个独立模块, 一个模块也可以被看作是一个文件。 模块的文件名就是模 块的名字加上扩展名 .py 。这里我们需要讨论一些关于模块文件结构的问题。 与其它可以导入类 (class)的语言不同,在 Python 中你导入的是模块或模块属性。 12.2.1 模块名称空间 Edit By Vheavens  Edit By Vheavens  本章的后面会详细的讨论名称空间, 但从基本概念来说, 一个名称空间就是一个从名称到对象 的关系映射集合。 我们已经明确地知道, 模块名称是它们的属性名称中的一个重要部分。 例如 string 模块中的 atoi() 函数就是 string.atoi() 。给定一个模块名之后, 只可能有一个模块被 导入到 Python 解释器中, 所以在不同模块间不会出现名称交叉现象; 所以每个模块都定义了它自 己的唯一的名称空间。 如果我在我自己的模块 mymodule 里创建了一个 atoi() 函数, 那么它的名 字应该是 mymodule.atoi() 。 所以即使属性之间有名称冲突, 但它们的完整授权名称(fully qualified name)——通过句点属性标识指定了各自的名称空间 - 防止了名称冲突的发生。 12.2.2 搜索路径和路径搜索 模块的导入需要一个叫做"路径搜索"的过程。 即在文件系统"预定义区域"中查找 mymodule.py 文件(如果你导入 mymodule 的话)。 这些预定义区域只不过是你的 Python 搜索路径的集合。路径 搜索和搜索路径是两个不同的概念, 前者是指查找某个文件的操作, 后者是去查找一组目录。 有时 候导入模块操作会失败: >>> import xxx Traceback (innermost last): File "", line 1, in ? ImportError: No module named xxx 发生这样的错误时, 解释器会告诉你它无法访问请求的模块, 可能的原因是模块不在搜索路 径里, 从而导致了路径搜索的失败。 默认搜索路径是在编译或是安装时指定的。 它可以在一个或两个地方修改。 一个是启动 Python 的 shell 或命令行的 PYTHONPATH 环境变量。 该变量的内容是一组用冒 号分割的目录路径。 如果你想让解释器使用这个变量, 那么请确保在启动解释器或执行 Python 脚 本前设置或修改了该变量。 解释器启动之后, 也可以访问这个搜索路径, 它会被保存在 sys 模块的 sys.path 变量里。 不过它已经不是冒号分割的字符串, 而是包含每个独立路径的列表。下面是一个 Unix 机器搜索路 径的样例。切记, 搜索路径在不同系统下一般是不同的。 >>> sys.path ['', '/usr/local/lib/python2.x/', '/usr/local/lib/ python2.x/plat-sunos5', '/usr/local/lib/python2.x/ lib-tk', '/usr/local/lib/python2.x/lib-dynload', '/ usr/local/lib/Python2.x/site-packages',] 这只是个列表, 所以我们可以随时随地对它进行修改。 如果你知道你需要导入的模块是什么, 而它的路径不在搜索路径里, 那么只需要调用列表的 append() 方法即可, 就像这样: sys.path.append('/home/wesc/py/lib') Edit By Vheavens  Edit By Vheavens  修改完成后, 你就可以加载自己的模块了。 只要这个列表中的某个目录包含这个文件, 它就会 被正确导入。 当然, 这个方法是把目录追加在搜索路径的尾部。 如果你有特殊需要, 那么应该使 用列表的 insert() 方法操作 。 上面的例子里, 我们是在交互模式下修改 sys.path 的, 在脚本 程序中也完全可以达到同样的目的。这里是使用交互模式执行时遇到的错误: >>> import sys >>> import mymodule Traceback (innermost last): File "", line 1, in ? ImportError: No module named mymodule >>> >>> sys.path.append('/home/wesc/py/lib') >>> sys.path ['', '/usr/local/lib/python2.x/', '/usr/local/lib/ python2.x/plat-sunos5', '/usr/local/lib/python2.x/ lib-tk', '/usr/local/lib/python2.x/lib-dynload', '/usr/ local/lib/python2.x/site-packages’,'/home/wesc/py/lib'] >>> >>> import mymodule >>> 从另一方面看, 你可能有一个模块的很多拷贝。 这时, 解释器会使用沿搜索路径顺序找到的 第一个模块。 使用 sys.modules 可以找到当前导入了哪些模块和它们来自什么地方。 和 sys.path 不同, sys.modules 是一个字典, 使用模块名作为键( key) , 对应物理地址作为值( value )。 12.3 名称空间 名称空间是名称(标识符)到对象的映射。 向名称空间添加名称的操作过程涉及到绑定标识符到 指定对象的操作(以及给该对象的引用计数加 1 )。 《Python语言参考》(Python Language Reference) 有如下的定义: 改变一个名字的绑定叫做重新绑定, 删除一个名字叫做解除绑定。 我们在第 11 章已经介绍过在执行期间有两个或三个活动的名称空间。 这三个名称空间分别是 局部名称空间, 全局名称空间和内建名称空间, 但局部名称空间在执行期间是不断变化的, 所以我 们说"两个或三个"。 从名称空间中访问这些名字依赖于它们的加载顺序, 或是系统加载这些名称空 间的顺序。 Python 解释器首先加载内建名称空间。 它由 __builtins__ 模块中的名字构成。 随后加载执 行模块的全局名称空间, 它会在模块开始执行后变为活动名称空间。 这样我们就有了两个活动的名 称空间。 Edit By Vheavens  Edit By Vheavens  核心笔记: __builtins__ 和 __builtin__ __builtins__ 模块和 __builtin__ 模块不能混淆。 虽然它们的名字相似——尤其对于新手来 说。 __builtins__ 模块包含内建名称空间中内建名字的集合。 其中大多数(如果不是全部的话)来 自 __builtin__ 模块, 该模块包含内建函数, 异常以及其他属性。 在标准 Python 执行环境下, __builtins__ 包含 __builtin__ 的所有名字。 Python 曾经有一个限制执行模式, 允许你修改 __builtins__ , 只保留来自 __builtin__ 的一部分, 创建一个沙盒(sandbox)环境。但是, 因为 它有一定的安全缺陷, 而且修复它很困难, Python 已经不再支持限制执行模式。(如版本 2.3 ) 如果在执行期间调用了一个函数, 那么将创建出第三个名称空间, 即局部名称空间。 我们可以 通过 globals() 和 locals() 内建函数判断出某一名字属于哪个名称空间。我们将在本章后面详细 介绍这两个函数。 12.3.1 名称空间与变量作用域比较 好了, 我们已经知道了什么是名称空间, 那么它与变量作用域有什么关系呢? 它们看起来极其 相似。 事实上也确实如此。 名称空间是纯粹意义上的名字和对象间的映射关系, 而作用域还指出了从用户代码的哪些物 理位置可以访问到这些名字。 图 12 - 1 展示了名称空间和变量作用域的关系。 注意每个名称空间是一个自我包含的单元。但从作用域的观点来看, 事情是不同的. 所有局部 名称空间的名称都在局部作用范围内。局部作用范围以外的所有名称都在全局作用范围内。 还要记得在程序执行过程中, 局部名称空间和作用域会随函数调用而不断变化, 而全局名称空 间是不变的。 图 12 - 1 名称空间和变量作用域 学完这一节后, 我们建议读者在遇到名称空间的时候想想"它存在吗?", 遇到变量作用域的时 候想想"我能看见它吗?" Edit By Vheavens  Edit By Vheavens  12.3.2 名称查找, 确定作用域, 覆盖 那么确定作用域的规则是如何联系到名称空间的呢? 它所要做的就是名称查询. 访问一个属性 时, 解释器必须在三个名称空间中的一个找到它。 首先从局部名称空间开始, 如果没有找到, 解释 器将继续查找全局名称空间. 如果这也失败了, 它将在内建名称空间里查找。 如果最后的尝试也失 败了, 你会得到这样的错误: >>> foo Traceback (innermost last): File "", line 1, in ? NameError: foo 这个错误信息体现了先查找的名称空间是如何"遮蔽"其他后搜索的名称空间的。 这体现了名称 覆盖的影响。 图 12 - 1 的灰盒子展示了遮蔽效应。 例如, 局部名称空间中找到的名字会隐藏全 局或内建名称空间的对应对象。 这就相当于"覆盖"了那个全局变量。 请参阅前面章节引入的这几 行代码: def foo(): print "\ncalling foo()..." bar = 200 print "in foo(), bar is", bar bar = 100 print "in __main__, bar is", bar foo() 执行代码, 我们将得到这样的输出: in __main__, bar is 100 calling foo()... in foo(), bar is 200 foo() 函数局部名称空间里的 bar 变量覆盖了全局的 bar 变量。 虽然 bar 存在于全局名称 空间里, 但程序首先找到的是局部名称空间里的那个, 所以"覆盖"了全局的那个。 关于作用域的更 多内容请参阅第 11.8 节。 12.3.3 无限制的名称空间 Python 的一个有用的特性在于你可以在任何需要放置数据的地方获得一个名称空间。 我们已 经在前一章见到了这一特性, 你可以在任何时候给函数添加属性(使用熟悉的句点属性标识)。 Edit By Vheavens  Edit By Vheavens  def foo(): pass foo.__doc__ = 'Oops, forgot to add doc str above!' foo.version = 0.2 在本章, 我们展示了模块是如何创建名称空间的, 你也可以使用相同的方法访问它们: mymodule.foo() mymodule.version 虽然我们还没介绍面向对象编程(OOP, 第 13 章), 但我们可以看看一个简单的 "Hello World!" 例子: class MyUltimatePythonStorageDevice(object): pass bag = MyUltimatePythonStorageDevice() bag.x = 100 bag.y = 200 bag.version = 0.1 bag.completed = False 你可以把任何想要的东西放入一个名称空间里。 像这样使用一个类(实例)是很好的, 你甚至 不需要知道一些关于 OOP 的知识(注解: 类似这样的变量叫做实例属性。) 不管名字如何, 这个实 例只是被用做一个名称空间。 随着学习的深入, 你会发现 OOP 是多么地有用, 比如在运行时临时(而且重要)变量的时候! 正如在《Python 之禅》(Zen of Python)中陈述的最后一条, "名字空间是一个响亮的杰出创意—— 那就让我们多用用它们吧!"(在交互模式解释器下导入 this 模块就可以看到完整的 《Zen》 )。 12.4 导入模块 12.4.1 语句 使用 import 语句导入模块, 它的语法如下所示: import module1 import module2[ : import moduleN Edit By Vheavens  Edit By Vheavens  也可以在一行内导入多个模块, 像这样 import module1[, module2[,... moduleN]] 但是这样的代码可读性不如多行的导入语句。 而且在性能上和生成 Python 字节代码时这两种 做法没有什么不同。 所以一般情况下, 我们使用第一种格式。 核心风格: import 语句的模块顺序 我们推荐所有的模块在 Python 模块的开头部分导入。 而且最好按照这样的顺序: z Python 标准库模块 z Python 第三方模块 z 应用程序自定义模块 然后使用一个空行分割这三类模块的导入语句。 这将确保模块使用固定的习惯导入, 有助于减 少每个模块需要的 import 语句数目。 其他的提示请参考《 Python 风格指南》(Python’s Style Guide), PEP8 。 解释器执行到这条语句, 如果在搜索路径中找到了指定的模块, 就会加载它。该过程遵循作用 域原则, 如果在一个模块的顶层导入, 那么它的作用域就是全局的; 如果在函数中导入, 那么它的 作用域是局部的。 如果模块是被第一次导入, 它将被加载并执行。 12.4.2 from-import 语句 你可以在你的模块里导入指定的模块属性。 也就是把指定名称导入到当前作用域。 使用 from-import 语句可以实现我们的目的, 它的语法是: from module import name1[, name2[,... nameN]] 12.4.3 多行导入 多行导入特性是 Python 2.4 为较长的 from-import 提供的。从一个模块导入许多属性时, import 行会越来越长, 直到自动换行, 而且需要一个 \ 。下面是 PEP 328 提供的样例代码: from Tkinter import Tk, Frame, Button, Entry, Canvas, \ Text, LEFT, DISABLED, NORMAL, RIDGE, END 你可以选择使用多行的 from-import 语句: Edit By Vheavens  Edit By Vheavens  from Tkinter import Tk, Frame, Button, Entry, Canvas, Text from Tkinter import LEFT, DISABLED, NORMAL, RIDGE, END 我们不提倡使用不再流行的 from Tkinter import * 语句 (参考第 12.5.3 一节的“核心风格”)。 真正的 Python 程序员应该使用 Python 的标准分组机制(圆括号)来创建更合理的多行导入语句: 你可以在 PEP 328 找到更多关于多行导入的内容。 12.4.4 扩展的 import 语句(as) 有时候你导入的模块或是模块属性名称已经在你的程序中使用了, 或者你不想使用导入的名字。 可能是它太长不便输入什么的, 总之你不喜欢它。 这已经成为 Python 程序员的一个普遍需求: 使 用自己想要的名字替换模块的原始名称。一个普遍的解决方案是把模块赋值给一个变量: >>> import longmodulename >>> short = longmodulename >>> del longmodulename 上边的例子中, 我们没有使用 longmodulename.attribute , 而是使用 short.attribute 来 访问相同的对象。 ( from-imoort 语句也可以解决类似的问题, 参见下面的例子。) 不过在程序 里一遍又一遍做这样的操作是很无聊的。 使用扩展的 import , 你就可以在导入的同时指定局部 绑定名称。 类似这样... import Tkinter from cgi import FieldStorage . . . 可以替换为 . . . import Tkinter as tk from cgi import FieldStorage as form Python 2.0 加入了这个特性。 不过那时 "as" 还不是一个关键字; Python 2.6 正式把它列为 一个关键字。 更多关于扩展导入语句的内容请参阅《 Python 语言参考》和 PEP 221。 12.5 模块导入的特性 12.5.1 载入时执行模块 加载模块会导致这个模块被"执行"。 也就是被导入模块的顶层代码将直接被执行。 这通常包 括设定全局变量以及类和函数的声明。 如果有检查 __name__ 的操作, 那么它也会被执行。 Edit By Vheavens  Edit By Vheavens  当然, 这样的执行可能不是我们想要的结果。 你应该把尽可能多的代码封装到函数。明确地说, 只把函数和模块定义放入模块的顶层是良好的模块编程习惯。 更多信息请参阅第 14.1.1 节以及相应的“核心笔记”。 Python 加入的一个新特性允许你把一个已经安装的模块作为脚本执行。 (当然, 执行你自己的 脚本很简单 [$ foo.py], 但执行一个标准库或是第三方包中的模块需要一定的技巧。) 你可以在第 14.4.3 一节了解更多。 12.5.2 导入(import )和加载(load) 一个模块只被加载一次, 无论它被导入多少次。 这可以阻止多重导入时代码被多次执行。 例 如你的模块导入了 sys 模块, 而你要导入的其他 5 个模块也导入了它, 那么每次都加载 sys (或 是其他模块)不是明智之举! 所以, 加载只在第一次导入时发生。 12.5.3 导入到当前名称空间的名称 调用 from-import 可以把名字导入当前的名称空间里去, 这意味着你不需要使用属性/句点 属性标识来访问模块的标识符。 例如, 你需要访问模块 module 中的 var 名字是这样被导入的: from module import var 我们使用单个的 var 就可以访问它自身。 把 var 导入到名称空间后就再没必要引用模块了。 当然, 你也可以把指定模块的所有名称导入到当前名称空间里: from module import * 核心风格: 限制使用 "from module import *" 在实践中, 我们认为 "from module import *" 不是良好的编程风格, 因为它"污染"当前名称 空间, 而且很可能覆盖当前名称空间中现有的名字; 但如果某个模块有很多要经常访问的变量或者 模块的名字很长, 这也不失为一个方便的好办法。 我们只在两种场合下建议使用这样的方法, 一个场合是:目标模块中的属性非常多, 反复键入 模块名很不方便, 例如 Tkinter (Python/Tk) 和 NumPy (Numeric Python) 模块, 可能还有 socket 模块。另一个场合是在交互解释器下, 因为这样可以减少输入次数。 12.5.4 被导入到导入者作用域的名字 只从模块导入名字的另一个副作用是那些名字会成为局部名称空间的一部分。 这可能导致覆盖 Edit By Vheavens  Edit By Vheavens  一个已经存在的具有相同名字的对象。 而且对这些变量的改变只影响它的局部拷贝而不是所导入模 块的原始名称空间。 也就是说, 绑定只是局部的而不是整个名称空间。 这里我们提供了两个模块的代码: 一个导入者, impter.py , 一个被导入者, imptee.py 。 impter.py 使用 from-import 语句只创建了局部绑定。 ############# # imptee.py # ############# foo = 'abc' def show(): print 'foo from imptee:', foo ############# # impter.py # ############# from imptee import foo, show show() foo = 123 print 'foo from impter:', foo show() 运行这个导入者程序, 我们发现从被导入者的观点看, 它的 foo 变量没有改变, 即使 我们 在 importer.py 里修改了它。 foo from imptee: abc foo from impter: 123 foo from imptee: abc 唯一的解决办法是使用 import 和完整的标识符名称(句点属性标识)。 ############# # impter.py # ############# import imptee imptee.show() imptee.foo = 123 print 'foo from impter:', imptee.foo imptee.show() 完成相应修改后, 结果如我们所料: foo from imptee: abc foo from impter: 123 foo from imptee: 123 Edit By Vheavens  Edit By Vheavens  12.5.5 关于 __future__ 回首 Python 2.0 , 我们认识到了由于改进, 新特性, 以及当前特性增强, 某些变化会影响到 当前功能。 所以为了让 Python 程序员为新事物做好准备, Python 实现了 __future__ 指令。 使用 from-import 语句"导入"新特性, 用户可以尝试一下新特性或特性变化, 以便在特性固 定下来的时候修改程序。 它的语法是: from __future__ import new_feature 只 import __future__ 不会有任何变化,所以这是被禁止的。 (事实上这是允许的, 但它不会 如你所想的那样启用所有特性。) 你必须显示地导入指定特性。 你可以在 PEP 236 找到更多关于 __future__ 的资料。 12.5.6 警告框架 和 __future__ 指令类似, 有必要去警告用户不要使用一个即将改变或不支持的操作, 这样他 们会在新功能正式发布前采取必要措施。 这个特性是很值得讨论的, 我们这里分步讲解一下。 首先是应用程序(员)接口(Application programmers' interface , API)。 程序员应该有从 Python 程序(通过调用 warnings 模块)或是 C 中(通过 PyErr_Warn() 调用)发布警告的能力。 这个框架的另个部分是一些警告异常类的集合。 Warning 直接从 Exception 继承, 作为所有 警告的基类: UserWarning , DeprecationWarning , SyntaxWarning , 以及 RuntimeWarning 。 都 在第 10 章中有详细介绍。 另一个组件是警告过滤器, 由于过滤有多种级别和严重性, 所以警告的数量和类型应该是可控 制的。 警告过滤器不仅仅收集关于警告的信息(例如行号, 警告原因等等), 而且还控制是否忽略警 告, 是否显示——自定义的格式——或者转换为错误(生成一个异常)。 警告会有一个默认的输出显示到 sys.stderr , 不过有钩子可以改变这个行为, 例如,当运行 会引发警告的 Python 脚本时,可以记录它的输出记录到日志文件中,而不是直接显示给终端用户。 Python 还提供了一个可以操作警告过滤器的 API 。 最后, 命令行也可以控制警告过滤器。 你可以在启动 Python 解释器的时候使用 -W 选项。请 参阅 PEP 230 的文档获得你的 Python 版本的对应开关选项。 Python 2.1 第一次引入警告框架。 12.5.7 从 ZIP 文件中导入模块 Edit By Vheavens  Edit By Vheavens  在 2.3 版中, Python 加入了从 ZIP 归档文件导入模块的功能。 如果你的搜索路径中存在一 个包含 Python 模块(.py, .pyc, or .pyo 文件)的 .zip 文件, 导入时会把 ZIP 文件当作目录处 理, 在文件中搜索模块。 如果要导入的一个 ZIP 文件只包含 .py 文件, 那么 Python 不会为其添加对应的 .pyc 文件, 这意味着如果一个 ZIP 归档没有匹配的 .pyc 文件时, 导入速度会相对慢一点。 同时你也可以为 .zip 文件加入特定的(子)目录, 例如 /tmp/yolk.zip/lib 只会从 yolk 归 档的 lib/ 子目录下导入。 虽然 PEP 273 指定了这个特性, 但事实上使用了 PEP 302 提供的导入 钩子来实现它。 12.5.8 "新的"导入钩子 导入 ZIP 归档文件这一特性其实新导入钩子( import hook , PEP 302) 的 "第一个顾客"。我 们使用了"新"这个字, 因为在这之前实现自定义导入器只能是使用一些很古老的模块, 它们并不会 简化创建导入器。 另一个解决方法是覆盖 __import__() , 但这并不简单, 你需要(重新)实现整个 导入机制。 Python 2.3 引入的新导入钩子,从而简化了这个操作。 你只需要编写可调用的 import 类, 然 后通过 sys 模块"注册"(或者叫"安装")它。 你需要两个类: 一个查找器和一个载入器。 这些类的实例接受一个参数:模块或包的全名称。 查找器实例负责查找你的模块, 如果它找到, 那么它将返回一个载入器对象。查找器可以接受一个 路径用以查找子包(subpackages) 。载入器会把模块载入到内存。它负责完成创建一个 Python 模 块所需要的一切操作, 然后返回模块。 这些实例被加入到 sys.path_hooks 。 sys.path_importer_cache 只是用来保存这些实例, 这 样就只需要访问 path_hooks 一次。 最后, sys.meta_path 用来保存一列需要在查询 sys.path 之 前访问的实例, 这些是为那些已经知道位置而不需要查找的模块准备的。 meta-path 已经有了指 定模块或包的载入器对象的读取器。 12.6 模块内建函数 系统还为模块提供了一些功能上的支持. 现在我们将详细讨论他们. 12.6.1 __import__() Python 1.5 加入了 __import__() 函数, 它作为实际上导入模块的函数, 这意味着 import 语 Edit By Vheavens  Edit By Vheavens  句调用 __import__() 函数完成它的工作。提供这个函数是为了让有特殊需要的用户覆盖它, 实现 自定义的导入算法。 __import__() 的语法是: __import__(module_name[, globals[, locals[, fromlist]]]) module_name 变量是要导入模块的名称, globals 是包含当前全局符号表的名字的字典, locals 是包含局部符号表的名字的字典, fromlist 是一个使用 from-import 语句所导入符号的 列表。 globals , locals , 以及 fromlist 参数都是可选的, 默认分别为 globals() , locals() 和 [] 。 调用 import sys 语句可以使用下边的语句完成: sys = __import__('sys') 12.6.2 globals() 和 locals() globals() 和 locals() 内建函数分别返回调用者全局和局部名称空间的字典。 在一个函数内 部, 局部名称空间代表在函数执行时候定义的所有名字, locals() 函数返回的就是包含这些名字 的字典。 globals() 会返回函数可访问的全局名字。 在全局名称空间下, globals() 和 locals() 返回相同的字典, 因为这时的局部名称空间就是 全局空间。 下边这段代码演示这两个函数的了使用: def foo(): print '\ncalling foo()...' aString = 'bar' anInt = 42 print "foo()'s globals:", globals().keys() print "foo()'s locals:", locals().keys() print "__main__'s globals:", globals().keys() print "__main__'s locals:", locals().keys() foo() 我们只在这里访问了字典的键 , 因为它的值在这里没有影响(而且他们会让行变得更长更难 懂)。 执行这个脚本, 我们得到如下的输出: $ namespaces.py __main__'s globals: ['__doc__', 'foo', '__name__', '__builtins__'] __main__'s locals: ['__doc__', 'foo', '__name__', '__builtins__'] Edit By Vheavens  Edit By Vheavens  calling foo()... foo()'s globals: ['__doc__', 'foo', '__name__', '__builtins__'] foo()'s locals: ['anInt', 'aString'] 12.6.3 reload() reload() 内建函数可以重新导入一个已经导入的模块。 它的语法如下: reload(module) module 是你想要重新导入的模块。使用 reload() 的时候有一些标准。 首先模块必须是全部 导入(不是使用 from-import), 而且它必须被成功导入。另外 reload() 函数的参数必须是模块自 身而不是包含模块名的字符串。 也就是说必须类似 reload(sys) 而不是 reload('sys')。 模块中的代码在导入时被执行, 但只执行一次. 以后执行 import 语句不会再次执行这些代码, 只是绑定模块名称。 而 reload() 函数不同。 12.7 包 包是一个有层次的文件目录结构, 它定义了一个由模块和子包组成的 Python 应用程序执行 环境。Python 1.5 加入了包, 用来帮助解决如下问题: z 为平坦的名称空间加入有层次的组织结构 z 允许程序员把有联系的模块组合到一起 z 允许分发者使用目录结构而不是一大堆混乱的文件 z 帮助解决有冲突的模块名称 与类和模块相同, 包也使用句点属性标识来访问他们的元素。 使用标准的 import 和 from-import 语句导入包中的模块。 12.7.1 目录结构 假定我们的包的例子有如下的目录结构: Phone/ __init__.py common_util.py Voicedta/ __init__.py Pots.py Edit By Vheavens  Edit By Vheavens  Isdn.py Fax/ __init__.py G3.py Mobile/ __init__.py Analog.py igital.py Pager/ __init__.py Numeric.py Phone 是最顶层的包, Voicedta 等是它的子包。 我们可以这样导入子包: import Phone.Mobile.Analog Phone.Mobile.Analog.dial() 你也可使用 from-import 实现不同需求的导入。 第一种方法是只导入顶层的子包, 然后使用属性/点操作符向下引用子包树: from Phone import Mobile Mobile.Analog.dial('555-1212') 此外, 我们可以还引用更多的子包: from Phone.Mobile import Analog Analog.dial('555-1212') 事实上, 你可以一直沿子包的树状结构导入: from Phone.Mobile.Analog import dial dial('555-1212') 在我们上边的目录结构中, 我们可以发现很多的 __init__.py 文件。 这些是初始化模块, from-import 语句导入子包时需要用到它。 如果没有用到, 他们可以是空文件。 程序员经常忘记 为它们的包目录加入 __init__.py 文件, 所以从 Python 2.5 开 始 , 这 将 会导致一个 ImportWarning 信息。 不过, 除非给解释器传递了 -Wd 选项, 否则它会被简单地忽略。 12.7.2 使用 from-import 导入包 Edit By Vheavens  Edit By Vheavens  包同样支持 from-import all 语句: from package.module import * 然而, 这样的语句会导入哪些文件取决于操作系统的文件系统. 所以我们在__init__.py 中加 入 __all__ 变量. 该变量包含执行这样的语句时应该导入的模块的名字. 它由一个模块名字符串 列表组成.。 12.7.3 绝对导入 包的使用越来越广泛, 很多情况下导入子包会导致和真正的标准库模块发生(事实上是它们的 名字)冲突。 包模块会把名字相同的标准库模块隐藏掉, 因为它首先在包内执行相对导入, 隐藏掉 标准库模块。 为此, 所有的导入现在都被认为是绝对的, 也就是说这些名字必须通过 Python 路 径 (sys.path 或是 PYTHONPATH )来访问。 这个决定的基本原理是子包也可以通过 sys.path 访问, 例如 import Phone.Mobile.Analog 。 在这个变化之前, 从 Mobile 子包内模块中导入 Analog 是合理的。作为一个折中方案, Python 允 许通过在模块或包名称前置句点实现相对导入。 更多信息请参阅第 12.7.4 节。 从 Python 2.7 开始, 绝对导入特性将成为默认功能。 ( 从 Python 2.5 开始, 你可以从 __future__ 导入 absolute_import , 体验这个功能。) 你可以参阅 PEP 328 了解更多相关内容。 12.7.4 相对导入 如前所述, 绝对导入特性限制了模块作者的一些特权。失去了 import 语句的自由, 必须有新 的特性来满足程序员的需求。这时候, 我们有了相对导入。 相对导入特性稍微地改变了 import 语 法, 让程序员告诉导入者在子包的哪里查找某个模块。因为 import 语句总是绝对导入的, 所以相 对导入只应用于 from-import 语句。 语法的第一部分是一个句点, 指示一个相对的导入操作。 之后的其他附加句点代表当前 from 起始查找位置后的一个级别。 我们再来看看上边的例子。在 Analog.Mobile.Digital , 也就是 Digital.py 模块中, 我们不 能简单地使用这样的语法。 下边的代码只能工作在旧版本的 Python 下, 在新的版本中它会导致一 个警告, 或者干脆不能工作: import Analog from Analog import dial Edit By Vheavens  Edit By Vheavens  这是绝对导入的限制造成的。你需要在使用绝对导入或是相对导入中做出选择。下边是一些可 行的导入方法: from Phone.Mobile.Analog import dial from .Analog import dial from ..common_util import setup from ..Fax import G3.dial. 从 2.5 版 开始, 相对导入被加入到了 Python 中 。 在 Python 2.6 中, 在模块内部的导入如 果没有使用相对导入, 那么会显示一个警告信息。 你可以在 PEP 328 的文档中获得更多相关信息。 12.8 模块的其他特性 12.8.1 自动载入的模块 当 Python 解释器在标准模式下启动时, 一些模块会被解释器自动导入, 用于系统相关操作。 唯一一个影响你的是 __builtin__ 模块, 它会正常地被载入, 这和 __builtins__ 模块相同。 sys.modules 变量包含一个由当前载入(完整且成功导入)到解释器的模块组成的字典, 模块 名作为键, 它们的位置作为值。 例如在 Windows 下, sys.modules 变量包含大量载入的模块, 我们这里截短它, 只提供他们的 模块名, 通过调用字典的 keys() 方法: >>> import sys >>> sys.modules.keys() ['os.path', 'os', 'exceptions', '__main__', 'ntpath', 'strop', 'nt', 'sys', '__builtin__', 'site', 'signal', 'UserDict', 'string', 'stat'] Unix 下载入的模块很类似: >>> import sys >>> sys.modules.keys() ['os.path', 'os', 'readline', 'exceptions', '__main__', 'posix', 'sys', '__builtin__', 'site', 'signal', 'UserDict', 'posixpath', 'stat'] 12.8.2 阻止属性导入 Edit By Vheavens  Edit By Vheavens  如果你不想让某个模块属性被 "from module import *" 导入 , 那么你可以给你不想导入的属 性名称加上一个下划线( _ )。 不过如果你导入了整个模块或是你显式地导入某个属性(例如 import foo._bar ), 这个隐藏数据的方法就不起作用了。 12.8.3 不区分大小的导入 有一些操作系统的文件系统是不区分大小写的。 Python 2.1 前, Python 尝试在不同平台下导 入模块时候"做正确的事情", 但随着 MacOS X 和 Cygwin 平台的流行, 这样的不足已经不能再被忽 视, 而需要被清除。 在 Unix(区分大小写)和 Win32(不区分大小写)下, 一切都很明了, 但那些新的不区分大小写 的系统不会被加入区分大小写的特性。 PEP 235 指定了这个特性, 尝试解决这个问题, 并避免那些 其他系统上"hack"式的解决方法。 底线就是为了让不区分大小写的导入正常工作, 必须指定一个叫 做 PYTHONCASEOK 的环境变量。 Python 会导入第一个匹配模块名( 使用不区分大小写的习惯 )。 否则 Python 会执行它的原生区分大小写的模块名称匹配, 导入第一个匹配的模块。 12.8.4 源代码编码 从 Python 2.3 开始, Python 的模块文件开始支持除 7 位 ASCII 之外的其他编码。 当然 ASCII 是默认的, 你只要在你的 Python 模块头部加入一个额外的编码指示说明就可以让导入者 使用指定的编码解析你的模块, 编码对应的 Unicode 字符串。 所以你使用纯 ASCII 文本编辑器的 时候不需要担心了(不需要把你的字符串放入 "Unicode 标签" 里) 。 一个 UTF-8 编码的文件可以这样指示: #!/usr/bin/env python # -*- coding: UTF-8 -*- 如果你执行或导入了包含非 ASCII 的 Unicode 字符串而没有在文件头部说明, 那么你会在 Python 2.3 得到一个 DeprecationWarning , 而在 2.5 中这样做会导致语法错误。你可以在 PEP 263 中得到更多关于源文件编码的相关内容。 12.8.5 导入循环 实际上,在使用 Python 时, 你会发现是能够导入循环的。 如果你开发了大型的 Python 工程, 那么你很可能会陷入这样的境地。 我们来看一个例子。 假定我们的产品有一个很复杂的命令行接口( command-line interface , CLI)。 其中将会有超过一百万的命令, 结果你就有了一个“超冗余处理器”(overly massive handler, Edit By Vheavens  Edit By Vheavens  OMH)子集。 每加入一个新特性, 将有一到三条的新命令加入, 用于支持新的特性。 下边是我们的 omh4cli.py 脚本: from cli4vof import cli4vof # command line interface utility function def cli_util(): pass # overly massive handlers for the command line interface def omh4cli(): : cli4vof() : omh4cli() 假定大多控制器都要用到这里的(其实是空的)工具函数。命令行接口的 OMH 都被封装在 omh4cli() 函数里。 如果我们要添加一个新的命令, 那么它会被调用。 现在这个模块不断地增长, 一些聪明的工程师会决定把新命令放入到隔离的模块里, 在原始模 块中只提供访问新东西的钩子。 这样, 管理代码会变得更简单, 如果在新加入内容中发现了 bug , 那么你就不必在一个几兆的 Python 文件里搜索。 在我们的例子中, 有一个兴奋的经理要我们加入一个 "非常好的特性"。我们将创建一个新的 cli4vof.py 脚本, 而不是把新内容集成到 omh4cli.py 里: import omh4cli # command-line interface for a very outstanding feature def cli4vof(): omh4cli.cli_util() 前边已经提到, 工具函数是每个命令必须的, 而且由于不能把代码从主控制器复制出来, 所以 我们导入了主模块, 在我们的控制器中添加对 omh , omh4cli() 的调用。 问题在于主控制器 omh4cli 会导入我们的 cli4vof 模块(获得新命令的函数), 而 cli4vof 也会导入 omh4cli (用于获得工具函数)。模块导入会失败, 这是因为 Python 尝试导入一个先前没 有完全导入的模块: $ python omh4cli.py Traceback (most recent call last): File "omh4cli.py", line 3, in ? from cli4vof import cli4vof File "/usr/prod/cli4vof.py", line 3, in ? import omh4cli File "/usr/prod/omh4cli.py", line 3, in ? from cli4vof import cli4vof Edit By Vheavens  Edit By Vheavens  ImportError: cannot import name cli4vof 注意跟踪返回消息中显示的对 cli4vof 的循环导入。 问题在于要想调用工具函数, cli4vof 必 须导入 omh4cli 。 如果它不需要这样做, 那么 omh4cli 将会成功导入 cli4vof , 程序正常执行。 但在这里, omh4cli 尝试导入 cli4vof , 而 cli4vof 也试着导入 omh4cli 。 最后谁也不会完成 导入工作, 引发错误。 这只是一个导入循环的例子。 事实上实际应用中会出现更复杂的情况。 解决这个问题几乎总是移除其中一个导入语句。 你经常会在模块的最后看到 import 语句。作 为一个初学者, 你只需要试着习惯它们, 如果你以前遇到在模块底部的 import 语句,现在你知道是 为什么了.。在我们的例子中, 我们不能把 import omh4cli 移到最后, 因为调用 cli4vof() 的时 候 omh4cli() 名字还没有被载入。 $ python omh4cli.py Traceback (most recent call last): File "omh4cli.py", line 3, in ? from cli4vof import cli4vof File "/usr/prod/cli4vof.py", line 7, in ? import omh4cli File "/usr/prod/omh4cli.py", line 13, in ? omh4cli() File "/usr/prod/omh4cli.py", line 11, in omh4cli cli4vof() File "/usr/prod/cli4vof.py", line 5, in cli4vof omh4cli.cli_util() NameError: global name 'omh4cli' is not defined 我们的解决方法只是把 import 语句移到 cli4vof() 函数内部: def cli4vof(): import omh4cli omh4cli.cli_util() 这样, 从 omh4cli() 导入 cli4vof() 模块会顺利完成, 在 omh4cli() 被调用之前它会被正 确导入。 只有在执行到 cli4vof.cli4vof() 时候才会导入 omh4cli 模块。 12.8.6 模块执行 有很多方法可以执行一个 Python 模块: 通过命令行或 shell , execfile() , 模块导入, 解 释器的 -m 选项, 等等。这已经超出了本章的范围。 你可以参考 第 14 章 "执行环境", 里边全面 地介绍了这些特性。 12.9 相关模块 Edit By Vheavens  Edit By Vheavens  下边这些模块可能是你在处理 Python 模块导入时会用到的辅助模块。 在这之中, modulefinder , pkgutil , 以及 zipimport 是 Python 2.3 新增内容, distutils 包在 Python 2.0 被引入。 z imp - 这个模块提供了一些底层的导入者功能。 z modulefinder - 该模块允许你查找 Python 脚本所使用的所有模块。你可以使用其中的 ModuleFinder 类或是把它作为一个脚本执行, 提供你要分析的(另个) Python 模块的文件 名。 z pkgutil - 该模块提供了多种把 Python 包打包为一个"包"文件分发的方法。 类似 site 模块, 它使用 *.pkg 文件帮助定义包的路径, 类似 site 模块使用的 *.pth 文件。 z site - 和 *.pth 文件配合使用, 指定包加入 Python 路径的顺序, 例如 sys.path , PYTHONPATH 。你不需要显式地导入它, 因为 Python 导入时默认已经使用该模块。你可能 需要使用 -S 开关在 Python 启动时关闭它。你也可以完成一些 site 相关的自定义操作, 例如在路径导入完成后在另个地方尝试。 z zipimport - 你可以使用该模块导入 ZIP 归档文件中的模块。 需要注意的是该功能已经" 自动"开启, 所以你不需要在任何应用中使用它。在这里我们提出它只是作为参考。 z distutils - 该模块提供了对建立、 安装、分发 Python 模块和包的支持。 它还可以帮助 建立使用 C/C++ 完成的 Python 扩展。 更多关于 distutils 的信息可以在 Python 文档 里找到, 参阅: http://docs.python.org/dist/dist.html http://docs.python.org/inst/inst.html 12.10 练习 12–1. 路径搜索和搜索路径。 路径搜索和搜索路径之间有什么不同? 12–2. 导入属性。 假设你的模块 mymodule 里有一个 foo() 函数。 (a) 把这个函数导入到你的名称空间有哪两种方法? (b) 这两种方法导入后的名称空间有什么不同? 12–3. 导入. "import module" 和 "fromn module import *" 有什么不同? 12–4. 名称空间和变量作用域。名称空间和变量作用域有什么不同? 12–5. 使用 __import__(). (a) 使用 __import__ 把一个模块导入到你的名称空间。 你最后使用了什么样的语法? (b) 和上边相同, 使用 __import__() 从指定模块导入特定的名字。 12–6. 扩展导入。创建一个 importAs() 函数. 这个函数可以把一个模块导入到你的名称空 间, 但使用你指定的名字, 而不是原始名字。 例如, 调用 newname=importAs('mymodule') 会导入 mymodule , 但模块和它的所有元素都通过新名称 newname 或 newname.attr 访问。 这是 Python 2.0 引入的扩展导入实现的功能。 12–7. 导入钩子。 研究 PEP 302 的导入钩子机制. 实现你自己的导入机制, 允许编码你的 模块(encryption, bzip2, rot13, 等), 这样解释器会自动解码它们并正确导入。你可以参看 zip 文件导入的实现 (参阅 第 12.5.7 节)。 Edit By Vheavens  Edit By Vheavens  面向对象编程 本章主题 z 引言 z 面向对象编程 z 类 z 实例 z 绑定与方法调用 z 子类,派生和继承 z 内建函数 z 定制类 z 私有性 z 授权与包装 z 新式类的高级特性 z 相关模块 Edit By Vheavens  Edit By Vheavens  在我们的描绘中,类最终解释了面向对象编程思想(OOP)。本章中,我们首先将给出一个总体上 的概述,涵盖了 Python 中使用类和 OOP 的所有主要方面。其余部分针对类,类实例和方法进行详细 探讨。我们还将描述 Python 中有关派生或子类化及继承机理。最后,Python 可以在特定功能方面定 制类,例如重载操作符,模拟Python 类型等。我们将展示如何实现这些特殊的方法来自定义你的类, 以让它们表现得更像 Python 的内建类型。 然而,除了这些外,Python 的面向对象编程(00P)还有一些令人兴奋的变动。在版本 2.2 中, Python 社区最终统一了类型(types)和类(classes),新式类具备更多高级的 OOP 特性,扮演了一个 经典类(或者说旧式类)超集的角色,后者是 Python 诞生时所创造的类对象。 下面,我们首先介绍在两种风格的类(译者注:新式类和旧式类)中都存在的核心特性,然后讲 解那些只有新式类才拥有的的高级特性。 13.1 介绍 在摸清 OOP 和类的本质之前,我们首先讲一些高级主题,然后通过几个简单的例子热一热身。 如果你刚学习面向对象编程,你可以先跳过这部分内容,直接进入第 13.2 节。如果你对有关面向对 象编程已经熟悉了,并且想了解它在 Python 中是怎样表现的,那么先看一下这部分内容,然后再进 入 13.3 节,看个究竟! 在 Python 中,面向对象编程主要有两个主题,就是类和类实例(见图 13-1) 类与实例 类与实例相互关联着:类是对象的定义,而实例是"真正的实物",它存放了类中所定义的对象 的具体信息。 Edit By Vheavens  Edit By Vheavens  下面的示例展示了如何创建一个类: class MyNewObjectType(bases): 'define MyNewObjectType class' class_suite #类体 关键字是 class,紧接着是一个类名。随后是定义类的类体代码。这里通常由各种各样的定义和 声明组成。新式类和经典类声明的最大不同在于,所有新式类必须继承至少一个父类,参数 bases 可以是一个(单继承)或多个(多重继承)用于继承的父类。 object 是“所有类之母”。如果你的类没有继承任何其他父类,object将作为默认的父类。它 位于所有类继承结构的最上层。如果你没有直接或间接的子类化一个对象,那么你就定义了一个经 典类: class MyNewObjectType: 'define MyNewObjectType classic class' class_suite 如果你没有指定一个父类,或者如果所子类化的基本类没有父类,你这样就是创建了一个经典 类。很多 Python 类都还是经典类。即使经典类已经过时了,在以后的 Python 版本中,仍然可以使 用它们。不过我们强烈推荐你尽可能使用新式类,尽管对于学习来说,两者都行。 Edit By Vheavens  Edit By Vheavens  图 13-1 左边的工厂制造机器相当于类,而生产出来的玩具就是它们各个类的实例。尽管每个 实例都有一个基本的结构,但各自的属性像颜色或尺寸可以改变-----这就好比实例的属性。 创建一个实例的过程称作实例化,过程如下(注意:没有使用 new 关键字): myFirstObject = MyNewObjectType() 类名使用我们所熟悉的函数操作符(()),以“函数调用”的形式出现。然后你通常会把这个新 建的实例赋给一个变量。赋值在语法上不是必须的,但如果你没有把这个实例保存到一个变量中, 它就没用了,会被自动垃圾收集器回收,因为任何引用指向这个实例。这样,你刚刚所做的一切, 就是为那个实例分配了一块内存,随即又释放了它。 只要你需要,类可以很简单,也可以很复杂。最简单的情况,类仅用作名称空间(namespaces) (参见第 11 章)。这意味着你把数据保存在变量中,对他们按名称空间进行分组,使得他们处于同 样的关系空间中-----所谓的关系是使用标准 Python 句点属性标识。比如,你有一个本身没有任何 属性的类,使用它仅对数据提供一个名字空间,让你的类拥有像 Pascal 中的记录集(records)和 C 语言中的结构体(structures)一样的特性,或者换句话说,这样的类仅作为容器对象来共享名字 空间。 示例如下: class MyData(object): pass Edit By Vheavens  Edit By Vheavens  注意有的地方在语法构成上需要有一行语句,但实际上不需要做任何操作,这时候可以使用pass 语句。这种情况,必要的代码就是类体,但我们暂不想提供这些。上面定义的类没有任何方法或属 性。下面我们创建一个实例,它只使用类作为名称空间容器。 >>> mathObj = MyData() >>> mathObj.x = 4 >>> mathObj.y = 5 >>> mathObj.x + mathObj.y 9 >>> mathObj.x * mathObj.y 20 我们当然也可以使用变量“x”,“y”来完成同样的事情,但本例中,实例名字mathObj 将 mathObj.x 和 mathObj.y 关联起来。这就是我们所说的使用类作为名字空间容器。mathObj.x 和 mathObj.y 是 实例属性,因为它们不是类 MyData 的属性,而是实例对象(mathObj)的独有属性。本章后面,我 们将看到这些属性实质上是动态的:你不需要在构造器中,或其它任何地方为它们预先声明或者赋值。 方法 我们改进类的方式之一就是给类添加功能。类的功能有一个更通俗的名字叫方法。在 Python 中,方法定义在类定义中,但只能被实例所调用。也就是说,调用一个方法的最终途径必须是这样 的:(1)定义类(和方法),(2)创建一个实例(3)最后一步,用这个实例调用方法。例如: class MyDataWithMethod(object): # 定义类 def printFoo(self): # 定义方法 print 'You invoked printFoo()!' 你可能注意到了 self 参数,它在所有的方法声明中都存在。这个参数代表实例对象本身,当你 用实例调用方法时,由解释器悄悄地传递给方法的,所以,你不需要自己传递 self 进来,因为它是 自动传入的。举例说明一下,假如你有一个带两参数的方法,所有你的调用只需要传递第二个参数, Python 把 self 作为第一个参数传递进来,如果你犯错的话,也不要紧。Python 将告诉你传入的参 数个数有误。总之,你只会犯一次错,下一次———你当然就记得了! 这种需要在每个方法中给出实例(self)的要求对于那些使用 C++或 Java 的人,可能是一种新 的体验,所以请意识到这点。 Python 的哲学本质上就是要明白清晰。在其它语言中,self称为“this”。可以在13.7 节的“核 心笔记”中找到有关self 更多内容。一般的方法会需要这个实例(self),而静态方法或类方法不会, 其中类方法需要类而不是实例。在第 13.8 节中可以看到有关静态方法和类方法的更多内容。 Edit By Vheavens  Edit By Vheavens  现在我们来实例化这个类,然后调用那个方法: >>> myObj = MyDataWithMethod() # 创建实例 >>> myObj.printFoo() # 现在调用方法 You invoked printFoo()! 在本节结束时,我们用一个稍复杂的例子来总结一下这部分内容,这个例子给出如何处理类(和 实例),还介绍了一个特殊的方法__init__(),子类化及继承。 对于已熟悉面向对象编程的人来说,__init__()类似于类构造器。如果你初涉 OOP 世界,可以 认为一个构造器仅是一个特殊的方法,它在创建一个新的对象时被调用。在 Python 中,__init__() 实际上不是一个构造器。你没有调用“new”来创建一个新对象。(Python根本就没有“new”关键字)。 取而代之,Python 创建实例后,在实例化过程中,调用__init__()方法,当一个类被实例化时,就 可以定义额外的行为,比如,设定初始值或者运行一些初步诊断代码———主要是在实例被创建后, 实例化调用返回这个实例之前,去执行某些特定的任务或设置。 (我们将把 print 语句添加到方法中,这样我们就清楚什么时候方法被调用了。通常,我们不把 输入或输出语句放入函数中,除非预期代码体具有输出的特性。) 创建一个类(类定义) class AddrBookEntry(object): # 类定义 'address book entry class' def __init__(self, nm, ph): # 定义构造器 self.name = nm # 设置 name self.phone = ph # 设置 phone print 'Created instance for:', self.name def updatePhone(self, newph): # 定义方法 self.phone = newph print 'Updated phone# for:', self.name 在 AddrBookEntry 类的定义中,定义了两个方法:__init__()和 updatePhone()。__init__() 在实例化时被调用,即,在 AddrBookEntry()被调用时。你可以认为实例化是对__init__()的一种隐 式的调用,因为传给AddrBookEntry()的参数完全与__init__()接收到的参数是一样的(除了self, 它是自动传递的)。 回忆一下,当方法在实例中被调用时,self(实例对象)参数自动由解释器传递,所以在上面 的__init__()中,需要的参数是 nm 和 ph,它们分别表示名字和电话号码。__init__()在实例化时, 设置这两个属性,以便,在实例从实例化调用中返回时,这两个属性对程序员是可见的了。你可能 Edit By Vheavens  Edit By Vheavens  已猜到,updatePhone()方法的目的是替换地址本条目的电话号码属性。 创建实例(实例化) >>> john = AddrBookEntry('John Doe', '408-555-1212') #为 John Doe 创建实例 >>> jane = AddrBookEntry('Jane Doe', '650-555-1212') #为 Jane Doe 创建实例 这就是实例化调用,它会自动调用__init__()。self 把实例对象自动传入__init__()。你可以 在脑子里把方法中的 self 用实例名替换掉。在上面第一个例子中,当对象 john 被实例化后,它的 john.name 就被设置了,你可在下面得到证实。 另外,如果不存在默认的参数,那么传给__init__()的两个参数在实例化时是必须的。 访问实例属性 >>> john <__main__.AddrBookEntry instance at 80ee610> >>> john.name 'John Doe' >>> john.phone '408-555-1212' >>> jane.name 'Jane Doe' >>> jane.phone '650-555-1212' 一旦实例被创建后,就可以证实一下,在实例化过程中,我们的实例属性是否确实被__init__() 设置了。我们可以通过解释器“转储”实例来查看它是什么类型的对象。(我们以后将学到如何定制 类来获得想要的 Python 对象字符串的输出形式,而不是现在看到的默认的 Python 对象字符串 (<...>)) 方法调用(通过实例) >>> john.updatePhone('415-555-1212') #更新 John Doe 的电话 >>> john.phone '415-555-1212' updatePhone()方法需要一个参数(不计 self 在内):新的电话号码。在updatePhone()之后, 立即检查实例属性,可以证实已生效。 Edit By Vheavens  Edit By Vheavens  创建子类 靠继承来进行子类化是创建和定制新类类型的一种方式,新的类将保持已存在类所有的特性, 而不会改动原来类的定义(指对新类的改动不会影响到原来的类,译者注)。对于新的类类型来说, 这个新的子类可以定制只属于它的特定功能。除了与父类或基类的关系外,子类与通常的类没有什 么区别,也像一般类一样进行实例化。注意下面,子类声明中提到了父类: class EmplAddrBookEntry(AddrBookEntry): 'Employee Address Book Entry class'#员工地址本类 def __init__(self, nm, ph, id, em): AddrBookEntry.__init__(self, nm, ph) self.empid = id self.email = em def updateEmail(self, newem): self.email = newem print 'Updated e-mail address for:', self.name 现在我们创建了第一个子类,EmplAddrBookEntry。Python 中,当一个类被派生出来,子类继 承了基类的属性,所以,在上面的类中,我们不仅定义了__init__(),updatEmail()方法,而且 EmplAddrBookEntry 还从 AddrBookEntry 中继承了 updatePhone()方法。 如果需要,每个子类最好定义它自己的构造器,不然,基类的构造器会被调用。然而,如果子 类重写基类的构造器,基类的构造器就不会被自动调用了--这样,基类的构造器就必须显式写出 才会被执行,像我们上面那样,用 AddrBookEntry.__init__()设置名字和电话号码。我们的子类在 构造器后面几行还设置了另外两个实例属性:员工 ID 和 E-mail 地址。 注意,这里我们要显式传递 self 实例对象给基类构造器,因为我们不是在其实例中调用那个方 法而是在一个子类实例中调用那个方法。因为我们不是通过实例来调用它,这种未绑定的方法调用 需要传递一个适当的实例(self)给方法。 本小节后面的例子,告诉我们如何创建子类的实例,访问它们的属性及调用它的方法,包括从 父类继承而来的方法。 使用子类 >>> john = EmplAddrBookEntry('John Doe', '408-555-1212',42, 'john@spam.doe') Created instance for: John Doe #给 John Doe 创建实例 >>> john <__main__.EmplAddrBookEntry object at 0x62030> Edit By Vheavens  Edit By Vheavens  >>> john.name 'John Doe' >>> john.phone '408-555-1212' >>> john.email 'john@spam.doe' >>> john.updatePhone('415-555-1212') Updated phone# for: John Doe >>> john.phone '415-555-1212' >>> john.updateEmail('john@doe.spam') Updated e-mail address for: John Doe >>> john.email 'john@doe.spam' 核心笔记:命名类、属性和方法 类名通常由大写字母打头。这是标准惯例,可以帮助你识别类,特别是在实例化过程中(有时看 起来像函数调用)。还有,数据属性(译者注:变量或常量)听起来应当是数据值的名字,方法名应 当指出对应对象或值的行为。另一种表达方式是:数据值应该使用名词作为名字,方法使用谓词(动 词加对象)。数据项是操作的对象、方法应当表明程序员想要在对象进行什么操作。在上面我们定义 的类中,遵循了这样的方针,数据值像“name”,“phone”和“email”,行为如“updatePhone”, “updateEmail”。这就是常说的“混合记法(mixedCase)”或“骆驼记法(camelCase)”。Python规 范推荐使用骆驼记法的下划线方式,比如,“update_phone”,“update_email”。类也要细致命名, 像“AddrBookEntry”,“RepairShop”等等就是很好的名字。 我希望你已初步理解如何在 Python 中进行面向对象编程了。本章其它小节将带你深入面向对象 编程,Python 类及实例的方方面面。 13.2 面向对象编程 编程的发展已经从简单控制流中按步的指令序列进入到更有组织的方式中,依靠代码块可以形 成命名子程序和完成既定的功能。结构化的或过程性编程可以让我们把程序组织成逻辑块,以便重 复或重用。创建程序的过程变得更具逻辑性;选出的行为要符合规范,才可以约束创建的数据。迪 特尔父子(这里指 DEITEL 系列书籍作者 Harvey M.Deitel 和 Paul James Deitel 父子,译者注)认 为结构化编程是“面向行为”的,因为事实上,即使没有任何行为的数据也必须“规定”逻辑性。 然而,如果我们能对数据加上动作呢?如果我们所创建和编写的数据片段,是真实生活中实体 的模型,内嵌数据体和动作呢?如果我们能通过一系列已定义的接口(又称存取函数集合)访问数据 属性,像自动取款机卡或能访问你的银行帐号的个人支票,我们就有了一个“对象”系统,从大的 方面来看,每一个对象既可以与自身进行交互,也可以与其它对象进行交互。 Edit By Vheavens  Edit By Vheavens  面向对象编程踩上了进化的步伐,增强了结构化编程,实现了数据与动作的融合:数据层和逻 辑层现在由一个可用以创建这些对象的简单抽象层来描述。现实世界中的问题和实体完全暴露了本 质,从中提供的一种抽象,可以用来进行相似编码,或者编入能与系统中对象进行交互的对象中。 类提供了这样一些对象的定义,实例即是这些定义的实现。二者对面向对象设计(object-oriented design,OOD)来说都是重要的,OOD 仅意味来创建你采用面向对象方式架构来创建系统。 13.2.1 面向对象设计与面向对象编程的关系 面向对象设计(OOD)不会特别要求面向对象编程语言。事实上,OOD 可以由纯结构化语言来实 现,比如 C,但如果想要构造具备对象性质和特点的数据类型,就需要在程序上作更多的努力。当一 门语言内建 OO 特性,OO 编程开发就会更加方便高效。 另一方面,一门面向对象的语言不一定会强制你写 OO 方面的程序。例如 C++可以被认为“更好 的C”;而 Java,则要求万物皆类,此外还规定,一个源文件对应一个类定义。然而,在 Python 中, 类和 OOP 都不是日常编程所必需的。尽管它从一开始设计就是面向对象的,并且结构上支持 OOP,但 Python 没有限定或要求你在你的应用中写 OO 的代码。OOP 是一门强大的工具,不管你是准备进入, 学习,过渡,或是转向 OOP,都可以任意支配。 13.2.2 现实世界中的问题 考虑用 OOD 来工作的一个最重要的原因,在于它直接提供建模和解决现实世界问题和情形的途 径。比如,让你来试着模拟一台汽车维修店,可以让你停车进行维修。我们需要建两个一般实体: 处在一个“系统”中并与其交互的人类,和一个修理店,它定义了物理位置,用于人类活动。因为 前者有更多不同的类型,我将首先对它进行描述,然后描述后者。在此类活动中,一个名为 Person 的类被创建以用来表示所有的人。Person 的实例可以包括消费者(Customer),技工(Mechanic),还 可能是出纳员(Cashier)。这些实例具有相似的行为,也有独一无二的行为。比如,他们能用声音进 行交流,都有 talk()方法,还有 drive_car()方法。不同的是,技工有 repair_car()方法,而出纳 有 ring_sale()方法。技工有一个repair_certification 属性,而所有人都有一个drivers_license 属性。 最后,所有这些实例都是一个检查(overseeing)类 RepairShop 的参与者,后者具有一个叫 operating_hours 的数据属性,它通过时间函数来确定何时顾客来修车,何时职员技工和出纳员来上 班。RepairShop 可能还有一个 AutoBay 类,拥有 SmogZone,TireBrakeZone 等实例,也许还有一个叫 GeneralRepair 的实例。 我们所编的 RepairShop 的一个关键点是要展示类和实例加上它们的行为是如何用来对现实生活 场景建模的。同样,你可以把诸如机场,餐厅,晶蕊,医院,其至一个邮订音乐公司想像为类,它 们完全具备各自的参与者和功能性。 Edit By Vheavens  Edit By Vheavens  13.2.3*常用术语 对于已熟悉有关 OOP 术语的朋友来说,看 Python 中是怎么称呼的: 抽象/实现 抽象指对现实世界问题和实体的本质表现,行为和特征建模,建立一个相关的子集,可以用于 描绘程序结构,从而实现这种模型。抽象不仅包括这种模型的数据属性,还定义了这些数据的接口。 对某种抽象的实现就是对此数据及与之相关接口的现实化(realization)。现实化这个过程对于客户 程序应当是透明而且无关的。 封装/接口 封装描述了对数据/信息进行隐藏的观念,它对数据属性提供接口和访问函数。通过任何客户端 直接对数据的访问,无视接口,与封装性都是背道而驰的,除非程序员允许这些操作。作为实现的 一部分,客户端根本就不需要知道在封装之后,数据属性是如何组织的。在 Python 中,所有的类属 性都是公开的,但名字可能被“混淆”了,以阻止未经授权的访问,但仅此而已,再没有其他预防 措施了。这就需要在设计时,对数据提供相应的接口,以免客户程序通过不规范的操作来存取封装 的数据属性。 合成 合成扩充了对类的描述,使得多个不同的类合成为一个大的类,来解决现实问题。合成描述了 一个异常复杂的系统,比如一个类由其它类组成,更小的组件也可能是其它的类,数据属性及行为, 所有这些合在一起,彼此是“有一个”的关系。比如,RepairShop“有一个”技工(应该至少有一个 吧),还“有一个”顾客(至少一个)。 这些组件要么通过联合关系组在一块,意思是说,对子组件的访问是允许的(对 RepairShop 来 说,顾客可能请求一个 SmogCheck,客户程序这时就是与 RepairShop 的组件进行交互),要么是聚合 在一起,封装的组件仅能通过定义好的接口来访问,对于客户程序来说是透明的。继续我的例子, 客户程序可能会建立一个 SmogCheck 请求来代表顾客,但不能够同 RepairShop 的 SmogZone 部分进 行交互,因为 SmogZone 是由 RepairShop 内部控制的,只能通过 smogCheckCar()方法调用。Python 支持上述两种形式的合成。 派生/继承/继承结构 派生描述了子类的创建,新类保留已存类类型中所有需要的数据和行为,但允许修改或者其它 的自定义操作,都不会修改原类的定义。继承描述了子类属性从祖先类继承这样一种方式。从前面 Edit By Vheavens  Edit By Vheavens  的例子中,技工可能比顾客多个汽车技能属性,但单独的来看,每个都“是一个”人,所以,不管 对谁而言调用 talk()都是合法得,因为它是人的所有实例共有的。继承结构表示多“代”派生,可 以描述成一个“族谱”,连续的子类,与祖先类都有关系。 泛化/特化 泛化表示所有子类与其父类及祖先类有一样的特点,所以子类可以认为同祖先类是“是一个” 的关系,因为一个派生对象(实例)是祖先类的一个“例子”。比如,技工“是一个”人,车“是一 个”交通工具,等等。在上面我们间接提到的族谱图中,我们可以从子类到祖先类画一条线,表示 “是一个”的关系。特化描述所有子类的自定义,也就是,什么属性让它与其祖先类不同。 多态 多态的概念指出了对象如何通过他们共同的属性和动作来操作及访问,而不需考虑他们具体的 类。多态表明了动态(又名,运行时)绑定的存在,允计重载及运行时类型确定和验证。 自省/反射 自省表示给予你,程序员,某种能力来进行像“手工类型检查”的工作,它也被称为反射。这 个性质展示了某对象是如何在运行期取得自身信息的。如果传一个对象给你,你可以查出它有什么 能力,这样的功能不是很好吗?这是一项强大的特性,在本章中,你会时常遇到。如果 Python 不 支持某种形式的自省功能,dir()和 type()内建函数,将很难正常工作。请密切关注这些调用,还 有那些特殊属性,像__dict__,__name__及__doc__。可能你对其中一些已经很熟悉了! 13.3 类 回想一下,类是一种数据结构,我们可以用它来定义对象,后者把数据值和行为特性融合在一 起。类是现实世界的抽象的实体以编程形式出现。实例是这些对象的具体化。可以类比一下,类是 蓝图或者模型,用来产生真实的物体(实例)。因此为什么是术语“class”?这个术语很可能起源 于使用类来识别和归类特定生物所属的生物种族,类还可以派生出相似但有差异的子类。编程中类 的概念就应用了很多这样的特征。 在 Python 中,类声明与函数声明很相似,头一行用一个相应的关键字,接下来是一个作为它的 定义的代码体,如下所示: def functionName(args): 'function documentation string' #函数文档字符串 function_suite #函数体 class ClassName(object): Edit By Vheavens  Edit By Vheavens  'class documentation string' #类文档字符串 class_suite #类体 二者都允许你在他们的声明中创建函数,闭包或者内部函数(即函数内的函数),还有在类中定 义的方法。最大的不同在于你运行函数,而类会创建一个对象。类就像一个 Python 容器类型。在这 部分,我们将特别留意类及它们有什么类型的属性。这只要记住,尽管类是对象(在 Python 中,一 切皆对象),但正被定义时,它们还不是对象的实现。在下节中会讲到实例,所以拭目以待吧。不过 现在,我们集中讲解类对象。 当你创建一个类,你就实际创建了一个你自己的数据类型。所以这个类的实例都是相似的,但 类之间彼此是有区别的(因此,不同类的实例自然也不可能相同了)。与其玩那些从玩具商那买来的 玩具礼物,为什么不设计并创造你自己的玩具来玩呢? 类还允许派生。你可以创建一个子类,它也是类,而且继续了父类所有的特征和属性。从 Python2.2 开始,你也可以从内建类型中派生子类,而不是仅仅从其它类。 13.3.1 创建类 Python 类使用 class 关键字来创建。简单的类的声明可以是关键字后紧跟类名: class ClassName(bases): 'class documentation string' #'类文档字符串' class_suite #类体 本章前面的概述中提到,基类是一个或多个用于继承的父类的集合;类体由所有声明语句,类 成员定义,数据属性和函数组成。类通常在一个模块的顶层进行定义,以便类实例能够在类所定义 的源代码文件中的任何地方被创建。 13.3.2 声明与定义 对于 Python 函数来说,声明与定义类没什么区别,因为他们是同时进行的,定义(类体)紧跟 在声明(含 class 关键字的头行[header line])和可选(但总是推荐使用)的文档字符串后面。同时, 所有的方法也必须同时被定义。如果对 OOP 很熟悉,请注意 Python 并不支持纯虚函数(像 C++)或 者抽象方法(如在 JAVA 中),这些都强制程序员在子类中定义方法。作为替代方法,你可以简单地 在基类方法中引发 NotImplementedError 异常,这样可以获得类似的效果。 13.4 类属性 Edit By Vheavens  Edit By Vheavens  什么是属性呢?属性就是属于另一个对象的数据或者函数元素,可以通过我们熟悉的句点属性 标识法来访问。一些 Python 类型比如复数有数据属性(实部和虚部),而另外一些,像列表和字典, 拥有方法(函数属性)。 有关属性的一个有趣的地方是,当你正访问一个属性时,它同时也是一个对象,拥有它自己的 属性,可以访问,这导致了一个属性链,比如,myThing,subThing,subSubThing.等等。常见例子如 下: z sys.stdout.write('foo') z print myModule.myClass.__doc__ z myList.extend(map(upper, open('x').readlines())) 类属性仅与其被定义的类相绑定,并且因为实例对象在日常 OOP 中用得最多,实例数据属性是 你将会一直用到的主要数据属性。类数据属性仅当需要有更加“静态”数据类型时才变得有用,它 和任何实例都无关,因此,这也是为什么下一节被表为高级主题,你可以选读。(如果你对静态不熟, 它表示一个值,不会因为函数调用完毕而消失,它在每两个函数调用的间隙都存在。或者说,一个 类中的一些数据对所有的实例来说,都是固定的。有关静态数据详细内容,见下一小节.) 接下来的一小节中,我们将简要描述,在 Python 中,方法是如何实现及调用的。通常,Python 中的所有方法都有一个限制:在调用前,需要创建一个实例。 13.4.1 类的数据属性 数据属性仅仅是所定义的类的变量。它们可以像任何其它变量一样在类创建后被使用,并且, 要么是由类中的方法来更新,要么是在主程序其它什么地方被更新。 这种属性已为 OO 程序员所熟悉,即静态变量,或者是静态数据。它们表示这些数据是与它们所 属的类对象绑定的,不依赖于任何类实例。如果你是一位 Java 或 C++程序员,这种类型的数据相当 于在一个变量声明前加上 static 关键字。 静态成员通常仅用来跟踪与类相关的值。大多数情况下,你会考虑用实例属性,而不是类属性。 在后面,我们正式介绍实例时,将会对类属性及实例属性进行比较。 看下面的例子,使用类数据属性(foo): >>> class C(object): ... foo = 100 >>> print C.foo 100 >>> C.foo = C.foo + 1 Edit By Vheavens  Edit By Vheavens  >>> print C.foo 101 注意,上面的代码中,看不到任何类实例的引用。 13.4.2 方法 方法,比如下面,类 MyClass 中的 myNoActionMethod 方法,仅仅是一个作为类定义一部分定义 的函数.(这使得方法成为类属性)。这表示myNoActionMethod 仅应用在 MyClass 类型的对象(实例) 上。这里,myNoActionMethod 是通过句点属性标识法与它的实例绑定的。 >>> class MyClass(object): def myNoActionMethod(self): pass >>> mc = MyClass() >>> mc.myNoActionMethod() 任何像函数一样对 myNoActionMethod 自身的调用都将失败: >>> myNoActionMethod() Traceback (innermost last): File "", line 1, in ? myNoActionMethod() NameError: myNoActionMethod 引发了 NameError 异常,因为在全局名字空间中,没有这样的函数存在。这就告诉你 myNoActionMethod 是一个方法,表示它属于一个类,而不是全局空间中的名字。如果 myNoActionMethod 是在顶层作为函数被定义的,那么我们的调用则会成功。 下面展示的是,甚至由类对象调用此方法也失败了。 >>> MyClass.myNoActionMethod() Traceback (innermost last): File "", line 1, in ? MyClass.myNoActionMethod() TypeError: unbound method must be called with class instance 1st argument TypeError 异常初看起来很让人困惑,因为你知道这种方法是类的一个属性,因此,一定很想知 道为何为失败吧?接下来将会解释这个问题。 绑定(绑定及非绑定方法) 为与 OOP 惯例保持一致,Python严格要求,没有实例,方法是不能被调用的。这种限制即Python Edit By Vheavens  Edit By Vheavens  所描述的绑定概念(binding),在此,方法必须绑定(到一个实例)才能直接被调用。非绑定的方法 可能可以被调用,但实例对象一定要明确给出,才能确保调用成功。然而,不管是否绑定,方法都 是它所在的类的固有属性,即使它们几乎总是通过实例来调用的。在 13.7 节中,我们会更深入地探 索本主题。 13.4.2 决定类的属性 要知道一个类有哪些属性,有两种方法。最简单的是使用 dir()内建函数。另外是通过访问类的 字典属性__dict__,这是所有类都具备的特殊属性之一。看一下下面的例子: >>> class MyClass(object): ... 'MyClass class definition' #MyClass 类定义 ... myVersion = '1.1' # static data 静态数据 ... def showMyVersion(self): # method 方法 ... print MyClass.myVersion ... 根据上面定义的类,让我们使用 dir()和特殊类属性__dict__来查看一下类的属性: >>> dir(MyClass) ['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', '__weakref__', 'myVersion', 'showMyVersion'] >>> MyClass.__dict__ >>> print MyClass.__dict__ {'showMyVersion': , '__dict__': , 'myVersion': '1.1', '__weakref__': , '__doc__': 'MyClass class definition'} 在新式类中,还新增加了一些属性,dir()也变得更健壮。作为比较,可以看下经典类是什么样 的: >>> dir(MyClass) Edit By Vheavens  Edit By Vheavens  ['__doc__', '__module__', 'showMyVersion', 'myVersion'] >>> >>> MyClass.__dict__ {'__doc__': None, 'myVersion': 1, 'showMyVersion': , '__module__': '__main__'} 从上面可以看到,dir()返回的仅是对象的属性的一个名字列表,而__dict__返回的是一个字典, 它的键(keys)是属性名,键值(values)是相应的属性对象的数据值。 结果还显示了 MyClass 类中两个熟悉的属性,showMyVersion 和 myVersion,以及一些新的属 性。这些属性,__doc__及__module__,是所有类都具备的特殊类属性(另外还有__dict__)。。内建 的 vars()函数接受类对象作为参数,返回类的__dict__属性的内容。 13.4.3 特殊的类属性 对任何类C,表 13.1 显示了类C的所有特殊属性: 表 13.1 特殊类属性 C.__name__ 类C的名字(字符串) C.__doc__ 类C的文档字符串 C.__bases__ 类C的所有父类构成的元组 C.__dict__ 类C的属性 C.__module__ 类C定义所在的模块(1.5 版本新增) C.__class__ 实例C对应的类(仅新式类中) 根据上面定义的类 MyClass,有如下结果: >>> MyClass.__name__ 'MyClass' >>> MyClass.__doc__ 'MyClass class definition' >>> MyClass.__bases__ (,) >>> print MyClass.__dict__ {'__doc__': None, 'myVersion': 1, 'showMyVersion': , '__module__': '__main__'} >>> MyClass.__module__ '__main__' >>> MyClass.__class__ Edit By Vheavens  Edit By Vheavens  __name__是给定类的字符名字。它适用于那种只需要字符串(类对象的名字),而非类对象本身 的情况。甚至一些内建的类型也有这个属性,我们将会用到其中之一来展示__name__字符串的益处。 类型对象是一个内建类型的例子,它有__name__的属性。回忆一下,type()返回被调用对象的 类型。这可能就是那种我们所说的仅需要一个字符串指明类型,而不需要一个对象的情况。我们能 可以使用类型对象的__name__属性来取得相应的字符串名。如下例示: >>> stype = type('What is your quest?') >>> stype # stype is a type object stype 是一个类型对象 >>> stype.__name__ # get type as a string 得到类型名(字符串表示) 'string' >>> >>> type(3.14159265) # also a type object 又一个类型对象 >>> type(3.14159265).__name__ # get type as a string 得到类型名(字符串表示) 'float' __doc__是类的文档字符串,与函数及模块的文档字符串相似,必须紧随头行(header line) 后的字符串。文档字符串不能被派生类继承,也就是说派生类必须含有它们自己的文档字符串。 本章后面会讲到,__bases__用来处理继承,它包含了一个由所有父类组成的元组。 前述的__dict__属性包含一个字典,由类的数据属性组成。访问一个类属性的时候,Python 解 释器将会搜索字典以得到需要的属性。如果在__dict__中没有找到,将会在基类的字典中进行搜索, 采用“深度优先搜索”顺序。基类集的搜索是按顺序的,从左到右,按其在类定义时,定义父类参 数时的顺序。对类的修改会仅影响到此类的字典;基类的__dict__属性不会被改动的。 Python 支持模块间的类继承。为更清晰地对类进行描述,1。1,1.5 版本中引入了__module__, 这样类名就完全由模块名所限定。看一下下面的例子: >>> class C(object): ... pass ... >>> C >>> C.__module__ '__main__' 类 C 的全名是“__main__.C”,比如,source_module.class_name。如果类C 位于一个导入的 Edit By Vheavens  Edit By Vheavens  模块中,如 mymod,像下面的: >>> from mymod import C >>> C >>> C.__module__ 'mymod' 在以前的版本中,没有特殊属性__module__,很难简单定位类的位置,因为类没有使用它们的 全名。 最后,由于类型和类的统一性,当访问任何类的__class__属性时,你将发现它就是一个类型对 象的实例。换句话说,一个类已是一种类型了。因为经典类并不认同这种等价性(一个经典类是一 个类对象,一个类型是一个类型对象),对这些对象来说,这个属性并未定义。 13.5 实例 如果说类是一种数据结构定义类型,那么实例则声明了一个这种类型的变量。换言之,实例是 有生命的类。就像设计完一张蓝图后,就是设法让它成为现实。实例是那些主要用在运行期时的对 象,类被实例化得到实例,该实例的类型就是这个被实例化的类。在 Python2.2 版本之前,实例是 “实例类型”,而不考虑它从哪个类而来。 13.5.1 初始化:通过调用类对象来创建实例 很多其它的 OO 语言都提供 new 关键字,通过 new 可以创建类的实例。Python 的方式更加简单。 一旦定义了一个类,创建实例比调用一个函数还容易------不费吹灰之力。实例化的实现,可以使 用函数操作符,如下示: >>> class MyClass(object): # define class 定义类 ... pass >>> mc = MyClass() # instantiate class 初始化类 可以看到,仅调用("calling")类:MyClass(),就创建了类 MyClass 的实例 mc。返回的对象是你 所调用类的一个实例。当使用函数记法来调用("call")一个类时,解释器就会实例化该对象,并且 调用 Python 所拥有与构造函数最相近的东西(如果你定义了的话)来执行最终的定制工作,比如设置 实例属性,最后将这个实例返回给你。 核心笔记:Python2.2 前后的类和实例 类和类型在 2.2 版本中就统一了,这使得 Python 的行为更像其它面向对象编程语言。任何类或 Edit By Vheavens  Edit By Vheavens  者类型的实例都是这种类型的对象。比如,如果你让 Python 告诉你,类 MyClass 的实例 mc 是否是 类 MyClass 的一个实例。回答是肯定的,Python 不会说谎。同样,它会告诉你零是 integer 类型的 一个实例: >>> mc = MyClass() >>> type(mc) >>> type(0) 但如果你仔细看,比较 MyClass 和 int,你将会发现二者都是类型(type): >>> type(MyClass) >>> type(int) 对比一下,如果在 Python 早于 2.2 版本时,使用经典类,此时类是类对象,实例是实例对象。 在这两个对象类型之间没有任何关系,除了实例的__class__属性引用了被实例化以得到该实例的类。 把 MyClass 在 Python2.1 版本中作为经典类重新定义,并运行相同的调用(注意:int()那时还不具 备工厂功能...它还仅是一个通常的内建函数): >>> type(mc) >>> type(0) >>> >>> type(MyClass) >>> type(int) 为了避免任何混淆,你只要记住当你定义一个类时,你并没有创建一个新的类型,而是仅仅一 个类对象;而对 2.2 及后续版本,当你定义一个(新式的)类后,你已创建了一个新的类型。 13.5.2 __init__() "构造器"方法 当类被调用,实例化的第一步是创建实例对象。一旦对象创建了,Python 检查是否实现了 __init__()方法。默认情况下,如果没有定义(或覆盖)特殊方法__init__(),对实例不会施加任 何特别的操作.任何所需的特定操作,都需要程序员实现__init__(),覆盖它的默认行为。如果 __init__()没有实现,则返回它的对象,实例化过程完毕。 然而,如果__init__()已经被实现,那么它将被调用,实例对象作为第一个参数(self)被传递 进去,像标准方法调用一样。调用类时,传进的任何参数都交给了__init__()。实际中,你可以想 像成这样:把创建实例的调用当成是对构造器的调用。 Edit By Vheavens  Edit By Vheavens  总之,(a)你没有通过调用 new 来创建实例,你也没有定义一个构造器。是 Python 为你创建了 对象; (b) __init__(),是在解释器为你创建一个实例后调用的第一个方法,在你开始使用它之前, 这一步可以让你做些准备工作。 __init__()是很多为类定义的特殊方法之一。其中一些特殊方法是预定义的,缺省情况下,不 进行任何操作,比如__init__(),要定制,就必须对它进行重载,还有些方法,可能要按需要去实 现。本章中,我们会讲到很多这样的特殊方法。你将会经常看到__init__()的使用,在此,就不举 例说明了。 13.5.3 __new__() “构造器”方法 与__init__()相比,__new__()方法更像一个真正的构造器。类型和类在版本 2.2 就统一了, Python 用户可以对内建类型进行派生,因此,需要一种途径来实例化不可变对象,比如,派生字符 串,数字,等等。 在这种情况下,解释器则调用类的__new__()方法,一个静态方法,并且传入的参数是在类实例 化操作时生成的。__new__()会调用父类的__new__()来创建对象(向上代理)。 为何我们认为__new__()比__init__()更像构造器呢?这是因为__new__()必须返回一个合法 的实例,这样解释器在调用__init__()时,就可以把这个实例作为self 传给它。调用父类的__new__() 来创建对象,正像其它语言中使用 new 关键字一样。 __new__()和__init__()在类创建时,都传入了(相同)参数。13.11.3 节中有个例子使用了 __new__()。 13.5.4 __del__() "解构器"方法 同样,有一个相应的特殊解构器(destructor)方法名为__del__()。然而,由于 Python 具有 垃圾对象回收机制(靠引用计数),这个函数要直到该实例对象所有的引用都被清除掉后才会执行。 Python 中的解构器是在实例释放前提供特殊处理功能的方法,它们通常没有被实现,因为实例很少 被显式释放。 举例 在下面的例子中,我们分别创建(并覆盖)__init__()和__del__()构造及解构函数,然后,初 始化类并给同样的对象分配很多别名。id()内建函数可用来确定引用同一对象的三个别名。最后一 步是使用 del 语句清除所有的别名,显示何时,调用了多少次解构器。 Edit By Vheavens  Edit By Vheavens  class C(P): # class declaration 类声明 def __init__(self): # "constructor" 构造器 print 'initialized' def __del__(self): # "destructor" 解构器 P.__del__(self) # call parent destructor print 'deleted' 调用父类解构器来打印 'deleted' >>> c1 = C() # instantiation initialized 实例初始化 >>> c2 = c1 # create additional alias 创建另外一个别名 >>> c3 = c1 # create a third alias 创建第三个别名 >>> id(c1), id(c2), id(c3) # all refer to same object 同一对象所有引用 (11938912, 11938912, 11938912) >>> del c1 # remove one reference 清除一个引用 >>> del c2 # remove another reference 清除另一个引用 >>> del c3 # remove final reference deleted # destructor finally invoked 解构器最 后调用 注意,在上面的例子中,解构器是在类 C 实例所有的引用都被清除掉后,才被调用的,比如, 当引用计数已减少到 0。如果你预期你的__del__()方法会被调用,却实际上没有被调用,这意味着, 你的实例对象由于某些原因,其引用计数不为 0,这可能有别的对它的引用,而你并不知道这些让 你的对象还活着的引用所在。 另外,要注意,解构器只能被调用一次,一旦引用计数为 0,则对象就被清除了。这非常合理, 因为系统中任何对象都只被分配及解构一次。 总结: z 不要忘记首先调用父类的__del__()。 z 调用 del x 不表示调用了 x.__del__() -----前面也看到,它仅仅是减少 x 的引用计数。 z 如果你有一个循环引用或其它的原因,让一个实例的引用逗留不去,该对象的__del__()可 能永远不会被执行。 z __del__()未捕获的异常会被忽略掉(因为一些在__del__()用到的变量或许已经被删除了)。 不要在__del__()中干与实例没任何关系的事情。 z 除非你知道你正在干什么,否则不要去实现__del__()。 z 如果你定义了__del__,并且实例是某个循环的一部分,垃圾回收器将不会终止这个循环— —你需要自已显式调用 del。 核心笔记:跟踪实例 Python 没有提供任何内部机制来跟踪一个类有多少个实例被创建了,或者记录这些实例是些什 么东西。如果需要这些功能,你可以显式加入一些代码到类定义或者__init__()和__del__()中去。 Edit By Vheavens  Edit By Vheavens  最好的方式是使用一个静态成员来记录实例的个数。靠保存它们的引用来跟踪实例对象是很危险的, 因为你必须合理管理这些引用,不然,你的引用可能没办法释放(因为还有其它的引用)!看下面一 个例子: class InstCt(object): count = 0 # count is class attr count 是一个类属性 def __init__(self): # increment count 增加 count InstCt.count += 1 def __del__(self): # decrement count 减少 count InstCt.count -= 1 def howMany(self): # return count 返回 count return InstCt.count >>> a = InstTrack() >>> b = InstTrack() >>> b.howMany() 2 >>> a.howMany() 2 >>> del b >>> a.howMany() 1 >>> del a >>> InstTrack.count 0 13.6 实例属性 实例仅拥有数据属性(方法严格来说是类属性),后者只是与某个类的实例相关联的数据值,并 且可以通过句点属性标识法来访问。这些值独立于其它实例或类。当一个实例被释放后,它的属性 同时也被清除了。 13.6.1 “实例化”实例属性(或创建一个更好的构造器) 设置实例的属性可以在实例创建后任意时间进行,也可以在能够访问实例的代码中进行。构造 器__init()__是设置这些属性的关键点之一。 Edit By Vheavens  Edit By Vheavens  核心笔记:实例属性 能够在“运行时”创建实例属性,是 Python 类的优秀特性之一,从 C++或 Java 转过来的人会被 小小的震惊一下,因为 C++或 Java 中所有属性在使用前都必须明确定义/声明。 Python 不仅是动态类型,而且在运行时,允许这些对象属性的动态创建。这种特性让人爱不释 手。当然,我们必须提醒读者,创建这样的属性时,必须谨慎。 一个缺陷是,属性在条件语句中创建,如果该条件语句块并未被执行,属性也就不存在,而你 在后面的代码中试着去访问这些属性,就会有错误发生。故事的精髓是告诉我们,Python 让你体验 从未用过的特性,但如果你使用它了,你还是要小心为好。 在构造器中首先设置实例属性 构造器是最早可以设置实例属性的地方,因为__init__()是实例创建后第一个被调用的方法。 再没有比这更早的可以设置实例属性的机会了。一旦__init__()执行完毕,返回实例对象,即完成 了实例化过程。 默认参数提供默认的实例安装 在实际应用中,带默认参数的__init__()提供一个有效的方式来初始化实例。在很多情况下, 默认值表示设置实例属性的最常见的情况,如果提供了默认值,我们就没必要显式给构造器传值了。 我们在 11.5.2 节中也提到默认参数的常见好处。需要明白一点,默认参数应当是不变的对象;像 列表(list)和字典(dictionary)这样的可变对象可以扮演静态数据,然后在每个方法调用中来维护 它们的内容。 例 13.1 描述了如何使用默认构造器行为来帮助我们计算在美国一些大都市中的旅馆中寄宿时, 租房总费用。 代码的主要目的是来帮助某人计算出每日旅馆租房费用,包括所有州销售税和房税。缺省为旧 金山附近的普通区域,它有 8.5%销售税及 10%的房间税。每日租房费用没有缺省值,因此在任何实 例被创建时,都需要这个参数。 例 13.1 使用缺省参数进行实例化 (hotel.py) 定义一个类来计算这个假想旅馆租房费用。__init__()构造器对一些实例属性进行初始化。 calcTotal()方法用来决定是计算每日总的租房费用还是计算所有天全部的租房费。 1 class HotelRoomCalc(object): 2 'Hotel room rate calculator' 3 4 def __init__(self, rt, sales=0.085, rm=0.1): Edit By Vheavens  Edit By Vheavens  5 '''HotelRoomCalc default arguments: 6 sales tax == 8.5% and room tax == 10%''' 7 self.salesTax = sales 8 self.roomTax = rm 9 self.roomRate = rt 10 11 def calcTotal(self, days=1): 12 'Calculate total; default to daily rate' 13 daily = round((self.roomRate *14 (1 + self.roomTax + self.salesTax)), 2) 15 return float(days) * daily 设置工作是由__init__()在实例化之后完成的,如上第 4 到 8 行,其余部分的核心代码是 calcTotal()方法,从第 10 到 14 行。__init__()的工作即是设置一些参数值来决定旅馆总的基本租 房费用(不包括住房服务,电话费,或其它偶发事情)。calcTotal()可以计算每日所有费用,如果 提供了天数,那么将计算整个旅程全部的住宿费用。内建的 round()函数可以大约计算出最接近的费 用(两个小数位)。下面是这个类的用法: >>> sfo = HotelRoomCalc(299) # new instance 新的实例 >>> sfo.calcTotal() # daily rate 日租金 354.32 >>> sfo.calcTotal(2) # 2-day rate 2天的租金 708.64 >>> sea = HotelRoomCalc(189, 0.086, 0.058) # new instance 新的实例 >>> sea.calcTotal() 216.22 >>> sea.calcTotal(4) 864.88 >>> wasWkDay = HotelRoomCalc(169, 0.045, 0.02) # new instance 新实例 >>> wasWkEnd = HotelRoomCalc(119, 0.045, 0.02) # new instance 新实例 >>> wasWkDay.calcTotal(5) + wasWkEnd.calcTotal() # 7-day rate 7 天的租金 1026.69 最开始的两个假想例子都是在旧金山(San Francisco), 使用了默认值,然后是在西雅图 (Seattle),这里我们提供了不同的销售税和房间税率。最后一个例子在华盛顿特区 (Washington.D.C)。经过计算更长的假想时间,来扩展通常的用法:停留五个工作日,外加一个周 六,此时有特价,假定是星期天出发回家。 不要忘记,函数所有的灵活性,比如默认参数,也可以应用到方法中去。在实例化时,可变长 度参数也是一个好的特性(当然,这要根据应用的需要) Edit By Vheavens  Edit By Vheavens  __init__()应当返回 None 你也知道,采用函数操作符调用类对象会创建一个类实例,也就是说这样一种调用过程返回的 对象就是实例,下面示例可以看出: >>> class MyClass(object): ... pass >>> mc = MyClass() >>> mc <__main__.MyClass instance at 95d390> 如果定义了构造器,它不应当返回任何对象,因为实例对象是自动在实例化调用后返回的。相 应地,__init__()就不应当返回任何对象(应当为 None);否则,就可能出现冲突,因为只能返回实 例。试着返回非 None 的任何其它对象都会导致 TypeError 异常: >>> class MyClass: ... def __init__(self): ... print 'initialized' ... return 1 ... >>> mc = MyClass() initialized Traceback (innermost last): File "", line 1, in ? mc = MyClass() TypeError: __init__() should return None 13.6.2 查看实例属性 内建函数 dir()可以显示类属性,同样还可以打印所有实例属性: >>> class C(object): ... pass >>> c = C() >>> c.foo = 'roger' >>> c.bar = 'shrubber' >>> dir(c) ['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', '__weakref__', 'bar', 'foo'] 与类相似,实例也有一个__dict__特殊属性(可以调用 vars()并传入一个实例来获取),它是实 例属性构成的一个字典: Edit By Vheavens  Edit By Vheavens  >>> c.__dict__ {'foo': 'roger', 'bar': 'shrubber'} 13.6.3 特殊的实例属性 实例仅有两个特殊属性(见表 13.2)。对于任意对象I: 表 13.2 特殊实例属性 I.__class__ 实例化 I 的类 I.__dict__ I 的属性 现在使用类 C 及其实例 C 来看看这些特殊实例属性: >>> class C(object): # define class 定义类 ... pass ... >>> c = C() # create instance 创建实例 >>> dir(c) # instance has no attributes 实例还没有属性 [] >>> c.__dict__ # yep, definitely no attributes 也没有属性 {} >>> c.__class__ # class that instantiated us 实例化 c 的类 你可以看到,c 现在还没有数据属性,但我们可以添加一些再来检查__dict__属性,看是否添加 成功了: >>> c.foo = 1 >>> c.bar = 'SPAM' >>> '%d can of %s please' % (c.foo, c.bar) '1 can of SPAM please' >>> c.__dict__ {'foo': 1, 'bar': 'SPAM'} __dict__属性由一个字典组成,包含一个实例的所有属性。键是属性名,值是属性相应的数据 值。字典中仅有实例属性,没有类属性或特殊属性。 核心风格:修改__dict__ 对类和实例来说,尽管__dict__属性是可修改的,但还是建议你不要修改这些字典,除非你知 道你的目的。这些修改可能会破坏你的 OOP,造成不可预料的副作用。使用熟悉的句点属性标识来访 Edit By Vheavens  Edit By Vheavens  问及操作属性会更易于接受。需要你直接修改__dict__属性的情况很少,其中之一是你要重载 __setattr__特殊方法。实现__setattr__()本身是一个冒险的经历,满是圈套和陷阱,例如无穷递 归和破坏实例对象。这个故事还是留到下次说吧。 13.6.4 内建类型属性 内建类型也是类,它们有没有像类一样的属性呢?那实例有没有呢?对内建类型也可以使用 dir(),与任何其它对象一样,可以得到一个包含它属性名字的列表: >>> x = 3+0.14j >>> x.__class__ >>> dir(x) ['__abs__', '__add__', '__class__', '__coerce__', '__delattr__', '__div__', '__divmod__', '__doc__', '__eq__', '__float__', '__floordiv__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__int__', '__le__', '__long__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__nonzero__', '__pos__', '__pow__', '__radd__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rmod__', '__rmul__', '__rpow__', '__rsub__', '__rtruediv__', '__setattr__', '__str__', '__sub__', '__truediv__', 'conjugate', 'imag', 'real'] >>> >>> [type(getattr(x, i)) for i in ('conjugate', 'imag', 'real')] [, , ] 既然我们知道了一个复数有什么样的属性,我们就可以访问它的数据属性,调用它的方法了: >>> x.imag 2.0 >>> x.real 1.0 >>> x.conjugate() (1-2j) 试着访问__dict__会失败,因为在内建类型中,不存在这个属性: >>> x.__dict__ Edit By Vheavens  Edit By Vheavens  Traceback (innermost last): File "", line 1, in ? AttributeError: __dict__ 13.6.5 实例属性 vs 类属性 我们已在 13.4.1 节中描述了类数据属性。这里简要提一下,类属性仅是与类相关的数据值,和 实例属性不同,类属性和实例无关。这些值像静态成员那样被引用,即使在多次实例化中调用类, 它们的值都保持不变。不管如何,静态成员不会因为实例而改变它们的值,除非实例中显式改变它 们的值。(实例属性与类属性的比较,类似于自动变量和静态变量,但这只是笼统的类推。在你对自 动变量和静态变量还不是很熟的情况下,不要深究这些。 类和实例都是名字空间。类是类属性的名字空间,实例则是实例属性的。 关于类属性和实例属性,还有一些方面需要指出。你可采用类来访问类属性,如果实例没有同 名的属性的话,你也可以用实例来访问。 访问类属性 类属性可通过类或实例来访问。下面的示例中,类 C 在创建时,带一个 version 属性,这样通 过类对象来访问它是很自然的了,比如,C.version。当实例 c 被创建后,对实例 c 而言,访问 c.version 会失败,不过 Python 首先会在实例中搜索名字 version,然后是类,再就是继承树中的 基类。本例中,version 在类中被找到了: >>> class C(object): # define class 定义类 ... version = 1.2 # static member 静态成员 ... >>> c = C() # instantiation 实例化 >>> C.version # access via class 通过类来访问 1.2 >>> c.version # access via instance 通过实例来访问 1.2 >>> C.version += 0.1 # update (only) via class 通过类(只能这样)来更新 >>> C.version # class access 类访问 1.3 >>> c.version # instance access, which 实例访问它,其值已被改变 1.3 # also reflected change 然而,我们只有当使用类引用 version 时,才能更新它的值,像上面的 C.version 递增语句。 如果尝试在实例中设定或更新类属性会创建一个实例属性 c.version,后者会阻止对类属性 C.versioin 的访问,因为第一个访问的就是 c.version,这样可以对实例有效地“遮蔽”类属性 Edit By Vheavens  Edit By Vheavens  C.version,直到 c.version 被清除掉。 从实例中访问类属性须谨慎 与通常 Python 变量一样,任何对实例属性的赋值都会创建一个实例属性(如果不存在的话)并 且对其赋值。如果类属性中存在同名的属性,有趣的副作用即产生。(经典类和新式类都存在) >>> class Foo(object): ... x = 1.5 ... >>> foo = Foo() >>> foo.x 1.5 >>> foo.x = 1.7 # try to update class attr 试着更新类属性 >>> foo.x # looks good so far... 现在看起来还不错 1.7 >>> Foo.x # nope, just created a new inst attr 呵呵,没有变,只是创建了一个新的实 例属性 1.5 在上面的代码片断中,创建了一个名为 version 的新实例属性,它覆盖了对类属性的引用。然 而,类属性本身并没有受到伤害,仍然存在于类域中,还可以通过类属性来访问它,如上例可以看 到的。好了,那么如果把这个新的 version 删除掉,会怎么样呢?为了找到结论,我们将使用 del 语句删除 c.version。 >>> del foo.x # delete instance attribute 删除实例属性 >>> foo.x # can now access class attr again 又可以访问到类属性 1.5 所以,给一个与类属性同名的实例属性赋值,我们会有效地“隐藏”类属性,但一旦我们删除 了这个实例属性,类属性又重见天日。现在再来试着更新类属性,但这次,我们只尝试一下“无辜” 的增量动作: >>> foo.x += .2 # try to increment class attr 试着增加类属性 >>> foo.x 1.7 >>> Foo.x # nope, same thing 呵呵,照旧 1.5 还是没变。我们同样创建了一个新的实例属性,类属性原封不动。(深入理解Python 相关知识: 属性已存于类字典[__dict__]中。通过赋值,其被加入到实例的__dict__中了。)赋值语句右边的表 达式计算出原类的变量,增加 0.2,并且把这个值赋给新创建的实例属性。注意下面是一个等价的赋 Edit By Vheavens  Edit By Vheavens  值方式,但它可能更加清楚些: foo.x = Foo.x + 0.2 但...在类属性可变的情况下,一切都不同了: >>> class Foo(object): ... x = {2003: 'poe2'} ... >>> foo = Foo() >>> foo.x {2003: 'poe2'} >>> foo.x[2004] = 'valid path' >>> foo.x {2003: 'poe2', 2004: 'valid path'} >>> Foo.x # it works!!! 生效了 {2003: 'poe2', 2004: 'valid path'} >>> del foo.x # no shadow so cannot delete 没有遮蔽所以不能删除掉 Traceback (most recent call last): File "", line 1, in ? del foo.x AttributeError: x >>> 类属性持久性 静态成员,如其名所言,任凭整个实例(及其属性)的如何进展,它都不理不采(因此独立于 实例)。同时,当一个实例在类属性被修改后才创建,那么更新的值就将生效。类属性的修改会影响 到所有的实例: >>> class C(object): ... spam = 100 # class attribute 类属性 ... >>> c1 = C() # create an instance 创建一个实例 >>> c1.spam # access class attr thru inst. 通过实例访问类属性 100 >>> C.spam += 100 # update class attribute 更新类属性 >>> C.spam # see change in attribute 查看属性值改变 200 >>> c1.spam # confirm change in attribute 在实例中验证属性值改变 200 >>> c2 = C() # create another instance 创建另一个实例 >>> c2.spam # verify class attribute 验证类属性 Edit By Vheavens  Edit By Vheavens  200 >>> del c1 # remove one instance 删除一个实例 >>> C.spam += 200 # update class attribute again 再次更新类属性 >>> c2.spam # verify that attribute changed 验证那个属性值改变 400 核心提示:使用类属性来修改自身(不是实例属性) 正如上面所看到的那样,使用实例属性来试着修改类属性是很危险的。原因在于实例拥有它们 自已的属性集,在 Python 中没有明确的方法来指示你想要修改同名的类属性,比如,没有 global 关键字可以用来在一个函数中设置一个全局变量(来代替同名的局部变量)。修改类属性需要使用类 名,而不是实例名。 13.7 从这里开始校对----------绑定和方法调用 现在我们需要再次阐述 Python 中绑定(binding)的概念,它主要与方法调用相关连。我们先 来回顾一下与方法相关的知识。首先,方法仅仅是类内部定义的函数。(这意味着方法是类属性而不 是实例属性)。 其次,方法只有在其所属的类拥有实例时,才能被调用。当存在一个实例时,方法才被认为是 绑定到那个实例了。没有实例时方法就是未绑定的。 最后,任何一个方法定义中的第一个参数都是变量 self,它表示调用此方法的实例对象。 核心笔记:self 是什么? self 变量用于在类实例方法中引用方法所绑定的实例。因为方法的实例在任何方法调用中总是 作为第一个参数传递的,self 被选中用来代表实例。你必须在方法声明中放上 self(你可能已经注 意到了这点),但可以在方法中不使用实例(self)。如果你的方法中没有用到 self , 那么请考虑创建 一个常规函数,除非你有特别的原因。毕竟,你的方法代码没有使用实例,没有与类关联其功能, 这使得它看起来更像一个常规函数。在其它面向对象语言中,self 可能被称为 this。 13.7.1 调用绑定方法 方法,不管绑定与否,都是由相同的代码组成的。唯一的不同在于是否存在一个实例可以调用 此方法。在很多情况下,程序员调用的都是一个绑定的方法。假定现在有一个 MyClass 类和此类的 一个实例 mc,而你想调用 MyClass.foo()方法。因为已经有一个实例,你只需要调用 mc.foo()就可 以。记得 self 在每一个方法声明中都是作为第一个参数传递的。当你在实例中调用一个绑定的方法 时,self 不需要明确地传入了。这算是"必须声明 self 作为第一个参数"对你的报酬。当你还没有 一个实例并且需要调用一个非绑定方法的时候你必须传递 self 参数。 Edit By Vheavens  Edit By Vheavens  13.7.2 调用非绑定方法 调用非绑定方法并不经常用到。需要调用一个还没有任何实例的类中的方法的一个主要的场景 是:你在派生一个子类,而且你要覆盖父类的方法,这时你需要调用那个父类中想要覆盖掉的构造方 法。这里是一个本章前面介绍过的例子: class EmplAddrBookEntry(AddrBookEntry): 'Employee Address Book Entry class' # 员工地址记录条目 def __init__(self, nm, ph, em): AddrBookEntry.__init__(self, nm, ph) self.empid = id self.email = em EmplAddrBookEntry 是 AddrBookEntry 的子类,我们重载了构造器__init__()。我们想尽可能多 地重用代码, 而不是去从父类构造器中剪切,粘贴代码。这样做还可以避免 BUG 传播,因为任何修 复都可以传递给子类。这正是我们想要的 --- 没有必要一行一行地复制代码。只需要能够调用父类 的构造器即可,但该怎么做呢? 我们在运行时没有 AddrBookEntry 的实例。那么我们有什么呢?我们有一个EmplAddrBookEntry 的实例,它与 AddrBookEntry 是那样地相似,我们难道不能用它代替呢?当然可以! 当一个 EmplAddrBookEntry 被实例化,并且调用 __init__() 时,其与 AddrBookEntry 的实例 只有很少的差别,主要是因为我们还没有机会来自定义我们的 EmplAddrBookEntry 实例,以使它与 AddrBookEntry 不同。 这是调用非绑定方法的最佳地方了。我们将在子类构造器中调用父类的构造器并且明确地传递 (父类)构造器所需要的 self 参数(因为我们没有一个父类的实例)。子类中 __init__() 的第一 行就是对父类__init__()的调用。我们通过父类名来调用它,并且传递给它 self 和其他所需要的 参数。一旦调用返回,我们就能定义那些与父类不同的仅存在我们的(子)类中的(实例)定制。 13.8 静态方法和类方法 静态方法和类方法在 Python2.2 中引入。经典类及新式(new-style)类中都可以使用它。一对内 建函数被引入,用于将作为类定义的一部分的某一方法声明“标记”(tag),“强制类型转换”(cast) 或者“转换”(convert)为这两种类型的方法之一。 如果你有一定的 C++或者 Java 经验,静态方法和这些语言中的是一样的。它们仅是类中的函数(不 Edit By Vheavens  Edit By Vheavens  需要实例)。事实上,在静态方法加入到 Python 之前,用户只能在全局名字空间中创建函数,作为 这种特性的替代实现 - 有时在这样的函数中使用类对象来操作类(或者是类属性)。使用模块函数 比使用静态类方法更加常见。 回忆一下,通常的方法需要一个实例(self)作为第一个参数,并且对于(绑定的)方法调用来 说,self 是自动传递给这个方法的。而对于类方法而言,需要类而不是实例作为第一个参数,它是 由解释器传给方法。类不需要特别地命名, 类似 self,不过很多人使用 cls 作为变量名字。 13.8.1 staticmethod()和 classmethod()内建函数 现在让我们看一下在经典类中创建静态方法和类方法的一些例子(你也可以把它们用在新式类 中): class TestStaticMethod: def foo(): print 'calling static method foo()' foo = staticmethod(foo) class TestClassMethod: def foo(cls): print 'calling class method foo()' print 'foo() is part of class:', cls.__name__ foo = classmethod(foo) 对应的内建函数被转换成它们相应的类型,并且重新赋值给了相同的变量名。如果没有调用这 两个函数,二者都会在 Python 编译器中产生错误,显示需要带 self 的常规方法声明。现在, 我们 可以通过类或者实例调用这些函数....这没什么不同: >>> tsm = TestStaticMethod() >>> TestStaticMethod.foo() calling static method foo() >>> tsm.foo() calling static method foo() >>> >>> tcm = TestClassMethod() >>> TestClassMethod.foo() calling class method foo() foo() is part of class: TestClassMethod >>> tcm.foo() Edit By Vheavens  Edit By Vheavens  calling class method foo() foo() is part of class: TestClassMethod 13.8.2 使用函数修饰符 现在,看到像 foo=staticmethod(foo)这样的代码会刺激一些程序员。很多人对这样一个没意义 的语法感到心烦,即使 van Rossum曾指出过,它只是临时的,有待社区对些语义进行处理。在第 11 章“函数”的 11.3.6 节中,我们了解了函数修饰符,一种在 Python2.4 中加入的新特征。你可以用 它把一个函数应用到另个函数对象上, 而且新函数对象依然绑定在原来的变量。我们正是需要它来 整理语法。通过使用 decorators,我们可以避免像上面那样的重新赋值: class TestStaticMethod: @staticmethod def foo(): print 'calling static method foo()' class TestClassMethod: @classmethod def foo(cls): print 'calling class method foo()' print 'foo() is part of class:', cls.__name__ 13.9 组合 一个类被定义后,目标就是要把它当成一个模块来使用,并把这些对象嵌入到你的代码中去, 同其它数据类型及逻辑执行流混合使用。有两种方法可以在你的代码中利用类。第一种是组合 (composition)。就是让不同的类混合并加入到其它类中,来增加功能和代码重用性。你可以在一个 大点的类中创建你自已的类的实例,实现一些其它属性和方法来增强对原来的类对象。另一种方法 是通过派生,我们将在下一节中讨论它. 举例来说,让我们想象一个对本章一开始创建的地址本类的加强性设计。如果在设计的过程中, 为 names,addresses 等等创建了单独的类。那么最后我们可能想把这些工作集成到 AddrBookEntry 类中去,而不是重新设计每一个需要的类。这样就节省了时间和精力,而且最后的结果是容易维护 的代码 --- 一块代码中的 bugs 被修正,将反映到整个应用中。 这样的类可能包含一个 Name 实例,以及其它的像 StreetAddress, Phone(home, work, telefacsimile, pager, mobile, 等等),Email (home, work, 等等。),还可能需要一些Date 实 例(birthday,wedding,anniversary,等等)。下面是一个简单的例子: Edit By Vheavens  Edit By Vheavens  class NewAddrBookEntry(object): # class definition 类定义 'new address book entry class' def __init__(self, nm, ph): # define constructor 定义构造器 self.name = Name(nm) # create Name instance 创建 Name 实例 self.phone = Phone(ph) # create Phone instance 创建 Phone 实例 print 'Created instance for:', self.name NewAddrBookEntry 类由它自身和其它类组合而成。这就在一个类和其它组成类之间定义了一种 “has-a / 有一个”的关系。比如,我们的NewAddrBookEntry 类“有一个” Name 类实例和一个 Phone 实例。 创建复合对象就可以实现这些附加的功能,并且很有意义,因为这些类都不相同。每一个类管 理它们自己的名字空间和行为。不过当对象之间有更接近的关系时,派生的概念可能对你的应用程 序来说更有意义,特别是当你需要一些相似的对象,但却有少许不同功能的时候。 13.10 子类和派生 当类之间有显著的不同,并且(较小的类)是较大的类所需要的组件时,组合表现得很好,但当 你设计“相同的类但有一些不同的功能”时,派生就是一个更加合理的选择了。 OOP 的更强大方面之一是能够使用一个已经定义好的类,扩展它或者对其进行修改,而不会影响 系统中使用现存类的其它代码片段。OOD允许类特征在子孙类或子类中进行继承。这些子类从基类(或 称祖先类,超类)继承它们的核心属性。而且,这些派生可能会扩展到多代。在一个层次的派生关 系中的相关类(或者是在类树图中垂直相邻)是父类和子类关系。从同一个父类派生出来的这些类 (或者是在类树图中水平相邻)是同胞关系。父类和所有高层类都被认为是祖先。 使用前一节中的例子,如果我们必须创建不同类型的地址本。即,不仅仅是创建地址本的多个 实例,在这种情况下,所有对象几乎是相同的。如果我们希望 EmplAddrBookEntry 类中包含更多与 工作有关的属性,如员工 ID 和 e-mail 地址?这跟 PersonalAddrBookEntry 类不同,它包含更多基 于家庭的信息,比如家庭地址,关系,生日等等。 两种情况下,我们都不想到从头开始设计这些类,因为这样做会重复创建通用的 AddressBook 类时的操作。包含 AddressBook 类所有的特征和特性并加入需要的定制特性不是很好吗?这就是类 派生的动机和要求。 13.10.1 创建子类 Edit By Vheavens  Edit By Vheavens  创建子类的语法看起来与普通(新式)类没有区别,一个类名,后跟一个或多个需要从其中派生 的父类: class SubClassName (ParentClass1[, ParentClass2, ...]): 'optional class documentation string' class_suite 如果你的类没有从任何祖先类派生,可以使用 object 作为父类的名字。经典类的声明唯一不同 之处在于其没有从祖先类派生---此时,没有圆括号: class ClassicClassWithoutSuperclasses: pass 至此,我们已经看到了一些类和子类的例子,下面还有一个简单的例子: class Parent(object): # define parent class 定义父类 def parentMethod(self): print 'calling parent method' class Child(Parent): # define child class 定义子类 def childMethod(self): print 'calling child method' >>> p = Parent() # instance of parent 父类的实例 >>> p.parentMethod() calling parent method >>> >>> c = Child() # instance of child 子类的实例 >>> c.childMethod() # child calls its method 子类调用它的方法 calling child method >>> c.parentMethod() # calls parent's method 调用父类的方法 calling parent method 13.11 继承 继承描述了基类的属性如何“遗传”给派生类。一个子类可以继承它的基类的任何属性,不管 是数据属性还是方法。 举个例子如下。P 是一个没有属性的简单类。C 从 P 继承而来(因此是它的子类),也没有属性: class P(object): # parent class 父类 pass class C(P): # child class 子类 Edit By Vheavens  Edit By Vheavens  pass >>> c = C() # instantiate child 实例化子类 >>> c.__class__ # child "is a" parent 子类“是一个”父类 >>> C.__bases__ # child's parent class(es) 子类的父类 (,) 因为 P 没有属性,C 没有继承到什么。下面我们给 P 添加一些属性: class P: # parent class 父类 'P class' def __init__(self): print 'created an instance of', \ self.__class__.__name__ class C(P): # child class 子类 pass 现在所创建的 P 有文档字符串(__doc__)和构造器,当我们实例化 P 时它被执行,如下面的 交互会话所示: >>> p = P() # parent instance 父类实例 created an instance of P >>> p.__class__ # class that created us 显示 p 所属的类名 >>> P.__bases__ # parent's parent class(es) 父类的父类 (,) >>> P.__doc__ # parent's doc string 父类的文档字符串 'P class' “created an instance”是由__init__()直接输出的。我们也可显示更多关于父类的信息。我 们现在来实例化 C,展示 __init__()(构造)方法在执行过程中是如何继承的: >>> c = C() # child instance 子类实例 created an instance of C >>> c.__class__ # class that created us 显示 c 所属的类名 >>> C.__bases__ # child's parent class(es) 子类的父类 (,) >>> C.__doc__ # child's doc string 子类的文档字符串 >>> Edit By Vheavens  Edit By Vheavens  C 没有声明__init__()方法,然而在类 C 的实例 c 被创建时,还是会有输出信息。原因在于 C 继 承了 P 的__init__()。__bases__元组列出了其父类 P。需要注意的是文档字符串对类,函数/方法, 还有模块来说都是唯一的,所以特殊属性__doc__不会从基类中继承过来。 13.11.1 __bases__类属性 在第 13.4.4 节中,我们概要地介绍了__bases__类属性,对任何(子)类,它是一个包含其父类 (parent)的集合的元组。注意,我们明确指出“父类”是相对所有基类(它包括了所有祖先类) 而言的。那些没有父类的类,它们的__bases__属性为空。下面我们看一下如何使用__bases__的。 >>> class A(object): pass # define class A 定义类 A ... >>> class B(A): pass # subclass of A A 的子类 ... >>> class C(B): pass # subclass of B (and indirectly, A) B 的子类(A 的间接子类) ... >>> class D(A, B): pass # subclass of A and B A,B 的子类 ... >>> A.__bases__ (,) >>> C.__bases__ (,) >>> D.__bases__ (, ) 在上面的例子中,尽管C是A和B的子类(通过 B 传递继承关系),但 C 的父类是 B,这从它的 声明中可以看出,所以,只有 B 会在 C.__bases__中显示出来。另一方面,D 是从两个类A和B中继 承而来的。(多重继承参见 13.11.4) 13.11.2 通过继承覆盖(Overriding)方法 我们在 P 中再写一个函数,然后在其子类中对它进行覆盖。 class P(object): def foo(self): print 'Hi, I am P-foo()' >>> p = P() Edit By Vheavens  Edit By Vheavens  >>> p.foo() Hi, I am P-foo() 现在来创建子类 C,从父类 P 派生: class C(P): def foo(self): print 'Hi, I am C-foo()' >>> c = C() >>> c.foo() Hi, I am C-foo() 尽管 C 继承了 P 的 foo()方法,但因为 C 定义了它自已的 foo()方法,所以 P 中的 foo() 方法 被覆盖。覆盖方法的原因之一是,你的子类可能需要这个方法具有特定或不同的功能。所以,你接 下来的问题肯定是:“我还能否调用那个被我覆盖的基类方法呢?” 答案是肯定的,但是这时就需要你去调用一个未绑定的基类方法,明确给出子类的实例,例如 下边: >>> P.foo(c) Hi, I am P-foo() 注意,我们上面已经有了一个 P 的实例 p,但上面的这个例子并没有用它。我们不需要 P 的实 例调用 P 的方法,因为已经有一个 P 的子类的实例 c 可用。典型情况下,你不会以这种方式调用父类 方法,你会在子类的重写方法里显式地调用基类方法。 class C(P): def foo(self): P.foo(self) print 'Hi, I am C-foo()' 注意,在这个(未绑定)方法调用中我们显式地传递了 self. 一个更好的办法是使用 super() 内建方法: class C(P): def foo(self): super(C, self).foo() print 'Hi, I am C-foo()' super()不但能找到基类方法,而且还为我们传进 self,这样我们就不需要做这些事了。现在我 们只要调用子类的方法,它会帮你完成一切: Edit By Vheavens  Edit By Vheavens  >>> c = C() >>> c.foo() Hi, I am P-foo() Hi, I am C-foo() 核心笔记:重写__init__不会自动调用基类的__init__ 类似于上面的覆盖非特殊方法,当从一个带构造器 __init()__的类派生,如果你不去覆盖 __init__(),它将会被继承并自动调用。但如果你在子类中覆盖了__init__(),子类被实例化时, 基类的__init__()就不会被自动调用。这可能会让了解 JAVA 的朋友感到吃惊。 class P(object): def __init__(self): print "calling P's constructor" class C(P): def __init__(self): print "calling C's constructor" >>> c = C() calling C's constructor 如果你还想调用基类的 __init__(),你需要像上边我们刚说的那样,明确指出,使用一个子 类的实例去调用基类(未绑定)方法。相应地更新类 C,会出现下面预期的执行结果: class C(P): def __init__(self): P.__init__(self) print "calling C's constructor" >>> c = C() calling P's constructor calling C's constructor 上边的例子中,子类的__init__()方法首先调用了基类的的__init__()方法。这是相当普遍(不 是强制)的做法,用来设置初始化基类,然后可以执行子类内部的设置。这个规则之所以有意义的 原因是,你希望被继承的类的对象在子类构造器运行前能够很好地被初始化或作好准备工作,因为它 (子类)可能需要或设置继承属性。 对 C++熟悉的朋友,可能会在派生类构造器声明时,通过在声明后面加上冒号和所要调用的所有 基类构造器这种形式来调用基类构造器。而在 JAVA 中,不管程序员如何处理,子类构造器都会去调 用基类的的构造器。 Python 使用基类名来调用类方法,对应在 JAVA 中,是用关键字 super 来实现的,这就是 super() 内建函数引入到 Python 中的原因,这样你就可以“依葫芦画瓢”了: class C(P): Edit By Vheavens  Edit By Vheavens  def __init__(self): super(C, self).__init__() print "calling C's constructor" 使用 super()的漂亮之处在于,你不需要明确给出任何基类名字...“跑腿事儿”,它帮你干了! 使用 super()的重点,是你不需要明确提供父类。这意味着如果你改变了类继承关系,你只需要改一 行代码(class 语句本身)而不必在大量代码中去查找所有被修改的那个类的名字。 13.11.3 从标准类型派生 经典类中,一个最大的问题是,不能对标准类型进行子类化。幸运的是,在 2.2 以后的版本中, 随着类型(types)和类(class)的统一和新式类的引入, 这一点已经被修正。下面,介绍两个子 类化 Python 类型的相关例子,其中一个是可变类型,另一个是不可变类型。 不可变类型的例子 假定你想在金融应用中,应用一个处理浮点数的子类。每次你得到一个贷币值(浮点数给出的), 你都需要通过四舍五入,变为带两位小数位的数值。(当然,Decimal类比起标准浮点类型来说是个 用来精确保存浮点值的更佳方案,但你还是需要[有时候]对其进行舍入操作!)你的类开始可以 这样写: class RoundFloat(float): def __new__(cls, val): return float.__new__(cls, round(val, 2)) 我们覆盖了__new__()特殊方法来定制我们的对象,使之和标准 Python 浮点数(float)有一些 区别:我们使用round()内建函数对原浮点数进行舍入操作,然后实例化我们的float,RoundFloat。 我们是通过调用父类的构造器来创建真实的对象的,float.__new__()。注意,所有的__new()__方 法都是类方法,我们要显式传入类传为第一个参数,这类似于常见的方法如__init__()中需要的self。 现在的例子还非常简单,比如,我们知道有一个float,我们仅仅是从一种类型中派生而来等等. 通常情况下,最好是使用 super()内建函数去捕获对应的父类以调用它的__new()__方法,下面,对 它进行这方面的修改: class RoundFloat(float): def __new__(cls, val): return super(RoundFloat, cls).__new__(cls, round(val, 2)) 这个例子还远不够完整,所以,请留意本章我们将使它有更好的表现。下面是一些样例输出: Edit By Vheavens  Edit By Vheavens  >>> RoundFloat(1.5955) 1.6 >>> RoundFloat(1.5945) 1.59 >>> RoundFloat(-1.9955) -2.0 可变类型的例子 子类化一个可变类型与此类似,你可能不需要使用__new__() (或甚至__init__()),因为通常 设置不多。一般情况下,你所继承到的类型的默认行为就是你想要的。下例中,我们简单地创建一 个新的字典类型,它的 keys()方法会自动排序结果: class SortedKeyDict(dict): def keys(self): return sorted(super( SortedKeyDict, self).keys()) 回忆一下,字典(dictionary)可以由dict(),dict(mapping),dict(sequence_of_2_tuples), 或者 dict(**kwargs)来创建,看看下面使用新类的例子: d = SortedKeyDict((('zheng-cai', 67), ('hui-jun', 68),('xin-yi', 2))) print 'By iterator:'.ljust(12), [key for key in d] print 'By keys():'.ljust(12), d.keys() 把上面的代码全部加到一个脚本中,然后运行,可以得到下面的输出: By iterator: ['zheng-cai', 'xin-yi', 'hui-jun'] By keys(): ['xin-yi', 'hui-jun', 'zheng-cai'] 在上例中,通过 keys 迭代过程是以散列顺序的形式,而使用我们(重写的)keys()方法则将 keys 变为字母排序方式了。 一定要谨慎,而且要意识到你正在干什么。如果你说,“你的方法调用super()过于复杂”,取而 代之的是,你更喜欢 keys()简简单单(也容易理解)....,像这样: def keys(self): return sorted(self.keys()) 这是本章后面的练习 13-19。 Edit By Vheavens  Edit By Vheavens  13.11.4 多重继承 同 C++一样,Python 允许子类继承多个基类。这种特性就是通常所说的多重继承。概念容易, 但最难的工作是,如何正确找到没有在当前(子)类定义的属性。当使用多重继承时,有两个不同 的方面要记住。首先,还是要找到合适的属性。另一个就是当你重写方法时,如何调用对应父类方 法以“发挥他们的作用”,同时,在子类中处理好自己的义务。我们将讨论两个方面,但侧重后者, 讨论方法解析顺序。 方法解释顺序(MRO) 在 Python 2.2 以前的版本中,算法非常简单:深度优先,从左至右进行搜索,取得在子类中使 用的属性。其它 Python 算法只是覆盖被找到的名字,多重继承则取找到的第一个名字。 由于类,类型和内建类型的子类,都经过全新改造, 有了新的结构,这种算法不再可行. 这样 一种新的 MRO 算法被开发出来,在 2.2 版本中初次登场,是一个好的尝试,但有一个缺陷(看下面 的核心笔记)。这在2.3 版本中立即被修改,也就是今天还在使用的版本。 精确顺序解释很复杂,超出了本文的范畴,但你可以去阅读本节后面的参考书目提到的有关内 容。这里提一下,新的查询方法是采用广度优先,而不是深度优先。 核心笔记:Python 2.2 使用一种唯一但不完善的 MRO Python 2.2 是首个使用新式 MRO 的版本,它必须取代经典类中的算法,原因在上面已谈到过。 在 2.2 版本中,算法基本思想是根据每个祖先类的继承结构,编译出一张列表,包括搜索到的类, 按策略删除重复的。然而,在 Python 核心开发人员邮件列表中,有人指出,在维护单调性方面失败 过(顺序保存),必须使用新的 C3 算法替换,也就是从 2.3 版开始使用的新算法。 下面的示例,展示经典类和新式类中,方法解释顺序有什么不同。 简单属性查找示例 下面这个例子将对两种类的方案不同处做一展示。脚本由一组父类,一组子类,还有一个子孙 类组成。 class P1: #(object): # parent class 1 父类 1 def foo(self): print 'called P1-foo()' class P2: #(object): # parent class 2 父类 2 def foo(self): Edit By Vheavens  Edit By Vheavens  print 'called P2-foo()' def bar(self): print 'called P2-bar()' class C1(P1, P2): # child 1 der. from P1, P2 #子类 1,从 P1,P2 派生 pass class C2(P1, P2): # child 2 der. from P1, P2 #子类 2,从 P1,P2 派生 def bar(self): print 'called C2-bar()' class GC(C1, C2): # define grandchild class #定义子孙类 pass # derived from C1 and C2 #从 C1,C2 派生 图 13-2 父类,子类及子孙类的关系图,还有它们各自定义的方法 在图 13-2 中,我们看到父类,子类及子孙类的关系。P1中定义了 foo(),P2定义了 foo()和 bar(), C2 定义了 bar()。下面举例说明一下经典类和新式类的行为。 经典类 首先来使用经典类。通过在交互式解释器中执行上面的声明,我们可以验证经典类使用的解释 顺序,深度优先,从左至右: >>> gc = GC() >>> gc.foo() # GC ==> C1 ==> P1 called P1-foo() >>> gc.bar() # GC ==> C1 ==> P1 ==> P2 called P2-bar() 当调用 foo()时,它首先在当前类(GC)中查找。如果没找到,就向上查找最亲的父类,C1。查找 未遂,就继续沿树上访到父类 P1,foo()被找到。 Edit By Vheavens  Edit By Vheavens  同样,对bar()来说,它通过搜索GC,C1,P1然后在 P2 中找到。因为使用这种解释顺序的缘故, C2.bar()根本就不会被搜索了。 现在,你可能在想,“我更愿意调用C2 的 bar()方法,因为它在继承树上和我更亲近些,这样才 会更合适。”在这种情况下,你当然还可以使用它,但你必须调用它的合法的全名,采用典型的非绑 定方式去调用,并且提供一个合法的实例: >>> C2.bar(gc) called C2-bar() 新式类 取消类 P1 和类 P2 声明中的对(object)的注释,重新执行一下。新式方法的查询有一些不同: >>> gc = GC() >>> gc.foo() # GC ==> C1 ==> C2 ==> P1 called P1-foo() >>> gc.bar() # GC ==> C1 ==> C2 called C2-bar() 与沿着继承树一步一步上溯不同,它首先查找同胞兄弟,采用一种广度优先的方式。当查找 foo(),它检查 GC,然后是 C1 和 C2,然后在 P1 中找到。如果 P1 中没有,查找将会到达 P2。foo() 的底线是,包括经典类和新式类都会在 P1 中找到它,然而它们虽然是同归,但殊途! 然而,bar()的结果是不同的。它搜索 GC 和 C1,紧接着在 C2 中找到了。这样,就不会再继续搜 索到祖父 P1 和 P2。这种情况下,新的解释方式更适合那种要求查找 GC 更亲近的 bar()的方案。当 然,如果你还需要调用上一级,只要按前述方法,使用非绑定的方式去做,即可。 >>> P2.bar(gc) called P2-bar() 新式类也有一个__mro__属性,告诉你查找顺序是怎样的: >>> GC.__mro__ (, , , , , ) 菱形效应为难 MRO Edit By Vheavens  Edit By Vheavens  经典类方法解释不会带来很多问题。它很容易解释,并理解。大部分类都是单继承的,多重继 承只限用在对两个完全不相关的类进行联合。这就是术语 mixin 类(或者“mix-ins”)的由来。 为什么经典类 MRO 会失败 在版本 2.2 中,类型与类的统一,带来了一个新的“问题”,波及所有从 object(所有类型的祖 先类)派生出来的(根)类,一个简单的继承结构变成了一个菱形。从 Guido van Rossum 的文章 中得到下面的灵感,打个比方,你有经典类B和C,C覆盖了构造器,B 没有,D从B和C继承而来: class B: pass class C: def __init__(self): print "the default constructor" class D(B, C): pass 当我们实例化 D,得到: >>> d = D() the default constructor 图 13.3 为 B,C 和 D 的类继承结构,现在把代码改为采用新式类的方式,问题也就产生了: class B(object): pass class C(object): def __init__(self): print "the default constructor" Edit By Vheavens  Edit By Vheavens  图 13.3 继承的问题是由于在新式类中,需要出现基类,这样就在继承结构中,形成了一个 菱形。D的实例上溯时,不应当错过C,但不能两次上溯到A(因为B和C都从A派生)。去读读Guido van Rossum 的文章中有关"协作方法"的部分,可以得到更深地理解。 代码中仅仅是在两个类声明中加入了(object),对吗?没错,但从图中,你可以看出,继承结 构已变成了一个菱形;真正的问题就存在于 MRO 了。如果我们使用经典类的 MRO,当实例化 D 时,不 再得到 C.__init__()之结果.....而是得到 object.__init__()!这就是为什么 MRO 需要修改的真正 原因。 尽管我们看到了,在上面的例子中,类 GC 的属性查找路径被改变了,但你不需要担心会有大量 的代码崩溃。经典类将沿用老式 MRO,而新式类将使用它自己的 MRO。还有,如果你不需要用到新 式类中的所有特性,可以继续使用经典类进行开发,不会有问题的。 总结 经典类,使用深度优先算法。因为新式类继承自 object,新的菱形类继承结构出现,问题也就 接着而来了,所以必须新建一个 MRO。 你可以在下面的链接中读在更多有关新式类、MRO 的文章: Guido van Rossum 的有关类型和类统一的文章: http://www.python.org/download/releases/2.2.3/descrintro PEP 252:使类型看起来更像类 http://www.python.org/doc/peps/pep-0252 “Python 2.2 新亮点” 文档 http://www.python.org/doc/2.2.3/whatsnew 论文:Python 2.3 方法解释顺序 http://python.org/download/releases/2.3/mro/ Edit By Vheavens  Edit By Vheavens  13.12 类、实例和其他对象的内建函数 13.12.1 issubclass() issubclass() 布尔函数判断一个类是另一个类的子类或子孙类。它有如下语法: issubclass(sub, sup) issubclass() 返回 True 的情况:给出的子类sub 确实是父类 sup 的一个子类(反之,则为False)。 这个函数也允许“不严格”的子类,意味着,一个类可视为其自身的子类,所以,这个函数如果当 sub 就是 sup,或者从 sup 派生而来,则返回 True。(一个“严格的”子类是严格意义上的从一个类 派生而来的子类。) 从 Python 2.3开始,issubclass()的第二个参数可以是可能的父类组成的tuple(元组),这时, 只要第一个参数是给定元组中任何一个候选类的子类时,就会返回 True。 13.12.2 isinstance() isinstance() 布尔函数在判定一个对象是否是另一个给定类的实例时,非常有用。它有如下 语法: isinstance(obj1, obj2) isinstance()在 obj1 是类 obj2 的一个实例,或者是 obj2 的子类的一个实例时,返回 True (反之,则为 False),看下面的例子: >>> class C1(object): pass ... >>> class C2(object): pass ... >>> c1 = C1() >>> c2 = C2() >>> isinstance(c1, C1) True >>> isinstance(c2, C1) False >>> isinstance(c1, C2) False >>> isinstance(c2, C2) True >>> isinstance(C2, c2) Traceback (innermost last): Edit By Vheavens  Edit By Vheavens  File "", line 1, in ? isinstance(C2, c2) TypeError: second argument must be a class 注意:第二个参数应当是类;不然,你会得到一个 TypeError。但如果第二个参数是一个类型对 象,则不会出现异常。这是允许的,因为你也可以使用 isinstance()来检查一个对象 obj1 是否是 obj2 的类型,比如: >>> isinstance(4, int) True >>> isinstance(4, str) False >>> isinstance('4', str) True 如果你对 Java 有一定的了解,那么你可能知道 Java 中有个等价函数叫 instanceof(),但由于 性能上的原因,instanceof()并不被推荐使用。调用 Python 的 isinstance()不会有性能上的问题, 主要是因为它只用来来快速搜索类族集成结构,以确定调用者是哪个类的实例,还有更重要的是, 它是用 C 写的! 同 issubclass()一样,isinstance()也可以使用一个元组(tuple)作为第二个参数。这个特 性是从 Python 2.2 版本中引进的。如果第一个参数是第二个参数中给定元组的任何一个候选类型 或类的实例时,就会返回 True。你还可以在 595 页,第 13.16.1 节中了解到更多有 isinstance()的 内容。 13.12.3 hasattr(), getattr(),setattr(), delattr() *attr()系列函数可以在各种对象下工作,不限于类(class)和实例(instances)。然而,因 为在类和实例中使用极其频繁,就在这里列出来了。需要说明的是,当使用这些函数时,你传入你 正在处理的对象作为第一个参数,但属性名,也就是这些函数的第二个参数,是这些属性的字符串 名字。换句话说,在操作 obj.attr 时,就相当于调用*attr(obj,'attr'....)系列函数------下面 的例子讲得很清楚。 hasattr()函数是 Boolean 型的,它的目的就是为了决定一个对象是否有一个特定的属性,一 般用于访问某属性前先作一下检查。getattr()和 setattr()函数相应地取得和赋值给对象的属性, getattr()会在你试图读取一个不存在的属性时,引发 AttributeError 异常,除非给出那个可选的 默认参数。setattr()将要么加入一个新的属性,要么取代一个已存在的属性。而 delattr()函数会 从一个对象中删除属性。 Here are some examples using all the *attr() BIFs: Edit By Vheavens  Edit By Vheavens  下面一些例子使用到了*attr()系列函数: >>> class myClass(object): ... def __init__(self): ... self.foo = 100 ... >>> myInst = myClass() >>> hasattr(myInst, 'foo') True >>> getattr(myInst, 'foo') 100 >>> hasattr(myInst, 'bar') False >>> getattr(myInst, 'bar') Traceback (most recent call last): File "", line 1, in ? getattr(myInst, 'bar') AttributeError: myClass instance has no attribute 'bar' >>> getattr(c, 'bar', 'oops!') 'oops!' >>> setattr(myInst, 'bar', 'my attr') >>> dir(myInst) ['__doc__', '__module__', 'bar', 'foo'] >>> getattr(myInst, 'bar') # same as myInst.bar #等同于 myInst.bar 'my attr' >>> delattr(myInst, 'foo') >>> dir(myInst) ['__doc__', '__module__', 'bar'] >>> hasattr(myInst, 'foo') False 13.12.4 dir() 前面用到 dir()是在练习 2-12,2-13 和 4-7。在这些练习中,我们用 dir()列出一个模块所有属 性的信息。现在你应该知道 dir()还可以用在对象上。 在 Python 2.2中, dir()得到了重要的更新。因为这些改变,那些 __members__和__methods__ 数据属性已经被宣告即将不支持。dir()提供的信息比以前更加详尽。根据文档,“除了实例变量名 和常用方法外,它还显示那些通过特殊标记来调用的方法,像__iadd__(+=),__len__(len()), __ne__(!=)。” 在Python 文档中有详细说明。 Edit By Vheavens  Edit By Vheavens  z dir()作用在实例上(经典类或新式类)时,显示实例变量,还有在实例所在的类及所有它 的基类中定义的方法和类属性。 z dir()作用在类上(经典类或新式类)时,则显示类以及它的所有基类的__dict__中的内容。 但它不会显示定义在元类(metaclass)中的类属性。 z dir()作用在模块上时,则显示模块的__dict__的内容。(这没改动)。 z dir()不带参数时,则显示调用者的局部变量。(也没改动)。 z 关于更多细节:对于那些覆盖了__dict__或__class__属性的对象,就使用它们;出于向后兼 容的考虑,如果已定义了__members__和__methods__,则使用它们。 13.12.5 super() super()函数在 Python2.2 版本新式类中引入。这个函数的目的就是帮助程序员找出相应的父类, 然后方便调用相关的属性。一般情况下,程序员可能仅仅采用非绑定方式调用祖先类方法。使用 super()可以简化搜索一个合适祖先的任务,并且在调用它时,替你传入实例或类型对象。 在第 13.11.4 节中,我们描述了文档解释顺序(MRO),用于在祖先类中查找属性。对于每个定 义的类,都有一个名为__mro__的属性,它是一个元组,按照他们被搜索时的顺序,列出了备搜索 的类。语法如下: super(type[, obj]) 给出 type,super()“返回此 type 的父类”。如果你希望父类被绑定,你可以传入obj 参数(obj 必须是 type 类型的).否则父类不会被绑定。obj 参数也可以是一个类型,但它应当是 type 的一个子 类。通常,当给出 obj 时: z 如果 obj 是一个实例,isinstance(obj,type)就必须返回 True z 如果 obj 是一个类或类型,issubclass(obj,type)就必须返回 True 事实上,super()是一个工厂函数,它创造了一个 super object,为一个给定的类使用__mro__ 去查找相应的父类。很明显,它从当前所找到的类开始搜索 MRO。更多详情,请再看一下 Guido van Rossum 有关统一类型和类的文章,他甚至给出了一个 super()的纯 Python 实现,这样,你可以加深 其印象,知道它是如何工作的! 最后想到.... super()的主要用途,是来查找父类的属性,比如, super(MyClass,self).__init__()。如果你没有执行这样的查找,你可能不需要使用 super()。 有很多如何使用 super()的例子分散在本章中。记得阅读一下第 13.11.2 节中有关 super()的重 要提示,尤其是那节中的核心笔记。 Edit By Vheavens  Edit By Vheavens  13.12.6 vars() vars()内建函数与 dir()相似,只是给定的对象参数都必须有一个__dict__属性。vars()返回一 个字典,它包含了对象存储于其__dict__中的属性(键)及值。如果提供的对象没有这样一个属性, 则会引发一个 TypeError 异常。如果没有提供对象作为 vars()的一个参数,它将显示一个包含本地 名字空间的属性(键)及其值的字典,也就是,locals()。我们来看一下例子,使用类实例调用 vars(): class C(object): pass >>> c = C() >>> c.foo = 100 >>> c.bar = 'Python' >>> c.__dict__ {'foo': 100, 'bar': 'Python'} >>> vars(c) {'foo': 100, 'bar': 'Python'} 表 13.3 概括了类和类实例的内建函数。 表 13.3 类,实例及其它对象的内建函数 内建函数 描述 issubclass(sub, sup) 如果类 sub 是类 sup 的子类,则返回 True,反之,为 False。 isinstance(obj1, obj2) 如果实例 obj1 是类 obj2 或者 obj2 子类的一个实例;或者如果 obj1 是 obj2 的类型,则返回 True;反之,为 False。 hasattr(obj, attr) 如果 obj 有属性 attr(用字符串给出),返回True,反之,返回 表 13.3 类,实例及其它对象的内建函数(续) 内建函数 描述 getattr(obj, attr[, default]) 获取 obj 的 attr 属性;与返回 obj.attr 类似;如果 attr 不是 obj 的属性,如果提供了默认值,则返回默认值;不然, 就会引发一个 AttributeError 异常。 setattr(obj, attr, val) 设置 obj 的 attr 属性值为 val,替换任何已存在的属性值; 不然,就创建属性;类似于 obj.attr=val delattr(obj, attr) 从 obj 中删除属性 attr(以字符串给出);类似于del obj.attr。 dir(obj=None) 返回 obj 的属性的一个列表;如果没有给定 obj,dir()则 Edit By Vheavens  Edit By Vheavens  显示局部名字空间空间中的属性,也就是 locals().keys() super(type, obj=None) a 返回一个表示父类类型的代理对象;如果没有传入 obj, 则返 回的 super 对象是非绑定的;反之,如果 obj 是一个 type , issubclass(obj,type) 必 为 True ; 否 则 , isinstance(obj,type)就必为 True。 vars(obj=None) 返回 obj 的属性及其值的一个字典;如果没有给出 obj, vars()显示局部名字空间字典(属性及其值),也就是 locals()。 ----------------------------------------- a. Python2.2 中新增;仅对新式类有效 13.13 用特殊方法定制类 我们已在本章前面部分讲解了方法的两个重要方面:首先,方法必须在调用前被绑定(到它们 相应类的某个实例中);其次,有两个特殊方法可以分别作为构造器和析够器的功能,分别名为 __init__()和__del__()。 事实上,__init__()和__del__()只是可自定义特殊方法集中的一部分。它们中的一些有预定 义的默认行为,而其它一些则没有,留到需要的时候去实现。这些特殊方法是 Python 中用来扩充 类的强有力的方式。它们可以实现: z 模拟标准类型 z 重载操作符 特殊方法允许类通过重载标准操作符+,*, 甚至包括分段下标及映射操作操作[] 来模拟标准 类型。如同其它很多保留标识符,这些方法都是以双下划线(__)开始及结尾的。表 13.4 列出了所有 特殊方法及其它的描述。 表 13.4 用来定制类的特殊方法 特殊方法 描述 基本定制型 C.__init__(self[, arg1, ...]) 构造器(带一些可选的参数) C.__new__(self[, arg1, ...])a 构造器(带一些可选的参数);通常用在设置不变数据类 型的子类。 C.__del__(self) 解构器 C.__str__(self) 可打印的字符输出;内建 str()及 print 语句 C.__repr__(self) 运行时的字符串输出;内建 repr() 和‘‘ 操作符 C.__unicode__(self)b Unicode 字符串输出;内建 unicode() Edit By Vheavens  Edit By Vheavens  C.__call__(self, *args) 表示可调用的实例 C.__nonzero__(self) 为object 定义 False 值;内建 bool() (从 2.2 版开始) C.__len__(self) “长度”(可用于类);内建len() ----------------------------------------- (待续) 表 13.4 可以定制类的特殊方法(续) 特殊方法 描述 对象(值)比较 c C.__cmp__(self, obj) 对象比较;内建 cmp() C.__lt__(self, obj) and 小于/小于或等于;对应<及<=操作符 C.__gt__(self, obj) and 大于/大于或等于;对应>及>=操作符 C.__eq__(self, obj) and 等于/不等于;对应==,!=及<>操作符 属性 C.__getattr__(self, attr) 获取属性;内建 getattr();仅当属性没有找到时调用 C.__setattr__(self, attr, val) 设置属性 C.__delattr__(self, attr) 删除属性 C.__getattribute__(self, attr) a 获取属性;内建 getattr();总是被调用 C.__get__(self, attr) a (描述符)获取属性 C.__set__(self, attr, val) a (描述符)设置属性 C.__delete__(self, attr) a (描述符)删除属性 定制类/模拟类型 数值类型:二进制操作符 C.__*add__(self, obj) 加;+操作符 C.__*sub__(self, obj) 减;-操作符 C.__*mul__(self, obj) 乘;*操作符 C.__*div__(self, obj) 除;/操作符 C.__*truediv__(self, obj) e True 除;/操作符 C.__*floordiv__(self, obj) e Floor 除;//操作符 C.__*mod__(self, obj) 取模/取余;%操作符 C.__*divmod__(self, obj) 除和取模;内建 divmod() C.__*pow__(self, obj[, mod]) 乘幂;内建 pow();**操作符 C.__*lshift__(self, obj) 左移位;<<操作符 表 13.4 可定制类的特殊方法(续) 特殊方法 描述 定制类/模拟类型 数值类型:二进制操作符 Edit By Vheavens  Edit By Vheavens  C.__*rshift__(self, obj) 右移;>>操作符 C.__*and__(self, obj) 按位与;&操作符 C.__*or__(self, obj) 按位或;|操作符 C.__*xor__(self, obj) 按位与或;^操作符 数值类型:一元操作符 C.__neg__(self) 一元负 C.__pos__(self) 一元正 C.__abs__(self) 绝对值;内建 abs() C.__invert__(self) 按位求反;~操作符 数值类型:数值转换 C.__complex__(self, com) 转为 complex(复数);内建 complex() C.__int__(self) 转为int;内建 int() C.__long__(self) 转为long;内建 long() C.__float__(self) 转为float;内建 float() 数值类型:基本表示法(String) C.__oct__(self) 八进制表示;内建 oct() C.__hex__(self) 十六进制表示;内建 hex() 数值类型:数值压缩 C.__coerce__(self, num) 压缩成同样的数值类型;内建 coerce() C.__index__(self)g 在有必要时,压缩可选的数值类型为整型(比如:用于切片 索引等等) ---------------------------------------- 续 表 13.4 定制类的特殊方法(续) 序列类型 C.__len__(self) 序列中项的数目 C.__getitem__(self, ind) 得到单个序列元素 C.__setitem__(self, ind,val) 设置单个序列元素 C.__delitem__(self, ind) 删除单个序列元素 特殊方法 描述 序列类型 C.__getslice__(self, ind1,ind2) 得到序列片断 C.__setslice__(self, i1, i2,val) 设置序列片断 C.__delslice__(self, ind1,ind2) 删除序列片断 C.__contains__(self, val) f 测试序列成员;内建 in 关键字 C.__*add__(self,obj) 串连;+操作符 C.__*mul__(self,obj) 重复;*操作符 C.__iter__(self) e 创建迭代类;内建 iter() Edit By Vheavens  Edit By Vheavens  映射类型 C.__len__(self) mapping 中的项的数目 C.__hash__(self) 散列(hash)函数值 C.__getitem__(self,key) 得到给定键(key)的值 C.__setitem__(self,key,val) 设置给定键(key)的值 C.__delitem__(self,key) 删除给定键(key)的值 C.__missing__(self,key) 给定键如果不存在字典中,则提供一个默认值 ----------------------------------------- a.Python 2.2 中新引入;仅用于新式类中。 b. Python 2.3 中新引入。 c. 除了 cmp()外,其余全是在 Python 新引入的。 d. "*" 代表''(selp OP obj), 'r'(obj OP self),或'i'(原位(in-place)操作, Py2.0 新增), 例如 __add__, __radd__, or __iadd__. e. Python 2.2 中新引入。 f. “*” either nothing (self OP obj), “r” (obj OP self ), or “i” for in-place operation (new in Python 1.6), i.e., __add__, __radd__, or __iadd__. g. Python 2.5 中新引入。 基本的定制和对象(值)比较特殊方法在大多数类中都可以被实现,且没有同任何特定的类型 模型绑定。延后设置,也就是所谓的 Rich 比较,在 Python2.1 中加入。属性组帮助管理您的类的 实例属性。这同样独立于模型。还有一个,__getattribute__(),它仅用在新式类中,我们将在后 面的章节中对它进行描述。 特殊方法中数值类型部分可以用来模拟很多数值操作,包括那些标准(一元和二进制)操作符, 类型转换,基本表示法,及压缩。也还有用来模拟序列和映射类型的特殊方法。实现这些类型的特 殊方法将会重载操作符,以使它们可以处理你的类类型的实例。 另外,除操作符__*truediv__()和__*floordiv__()在 Python2.2 中加入,用来支持 Python 除 操作符中待定的更改---可查看 5.5.3 节。基本上,如果解释器启用新的除法,不管是通过一个开关 来启动 Python,还是通过"from __future__ import division",单斜线除操作(/)表示的将是 ture 除法,意思是它将总是返回一个浮点值,不管操作数是否为浮点数或者整数(复数除法保持不变)。 双斜线除操作(//)将提供大家熟悉的浮点除法,从标准编译型语言像 C/C++及 Java 过来的工程师 一定对此非常熟悉。同样,这些方法只能处理实现了这些方法并且启用了新的除操作的类的那些符 号。 表格中,在它们的名字中,用星号通配符标注的数值二进制操作符则表示这些方法有多个版本, 在名字上有些许不同。星号可代表在字符串中没有额外的字符,或者一个简单的“r”指明是一个右 结合操作。没有“r”,操作则发生在对于 self OP obj的格式; “r”的出现表明格式 obj OP self。 比如,__add__(self,obj)是针对self+obj 的调用,而__radd__(self,obj)则针对 obj+self 来调用。 Edit By Vheavens  Edit By Vheavens  增量赋值,起于 Python 2.0,介绍了“原位”操作符。一个“i”代替星号的位置,表示左结合 操作与赋值的结合,相当是在 self=self OP obj。举例,__iadd__(self,obj)相当于self=self+obj 的调用。 随着 Python 2.2 中新式类的引入,有一些更多的方法增加了重载功能。然而,在本章开始部分 提到过,我们仅关注经典类和新式类都适应的核心部分,本章的后续部分,我们介绍新式类的高级 特性。 13.13.1 简单定制(RoundFloat2) 我们的第一个例子很普通。在某种程度上,它基于我们前面所看到的从 Python 类型中派生出的 派生类 RoundFloat。这个例子很简单。事实上,我们甚至不想去派生任何东西(当然,除object 外)... 我们也不想采用与 floats 有关的所有“好东西”。不,这次,我们想创建一个苗条的例子,这样你 可以对类定制的工作方式有一个更好的理解。这种类的前提与其它类是一样的:我们只要一个类来 保存浮点数,四舍五入,保留两位小数位。 class RoundFloatManual(object): def __init__(self, val): assert isinstance(val, float), \ "Value must be a float!" self.value = round(val, 2) 这个类仅接收一个浮点值----它断言了传递给构造器的参数类型必须为一个浮点数----并且将 其保存为实例属性值。让我们来试试,创建这个类的一个实例: >>> rfm = RoundFloatManual(42) Traceback (most recent call last): File "", line 1, in ? File "roundFloat2.py", line 5, in __init__ assert isinstance(val, float), \ AssertionError: Value must be a float! >>> rfm = RoundFloatManual(4.2) >>> rfm >>> print rfm 你已看到,它因输入非法,而“噎住”,但如果输入正确时,就没有任何输出了。可是,当把这 Edit By Vheavens  Edit By Vheavens  个对象转存在交互式解释器中时,看一下发生了什么。我们得到一些信息,却不是我们要找的。(我 们想看到数值,对吧?)调用 print 语句同样没有明显的帮助。 不幸的是,print(使用 str())和真正的字符串对象表示(使用 repr())都没能显示更多有关 我们对象的信息。一个好的办法是,去实现__str__()和__repr__()二者之一,或者两者都实现,这 样我们就能“看到”我们的对象是个什么样子了。换句话说,当你想显示你的对象,实际上是想看 到有意义的东西,而不仅仅是通常的 Python 对象字符串()。让我们来添加 一个__str()__方法,以覆盖默认的行为: def __str__(self): return str(self.value) 现在我们得到下面的: >>> rfm = RoundFloatManual(5.590464) >>> rfm >>> print rfm 5.59 >>> rfm = RoundFloatManual(5.5964) >>> print rfm 5.6 我们还有一些问题...一个问题是仅仅在解释器中转储(dump)对象时,仍然显示的是默认对象符 号,但这样做也算不错。如果我们想修复它,只需要覆盖__repr__()。因为字符串表示法也是Python 对象,我们可以让__repr__()和__str__()的输出一致。 为了完成这些,只要把__str__()的代码复制给__repr__()。这是一个简单的例子,所以它没有 真正对我们造成负面影响,但作为程序员,你知道那不是一个最好的办法。如果__str__()中存在bug, 那么我们会将 bug 也复制给__repr__()了。 最好的方案,在__str__()中的代码也是一个对象,同所有对象一样,引用可以指向它们,所以, 我们可以仅仅让__repr__()作为__str__()的一个别名: __repr__ = __str__ 在带参数 5.5964 的第二个例子中,我们看到它舍入值刚好为 5.6,但我们还是想显示带两位小 数的数。来玩玩一个更好的妙计吧,看下面: def __str__(self): return '%.2f' % self.value Edit By Vheavens  Edit By Vheavens  这里就同时具备 str()和 repr()的输出了: >>> rfm = RoundFloatManual(5.5964) >>> rfm 5.60 >>> print rfm 5.60 例 13.2 基本定制(roundFloat2.py) 1 #!/usr/bin/env python 2 3 class RoundFloatManual(object): 4 def __init__(self, val): 5 assert isinstance(val, float), \ 6 "Value must be a float!" 7 self.value = round(val, 2) 8 9 def __str__(self): 10 return '%.2f' % self.value 11 12 __repr__ = __str__ 在本章开始部分,最初的 RoundFloat 例子,我们没有担心所有细致对象的显示问题;原因是 __str__()和__repr__()作为 float 类的一部分已经为我们定义好了。我们所要做的就是去继承它们。 增强版本“手册”中需要另外的工作。你发现派生是多么的有益了吗?我们甚至不需要知道解释器 在继承树上要执行多少步才能找到一个已声明的你正在使用却没有考虑过的方法。我们将在例 13.2 中列出这个类的全部代码。 现在开始一个稍复杂的例子。 13.13.2 数值定制(Time60) 作为第一个实际的例子,我们可以想象需要创建一个简单的应用,用来操作时间,精确到小时 和分。我们将要创建的这个类可用来跟踪职员工作时间,ISP用户在线时间,数据库总的运行时间(不 包括备份及升级时的停机时间),在扑克比赛中玩家总时间,等等。 在 Time60 类中,我们将整数的小时和分钟作为输入传给构造器。 class Time60(object): # ordered pair 顺序对 Edit By Vheavens  Edit By Vheavens  def __init__(self, hr, min): # constructor 构造器 self.hr = hr # assign hours 给小时赋值 self.min = min # assign minutes 给分赋值 显示 同样,如前面的例子所示,在显示我们的实例的时候,我们需要一个有意义的输出,那么就要 覆盖__str__()(如果有必要的话,__repr__()也要覆盖)。我们都习惯看小时和分,用冒号分隔开的 格式,比如,“4:30”,表示四个小时,加半个小时(4个小时及 30 分钟): def __str__(self): return '%d:%d' % (self.hr, self.min) 用此类,可以实例化一些对象。在下面的例子中,我们启动一个工时表来跟踪对应构造器的计 费小时数: >>> mon = Time60(10, 30) >>> tue = Time60(11, 15) >>> >>> print mon, tue 10:30 11:15 输出不错,正是我们想看到的。下一步干什么呢?可考虑与我们的对象进行交互。比如在时间 片的应用中,有必要把 Time60 的实例放到一起让我们的对象执行所有有意义的操作。我们更喜欢像 这样的: >>> mon + tue 21:45 加法 Python 的重载操作符很简单。像加号(+),我们只需要重载__add__()方法,如果合适,还可以 用__radd__()及__iadd__()。稍后有更多有关这方面的描述。实现__add__()听起来不难----只要把 分和小时加在一块。大多数复杂性源于我们怎么处理这个新的总数。如果我们想看到“21:45”,就 必须认识到这是另一个 Time60 对象,我们没有修改 mon 或 tue,所以,我们的方法就应当创建另一 个对象并填入计算出来的总数。 实现__add__()特殊方法时,首先计算出个别的总数,然后调用类构造器返回一个新的对象: def __add__(self, other): Edit By Vheavens  Edit By Vheavens  return self.__class__(self.hr + other.hr, self.min + other.min) 和正常情况下一样,新的对象通过调用类来创建。唯一的不同点在于,在类中,你一般不直接 调用类名, 而是使用 self 的__class__属性,即实例化 self 的那个类,并调用它。由于 self.__class__与 Time60 相同,所以调用 self.__class__()与调用 Time60()是一回事。 不管怎样,这是一个更面向对象的方式。另一个原因是,如果我们在创建一个新对象时,处处 使用真实的类名,然后,决定将其改为别的名字,这时,我们就不得不非常小心地执行全局搜索并 替换。如果靠使用 self.__class__,就不需要做任何事情,只需要直接改为你想要的类名。 好了,我们现在来使用加号重载,“增加”Time60 对象: >>> mon = Time60(10, 30) >>> tue = Time60(11, 15) >>> mon + tue >>> print mon + tue 21:45 哎哟,我们忘记添加一个别名__repr__给__str__了,这很容易修复。你可能会问,“当我们试 着在重载情况下使用一个操作符,却没有定义相对应的特殊方法时还有很多需要优化和重要改良的 地方,会发生什么事呢?” 答案是一个 TypeError 异常: >>> mon - tue Traceback (most recent call last): File "", line 1, in ? TypeError: unsupported operand type(s) for -: 'Time60' and 'Time60' 原位加法 有了增量赋值(在Python 2.0中引入),我们也许还有希望覆盖“原位”操作,比如,__iadd__()。 这是用来支持像 mon += tue 这样的操作符,并把正确的结果赋给 mon。重载一个__i*__()方法的唯 一秘密是它必须返回 self。把下面的片断加到我们例子中,以修复上面的 repr()问题,并支持增量 赋值: __repr__ = __str__ def __iadd__(self, other): self.hr += other.hr self.min += other.min return self Edit By Vheavens  Edit By Vheavens  下面是结果输出: >>> mon =Time60(10,30) >>> tue =Time60(11,15) >>> mon 10:30 >>> id(mon) 401872 >>> mon += tue >>> id(mon) 401872 >>> mon 21:45 注意,使用 id()内建函数是用来确定一下,在原位加的前后,我们确实是修改了原来的对象, 而没有创建一个新的对象。对一个具有巨大潜能的类来说,这是很好的开始。在例 13.3 中给出了 Time60 的类的完全定义 例 13.3 中级定制(time60.py) 1 #!/usr/bin/env python 2 3 class Time60(object): 4 'Time60 - track hours and minutes' 5 6 def __init__(self, hr, min): 7 'Time60 constructor - takes hours and minutes' 8 self.hr = hr 9 self.min = min 10 11 def __str__(self): 12 'Time60 - string representation' 13 return '%d:%d' % (self.hr, self.min) 14 15 __repr__ = __str__ 16 17 def __add__(self, other): 18 'Time60 - overloading the addition operator' 19 return self.__class__(self.hr + other.hr,self.min + other.min) Edit By Vheavens  Edit By Vheavens  21 22 def __iadd__(self, other): 23 'Time60 - overloading in-place addition' 24 self.hr += other.hr 25 self.min += other.min 26 return self 例 13.4 随机序列迭代器(randSeq.py) 1 #!/usr/bin/env python 2 3 from random import choice 4 5 class RandSeq(object): 6 def __init__(self, seq): 7 self.data = seq 8 9 def __iter__(self): 10 return self 11 12 def next(self): 13 return choice(self.data) 升华 现在暂不管它了,但在这个类中,还有很多需要优化和改良的地方。比如,如果我们不传入两 个分离的参数,而传入一个 2 值元组给构造器作为参数,是不是更好些呢?如果是像“10:30”这样 的字符串的话,结果会怎样? 答案是肯定的,你可以这样做,在 Python 中很容易做到,但不是像很多其他面向对象语言一样 通过重载构造器来实现.Python 不允许用多个签名重载可调用对象.所以实现这个功能的唯一的方式 是使用单一的构造器,并由 isinstance()和(可能的)type()内建函数执行自省功能。 能支持多种形式的输入,能够执行其它操作像减法等,可以让我们的应用更健壮,灵活。当然 这些是可选的,就像“蛋糕上的冰”,但我们首先应该担心的是两个中等程度的缺点:1.当比十分钟 还少时,格式并不是我们所希望的,2. 不支持 60 进制(基数 60)的操作: >>> wed = Time60(12, 5) >>> wed 12:5 Edit By Vheavens  Edit By Vheavens  >>> thu = Time60(10, 30) >>> fri = Time60(8, 45) >>> thu + fri 18:75 --------------------------------------------------------------------------------- 1.源自拉丁语的基数是 60 的名字;有时,六十进制会被用到,这是一种希腊词根“hexe”和拉 丁“gesmal”的混合。 显示 wed 结果是“12:05”,把thu 和 fri 加起来结果会是“19:15”。修改这些缺陷,实现上面 的改进建议可以实际性地提高你编写定制类技能。这方面的更新,更详细的描述在本章的练习 13.20 中。 我们希望,你现在对于操作符重载,为什么要使用操作符重载,以及如何使用特殊方法来实现 它已有了一个更好的理解了。接下来为选看章节内容,让我们来了解更多复杂的类定制的情况。 13.13.3 迭代器(RandSeq 和 AnyIter) RandSeq 我们正式介绍迭代器是在第 8 章,但在全书中都在用它。它可以一次一个的遍历序列(或者是 类似序列对象)中的项。在第 8 章中,我们描述了如何利用一个类中的__iter__()和 next()方法, 来创建一个迭代器。我们在此展示两个例子。 第一个例子是 RandSeq(RANDom SEQuence 的缩写)。我们给我们的类传入一个初始序列,然后 让用户通过 next()去迭代(无穷)。 __init__()方法执行前述的赋值操作。__iter__()仅返回 self,这就是如何将一个对象声明为 迭代器的方式,最后,调用next()来得到迭代器中连续的值。这个迭代器唯一的亮点是它没有终点。 这个例子展示了一些我们可以用定制类迭代器来做的与众不同的事情。一个是无穷迭代。因为 我们无损地读取一个序列,所以它是不会越界的。每次用户调用next()时,它会得到下一个迭代值, 但我们的对象永远不会引发 StopIteration 异常。我们来运行它,将会看到下面的输出: >>> from randseq import RandSeq >>> for eachItem in RandSeq( ... ('rock', 'paper', 'scissors')): ... print eachItem ... scissors Edit By Vheavens  Edit By Vheavens  scissors rock paper paper scissors : 例 13.5 任意项的迭代器(anyIter.py) 1 #!/usr/bin/env python 2 3 class AnyIter(object): 4 def __init__(self, data, safe=False): 5 self.safe = safe 6 self.iter = iter(data) 7 8 def __iter__(self): 9 return self 10 11 def next(self, howmany=1): 12 retval = [] 13 for eachItem in range(howmany): 14 try: 15 retval.append(self.iter.next()) 16 except StopIteration: 17 if self.safe: 18 break 19 else: 20 raise 21 return retval AnyIter 在第二个例子中,我们的确创建了一个迭代器对象,我们传给 next()方法一个参数,控制返回 条目的数目,而不是去一次一个地迭代每个项。下面是我们的代码(ANY number of items ITERator): 和 RandSeq 类的代码一样,类 AnyIter 很容易领会。我们在上面描述了基本的操作...它同其它 迭代器一样工作,只是用户可以请求一次返回 N 个迭代的项,而不仅是一个项。 我们给出一个迭代器和一个安全标识符(safe)来创建这个对象。如果这个标识符(safe)为真 Edit By Vheavens  Edit By Vheavens  (True),我们将在遍历完这个迭代器前,返回所获取的任意条目,但如果这个标识符为假(False), 则在用户请求过多条目时,将会引发一个异常。错综复杂的核心在于 next(),特别是它如何退出的 (14-21 行)。 在 next()的最后一部分中,我们创建用于返回的一个列表项,并且调用对象的 next()方法来获 得每一项条目。如果我们遍历完列表,得到一个StopIteration 异常,这时则检查安全标识符(safe)。 如果不安全(即,self.safe=False),则将异常抛还给调用者(raise);否则, 退出(break)并返回 (return)已经保存过的所有项。 >>> a = AnyIter(range(10)) >>> i = iter(a) >>> for j in range(1,5): >>> print j, ':', i.next(j) 1 : [0] 2 : [1, 2] 3 : [3, 4, 5] 4 : [6, 7, 8, 9] 上面程序的运行没有问题,因为迭代器正好符合项的个数。当情况出现偏差,会发生什么呢? 让我们首先试试“不安全(unsafe)”的模式,这也就是紧随其后创建我们的迭代器: >>> i = iter(a) >>> i.next(14) Traceback (most recent call last): File "", line 1, in ? File "anyIter.py", line 15, in next retval.append(self.iter.next()) StopIteration 因为超出了项的支持量,所以出现了 StopIteration 异常,并且这个异常还被重新引发回调用 者(第 20 行)。如果我们使用“安全(safe)”模式重建迭代器,再次运行一次同一个例子的话,我 们就可以在项失控出现前得到迭代器所得到的元素: >>> a = AnyIter(range(10), True) >>> i = iter(a) >>> i.next(14) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 13.13.4 *多类型定制(NumStr) Edit By Vheavens  Edit By Vheavens  现在创建另一个新类,NumStr,由一个数字-字符对组成,相应地,记为n和s,数值类型使用 整型(integer)。尽管这组顺序对的“合适的”记号是(n,s),但我们选用[n::s]来表示它,有点 不同。暂不管记号,这两个数据元素只要我们模型考虑好了,就是一个整体。可以创建我们的新类 了,叫做 NumStr,有下面的特征: 初始化 类应当对数字和字符串进行初始化;如果其中一个(或两)没有初始化,则使用 0 和空字符串, 也就是,n=0 且 s='',作为默认。 加法 我们定义加法操作符,功能是把数字加起来,把字符连在一起;要点部分是字符串要按顺序相 连。比如,NumStr1=[n1::s1]且 NumStr2=[n2::s2]。则 NumStr1+NumStr2 表示[n1+n2::s1+s2], 其中,+代表数字相加及字符相连接。 乘法 类似的,定义乘法操作符的功能为,数字相乘,字符累积相连,也就是, NumStr1*NumStr2=[n1*n::s1*n]。 False 值 当数字的数值为 0 且字符串为空时,也就是当 NumStr=[0::'']时,这个实体即有一个 false 值。 比较 比较一对 NumStr 对象,比如,[n1::s1] vs. [n2::s2],我们可以发现九种不同的组合(即,n1>n2 and s1<s2,n1==n2 and s1>s2,等等)。对数字和字符串,我们一般按照标准的数值和字典顺 序的进行比较,即,如果 obj1obj2 时,比较的返回值大于 0,当两个对象有相同的值时,比较的返回值等于 0。 我们的类的解决方案是把这些值相加,然后返回结果。有趣的是 cmp()不会总是返回-1,0,或 1。上面提到过,它是一个小于,等于或大于 0 的整数。 为了能够正确的比较对象,我们需要让__cmp__()在(n1>n2) 且 (s1>s2)时,返回 1,在(n1s2),或相反),返回 0.反之亦然。 例 13.6 多类型类定制(numstr.py) Edit By Vheavens  Edit By Vheavens  1 #!/usr/bin/env python 2 3 class NumStr(object): 4 5 def __init__(self, num=0, string=''): 6 self.__num = num 7 self.__string = string 8 9 def __str__(self): # define for str() 10 return '[%d :: %r]' % \ 11 self.__num, self.__string) 12 __repr__ = __str__ 13 14 def __add__(self, other): # define for s+o 15 if isinstance(other, NumStr): 16 return self.__class__(self.__num + \ 17 other.__num, \ 18 self.__string + other.__string) 19 else: 20 raise TypeError, \ 21 'Illegal argument type for built-in operation' 22 23 def __mul__(self, num): # define for o*n 24 if isinstance(num, int): 25 return self.__class__(self.__num * num 26 self.__string * num) 27 else: 28 raise TypeError, \ 29 'Illegal argument type for built-in operation' 30 31 def __nonzero__(self): # False if both are 32 return self.__num or len(self.__string) 33 34 def __norm_cval(self, cmpres):# normalize cmp() 35 return cmp(cmpres, 0) 36 37 def __cmp__(self, other): # define for cmp() 38 return self.__norm_cval( 39 cmp(self.__num, other.__num)) + \ Edit By Vheavens  Edit By Vheavens  40 self.__norm_cval( 41 cmp(self.__string, other.__string)) 根据上面的特征,我们列出 numstr.py 的代码,执行一些例子: >>> a = NumStr(3, 'foo') >>> b = NumStr(3, 'goo') >>> c = NumStr(2, 'foo') >>> d = NumStr() >>> e = NumStr(string='boo') >>> f = NumStr(1) >>> a [3 :: 'foo'] >>> b [3 :: 'goo'] >>> c [2 :: 'foo'] >>> d [0 :: ''] >>> e [0 :: 'boo'] >>> f [1 :: ''] >>> a < b True >>> b < c False >>> a == a True >>> b * 2 [6 :: 'googoo'] >>> a * 3 [9 :: 'foofoofoo'] >>> b + e [3 :: 'gooboo'] >>> e + b [3 :: 'boogoo'] >>> if d: 'not false' # also bool(d) ... Edit By Vheavens  Edit By Vheavens  >>> if e: 'not false' # also bool(e) ... 'not false' >>> cmp(a,b) -1 >>> cmp(a,c) 1 >>> cmp(a,a) 0 逐行解释 第 1-7 行 脚本的开始部分为构造器__init__(),通过调用 NumStr()时传入的值来设置实例,完成自身初 始化。如果有参数缺失,属性则使用 false 值,即默认的 0 或空字符,这取决于参数情况。 一个重要怪癖是命名属性时,双下划线的使用。我们在下一节中会看到,这是在信息隐藏时, 强加一个级别,尽管不够成熟。程序员导入一个模块时,就不能直接访问到这些数据元素。我们正 试着执行一种 OO 设计中的封装特性,只有通过存取函数才能访问。如果这种语法让你感觉有点怪 异,不舒服的话,你可以从实例属性中删除所有双下划线,程序同样可以良好地运行。 所有的由双下划线(__)开始的属性都被“混淆”(mangled)了,导致这些名字在程序运行时 很难被访问到。但是它们并没有用一种难于被逆向工程的方法来“混淆”。事实上,“混淆”属性的 方式已众所周知,很容易被发现。这里主要是为了防止这些属性在被外部模块导入时,由于被意外 使用而造成的名字冲突。我们将名字改成含有类名的新标志符,这样做,可以确保这些属性不会被 无意“访问”。更多信息,请参见13.14 节中关于私有成员的内容。 第 9-12 行 我们把顺序对的字符串表示形式确定为“[num::'str']”,这样不论我们的实例用str()还是包 含在 print 语句中时候,我们都可以用__str__()来提供这种表示方式。我们想强调一点,第二个元 素是一个字符串,如果用户看到由引号标记的字符串时,会更加直观。要做到这点,我们使用“repr()” 表示法对代码进行转换,把“%s”替换成“%r”。这相当于调用 repr()或者使用单反引号来给出字符 串的可求值版本--可求值版本的确要有引号: >>> print a [3 :: 'foo'] 如果在 self.__string 中没有调用 repr()(去掉单反引号或使用“%s”)将导致字符串引号丢 失: return '[%d :: %s]' % (self.__num, self.__string) Edit By Vheavens  Edit By Vheavens  现在对实例再次调用 print,结果: >>> print a [3 :: foo] 没有引号,看起来会如何呢?不能信服“foo”是一个字符串,对吧?它看起来更像一个变量。 连作者可能也不能确定。(我们快点悄悄回到这一变化之前,假装从来没看到这个内容。) 代码中__str__()函数后的第一行是把这个函数赋给另一个特殊方法名,__repr__。我们决定我 们的实例的一个可求值的字符串表示应当与可打印字符串表示是一样的。而不是去定义一个完整的 新函数,成为__str__()的副本,我们仅去创建一个别名,复制其引用。当你实现__str__()后,一 旦使用那个对象作为参数来应用内建 str()函数,解释器就会调用这段代码.对__repr__()及 repr() 也一样。 如果不去实现__repr__(),我们的结果会有什么不同呢?如果赋值被取消,只有调用 str()的 print 语 句才会显示对象的内容。而可求值字符串表示恢复成默认的 Python 标 准 形 式 <...some_object_ information...> >>> print a # calls str(a) [3 :: 'foo'] >>> a # calls repr(a) 第 14-21 行 我们想加到我们的类中的一个特征就是加法操作,前面已提到过。Python 用于定制类的特征之 一是,我们可以重载操作符,以使定制的这些类型更“实用”。调用一个函数,像“add(obj1,obj2)” 是为“add”对象 obj1 和 ojb2,这看起来好像加法,但如果能使用加号(+)来调用相同的操作是不是 更具竞争力呢?像这样,obj1+obj2。 重载加号,需要去为 self(SELF)和其它操作数实现(OTHER)__add__().__add__()函数考虑 Self+Other 的情况,但我们不需要定义__radd__()来处理 Other+Self,因为这可以由 Other 的 __add__()去考虑。数值加法不像字符串那样结果受到(操作数)顺序的影响. 加法操作把两个部分中的每一部分加起来,并用这个结果对形成一个新的对象----通过将结果 做为参数调用 self.__class__()来实例化(同样,在前面已解释过).碰到任何类型不正确的对象时, 我们会引发一个 TypeError 异常. 第 23-29 行 我们也可以重载星号[靠实现__mul__()],执行数值乘法和字符串重复,并同样通过实例化来创 建一个新的对象。因为重复只允许整数在操作数的右边,因此也必执行此规则。基于同样的原因, 我们在此也没有实现__rmul__()。 Edit By Vheavens  Edit By Vheavens  第 31-32 行 Python 对象任何时候都有一个 Boolean 值。对标准类型而言,对象有一个 false 值的情况为: 它是一个类似于 0 的数值,或是一个空序列,或者映射。就我们的类而言,我们选择数值必须为 0, 字符串要为空 作为一个实例有一个 false 值的条件。覆盖__nonzero__()方法,就是为此目的。其 它对象,像严格模拟序列或映射类型的对象,使用一个长度为 0 作为 false 值。这些情况,你需要 实现__len__()方法,以实现那个功能。 第 34-41 行 __norm_cval() (“normalize cmp() value 的缩写”)不是一个特殊方法。它是一个帮助我们 重载__cmp__()的助手函数:唯一的目的就是把 cmp()返回的正值转为 1,负值转为-1。cmp()基于比 较的结果,通常返回任意的正数或负数(或0),但为了我们的目的,需要严格规定返回值为-1,0和 1。 对整数调用 cmp()及与 0 比较,结果即是我们所需要的,相当于如下代码片断: def __norm_cval(self, cmpres): if cmpres < 0: return -1 elif cmpres > 0: return 1 else: return 0 两个相似对象的实际比较是比较数字,比较字符串,然后返回这两个比较结果的和。 13.14 私有化 默认情况下,属性在 Python 中都是“public”,类所在模块和导入了类所在模块的其他模块的 代码都可以访问到。很多 OO 语言给数据加上一些可见性,只提供访问函数来访问其值。这就是熟知 的实现隐藏,是对象封装中的一个关键部分。 大多数 OO 语言提供“访问控制符”来限定成员函数的访问。 双下划线(__) Python 为类元素(属性和方法)的私有性提供初步的形式。由双下划线开始的属性在运行时被 “混淆”,所以直接访问是不允许的。实际上,会在名字前面加上下划线和类名。比如,以例 13.6(numstr.py)中的 self.__num 属性为例,被“混淆”后,用于访问这个数据值的标识就变成了 self._NumStr__num。把类名加上后形成的新的“混淆”结果将可以防止在祖先类或子孙类中的同名 冲突。 尽管这样做提供了某种层次上的私有化,但算法处于公共域中并且很容易被“击败”。这更多的 Edit By Vheavens  Edit By Vheavens  是一种对导入源代码无法获得的模块或对同一模块中的其他代码的保护机制. 这种名字混淆的另一个目的,是为了保护__XXX 变量不与父类名字空间相冲突。如果在类中有一 个__XXX 属性,它将不会被其子类中的__XXX 属性覆盖。(回忆一下,如果父类仅有一个 XXX 属性, 子类也定义了这个,这时,子类的 XXX 就是覆盖了父类的 XXX,这就是为什么你必须使用 PARENT.XXX 来调用父类的同名方法。) 使用__XXX,子类的代码就可以安全地使用__XXX,而不必担心它会影响 到父类中的__XXX。 单下划线(_) 与我们在第十二章发现的那样,简单的模块级私有化只需要在属性名前使用一个单下划线字符。 这就防止模块的属性用“from mymodule import *”来加载。这是严格基于作用域的,所以这同样 适合于函数。 在 Python 2.2 中引进的新式类,增加了一套全新的特征,让程序员在类及实例属性提供保护 的多少上拥有大量重要的控制权。尽管 Python 没有在语法上把 private,protected,friend 或 protected friend 等特征内建于语言中,但是可以按你的需要严格地定制访问权。我们不可能涵盖 所有的内容,但会在本章后面给你一些有关新式类属性访问的建议。 13.15 *授权 13.15.1 包装 “包装”在 Python 编程世界中经常会被提到的一个术语。它是一个通用的名字,意思是对一 个已存在的对象进行包装,不管它是数据类型,还是一段代码,可以是对一个已存在的对象,增加 新的,删除不要的,或者修改其它已存在的功能。 在 Python 2.2 版本前,从 Python 标准类型子类化或派生类都是不允许的。即使你现在可以对 新式类这样做,这一观念仍然很流行。你可以包装任何类型作为一个类的核心成员,以使新对象的 行为模仿你想要的数据类型中已存在的行为,并且去掉你不希望存在的行为;它可能会要做一些额 外的事情。这就是“包装类型”。在附录中,我们还将讨论如何扩充Python,包装的另一种形式。 包装包括定义一个类,它的实例拥有标准类型的核心行为。换句话说,它现在不仅能唱能跳, 还能够像原类型一样步行,说话。图 15-4 举例说明了在类中包装的类型看起像个什么样子。在图的 中心为标准类型的核心行为,但它也通过新的或最新的功能,甚至可能通过访问实际数据的不同方 法得到提高。 Edit By Vheavens  Edit By Vheavens  类对象(其表现像类型) 你还可以包装类,但这不会有太多的用途,因为已经有用于操作对象的机制,并且在上面已描 述过,对标准类型有对其进行包装的方式。你如何操作一个已存的类,模拟你需要的行为,删除你 不喜欢的,并且可能让类表现出与原类不同的行为呢?我们前面已讨论过,就是采用派生。 图 13-4 包装类型 13.15.2 实现授权 授权是包装的一个特性,可用于简化处理有关 dictating 功能,采用已存在的功能以达到最大 限度的代码重用。 包装一个类型通常是对已存在的类型的一些定制。我们在前面提到过,这种做法可以新建,修 改或删除原有产品的功能。其它的则保持原样,或者保留已存功能和行为。授权的过程,即是所有 更新的功能都是由新类的某部分来处理,但已存在的功能就授权给对象的默认属性。 实现授权的关键点就是覆盖__getattr__()方法,在代码中包含一个对 getattr()内建函数的调 用。特别地,调用 getattr()以得到默认对象属性(数据属性或者方法)并返回它以便访问或调用。 特殊方法__getattr__()的工作方式是,当搜索一个属性时,任何局部对象首先被找到(定制的对象)。 如果搜索失败了,则__getattr__()会被调用,然后调用 getattr()得到一个对象的默认行为。 换言之,当引用一个属性时,Python 解释器将试着在局部名称空间中查找那个名字,比如一个 自定义的方法或局部实例属性。如果没有在局部字典中找到,则搜索类名称空间,以防一个类属性 被访问。最后,如果两类搜索都失败了,搜索则对原对象开始授权请求,此时,__getattr__()会被 调用。 包装对象的简例 看一个例子。这个类已乎可以包装任何对象,提供基本功能,比如使用 repr()和 str()来处理 Edit By Vheavens  Edit By Vheavens  字符串表示法。另外定制由 get()方法处理,它删除包装并且返回原始对象。所以保留的功能都授权 给对象的本地属性,在必要时,可由__getattr__()获得。 下面是包装类的例子: class WrapMe(object): def __init__(self, obj): self.__data = obj def get(self): return self.__data def __repr__(self): return ‘self.__data‘ def __str__(self): return str(self.__data) def __getattr__(self, attr): return getattr(self.__data, attr) 在第一个例子中,我们将用到复数,因为所有Python 数值类型,只有复数拥有属性:数据属性, 及 conjugate()内建方法(求共轭复数,译者注!)。记住,属性可以是数据属性,还可以是函数或 方法: >>> wrappedComplex = WrapMe(3.5+4.2j) >>> wrappedComplex # wrapped object: repr() 包装的对象:repr() (3.5+4.2j) >>> wrappedComplex.real # real attribute 实部属性 3.5 >>> wrappedComplex.imag # imaginary attribute 虚部属性 42.2 >>> wrappedComplex.conjugate() # conjugate() method conjugate()方法 (3.5-4.2j) >>> wrappedComplex.get() # actual object 实际对象 (3.5+4.2j) 一旦我们创建了包装的对象类型,只要由交互解释器调用repr(),就可以得到一个字符串表示。 然后我们继续访问了复数的三种属性,我们的类中一种都没有定义。在例子中,寻找实部,虚部及 共轭复数的定义...they are not there! 对这些属性的访问,是通过 getattr()方法,授权给对象.最终调用 get()方法没有授权,因为 它是为我们的对象定义的----它返回包装的真实的数据对象。 Edit By Vheavens  Edit By Vheavens  下一个使用我们的包装类的例子用到一个列表。我们将会创建对象,然后执行多种操作,每次 授权给列表方法。 >>> wrappedList = WrapMe([123, 'foo', 45.67]) >>> wrappedList.append('bar') >>> wrappedList.append(123) >>> wrappedList [123, 'foo', 45.67, 'bar', 123] >>> wrappedList.index(45.67) 2 >>> wrappedList.count(123) 2 >>> wrappedList.pop() 123 >>> wrappedList [123, 'foo', 45.67, 'bar'] 注意,尽管我们正在我们的例子中使用实例,它们展示的行为与它们包装的数据类型非常相似。 然后,需要明白,只有已存在的属性是在此代码中授权的。 特殊行为没有在类型的方法列表中,不能被访问,因为它们不是属性。一个例子是,对列表的 切片操作,它是内建于类型中的,而不是像append()方法那样作为属性存在的。从另一个角度来说, 切片操作符是序列类型的一部分,并不是通过__getitem__()特殊方法来实现的。 >>> wrappedList[3] Traceback (innermost last): File "", line 1, in ? File "wrapme.py", line 21, in __getattr__ return getattr(self.data, attr) AttributeError: __getitem__ AttributeError 异常出现的原因是切片操作调用了__getitem__()方法,且__getitme__()没有 作为一个类实例方法进行定义,也不是列表对象的方法。回忆一下,什么时候调用 getattr()呢?当 在实例或类字典中的完整搜索失败后,就调用它来查找一个成功的匹配。你在上面可以看到,对 getattr()的调用就是失败的那个,触发了异常。 然而,我们还有一种"作弊"的方法,访问实际对象[通过我们的 get()方法]和它的切片能力. >>> realList = wrappedList.get() >>> realList[3] 'bar' Edit By Vheavens  Edit By Vheavens  你现在可能知道为什么我们实现 get()方法了----仅仅是为了我们需要取得对原对象进行访问 这种情况,我们可以从访问调用中直接访问对象的属性,而忽略局部变量(realList): >>> wrappedList.get()[3] 'bar' get()方法返回一个对象,随后被索引以得到切片片断。 >>> f = WrapMe(open('/etc/motd')) >>> f >>> f.get() >>> f.readline() 'Have a lot of fun...\012' >>> f.tell() 21 >>> f.seek(0) >>> print f.readline(), Have a lot of fun... >>> f.close() >>> f.get() 一旦你熟悉了对象的属性,你就能够开始理解一些信息片断从何而来,能够利用新得到的知识 来重复功能: >>> print "<%s file %s, mode %s at %x>" % \ ... (f.closed and 'closed' or 'open', 'f.name', 'f.mode', id(f.get())) 这总结了我们的简单包装类的例子。我们还刚开始接触使用类型模拟来进行类自定义。你将会 发现你可以进行无限多的改进,来进一步增加你的代码的用途。一种改进方法是为对象添加时间戳。 在下一小节中,我们将对我们的包装类增加另一个维度(dimension): 更新简单的包裹类 创建时间,修改时间,及访问时间是文件的几个常见属性,但没人说,你不能为对象加上这类信 息。毕竟,一些应用能因有这些额外信息而受益。 Edit By Vheavens  Edit By Vheavens  如果你对使用这三类时间顺序(chronological)数据还不熟,我们将会对它们进行解释。创建时 间(或'ctime')是实例化的时间,修改时间(或'mtime')指的是核心数据升级的时间[通常会调 用新的 set()方法],而访问时间(或'atime')是最后一次对象的数据值被获取或者属性被访问时的 时间戳。 更新我们前面定义的类,可以创建一个模块 twrapme.py,看例 13.7。 如何更新这些代码呢?好,首先,你将会发现增加了三个新方法:gettimeval(),gettimestr(), 及 set()。我们还增加数行代码,根据所执行的访问类型,更新相应的时间戳。 例 13.7 包装标准类型(twrapme.py) 类定义包装了任何内建类型,增加时间属性;get(),set(),还有字符串表示的方法;并授权 所有保留的属性,访问这些标准类型。 1 #!/usr/bin/env python 2 3 from time import time, ctime 4 5 class TimedWrapMe(object): 6 7 def __init__(self, obj): 8 self.__data = obj 9 self.__ctime = self.__mtime = \ 10 self.__atime = time() 11 12 def get(self): 13 self.__atime = time() 14 return self.__data 15 16 def gettimeval(self, t_type): 17 if not isinstance(t_type, str) or \ 18 t_type[0] not in 'cma': 19 raise TypeError, \ 20 "argument of 'c', 'm', or 'a' req'd" 21 return getattr(self, '_%s__%stime' % \ 22 (self.__class__.__name__, t_type[0])) 23 24 def gettimestr(self, t_type): Edit By Vheavens  Edit By Vheavens  25 return ctime(self.gettimeval(t_type)) 26 27 def set(self, obj): 28 self.__data = obj 29 self.__mtime = self.__atime = time() 30 31 def __repr__(self): # repr() 32 self.__atime = time() 33 return ‘self.__data‘ 34 35 def __str__(self): # str() 36 self.__atime = time() 37 return str(self.__data) 38 39 def __getattr__(self, attr): # delegate 40 self.__atime = time() 41 return getattr(self.__data, attr) gettimeval()方法带一个简单的字符参数,“c”,“m”或“a”,相应地,对应于创建,修改或 访问时间,并返回相应的时间,以一个浮点值保存。gettimestr()仅仅返回一个经 time.ctime() 函数格式化的打印良好的字符串形式的时间。 为新的模块作一个测试驱动。我们已看到授权是如何工作的,所以,我们将包装没有属性的对 象,来突出刚加入的新的功能。在例子中,我们包装了一个整数,然后,将其改为字符串。 >>> timeWrappedObj = TimedWrapMe(932) >>> timeWrappedObj.gettimestr('c') ‘Wed Apr 26 20:47:41 2006' >>> timeWrappedObj.gettimestr('m') 'Wed Apr 26 20:47:41 2006' >>> timeWrappedObj.gettimestr('a') 'Wed Apr 26 20:47:41 2006' >>> timeWrappedObj 932 >>> timeWrappedObj.gettimestr('c') 'Wed Apr 26 20:47:41 2006' >>> timeWrappedObj.gettimestr('m') 'Wed Apr 26 20:47:41 2006' >>> timeWrappedObj.gettimestr('a') 'Wed Apr 26 20:48:05 2006' Edit By Vheavens  Edit By Vheavens  你将注意到,一个对象在第一次被包装时,创建,修改,及最后一次访问时间都是一样的。一 旦对象被访问,访问时间即被更新,但其它的没有动。如果使用 set()来置换对象,则修改和最后一 次访问时间会被更新。例子中,最后是对对象的读访问操作。 >>> timeWrappedObj.set('time is up!') >>> timeWrappedObj.gettimestr('m') 'Wed Apr 26 20:48:35 2006' >>> timeWrappedObj 'time is up!' >>> timeWrappedObj.gettimestr('c') 'Wed Apr 26 20:47:41 2006' >>> timeWrappedObj.gettimestr('m') 'Wed Apr 26 20:48:35 2006' >>> timeWrappedObj.gettimestr('a') 'Wed Apr 26 20:48:46 2006' 改进包装一个特殊对象 下一个例子,描述了一个包装文件对象的类。我们的类与一般带一个异常的文件对象行为完全 一样:在写模式中,字符串只有全部为大写时,才写入文件。 这里,我们要解决的问题是,当你正在写一个文本文件,其数据将会被一台旧电脑读取。很多 老式机器在处理时,严格要求大写字母,所以,我们要实现一个文件对象,其中所有写入文件的文 本会自动转化为大写,程序员就不必担心了。 事实上,唯一值得注意的不同点是并不使用open()内建函数,而是调用CapOpen 类时行初始化。 尽管,参数同 open()完全一样。 例 13.8 展示那段代码,文件名是 capOpen.py。下面看一下例子中是如何使用这个类的: >>> f = CapOpen('/tmp/xxx', 'w') >>> f.write('delegation example\n') >>> f.write('faye is good\n') >>> f.write('at delegating\n') >>> f.close() >>> f 例 13.8 包装文件对象(capOpen.py) Edit By Vheavens  Edit By Vheavens  这个类扩充了 Python FAQs中的一个例子,提供一个文件类对象,定制 write()方法,同时,给 文件对象授权其它的功能。 1 #!/usr/bin/env python 2 3 class CapOpen(object): 4 def __init__(self, fn, mode='r', buf=-1): 5 self.file = open(fn, mode, buf) 6 7 def __str__(self): 8 return str(self.file) 9 10 def __repr__(self): 11 return 'self.file' 12 13 def write(self, line): 14 self.file.write(line.upper()) 15 16 def __getattr__(self, attr): 17 return getattr(self.file, attr) 可以看到,唯一不同的是第一次对 CapOpen()的调用,而不是 open()。如果你正与一个实际文 件对象,而非行为像文件对象的类实例进行交互,那么其它所有代码与你本该做的是一样的。除了 write(),所有属性都已授权给文件对象。为了确定代码是否正确,我们加载文件,并显示其内容。 (注:可以使用 open()或 CapOpen(),这里因在本例中用到,所以选用 CapOpen()。) >>> f = CapOpen('/tmp/xxx', 'r') >>> for eachLine in f: ... print eachLine, ... DELEGATION EXAMPLE FAYE IS GOOD AT DELEGATING 13.16 新式类的高级特性 (Python 2.2+) 13.16.1 新式类的通用特性 我们已提讨论过有关新式类的一些特性。由于类型和类的统一,这些特性中最重要的是能够子 类化 Python 数据类型。其中一个副作用是,所有的 Python 内建的 “casting” 或转换函数现在都 Edit By Vheavens  Edit By Vheavens  是工厂函数。当这些函数被调用时,你实际上是对相应的类型进行实例化。 下面的内建函数,跟随 Python 多日,都已“悄悄地”(也许没有)转化为工厂函数: z int(), long(), float(), complex() z str(), unicode() z list(), tuple() z type() 还有,加入了一些新的函数来管理这些“散兵游勇”: z basestring()1 z dict() z bool() z set(),2 frozenset()2 z object() z classmethod() z staticmethod() z super() z property() z file() 这些类名及工厂函数使用起来,很灵活。不仅能够创建这些类型的新对象,它们还可以用来作 为基类,去子类化类型,现在还可以用于 isinstance()内建函数。使用 isinstance()能够用于替 换用烦了的旧风格,而使用只需少量函数调用就可以得到清晰代码的新风格。比如,为测试一个对 象是否是一个整数,旧风格中,我们必须调用 type()两次或者 import 相关的模块并使用其属性;但 现在只需要使用 isinstance(),甚至在性能上也有所超越: OLD (not as good): z if type(obj) == type(0)… z if type(obj) == types.IntType… BETTER: z if type(obj) is type(0)… EVEN BETTER: z if isinstance(obj, int)… z if isinstance(obj, (int, long))… z if type(obj) is int… 记住:尽管 isinstance()很灵活,但它没有执行“严格匹配”比较----如果 obj 是一个给定类 型的实例或其子类的实例,也会返回 True。但如果想进行严格匹配,你仍然需要使用 is 操作符。 Edit By Vheavens  Edit By Vheavens  1. Python 2.3 中新增。 2. Python 2.4 中新增。 请复习 13.12.2 节中有关 isinstance()的深入解释,还有在第 4 章中介绍这些调用是如何随同 Python 的变化而变化的。 13.16.2 __slots__类属性 字典位于实例的“心脏”。__dict__属性跟踪所有实例属性。举例来说,你有一个实例inst.它 有一个属性 foo,那使用 inst.foo 来访问它与使用 inst.__dict__['foo']来访问是一致的。 字典会占据大量内存,如果你有一个属性数量很少的类,但有很多实例,那么正好是这种情况。 为内存上的考虑,用户现在可以使用__slots__属性来替代__dict__。 基本上,__slots__是一个类变量,由一序列型对象组成,由所有合法标识构成的实例属性的集 合来表示。它可以是一个列表,元组或可迭代对象。也可以是标识实例能拥有的唯一的属性的简单 字符串。任何试图创建一个其名不在__slots__中的名字的实例属性都将导致 AttributeError 异常: class SlottedClass(object): __slots__ = ('foo', 'bar') >>> c = SlottedClass() >>> >>> c.foo = 42 >>> c.xxx = "don't think so" Traceback (most recent call last): File "", line 1, in ? AttributeError: 'SlottedClass' object has no attribute 'xxx' 这种特性的主要目的是节约内存。其副作用是某种类型的"安全",它能防止用户随心所欲的动态 增加实例属性。带__slots__属性的类定义不会存在__dict__了(除非你在__slots__中增加 '__dict__'元素)。更多有关__slots__的信息,请参见Python(语言)参考手册中有关数据模型章 节。 13.16.3 特殊方法__getattribute__() Python 类有一个名为__getattr__()的特殊方法,它仅当属性不能在实例的__dict__或它的类 (类的__dict__),或者祖先类(其__dict__)中找到时,才被调用。我们曾在实现授权中看到过使 用__getattr__()。 Edit By Vheavens  Edit By Vheavens  很多用户碰到的问题是,他们想要一个适当的函数来执行每一个属性访问,不光是当属性不能 找到的情况。这就是__getattribute__()用武之处了。它使用起来,类似__getattr__(),不同之处 在于,当属性被访问时,它就一直都可以被调用,而不局限于不能找到的情况。 如果类同时定义了__getattribute__()及__getattr__()方法,除非明确从__get-attribute__() 调用,或__getattribute__()引发了 AttributeError 异常,否则后者不会被调用. 如果你将要在此(译者注:__getattribute__()中)访问这个类或其祖先类的属性,请务必小心。 如果你在__getattribute__()中不知何故再次调用了__getattribute__(),你将会进入无穷递归。 为避免在使用此方法时引起无穷递归,为了安全地访问任何它所需要的属性,你总是应该调用祖先类 的同名方法;比如,super(obj,self).__getattribute__(attr)。此特殊方法只在新式类中有效。 同__slots__一样,你可以参考 Python(语言)参考手册中数据模型章节,以得到更多有关 __getattribute__()的信息。 13.16.4 描述符 描述符是 Python 新式类中的关键点之一。它为对象属性提供强大的 API。你可以认为描述符是 表示对象属性的一个代理。当需要属性时,可根据你遇到的情况,通过描述符(如果有)或者采用 常规方式(句点属性标识法)来访问它。 如你的对象有代理,并且这个代理有一个“get”属性(实际写法为__get__),当这个代理被调 用时,你就可以访问这个对象了。当你试图使用描述符(set)给一个对象赋值或删除一个属性 (delete)时,这同样适用。 __get__(),__set__(),__delete__()特殊方法 严格来说,描述符实际上可以是任何(新式)类,这种类至少实现了三个特殊方法 __get__(),__set__()及__delete__()中的一个,这三个特殊方法充当描述符协议的作用。刚才提到 过,__get__()可用于得到一个属性的值,__set__()是为一个属性进行赋值的,在采用 del 语句(或 其它,其引用计数递减)明确删除掉某个属性时会调__delete__()方法。三者中,后者很少被实现。 还有,也不是所有的描述符都实现了__set__()方法。它们被当作方法描述符,或更准确来说是, 非数据描述符来被引用。那些同时覆盖__get__()及__set__()的类被称作数据描述符,它比非数据 描述符要强大些。 The signatures for __get__(), __set__(), and __delete__() look like this: __get__(),__set__()及__delete__()的原型,如下: z def __get__(self, obj, typ=None) ==> value z def __set__(self, obj, val) ==> None Edit By Vheavens  Edit By Vheavens  z def __delete__(self, obj) ==> None 如果你想要为一个属性写个代理,必须把它作为一个类的属性,让这个代理来为我们做所有的 工作。当你用这个代理来处理对一个属性的操作时,你会得到一个描述符来代理所有的函数功能。 我们在前面的一节中已经讲过封装的概念。这里我们会进一步来探讨封装的问题。现在让我们来处 理更加复杂的属性访问问题,而不是将所有任务都交给你所写的类中的对象们。 __getattribute__() 特殊方法(二) 使用描述符的顺序很重要,有一些描述符的级别要高于其它的。整个描述符系统的心脏是 __getattribute__(),因为对每个属性的实例都会调用到这个特殊的方法。这个方法被用来查找类 的属性,同时也是你的一个代理,调用它可以进行属性的访问等操作。 回顾一下上面的原型,如果一个实例调用了__get__()方法,这就可能传入了一个类型或类的对 象。举例来说,给定类 X 和实例 x, x.foo 由__getattribute__()转化成: type(x).__dict__['foo'].__get__(x, type(x)) 如果类调用了__get__()方法,那么 None 将作为对象被传入(对于实例, 传入的是 self): X.__dict__['foo'].__get__(None, X) 最后,如果 super()被调用了,比如,给定Y为X的子类,然后用 super(Y,obj).foo 在 obj.__class__.__mro__中紧接类 Y 沿着继承树来查找类 X,然后调用: X.__dict__['foo'].__get__(obj, X) 然后,描述符会负责返回需要的对象。 优先级别 由于__getattribute__()的实现方式很特别,我们在此对__getattribute__()方法的执行方式 做一个介绍。因此了解以下优先级别的排序就非常重要了: z 类属性 z 数据描述符 z 实例属性 z 非数据描述符 z 默认为__getattr__() Edit By Vheavens  Edit By Vheavens  描述符是一个类属性,因此所有的类属性皆具有最高的优先级。你其实可以通过把一个描述符 的引用赋给其它对象来替换这个描述符。比它们优先级别低一等的是实现了__get__()和__set__() 方法的描述符。如果你实现了这个描述符,它会像一个代理那样帮助你完成所有的工作! 否则,它就默认为局部对象的__dict__的值,也就是说,它可以是一个实例属性。接下来是非 数据描述符。可能第一次听起来会吃惊,有人可能认为在这条“食物链”上非数据描述符应该比实 例属性的优先级更高,但事实并非如此。非数据描述符的目的只是当实例属性值不存在时,提供一 个 值 而已。 这 与以下 情 况类似 : 当在一 个 实例的 __dict__ 中 找不到 某 个属性 时 ,才 去 调 用 __getattr__()。 关于__getattr__()的说明,如果没有找到非数据描述符,那么__getattribute__()将会抛出一 个 AttributeError 异常,接着会调用__getattr__()做为最后一步操作,否则AttributeError 会 返 回给用户。 描述符举例 让我们来看一个简单的例子...用一个描述符禁止对属性进行访问或赋值的请求。事实上,以下 所有示例都忽略了全部请求,但它们的功能逐步增多,我们希望你通过每个示例逐步掌握描述符的 使用: class DevNull1(object): def __get__(self, obj, typ=None): pass def __set__(self, obj, val): pass 我们建立一个类,这个类使用了这个描述符,给它赋值并显示其值: >>> class C1(object): ... foo = DevNull1() ... >>> c1 = C1() >>> c1.foo = 'bar' >>> print 'c1.foo contains:', c1.foo c1.foo contains: None That was not too terribly exciting … how about one where the descriptor methods at least give some output to show what is going on? 这并没有什么有趣的 ... 让我们来看看在这个描述符中写一些输出语句会怎么样? Edit By Vheavens  Edit By Vheavens  class DevNull2(object): def __get__(self, obj, typ=None): print 'Accessing attribute... ignoring' def __set__(self, obj, val): print 'Attempt to assign %r... ignoring' % (val) 现在我们来看看修改后的结果: >>> class C2(object): ... foo = DevNull2() ... >>> c2 = C2() >>> c2.foo = 'bar' Attempt to assign 'bar'... ignoring >>> x = c2.foo Accessing attribute... ignoring >>> print 'c2.foo contains:', x c2.foo contains: None 最后,我们在描述符所在的类中添加一个占位符,占位符包含有关于这个描述符的有用信息: class DevNull3(object): def __init__(self, name=None): self.name = name def __get__(self, obj, typ=None): print 'Accessing [%s]... ignoring' % self.name) def __set__(self, obj, val): print 'Assigning %r to [%s]... ignoring' % val, self.name) 下面的输出结果表明我们前面提到的优先级层次结构的重要性,尤其是我们说过,一个完整的 数据描述符比实例的属性具有更高的优先级: >>> class C3(object): ... foo = DevNull3('foo') ... >>> c3 = C3() >>> c3.foo = 'bar' Assigning 'bar' to [foo]... ignoring Edit By Vheavens  Edit By Vheavens  >>> x = c3.foo Accessing [foo]... ignoring >>> print 'c3.foo contains:', x c3.foo contains: None >>> print 'Let us try to sneak it into c3 instance...' Let us try to sneak it into c3 instance... >>> c3.__dict__['foo'] = 'bar' >>> x = c3.foo Accessing [foo]... ignoring >>> print 'c3.foo contains:', x c3.foo contains: None >>> print "c3.__dict__['foo'] contains: %r" % \ c3.__dict__['foo'], "... why?!?" c3.__dict__['foo'] contains: 'bar' ... why?!? 请注意我们是如何给实例的属性赋值的。给实例属性 c3.foo 赋值为一个字符串“bar”。但由于 数据描述符比实例属性的优先级高,所赋的值“bar”被隐藏或覆盖了。 同样地,由于实例属性比非数据描述符的优先级高,你也可以将非数据描述符隐藏。这就和你 给一个实例属性赋值,将对应类的同名属性隐藏起来是同一个道理: >>> class FooFoo(object): ... def foo(self): ... print 'Very important foo() method.' ... >>> >>> bar = FooFoo() >>> bar.foo() Very important foo() method. >>> >>> bar.foo = 'It is no longer here.' >>> bar.foo 'It is no longer here.' >>> >>> del bar.foo >>> bar.foo() Very important foo() method. 这是一个直白的示例。我们将 foo 做为一个函数调用,然后又将它作为一个字符串访问,但我 们也可以使用另一个函数,而且保持相同的调用机制: Edit By Vheavens  Edit By Vheavens  >>> def barBar(): ... print 'foo() hidden by barBar()' ... >>> bar.foo = barBar >>> bar.foo() foo() hidden by barBar() >>> >>> del bar.foo >>> bar.foo() Very important foo() method. 要强调的是:函数是非数据描述符,实例属性有更高的优先级,我们可以遮蔽任一个非数据描 述符,只需简单的把一个对象赋给实例(使用相同的名字)就可以了。 我们最后这个示例完成的功能更多一些,它尝试用文件系统保存一个属性的内容,这是个雏形 版本。 第 1-10 行 在引入相关模块后,我们编写一个描述符类,类中有一个类属性(saved), 它用来记录描述符 访问的所有属性。描述符创建后,它将注册并且记录所有从用户处接收的属性名。 第 12-26 行 在获取描述符的属性之前,我们必须确保用户给它们赋值后才能使用。如果上述条件成立,接 着我们将尝试打开 pickle 文件以读取其中所保存的值。如果文件打开失败,将引发一个异常。文件 打开失败的原因可能有以下几种:文件已被删除了(或从未创建过),或是文件已损坏,或是由于某 种原因,不能被 pickle 模块反串行化。 第 18-38 行 将属性保存到文件中需要经过以下几个步骤:打开用于写入的 pickle 文件(可能是首次创建一 个新的文件,也可能是删掉旧的文件),将对象串行化到磁盘,注册属性名,使用户可以读取这些属 性值。如果对象不能被 pickle{待统一命名},将引发一个异常。注意,如果你使用的是 Python2.5 以前的版本,你就不能合并 try-except 和 try-finally 语句(第 30-38 行)。 例 13.9 使用文件来存储属性(descr.py) 这个类是一个雏形,但它展示了描述符的一个有趣的应用--可以在一个文件系统上保存属性 的内容。 1 #!/usr/bin/env python 2 Edit By Vheavens  Edit By Vheavens  3 import os 4 import pickle 5 6 class FileDescr(object): 7 saved = [] 8 9 def __init__(self, name=None): 10 self.name = name 11 12 def __get__(self, obj, typ=None): 13 if self.name not in FileDescr.saved: 14 raise AttributeError, \ 15 "%r used before assignment" % self.name 16 17 try: 18 f = open(self.name, 'r') 19 val = pickle.load(f) 20 f.close() 21 return val 22 except(pickle.InpicklingError, IOError, 23 EOFError, AttributeError, 24 ImportError, IndexError), e: 25 raise AttributeError, \ 26 "could not read %r: %s" % self.name 27 28 def __set__(self, obj, val): 29 f = open(self.name, 'w') 30 try: 31 try: 32 pickle.dump(val, f) 33 FileDescr.saved.append(self.name) 34 except (TypeError, pickle.PicklingError), e: 35 raise AttributeError, \ 36 "could not pickle %r" % self.name 37 finally: 38 f.close() 39 40 def __delete__(self, obj): 41 try: 42 os.unlink(self.name) Edit By Vheavens  Edit By Vheavens  43 FileDescr.saved.remove(self.name) 44 except (OSError, ValueError), e: 45 pass 第 40-45 行 最后,如果属性被删除了,文件会被删除,属性名字也会被注销。以下是这个类的用法示例: >>> class MyFileVarClass(object): ... foo = FileDescr('foo') ... bar = FileDescr('bar') ... >>> fvc = MyFileVarClass() >>> print fvc.foo Traceback (most recent call last): File "", line 1, in ? File "descr.py", line 14, in __get__ raise AttributeError, \ AttributeError: 'foo' used before assignment >>> >>> fvc.foo = 42 >>> fvc.bar = 'leanna' >>> >>> print fvc.foo, fvc.bar 42 leanna >>> >>> del fvc.foo >>> print fvc.foo, fvc.bar Traceback (most recent call last): File "", line 1, in ? File "descr.py", line 14, in __get__ raise AttributeError, \ AttributeError: 'foo' used before assignment >>> >>> fvc.foo = __builtins__ Traceback (most recent call last): File "", line 1, in ? File "descr.py", line 35, in __set__ raise AttributeError, \ AttributeError: could not pickle 'foo' 属性访问没有什么特别的,程序员并不能准确判断一个对象是否能被打包后存储到文件系统中 (除非如最后示例所示,将模块 pickle,我们不该这样做)。我们也编写了异常处理的语句来处理文 Edit By Vheavens  Edit By Vheavens  件损坏的情况。在本例中,我们第一次在描述符中实现__delete__()方法。 请注意,在示例中,我们并没有用到 obj 的实例。别把 obj 和 self 搞混淆,这个 self 是指描 述符的实例,而不是类的实例。 描述符总结 你已经看到描述符是怎么工作的。静态方法、类方法、属性(见下面一节),甚至所有的函数都 是描述符。想一想:函数是 Python 中常见的对象。有内置的函数、用户自定义的函数、类中定义的 方法、静态方法、类方法。这些都是函数的例子。 它们之间唯一的区别在于调用方式的不同。通常, 函数是非绑定的。虽然静态方法是在类中被定义的,它也是非绑定的。但方法必须绑定到一个实例 上,类方法必须绑定到一个类上,对不?一个函数对象的描述符可以处理这些问题,描述符会根据 函数的类型确定如何“封装”这个函数和函数被绑定的对象,然后返回调用对象。它的工作方式是 这样的:函数本身就是一个描述符,函数的__get__()方法用来处理调用对象,并将调用对象返回给 你。描述符具有非常棒的适用性,因此从来不会对 Python 自己的工作方式产生影响。 属性和 property()内建函数 属性是一种有用的特殊类型的描述符。它们是用来处理所有对实例属性的访问,其工作方式和 我们前面说过的描述符相似。“一般”情况下,当你使用点属性符号来处理一个实例属性时,其实 你是在修改这个实例的__dict__属性。 表面上来看,你使用 property()访问和一般的属性访问方法没有什么不同,但实际上这种访问 的实现是不同的 - 它使用了函数(或方法)。在本章的前面,你已看到在 Python 的早期版本中,我 们一般用__getattr__() 和 __setattr__() 来处理和属性相关的问题。属性的访问会涉及到以上特 殊的方法(和__getattribute__()),但是如果我们用 property()来处理这些问题,你就可以写一个 和属性有关的函数来处理实例属性的获取(getting),赋值(setting),和删除(deleting)操作,而不 必再使用那些特殊的方法了(如果你要处理大量的实例属性,使用那些特殊的方法将使代码变得很臃 肿)。 property()内建函数有四个参数,它们是 : property(fget=None, fset=None, fdel=None, doc=None) 请注意 property()的一般用法是,将它写在一个类定义中,property()接受一些传进来的函数 (其实是方法)作为参数。实际上,property()是在它所在的类被创建时被调用的,这些传进来的(作 为参数的)方法是非绑定的,所以这些方法其实就是函数! 下面的一个例子:在类中建立一个只读的整数属性,用逐位异或操作符将它隐藏起来: Edit By Vheavens  Edit By Vheavens  class ProtectAndHideX(object): def __init__(self, x): assert isinstance(x, int), \ '"x" must be an integer!' self.__x = ~x def get_x(self): return ~self.__x x = property(get_x) 我们来运行这个例子,会发现它只保存我们第一次给出的值,而不允许我们对它做第二次修改: >>> inst = ProtectAndHideX('foo') Traceback (most recent call last): File "", line 1, in ? File "prop.py", line 5, in __init__ assert isinstance(x, int), \ AssertionError: "x" must be an integer! >>> inst = ProtectAndHideX(10) >>> print 'inst.x =', inst.x inst.x = 10 >>> inst.x = 20 Traceback (most recent call last): File "", line 1, in ? AttributeError: can't set attribute 下面是另一个关于 setter 的例子: class HideX(object): def __init__(self, x): self.x = x def get_x(self): return ~self.__x def set_x(self, x): assert isinstance(x, int), \ '"x" must be an integer!' self.__x = ~x Edit By Vheavens  Edit By Vheavens  x = property(get_x, set_x) 本示例的输出结果: >>> inst = HideX(20) >>> print inst.x 20 >>> inst.x = 30 >>> print inst.x 30 属性成功保存到 x 中并显示出来,是因为在调用构造器给 x 赋初始值前,在 getter 中已经将~x 赋给了 self.__x. 你还可以给自己写的属性添加一个文档字符串,参见下面这个例子: from math import pi def get_pi(dummy): return pi class PI(object): pi = property(get_pi, doc='Constant "pi"') 为了说明这是可行的实现方法,我们在 property 中使用的是一个函数而不是方法。注意在调用 函数时 self 作为第一个(也是唯一的)参数被传入,所以我们必须加一个伪变量把 self 丢弃。下面 是本例的输出: >>> inst = PI() >>> inst.pi 3.1415926535897931 >>> print PI.pi.__doc__ Constant "pi" 你明白 properties 是如何把你写的函数(fget, fset 和 fdel)影射为描述符的__get__(), __set__(), 和__delete__()方法的吗?你不必写一个描述符类,并在其中定义你要调用的这些方法。 只要把你写的函数(或方法)全部传递给 property()就可以了。 在你写的类定义中创建描述符方法的一个弊端是它会搞乱类的名字空间。不仅如此,这种做法 Edit By Vheavens  Edit By Vheavens  也不会像 property()那样很好地控制属性访问。如果不用 property()这种控制属性访问的目的就不 可能实现。我们的第二个例子没有强制使用 property(),因为它允许对属性方法的访问(由于在类定 义中包含属性方法): >>> inst.set_x(40) # can we require inst.x = 40? >>> print inst.x 40 APNPC(ActiveState Programmer Network Python Cookbook) (http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183)上的一条精明的办 法解决了以下问题: z “借用”一个函数的名字空间 z 编写一个用作内部函数的方法作为 property()的(关键字)参数 z (用 locals())返回一个包含所有的(函数/方法)名和对应对象的字典 z 把字典传入 property(),然后 z 去掉临时的名字空间 这样,方法就不会再把类的名字空间搞乱了,因为定义在内部函数中的这些方法属于其它的 名字空间。由于这些方法所属的名字空间已超出作用范围,用户是不能够访问这些方法的,所以通 过使用属性 property()来访问属性就成为了唯一可行的办法。根据 APNPC 上方法,我们来修改这个 类: class HideX(object): def __init__(self, x): self.x = x @property def x(): def fget(self): return ~self.__x def fset(self, x): assert isinstance(x, int), \ '"x" must be an integer!' self.__x = ~x return locals() 我们的代码工作如初,但有两点明显不同:(1) 类的名字空间更加简洁,只有 ['__doc__', Edit By Vheavens  Edit By Vheavens  '__init__', '__module__', 'x'], (2), 用户不能再通过inst.set_x(40) 给属性赋值 ... 必须 使用 init.x = 40. 我们还使用函数修饰符 (@property) 将函数中的 x 赋值到一个属性对象。由于 修饰符是从 Python 2.4 版本开始引入的,如果你使用的是 Python 的早期版本 2.2.x 或 2.3.x,请 将修饰符@property 去掉,在 x()的函数声明后添加 x = property(**x())。 13.16.5 Metaclasses 和__metaclass__ 元类(Metaclasses)是什么? 元类可能是添加到新风格类中最难以理解的功能了。元类让你来定义某些类是如何被创建的, 从根本上说,赋予你如何创建类的控制权。(你甚至不用去想类实例层面的东西。)早在 Python1.5 的时代,人们就在谈论这些功能(当时很多人都认为不可能实现),但现在终于实现了。 从根本上说,你可以把元类想成是一个类中类,或是一个类,它的实例是其它的类。实际上, 当你创建一个新类时,你就是在使用默认的元类,它是一个类型对象。(对传统的类来说,它们的元 类是 types.ClassType.)当某个类调用 type()函数时,你就会看到它到底是谁的实例: class C(object): pass class CC: pass >>> type(C) >>> >>> type(CC) >>> >>> import types >>> type(CC) is types.ClassType True 什么时候使用元类? 元类一般用于创建类。在执行类定义时,解释器必须要知道这个类的正确的元类。解释器会先 寻找类属性__metaclass__,如果此属性存在,就将这个属性赋值给此类作为它的元类。如果此属性 没有定义,它会向上查找父类中的__metaclass__. 所有新风格的类如果没有任何父类,会从对象或 类型中继承。(type (object) 当然是类型). Edit By Vheavens  Edit By Vheavens  如果还没有发现__metaclass__属性,解释器会检查名字为__metaclass__的全局变量,如果它 存在,就使用它作为元类。否则, 这个类就是一个传统类,并用 types.ClassType 作为此类的元类。 (注意:在这里你可以运用一些技巧... 如果你定义了一个传统类,并且设置它的__metaclass__ = type,其实你是在将它升级为一个新风格的类!) 在执行类定义的时候,将检查此类正确的(一般是默认的)元类,元类(通常)传递三个参数(到构 造器):类名,从基类继承数据的元组,和(类的)属性字典。 谁在用元类? 元类这样的话题对大多数人来说属于理论化或纯面向对象思想的范畴,认为它在实际编程中没 有什么实际意义。从某种意义上讲这种想法是正确的;但最重要的请铭记在心的是,元类的最终使 用者不是用户,正是程序员自己。你通过定义一个元类来“迫使”程序员按照某种方式实现目标类, 这将既可以简化他们的工作,也可以使所编写的程序更符合特定标准。 元类何时被创建? 前面我们已提到创建的元类用于改变类的默认行为和创建方式。大多数 Python 用户都无须创 建或明确地使用元类。创建一个新风格的类或传统类的通用做法是使用系统自己所提供的元类的默 认方式。 用户一般都不会觉察到元类所提供的创建类(或元类实例化)的默认模板方式。虽然一般我们并 不创建元类,还是让我们来看下面一个简单的例子。(关于更多这方面的示例请参见本节末尾的文档 列表。) 元类示例 1 我们第一个关于元类的示例非常简单(希望如此)。它只是在用元类创建一个类时,显示时间标 签。(你现在该知道,这发生在类被创建的时候。) 看下面这个脚本。它包含的 print 语句散落在代码各个地方,便于我们了解所发生的事情: #!/usr/bin/env python from time import ctime print '*** Welcome to Metaclasses!' print '\tMetaclass declaration first.' Edit By Vheavens  Edit By Vheavens  class MetaC(type): def __init__(cls, name, bases, attrd): super(MetaC, cls).__init__(name, bases, attrd) print '*** Created class %r at: %s' % (name, ctime()) print '\tClass "Foo" declaration next.' class Foo(object): __metaclass__ = MetaC def __init__(self): print '*** Instantiated class %r at: %s' % ( self.__class__.__name__, ctime()) print '\tClass "Foo" instantiation next.' f = Foo() print '\tDONE' 当我们执行此脚本时,将得到以下输出: *** Welcome to Metaclasses! Metaclass declaration first. Class "Foo" declaration next. *** Created class 'Foo' at: Tue May 16 14:25:53 2006 Class "Foo" instantiation next. *** Instantiated class 'Foo' at: Tue May 16 14:25:53 2006 DONE 当你明白了一个类的定义其实是在完成某些工作的事实以后,你就容易理解这是怎么一回事情 了。 元类示例 2 在第二个示例中,我们将创建一个元类,要求程序员在他们写的类中提供一个__str__()方法的 实现,这样用户就可以看到比我们在本章前面所见到的一般 Python 对象字符串()更有用的信息。 如果您还没有在类中覆盖__repr__()方法,元类会(强烈)提示您这么做,但这只是个警告。如 果未实现__str__()方法,将引发一个 TypeError 的异常,要求用户编写一个同名方法。以下是关于 元类的代码: from warnings import warn Edit By Vheavens  Edit By Vheavens  class ReqStrSugRepr(type): def __init__(cls, name, bases, attrd): super(ReqStrSugRepr, cls).__init__( name, bases, attrd) if '__str__' not in attrd: raise TypeError("Class requires overriding of __str__()") if '__repr__' not in attrd: warn('Class suggests overriding of __repr__()\n', stacklevel=3) 我们编写了三个关于元类的示例,其中一个(Foo)重载了特殊方法__str__()和__repr__(),另一 个(Bar)只实现了特殊方法__str__(),还有一个(FooBar)没有实现__str__()和 __repr__(),这种 情况是错误的。完整的程序见示例 13.10. 执行此脚本,我们得到如下输出: $ python meta.py *** Defined ReqStrSugRepr (meta)class *** Defined Foo class sys:1: UserWarning: Class suggests overriding of __repr__() *** Defined Bar class Traceback (most recent call last): File "meta.py", line 43, in ? class FooBar(object): File "meta.py", line 12, in __init__ raise TypeError( TypeError: Class requires overriding of __str__() 示例 13.10 元类示例 (meta.py) 这个模块有一个元类和三个受此元类限定的类。每创建一个类,将打印一条输出语句。 1 #!/usr/bin/env python 2 3 from warnings import warn Edit By Vheavens  Edit By Vheavens  4 5 class ReqStrSugRepr(type): 6 7 def __init__(cls, name, bases, attrd): 8 super(ReqStrSugRepr, cls).__init__( 9 name, bases, attrd) 10 11 if '__str__' not in attrd: 12 raise TypeError( 13 "Class requires overriding of __str__()") 14 15 if '__repr__' not in attrd: 16 warn( 17 'Class suggests overriding of __repr__()\n', 18 stacklevel=3) 19 20 print '*** Defined ReqStrSugRepr (meta)class\n' 21 22 class Foo(object): 23 __metaclass__ = ReqStrSugRepr 24 25 def __str__(self): 26 return 'Instance of class:', \ 27 self.__class__.__name__ 28 29 def __repr__(self): 30 return self.__class__.__name__ 31 32 print '*** Defined Foo class\n' 33 34 class Bar(object): 35 __metaclass__ = ReqStrSugRepr 36 37 def __str__(self): 38 return 'Instance of class:', \ 39 self.__class__.__name__ 40 41 print '*** Defined Bar class\n' 42 43 class FooBar(object): Edit By Vheavens  Edit By Vheavens  44 __metaclass__ = ReqStrSugRepr 45 46 print '*** Defined FooBar class\n' 注意我们是如何成功声明 Foo 定义的;定义 Bar 时,提示警告__repr__()未实现;FooBar 的创 建没有通过安全检查,以致程序最后没有打印出关于 FooBar 的语句。另外要注意的是我们并没有创 建任何测试类的实例... 这些甚至根本不包括在我们的设计中。但别忘了这些类本身就是我们自己 的元类的实例。这个示例只显示了元类强大功能的一方面。 关于元类的在线文档众多,包括 Python 文档 PEPs 252 和 253,《What’s New in Python 2.2》 文档,Guido van Rossum 所写的名为“Unifying Types and Classes in Python 2.2”的文章。在 Python 2.2.3 发布的主页上你也可以找到相关文档的链接地址。 13.17 相关模块和文档 我们在本章已经对核心语言做了讲述,而 Python 语言中有几个扩展了核心语言功能的经典类。 这些类为 Python 数据类型的子类化提供了方便。 模块好比速食品,方便即食。我们曾提到类可以有特殊的方法,如果实现了这些特殊方法,就 可以对类进行定制,这样当对一个标准类型封装时,可以给实例带来和类型一样的使用效果。 UserList 和 UserDict,还有新的 UserString(从 Python1.6 版本开始引入)分别代表对列表、字 典、字符串对象进行封装的类定义模块。这些模块的主要用处是提供给用户所需要的功能,这样你 就不必自己动手去实现它们了,同时还可以作为基类,提供子类化和进一步定制的功能。Python 语 言已经为我们提供了大量有用的内建类型,但这种"由你自己定制"类型的附加功能使得 Python 语言 更加强大。 在第四章里,我们介绍了 Python 语言的标准类型和其它内建类型。types 模块是进一步学习 Python 类型方面知识的好地方,其中的一些内容已超出了本书的讨论范围。types 模块还定义了一 些可以用于进行比较操作的类型对象。(这种比较操作在 Python 中很常见,因为它不支持方法的重 载 - 这简化的语言本身,同时又提供了一些工具,为貌似欠缺的地方添加功能.) 下面的代码检查传递到 foo 函数的数据对象是否是一个整数或一个字符串,不允许其他类型出 现(否则会引发一个异常): def foo(data): if isinstance(data, int): print 'you entered an integer' elif isinstance(data, str): Edit By Vheavens  Edit By Vheavens  print 'you entered a string' else: raise TypeError, 'only integers or strings!' 最后一个相关模块是 operator 模块。这个模块提供了Python 中大多数标准操作符的函数版本。 在某些情况下,这种接口类型比标准操作符的硬编码方式更通用。 请看下边的示例。在你阅读代码时,请设想一下如果此实现中使用的是一个个操作符的话,那 会多写多少行代码啊? >>> from operator import * # import all operators >>> vec1 = [12, 24] >>> vec2 = [2, 3, 4] >>> opvec = (add, sub, mul, div) # using +, -, *, / >>> for eachOp in opvec: # loop thru operators ... for i in vec1: ... for j in vec2: ... print '%s(%d, %d) = %d' % \ ... (eachOp.__name__, i, j, eachOp(i, j)) ... add(12, 2) = 14 add(12, 3) = 15 add(12, 4) = 16 add(24, 2) = 26 add(24, 3) = 27 add(24, 4) = 28 sub(12, 2) = 10 sub(12, 3) = 9 sub(12, 4) = 8 sub(24, 2) = 22 sub(24, 3) = 21 sub(24, 4) = 20 mul(12, 2) = 24 mul(12, 3) = 36 mul(12, 4) = 48 mul(24, 2) = 48 mul(24, 3) = 72 mul(24, 4) = 96 div(12, 2) = 6 div(12, 3) = 4 Edit By Vheavens  Edit By Vheavens  div(12, 4) = 3 div(24, 2) = 12 div(24, 3) = 8 div(24, 4) = 6 上面这段代码定义了三个向量,前两个包含着操作数,最后一个代表程序员打算对两个操作数 进行的一系列操作。最外层循环遍历每个操作运算,而最内层的两个循环用每个操作数向量中的元 素组成各种可能的有序数据对。最后,print 语句打印出将当前操作符应用在给定参数上所得的运算 结果。 我们前面介绍过的模块都列在表 13.5 中 表 13.5 与类相关的模块 模块 说明 UserList 提供一个列表对象的封装类 UserDict 提供一个字典对象的封装类 UserString a 提供一个字符串对象的封装类;它又包括一个 MutableString 子类,如果有需 要,可以提供有关功能 types 定义所有 Python 对象的类型在标准 Python 解释器中的名字 operator 标准操作符的函数接口 a. 新出现于 Python 1.6 版本 在 Python FAQ 中,有许多与类和面向对象编程有关的问题。它对 Python 类库以及语言参考手 册都是很好的补充材料。关于新风格的类,请参考 PEPs252 和 253 和 Python2.2 以后的相关文档。 13.18 练习 13-1. 程序设计。请列举一些面向对象编程与传统旧的程序设计形式相比的先进之处。 13-2. 函数和方法的比较。函数和方法之间的区别是什么? 示例 13.11 金额转换程序 (moneyfmt.py) 字符串格式类用来对浮点数值进行"打包",使这个数值显示为带有正确符号的金额。 1 #!/usr/bin/env python 2 3 class MoneyFmt(object): 4 def __init__(self, value=0.0): # constructor #构造器 Edit By Vheavens  Edit By Vheavens  5 self.value = float(value) 6 7 def update(self, value=None): # allow updates #允许修改 8 ### 9 ### (a) complete this function 10 ### 11 12 def __repr__(self): # display as a float #显示为浮点数 13 return 'self.value' 14 15 def __str__(self): # formatted display #格式化显示 16 val = '' 17 18 ### 19 ### (b) complete this function... do NOT 20 ### forget about negative numbers!! 21 ### 22 23 return val 24 25 def __nonzero__(self): # boolean test 26 ### 27 ### (c) find and fix the bug 28 ### 29 30 return int(self.value) You will find the code skeleton for moneyfmt.py presented as Example 13.11. You will find a fully documented (yet incomplete) version of moneyfmt.py on the Web site. If we were to import the completed class within the interpreter, execution should behave similar to the following: >>> import moneyfmt >>> >>> cash = moneyfmt.MoneyFmt(123.45) >>> cash 123.45 >>> print cash $123.45 >>> Edit By Vheavens  Edit By Vheavens  >>> cash.update(100000.4567) >>> cash 100000.4567 >>> print cash $100,000.46 >>> >>> cash.update(-0.3) >>> cash -0.3 >>> print cash -$0.30 >>> repr(cash) '-0.3' >>> 'cash' '-0.3' >>> str(cash) '-$0.30' 13-3. 对类进行定制。写一个类,用来将浮点数值转换为金额。在本练习里,我们使用美国 货币,但读者也可以自选任意货币。 基本任务: 编写一个 dollarize()函数,它以一个浮点数值作为输入,返回一个字符串形式的 金额数。比如说: dollarize(1234567.8901) ==> ‘$1,234,567.89. dollarize()返回的金额数里应该允许有逗号(比如 1,000,000),和美元的货币符号。如果有负 号,它必须出现在美元符号的左边。完成这项工作后,你就可以把它转换成一个有用的类,名为 MoneyFmt。 MoneyFmt 类里只有一个数据值(即,金额),和五个方法(你可以随意编写其他方法)。__init__() 构造器对数据进行初始化,update()方法把数据值替换成一个新值,__nonzero__()是布尔型的,当 数据值非零时返回 True,__repr__()方法以浮点数的形式返回金额;而__str__()方法采用和 dollarize()一样的字符格式显示该值。 (a) 编写 update()方法,以实现数据值的修改功能。 (b) 以你已经编写的 dollarize()的代码为基础,编写__str__()方法的代码 (c) 纠正__nonzero__()方法中的错误,这个错误认为所有小于1 的数值,例如,50美分($0.50), 返回假值(False)。 (d) 附加题: 允许用户通过一个可选参数指定是把负数数值显示在一对尖括号里还是显示一个 负号。默认参数是使用标准的负号。 13-4. 用户注册。建立一个用户数据库(包括登录名、密码和上次登录时间戳)类(参考练习7-5 和 9-12),来管理一个系统,该系统要求用户在登录后才能访问某些资源。这个数据库类对用户进行 管理,并在实例化操作时加载之前保存的用户信息,提供访问函数来添加或更新数据库的信息。在 Edit By Vheavens  Edit By Vheavens  数据修改后,数据库会在垃圾回收时将新信息保存到磁盘。(参见__del__()). 13-5. 几何. 创建一个由有序数值对(x, y) 组成的 Point 类,它代表某个点的 X 坐标和 Y 坐 标。X 坐标和 Y 坐标在实例化时被传递给构造器,如果没有给出它们的值,则默认为坐标的原点。 13-6. 几何. 创建一个直线/直线段类。除主要的数据属性:一对坐标值(参见上一个练习)外, 它还具有长度和斜线属性。你需要覆盖__repr__()方法(如果需要的话,还有__str__()方法),使得 代表那条直线(或直线段)的字符串表示形式是由一对元组构成的元组,即,((x1, y1), (x2, y2)). 总结: __repr__ 将直线的两个端点(始点和止点)显示成一对元组 length 返回直线段的长度 - 不要使用"len", 因为这样使人误解它是整数。 slope 返回此直线段的斜率(或在适当的时候返回 None) 13-7. 数据类。提供一个 time 模块的接口,允许用户按照自己给定时间的格式,比如: “MM/DD/YY,” “MM/DD/YYYY,” “DD/MM/YY,” “DD/MM/ YYYY,” “Mon DD, YYYY,” 或是标准 的 Unix 日期格式:“Day Mon DD, HH:MM:SS YYYY” 来查看日期。你的类应该维护一个日期值,并 用给定的时间创建一个实例。如果没有给出时间值,程序执行时会默认采用当前的系统时间。还包 括另外一些方法: update() 按给定时间或是默认的当前系统时间修改数据值 display() 以代表时间格式的字符串做参数,并按照给定时间的格式显示: 'MDY' ==> MM/DD/YY 'MDYY' ==> MM/DD/YYYY 'DMY' ==> DD/MM/YY 'DMYY' ==> DD/MM/YYYY 'MODYY' ==> Mon DD, YYYY 如果没有提供任何时间格式,默认使用系统时间或 ctime()的格式。附加题: 把这个类和练习 6-15 结合起来。 13-8. 堆栈类。一个堆栈(Stack)是一种具有后进先出(last-in-first-out,LIFO)特性的数 据结构。我们可以把它想象成一个餐盘架。最先放上去的盘子将是最后一个取下来的,而最后一个 放上去的盘子是最先被取下来的。你的类中应该有 push()方法(向堆栈中压入一个数据项)和 pop() 方法(从堆栈中移出一个数据项)。还有一个叫 isempty()的布尔方法,如果堆栈是空的,返回布尔值 1,否则返回 0;一个名叫 peek()的方法,取出堆栈顶部的数据项,但并不移除它。 注意,如果你使用一个列表来实现堆栈,那么 pop()方法从 Python1.5.2 版本起已经存在了。那 就在你编写的新类里,加上一段代码检查 pop()方法是否已经存在。如果经检查 pop()方法存在,就 Edit By Vheavens  Edit By Vheavens  调用这个内建的方法;否则就执行你自己编写的 pop()方法。你很可能要用到列表对象;如果用到它 时,不需要担心实现列表的功能(例如,切片)。只要确保你写的堆栈类能够正确实现上面的两项功 能就可以了。你可以用列表对象的子类或自己写个类似列表的对象,请参考示例 6.2. 13-9. 队列类。一个队列(queue)是一种具有先进先出(first-in-first-out,FIFO)特性的数 据结构。一个队列就像是一行队伍,数据从前端被移除,从后端被加入。这个类必须支持下面几种 方法: enqueue() 在列表的尾部加入一个新的元素 dequeue() 在列表的头部取出一个元素,返回它并且把它从列表中删除。 请参见上面的练习和示例 6.3. 13-10. 堆栈和队列。编写一个类,定义一个能够同时具有堆栈(FIFO)和队列(LIFO)操作行为 的数据结构。这个类和 Perl 语言中数组相像。需要实现四个方法: shift() 返回并删除列表中的第一个元素,类似于前面的 dequeue()函数。 unshift() 在列表的头部"压入"一个新元素 push() 在列表的尾部加上一个新元素,类似于前面的 enqueue()和 push()方法。 pop() 返回并删除列表中的最后一个元素,与前面的 pop()方法完全一样。 请参见练习 13-8 和 13-9. 13-11. 电子商务。 你需要为一家 B2C(商业到消费者)零售商编写一个基础的电子商务引擎。你需要写一个针对顾客 的类 User, 一个对应存货清单的类 Item, 还有一个对应购物车的类叫 Cart. 货物放到购物车里,顾 客可以有多个购物车。同时购物车里可以有多个货物,包括多个同样的货物。 13-12 聊天室. 你对目前的聊天室程序感到非常失望,并决心要自己写一个,创建一家新的 因特网公司,获得风险投资,把广告集成到你的聊天室程序中,争取在 6 个月的时间里让收入翻五 倍,股票上市,然后退休。但是,如果你没有一个非常酷的聊天软件,这一切都不会发生。 你需要三个类: 一个 Message 类,它包含一个消息字符串以及诸如广播、单方收件人等其他信 息,一个User 类, 包含了进入你聊天室的某个人的所有信息。为了从风险投资者那里拿到启动资金, 你加了一个 Room 类,它体现了一个更加复杂的聊天系统,用户可以在聊天时创建单独的“聊天屋”, 并邀请其他人加入。附加题: 请为用户开发一个图形化用户界面应用程序。 13-13. 股票投资组合类.你的数据库中记录了每个公司的名字,股票代号,购买日期,购买 价格和持股数量。需要编写的方法包括:添加新代号(新买的股票)、删除代号(所有卖出股票),根 据当前价格(及日期)计算出的 YTD 或年回报率。请参见练习 7-6。 Edit By Vheavens  Edit By Vheavens  13-14. DOS. 为 DOS 机器编写一个 UNIX 操作界面的 shell。你向用户提供一个命令行,使得 用户可以在那里输入 Unix 命令,你可以对这些命令进行解释,并返回相应的输出,例如:“ls”命 令调用“dir”来显示一个目录中的文件列表,“more”调用同名命令(分页显示一个文件),“cat” 调 用 “type,” “cp” 调用“copy,” “mv” 调用 “ren,” “rm” 调用 “del,” 等. 13-15. 授权。示例 13.8 的执行结果表明我们的类 CapOpen 能成功完成数据的写入操作。在 我们的最后评论中,提到可以使用 CapOpen() 或 open()来读取文件中的文本。为什么呢?这两者使 用起来有什么差异吗? 13-16. 授权和函数编程。 (a) 请为示例 13.8 中的 CapOpen 类编写一个 writelines()方法。这个新函数将可以一次读入 多行文本,然后将文本数据转换成大写的形式,它与write()方法的区别和通常意思上的 writelines() 与 write()方法之间的区别相似。注意:编写完这个方法后,writelines()将不再由文件对象"代理"。 (b) 在 writelines()方法中添加一个参数,用这个参数来指明是否需要为每行文本加上一个 换行符。此参数的默认值是 False,表示不加换行符。 13-17. 数值类型子类化。在示例 13.3 中所看到的 moneyfmt.py 脚本基础上修改它,使得它 可以扩展 Python 的浮点类型。请确保它支持所有操作,而且是不可变的。 13-18. 序列类型子类化。模仿前面练习 13-4 中的用户注册类的解决方案,编写一个子类。 要求允许用户修改密码,但密码的有效期限是 12 个月,过期后不能重复使用。附加题:支持“相 似密码”检测的功能(任何算法皆可),不允许用户使用与之前 12 个月期间所使用的密码相似的任何 密码。 13-19. 映射类型子类化。假设在 13.11.3 节中字典的子类,若将 keys()方法重写为: def keys(self): return sorted(self.keys()) (a) What happens when keys() is called for a method? (a) 当方法 keys()被调用,结果如何? (b) Why is this, and what makes our original solution work? (b) 为什么会有这样的结果?如何使我们的原解决方案顺利工作? 13-20. 类的定制。改进脚本 time60.py,见 13.13.2 节,示例 13.3. (a) 允许“空”实例化: 如果小时和分钟的值没有给出,默认为零小时、零分钟。 (b) 用零占位组成两位数的表示形式,因为当前的时间格式不符合要求。如下面的示例,wed 应该输出为“12:05.” Edit By Vheavens  Edit By Vheavens  >>> wed = Time60(12, 5) >>> wed 12:5 (c)除了用 hours (hr) 和 minutes (min)进行初始化外,还支持以下时间输入格式: z 一个由小时和分钟组成的元组(10, 30) z 一个由小时和分钟组成的字典({'hr': 10, 'min': 30}) z 一个代表小时和分钟的字符串("10:30") 附加题: 允许不恰当的时间字符串表示形式,如 “12:5”. (d) 我们是否需要实现__radd__()方法? 为什么? 如果不必实现此方法,那我们什么时候可 以或应该覆盖它? (e) __repr__()函数的实现是有缺陷而且被误导的。我们只是重载了此函数,这样我们可以省 去使用 print 语句的麻烦,使它在解释器中很好的显示出来。但是,这个违背了一个原则:对于可估 值的 Python 表达式,repr()总是应该给出一个(有效的)字符串表示形式。12:05 本身不是一个合法 的 Python 表达式,但 Time60('12:05')是合法的。请实现它。 (f) 添加六十进制(基数是 60)的运算功能。下面示例中的输出应该是 19:15,而不是 18:75: >>> thu = Time60(10, 30) >>> fri = Time60(8, 45) >>> thu + fri 18:75 13-21.装饰符和函数调用语法。第 13.16.4 节末尾,我们使用过一个装饰函数符把 x 转化成一 个属性对象,但由于装饰符是 Python2.4 才有的新功能,我们给出了另一个适用于旧版本的语法: X = property (**x()). 执行这个赋值语句时到底发生了什么呢?为什么它和使用装饰符语句是等价的? Edit By Vheavens  Edit By Vheavens  执行环境 本章主题 z 可调用对象 z 代码对象 z 语句和内置函数 z 执行其他程序 z 终止执行 z 各类操作系统接口 z 相关模块 Edit By Vheavens  Edit By Vheavens  在 python 中有多种运行外部程序的方法,比如,运行操作系统命令或另外的 python 脚本,或 执行一个磁盘上的文件,或通过网络来运行文件。这完全取决于你想要干什么。有些特定的执行场 景包括: z 在当前脚本继续运行 z 创建和管理子进程 z 执行外部命令或程序 z 执行需要输入的命令 z 通过网络来调用命令 z 执行命令来创建需要处理的输出 z 执行其他的 Python 脚本 z 执行一系列动态生成的 Python 语句 z 导入 Python 模块 (和执行它顶层的代码) python 中,内建和外部模块都可以提供上述各种功能。程序员得根据实现的需要,从这些模块 中选择合适的处理方法。本章将对 python 执行环境进行全面的描述,但不会涉及如何启动 python 解释器和不同的命令行选项。读者可以从第二章中查阅到相关信息。 我们的 python 执行环境之旅从可调用对象开始,接着是代码对象,然后去看看什么样的 python 语句和内建函数适合支持我们需要的功能。执行其他程序的能力不仅大大增强了python 脚本的威力, 也节约了资源,因为重复实现这些代码肯定是不合逻辑的,更是浪费时间和人力。python 给当前脚 本环境提供了许多执行程序或者外部命令的机制,我们将介绍下最普遍的几个命令。接下来,我们 Edit By Vheavens  Edit By Vheavens  对 python 的受限执行环境作一个简短的概况,最后,介绍下各种终止执行的方法(而不是让程序正 常完成)。就从可调用对象开始我们的旅程吧。 14.1 可调用对象 许多的 python 对象都是我们所说的可调用的,即是任何能通过函数操作符“()”来调用的对象。 要调用可调用对象,函数操作符得紧跟在可调用对象之后。比方说,用“foo()”来调用函数"foo"。 可调用对象可以通过函数式编程接口来进行调用,如 apply(),filter(),map(),以及 reduce(),这 四个接口我们都在 11 章讨论过了。Python有 4 种可调用对象:函数,方法,类,以及一些类的实例。 记住这些对象的任何引用或者别名都是可调用的。 14.1.1 函数 我们介绍的第一种可调用的对象是函数。python有 3 种不同类型函数对象。第一种是内建函数。 内建函数(BIFs) BIF 是用 c/c++写的,编译过后放入 python 解释器,然后把它们作为第一(内建)名字空间的 一部分加载进系统。如前面章节所提到的,这些函数在_bulitin_模块里,并作为__builtins__模 块导入到解释器中。 表 14.1 内建函数属性 BIF 属性 描述 bif.__doc__ 文档字符串(或 None) bif.__name__ 字符串类型的文档名字 bif.__self__ 设置为 None(保留给 built-in 方法) bif.__module__ 存放 bif 定义的模块名字(或 None) BIF 有基础类型属性,其中一些独特的属性已列在表 14.1 中 你可以用 dir()列出函数的所有属性: >>> dir(type) ['__call__', '__class__', '__cmp__', '__delattr__', '__doc__', '__getattribute__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__setattr__', '__str__'] Edit By Vheavens  Edit By Vheavens  从内部机制来看,因为BIFs 和内建方法(BIMs)属于相同的类型,所以对 BIF 或者 BIM 调用 type() 的结果是: >>> type(dir) 注意这不能应用于工厂函数,因为 type()正好会返回产生对象的类型: >>> type(int) >>> type(type) 用户定义的函数(UDF) UDF(User-Defined Function,用户定义的函数)通常是用python 写的,定义在模块的最高级, 因此会作为全局名字空间的一部分(一旦创建好内建名字空间)装载到系统中。函数也可在其他的函 数体内定义,并且由于在 2.2 中嵌套作用域的改进,我们现在可以对多重嵌套作用域中的属性进行 访问。可以用 func_closure 属性来钩住在其他地方定义的属性。 表 14.2 用户自定义函数属性 UDF 属性 描述 udf.__doc__ 文档字符串(也可以用 udf.func_doc) udf.__name__ 字符串类型的函数名字(也可以用 udf.func_name) udf.func_code 字节编译的代码对象 udf.func_defaults 默认的参数元组 udf.func_globals 全局名字空间字典; 和从函数内部调用 globals(x)一样 udf.func_dict 函数属性的名字空间 udf.func_doc (见上面的 udf.__doc__) udf.func_name (见上面的 udf.__name__) udf.func_closure 包含了自由变量的引用的单元对象元组(自用变量在 UDF 中使用,但在别 处定义;参见 python[语言]参考手册) 如同上面的 BIFs,UDF也有许多的属性。UDF最让人感兴趣和最特殊的属性都列在下面的表 14.2 中 从内部机制来看,用户自定义的函数是“函数“类型的,如在下面的例子中用 type()表明的一 样: Edit By Vheavens  Edit By Vheavens  >>> def foo(): pass >>> type(foo) lambda 表达式和用户自定义对函数相比,略有不同。虽然它们也是返回一个函数对象,但是 lambda 表达式不是用 def 语句创建的,而是用 lambda 关键字: 因为 lambda 表达式没有给命名绑定的代码提供基础结构,所以要通过函数式编程接口来调用, 或把它们的引用赋值给一个变量,然后就可以直接调用或者再通过函数来调用。变量仅是个别名, 并不是函数对象的名字。 通过 lambda 来创建函数的对象除了没有命名之外,享有和用户自定义函数相同的属性;__name__ 或者 func_name 属性给定为字符串""。使用 type()工厂函数,我们来演示下 lambda 表达式 返回和用户自定义函数相同的函数对象。 >>> lambdaFunc = lambda x: x * 2 >>> lambdaFunc(100) 200 >>> type(lambdaFunc) 在上面的例子中,我们将表达式赋值给一个别名。我们也可以直接在一个 lambda 表达式上调 用 type(): >>> type(lambda:1) 我们快速的来看看 UDF 名字,使用上面的 lambdaFunc 和先前小节中的 foo(): >>> foo.__name__ 'foo' >>> lambdaFunc.__name__ '' 从 11.9 小节中我们可以看到,一旦函数声明以后(且函数对象可用),程序员也可以自定义函 数属性。所有的新属性变成 udf.__dict__对象的一部分。在本章的稍后内容中,我们将讨论获取含 有 python 代码的字符串并执行该代码。到了本章最后,会有一个组合例子,着重描写函数属性和 python 代码(字符串)的动态求值和执行语句。 Edit By Vheavens  Edit By Vheavens  14.1.2 方法 在 13 章,我们研究了方法。用户自定义方法是被定义为类的一部分的函数。许多 python 数据 类型,比如列表和字典,也有方法,这些被称为内建方法。为了进一步说明“所有权“的类型,方 法通过对象的名字和句点属性标识进行命名。 14.3 内建方法(BIM)属性 BIM 属性 描述 bim.__doc__ 文档字串 bim.__name__ 字符串类型的函数名字 bim.__self__ 绑定的对象 内建方法(BIMs) 在前面的小节中,我们讨论了内建方法与内建函数的类似之处。只有内建类型(BIT)有 BIM.正如 你在下面看到的,对于内建方法,type()工厂函数给出了和 BIF 相同的输出--注意,我们是如何提 供一个内建对象来访问 BIM: >>> type([].append) 此外,BIM 和 BIF 两者也都享有相同属性。不同之处在于 BIM 的__self__属性指向一个 Python 对象,而 BIF 指向 None。 对于类和实例,都能以该对象为参数,通过内建函数 dir()来获得他们的数据和方法属性。这也 可以用在 BIM 上: >>> dir([].append) ['__call__', '__class__', '__cmp__', '__delattr__', '__doc__', '__getattribute__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__self__', '__setattr__', '__str__'] 然而,不用多久就会发现,从功能上看,用实际的对象去访问其方法并不是非常有用,如最后 的例子。由于没有引用来保存这个对象,所以它立即被垃圾回收了。你处理这种访问的类型唯一的 用处就是显示 BIT 有什么方法。 Edit By Vheavens  Edit By Vheavens  用户定义的方法(UDM) UDM(User-defined method,用户定义的方法)包含在类定义之中,只是拥有标准函数的包装, 仅有定义它们的类可以使用。如果没有在子类定义中被覆盖掉,也可以通过子类实例来调用它们。 正如在 13 章解释的那样, UDM 与类对象是关联的(非绑定方法),但是只能通过类的实例来调用(绑 定方法)。无论UDMs 是否绑定,所有的 UMD 都是相同的类型——“实例方法“,如在下面例子看到 的 type()调用: >>> class C(object): # define class # 定义类 ... def foo(self): pass # define UDM # 定义 UDM ... >>> c = C() # instantiation # 实例化 >>> type(C) # type of class # 类的类别 >>> type(c) # type of instance # 实例的类别 >>> type(C.foo) # type of unbound method # 非绑定方法的类别 >>> type(c.foo) # type of bound method # 绑定方法的类别 表 11.4 中展示了 UDM 的属性。访问对象本身将会揭示你正在引用一个绑定方法还是非绑定方 法。正如你从下面看到的,绑定的方法揭示了方法绑定到哪一个实例。 >>> C.foo # unbound method object # 非绑定方法对象 >>> >>> c.foo # bound method object # 绑定方法对象 >>> c # instance foo()'s bound to # foo()实例被绑定到…… <__main__.C object at 0x00B42DD0> 表 14.4 用户自定义属性 UDM 属性 描述 udm.__doc__ 文档字符串(与 udm.im_fuc.__doc__相同) udm.__name__ 字符串类型的方法名字(与 umd.im_func.__name__相同) udm.__module__ 定义 udm 的模块的名字(或 none) udm.im_class 方法相关联的类(对于绑定的方法;如果是非绑定,那么为要求 udm 的类) Edit By Vheavens  Edit By Vheavens  udm.im_func 方法的函数对象(见 UDFs) udm.im_self 如果绑定的话为相关联的实例,如果非绑定位为 none 14.1.3 类 我们可以利用类的可调用性来创建实例。“调用”类的结果便是创建了实例,即大家所知道的实 例化。类有默认构造函数,该函数什么都不做,基本上只有一个 pass 语句。程序员可以通过实现 __int__()方法,来自定义实例化过程。实例化调用的任何参数都会传入到构造函数里。 >>> class C(object): def __init__(self, *args): print 'Instantiated with these arguments:\n', args >>> c1 = C() # invoking class to instantiate c1 Instantiated with these arguments: () >>> c2 = C('The number of the counting shall be', 3) Instantiated with these arguments: ('The number of the counting shall be', 3) 我们已经很熟悉实例化过程以及它是如何完成的,在这里将不再赘述。不过,一个新的问题是 如何让实例能够被调用。 14.1.4 类的实例 python 给类提供了名为__call__的特别方法,该方法允许程序员创建可调用的对象(实例)。默 认情况下,__call__()方法是没有实现的,这意味着大多数实例都是不可调用的。然而,如果在类 定义中覆盖了这个方法,那么这个类的实例就成为可调用的了。调用这样的实例对象等同于调用 __call__()方法。自然地,任何在实例调用中给出的参数都会被传入到__call()__中。……那么foo() 就和 foo.__call__(foo)的效果相同, 这里 foo 也作为参数出现,因为是对自己的引用,实例将自 动成为每次方法调用的第一个参数。如果 ___call___()有参数,比如,(self, arg),那么foo(arg) 就和调用 foo.__call__(foo, arg)一样。这里我们给出一个可调用实例的例子,和前面小节的例子 相似: >>> class C(object): ... def __call__(self, *args): ... print "I'm callable! Called with args:\n", args ... Edit By Vheavens  Edit By Vheavens  >>> c = C() # instantiation # 实例化 >>> c # our instance # 我们的实例 <__main__.C instance at 0x00B42DD0> >>> callable(c) # instance is callable #实例是可调用的 True >>> c() # instance invoked # 调用实例 I'm callable! Called with arguments: () >>> c(3) # invoked with 1 arg # 呼叫的时候给出一个参数 I'm callable! Called with arguments: (3,) >>> c(3, 'no more, no less') # invoked with 2 args # 呼叫的时候给出两个参数 I'm callable! Called with arguments: (3, 'no more, no less') 记住只有定义类的时候实现了__call__方法,类的实例才能成为可调用的。 14.2 代码对象 可调用的对象是 python 执行环境里最重要的部分,然而他们只是冰山一角。python语句,赋值, 表达式,甚至还有模块构成了更宏大的场面。这些可执行对象无法像可调用物那样被调用。更确切 地说,这些对象只是构成可执行代码块的拼图的很小一部分,而这些代码块被称为代码对象。 每个可调用物的核心都是代码对象,由语句,赋值,表达式,以及其他可调用物组成。察看一 个模块意味着观察一个较大的、包含了模块中所有代码的对象。然后代码可以分成语句,赋值,表 达式,以及可调用物。可调用物又可以递归分解到下一层,那儿有自己的代码对象。 一般说来,代码对象可以作为函数或者方法调用的一部分来执行,也可用 exec 语句或内建函数 eval()来执行。从整体上看,一个 python 模块的代码对象是构成该模块的全部代码。 如果要执行 python 代码,那么该代码必须先要转换成字节编译的代码(又称字节码)。这才是 真正的代码对象。然而,它们不包含任何关于它们执行环境的信息,这便是可调用物存在的原因, 它被用来包装一个代码对象并提供额外的信息。 还记得前面的小节中 UDF 的 udf.func_code 属性吗?呃,想不到吧?那就是代码对象。UDM 的 Edit By Vheavens  Edit By Vheavens  udm.im_func 函数对象又是怎么一回事呢?因为那也是一个函数对象,所以他同样有它自己的 udm.im_func.func_code 代码对象。这样的话,你会发现,函数对象仅是代码对象的包装,方法则是 给函数对象的包装。你可以到处看看。当研究到最底层,你会发现便是一个代码对象 14.3 可执行的对象声明和内建函数 Python 提供了大量的 BIF 来支持可调用/可执行对象,其中包括 exec 语句。这些函数帮助程序 员执行代码对象,也可以用内建函数 complie()来生成代码对象。 表 14.5 可执行对象和内建函数 内建函数和语句 描述 callable(obj) 如果 obj 可调用,返回 True,否则返回 FALSE compile(string,file, type) 从type 类型中创建代码对象;file 是代码存放的地方(通常设 为"") eval(obj, glo- bals=globals(), locals=locals()) 对 obj 进行求值,obj 是已编译为代码对象的表达式,或是一个 字符串表达式;可以给出全局或者/和局部的名字空间 exec obj 执行 obj、单一的 python 语句或者语句的集合,也就是说格式 是代码对象或者字符串;obj 也可以是一个文件对象(已经打开的有 效 python 脚本中) input(prompt='') 等同于 eval(raw_input(prompt=”)) 14.3.1 callable() callable()是一个布尔函数,确定一个对象是否可以通过函数操作符(())来调用。如果函数可 调用便返回 True,否则便是 False(对与 2.2 和较早的版本而言,分别是1和0)。这里有些对象及 其对应的 callable 返回值 >>> callable(dir) # built-in function # 内建函数 True >>> callable(1) # integer #整数 False >>> def foo(): pass ... >>> callable(foo) # user-defined function # 用户自定义函数 True >>> callable('bar') # string #字符串 Edit By Vheavens  Edit By Vheavens  False >>> class C(object): pass ... >>> callable(C) # class #类 True 14.3.2 compile() compile()函数允许程序员在运行时刻迅速生成代码对象,然后就可以用 exec 语句或者内建函 数 eval()来执行这些对象或者对它们进行求值。一个很重要的观点是:exec 和 eval()都可以执行字 符串格式的 Python 代码。当执行字符串形式的代码时,每次都必须对这些代码进行字节编译处理。 compile()函数提供了一次性字节代码预编译,以后每次调用的时候,都不用编译了。 compile 的三个参数都是必需的,第一参数代表了要编译的 python 代码。第二个字符串,虽然 是必需的,但通常被置为空串。该参数代表了存放代码对象的文件的名字(字符串类型)。compile 的 通常用法是动态生成字符串形式的 Python 代码, 然后生成一个代码对象——代码显然没有存放在 任何文件。 最后的参数是个字符串,它用来表明代码对象的类型。有三个可能值: 'eval' 可求值的表达式[和 eval()一起使用] 'single' 单一可执行语句[和exec 一起使用] 'exec' 可执行语句组[和 exec 一起使用] 可求值表达式 >>> eval_code = compile('100 + 200', '', 'eval') >>> eval(eval_code) 300 单一可执行语句 >>> single_code = compile('print "Hello world!"', '', 'single') >>> single_code >>> exec single_code Hello world! 可执行语句组 Edit By Vheavens  Edit By Vheavens  >>> exec_code = compile(""" ... req = input('Count how many numbers? ') ... for eachNum in range(req): ... print eachNum ... """, '', 'exec') >>> exec exec_code Count how many numbers? 6 0 1 2 3 4 5 在最后的例子中,我们第一次看到input()。一直以来,我们都是从raw_input()中读取输入的。 内建函数 input()是我们将在本章稍后讨论的一个快捷函数。 14.3.3 eval() eval()对表达式求值,后者可以为字符串或内建函数 complie()创建的预编译代码对象。这是 eval()第一个也是最重要的参数.......这便是你想要执行的对象。第二个和第三个参数,都为可选 的,分别代表了全局和局部名字空间中的对象。如果给出这两个参数,globals必须是个字典,locals 可以是任意的映射对象,比如,一个实现了__getitem__()方法的对象。(在 2.4 之前,local 必须是 一个字典)如果都没给出这两个参数,分别默认为 globals()和 locals()返回的对象,如果只传入 了一个全局字典,那么该字典也作为 locals 传入。好了,我们一起来看看 eval(): >>> eval('932') 932 >>> int('932') 932 在这种情况下,eval()和 int()都返回相同的结果:整数 932。然而,它们采用的方式却不尽相 同。内建函数 eval()接收引号内的字符串并把它作为 python 表达式进行求值。内建函数 int()接收 代表整数的字符串并把它转换为整数。这只有在该字符串只由字符串 932 组成的时候才会成功,而 该字符串作为表达式返回值 932,932 也是字符串”932”所代表的整数。当我们用纯字符串表达式 的时候,两者便不再相同了: >>> eval('100 + 200') Edit By Vheavens  Edit By Vheavens  300 >>> int('100 + 200') Traceback (innermost last): File "", line 1, in ? ValueError: invalid literal for int(): 100 + 200 在这种情况下,eval()接收一个字符串并把"100+200"作为表达式求值,当进行整数加法后,给 出返回值 300。而对 int()的调用失败了,因为字符串参数不是能代表整数的字符串, 因为在字符 串中有非法的文字,即,空格以及“+”字符。可以这样理解 eval()函数的工作方式:对表达式两端 的引号视而不见,接着假设“如果我是 python 解释器,我会怎样去观察表达式呢?”,换句话说, 如果以交互方式输入相同的表达式,解释器会做出怎么样的反应呢?按下回车后的结果应该和eval() 返回的结果相同。 14.3.4 exec