Python 3程序开发指南(第2版•修订版)

978-7-115-38338-9
作者: [美]马克 萨默菲尔德(Mark Summerfield)
译者: 王弘博孙传庆
编辑: 傅道坤王旭丹
分类: Python

图书目录:

详情

本书首先讲述了构成Python语言的8个关键要素,之后分章节对其进行了详尽的阐述,包括数据类型、控制结构与函数、模块、文件处理、调试、进程与线程、网络、数据库、正则表达式、GUI程序设计等各个方面,并介绍了其他一些相关主题。全书内容以实例讲解为主线,每章后面附有练习题,便于读者更好地理解和掌握所讲述的内容。 本书适合于作为Python语言教科书使用,对Python程序设计人员也有一定的参考价值。

图书摘要

版权信息

书名:Python 3程序开发指南(第2版•修订版)

ISBN:978-7-115-38338-9

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

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

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

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

• 著    [美] Mark Summerfield

  译    王弘博 孙传庆

  责任编辑 傅道坤

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

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

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

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

  反盗版热线:(010)81055315


Authorized translation from the English language edition, entitled Programming in Python 3, 9780321680563 by Mark Summerfield, published by Pearson Education, Inc, publishing as Addison-Wesley, Copyright © 2010 Pearson Education, Inc.

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc. CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD., and POSTS & TELECOMMUNICATIONS PRESS Copyright © 2010.

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签。无标签者不得销售。


Python 是一种脚本语言,在各个领域得到了日益广泛的应用。本书全面深入地对Python 语言进行了讲解。

本书首先讲述了构成Python 语言的8 个关键要素,之后分章节对其进行了详尽的阐述,包括数据类型、控制结构与函数、模块、文件处理、调试、进程与线程、网络、数据库、正则表达式、GUI 程序设计等各个方面,并介绍了其他一些相关主题。全书内容以实例讲解为主线,每章后面附有练习题,便于读者更好地理解和掌握所讲述的内容。

本书适合于作为Python 语言教科书使用,对Python 程序设计人员也有一定的参考价值。


在应用广泛的各种语言中,Python或许是最容易学习和最好使用的。Python代码很容易阅读和编写,并且非常清晰,而没有什么隐秘的。Python是一种表达能力非常强的语言,这意味着,在设计同样的应用程序时,使用Python进行编码所需要的代码量要远少于使用其他语言(比如C++或Java)的代码量。

Python是一种跨平台的语言:一般来说,同样的Python程序可以同时在Windows平台与UNIX类平台(比如Linux、BSD与Mac OS X)上运行——只需要将构成Python程序的单个或多个文件复制到目标机器上,而不需要“构建”或编译(Python是解释型语言)。当然,Python程序使用特定平台功能也是可能的,但通常很少需要这样做,因为几乎所有Python标准库与大多数第三方库都是完全跨平台的,或至少对用户是透明的。

Python的强大功能之一是带有一个非常完全的标准库,通过该标准库,我们可以方便地实现大量功能,比如,从Internet下载一个文件、对压缩的存档文件进行解压,或创建一个Web服务器,而这些貌似复杂的功能,只需要少数几行Python代码就可以实现。除标准库外,还有数以千计的第三方库,其中一些提供了比标准库更强大、更复杂的功能,比如,Twisted网络库与NumPy数值型库。其他一些库提供了极专业化的功能,因而没有包含在标准库中,比如,SimPy模拟包。大多数第三方库都可以通过Python Package Index,网址为http://pypi.python.org/pypi进行访问。

虽然本质上是一种面向对象语言,但是实际上Python可以用于进行过程型程序设计、面向对象设计,以及某种程度上的函数型程序设计。本书主要展示如何使用Python进行过程型程序设计与面向对象程序设计,也介绍了Python的函数型程序设计功能。

本书的目标是展示如何使用良好的Python 3惯用风格编写Python程序,在阅读本书之后,你就可以发现,本书是一本非常有用的Python 3语言索引。虽然与Python 2相比,Python 3所做的改进和改变是渐进的,而非革新,但是在Python 3中,Python 2中的一些既有做法变得不再合适或不再必要,因此必须介绍和使用Python 3中的一些新做法,以便充分利用Python 3的功能。毋庸置疑,Python 3优于Python 2;它构建于Python 2多年的实践基础上,并添加了大量的新功能(还摒弃了Python 2的一些不良特性)。与Python 2相比,使用Python 3更富于乐趣,更便利、容易和具有一致性。

本书旨在讲解Python语言本身,虽然中间也涉及很多标准Python库,但是没有全部介绍。不过这不是问题,因为在阅读本书之后,将具备充分的Python知识,读者可以自如地使用任意的标准库或任意第三方库,并可以创建自己的库模块。

本书适用于多种不同类型的读者,包括自学者、程序设计爱好者、学生、科学家、工程师,以及工作中需要进行程序设计的人,当然,也包括计算专业工作者和计算机科学家。要面对这些不同类型的读者,既让已具备丰富知识的读者不厌烦,又让经验不足的读者可以理解,因此,本书假定读者至少具备一定的程序设计经验(任何程序语言)。特别是,本书需要读者了解数据类型(比如数与字符串)、集合数据类型(比如集合与列表)、控制结构(比如if与while语句)以及函数。此外,有些实例与练习需要读者具备HTML markup的相关知识,后面某些更专业化的章节需要读者具备一定领域的知识,比如,数据库那一章需要读者具备基本的SQL知识。

在结构上,本书尽可能让读者阅读时最富有效率。在第1章结束时,读者应该就可以编写短小但有用的Python程序。后续的每一章都分别讲述一个新主题,在内容上通常都会比前一章更广、更深。这意味着,如果顺序阅读本书各章,在每一章结束后,都可以停止阅读,并利用该章讲解的知识编写完整的Python程序,当然,你也可以继续阅读以便学习更高级、更复杂的技术。出于这一考虑,有些主题在某一章中介绍,在后续的一章或几章中又进行了深入讲解。

讲解一门新的程序设计语言时,有两个关键的问题。第一个问题是:有时候,需要讲解某个特定概念时,会发现该概念依赖于另外一个概念,而这个概念反过来又直接或间接地依赖于这个“特定概念”。第二个问题是:在最开始的时候,由于读者对该语言毫无所知,或者只具备极为有限的知识,因此要给出有趣的、有用的实例或练习非常困难。在本书中,我们力图解决这两个问题。对第一个问题,首先要求读者具备一定的程序设计经验,了解基本的概念;对第二个问题,我们在第1章中就讲解了Python的“beautiful heart”——Python的8个关键要素,足以用于编写良好的程序。这种做法也有一个不足的地方:在前几章中,有些实例在风格上会有一点刻意为之的痕迹,这是因为这些实例中只是使用了到该章为止所讲解的知识,不过这种副作用越到后面的章节越弱,到第7章结束时,所有实例都使用完全自然的Python 3惯用风格编写。

本书所讲述的方法是完全实践型的,我们建议读者尝试书中讲述的每个实例,做好每一个练习,以便获取实际的动手经验。在可能的地方,本书都提供了虽然短小但是完整的程序,这些程序实例展现了真实的应用场景。本书所带实例、练习及其解决方案都可以在www.qtrac.eu/py3book.html处获取,并且都已经在Windows、Linux、Mac OS X等操作平台上的Python 3环境下进行了测试。

第1章,提出了Python的8个关键要素,这些要素足以用于编写完整的Python程序。本章描述了一些可用的Python程序设计环境,给出了两个小实例,这两个实例都是使用前面讲述的8个关键要素构建的。

第2章~第5章介绍了Python的过程型程序设计功能,包括基本数据类型与集合数据类型、很多有用的内置函数与控制结构,以及比较简单的文本文件处理功能。第5章展示了如何创建自定义模块与包,并提供了Python标准库概览,以便读者对Python提供的功能有充分的了解,避免重复工作。

第6章对使用Python进行面向对象程序设计进行了全面深入的讲解。由于面向对象程序设计是建立在过程型程序设计基础之上的,因此,此前几章讲述的过程型程序设计相关的知识仍然可以用于面向对象程序设计,比如,利用同样的数据类型、集合数据类型以及控制结构。

第7章主要讲述文件的读、写。对于二进制文件,包括压缩、随机存取;对于文本文件,包括人工分析以及正则表达式的使用。本章也包括了如何读、写XML文件,包括使用元素树、DOM(文档对象模型)以及SAX(用于XML的简单API)。

第8章回顾了前面一些章节中讲述的内容,探讨了数据类型、集合数据类型、控制结构、函数、面向对象程序设计等领域一些更高级的内容。本章还介绍了很多新功能、类以及高级技术,包括函数型程序设计——其中的内容有挑战性,但也很有用。

第9章与其他章节的不同之处在于,它不是介绍新的Python特性,而是讨论了用于调试、测试和profiling程序的技术和库。

余下的几章讲述了其他一些高级主题。第10章展示了如何将程序的工作负载分布在多个进程与线程上;第11章展示了如何使用Python的标准网络支持功能编写客户端/服务器应用程序;第12章讲解了数据库程序设计(包括键-值对DBM文件与SQL数据库);第13章讲述了Python的正则表达式mini-language,介绍了正则表达式模块;第14章讲解使用正则表达式,以及使用两种第三方模块(PyParsing和PLY)的解析技术;第15章介绍了GUI(图形用户界面)程序设计。

本书的大部分章都较长,这样是为了将所有相关资料放在一起,以便于查询引用,不过,各章都进一步划分为节、小节,因此,本书仍然是可以按照适合自己的节奏阅读的,比如,每次阅读一节或一个小节。

如果使用的是较新版本的Mac或UNIX类系统并及时更新,就应该已经安装了Python 3。要检查是否已经安装,可以在控制台(在Mac OS X上是Terminal.app)中输入命令python V(注意是大写的V),如果版本为3.X,就说明系统中已经安装了Python 3,而不需要自己再安装,如果不是,请继续阅读。

对Windows与Mac OS X系统,存在易于使用的图形界面安装包,只需要按照提示就可以一步一步地完成安装过程。安装工具包可以从www.python.org/download处获取,该网站为Windows系统提供了3个独立的安装程序,一般需要下载的是普通的“Windows ×86 MSI Installer”,除非确认自己的机器使用的是AMD64或Itanium处理器,这种情况需要下载处理器特定的安装程序。下载安装程序后,只需要运行并按提示进行操作,就可以安装好Python 3。

对Linux、BSD以及其他UNIX类系统,安装Python的最简单方法是使用该操作系统的软件包管理系统。大多数情况下,Python安装程序是以几个单独的软件包形式提供的。比如,在Fedora中,用于Python的安装包为python,用于IDLE(一个简单的开发环境)的安装包为python-tools。需要注意的是,只有在Fedora为更新的版本时(版本10或后续版本),这些安装包才是基于Python 3的。同样,对基于Debian的系统,比如Ubuntu,对应的安装包为python3与idle3。

