Python核心编程(中文第二版)带目录

mp68 贡献于2016-02-02

作者 administrator  创建于2012-05-08 05:26:00   修改者administrator  修改于2012-05-08 05:26:00字数121829

文档摘要:Python核心编程(中文第二版)带目录.txt什么叫乐观派?这个。。。。。。就象茶壶一样,屁股被烧得红红的,还有心情吹口哨。生活其实很简单,过了今天就是明天。一生看一个女人是不科学的,容易看出病来。符串的文件名.第9-19行我们创建了一个Unicode字符串,用我们指定的编码格式对其进行编码,然后把它写入到文 ,接着我们把内容从文件中重新读出来,解码,显示到屏幕上,输出的时候去掉print的自动换行,因为我们已经在字符串中写了一个换行符(15-19行).例6.2简单Unicode字符串例子(uniFile.py)这个简单的例子中,我们把一个Unicode字符串写入到磁盘文件,然后再把它读出并显示出来。
关键词:

Python核心编程(中文第二版)带目录.txt什么叫乐观派?这个。。。。。。就象茶壶一样,屁股被烧得红红的,还有心情吹口哨。生活其实很简单,过了今天就是明天。一生看一个女人是不科学的,容易看出病来。符串的文 件名. 第 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 2 3 4 5 6 7 8 9 #!/usr/bin/env python ''' An example of reading and writing Unicode strings:Writes a Unicode string to a file in utf-8 and reads itback in. ''' CODEC = 'utf-8' FILE = 'unicode.txt' 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 就是这么简单: 程序中出现字符串时一定要加个前缀 u. 不要用 str()函数,用 unicode()代替. 不要用过时的 string 模块 -- 如果传给它的是非 ASCII 字符,它会把一切搞砸。 不到必须时不要在你的程序里面编解码 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 的支持: 数据库服务器(MySQL,PostgreSQL,SQL Server,等等) 数据库适配器(MySQLdb 等等) 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 进行单方面加密 a difflib 找出序列间的不同 b 多种不同安全哈希算法和信息摘要算法的 API hashlib c hma HMAC 信息鉴权算法的 Python 实现 d RSA 的 MD5 信息摘要鉴权 md5 rotor 提供多平台的加解密服务 d sha NIAT 的安全哈希算法 SHA e stringprep 提供用于 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 >>> 1ist1 True >>> list2 False >>> list2 True = ['abc', 123] < list2 < list3 > list3 and list1 == list3 在使用比较操作符时,比较数字和字符串是很明了的,但是用在列表上时就不是那么简单了, 列表比较操作有些狡猾,但是合乎逻辑.比较列表时也是用的内建的 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 6.13.2 len() 序列类型函数 对字符串来说 len()返回字符串的长度,就是字符串包含的字符个数.对列表或者元组来说, 它会像你想像的那样返回列表或者元组的元素个数,容器里面的每个对象被作为一个项来处理. 我们下面的例子用了上面已经定义的列表. >>> 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 list.append(obj) list.count(obj) list.extend(seq)a list.index(obj, i=0, j=len(list)) Operation 向列表中添加一个对象 obj 返回一个对象 obj 在列表中出现的次数 把序列 seq 的内容添加到列表中 返回 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 2 3 45 6 7 8 9 #!/usr/bin/env python stack = [] def pushit(): stack.append(raw_input('Enter new string: ').strip()) def popit(): 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 operator re StringIO/ cStringIO 一种受限制的可变序列类型,要求所有的元素必须都是相同的类型。 提供浅拷贝和深拷贝的能力(详见 6.20) 包含函数调用形式的序列操作符,比如 operator.concat(m,n)就相当于连 接操作(m+n)。 Perl 风格的正则表达式查找(和匹配);见第 15 章 把长字符串作为文件来操作,比如 read(),seek()函数等,C 版的更快一些, 但是它不能被继承. a Textwrap 用作包裹/填充文本的函数,也有一个类 types 包含 Python 支持的所有类型 b collections 高性能容器数据类型 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) [12092832, >>> [id(x) [12191712, for x in hubby] 11826320] for x in wifey] 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 15 16 17 18 19 20 21 22 23 24 25 26 27 # i = 0 # while i < len(fac_list): # if num_num % fac_list[i] == 0: del fac_list[i] # i = i + 1 # 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? 映射和集合类型? 本章主题 映射类型: 字典 操作符 内建函数 内建方法 字典的键 集合类型 操作符 内建函数 内建方法 相关模块 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'] dict2.clear() del dict2 dict2.pop('name') # # # # 删除键为“name”的条目 删除 dict2 中所有的条目 删除整个 dict2 字典 删除并返回键为“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 d[k] 通过键'k',给字典中某元素赋值'v' 通过键'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 中也存在。 >>> >>> 1 >>> >>> 0 dict1['port'] = 8080 cmp(dict1, dict2) dict1['port'] = 80 cmp(dict1, dict2) 在向 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). } >>> >>> 1 >>> >>> -1 dict1['prot'] = 'tcp' cmp(dict1, dict2) dict2['prot'] = 'udp' cmp(dict1, dict2) 当向两个字典中的任何一个添加新元素时,这个字典马上会成为大的那个字典,就像例子中的 dict1 一样。向 dict2 添加键-值对后,因为两个字典的长度又相等了,会继续比较它们的键和值。 >>> >>> >>> 0 >>> >>> >>> 14 cdict = {'fruits':1} ddict = {'fruits':1} cmp(cdict, ddict) cdict['oranges'] = 0 ddict['apples'] = 0 cmp(cdict, ddict) 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): TypeError: list objects are >>> >>> dict2[{}] = 'foo' Traceback (most recent call TypeError: dict objects are File "", line 1, in ? unhashable last): File "", line 1, in ? unhashable 在表 7.1 中,我们列出以下三个映射类型的相关函数。 表 7.1 映射类型的相关函数 Edit?By?Vheavens? Edit?By?Vheavens? 函数 dict([container]) len(mapping) hash(obj) 操作 创 建 字 典 的 工 厂 函 数 。 如 果 提 供 了 容 器 类 (container) , 就 用其中的条目填充字典,否则就创建一个空字典。 返回映射的长度(键-值对的个数) 返回 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 字典类型方法 方法名字 操作 a 删除字典中所有元素 dict.clear () a dict.copy () 返回字典(浅复制)的一个副本 c dict.fromkeys (seq, Edit?By?Vheavens? Edit?By?Vheavens? val=None) c dict.get(key, default=None)a dict.has_key(key) 创建并返回一个新字典,以 seq 中的元素做该字典的键,val 做该字 典中所有键对应的初始值(如果不提供此值,则默认为 None) 对字典 dict 中的键 key,返回它对应的值 value,如果字典中不存在此 键,则返回 default 的值(注意,参数 default 的默认值为 None) 如果键(key)在字典中存在, 返回 True, 否则返回 False. 在 Python2.2 版本引入 in 和 not in 后,此方法几乎已废弃不用了,但仍提供一个 可工作的接口。 返回一个包含字典中(键, 值)对元组的列表 返回一个包含字典中键的列表 方法 iteritems(), iterkeys(), itervalues()与它们对应的非迭代方法 一样,不同的是它们返回一个迭代子,而不是一个列表。 和方法 get()相似,如果字典中 key 键存在,删除并返回 dict[key], 如果 key 键不存在,且没有给出 default 的值,引发 KeyError 异常。 dict.items() dict.keys() dict.iter()d dict.popc(key [, default]) c dict.setdefault(key, default=None)e 和方法 set()相似,如果字典中不存在 key 键,由 dict[key]=default 为 它赋值。 a dict.update(dict2) 将字典 dict2 的键-值对添加到字典 dict dict.values() 返回一个包含字典中所有值的列表 a. b. c. d. e. Python 1.5 新增 关于深复制和浅复制的详细信息请参见 6.19 节. Python 2.3 新增 Python 2.2 新增 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 33 34 35 36 37 38 39 Enter choice: """ done = False while not done: chosen = False while not chosen: try: Edit?By?Vheavens? Edit?By?Vheavens? 40 41 42 43 44 45 46 47 49 50 newuser() 51 olduser() 52 53 if __name__ 54 showmenu() choice = raw_input(prompt).strip()[0].lower() except (EOFError, KeyboardInterrupt): choice = 'q' print '\nYou picked: [%s]' % choice if choice not in 'neq': print 'invalid option, try again' else: chosen = True done = True == '__main__': 他们提供登录名和密码。帐号建立后,已存在用户可用登录名和正确的密码重返系统。新用户 不能用别人的登录名建立帐号。 逐行解释 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 = >>> t = >>> 'k' False >>> 'k' True >>> 'c' True set('cheeseshop') frozenset('bookshop') in s in t not in t 集合等价/不等价 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) set([obj]) frozenset([obj]) obj in s obj not in s s == t s != t s < t s.issubset(t) s <= t s > t s.issuperset(t) s >= t 说明 集合基数: 集合 s 中元素的个数 可变集合工厂函数; obj 必须是支持迭代的,由 obj 中 的元素创建集合,否则创建一个空集合 不可变集合工厂函数; 执行方式和 set()方法相同, 但它返回的是不可变集合 成员测试:obj 是 s 中的一个元素吗? 非成员测试:obj 不是 s 中的一个元素吗? 等价测试: 测试 s 和 t 是否具有相同的元素? 不等价测试: 与==相反 (严格意义上)子集测试; s != t 而且 s 中 所 有 的元素都是 t 的成员 子集测试(允许不严格意义上的子集): s 中所有的元素 都是 t 的成员 (严格意义上)超集测试: s != t 而且 t 中所有的元素 都是 s 的成员 超集测试(允许不严格意义上的超集): t 中所有的元素 都是 s 的成员 合并操作:s 或 t 中的元素 交集操作:s 和 t 中的元素 差分操作: s 中的元素,而不是 t 中的元素 对称差分操作:s 或 t 中的元素,但不是 s 和 t 共有 的元素 复制操作:返回 s 的(浅复制)副本 s.union(t) s | t s.intersec- tion(t) s & t s.difference(t) s - t s.symmetric_difference(t)s ^ t s.copy() Table 7.6 集合类型,函数和方法(继续) Edit?By?Vheavens? Edit?By?Vheavens? 函数/方法名字 操作符 仅用于可变集合 s.update(t) s |= t s.intersection_update(t) s &= t s.difference_update(t) s -= t s.symmetric_ difference_ update(t) s ^= t 等价描述 (Union) 修改操作: 将 t 中的成员添加 s 交集修改操作: s 中仅包括 s 和 t 中共有的成员 差修改操作: s 中包括仅属于 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? 条件和循环? 本章主题 if 语句 else 语句 elif 语句 条件表达式 while 语句 for 语句 break 语句 continue 语句 pass 语句 else 语句 (再看) Iterators 迭代器 列表解析(List Comprehensions) 生成器表达式(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) 众所周知, 使用映射对象(比如字典)的一个最大好处就是它的搜索操作比类似 语句或是 for 循环这样的序列查询要快很多. if-elif-else 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 的操作: >>> >>> ... ... ... ... >>> 3 x, y = 4, 3 if x < y: smaller = x else: smaller = y smaller 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? 结果: >>> >>> ... ... ... the the the the the the the the the count = 0 while (count < 9): print 'the index is:', count count += 1 index index index index index index index index index is: is: is: is: is: is: is: is: is: 0 1 2 3 4 5 6 7 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.5xrange() 内建函数 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 1987 1990 2003 Poe Gaudi Freud 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 12 13 14 15 else: print num, "is prime" for eachNum in range(10, 21): 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 8.11.1 迭代器和 iter() 函数 什么是迭代器? 迭代器是在版本 2.2 被加入 Python 的, 它为类序列对象提供了一个类序列的接口. 我们在 前边的第 6 章已经正式地介绍过序列. 它们是一组数据结构,你可以利用它们的索引从 0 开始一直 "迭代" 到序列的最后一个条目. 用"计数"的方法迭代序列是很简单的. Python 的迭代无缝地支持 序列对象, 而且它还允许程序员迭代非序列类型, 包括用户定义的对象. 迭代器用起来很灵巧, 你可以迭代不是序列但表现出序列行为的对象, 例如字典的 key , 一个 文件的行, 等等. 当你使用循环迭代一个对象条目时, 你几乎不可能分辨出它是迭代器还是序列. 你不必去关注这些, 因为 Python 让它象一个序列那样操作. 8.11.2 为什么要迭代器? 援引 PEP (234) 中对迭代器的定义: 提供了可扩展的迭代器接口. 对列表迭代带来了性能上的增强. 在字典迭代中性能提升. 创建真正的迭代接口, 而不是原来的随机对象访问. 与所有已经存在的用户定义的类以及扩展的模拟序列和映射的对象向后兼容 迭代非序列集合(例如映射和文件)时, 可以创建更简洁可读的代码. 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 calllast): 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 pass elif x < 0: # statement pass else: # statement pass # statement B C D 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? 文件和输入输出? ? ? ? ? ? 本章主题 文件对象 文件内建函数 文件内建方法 文件内建属性 标准文件 命令行参数 文件系统 文件执行 持久存储 相关模块 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 w a r+ w+ a+ rb wb ab rb+ wb+ ab+ a. 以读方式打开, 同时提供通用换行符支持 (PEP 278) 以写方式打开 (必要时清空) 以追加模式打开 (从 EOF 开始, 必要时创建新文件) 以读写模式打开 以读写模式打开 (参见 w ) 以读写模式打开 (参见 a ) 以二进制读模式打开 以二进制写模式打开 (参见 w ) 以二进制追加模式打开 (参见 a ) 以二进制读写模式打开 (参见 r+ ) 以二进制读写模式打开 (参见 w+ ) 以二进制读写模式打开 (参见 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.3 文件迭代 一行一行访问文件很简单: 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.flush() file.isatty() file.nexta() 操作 关闭文件 返回文件的描述符(file descriptor ,FD, 整数值) 刷新文件的内部缓冲区 判断 file 是否是一个类 tty 设备 返回文件的下一行(类似于 file.readline() ), 或在没有其它行时 引发 StopIteration 异常 file.read(size=-1) 从文件读取 size 个字节, 当未给定 size 或给定负值的时候, 读 取剩余的所有字节, 然后作为字符串返回 b file.readinto (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———译者按) c file.xreadlines () 用于迭代, 可以替换 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. b. c. Python 2.2 中新增 Python 1.5.2 中新增, 不再支持 Python 2.1 中新增, 在 Python 2.3 中废弃 9.4 文件内建属性 文件对象除了方法之外,还有一些数据属性. 这些属性保存了文件对象相关的附加数据, 例如 文件名(file.name ), 文件的打开模式 ( file.mode ), 文件是否已被关闭 ( file.closed), 以及 一 个 标 志 变 量 , 它 可 以 决 定 使 用 print 语 句 打 印 下 一 行 前 是 否 要 加 入 一 个 空 白 字 符 ( file.softspace ). 表 9.4 列出了这些属性并做了简短说明。 表 9.4 文件对象的属性 文件对象的属性 file.closed file.encodinga 描述 True 表示文件已经被关闭, 否则为 False 文件所使用的编码 - 当 Unicode 字符串被写入数据时, 它们将自动使 用 file.encoding 转换为字节字符串; 若 file.encoding 为 None 时使 用系统默认编码 文件打开时使用的访问模式 文件名 未读取到行分隔符时为 None , 只有一种行分隔符时为一个字符串, 当 文件有多种类型的行结束符时,则为一个包含所有当前所遇到的行结束 符的列表 为 0 表示在输出一数据后,要加上一个空格符,1 表示不加。这个属性 file.mode file.name file.newlinesa file.softspace 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] 永远是程序的名称. 总结如下: sys.argv 是命令行参数的列表 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 删除文件 b rename()/renames() 重命名文件 c *stat () 返回文件信息 symlink() 创建符号链接 utime() 更新时间戳 tmpfile() 创建并打开('w+b')一个新的临时文件 a 生成一个目录树下的所有文件名 walk() 目录/文件夹 chdir()/fchdir()a 改变当前工作目录/通过一个文件描述符改变当前工作目录 d 改变当前进程的根目录 chroot() listdir() 列出指定目录的文件 a getcwd()/getcwdu() 返回当前工作目录/功能相同, 但返回一个 Unicode 对象 mkdir()/makedirs() 创建目录/创建多层目录 rmdir()/removedirs() 删除目录/删除多层目录 访问/权限 access() 检验权限模式 chmod() 改变权限模式 a chown()/lchown() 改变 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. b. c. d. New in Python 2.3. New in Python 1.5.2. Includes stat(), lstat(), xstat(). 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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 print '*** (filename, extension) ==' print os.path.splitext(os.path.basename(path)) print '*** displaying file contents:' fobj = open(path) for eachLine in fobj: print eachLine, fobj.close() print '*** deleting test file' os.remove(path) print '*** updated directory listing:' print os.listdir(cwd) os.chdir(os.pardir) print '*** deleting test directory' os.rmdir('example') 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 编码的二进制字符串间的编码/解码操作 a bz2 访问 BZ2 格式的压缩文件 a 访问 csv 文件(逗号分隔文件) csv b filecmp 用于比较目录和文件 fileinput 提供多个文本文件的行迭代器 a 提供了命令行参数的解析/处理 getopt/optparse glob/fnmatch 提供 Unix 样式的通配符匹配的功能 gzip/zlib 读写 GNU zip( gzip) 文件(压缩需要 zlib 模块) shutil 提供高级文件访问功能 c/StringIO 对字符串对象提供类文件接口 tarfilea 读写 TAR 归档文件, 支持压缩文件 tempfile 创建一个临时文件(名) uu 格式的编码和解码 c zipfile 用于读取 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. 9–3. 文件访问. 提示输入数字 N 和文件 F, 然后显示文件 F 的前 N 行. 文件信息. 提示输入一个文件名, 然后显示这个文本文件的总行数. 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 3 $ calc.py 27 $ calc.py 1 + 2 3 3 ^ 3 27 $ calc.py $ 1 + 2 3 ^ 3 print 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? 错误和异常 本章主题 什么是异常? Python 中的异常 探测和处理异常 上下文管理 引发异常 断言 标准异常 创建异常 相关模块 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 将会是一个包含来自导致异常的代码的诊断信息的类实例. 异常参数自身会组成一个 元 组 , 并 存 储 为 类 实 例 ( 异 常 类

下载文档到电脑,查找使用更方便

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 8 金币 [ 分享文档获得金币 ] 0 人已下载

下载文档