如果没有适合自己操作系统的安装包,就需要从www.python.org/download处下载源程序,并从头编译Python。你可以下载source tarballs中的任意一个,并根据其文件格式选择不同的工具进行解压:如果下载的是gzipped tarball,则需要使用tar xvfz Python-3.0.tgz;如果下载的是bzip2 tarball,则需要使用tar xvfj Python-3.0.tar.bz2。配置与构建过程是标准的,首先切换到新创建的Python-3.0目录,运行./configure(如果需要本地安装,可以使用--prefix选项),之后运行make。

安装Python 3时,可能出现的一种情况是,在安装结束时弹出提示消息,声称不是所有的模块都已经安装,这通常意味着机器上缺少某些必要的库或头文件。这种情况可以通过单独安装相应程序包处理,比如,如果readline模块无法构建,可以使用包管理系统安装相应的开发库,如在基于Fedora的系统上安装readline-devel,在基于Debian的系统上安装readline-dev(遗憾的是,相关包的名字并不总是那么显而易见的)。安装了缺少的包之后,再次运行./configure与make。

成功构建之后,可以运行make test,以便确认是否一切正常——尽管这并非必需,并且可能需要花费一些时间。

如果使用了--prefix进行本地安装,那么只需要运行make install。你可能需要为python可执行程序添加软链接(如果使用的是--prefix=$HOME/local/python3,并且PATH中包含$HOME/bin目录,则需要ln -s ~/local/python3/bin/python3.0 ~/bin/py- thon3),为IDLE添加软链接也会带来不少方便(假定前提与上面的一样,则需要ln -s ~/local/python3/bin/idle ~/bin/idle3)。

如果不使用--prefix并具备root权限,应该以root用户登录,并执行make install。在基于sudo的系统(比如Ubuntu)上,则执行sudo make install。如果系统上已经存在Python 2,/usr/bin/python并不会改变,同时Python 3将以python3的形式存在,同样地,Python 3的IDLE以idle3的形式存在。

首先感谢读者对本书第一版的反馈,他们在反馈中给出了修改意见和建议。

其次要感谢的是本书的技术评审Jasmin Blanchette,他是一位计算机科学家、程序员,我们曾共同编写过两本C++/Qt书籍。Jasmin对章节布局的规划、对所有实例的建议与批评以及对本书的详细审阅,这一切都极大地提高了本书的质量。

Georg Brandl是一位一流的Python开发人员,也是一位负责创建Python的新文档工具链的文档编辑。Georg挑出了很多微妙的错误,并非常耐心、非常坚持地对其进行解释,直至可以被准确理解和纠正。他还对很多实例进行了改进。

Phil Thompson是一位Python专家,也是PyQt(可能是可用的Python GUI库中最好的)的创建者。Phil的敏锐洞察力,有时候甚至是带有挑战性的反馈,都促使我对本书的很多内容进行了澄清和纠正。

Trenton Schulz是Nokia的Qt Software(以前的Trolltech)部门的一位高级软件工程师,也是我以前撰写的所有书籍的有见地的评审,在本书的评审编辑中又一次给予了我宝贵的帮助。Trenton对本书的细致阅读与提出的大量宝贵建议,帮助我澄清了很多问题,在很大程度上提高了本书质量。

除上面提及的各位评审人员之外(他们都读完了整本书),David Boddie,Nokia的Qt Software的一位高级技术作者,也是一位经验丰富的Python老手和开源软件开发者,阅读了本书的部分章节并给出了有价值的回馈。

同时也要感谢Guido van Rossum,Python的创建者,感谢大量的Python社区,是他们的努力,使得Python(尤其是库文件)变得如此有用而好用。

还要感谢Jeff Kingston,Lout typesetting语言(我使用这种语言的时间超过10年)的创建者。

特别感谢本书的编辑Debra Williams Cauley,感谢她给予的支持,并再一次使得本书的整个编辑、出版过程尽可能顺畅;感谢Anna Popick,他将本书的生产过程管理得非常好;感谢校对人员Audrey Doyle再一次做了良好的工作。

最后也是最重要的是,感谢我的妻子Andrea,感谢她对我在凌晨4点起床,记录下编写本书的灵感,以及对代码进行纠正和测试时,所表现出来的忍耐,以及她的爱、忠诚和一如既往的支持。


1.1 创建并运行Python程序

1.2 Python的关键要素

本章提供了足以开始编写Python程序的信息。如果此时尚未安装Python,强烈建议读者先行安装Python,以便随时进行编程实践,获取实际经验,巩固所学的内容。

本章第1节展示了如何创建并执行Python程序。你可以使用自己最喜欢的普通文本编辑器来编写Python代码,但本节中讨论的IDLE程序设计环境提供的不仅是一个代码编辑器,还提供了很多附加的功能,包括一些有助于测试Python代码、调试Python程序的工具。

第2节介绍了Python的8个关键要素,通过这8个要素本身,就足以编写有用的程序。这8个要素在本书的后续章节中将全面涉及与讲解,随着本书内容的推进,这些要素将被Python的其他组成部分逐渐补充、完善。到本书结束时,读者将对Python语言有完整的了解,并充分利用该语言提供的所有功能编写自己的Python程序。

本章最后一节介绍了两个短小的程序,这两个小程序利用了第2节中介绍的Python特性的一部分,以便读者可以及时尝试Python程序设计。

要编写Python代码,可以使用任意能加载与保存文本(使用ASCII或UTF-8 Unicode字符编码)的普通文本编辑器。默认情况下,Python文件使用UTF-8字符编码,UTF-8是ASCII的超集,可以完全表达每种语言中的所有字符。通常,Python文件的扩展名为.py,不过在一些UNIX类系统上(比如Linux与Mac OS X),有些Python应用程序没有扩展名,Python GUI(图形用户界面)程序的扩展名则为.pyw(特别是在Windows与Mac OS X上)。在本书中,我们总是使用.py作为Python控制台程序与Python模块的扩展名,使用.pyw作为GUI程序的扩展名。本书中提供的所有实例可以不需修改地在安装Python 3的所有平台上运行。

为确认系统已经正确安装Python,也为了展示经典的第1个程序,在普通文本编辑器(Windows记事本即可,后面我们会使用更好的编辑器)中创建一个名为hello.py的程序,其中包含如下一些内容:

#!/usr/bin/env python3
print("Hello", "World!")

第1行为注释。在Python中,注释以#开始,作用范围为该行(后面我们将解释更隐秘的一些注释信息)第2行为空行,Python会忽视空行,但空行通常有助于将大块代码分割,以便于阅读。第3行为Python代码,其中调用了print()函数,该函数带2个参数,每个参数的类型都是str(字符串,即一个字符序列)。

.py文件中的每个语句都是顺序执行的,从第1条语句开始,逐行执行。这与其他一些语言是不同的,比如,C++与Java一般是从某个特定函数或方法(带有函数或方法名)开始执行。当然,下一节讨论Python控制结构时我们将看到,Python程序的控制流也是可以改变的。

这里,我们假定Windows用户将其Python代码保存在C:\py3eg目录下,UNIX(包括UNIX、Linux与Mac OS X)用户将其Python代码保存在$HOME/py3eg目录下。输入上面的代码后,将其保存在py3eg目录,退出文本编辑器。

保存了程序之后,就可以运行该程序了。Python程序是由Python解释器执行的,通常在控制台窗口内进行。在Windows系统上,控制台窗口称为“控制台”、“DOS提示符”或“MS-DOS提示符”,或其他类似的称谓,通常可以通过“开始”、“所有程序”、“附件”这一顺序打开。在Mac OS X上,控制台是由Terminal.app程序(默认情况下在应用程序/工具这一目录下)提供的,通过Finder可以进行访问。在其他UNIX系统上,可以使用xterm或窗口环境提供的控制台,比如konsole或gnome- terminal。

启动一个控制台,在Windows系统上,输入如下命令(前提是假定Python安装在默认位置)——控制台的输出以粗体展示,输入的命令以细体展示。

C:\>cd c:\py3eg
C:\py3eg\>C:\Python30\python.exe hello.py

由于cd(切换目录)命令是采用绝对路径的,因此从哪个目录启动并不会影响程序执行。

UNIX用户需要输入如下命令(假定Python 3在PATH下):

$ cd $HOME/py3eg
$ python3 hello.py

上面两种情况下,输出应该是相同的:

Hello World!

需要注意的是,除非特别声明,Python在Mac OS X上的行为与在其他UNIX系统上是相同的。实际上,提及“UNIX”时,通常意味着Linux、BSD、Mac OS X以及大多数其他UNIX系统与UNIX类系统。

虽然上面的程序只有一行可执行语句,但是通过运行该程序,我们仍然可以推断出关于print()函数的一些信息。首先,print()函数是Python语言内置的一部分——我们不需要从某个库文件中对其进行“import”或“include”,就可以直接引用该函数。此外,该函数使用一个空格分隔其打印项,在最后一个打印项打印完成后,打印一个新行。后面我们将看到,这些默认的行为是可以改变的。另一个值得注意的情况是,print()可以按我们的需要赋予其很多或很少的参数。

要输入这样复杂的命令行才能引用我们的Python程序,很快就会让人乏味,幸运的是,无论在Windows还是UNIX系统上,都有更便利的方法。假定我们在py3eg目录下,在Windows系统上,只需要输入:

C:\py3eg\>hello.py

在控制台中输入扩展名为.py的文件名时,Windows会使用其注册表中的文件关联自动调用Python解释器。

遗憾的是,并不是总可以使用这种便利,因为有些Windows版本存在bug,有时会影响文件关联调用程序的执行。这并不是Python特有的问题,其他一些解释器,甚至.bat文件也会受到这一bug的影响,如果出现这一问题,可直接调用Python,而不是依赖于文件关联。

如果在Windows上的输出为:

('Hello', 'World!')

那么说明系统上存在Python 2,并且调用的是Python 2,而非Python 3。对于这种情况,一种解决方法是将.py文件的文件关联从Python 2改为Python 3,另一种方法(不是很方便,但很安全)是将Python 3解释器设置在路径中(假定Python 3安装在默认位置),并且每次显式地执行:

C:\py3eg\>path=c:\python31;%path%
C:\py3eg\>python hello.py

或许更方便的方法是创建一个py3.bat文件,其中只包含一行代码:path=c:\python 30;%path%,将该文件保存在C:\Windows目录下。之后,在需要启动控制台运行Python 3程序时,都先执行py3.bat。或者也可以让py3.bat自动执行,为此,需要修改控制台属性(在“开始”菜单中找到控制台,之后鼠标右击,会弹出其属性对话框),在“快捷方式”选项卡中的“目标”处,附加文本“/u /k c:\windows\py3.bat”(注意“/u”、“/k”选项前、后以及之间的空格,并确认这些内容添加在“cmd.exe”之后)。

在UNIX上,必须首先给该程序赋予可执行权限,之后才能运行该程序:

$ chmod +x hello.py
$ ./hello.py

当然,只需要运行一次chmod命令,之后就可以简单地通过./hello.py来运行该程序。

在UNIX上,当某程序在控制台中被引用时,该文件的头两个字节先被读入。如果这两个字节是ASCII字符#!,shell就会认为该文件将要由解释器执行,并且该文件的首行指定了要使用哪个解释器。该行称为shebang(shell执行)行,如果存在,就必须为可执行文件的首行。

shebang行通常呈现为如下两种形式之一:

#!/usr/bin/python3

#!/usr/bin/env python3

如果是第一种形式,就会使用指定的解释器。这种形式对将由Web服务器运行的Python程序是必要的,尽管指定的路径可能与这里给出的不同。如果是第二种形式,就会使用在shell当前环境中发现的第一个python3解释器。第二种形式具有更强的适应性,因为这种情况考虑了Python 3解释器位于/usr/bin之外(比如,安装在/usr/local/bin或$HOME目录之下)的可能性。在Windows系统中,shebang行并非是必需的(但没有坏处)。在本书中,所有实例都带有第二种形式的shebang行,但是没有明确给出。

需要注意的是,对UNIX系统,我们假定在PATH路径下,Python 3的可执行程序名(或到该名的软链接)是python3。如果不是这种情况,就需要改变实例中的shebang行,以便使用正确的程序名(如果使用的是第一种形式,就需要正确的名称与路径),或在PATH环境变量中的适当位置创建从Python 3可执行程序到python3的软链接。

很多功能强大的普通文本编辑器(比如Vim或Emacs)都带有对编辑Python程序的内置支持。典型情况下,这种支持包括彩色的语法高亮显示,以及对相关代码行的正确缩排与非缩排。另一种方法是使用IDLE Python程序设计环境。在Windows系统与Mac OS X系统上,IDLE是默认安装的;在UNIX系统上,正如简介中所描述的,IDLE是以一个单独的软件包形式提供的。

如图1-1中的快照所示,IDLE有一个相当“复古”的外观,使我们回退到在UNIX与Windows 95上使用Motif的时代。这是因为IDLE使用了基于Tk的Tkinter GUI库(第15章对其进行介绍),而没有使用功能更强大的GUI库,比如PyGtk、PyQt或wxPython。之所以使用Tkinter,是历史原因、自由许可协议条件以及Tkinter比其他GUI库更小等多种因素共同造成的。另一方面,作为Python标准配置组成部分的IDLE非常简单,容易学习和使用。

图1-1 IDLE的Python Shell

IDLE提供了3个关键功能:输入Python表达式与代码,并在Python Shell中直接查看结果;代码编辑器,提供了Python特定的彩色语法高亮显示功能与对代码缩排的支持;调试器,可用于单步跟进代码,识别并纠正其中存在的bug。在对简单算法、代码段以及正则表达式进行实验时,Python Shell尤其有用,当然也可以用作功能非常强大、灵活的计算器。

还有其他几种Python开发环境也可以使用,但我们建议使用IDLE,或至少最初使用IDLE。另一种方法是在普通的文本编辑器中创建程序,并调用print()进行调试。

调用Python解释器而不指定Python程序也是可以的。如果这样做,就会以交互模式启动解释器,在这种模式下,也可以输入Python语句并查看结果,就像使用IDLE的 Python Shell窗口一样,并且提示符也同样是>>>。但是,IDLE更易于使用,因此,建议使用IDLE对代码段进行试验。书中所展示的那些简短的交互式实例都是假定读者在交互式Python解释器或IDLE的Python Shell中输入的。

现在,我们已经学习了如何创建并运行Python程序,但显然我们所知甚少,仅仅学习了只使用一个函数的程序。通过下一节的学习,我们将大幅增加Python知识,并有能力创建虽然短小但是很有用的Python程序(这是本章最后一节将要做的事情)。

在本节中,我们将学习Python的8个关键要素,下一节中,我们将展示如何借助这些要素编写实际的小程序。关于本节中讲述的各关键要素,都有更多的内容需要阐述,因此,阅读本节的内容时,有时候你会觉得Python似乎遗失了一些内容,使得很多工作只能以一种冗繁的模式完成,如果使用前向索引或索引表格中的内容,那么你几乎总是可以发现Python具备你需要的特性,并且可以更紧凑的表达方式来完成当前展示的工作方式——还有很多其他内容。

任何程序语言都必须能完成的基本任务是表示数据项。Python提供了几种内置的数据类型,现在我们只关注其中的两种。Python使用int类型表示整数(正整数或负整数),使用str类型表示字符串(Unicode字符序列)。下面给出几个整数类型与字符串类型变量实例:

-973
210624583337114373395836055367340864637790190801098222508621955072
0
"Infinitely Demanding"
'Simon Critchley'
'positively αβγ€÷©'
''

顺便说一下,上面第二个整数代表的是2217。Python所能表示的整数大小只受限于机器内存,而非固定数量的字节数。字符串可以使用双引号或单引号封装——只要字符串头尾使用的符号是对称的。由于Python使用的是Unicode编码,因此字符串中的符号不局限于ASCII字符,比如上面倒数第二个字符串。空字符串则只是使用引号,中间没有任何内容。

Python使用方括号([])来存取字符串等序列中的某一项,比如,在Python Shell中(交互式的解释器,或IDLE),有如下的输入和输出信息——Python Shell的输出是以粗体展示的,输入则是以细体展示的:

>>> "Hard Times"[5]
'T'
>>> "giraffe"[0]
'g'

传统上,Python Shell使用>>>作为其提示符,当然这并非一成不变。方括号存取这种语法适用于任意数据类型(只要构成一个序列)的数据项,比如字符串或列表。这种语法的一致性是Python之所以如此美丽的原因之一。需要注意的是,Python语法中,索引位置是从0开始计数的。

在Python中,str类型与基本的数值类型(比如int)都是固定的——也就是说,一旦设定,其值就不能改变。乍一看,这似乎是一个相当奇怪的限制,但是Python语法的特点使这一限制在实践中并不会成为问题。之所以提及这一点,唯一的原因是想说明:虽然可以使用方括号取回字符串中某给定索引位置处的字符,但是不能将其设置为新字符(注意,在Python中,字符就是指长度为1的字符串)。

如果需要将一个数据项从某种类型转换为另一种类型,那么可以使用语法datatype (item),例如:

>>> int("45")
45
>>> str(912)
'912'

int()转换可以允许头尾处带有空格,因此,int(" 45 ")也是正确的。str()转换几乎可以应用于所有数据项。在第6章中可以看到,我们可以轻易地使自定义的数据类型支持str()转换,也可以支持int()转换或其他转换——只要这种转换是有意义的。如果转换失败,就会给出异常信息——我们将在要素5中简要介绍异常处理,在第4章中对其进行全面介绍。

字符串与整数这两种数据类型和其他一些内置的数据类型与某些来自Python标准库的数据类型一起在第2章中进行全面的讲解。第2章中还介绍了可用于固定序列(比如字符串)的操作。

定义了数据类型之后,接下来要做的事情就是定义存储某种类型数据的变量,但Python没有这样的变量,而是使用“对象引用”。对固定对象(比如intS与strS)而言,变量与对象引用之间没有可察觉的差别。对于可变对象,则存在差别,但是在实际工作中很少有影响。在后面的讲解中,我们将交替地使用术语“变量”与“对象引用”。

下面看几个tiny实例,并对其中的某些细节进行讨论。

x = "blue"
y = "green"
z = x

在上面的几条语句中,语法都是简单的objectReference = value。这里不需要预先的声明语句,也没有必要指定值的类型。执行上面的第一条语句时,Python会创建一个str对象,其文本内容为“blue”,同时还创建了一个名为x的对象引用,x引用的就是这个str对象。出于实用性的目的,我们可以说“变量x已经被分配了‘blue’这一字符串”;第二条语句是类似的;第三条语句创建了一个名为z的新对象引用,并将其设置为对象引用x所指向的相同对象(这里是包含文本“blue”的str对象)。

在其他一些语言中,操作符“=”与变量分配操作符是不一致的。在Python中,“=”的作用是将对象引用与内存中的某对象进行绑定。如果对象引用已经存在,就简单地进行重绑定,以便引用“=”操作符右面的对象;如果对象引用尚未存在,就由“=”操作符创建对象引用。

让我们继续对上面的x、y、z实例进行讲解,并进行一些重绑定操作——如前面所讲述的,以“#”引导的是注释语句,并作用到该行最后。

print(x, y, z) # prints: blue green blue
z = y
print(x, y, z) # prints: blue green green
x = z
print(x, y, z) # prints: green green green

在第4条语句(x=z)执行之后,所有3个对象引用实际上引用的都是同一个str。由于不存在更多的对字符串“blue”的对象引用,因此Python可以对其进行垃圾收集处理。

图1-2通过图形化方式展示了对象与对象引用之间的关系。

图1-2 对象引用与对象

用于对象引用的名称(称为“标识符”)有一些限制,尤其是不能与任何Python关键字相同,并且必须以字母或下划线引导,其后跟随0个或多个非空格字符、下划线或数字。标识符没有长度限制,字母与数字指的是Unicode编码中所定义的,也就是说,包含但不仅仅限于ASCII编码定义的字母与数字(“a”、“b”、……、“z”、“A”、“B”、……、“Z”、“0”、“1”、……、“9”)。此外,Python标识符是大小写敏感的,因此,LIMIT、Limit与limit是3个不同的标识符。第2章中会给出进一步的详细信息与一些稍微有些特别的实例。

Python使用“动态类型”机制,也就是说,在任何时刻,只要需要,某个对象引用都可以重新引用一个不同的对象(可以是不同的数据类型)。使用强类型的语言(比如C++、Java),只允许执行与某种特定数据类型绑定的操作,Python也适用于这一约束,但在Python中,由于使用的是动态类型机制,因此有效的操作是可以改变的,比如某个对象引用可以重新绑定到不同数据类型的对象。例如:

route = 866
print(route, type(route)) # prints: 866 <class 'int'>
route = "North"
print(route, type(route)) # prints: North <class 'str'>

这里我们创建了一个称为route的对象引用,并将其设置为引用一个新的int型数值866。对于route,我们可以使用“/”,因为除法对整数而言是一个有效的操作符。之后,我们重用route这一对象引用,并用其引用一个新的str变量,值为“North”,int对象将进入垃圾收集流程,这是因为没有对象引用再对其进行引用。如果此时再使用“/”,就会导致产生一个TypeError,因为对字符串而言,“/”并不是一个有效的操作符。

type()函数会返回给定数据项的数据类型(也称为“类”)——在测试与调试时,这一功能是非常有用的,但是在实际的应用代码中并不常见,因为存在一种更好的替代方案,我们将会在第6章介绍。

如果在交互式解释器环境下(或者在类似于IDLE环境提供的Python Shell环境下)执行Python代码,只需要简单地输入对象引用名,就足以打印出其值,例如:

>>> x = "blue"
>>> y = "green"
>>> z = x
>>> x
'blue'
>>> x, y, z
('blue', 'green', 'blue')

这种方式比调用print()函数打印值信息要方便得多,但是只有在交互式地使用Python时才有效——我们所编写的任意程序与模块都必须使用print()或类似函数才能产生输出信息。在上面的最后一个实例中,输出信息包含在圆括号中,数据项之间以逗号分隔,这意味着输出的是元组,也就是有序的固定对象序列。下一个要素将对元组进行进一步讲述。

通常,将整个的数据项组合在一起会带来不少方便。Python提供了几种组合数据类型,包括关联数组与集合等类型,这里我们只讨论其中的两种:元组与列表。Python元组与列表可用于存储任意数量、任意类型的数据项。元组是固定的,创建之后就不能改变;列表是可变的,在需要的时候,可以插入或移除数据项。

元组使用逗号创建,例如:

>>> "Denmark","Finland", "Norway", "Sweden"
('Denmark', 'Finland', 'Norway', 'Sweden')
>>> "one",
('one',)

在输出元组时,Python使用圆括号将其封装在一起。很多程序员模仿这种机制,总是将自己定义的元组常值包括在圆括号中。如果某个元组只有一个数据项,又需要使用圆括号,就仍然必须使用逗号,比如(1,)。空元组则使用空的()创建。逗号还可以用于在函数调用时分隔参数,因此,如果需要将元组常值作为参数传递,就必须使用括号对其进行封装,以免混淆。

下面给出了一些元组实例:

[1, 4, 9, 16, 25, 36, 49]
['alpha', 'bravo', 'charlie', 'delta', 'echo']
['zebra', 49, -879, 'aardvark', 200]
[]

列表可以使用方括号([])创建,就像上面这些实例,第四个实例展示的是一个空列表。后面我们会看到,还有其他一些创建列表的方法。

实质上,列表与元组并不真正存储数据项,而是存放对象引用。创建列表与元组时(以及在列表中插入数据项时),实际上是使用其给定的对象引用的副本。在字面意义项(比如整数或字符串)的情况下,会在内存中创建适当数据类型的对象,而存放在列表或元组中的才是对象引用。

与Python中的其他内容一样,组合数据类型也是对象,因此,可以将某种组合数据类型嵌套在其他组合数据类型中,比如,创建一个列表,其中的每个元素也是列表,不拘形式。列表、元组以及大多数Python的其他组合数据类型存储的是对象引用,而非对象本身——有些情况下,这一事实会有一定的影响,第3章将会涉及这一问题。

在过程型程序设计中,我们经常需要调用函数,并将某些数据项作为参数传递。比如,前面我们已经看到的print()函数。另一个常用的Python函数是len()函数,该函数以某个单独的数据项作为参数,并返回该数据项的“长度”(int类型)。下面有几个调用len()函数的实例——考虑到读者应该可以区分哪些是自己的输入,哪些是解释器的输出,这里没有再使用粗体表示解释器的输出。

>>> len(("one",))
1
>>> len([3, 5, 1, 2, "pause", 5])
6
>>> len("automatically")
13

元组、列表以及字符串等数据类型是“有大小的”,也就是说,对这些数据类型而言,长度或大小等度量是有意义的,将这些数据类型的数据项作为参数传递给len()函数是有意义的。(如果是那种不能用长度进行度量的数据项传递给len()函数,则会导致异常)。

所有Python数据项都是某种特定数据类型(也称之为“类”)的“对象”(也称之为“实例”),我们将交替地使用术语“数据类型”与“类”。对象与有些其他语言(比如,C++或Java的内置数值类型)提供的数据项的关键区别在于,对象可以有“方法”。实质上,简单地说,方法就是某特定对象可以调用的函数。比如,数据类型list有一个append()方法,借助于该方法,可以以如下方式添加对象:

>>> x = ["zebra", 49, -879, "aardvark", 200]
>>> x.append("more")
>>> x
['zebra', 49, -879, 'aardvark', 200, 'more']

对象x知道自身是一个list(所有Python对象都知道自身的数据类型),因此,不需要明确地指定数据类型。在append()方法的实现中,第一个参数是x对象本身——这是由Python自动完成的(作为其对方法的句法支持的一部分)。

append()方法会改变原始的列表。这是可以实现的,因为列表这种数据类型本身就是可变的。与创建新列表(使用原始的数据项以及额外要添加的数据项)、之后重新绑定对新列表的对象引用相比,append()方法具有潜在的更高的效率,对于很长的列表尤其如此。

在过程型程序设计语言中,以如下的方式(完全有效的Python语法)使用列表的append()方法可以完成同样的功能:

>>> list.append(x, "extra")
>>> x
['zebra', 49, -879, 'aardvark', 200, 'more', 'extra']

这里,我们指定了数据类型以及该数据类型的方法,并将要调用该方法的数据项本身作为第一个参数,其后跟随其他一些参数。(在涉及到继承时,两种语法存在微妙的语义差别,第一种形式在实际中应用的更加广泛。第6章将会对继承进行讲解。)

如果你对面向对象程序设计并不熟悉,最初看到这些内容可能会有些奇怪。不过,你现在只需要知道,Python有一种常规的函数调用方式functionName(arguments)、方法调用方式objectName.methodName(arguments)(第6章将会对面向对象程序设计进行讲解)。

点(“存取属性”)操作符用于存取对象的某个属性。虽然到目前为止,我们展示的只有方法属性,但是属性可以是任意类型的对象。由于属性可以是对象,该对象包含某些属性,这些属性又可以包含其他属性,依此类推,因此,可以根据需要使用多级嵌套的点操作符来存取特定的属性。

list类型有很多其他方法,包括insert()方法,该方法用于在某给定的索引位置插入数据项;remove()方法,该方法用于移除某给定索引位置上的数据项。如前面所说的,Python索引总是以0开始。

前面曾经提及,我们可以使用方括号操作符从字符串中获得某个字符,并且该操作符可用于任意序列。列表本身也是一种序列,因此,可以对其进行如下一些操作:

>>> x
['zebra', 49, -879, 'aardvark', 200, 'more', 'extra']
>>> x[0]
'zebra'
>>> x[4]
200

元组也是一种序列,因此,如果x是一个元组,我们也可以以完全同样的方式使用方括号取回项目,就像对x列表一样。但是,由于列表是可变的(不像字符串与元组是固定的),因此我们也可以使用方括号操作符来设置列表元素,例如:

>>> x[1] = "forty nine"
>>> x
['zebra', 'forty nine', -879, 'aardvark', 200, 'more', 'extra']

如果我们给定了一个超出范围的索引位置,就会产生例外——我们将在要素5中简要介绍意外处理,并在第4章中对其进行全面讲解。

我们几次使用了“序列”这个术语,依赖于对其含义的非形式化的理解,目前也将继续这样处理。不过,Python精确地定义了序列必须支持的特性,也定义了有大小的对象必须支持的特性,以及某种数据类型可能属于的其他不同类别,这些内容将在第8章讲述。

列表、元组以及Python其他内置的组合数据类型将在第3章中讲述。

任何程序设计语言的一个基本功能都是其逻辑运算。Python提供了4组逻辑运算,我们将在这里对其全部进行讲述。

1.2.4.1 身份操作符

由于所有的Python变量实际上都是对象引用,有时,询问两个或更多的对象引用是否都指向相同的对象是有意义的。is操作符是一个二元操作符,如果其左端的对象引用与右端的对象引用指向的是同一个对象,则会返回true。下面给出几个实例:

>>> a = ["Retention", 3, None]
>>> b = ["Retention", 3, None]
>>> a is b
False
>>> b = a
>>> a is b
True

需要注意的是,通常,对intS、strS以及很多其他数据类型进行比较是没有意义的,因为我们几乎总是想比较这些值。实际上,使用is对数据项进行比较可能会导致直觉外的结果。例如,在前面的实例中,虽然a与b在最初设置为同样的列表值,但是列表本身是以单独的list对象存储的,因此,在第一次使用时,is操作符将返回false。

身份比较的一个好处是速度非常快,这是因为,并不必须对进行比较的对象本身进行检查,is操作符只需要对对象所在的内存地址进行比较——同样的地址存储的是同样的对象。

最常见的使用is的情况是将数据项与内置的空对象None进行比较,None通常用作位置标记值,指示“未知”或“不存在”:

>>> a = "Something"
>>> b = None
>>> a is not None, b is None
(True, True)

上面使用is not是对身份测试的反向测试。

身份操作符的作用是查看两个对象引用是否指向相同的对象,或查看某个对象是否为None。如果我们需要比较对象值,就应该使用比较操作符。

1.2.4.2 比较操作符

Python提供了二进制比较操作符的标准集合,每个操作符带有期待中的语义:<表示小于,<=表示小于或等于,==表示等于。!=表示不等于,>=表示大于或等于,>表示大于。这些操作符对对象值进行比较,也就是对象引用在比较中指向的具体对象。下面是在Python Shell中输入的一些实例:

>>> a = 2
>>> b = 6
>>> a == b
False
>>> a < b
True
>>> a <= b, a != b, a >= b, a > b
(True, True, False, False)

对于整数,比较的结果与我们期待的结果是一样的。同样,对字符串进行比较操作,也可以获得正确的结果:

>>> a = "many paths"
>>> b = "many paths"
>>> a is b
False
>>> a == b
True

从上面的实例可以看出,虽然a与b是不同的对象(有不同的身份),但是具有相同的值,因此比较的结果是相等的。需要注意的是,因为Python使用Unicode编码表示字符串,与简单的ASCII字符串比较相比,对包含非ASCII字符的字符串进行比较可能更微妙、更复杂——我们将在第2章对这些问题进行全面讨论。

Python比较操作符的一个特别好用的特性是可以进行结链比较,例如:

>>> a = 9
>>> 0 <= a <= 10
True

对给定数据项取值在某个范围内的情况,这种测试方式提供了很多便利,不再需要使用逻辑运算符and进行两次单独的比较操作(大多数其他语言都需要如此)。这种方式的另一个附加的好处是只需要对数据项进行一次评估(因为数据项在表达式中只出现一次),如果数据项值的计算需要耗费大量时间或存取数据项会带来一些副作用,这种优势就会更加明显。

归功于Python动态类型机制的“强大”,进行无意义的比较会导致异常,例如:

>>> "three" < 4
Traceback (most recent call last):
...
TypeError: unorderable types: str() < int()

出现异常而又未得到处理时,Python会输出该异常错误消息的回溯与追踪信息。为使得输出更加清晰,我们忽略了输出信息中的回溯部分,而使用省略号替代。如果我们输入的是“3”< 4,就会导致同样的异常,因为Python并不会猜测我们的意图——正确的方法或者是进行明确的转换,例如使用可比较的类型,也就是说,都是整数或都是字符串。

Python中可以容易地创建自定义数据类型,并且与已有数据类型进行很好的整合,比如,我们可以创建自定义的数值类型,并将其与内置的int类型进行比较,也可以与其他内置的或自定义的数值类型进行比较,但不能与字符串或其他非数值类型进行比较。

1.2.4.3 成员操作符

对序列或集合这一类数据类型,比如字符串、列表或元组,我们可以使用操作符in来测试成员关系,用not in来测试非成员关系。例如:

>>> p = (4, "frog", 9, -33, 9, 2)
>>> 2 in p
True
>>> "dog" not in p
True

对列表与元组,in操作符使用线性搜索,对非常大的组合类型(包含数万个或更多的数据项),速度可能会较慢;而对字典或集合,in操作可以非常快。这些组合数据类型都将在第3章讲述,这里只展示如何使用in操作符对字符串进行相关操作:

>>>phrase = "Wild Swans by Jung Chang"
>>> "J" in phrase
True
>>> "han" in phrase
True

对字符串数据类型,使用成员运算符可以很方便地测试任意长度的子字符串。(前面已经讲过,字符只不过是长度为1的字符串。)

1.2.4.4 逻辑运算符

Python提供了3个逻辑运算符:and、or与not。and与or都使用short-circuit逻辑,并返回决定结果的操作数——而不是返回布尔值(除非实际上就是布尔操作数)。下面给出一些实际例子:

>>> five = 5
>>> two = 2
>>> zero = 0
>>> five and two
2
>>> two and five
5
>>> five and zero
0

如果逻辑表达式本身出现在布尔上下文中,那么结果也为布尔值,因此,前面的表达式结果将变为True、True与False,比如,if语句。

>>> nought = 0
>>> five or two
5
>>> two or five
2
>>> zero or five
5
>>> zero or nought
0

or操作符是类似的,这里,在布尔上下文中,结果应该为True、True、True与False。

not单一操作符在布尔上下文中评估其参数,并总是返回布尔型结果,对前面的实例,not(zero or nought)的结果为True,not two的结果为False。

前面我们曾提及,.py文件中的每条语句都是顺序执行的,从第一条语句开始,逐行执行。实际上,函数、方法调用或控制结构都可以使控制流转向,比如条件分支或循环语句。有意外产生时,控制流也会被转向。

在这一小节中,我们将讲述Python的if语句、while语句以及loop语句,函数将在要素#8中讲解,方法将在第6章讲解。这里还会介绍基本的异常处理机制,并在第4章对其进行全面讲解,但这里首先澄清一些术语。

布尔表达式实际上就是对某对象进行布尔运算,并可以产生一个布尔值结果(True或False)。在Python中,预定义为常量False的布尔表达式、特殊对象None、空序列或集合(比如,空字符串、列表或元组)、值为0的数值型数据项等的布尔结果为False,其他的则为True。创建自定义数据类型(比如在第6章中)时,我们可以自己决定这些自定义数据类型在布尔上下文中的返回值。

在Python中,一块代码,也就是说一条或多条语句组成的序列,称为suite。由于Python中的某些语法要求存在一个suite,Python就提供了关键字pass,pass实际上是一条空语句,不进行任何操作,可以用在需要suite(或者需要指明我们已经考虑了特殊情况)但又不需要进行处理的地方。

1.2.5.1 if语句

Python的常规if语句语法如下

if boolean_expression1:
  suite1
elif boolean_expression2:
  suite2
...
elif boolean_expressionN:
  suiteN
else:
  else_suite

与if语句对应的可以有0个或多个elif分支,最后的else分支是可选的。如果需要考虑某个特定的情况,但又不需要在这种情况发生时进行处理,那么可以使用pass作为该分支的suite。

对习惯于C++或Java语法的程序员而言,第一个突出的差别是这里没有圆括号与方括号,另一个需要注意到的情况是冒号的使用,冒号是上面语法中的一个组成部分,但是最初使用时容易忘记。冒号与else、elif一起使用,实质上在后面要跟随suite的任意地方也需要使用。

与大多数其他程序设计语言不同的是,Python使用缩排来标识其块结构。有些程序员不喜欢这一点,尤其是在使用这一功能之前,有些人对这一情况甚至极为讨厌。但习惯这一情况只需要几天时间,在几个星期或几个月之后,不带方括号的代码看起来比使用方括号的代码更优美、更不容易混乱。

由于suite是使用缩排指明的,因此很自然地带来的一个问题是:使用哪种缩排?Python 风格指南建议的是每层缩排4个空格,并且只有空格(没有制表符)。大多数现代的文本编辑器可设置为自动处理这一问题(IDLE的编辑器当然也是如此,大多数其他可以感知Python代码的编辑器也是如此)。如果使用的缩排是一致的,那么对任意数量的空格或制表符,或二者的混合,Python都可以正常处理。本书中,我们遵循官方的Python指南。

下面给出一个非常简单的if语句实例:

if x:
  print("x is nonzero")

对于上面的情况,如果条件x为真,那么suite(print()函数调用)将得以执行。

if lines < 1000:
 print("small")
elif lines < 10000:
 print("medium")
else:
 print("large")

上面给出的是一条稍微复杂一些的if语句,该语句打印一个单词,这个单词用于描述lines变量的值。

1.2.5.2 while语句

while语句用于0次或多次执行某个suite,循环执行的次数取决于while循环中布尔表达式的状态,下面给出其语法格式:

while boolean_expression:
  suite

实际上,while循环的完整语法比上面的要复杂得多,这是因为在其中可以支持break与continue,还包括可选的else分支,这些在第4章将进行讨论。break语句的作用是将控制流导向到break所在的最内层循环——也就是说跳出循环;continue语句的作用是将控制流导向到循环起始处。通常,break语句与continue语句都用在if语句内部,以便条件性地改变某个循环的执行流程。

while True:
  item = get_next_item()
  if not item:
    break
  process_item(item)

while循环具有非常典型的结构,只要还存在需要处理的数据项,就一直循环(get_next_item()与process_item()都是在某处定义的自定义函数)。在上面的实例中,while语句的suite中包含了一条if语句,该if语句本身又包含了自己的suite,因此,在这一实例中必须包含一条break语句。

1.2.5.3 for…in语句

Python的for循环语句重用了关键字in(在其他上下文中,in是一个成员操作符),并使用如下的语法格式:

for variable in iterable:
  suite

与while循环类似,for循环也支持break语句与continue语句,也包含可选的else分支。variable将逐一引用iterable中的每个对象,iterable是可以迭代的任意数据类型,包括字符串(此时的迭代是逐个字符进行)、列表、元组以及Python的其他组合数据类型。

for country in ["Denmark", "Finland", "Norway", "Sweden"]:
  print(country)

上面给出的是一个非常简化的方法,用于打印国家列表。在实际的代码中,更常见的做法是使用变量:

countries = ["Denmark", "Finland", "Norway", "Sweden"]
for country in countries:
  print(country)

实际上,完整的列表(或元组)可以使用print()函数直接打印,比如print(countries),但我们通常更愿意使用一个for循环(或list comprehension,后面会讲述)来打印,以便对格式进行完全的控制。

for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
  if letter in "AEIOU":
   print(letter, "is a vowel")
  else:
   print(letter, "is a consonant")

在这一代码段中,第一次使用关键字in是将其作为for循环的一部分,变量letter则取值从“A”、“B”一直到“Z”,在循环的每次迭代中变化一次。在该代码段的第二行又一次使用了in,但这次是将其作为成员关系测试操作符。还要注意的是,该实例展示了嵌套循环结构:for循环的suite是if…else语句,同时if语句与else语句又都有自己的suite。

1.2.5.4 基本的异常处理

Python的很多函数与方法都会产生异常,并将其作为发生错误或重要事件的标志。与Python的其他对象一样,异常也是一个对象,转换为字符串时(比如,打印时),异常会产生一条消息文本。异常处理的简单语法格式如下:

try:
  try_suite
except exception1 as variable1:
  exception_suite1
...
except exceptionN as variableN:
  exception_suiteN

要注意的是,as variable部分是可选的。我们可以只关心产生了某个特定的异常,而不关心其具体的消息文本。

完整的语法要更加复杂一些,比如,每个except分支都可以处理多个异常,还有可选的else分支,所有这些内容将在第4章中集中讲述。

异常处理以如下的逻辑工作:如果try块中的suite都正常执行,而没有产生异常,则except模块将被跳过;如果try块中产生了异常,则控制流会立即转向第一个与该异常匹配的suite——这意味着,跟随在产生异常的语句后面的suite中的语句将不再执行;如果发生了异常,并且给定了as variable部分,则在异常处理suite内部,variable引用的是异常对象。

如果异常发生在处理except块时,或者某个异常不能与任何一个except块匹配,Python就会在下一个封闭范围内搜索一个匹配的except块。对合适的异常处理模块的搜索是向外扩展的,并可以延展到调用栈内,直到发现一个匹配的异常处理模块,或者找不到匹配的模块,这种情况下,程序将终止,并留下一个未处理的异常,此时,Python会打印回溯信息以及异常的消息文本。

下面给出一个实例:

s = input("enter an integer: ")
try:
  i = int(s)
  print("valid integer entered:", i)
except ValueError as err:
  print(err)

如果用户输入的是3.5,那么输出为:

invalid literal for int() with base 10: '3.5'

但是如果用户输入的是13,那么输出为:

valid integer entered: 13

很多书籍都把异常处理作为一个高级专题,并尽可能将其安排在后面讲解。实际上,了解异常的产生与处理机制对理解Python的工作方式是基本的要求,因此,我们在一开始就讲述这方面的内容。我们会看到,通过将异常情况从我们真正关心的处理流程中剥离出来,异常处理机制可以使Python代码更具可读性。

Python提供了完整的算术运算符集,包括用于基本四则数学运算的操作符+、-、*、/。此外,很多Python数据类型可以使用一些增强的赋值操作符,比如+=与*=。在操作数都是整数时,+、-、*等操作符可以分别按正常的加法、减法、乘法进行运算:

>>> 5 + 6
11
>>> 3 - 7
-4
>>> 4 * 8
32

要注意的是,像大多数程序设计语言一样,在Python中,-号既可以作为单值操作符(否定),也可以作为二元操作符(减法),Python与一般程序语言不同的地方在于对除法的处理:

>>> 12 / 3
4.0
>>> 3 / 2
1.5

除法操作符会产生一个浮点值,而不是一个整数值;很多其他程序设计语言都是产生一个整数值,并剥离掉小数部分。如果需要产生一个整数值结果,我们可以使用int()进行转换(或使用剥离操作符//,后面会进行讨论)。

>>> a = 5
>>> a
5
>>> a += 8
>>> a
13

乍一看,上面的语句没什么奇怪的地方,尤其是对熟悉类C语言的读者而言,在这种语言中,增强的赋值操作符是一种速记法,用于对某操作生成的结果进行赋值。例如,a += 8实际上与a = a + 8是一样的。然而,这里有两个重要的地方,一个是Python特定的,另一个是任何语言中处理增强的赋值操作符时都会用到的。

第一点需要记住的是,int数据类型是固定的——也就是说,一旦赋值,就不能改变,因此,对固定的对象使用增强的赋值操作符时,实际上是创建一个对象来存储结果,之后,目标对象引用重新绑定,以便引用上面创建的结果对象,而不再引用以前的对象。根据这一原理,前面的例子中,在执行到a += 8语句时,Python会计算a+8,将所得结果存储到新的int对象,之后将a重新绑定为引用这个新的int对象(如果a正在引用的原始对象没有其他的对象引用,就会进入垃圾收集流程)。图1-3所示示出了这一过程。

图1-3 固定对象的增强的赋值操作符

第二个微妙之处在于,a operator= b与a = a operator b并不完全一致。前者只查询一次a的值,因而具有更快的可能性。此外,如果a是一个复杂的表达式(例如,列表元素的索引位置计算,如items[offset + index]),那么使用增强的赋值操作符时后者较少出错。这是因为,如果计算过程需要改变,那么维护者只需要改变一次,而不是两次。

Python重载(对不同的数据类型进行重用)了操作符+与+=,将其分别用于字符串与列表,前者表示连接,后者表示追加字符串并扩展(追加另一个字符串)列表:

>>> name = "John"
>>> name + "Doe"
'JohnDoe'
>>> name += " Doe"
>>> name
'John Doe'

与整数类似,字符串也是固定的,因此,当使用+=时,会创建一个新字符串,并且表达式左边的对象引用将重新绑定到新字符串,就像前面描述的ints一样。列表支持同样的语法,但隐含在后面的流程并不相同:

>>> seeds = ["sesame", "sunflower"]
>>> seeds += ["pumpkin"]
>>> seeds
['sesame', 'sunflower', 'pumpkin']

由于列表是可变的,使用+=后,原始的列表对象会被修改,因此,没有必要对seeds进行重新绑定。图1-4示出了这一过程。

图1-4 可变对象的增强的赋值操作符

既然Python语法聪明地隐藏了可变的与固定的数据类型的区别,为什么还需要规定这两种数据类型?原因最可能还是在于性能。在实现上,固定的数据类型具有比可变的数据类型更加高效的潜力(因为这些固定的数据类型从不改变)。此外,有些组合数据类型(比如集合)只能操纵固定的数据类型。另一方面,可变的数据类型使用起来更加方便。在这些差别起作用的地方,我们将对其进行讨论,比如,在第4章中讨论为自定义函数设置默认参数时,在第3章中讨论列表、集合以及一些其他数据类型时,以及在第6章中展示如何创建自定义数据类型时。

列表+=操作符右边的操作数必须是一个iterable,如果不是,就会产生意外:

>>> seeds += 5
Traceback (most recent call last):
 ...
TypeError: 'int' object is not iterable

对列表进行扩展的正确方法是使用iterable对象,例如:

>>> seeds += [5]
>>> seeds
['sesame', 'sunflower', 'pumpkin', 5]

当然,用于扩展列表的iterable对象本身就有多个数据项:

>>> seeds += [9, 1, 5, "poppy"]
>>> seeds
['sesame', 'sunflower', 'pumpkin', 5, 9, 1, 5, 'poppy']

添加一个普通的字符串——比如“durian—”而不是包含字符串的列表["durian"],就会导致一个合乎逻辑但可能比较奇怪的结果。

>>> seeds = ["sesame", "sunflower", "pumpkin"]
>>> seeds += "durian"
>>> seeds
['sesame', 'sunflower', 'pumpkin', 'd', 'u', 'r', 'i', 'a', 'n']

列表的+=操作符会扩展列表,并将给定的iterable中的每一项追加到列表后。由于字符串是一个iterable,这会导致字符串中的每个字符被单独添加。如果我们使用append()方法,那么该参数总是以单独的项目添加。

如果要编写真正有用的程序,我们必须能够读取输入(比如,从控制台用户处,或者从文件中),还要产生输出,并写到控制台或文件。我们已经展示过如何使用Python的内置print()函数,在第4章中我们将对其进一步展开。在这一小节中,我们将集中讲解控制台I/O,并使用shell重定向读取或写入文件。

Python提供了内置的input()函数,用于接收来自用户的输入。这一函数需要一个可选的字符串参数(并将其在控制台上打印),之后等待用户输入响应信息或按Enter键(或Return键)来终止。如果用户不输入任何文本,而只是按Enter键,那么input()函数会返回一个空字符串;否则,会返回一个包含了用户输入内容的字符串,而没有任何行终止符。

下面给出的是我们提供的第一个完整的“有用的”程序,这个程序吸取了前面很多有用的要素——唯一新的是input()函数:

print("Type integers, each followed by Enter; or just Enter to finish")

total = 0
count = 0

while True:
  line = input("integer: ")
  if line:
    try:
      number = int(line)
    except ValueError as err:
      print(err)
      continue
    total += number
    count += 1
  else:
    break

if count:
  print("count =", count, "total =", total, "mean =", total / count)

上面的程序(在本书实例中的sum1.py文件中)只有17个可执行行,下面给出了该程序的典型运行情况:

Type integers, each followed by Enter; or just Enter to finish
number: 12
number: 7
number: 1x
invalid literal for int() with base 10: '1x'
number: 15
number: 5
number:
count = 4 total = 39 mean = 9.75

尽管这一程序很短,但是程序鲁棒性很好。如果用户输入的是无法转换为整数的字符串,那么这一问题会被异常处理流程捕捉,并打印一条相关信息,之后程序流程转向到循环的开始处(“继续循环”)。最后一个if语句的作用是:如果用户不输入任何数值,那么摘要不会输出,并且被0除也会避免。

第7章中将全面讲解文件处理,不过在这里我们也可以创建文件,这是通过将print()函数的输出从控制台重定向到文件来实现的,例如:

C:\>test.py > results.txt

上面的语句使得test.py中的print()函数调用产生的结果写入到文件result.txt中。上面的语法格式在Windows控制台与UNIX控制台中都可以正常工作。对于Windows,如果Python 2是系统默认的Python版本,我们就必须写成C:\Python30\python.exe test.py > results.txt,或者如果Python 3在PATH中占先(尽管我们不会再提及这个问题),就写成python.exe test.py > results.txt;对于UNIX,我们首先要把程序变为可执行的(chmod +x test.py),之后通过./test.py调用该程序——除非程序所在目录恰好为PATH。

通过将数据文件重定向为输入(与上面重定向输出的方式类似),可以实现数据的读取。然而,如果我们对sum1.py使用重定向,就会导致失败。这是因为,在收到EOF(文件终止)字符时,input()函数会产生异常。下面给出了一个更具鲁棒性的程序版本(sum2.py),该程序可以接收来自用户键盘输入的输入信息,也可以接收来自文件重定向的输入信息:

print("Type integers, each followed by Enter; or ^D or ^Z to finish")

total = 0
count = 0

while True:
  try:
    line = input()
    if line:
      number = int(line)
      total += number
      count += 1
  except ValueError as err:
    print(err)
    continue
  except EOFError:
    break

if count:
  print("count =", count, "total =", total, "mean =", total / count)

在命令行中,输入sum2.py< data\sum2.dat(这里,实例的data子目录中的文件sum2.dat包含了一列数据,每行一个),输出信息为:

Type integers, each followed by Enter; or ^D or ^Z to finish
count = 37 total = 1839 mean = 49.7027027027

为使程序更适合交互式使用并使用重定向技术,我们对该程序进行了几处修改。首先,我们将终止方式从空白行变为EOF字符(在UNIX上为Ctrl+D,Windows上为Ctrl+Z并按Enter键),这样,在处理包含空白行的输入文件时,程序更富于鲁棒性,不再为每个数值打印一个提示符(因为对重定向的输入而言这个没有意义)。此外,我们还使用了一个单独的try块,其中包含两个异常处理过程。

要注意的是,如果输入了无效的整数(来自键盘或重定向输入文件中的损坏行),那么int()转换将产生一个ValueError异常,控制流也将立即转向相关的except模块。这意味着,输入无效数据时,total或count都不会递增,而这正是我们所期望的。

我们也可以使用两个单独的异常处理Try语句块:

while True:
  try:
    line = input()
    if line:
      try:
        number = int(line)
      except ValueError as err:
        print(err)
        continue
      total += number
      count += 1
  except EOFError:
    break

但是我们更愿意将异常处理模块集中放在程序末尾,以保证主要流程尽可能清晰。

使用前面几个要素中讲解的数据类型与控制结构编写程序是完全可能的,然而,在实际中,非常常见的情况是重复进行同样的处理过程,只不过有细微的差别,比如不同的起点值。Python提供了一种将多个suites封装为函数的途径,函数就可以参数化,并通过传递不同的参数来调用。下面给出的是用于创建函数的通常语法格式:

def functionName(arguments):
  suite

这里,arguments是可选的;如果有多个参数,就必须使用逗号进行分隔。每个Python函数有一个返回值,默认情况下为None,除非我们使用语法return value来从函数返回,此时value是实际的返回值。返回值可以是仅仅一个值,也可以是一组值。调用者可以忽略返回值,并简单地将其丢弃。

要注意的是,def是一条与赋值操作符工作方式类似的语句。执行def时,会创建一个函数对象,同时创建一个带有指定名的对象引用(引用该函数对象)。由于函数也是对象,因此可以存储在组合数据类型中,并作为参数传递给其他函数,后续章节中将展示这一点。

在编写交互式的控制台应用程序时,一个频繁的需求是从用户处获取整数,下面给出了一个完成这一功能的函数:

def get_int(msg):
  while True:
    try:
      i = int(input(msg))
      return i
    except ValueError as err:
      print(err)

这个函数有一个参数msg,在while循环内部,用户被要求输入一个整数,如果输入无效,则会产生一个ValueError异常,并打印错误消息,同时循环也将迭代进行。输入有效的整数后,会返回给调用者。下面展示了如何调用这个函数:

age = get_int("enter your age: ")

在这一实例中,强制使用单一的参数,这是因为我们没有提供默认值。实际上,对于支持默认参数值、位置参数与关键字参数的函数参数,Python支持非常复杂与灵活的语法结构。第4章将集中讲述这些语法。

尽管创建自己的函数是一件很惬意的事情,但是很多时候并不需要这样做。这是因为,Python有大量的内置函数,其标准库的大量模块中包含更多的函数,因此大多数我们所需要的函数都可以直接使用。

Python模块实际上就是包含Python代码的.py文件,比如自定义函数与类(自定义数据类型)的定义,有时候还包括变量等。要使用某个模块内的函数功能,必须先导入该模块,例如:

import sys

要导入一个模块,必须使用inport语句,其后跟随.py文件名,但是不需要写出该扩展名。导入一个模块后,就可以访问其内部包含的任意函数、类以及变量。例如:

print(sys.argv)

sys模块提供了argv变量——该变量实际上是一个列表,其首项为该程序的名称,第二个参数及后续的参数为该程序的命令行参数。前面两行构成了完整的echoargs.py程序。如果在命令行中以echoargs.py -v命令调用该程序,就会在控制台上打印['echoargs.py', '-v']。(在UNIX上,第一个条目应该是/echoargs.py')

通常,使用模块中函数的语法格式为moduleName.functionName(arguments)。其中使用了我们在要素3中介绍的点(“存取属性”)操作符。标准库中包含大量的模块,随着本书的逐步展开,我们会逐渐介绍和使用其中的大量模块。标准模块的模块名都是小写字母,因此,一些程序员为自己编写的模块使用首字母大写的名(比如,My Module),以便区别于标准模块。

下面看一个实例,random模块(在标准库的random.py文件中)提供了很多有用的函数:

import random
x = random.randint(1, 6)
y = random.choice(["apple", "banana", "cherry", "durian"])

在这些语句执行之后,x将包含一个1~6之间(包含边界值)的一个整数,y将包含传递给random.choice()函数的列表之间的一个字符串。

常规的做法是将所有import语句放在.py文件的起始处,在shebang行和模块的文档之后(第5章将对模块文档进行介绍)。我们建议程序员首先导入标准库模块,之后导入第三方库模块,最后才导入自己编写的模块。

在前面的几节中,我们介绍了足以编写实际程序的Python知识与技术。在这一节中,我们将介绍两个完整的程序,这些程序只涉及前面已经讲过的知识。一方面是为了展示前面所学的知识可以完成什么任务,一方面也是为了巩固前面所学的知识。

在后面的章节中,我们会逐渐学习更多的Python知识与库模块,以便于编写出比这里展示的程序更精确与更强壮的程序——但是首先我们必须先掌握这些基础知识。

这里给出的第一个程序非常短小,尽管该程序也有一些微妙之处,包括列表组成的列表等。这个程序的功能是:在命令行中提供一个数值,之后该程序会使用“大”数字向控制台输出该数值。

在大量用户共享高速行式打印机的站点上,使用这种技术是很常见的做法,即为每个用户的打印作业打印一个引导页,使其包含用户名与其他有助于区分不同用户的详细资料。

我们分3个部分查看该程序的代码:import部分;创建列表(其中存放程序要使用的数据)部分;处理部分本身。不过,我们首先看一下运行的效果:

bigdigits.py 41072819
  *   *   ***  *****  ***  ***  *   ****
 **  **  *  *    * *  * *  * **  *  *
 * *   *  *   *   *  * *  *  *  *  *  *
* *   *  *   *  *   *   ***  *   ****
******  *  *   *  *   *   *  *  *    *
  *   *  *  *  *   *   *  *  *     *
  *  ***  ***  *   *****  ***  ***    *

我们没有展示控制台提示符(或UNIX用户的./),而是将这些内容默认为已经存在的。

import sys

由于我们必须从命令行中读入一个参数(也就是要输出的数值),我们需要访问sys.argv列表,因此我们从导入sys模块开始。

我们以字符串列表的形式展示每个数值,比如,下面展示的是zreo:

Zero = [" *** ",
    " *  * ",
    "*   *",
    "*   *",
    "*   *",
    " *  * ",
    " *** "]

这里需要注意的一个细节是,Zero的字符串列表表示形式跨越了多个行。通常,Python语句只占用一行,但是也可以跨越多行,比如使用圆括号包含的表达式、列表、集合、字典字面值、函数调用参数列表以及多行语句(除最后一行之外,每行的行终结符都使用反斜线进行引导并转义处理)。上面的这些Python语句可以跨越任意多的行,代码缩排也并不会影响第二行以及后续的行。

用于表示数值的每个列表包含7个字符串,在对同一个数值的表示中,这些字符串是等宽的,而表示不同数值的字符串宽度不同。表示其他数值的列表在形式上与上面给出的Zero类似。下面给出的几个表示主要是出于紧致性考虑,因而不那么形象和清晰:

One = [" * ", "** ", " * ", " * ", " * ", " * ", "***"]
Two = [" *** ", "*  *", "* * ", " * ", " *  ", "*  ", "*****"]
# ...
Nine = [" ****", "* *", "* *", " ****", "  *", "  *", "  *"]

我们还需要的最后一个数据结构是所有数字列表组成的列表:

Digits = [Zero, One, Two, Three, Four, Five, Six, Seven, Eight, Nine]

我们也可以直接创建Digits列表,而不必创建额外的变量,例如:

Digits = [
  [" *** ", " *  * ", "*   *", "*   *", "*   *",
   " *  * ", " *** "], # Zero
  [" * ", "** ", " * ", " * ", " * ", " * ", "***"], # One
  # ...
  [" ****", "*  *", "*  *", " ****", "  *", "  *",
   "  *"] # Nine
  ]

我们更愿意使用单独的变量来分别表示每个数值,一方面是为了便于理解,另一方面是因为使用变量来表示看起来更整洁。

下面一起展示了余下的代码部分,以便你在阅读其后的解释之前自己可以设想其工作方式。

try:
  digits = sys.argv[1]
  row = 0
  while row < 7:
    line = ""
    column = 0
    while column < len(digits):
      number = int(digits[column])
      digit = Digits[number]
      line += digit[row] + " "
      column += 1
    print(line)
    row += 1
except IndexError:
  print("usage: bigdigits.py <number>")
except ValueError as err:
  print(err, "in", digits)

上面这段代码整体包含在一个异常处理模块中,并可以捕获两个异常。该段代码首先取回程序的命令行参数,与所有Python列表类似,sys.argv列表的索引项是从0开始的,索引位置为0的数据项是调用的程序名,因此,在一个运行的程序中,该列表总是至少包含一项。如果没有给定参数,我们会在一个单数据项的列表中尝试访问第二个数据项,并导致产生一个IndexError异常。如果发生这种情况,控制流立即转向相应的异常处理块,这里只是简单地打印出程序的用法。在Try块结束后,程序继续执行,但是由于已经没有更多的代码,因此程序只是简单地退出。

如果没有发生IndexError异常,那么digits字符串会存放命令行参数,如果一切正常,就应该是一个数字字符序列。(记住,要素2中讲过,标识符对大小写敏感,因此,digits与Digits是不同的。)每个大数都使用7个字符串表示,为正确地输出数值,我们必须首先输出每个数字的顶行,之后是下一行,依此类推,直至输出所有的7行。我们使用一个while循环,以便逐行迭代。我们也可以采用另一种方法:for row in (0, 1, 2, 3, 4, 5, 6):,后面我们还将看到一种更好的、使用内置的range()函数的方法。

我们使用line字符串来存放所有数字的行字符串,之后根据列进行循环,也就是说,根据命令行参数中每个相继的字符进行循环。我们使用digits[column]取回每个字符,并将数字转换为称为number的整数。如果转换失败,就会产生一个ValueError异常,控制流立即转向相应的异常处理模块。这种情况下,我们将打印出错误消息,并在try块之后恢复控制。与前面类似,由于没有其他代码等待执行,因此程序只是简单地退出。

如果转换成功,我们就使用number作为索引来存取digits列表,并从其中抽取字符串列表digit,之后我们从这一列表中将相应的字符串添加到我们正在构建的行,并添加两个空格,以便在数字之间添加水平分隔。

内部的while循环每次结束时,我们会打印出刚构建好的行。理解这一程序的关键之处在于我们将每个数字的row字符串添加到当前row的行。读者可以尝试运行该程序,以便理解其运作方式。在章后练习中,我们将再次讲到该程序,以便对其输出进行稍许改变。

我们频繁面临的需求是测试数据的生成。由于不同场景下测试数据变化巨大,因此无法找到一个满足所有测试数据需求的通用程序。由于编写与修改Python程序都很容易, Python经常被用于生成测试数据。在这一小节中,我们将创建一个生成随机整数组成的网格的程序,用户可以规定需要多少行、多少列,以及整数所在的区域。我们首先从一个运行实例开始:

generate_grid.py
rows: 4x
invalid literal for int() with base 10: '4x'
rows: 4
columns: 7
minimum (or Enter for 0): -100
maximum (or Enter for 1000):
    554    720    550    217    810    649    912
    -24    908    742    -65    -74    724    825
    711    968    824    505    741    55    723
    180    -60    794    173    487     4    -35

该程序以交互式的方式运行,最开始在输入行数时,由于输入的行数有误,导致程序打印一条错误消息,并要求用户重新输入行数。对于maximum,我们只是简单地按Enter键,以便接受默认值。

我们将分别解读该程序的4个部分:import、函数get_int()的定义(此函数比要素8中展示的类似函数更复杂)、用户交互以便获取要使用的值、处理过程本身。

import random

我们需要random模块,以便访问其中的random.randinit()函数。

def get_int(msg, minimum, default):
  while True:
    try:
      line = input(msg)
      if not line and default is not None:
        return default
      i = int(line)
      if i < minimum:
        print("must be >=", minimum)
      else:
        return i
    except ValueError as err:
      print(err)

这一函数需要3个参数:一个消息字符串、一个最小值、一个默认值。如果用户只是简单地按Enter键,就有两种可能性。如果default为None,也就是说没有给定默认值,那么控制流将转到int()行,在该处转换将失败(因为无法转换为整数),并产生一个ValueError异常;如果default非None,就返回该值。否则,函数将尝试把用户输入的文本转换为整数,如果转换成功,接下来将检查该整数是否至少等于指定的minimum。

因此,该函数的返回总是两种情况,或者是default(用户只是按Enter键),或者是一个有效的整数(大于或等于指定的minimum)。

rows = get_int("rows: ", 1, None)
columns = get_int("columns: ", 1, None)
minimum = get_int("minimum (or Enter for 0): ", -1000000, 0)

default = 1000
if default < minimum:
  default = 2 * minimum
maximum = get_int("maximum (or Enter for " + str(default) + "): ",
          minimum, default)

通过我们的get_int()函数,可以很容易地获取行数、列数以及用户需要的最小随机数值。对于给定默认值None的行数与列数,也就是没有指定默认值的情况,用户必须输入一个整数。对于minimum,我们提供的默认值为0:对于maximum,我们提供的默认值为1000或minimum的2倍(如果minimum大于或等于1000)。

与前面的例子类似,函数调用参数列表可以跨越任意数量的行数,并且缩排与第二行及后继行无关。

在确定用户需要的具体行数、列数以及随机数的最大值与最小值后,就可以进行具体的随机数生成过程:

row = 0
while row < rows:
  line = ""
  column = 0
  while column < columns:
    i = random.randint(minimum, maximum)
    s = str(i)
    while len(s) < 10:
      s = " " + s
    line += s
    column += 1
  print(line)
  row += 1

为生成随机数网格,我们使用3个while循环,外部循环以行数进行循环,中间循环以列数进行循环,内部循环则以字符进行循环。在中间循环中,我们获取指定范围内的随机数,并将其转换为字符串。内部while循环用于对字符串进行填充(填充数据为空格),以便每个数字都使用10个字符的字符串表示,对每一行,使用字符串line来累积数值,在每一列的数字添加完毕后,就打印出该行表示的数字,至此,第二个程序功能讲解完毕。

Python提供了非常高级的格式化功能,以及对for ... in循环的良好支持能力,因此,bigdigits.py与generate_grid.py这两个程序的更真实的版本会使用for ... in循环,generate_grid.py程序将使用Python的字符串格式化功能,而不是像这里这样不带修饰地使用空格进行填充。但是在本章中,我们将自己约束在使用本章介绍的8个关键要素进行程序设计,并且这8个要素对编写完整而有用的程序也已足够。在接下来的每一章中,我们都将学习Python的一些新特性,因此,随着本书内容的推进,所看到的程序将逐步复杂起来。

在本章中,我们学习了如何编辑并运行Python程序,并讲解了几个虽然短小但完整的程序。本章的大部分在于讲解足以编写实际Python程序的8个要素——Python的“关键要素”。

我们从Python最基本的两个数据类型int与str开始。整数的编写就像在大多数其他程序设计语言中一样,字符串的编写需要使用单引号或双引号——只要字符串两端是同样的引号类型即可。我们可以在字符串与整数之间进行转换,比如int("250")与str(125)。如果转换整数失败,就会产生ValueError参数,而几乎所有对象都可以转换为字符串。

字符串也是序列,因此,那些可用于序列的函数与操作也可以用于字符串。例如,我们可以使用数据项存取操作符([])存取某个特定的字符,使用符号+连接字符串,使用+=追加字符串——由于字符串是固定的,因此,这里的追加操作实际上创建了一个新字符串,该字符串实际上是将给定的字符串连接在一起,并将左边字符串的对象引用重绑定到新字符串。我们也可以使用for…in循环来对字符串进行逐个字符的迭代。我们可以使用内置的len()函数统计一个字符串中包含多少个字符。

对于固定的对象,比如字符串、整数与元组,我们可以编写自己的代码,就像对象引用是一个变量一样,也就是说,就像对象引用是其引用的对象本身。我们也可以对可变的对象做类似的事情,尽管对可变的对象的任何改变都会影响到该对象的所有出现(对该对象的所有对象引用),我们将在第3章中进行讲解。

Python提供了几种内置的组合数据类型,其标准库中还包含其他几种类型。我们学习了list与tuple这两种数据类型,尤其是学习了如何从字面上创建元组与列表,比如,even = [2, 4, 6, 8]。与Python中的其他对象一样,列表也是对象,因此我们可以在其上进行方法调用,比如,even.append(10)将向列表中添加一个额外的数据项。与字符串类似,列表与元组也是序列,因此,我们可以使用for…in循环在其上进行逐项迭代,也可以使用len()函数计算其包含多少个数据项。使用数据项存取操作符([]),我们可以取回列表或元素中的某个特定项;使用+,可以连接两个列表或元组;使用+=,可以把某个列表或元组附加到另一个上。如果我们需要将一个单独的数据项添加到列表,就必须使用list.append()或+= 将该项添加到列表中,使其变为一个单项目列表,比如even += [12]。由于列表是可变的,因此我们可以使用[]改变单个数据项,比如,even[1]=16。

is与is not这两个身份操作符可以用于检测两个对象引用是否引用了相同的对象——在检测内置的None对象时,这两个操作符尤其有用。所有常见的比较操作符都可以使用(<, <=, ==, !=, >=, >),但是只能用于兼容的数据类型,并且要求数据类型支持该操作符。到目前为止我们看到的数据类型(包括int、str、list与tuple)都支持比较操作符的全集。对不兼容的数据类型进行比较,比如,将int与str或list进行比较,将很直观地产生一个TypeError异常。

Python支持标准的逻辑操作符and、or与not。and与or都是“短路”操作符,返回的是决定结果的操作数——这可能并非布尔类型值(尽管可以转换为布尔类型值),not总是返回true或false。

利用in与not in操作符,可以测试序列类型的成员关系,包括字符串、列表与元组等。进行成员关系测试时,对列表与元组使用的是较慢的线性搜索,对字符串使用的则是可能更快的混合搜索算法。不过,除非字符串、列表或元组非常长,一般情况下性能很少会成为问题。在第3章中,我们将学习Python的关联数组与集合等组合数据类型,这些数据类型都提供了非常快的成员关系测试功能。使用type()函数,也可能判断对象变量的类型(也就是说,对象引用实际引用的对象的类型),但这一函数通常只用于调试与测试过程。

Python提供了几种控制结构,包括条件分支(if…elif…else)、条件循环(while)、序列上的迭代(for…in)与异常处理(try…except块)等。while循环与for…in循环都可以使用break语句贸然地终止,也都可以使用continue语句将控制流转向循环的开始。

Python支持常见的算术操作符,包括+、-、*、/等四则运算,不过Python的不同之处在于,/操作符总是产生一个浮点值结果(即便两个操作数都是整数)。(很多其他程序设计语言使用的截取除法在Python中也支持,不过其操作符是//。)Python也提供了增强的赋值操作符,比如+=与*=,如果左边的操作数是固定的,这类操作符实际上就创建一个新对象,并进行重新绑定。此外,前面也已说明,str与list数据类型对算术运算操作符进行了重用。

通过input()函数与print()函数,可以实现控制台I/O。通过在控制台中使用文件重定向,可以使用同样的内置函数读、写文件。

Python除了提供丰富的内置功能之外,还提供了广泛的标准库,在使用import语句进行导入操作后,就可以使用模块中的函数。sys模块是一个很常见的需要导入的模块,其中存放了sys.argv列表(命令行参数)。如果Python不能提供我们需要的函数,可以使用def语句很容易地创建一个完成所需功能的函数。

利用本章中讲解的知识与技术,可以编写短小但有用的Python程序。在接下来的章节中,我们将学习关于Python数据类型的更多知识,对intS与strS进行更深入的讲解,并介绍一些全新的数据类型。之后,在第4章中,将详细讲解Python的控制结构,并介绍如何创建自己的函数,以便将相关功能进行包装,促进代码重用,防止重复工作。

在本书的每一章最后,都有一节练习,设置练习的目的是鼓励读者对Python进行实践,获取实际经验,以助于吸收每章中所学的知识。本节包括的实例与练习既涉及数字处理,也涉及文本处理,以便尽可能满足更多读者的需求。此外,这些实例与练习都有非常小的代码规模,以便于读者将重点和注意力集中于学习与思考,而不是仅仅输入代码。本书的实例中,为每个练习都提供了一个解决方案。

1.bigdigits.py程序的一个变形,不再打印*,而是打印具体的数字。例如:

bigdigits_ans.py 719428306
77777  1  9999   4   222  888  333   000   666
  7  11  9  9  44  2  2 8  8 3  3  0  0  6
  7   1  9  9  4 4  2  2  8 8   3 0  0  6
 7   1  9999 4 4   2   888   33  0  0  6666
 7    1    9 444444  2   8 8   3 0   0  6  6
7    1    9   4  2   8  8 3  3  0  0  6  6
7    111   9   4  22222  888  333   000   666

可以采取两种方法。最简单的方法只是简单地改变列表中的*,但这种方法太过死板,也不是你应该采取的方法。读者应该采取的方法是,改变处理代码,不再将每个数字的行字符串一次性地添加到行,而是逐个字符添加,遇到*时,就使用相关的数字替代。

为实现上述方法,可以复制本章中讲解的bigdigits.py,并修改其中大概5行,这些工作并不难,但稍有些微妙之处。对于这一练习题,提供了解决方案bigdigits_ans.py。

2.IDLE可以用作一个功能非常强大而灵活的计算器,但有时候,针对特定任务的计算器也是有用的。创建一个程序,该程序提示用户在while循环中输入数值,并根据输入的数值逐步构建一个列表。用户结束输入(按Enter键)时,打印出输入的数值本身、输入数值个数、输入数值和、输入的最小值与最大值以及平均值(sum/count),下面给出一个运行实例:

average1_ans.py
enter a number or Enter to finish: 5
enter a number or Enter to finish: 4
enter a number or Enter to finish: 1
enter a number or Enter to finish: 8
enter a number or Enter to finish: 5
enter a number or Enter to finish: 2
enter a number or Enter to finish:
numbers: [5, 4, 1, 8, 5, 2]
count = 6 sum = 25 lowest = 1 highest = 8 mean = 4.16666666667

要完成这一程序,需要大约4行代码初始化必要的变量(空列表使用[]表示),少于15行代码实现while循环,包括基本的错误处理,最后打印相关结果也可以在几行代码中实现,因此,整个程序(包括为代码清晰添加的空白行)应该在25行左右。

3.有些情况下,我们需要生成测试文本——比如,在网站真实内容可用之前,生成一个Web站点的设计方案,或者在开发报告写入者之前提供测试内容。为这一目的,可以编写一个用于生成可怕的诗歌(那种让Vogon都自愧不如的诗歌)的程序。

创建一些词汇列表,比如,冠词("the"、"a"等)、主题("cat"、"dog"、"man"、"woman")、动词("sang"、"ran"、"jumped")与状语("loudly"、"quietly"、"well"、"badly")等,之后循环5次,在每次迭代中,使用random.choice()函数选取冠词、主题、动词、状语等内容。使用random.randint()函数在两种语句结构之间进行选择:冠词、主题、动词、状语;只包括冠词、主题与动词,之后打印语句,下面给出了一个运行的实例:

awfulpoetry1_ans.py
another boy laughed badly
the woman jumped
a boy hoped
a horse jumped
another man laughed rudely

为实现上述功能,你需要导入random模块。列表部分大概需要4~10行代码完成,具体代码量依赖于在其中放置多少词汇;循环本身需要不到10行代码,加上一些必要的空白行,整个程序代码量大约在20行左右。对于这一练习题,提供了解决方案awfulpoetry1_ans.py。

4.为了使得产生可怕诗歌的程序功能更丰富,可以向其中添加一些代码,以便于用户在命令行上输入一个数字(在1与10之间)时,程序将输出该数字代表的行数。如果没有给定命令行参数,默认就像以前一样打印5行。为了完成上述任务,需要改变主循环(比如,变为一个while循环)。要记住的是,Python的比较操作符可以结链,因此,在检查某参数是否在范围内时,并不需要使用逻辑and操作符。添加这些额外的功能,大概需要10行代码。对于这一练习题,提供了解决方案awfulpoetry2_ans.py.

5.对于练习2,如果可以计算中间值以及平均值,那么应该是一个不错的功能。为了做到这一点,我们必须对列表进行排序。在Python中,列表可以很容易地使用list.sort()方法排序,但是目前尚未讲解这方面的知识,因此这里不适用这种方法。使用一段对数列表排序的代码,扩展平均值计算程序——不需要太考虑性能问题,使用你能想到的最简单的方法即可。对列表排序后,如果列表有奇数个数据项,中间值就是中间那个数据项的值;如果列表有偶数个数据项,中间值就是两个中间项值的平均值。计算中间值,并将其与其他相关信息一起输出。

上面的要求还是相当棘手的,对不熟练的程序员更是如此。如果你已具备一些Python编程经验,可能仍然会感觉到有些棘手,尤其是要限定在使用本书到此为止讲解的相关知识。排序需要大约十几行代码,中间值计算(注意不能使用modulus操作符,因为本书尚未讲到)大约要4行代码。对于这一练习题,提供了解决方案average2_ans.py。

 UNIX提示符可能并不是$的形式,这并无影响。

 用户与控制台之间的交互是由“shell”程序处理的,控制台与shell之间的差别这里不作讨论,我们使用这两个概念时可以互换。

 回溯(有时也称为函数调用栈)是一个调用列表,其中包含了从待处理异常出现点回溯到调用栈栈顶的所有调用。

 本书中,省略号(…)用于代表没有展示的行。

 sys模块、一些其他的内置模块以及以C语言实现的模块并不一定必须有相应的.py文件,但使用的方式是一样的。


相关图书

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

相关文章

相关课程