Dive Into Python 中文版(二)
第五章 对象和面向对象
第五章 对象和面向对象
- 5.1. 概览
- 5.2. 使用 from module import 导入模块
- 5.3. 类的定义
- 5.3.1. 初始化并开始类编码
- 5.3.2. 了解何时去使用 self 和 init
- 5.4. 类的实例化
- 5.4.1. 垃圾回收
- 5.5. 探索 UserDict:一个封装类
- 5.6. 专用类方法
- 5.6.1. 获得和设置数据项
- 5.7. 高级专用类方法
- 5.8. 类属性介绍
- 5.9. 私有函数
- 5.10. 小结
这一章,和此后的许多章,均讨论了面向对象的 Python 程序设计。
5.1. 概览
5.1. 概览
下面是一个完整的,可运行的 Python 程序。请阅读模块、类和函数的 doc strings,可以大概了解这个程序所做的事情和工作情况。像平时一样,不用担心你不理解的东西,这就是本章其它部分将告诉你的内容。
例 5.1. fileinfo.py
如果您还没有下载本书附带的样例程序, 可以 下载本程序和其他样例程序。
"""Framework for getting filetype-specific metadata.
Instantiate appropriate class with filename. Returned object acts like a
dictionary, with key-value pairs for each piece of metadata.
import fileinfo
info = fileinfo.MP3FileInfo("/music/ap/mahadeva.mp3")
print "\\n".join(["%s=%s" % (k, v) for k, v in info.items()])
Or use listDirectory function to get info on all files in a directory.
for info in fileinfo.listDirectory("/music/ap/", [".mp3"]):
...
Framework can be extended by adding classes for particular file types, e.g.
HTMLFileInfo, MPGFileInfo, DOCFileInfo. Each class is completely responsible for
parsing its files appropriately; see MP3FileInfo for example.
"""
import os
import sys
from UserDict import UserDict
def stripnulls(data):
"strip whitespace and nulls"
return data.replace("\00", "").strip()
class FileInfo(UserDict):
"store file metadata"
def __init__(self, filename=None):
UserDict.__init__(self)
self["name"] = filename
class MP3FileInfo(FileInfo):
"store ID3v1.0 MP3 tags"
tagDataMap = {"title" : ( 3, 33, stripnulls),
"artist" : ( 33, 63, stripnulls),
"album" : ( 63, 93, stripnulls),
"year" : ( 93, 97, stripnulls),
"comment" : ( 97, 126, stripnulls),
"genre" : (127, 128, ord)}
def __parse(self, filename):
"parse ID3v1.0 tags from MP3 file"
self.clear()
try:
fsock = open(filename, "rb", 0)
try:
fsock.seek(-128, 2)
tagdata = fsock.read(128)
finally:
fsock.close()
if tagdata[:3] == "TAG":
for tag, (start, end, parseFunc) in self.tagDataMap.items():
self[tag] = parseFunc(tagdata[start:end])
except IOError:
pass
def __setitem__(self, key, item):
if key == "name" and item:
self.__parse(item)
FileInfo.__setitem__(self, key, item)
def listDirectory(directory, fileExtList):
"get list of file info objects for files of particular extensions"
fileList = [os.path.normcase(f)
for f in os.listdir(directory)]
fileList = [os.path.join(directory, f)
for f in fileList
if os.path.splitext(f)[1] in fileExtList]
def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]):
"get file info class from filename extension"
subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:]
return hasattr(module, subclass) and getattr(module, subclass) or FileInfo
return [getFileInfoClass(f)(f) for f in fileList]
if __name__ == "__main__":
for info in listDirectory("/music/_singles/", [".mp3"]):
print "\n".join(["%s=%s" % (k, v) for k, v in info.items()])
print
| [1] | 这个程序的输入要取决于你硬盘上的文件。为了得到有意义的输出,你应该修改目录路径指向你自已机器上的一个 MP3 文件目录。 |
下面就是从我的机器上得到的输出。你的输出将不一样,除非,由于某些令人吃惊的巧合,你与我有着共同的音乐品味。
album=
artist=Ghost in the Machine
title=A Time Long Forgotten (Concept
genre=31
name=/music/_singles/a_time_long_forgotten_con.mp3
year=1999
comment=http://mp3.com/ghostmachine
album=Rave Mix
artist=***DJ MARY-JANE***
title=HELLRAISER****Trance from Hell
genre=31
name=/music/_singles/hellraiser.mp3
year=2000
comment=http://mp3.com/DJMARYJANE
album=Rave Mix
artist=***DJ MARY-JANE***
title=KAIRO****THE BEST GOA
genre=31
name=/music/_singles/kairo.mp3
year=2000
comment=http://mp3.com/DJMARYJANE
album=Journeys
artist=Masters of Balance
title=Long Way Home
genre=31
name=/music/_singles/long_way_home1.mp3
year=2000
comment=http://mp3.com/MastersofBalan
album=
artist=The Cynic Project
title=Sidewinder
genre=18
name=/music/_singles/sidewinder.mp3
year=2000
comment=http://mp3.com/cynicproject
album=Digitosis@128k
artist=VXpanded
title=Spinning
genre=255
name=/music/_singles/spinning.mp3
year=2000
comment=http://mp3.com/artists/95/vxp
5.2. 使用 from _module_ import 导入模块
5.2. 使用 from _module_ import 导入模块
Python 有两种导入模块的方法。两种都有用,你应该知道什么时候使用哪一种方法。一种方法,import _module_,你已经在第 2.4 节 “万物皆对象”看过了。另一种方法完成同样的事情,但是它与第一种有着细微但重要的区别。
下面是 from _module_ import 的基本语法:
from UserDict import UserDict
它与你所熟知的 import _module_ 语法很相似,但是有一个重要的区别:UserDict 被直接导入到局部名字空间去了,所以它可以直接使用,而不需要加上模块名的限定。你可以导入独立的项或使用 from _module_ import * 来导入所有东西。
注意 Python 中的
from _module_ import *像 Perl 中的use _module_;Python 中的import _module_像 Perl 中的require _module_。注意 Python 中的
from _module_ import *像 Java 中的import _module_.*;Python 中的import _module_像 Java 中的import _module_。
例 5.2. import _module_ vs. from _module_ import
>>> import types
>>> types.FunctionType
<type 'function'>
>>> FunctionType
Traceback (innermost last):
File "<interactive input>", line 1, in ?
NameError: There is no variable named 'FunctionType'
>>> from types import FunctionType
>>> FunctionType
<type 'function'>
| [1] | types 模块不包含方法,只是表示每种 Python 对象类型的属性。注意这个属性必需用模块名 types 进行限定。 |
| [2] | FunctionType 本身没有被定义在当前名字空间中;它只存在于 types 的上下文环境中。 |
| [3] | 这个语法从 types 模块中直接将 FunctionType 属性导入到局部名字空间中。 |
| [4] | 现在 FunctionType 可以直接使用,与 types 无关了。 |
什么时候你应该使用 from _module_ import?
- 如果你要经常访问模块的属性和方法,且不想一遍又一遍地敲入模块名,使用
from _module_ import。 - 如果你想要有选择地导入某些属性和方法,而不想要其它的,使用
from _module_ import。 - 如果模块包含的属性和方法与你的某个模块同名,你必须使用
import _module_来避免名字冲突。
除了这些情况,剩下的只是风格问题了,你会看到用两种方式编写的 Python 代码。
小心 尽量少用
from module import *,因为判定一个特殊的函数或属性是从哪来的有些困难,并且会造成调试和重构都更困难。
进一步阅读关于模块导入技术
- eff-bot 有更多关于
import _module_vs.from _module_ import的论述。 - Python Tutorial 讨论了高级的导入技术,包括
from _module_ import *。
5.3. 类的定义
5.3. 类的定义
- 5.3.1. 初始化并开始类编码
- 5.3.2. 了解何时去使用 self 和 init
Python 是完全面向对象的:你可以定义自已的类,从自已的或内置的类继承,然后从你定义的类创建实例。
在 Python 中定义类很简单。就像定义函数,没有单独的接口定义。只要定义类,然后就可以开始编码。Python 类以保留字 class 开始,后面跟着类名。从技术上讲,有这些就够了,因为一个类并非必须从其它类继承。
例 5.3. 最简单的 Python 类
class Loaf:
pass
| [1] | 这个类的名字是 Loaf,它没有从其它类继承。类名通常是第一个字母大写,如:EachWordLikeThis,但这只是一个习惯,不是一个必要条件。 |
| [2] | 这个类没有定义任何方法或属性,但是从语法上,需要在定义中有些东西,所以你使用 pass。这是一个 Python 保留字,仅仅表示 “向前走,不要往这看”。它是一条什么都不做的语句,当你删空函数或类时,它是一个很好的占位符。 |
| [3] | 你可能猜到了,在类中的所有东西都要缩近,就像位于函数、if 语句,for 循环,诸如此类的代码。第一条不缩近的东西不属于这个类。 |
注意 在 Python 中的
pass语句就像 Java 或 C 中的大括号空集 ({})。
当然,实际上大多数的类都是从其它的类继承来的,并且它们会定义自已的类方法和属性。但是就像你刚才看到的,除了名字以外,类没有什么必须要具有的。特别是,C++ 程序员可能会感到奇怪,Python 的类没有显示的构造函数和析构函数。Python 类的确存在与构造函数相似的东西:__init__ 方法。
例 5.4. 定义 FileInfo 类
from UserDict import UserDict
class FileInfo(UserDict):
| [1] | 在 Python 中,类的基类只是简单地列在类名后面的小括号里。所以 FileInfo 类是从 UserDict 类 (它是从 UserDict 模块导进来的) 继承来的。UserDict 是一个像字典一样工作的类,它允许你完全子类化字典数据类型,同时增加你自已的行为。{也存在相似的类 UserList 和 UserString ,它们允许你子类化列表和字符串。)[2] 在这个类的背后有一些“巫术”,我们将在本章的后面,随着更进一步地研究 UserDict 类,揭开这些秘密。 |
注意 在 Python 中,类的基类只是简单地列在类名后面的小括号里。不像在 Java 中有一个特殊的
extends关键字。
Python 支持多重继承。在类名后面的小括号中,你可以列出许多你想要的类名,以逗号分隔。
5.3.1. 初始化并开始类编码
本例演示了使用 __init__ 方法来进行 FileInfo 类的初始化。
例 5.5. 初始化 FileInfo 类
class FileInfo(UserDict):
"store file metadata"
def __init__(self, filename=None):
| [1] | 类也可以 (并且应该) 有 doc strings ,就像方法和函数一样。 |
| [2] | __init__ 在类的实例创建后被立即调用。它可能会引诱你称之为类的构造函数,但这种说法并不正确。说它引诱,是因为它看上去像 (按照习惯,__init__ 是类中第一个定义的方法),行为也像 (在一个新创建的类实例中,它是首先被执行的代码),并且叫起来也像 (“init”当然意味着构造的本性)。说它不正确,是因为对象在调用 __init__ 时已经被构造出来了,你已经有了一个对类的新实例的有效引用。但 __init__ 是在 Python 中你可以得到的最接近构造函数的东西,并且它也扮演着非常相似的角色。 |
| [3] | 每个类方法的第一个参数,包括 __init__,都是指向类的当前实例的引用。按照习惯这个参数总是被称为 self。在 __init__ 方法中,self 指向新创建的对象;在其它的类方法中,它指向方法被调用的类实例。尽管当定义方法时你需要明确指定 self,但在调用方法时,你不 用指定它,Python 会替你自动加上的。 |
| [4] | __init__ 方法可以接受任意数目的参数,就像函数一样,参数可以用缺省值定义,即可以设置成对于调用者可选。在本例中,filename 有一个缺省值 None,即 Python 的空值。 |
注意 习惯上,任何 Python 类方法的第一个参数 (对当前实例的引用) 都叫做
self。这个参数扮演着 C++ 或 Java 中的保留字this的角色,但self在 Python 中并不是一个保留字,它只是一个命名习惯。虽然如此,也请除了self之外不要使用其它的名字,这是一个非常坚固的习惯。
例 5.6. 编写 FileInfo 类
class FileInfo(UserDict):
"store file metadata"
def __init__(self, filename=None):
UserDict.__init__(self)
self["name"] = filename
| [1] | 一些伪面向对象语言,像 Powerbuilder 有一种“扩展”构造函数和其它事件的概念,即父类的方法在子类的方法执行前被自动调用。Python 不是这样,你必须显示地调用在父类中的合适方法。 |
| [2] | 我告诉过你,这个类像字典一样工作,那么这里就是第一个印象。我们将参数 filename 赋值给对象 name 关键字,作为它的值。 |
| [3] | 注意 __init__ 方法从不返回一个值。 |
5.3.2. 了解何时去使用 self 和 __init__
当定义你自已的类方法时,你必须 明确将 self 作为每个方法的第一个参数列出,包括 __init__。当从你的类中调用一个父类的一个方法时,你必须包括 self 参数。但当你从类的外部调用你的类方法时,你不必对 self 参数指定任何值;你完全将其忽略,而 Python 会自动地替你增加实例的引用。我知道刚开始这有些混乱,它并不是自相矛盾的,因为它依靠于一个你还不了解的区别 (在绑定与非绑定方法之间),故看上去是矛盾的。
噢。我知道有很多知识需要吸收,但是你要掌握它。所有的 Python 类以相同的方式工作,所以一旦你学会了一个,就是学会了全部。如果你忘了别的任何事,也要记住这件事,因为我认定它会让你出错:
注意
__init__方法是可选的,但是一旦你定义了,就必须记得显示调用父类的__init__方法 (如果它定义了的话)。这样更是正确的:无论何时子类想扩展父类的行为,后代方法必须在适当的时机,使用适当的参数,显式调用父类方法。
进一步阅读关于 Python 类
- Learning to Program 有优雅的类的介绍。
- How to Think Like a Computer Scientist 展示了如何使用类来实现复合数据类型模型。
- Python Tutorial 深入考虑了类、名字空间和继承。
- Python Knowledge Base 回答了关于类的常见问题。
Footnotes
[2] 在 2.2 之后已经可以从 dict、list 来派生子类了,关于这一点作者在后文也会提到。――译注
5.4. 类的实例化
5.4. 类的实例化
- 5.4.1. 垃圾回收
在 Python 中对类进行实例化很直接。要对类进行实例化,只要调用类 (就好像它是一个函数),传入定义在 __init__ 方法中的参数。返回值将是新创建的对象。
例 5.7. 创建 FileInfo 实例
>>> import fileinfo
>>> f = fileinfo.FileInfo("/music/_singles/kairo.mp3")
>>> f.__class__
<class fileinfo.FileInfo at 010EC204>
>>> f.__doc__
'store file metadata'
>>> f
{'name': '/music/_singles/kairo.mp3'}
| [1] | 你正在创建 FileInfo 类 (定义在 fileinfo 模块中) 的实例,并且将新创建的实例赋值给变量 f。你传入了一个参数,/music/_singles/kairo.mp3,它将最后作为在 FileInfo 中 __init__ 方法中的 filename 参数。 |
| [2] | 每一个类的实例有一个内置属性,__class__,它是对象的类。(注意这个表示包括了在我机器上的实例的物理地址,你的表示不会一样。)Java 程序员可能对 Class 类熟悉,这个类包含了像 getName 和 getSuperclass 之类用来得到一个对象元数据信息的方法。在 Python 中,这类元数据可以直接通过对象本身的属性,像 __class__、__name__ 和 __bases__ 来得到。 |
| [3] | 你可以像对函数或模块一样来访问实例的 doc string。一个类的所有实例共享相同的 doc string。 |
| [4] | 还记得什么时候 __init__ 方法将它的 filename 参数赋给 self["name"] 吗?哦,答案在这。在创建类实例时你传入的参数被正确发送到 __init__ 方法中 (当我们创建类实例时,我们所传递的参数被正确地发送给 __init__ 方法 (随同一起传递的还有对象的引用,self,它是由 Python 自动添加的)。 |
注意 在 Python 中,创建类的实例只要调用一个类,仿佛它是一个函数就行了。不像 C++ 或 Java 有一个明确的
new操作符。
5.4.1. 垃圾回收
如果说创建一个新的实例是容易的,那么销毁它们甚至更容易。通常,不需要明确地释放实例,因为当指派给它们的变量超出作用域时,它们会被自动地释放。内存泄漏在 Python 中很少见。
例 5.8. 尝试实现内存泄漏
>>> def leakmem():
... f = fileinfo.FileInfo('/music/_singles/kairo.mp3')
...
>>> for i in range(100):
... leakmem()
| [1] | 每次 leakmem 函数被调用,你创建了 FileInfo 的一个实例,将其赋给变量 f,这个变量是函数内的一个局部变量。然后函数结束时没有释放 f,所以你可能认为有内存泄漏,但是你错了。当函数结束时,局部变量 f 超出了作用域。在这个地方,不再有任何对 FileInfo 新创建实例的引用 (因为除了 f 我们从未将其赋值给其它变量),所以 Python 替我们销毁掉实例。 |
| [2] | 不管我们调用 leakmem 函数多少次,决不会泄漏内存,因为每一次,Python 将在从 leakmem 返回前销毁掉新创建的 FileInfo 类实例。 |
对于这种垃圾收集的方式,技术上的术语叫做“引用计数”。Python 维护着对每个实例的引用列表。在上面的例子中,只有一个 FileInfo 的实例引用:局部变量 f。当函数结束时,变量 f 超出作用域,所以引用计数降为 0,则 Python 自动销毁掉实例。
在 Python 的以前版本中,存在引用计数失败的情况,这样 Python 不能在后面进行清除。如果你创建两个实例,它们相互引用 (例如,双重链表,每一个结点有都一个指向列表中前一个和后一个结点的指针),任一个实例都不会被自动销毁,因为 Python (正确) 认为对于每个实例都存在一个引用。Python 2.0 有一种额外的垃圾回收方式,叫做“标记后清除”,它足够聪明,可以正确地清除循环引用。
作为曾经读过哲学专业的一员,让我感到困惑的是,当没有人对事物进行观察时,它们就消失了,但是这确实是在 Python 中所发生的。通常,你可以完全忘记内存管理,让 Python 在后面进行清理。
进一步阅读
- Python Library Reference 总结了像
__class__之类的内置属性。 - Python Library Reference 提供了
gc模块的文档,此模块给予你对 Python 的垃圾回收的底层控制权。
5.5. 探索 UserDict:一个封装类
5.5. 探索 UserDict:一个封装类
如你所见,FileInfo 是一个有着像字典一样的行为方式的类。为了进一步揭示这一点,让我们看一看在 UserDict 模块中的 UserDict 类,它是我们的 FileInfo 类的父类。它没有什么特别的,也是用 Python 写的,并且保存在一个 .py 文件里,就像我们其他的代码。特别之处在于,它保存在你的 Python 安装目录的 lib 目录下。
提示 在 Windows 下的 ActivePython IDE 中,你可以快速打开在你的库路径中的任何模块,使用 File->Locate... (Ctrl-L)。
例 5.9. 定义 UserDict 类
class UserDict:
def __init__(self, dict=None):
self.data = {}
if dict is not None: self.update(dict)
| [1] | 注意 UserDict 是一个基类,不是从任何其他类继承而来。 |
| [2] | 这就是我们在 FileInfo 类中进行了覆盖的 __init__ 方法。注意这个父类的参数列表与子类不同。很好,每个子类可以拥有自已的参数集,只要使用正确的参数调用父类就可以了。这里父类有一个定义初始值的方法 (通过在 dict 参数中传入一个字典),这一方法我们的 FileInfo 没有用上。 |
| [3] | Python 支持数据属性 (在 Java 和 Powerbuilder 中叫做 “实例变量”,在 C++ 中叫 “数据成员”),它是由某个特定的类实例所拥有的数据。在本例中,每个 UserDict 实例将拥有一个 data 数据属性。要从类外的代码引用这个属性,需要用实例的名字限定它,_instance_.data,限定的方法与你用模块的名字来限定函数一样。要在类的内部引用一个数据属性,我们使用 self 作为限定符。习惯上,所有的数据属性都在 __init__ 方法中初始化为有意义的值。然而,这并不是必须的,因为数据属性,像局部变量一样,当你首次赋给它值的时候突然产生。 |
| [4] | update 方法是一个字典复制器:它把一个字典中的键和值全部拷贝到另一个字典。这个操作并不 事先清空目标字典,如果一些键在目标字典中已经存在,则它们将被覆盖,那些键名在目标字典中不存在的则不改变。应该把 update 看作是合并函数,而不是复制函数。 |
| [5] | 这个语法你可能以前没看过 (我还没有在这本书中的例子中用过它)。这是一条 if 语句,但是没有在下一行有一个缩近块,而只是在冒号后面,在同一行上有单条语句。这完全是合法的,它只是当你在一个块中仅有一条语句时的一个简写。(它就像在 C++ 中没有用大括号包括的单行语句。) 你可以用这种语法,或者可以在后面的行写下缩近代码,但是不能对同一个块同时用两种方式。 |
注意 Java 和 Powerbuilder 支持通过参数列表的重载,也就是 一个类可以有同名的多个方法,但这些方法或者是参数个数不同,或者是参数的类型不同。其它语言 (最明显如 PL/SQL) 甚至支持通过参数名的重载,也就是 一个类可以有同名的多个方法,这些方法有相同类型,相同个数的参数,但参数名不同。Python 两种都不支持,总之是没有任何形式的函数重载。一个
__init__方法就是一个__init__方法,不管它有什么样的参数。每个类只能有一个__init__方法,并且如果一个子类拥有一个__init__方法,它总是 覆盖父类的__init__方法,甚至子类可以用不同的参数列表来定义它。注意 Python 的原作者 Guido 是这样解释方法覆盖的:“子类可以覆盖父类中的方法。因为方法没有特殊的优先级设置,父类中的一个方法在调用同类中的另一方法时,可能其实调用到的却是一个子类中覆盖父类同名方法的方法。 (C++ 程序员可能会这样想:所有的 Python 方法都是虚函数。)”如果你不明白 (它令我颇感困惑),不必在意。我想我要跳过它。[3]
小心 应该总是在
__init__方法中给一个实例的所有数据属性赋予一个初始值。这样做将会节省你在后面调试的时间,不必为捕捉因使用未初始化 (也就是不存在) 的属性而导致的AttributeError异常费时费力。
例 5.10. UserDict 常规方法
def clear(self): self.data.clear()
def copy(self):
if self.__class__ is UserDict:
return UserDict(self.data)
import copy
return copy.copy(self)
def keys(self): return self.data.keys()
def items(self): return self.data.items()
def values(self): return self.data.values()
| [1] | clear 是一个普通的类方法,可以在任何时候被任何人公开调用。注意,clear 像所有的类方法一样 (常规的或专用的),使用 self 作为它的第一个参数。(记住,当你调用方法时,不用包括 self;这件事是 Python 替你做的。) 还应注意这个封装类的基本技术:将一个真正的字典 (data) 作为数据属性保存起来,定义所有真正字典所拥有的方法,并且将每个类方法重定向到真正字典上的相应方法。(你可能已经忘了,字典的 clear 方法删除它的所有关键字和关键字相应的值。) |
| [2] | 真正字典的 copy 方法会返回一个新的字典,它是原始字典的原样的复制 (所有的键-值对都相同)。但是 UserDict 不能简单地重定向到 self.data.copy,因为那个方法返回一个真正的字典,而我们想要的是返回同一个类的一个新的实例,就像是 self。 |
| [3] | 我们使用 __class__ 属性来查看 self 是否是一个 UserDict,如果是,太好了,因为我们知道如何拷贝一个 UserDict:只要创建一个新的 UserDict ,并传给它真正的字典,这个字典已经存放在 self.data 中了。然后你立即返回这个新的 UserDict,你甚至于不需要在下面一行中使用 import copy。 |
| [4] | 如果 self.__class__ 不是 UserDict,那么 self 一定是 UserDict 的某个子类 (如可能为 FileInfo),生活总是存在意外。UserDict 不知道如何生成它的子类的一个原样的拷贝,例如,有可能在子类中定义了其它的数据属性,所以我们只能完全复制它们,确定拷贝了它们的全部内容。幸运的是,Python 带了一个模块可以正确地完成这件事,它叫做 copy。在这里我不想深入细节 (然而它是一个绝对酷的模块,你是否已经想到要自已研究它了呢?)。说 copy 能够拷贝任何 Python 对象就够了,这就是我们在这里用它的原因。 |
| [5] | 其余的方法是直截了当的重定向到 self.data 的内置函数上。 |
注意 在 Python 2.2 之前的版本中,你不可以直接子类化字符串、列表以及字典之类的内建数据类型。作为补偿,Python 提供封装类来模拟内建数据类型的行为,比如:
UserString、UserList和UserDict。通过混合使用普通和特殊方法,UserDict类能十分出色地模仿字典。在 Python 2.2 和其后的版本中,你可以直接从dict内建数据类型继承。本书fileinfo_fromdict.py中有这方面的一个例子。
如例子中所示,在 Python 中,你可以直接继承自内建数据类型 dict,这样做有三点与 UserDict 不同。
例 5.11. 直接继承自内建数据类型 dict
class FileInfo(dict):
"store file metadata"
def __init__(self, filename=None):
self["name"] = filename
| [1] | 第一个区别是你不需要导入 UserDict 模块,因为 dict 是已经可以使用的内建数据类型。第二个区别是你不是继承自 UserDict.UserDict ,而是直接继承自 dict。 |
| [2] | 第三个区别有些晦涩,但却很重要。UserDict 内部的工作方式要求你手工地调用它的 __init__ 方法去正确初始化它的内部数据结构。dict 并不这样工作,它不是一个封装所以不需要明确的初始化。 |
进一步阅读
- Python Library Reference 提供了
UserDict模块 和copy模块 的文档。
Footnotes
[3] 实际上,这一点并不是那么难以理解。考虑两个类,base 和 child,base 中的方法 a 需要调用 self.b;而我们又在 child 中覆盖了方法 b。然后我们创建一个 child 的实例,ch。调用 ch.a,那么此时的方法 a 调用的 b 函数将不是 base.b,而是 child.b。――译注
5.6. 专用类方法
5.6. 专用类方法
- 5.6.1. 获得和设置数据项
除了普通的类方法,Python 类还可以定义专用方法。专用方法是在特殊情况下或当使用特别语法时由 Python 替你调用的,而不是在代码中直接调用 (像普通的方法那样)。
就像你在上一节所看到的,普通的方法对在类中封装字典很有帮助。但是只有普通方法是不够的,因为除了对字典调用方法之外,还有很多事情可以做的。例如,你可以通过一种没有包括明确方法调用的语法来获得和设置数据项。这就是专用方法产生的原因:它们提供了一种方法,可以将非方法调用语法映射到方法调用上。
5.6.1. 获得和设置数据项
例 5.12. __getitem__ 专用方法
def __getitem__(self, key): return self.data[key]
>>> f = fileinfo.FileInfo("/music/_singles/kairo.mp3")
>>> f
{'name':'/music/_singles/kairo.mp3'}
>>> f.__getitem__("name")
'/music/_singles/kairo.mp3'
>>> f["name"]
'/music/_singles/kairo.mp3'
| [1] | __getitem__ 专用方法很简单。像普通的方法 clear,keys 和 values 一样,它只是重定向到字典,返回字典的值。但是怎么调用它呢?哦,你可以直接调用 __getitem__,但是在实际中你其实不会那样做:我在这里执行它只是要告诉你它是如何工作的。正确地使用 __getitem__ 的方法是让 Python 来替你调用。 |
| [2] | 这个看上去就像你用来得到一个字典值的语法,事实上它返回你期望的值。下面是隐藏起来的一个环节:暗地里,Python 已经将这个语法转化为 f.__getitem__("name") 的方法调用。这就是为什么 __getitem__ 是一个专用类方法的原因,不仅仅是你可以自已调用它,还可以通过使用正确的语法让 Python 来替你调用。 |
当然,Python 有一个与 __getitem__ 类似的 __setitem__ 专用方法,参见下面的例子。
例 5.13. __setitem__ 专用方法
def __setitem__(self, key, item): self.data[key] = item
>>> f
{'name':'/music/_singles/kairo.mp3'}
>>> f.__setitem__("genre", 31)
>>> f
{'name':'/music/_singles/kairo.mp3', 'genre':31}
>>> f["genre"] = 32
>>> f
{'name':'/music/_singles/kairo.mp3', 'genre':32}
| [1] | 与 __getitem__ 方法一样,__setitem__ 简单地重定向到真正的字典 self.data ,让它来进行工作。并且像 __getitem__ 一样,通常你不会直接调用它,当你使用了正确的语法,Python 会替你调用 __setitem__ 。 |
| [2] | 这个看上去像正常的字典语法,当然除了 f 实际上是一个类,它尽可能地打扮成一个字典,并且 __setitem__ 是打扮的一个重点。这行代码实际上暗地里调用了 f.__setitem__("genre", 32)。 |
__setitem__ 是一个专用类方法,因为它可以让 Python 来替你调用,但是它仍然是一个类方法。就像在 UserDict 中定义 __setitem__ 方法一样容易,我们可以在子类中重新定义它,对父类的方法进行覆盖。这就允许我们定义出在某些方面像字典一样动作的类,但是可以定义它自已的行为,超过和超出内置的字典。
这个概念是本章中我们正在学习的整个框架的基础。每个文件类型可以拥有一个处理器类,这些类知道如何从一个特殊的文类型得到元数据。只要知道了某些属性 (像文件名和位置),处理器类就知道如何自动地得到其它的属性。它的实现是通过覆盖 __setitem__ 方法,检查特别的关键字,然后当找到后加入额外的处理。
例如,MP3FileInfo 是 FileInfo 的子类。在设置了一个 MP3FileInfo 类的 name 时,并不只是设置 name 关键字 (像父类 FileInfo 所做的),它还要在文件自身内进行搜索 MP3 的标记然后填充一整套关键字。下面的例子将展示其工作方式。
例 5.14. 在 MP3FileInfo 中覆盖 __setitem__
def __setitem__(self, key, item):
if key == "name" and item:
self.__parse(item)
FileInfo.__setitem__(self, key, item)
| [1] | 注意我们的 __setitem__ 方法严格按照与父类方法相同的形式进行定义。这一点很重要,因为 Python 将替你执行方法,而它希望这个函数用确定个数的参数进行定义。(从技术上说,参数的名字没有关系,只是个数。) |
| [2] | 这里就是整个 MP3FileInfo 类的难点:如果给 name 关键字赋一个值,我们还想做些额外的事情。 |
| [3] | 我们对 name 所做的额外处理封装在了 __parse 方法中。这是定义在 MP3FileInfo 中的另一个类方法,则当我们调用它时,我们用 self 对其限定。仅是调用 __parse 将只会看成定义在类外的普通方法,调用 self.__parse 将会看成定义在类中的一个类方法。这不是什么新东西,你用同样的方法来引用数据属性。 |
| [4] | 在做完我们额外的处理之后,我们需要调用父类的方法。记住,在 Python 中不会自动为你完成,需手工执行。注意,我们在调用直接父类,FileInfo,尽管它没有 __setitem__ 方法。没问题,因为 Python 将会沿着父类树走,直到它找到一个拥有我们正在调用方法的类,所以这行代码最终会找到并且调用定义在 UserDict 中的 __setitem__。 |
注意 当在一个类中存取数据属性时,你需要限定属性名:
self._attribute_。当调用类中的其它方法时,你属要限定方法名:self._method_。
例 5.15. 设置 MP3FileInfo 的 name
>>> import fileinfo
>>> mp3file = fileinfo.MP3FileInfo()
>>> mp3file
{'name':None}
>>> mp3file["name"] = "/music/_singles/kairo.mp3"
>>> mp3file
{'album': 'Rave Mix', 'artist': '***DJ MARY-JANE***', 'genre': 31,
'title': 'KAIRO****THE BEST GOA', 'name': '/music/_singles/kairo.mp3',
'year': '2000', 'comment': 'http://mp3.com/DJMARYJANE'}
>>> mp3file["name"] = "/music/_singles/sidewinder.mp3"
>>> mp3file
{'album': '', 'artist': 'The Cynic Project', 'genre': 18, 'title': 'Sidewinder',
'name': '/music/_singles/sidewinder.mp3', 'year': '2000',
'comment': 'http://mp3.com/cynicproject'}
| [1] | 首先,我们创建了一个 MP3FileInfo 的实例,没有传递给它文件名。(我们可以不用它,因为 __init__ 方法的 filename 参数是可选的。) 因为 MP3FileInfo 没有它自已的 __init__ 方法,Python 沿着父类树走,发现了 FileInfo 的 __init__ 方法。这个 __init__ 方法手工调用了 UserDict 的 __init__ 方法,然后设置 name 关键字为 filename,它为 None,因为我们还没有传入一个文件名。所以,mp3file 最初看上去像是有一个关键字的字典,name 的值为 None。 |
| [2] | 现在真正有趣的开始了。设置 mp3file 的 name 关键字触发了 MP3FileInfo 上的 __setitem__ 方法 (而不是 UserDict 的),这个方法注意到我们正在用一个真实的值来设置 name 关键字,接着调用 self.__parse。尽管我们完全还没有研究过 __parse 方法,从它的输出你可以看出,它设置了其它几个关键字:album、artist、genre、title、year 和 comment。 |
| [3] | 修改 name 关键字将再次经受同样的处理过程:Python 调用 __setitem__,__setitem__调用 self.__parse,self.__parse 设置其它所有的关键字。 |
5.7. 高级专用类方法
5.7. 高级专用类方法
除了 __getitem__ 和 __setitem__ 之外 Python 还有更多的专用函数。某些可以让你模拟出你甚至可能不知道的功能。
下面的例子将展示 UserDict 一些其他专用方法。
例 5.16. UserDict 中更多的专用方法
def __repr__(self): return repr(self.data)
def __cmp__(self, dict):
if isinstance(dict, UserDict):
return cmp(self.data, dict.data)
else:
return cmp(self.data, dict)
def __len__(self): return len(self.data)
def __delitem__(self, key): del self.data[key]
| [1] | __repr__ 是一个专用的方法,在当调用 repr(_instance_) 时被调用。repr 函数是一个内置函数,它返回一个对象的字符串表示。它可以用在任何对象上,不仅仅是类的实例。你已经对 repr 相当熟悉了,尽管你不知道它。在交互式窗口中,当你只敲入一个变量名,接着按ENTER,Python 使用 repr 来显示变量的值。自已用一些数据来创建一个字典 d ,然后用 print repr(d) 来看一看吧。 |
| [2] | __cmp__ 在比较类实例时被调用。通常,你可以通过使用 == 比较任意两个 Python 对象,不只是类实例。有一些规则,定义了何时内置数据类型被认为是相等的,例如,字典在有着全部相同的关键字和值时是相等的。对于类实例,你可以定义 __cmp__ 方法,自已编写比较逻辑,然后你可以使用 == 来比较你的类,Python 将会替你调用你的 __cmp__ 专用方法。 |
| [3] | __len__ 在调用 len(_instance_) 时被调用。len 是一个内置函数,可以返回一个对象的长度。它可以用于任何被认为理应有长度的对象。字符串的 len 是它的字符个数;字典的 len 是它的关键字的个数;列表或序列的 len 是元素的个数。对于类实例,定义 __len__ 方法,接着自已编写长度的计算,然后调用 len(_instance_),Python 将替你调用你的 __len__ 专用方法。 |
| [4] | __delitem__ 在调用 del _instance_[_key_] 时调用 ,你可能记得它作为从字典中删除单个元素的方法。当你在类实例中使用 del 时,Python 替你调用 __delitem__ 专用方法。 |
注意 在 Java 中,通过使用
str1 == str2可以确定两个字符串变量是否指向同一块物理内存位置。这叫做对象同一性,在 Python 中写为str1 is str2。在 Java 中要比较两个字符串值,你要使用str1.equals(str2);在 Python 中,你要使用str1 == str2。某些 Java 程序员,他们已经被教授得认为,正是因为在 Java 中==是通过同一性而不是值进行比较,所以世界才会更美好。这些人要接受 Python 的这个“严重缺失”可能要花些时间。
在这个地方,你可能会想,“所有这些工作只是为了在类中做一些我可以对一个内置数据类型所做的操作”。不错,如果你能够从像字典一样的内置数据类型进行继承的话,事情就容易多了 (那样整个 UserDict 类将完全不需要了)。尽管你可以这样做,专用方法仍然是有用的,因为它们可以用于任何的类,而不只是像 UserDict 这样的封装类。
专用方法意味着任何类 可以像字典一样保存键-值对,只要定义 __setitem__ 方法。任何类可以表现得像一个序列,只要定义 __getitem__ 方法。任何定义了 __cmp__ 方法的类可以用 == 进行比较。并且如果你的类表现为拥有类似长度的东西,不要定义 GetLength 方法,而定义 __len__ 方法,并使用 len(_instance_)。
注意 其它的面向对象语言仅让你定义一个对象的物理模型 (“这个对象有
GetLength方法”),而 Python 的专用类方法像__len__允许你定义一个对象的逻辑模型 (“这个对象有一个长度”)。
Python 存在许多其它的专用方法。有一整套的专用方法,可以让类表现得象数值一样,允许你在类实例上进行加、减,以及执行其它算数操作。(关于这一点典型的例子就是表示复数的类,数值带有实数和虚数部分。) __call__ 方法让一个类表现得像一个函数,允许你直接调用一个类实例。并且存在其它的专用函数,允许类拥有只读或只写数据属性,在后面的章节中我们会更多地谈到这些。
进一步阅读
- Python Reference Manual 提供了所有专用类方法的文档。
5.8. 类属性介绍
5.8. 类属性介绍
你已经知道了数据属性,它们是被一个特定的类实例所拥有的变量。Python 也支持类属性,它们是由类本身所拥有的。
例 5.17. 类属性介绍
class MP3FileInfo(FileInfo):
"store ID3v1.0 MP3 tags"
tagDataMap = {"title" : ( 3, 33, stripnulls),
"artist" : ( 33, 63, stripnulls),
"album" : ( 63, 93, stripnulls),
"year" : ( 93, 97, stripnulls),
"comment" : ( 97, 126, stripnulls),
"genre" : (127, 128, ord)}
>>> import fileinfo
>>> fileinfo.MP3FileInfo
<class fileinfo.MP3FileInfo at 01257FDC>
>>> fileinfo.MP3FileInfo.tagDataMap
{'title': (3, 33, <function stripnulls at 0260C8D4>),
'genre': (127, 128, <built-in function ord>),
'artist': (33, 63, <function stripnulls at 0260C8D4>),
'year': (93, 97, <function stripnulls at 0260C8D4>),
'comment': (97, 126, <function stripnulls at 0260C8D4>),
'album': (63, 93, <function stripnulls at 0260C8D4>)}
>>> m = fileinfo.MP3FileInfo()
>>> m.tagDataMap
{'title': (3, 33, <function stripnulls at 0260C8D4>),
'genre': (127, 128, <built-in function ord>),
'artist': (33, 63, <function stripnulls at 0260C8D4>),
'year': (93, 97, <function stripnulls at 0260C8D4>),
'comment': (97, 126, <function stripnulls at 0260C8D4>),
'album': (63, 93, <function stripnulls at 0260C8D4>)}
| [1] | MP3FileInfo 是类本身,不是任何类的特别实例。 |
| [2] | tagDataMap 是一个类属性:字面的意思,一个类的属性。它在创建任何类实例之前就有效了。 |
| [3] | 类属性既可以通过直接对类的引用,也可以通过对类的任意实例的引用来使用。 |
注意 在 Java 中,静态变量 (在 Python 中叫类属性) 和实例变量 (在 Python 中叫数据属性) 两者都是紧跟在类定义之后定义的 (一个有
static关键字,一个没有)。在 Python 中,只有类属性可以定义在这里,数据属性定义在__init__方法中。
类属性可以作为类级别的常量来使用 (这就是为什么我们在 MP3FileInfo 中使用它们),但是它们不是真正的常量。你也可以修改它们。
注意 在 Python 中没有常量。如果你试图努力的话什么都可以改变。这一点满足 Python 的核心原则之一:坏的行为应该被克服而不是被取缔。如果你真正想改变
None的值,也可以做到,但当无法调试的时候别来找我。
例 5.18. 修改类属性
>>> class counter:
... count = 0
... def __init__(self):
... self.__class__.count += 1
...
>>> counter
<class __main__.counter at 010EAECC>
>>> counter.count
0
>>> c = counter()
>>> c.count
1
>>> counter.count
1
>>> d = counter()
>>> d.count
2
>>> c.count
2
>>> counter.count
2
| [1] | count 是 counter 类的一个类属性。 |
| [2] | __class__ 是每个类实例的一个内置属性 (也是每个类的)。它是一个类的引用,而 self 是一个类 (在本例中,是 counter 类) 的实例。 |
| [3] | 因为 count 是一个类属性,它可以在我们创建任何类实例之前,通过直接对类引用而得到。 |
| [4] | 创建一个类实例会调用 __init__ 方法,它会给类属性 count 加 1。这样会影响到类自身,不只是新创建的实例。 |
| [5] | 创建第二个实例将再次增加类属性 count。注意类属性是如何被类和所有类实例所共享的。 |
5.9. 私有函数
5.9. 私有函数
与大多数语言一样,Python 也有私有的概念:
- 私有函数不可以从它们的模块外面被调用
- 私有类方法不能够从它们的类外面被调用
- 私有属性不能够从它们的类外面被访问
与大多数的语言不同,一个 Python 函数,方法,或属性是私有还是公有,完全取决于它的名字。
如果一个 Python 函数,类方法,或属性的名字以两个下划线开始 (但不是结束),它是私有的;其它所有的都是公有的。 Python 没有类方法保护 的概念 (只能用于它们自已的类和子类中)。类方法或者是私有 (只能在它们自已的类中使用) 或者是公有 (任何地方都可使用)。
在 MP3FileInfo 中,有两个方法:__parse 和 __setitem__。正如我们已经讨论过的,__setitem__ 是一个专有方法;通常,你不直接调用它,而是通过在一个类上使用字典语法来调用,但它是公有的,并且如果有一个真正好的理由,你可以直接调用它 (甚至从 fileinfo 模块的外面)。然而,__parse 是私有的,因为在它的名字前面有两个下划线。
注意 在 Python 中,所有的专用方法 (像
__setitem__) 和内置属性 (像__doc__) 遵守一个标准的命名习惯:开始和结束都有两个下划线。不要对你自已的方法和属性用这种方法命名;到最后,它只会把你 (或其它人) 搞乱。
例 5.19. 尝试调用一个私有方法
>>> import fileinfo
>>> m = fileinfo.MP3FileInfo()
>>> m.__parse("/music/_singles/kairo.mp3")
Traceback (innermost last):
File "<interactive input>", line 1, in ?
AttributeError: 'MP3FileInfo' instance has no attribute '__parse'
| [1] | 如果你试图调用一个私有方法,Python 将引发一个有些误导的异常,宣称那个方法不存在。当然它确实存在,但是它是私有的,所以在类外是不可使用的。严格地说,私有方法在它们的类外是可以访问的,只是不容易 处理。在 Python 中没有什么是真正私有的;在内部,私有方法和属性的名字被忽然改变和恢复,以致于使得它们看上去用它们给定的名字是无法使用的。你可以通过 _MP3FileInfo__parse 名字来使用 MP3FileInfo 类的 __parse 方法。知道了这个方法很有趣,然后要保证决不在真正的代码中使用它。私有方法由于某种原因而私有,但是像其它很多在 Python 中的东西一样,它们的私有化基本上是习惯问题,而不是强迫的。 |
进一步阅读
- Python Tutorial 讨论了私有变量的内部工作方式。
5.10. 小结
5.10. 小结
实打实的对象把戏到此为止。你将在 第十二章 中看到一个真实世界应用程序的专有类方法,它使用 getattr 创建一个到远程 Web 服务的代理。
下一章将继续使用本章的例程探索其他 Python 的概念,例如:异常、文件对象 和 for 循环。
在研究下一章之前,确保你可以无困难地完成下面的事情:
- 使用
import _module_或from _module_ import导入模块 - 定义和实例化类
- 定义
__init__方法和其他专用类方法,并理解它们何时会调用 - 子类化
UserDict来定义行为像字典的类 - 定义数据属性和类属性,并理解它们之间的不同
- 定义私有属性和方法
第六章 异常和文件处理
第六章 异常和文件处理
- 6.1. 异常处理
- 6.1.1. 为其他用途使用异常
- 6.2. 与文件对象共事
- 6.2.1. 读取文件
- 6.2.2. 关闭文件
- 6.2.3. 处理 I/O 错误
- 6.2.4. 写入文件
- 6.3. for 循环
- 6.4. 使用 sys.modules
- 6.5. 与目录共事
- 6.6. 全部放在一起
- 6.7. 小结
在本章中,将研究异常、文件对象、for 循环、os 和 sys 模块等内容。如果你已经在其它编程语言中使用过异常,你可以简单看看第一部分来了解 Python 的语法。但是本章其它的内容仍需仔细研读。
6.1. 异常处理
6.1. 异常处理
- 6.1.1. 为其他用途使用异常
与许多面向对象语言一样,Python 具有异常处理,通过使用 try...except 块来实现。
注意 Python 使用
try...except来处理异常,使用raise来引发异常。Java 和 C++ 使用try...catch来处理异常,使用throw来引发异常。
异常在 Python 中无处不在;实际上在标准 Python 库中的每个模块都使用了它们,并且 Python 自已会在许多不同的情况下引发它们。在整本书中你已经再三看到它们了。
- 使用不存在的字典关键字将引发
KeyError异常。 - 搜索列表中不存在的值将引发
ValueError异常。 - 调用不存在的方法将引发
AttributeError异常。 - 引用不存在的变量将引发
NameError异常。 - 未强制转换就混用数据类型将引发
TypeError异常。
在这些情况下,我们都在简单地使用 Python IDE:一个错误发生了,异常被打印出来 (取决于你的 IDE,可能会有意地以一种刺眼的红色形式表示),这便是。这叫做未处理 异常;当异常被引发时,没有代码来明确地关注和处理它,所以异常被传给置在 Python 中的缺省的处理,它会输出一些调试信息并且终止运行。在 IDE 中,这不是什么大事,但是如果发生在你真正的 Python 程序运行的时候,整个程序将会终止。
然而,一个异常不一定会引起程序的完全崩溃。当异常引发时,可以被处理 掉。有时候一个异常实际是因为代码中的 bug (比如使用一个不存在的变量),但是许多时候,一个异常是可以预见的。如果你打开一个文件,它可能不存在。如果你连接一个数据库,它可能不可连接或没有访问所需的正确的安全证书。如果知道一行代码可能会引发异常,你应该使用一个 try...except 块来处理异常。
例 6.1. 打开一个不存在的文件
>>> fsock = open("/notthere", "r")
Traceback (innermost last):
File "<interactive input>", line 1, in ?
IOError: [Errno 2] No such file or directory: '/notthere'
>>> try:
... fsock = open("/notthere")
... except IOError:
... print "The file does not exist, exiting gracefully"
... print "This line will always print"
The file does not exist, exiting gracefully
This line will always print
| [1] | 使用内置 open 函数,我们可以试着打开一个文件来读取 (在下一节有关于 open 的更多内容)。但是那个文件不存在,所以这样就引发 IOError 异常。因为我们没有提供任何显式的对 IOError 异常的检查,Python 仅仅打印出某个关于发生了什么的调试信息,然后终止。 |
| [2] | 我们试图打开同样不存在的文件,但是这次我们在一个 try...except 内来执行它。 |
| [3] | 当 open 方法引发 IOError 异常时,我们已经准备好处理它了。except IOError: 行捕捉异常,接着执行我们自已的代码块,这个代码块在本例中只是打印出更令人愉快的错误信息。 |
| [4] | 一旦异常被处理了,处理通常在 try...except 块之后的第一行继续进行。注意这一行将总是打印出来,无论异常是否发生。如果在你的根目录下确实有一个叫 notthere 的文件,对 open 的调用将成功,except 子句将忽略,并且最后一行仍将执行。 |
异常可能看上去不友好 (毕竟,如果你不捕捉异常,整个程序将崩溃),但是考虑一下别的方法。你该不会希望获得一个指向不存在的文件的对象吧?不管怎么样你都得检查它的有效性,而且如果你忘记了,你的程序将会在下面某个地方给出奇怪的错误,这样你将不得不追溯到源程序。我确信你做过这种事;这可并不有趣。使用异常,一发生错误,你就可以在问题的源头通过标准的方法来处理它们。
6.1.1. 为其他用途使用异常
除了处理实际的错误条件之外,对于异常还有许多其它的用处。在标准 Python 库中一个普通的用法就是试着导入一个模块,然后检查是否它能使用。导入一个并不存在的模块将引发一个 ImportError 异常。你可以使用这种方法来定义多级别的功能――依靠在运行时哪个模块是有效的,或支持多种平台 (即平台特定代码被分离到不同的模块中)。
你也能通过创建一个从内置的 Exception 类继承的类定义你自己的异常,然后使用 raise 命令引发你的异常。如果你对此感兴趣,请看进一步阅读的部分。
下面的例子演示了如何使用异常支持特定平台功能。代码来自 getpass 模块,一个从用户获得口令的封装模块。获得口令在 UNIX、Windows 和 Mac OS 平台上的实现是不同的,但是这个代码封装了所有的不同之处。
例 6.2. 支持特定平台功能
# Bind the name getpass to the appropriate function
try:
import termios, TERMIOS
except ImportError:
try:
import msvcrt
except ImportError:
try:
from EasyDialogs import AskPassword
except ImportError:
getpass = default_getpass
else:
getpass = AskPassword
else:
getpass = win_getpass
else:
getpass = unix_getpass
| [1] | termios 是 UNIX 独有的一个模块,它提供了对于输入终端的底层控制。如果这个模块无效 (因为它不在你的系统上,或你的系统不支持它),则导入失败,Python 引发我们捕捉的 ImportError 异常。 |
| [2] | OK,我们没有 termios,所以让我们试试 msvcrt,它是 Windows 独有的一个模块,可以提供在 Microsoft Visual C++ 运行服务中的许多有用的函数的一个 API。如果导入失败,Python 会引发我们捕捉的 ImportError 异常。 |
| [3] | 如果前两个不能工作,我们试着从 EasyDialogs 导入一个函数,它是 Mac OS 独有的一个模块,提供了各种各样类型的弹出对话框。再一次,如果导入失败,Python 会引发一个我们捕捉的 ImportError 异常。 |
| [4] | 这些平台特定的模块没有一个有效 (有可能,因为 Python 已经移植到了许多不同的平台上了),所以我们需要回头使用一个缺省口令输入函数 (这个函数定义在 getpass 模块中的别的地方)。注意我们在这里所做的:我们将函数 default_getpass 赋给变量 getpass。如果你读了官方 getpass 文档,它会告诉你 getpass 模块定义了一个 getpass 函数。它是这样做的:通过绑定 getpass 到正确的函数来适应你的平台。然后当你调用 getpass 函数时,你实际上调用了平台特定的函数,是这段代码已经为你设置好的。你不需要知道或关心你的代码正运行在何种平台上;只要调用 getpass,则它总能正确处理。 |
| [5] | 一个 try...except 块可以有一条 else 子句,就像 if 语句。如果在 try 块中没有异常引发,然后 else 子句被执行。在本例中,那就意味着如果 from EasyDialogs import AskPassword 导入可工作,所以我们应该绑定 getpass 到 AskPassword 函数。其它每个 try...except 块有着相似的 else 子句,当我们发现一个 import 可用时,就绑定 getpass 到适合的函数。 |
进一步阅读
- Python Tutorial 讨论了异常,包括定义和引发你自已的异常,以及一次处理多个异常。
- Python Library Reference 总结了所有内置异常。
- Python Library Reference 提供了 getpass 模块的文档。
- Python Library Reference 提供了
traceback模块 的文档,这个模块在异常引发之后,提供了底层的对异常属性的处理。 - Python Reference Manual 讨论了
try...except块 的内部工作方式。
6.2. 与文件对象共事
6.2. 与文件对象共事
- 6.2.1. 读取文件
- 6.2.2. 关闭文件
- 6.2.3. 处理 I/O 错误
- 6.2.4. 写入文件
Python 有一个内置函数,open,用来打开在磁盘上的文件。open 返回一个文件对象,它拥有一些方法和属性,可以得到被打开文件的信息,以及对被打开文件进行操作。
例 6.3. 打开文件
>>> f = open("/music/_singles/kairo.mp3", "rb")
>>> f
<open file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.mode
'rb'
>>> f.name
'/music/_singles/kairo.mp3'
| [1] | open 方法可以接收三个参数:文件名、模式和缓冲区参数。只有第一个参数 (文件名) 是必须的;其它两个是可选的。如果没有指定,文件以文本方式打开。这里我们以二进制方式打开文件进行读取。(print open.__doc__ 会给出所有可能模式的很好的解释。) |
| [2] | open 函数返回一个对象 (到现在为止,这一点应该不会使你感到吃惊)。一个文件对象有几个有用的属性。 |
| [3] | 文件对象的 mode 属性告诉你文件以何种模式被打开。 |
| [4] | 文件对象的 name 属性告诉你文件对象所打开的文件名。 |
6.2.1. 读取文件
你打开文件之后,你要做的第一件事是从中读取,正如下一个例子所展示的。
例 6.4. 读取文件
>>> f
<open file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.tell()
0
>>> f.seek(-128, 2)
>>> f.tell()
7542909
>>> tagData = f.read(128)
>>> tagData
'TAGKAIRO****THE BEST GOA ***DJ MARY-JANE***
Rave Mix 2000http://mp3.com/DJMARYJANE \037'
>>> f.tell()
7543037
| [1] | 一个文件对象维护它所打开文件的状态。文件对象的 tell 方法告诉你在被打开文件中的当前位置。因为我们还没有对这个文件做任何事,当前位置为 0,它是文件的起始处。 |
| [2] | 文件对象的 seek 方法在被打开文件中移动到另一个位置。第二个参数指出第一个参数是什么意思:0 表示移动到一个绝对位置 (从文件起始处算起),1 表示移到一个相对位置 (从当前位置算起),还有 2 表示相对于文件尾的位置。因为我们搜索的 MP3 标记保存在文件的末尾,我们使用 2 并且告诉文件对象从文件尾移动到 128 字节的位置。 |
| [3] | tell 方法确认了当前位置已经移动了。 |
| [4] | read 方法从被打开文件中读取指定个数的字节,并且返回含有读取数据的字符串。可选参数指定了读取的最大字节数。如果没有指定参数,read 将读到文件末尾。(我们本可以在这里简单地说 read() ,因为我们确切地知道在文件的何处,事实上,我们读的是最后 128 个字节。) 读出的数据赋给变量 tagData,并且当前的位置根据所读的字节数作了修改。 |
| [5] | tell 方法确认了当前位置已经移动了。如果做一下算术,你会看到在读了 128 个字节之后,位置数已经增加了 128。 |
6.2.2. 关闭文件
打开文件消耗系统资源,并且其间其它程序可能无法访问它们 (取决于文件模式)。这就是一旦操作完毕就该关闭文件的重要所在。
例 6.5. 关闭文件
>>> f
<open file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.closed
False
>>> f.close()
>>> f
<closed file '/music/_singles/kairo.mp3', mode 'rb' at 010E3988>
>>> f.closed
True
>>> f.seek(0)
Traceback (innermost last):
File "<interactive input>", line 1, in ?
ValueError: I/O operation on closed file
>>> f.tell()
Traceback (innermost last):
File "<interactive input>", line 1, in ?
ValueError: I/O operation on closed file
>>> f.read()
Traceback (innermost last):
File "<interactive input>", line 1, in ?
ValueError: I/O operation on closed file
>>> f.close()
| [1] | 文件对象的 closed 属性表示对象是打开还是关闭了文件。在本例中,文件仍然打开着 (closed 是 False)。 |
| [2] | 为了关闭文件,调用文件对象的 close 方法。这样就释放掉你加在文件上的锁 (如果有的话),刷新被缓冲的系统还未写入的输出 (如果有的话),并且释放系统资源。 |
| [3] | closed 属性证实了文件被关闭了。 |
| [4] | 文件被关闭了,但这并不意味着文件对象不再存在。变量 f 将继续存在,直到它超出作用域或被手工删除。然而,一旦文件被关闭,操作它的方法就没有一个能使用;它们都会引发异常。 |
| [5] | 对一个文件已经关闭的文件对象调用 close 不会 引发异常,它静静地失败。 |
6.2.3. 处理 I/O 错误
现在你已经足能理解前一章的例子程序 fileinfo.py 的文件处理代码了。下面这个例子展示了如何安全地打开文件和读取文件,以及优美地处理错误。
例 6.6. MP3FileInfo 中的文件对象
try:
fsock = open(filename, "rb", 0)
try:
fsock.seek(-128, 2)
tagdata = fsock.read(128)
finally:
fsock.close()
.
.
.
except IOError:
pass
| [1] | 因为打开和读取文件有风险,并且可能引发异常,所有这些代码都用一个 try...except 块封装。(嘿,标准化的缩近不好吗?这就是你开始欣赏它的地方。) |
| [2] | open 函数可能引发 IOError 异常。(可能是文件不存在。) |
| [3] | seek 方法可能引发 IOError 异常。(可能是文件长度小于 128 字节。) |
| [4] | read 方法可能引发 IOError 异常。(可能磁盘有坏扇区,或它在一个网络驱动器上,而网络刚好断了。) |
| [5] | 这是新的:一个 try...finally 块。一旦文件通过 open 函数被成功地打开,我们应该绝对保证把它关闭,即使是在 seek 或 read 方法引发了一个异常时。try...finally 块可以用来:在 finally 块中的代码将总是 被执行,甚至某些东西在 try 块中引发一个异常也会执行。可以这样考虑,不管在路上发生什么,代码都会被 “即将灭亡” 地执行。 |
| [6] | 最后,处理我们的 IOError 异常。它可能是由调用 open、seek 或 read 引发的 IOError 异常。这里,我们其实不用关心,因为将要做的事就是静静地忽略它然后继续。(记住,pass 是一条不做任何事的 Python 语句。) 这样完全合法,“处理” 一个异常可以明确表示不做任何事。它仍然被认为处理过了,并且处理将正常继续,从 try...except 块的下一行代码开始。 |
6.2.4. 写入文件
正如你所期待的,你也能用与读取文件同样的方式写入文件。有两种基本的文件模式:
- 追加 (Append) 模式将数据追加到文件尾。
- 写入 (write) 模式将覆盖文件的原有内容。
如果文件还不存在,任意一种模式都将自动创建文件,因此从来不需要任何复杂的逻辑:“如果 log 文件还不存在,将创建一个新的空文件,正因为如此,你可以第一次就打开它”。打开文件并开始写就可以了。
例 6.7. 写入文件
>>> logfile = open('test.log', 'w')
>>> logfile.write('test succeeded')
>>> logfile.close()
>>> print file('test.log').read()
test succeeded
>>> logfile = open('test.log', 'a')
>>> logfile.write('line 2')
>>> logfile.close()
>>> print file('test.log').read()
test succeededline 2
| [1] | 你可以大胆地开始创建新文件 test.log 或覆盖现有文件,并为写入目的而打开它。(第二个参数 "w" 的意思是为文件写入而打开。) 是的,它和想象中的一样危险。我希望你不要关心文件以前的内容,因为它现在已经不存在了。 |
| [2] | 你可以使用 open 返回的文件对象的 write 方法向一个新打开的文件添加数据。 |
| [3] | file 是 open 的同义语。这一行语句打开文件,读取内容,并打印它们。 |
| [4] | 碰巧你知道 test.log 存在 (因为你刚向它写完了数据),所以你可以打开它并向其追加数据。("a" 参数的意思是为追加目的打开文件。) 实际上即使文件不存在你也可以这样做,因为以追加方式打开一文件时,如果需要的话会创建文件。但是追加操作从不 损坏文件的现有内容。 |
| [5] | 正如你所看到的,原来的行和你以追加方式写入的第二行现在都在 test.log 中了。同时注意两行之间并没包含回车符。因为两次写入文件时都没有明确地写入回车符,所以文件中没有包含回车符。你可以用 "\n" 写入回车符。因为你没做这项工作,所以你写到文件的所有内容都将显示在同一行上。 |
进一步阅读
- Python Tutorial 讨论了文件的读取和写入,包括如何将一个文件一次一行地读到 list 中。
- eff-bot 讨论了各种各样读取文件方法 的效率和性能。
- Python Knowledge Base 回答了关于文件的常见问题。
- Python Library Reference 总结了所有文件对象模块。
6.3. for 循环
6.3. for 循环
与其它大多数语言一样,Python 也拥有 for 循环。你到现在还未曾看到它们的唯一原因就是,Python 在其它太多的方面表现出色,通常你不需要它们。
其它大多数语言没有像 Python 一样的强大的 list 数据类型,所以你需要亲自做很多事情,指定开始,结束和步长,来定义一定范围的整数或字符或其它可重复的实体。但是在 Python 中,for 循环简单地在一个列表上循环,与 list 解析的工作方式相同。
例 6.8. for 循环介绍
>>> li = ['a', 'b', 'e']
>>> for s in li:
... print s
a
b
e
>>> print "\n".join(li)
a
b
e
| [1] | for 循环的语法同 list 解析相似。li 是一个 list,而 s 将从第一个元素开始依次接收每个元素的值。 |
| [2] | 像 if 语句或其它任意缩进块,for 循环可以包含任意数目的代码行。 |
| [3] | 这就是你以前没看到过 for 循环的原因:至今我们都不需要它。太令人吃惊了,当你想要的只是一个 join 或是 list 解析时,在其它语言中常常需要使用 for 循环。 |
要做一个 “通常的” (Visual Basic 标准的) 计数 for 循环也非常简单。
例 6.9. 简单计数
>>> for i in range(5):
... print i
0
1
2
3
4
>>> li = ['a', 'b', 'c', 'd', 'e']
>>> for i in range(len(li)):
... print li[i]
a
b
c
d
e
| [1] | 正如你在 例 3.20 “连续值赋值” 所看到的,range 生成一个整数的 list,通过它来控制循环。我知道它看上去有些奇怪,但是它对计数循环偶尔 (我只是说偶尔) 会有用 。 |
| [2] | 我们从来没这么用过。这是 Visual Basic 的思维风格。摆脱它吧。正确遍历 list 的方法是前面的例子所展示的。 |
for 循环不仅仅用于简单计数。它们可以遍历任何类型的东西。下面的例子是一个用 for 循环遍历 dictionary 的例子。
例 6.10. 遍历 dictionary
>>> import os
>>> for k, v in os.environ.items():
... print "%s=%s" % (k, v)
USERPROFILE=C:\Documents and Settings\mpilgrim
OS=Windows_NT
COMPUTERNAME=MPILGRIM
USERNAME=mpilgrim
[...略...]
>>> print "\n".join(["%s=%s" % (k, v)
... for k, v in os.environ.items()])
USERPROFILE=C:\Documents and Settings\mpilgrim
OS=Windows_NT
COMPUTERNAME=MPILGRIM
USERNAME=mpilgrim
[...略...]
| [1] | os.environ 是在你的系统上所定义的环境变量的 dictionary。在 Windows 下,这些变量是可以从 MS-DOS 访问的用户和系统变量。在 UNIX 下,它们是在你的 shell 启动脚本中所 export (输出) 的变量。在 Mac OS 中,没有环境变量的概念,所以这个 dictionary 为空。 |
| [2] | os.environ.items() 返回一个 tuple 的 list:[(_key1_, _value1_), (_key2_, _value2_), ...]。for 循环对这个 list 进行遍历。第一轮,它将 _key1_ 赋给 k ,_value1_ 赋给 v,所以 k = USERPROFILE,v = C:\Documents and Settings\mpilgrim。第二轮,k 得到第二个键字 OS,v 得到相应的值 Windows_NT。 |
| [3] | 使用多变量赋值和 list 解析,你可以使用单行语句来替换整个 for 循环。在实际的编码中是否这样做只是个人风格问题;我喜欢它是因为,将一个 dictionary 映射到一个 list,然后将 list 合并成一个字符串,这一过程显得很清晰。其它的程序员宁愿将其写成一个 for 循环。请注意在两种情况下输出是一样的,然而这一版本稍微快一些,因为它只有一条 print 语句而不是许多。 |
现在我们来看看在 第五章 介绍的样例程序 fileinfo.py 中 MP3FileInfo 的 for 循环 。
例 6.11. MP3FileInfo 中的 for 循环
tagDataMap = {"title" : ( 3, 33, stripnulls),
"artist" : ( 33, 63, stripnulls),
"album" : ( 63, 93, stripnulls),
"year" : ( 93, 97, stripnulls),
"comment" : ( 97, 126, stripnulls),
"genre" : (127, 128, ord)}
.
.
.
if tagdata[:3] == "TAG":
for tag, (start, end, parseFunc) in self.tagDataMap.items():
self[tag] = parseFunc(tagdata[start:end])
| [1] | tagDataMap 是一个类属性,它定义了我们正在一个 MP3 文件中搜索的标记。标记存储为定长字段,只要我们读出文件最后 128 个字节,那么第 3 到 32 字节总是歌曲的名字,33-62 总是歌手的名字,63-92 为专辑的名字,等等。请注意 tagDataMap 是一个 tuple 的 dictionary,每个 tuple 包含两个整数和一个函数引用。 |
| [2] | 这个看上去复杂一些,但其实并非如此。这里的 for 变量结构与 items 所返回的 list 的元素的结构相匹配。记住,items 返回一个形如 (_key_, _value_) 的 tuple 的 list。list 第一个元素是 ("title", (3, 33, <function stripnulls>)),所以循环的第一轮,tag 为 "title",start 为 3,end 为 33,parseFunc 为函数 stripnulls。 |
| [3] | 现在我们已经从一个单个的 MP3 标记中提取出了所有的参数,将标记数据保存起来挺容易。我们从 start 到 end 对 tagdata 进行分片"),从而得到这个标记的实际数据,调用 parseFunc 对数据进行后续的处理,接着将 parseFunc 的返回值作为值赋值给伪字典 self 中的键字 tag。在遍历完 tagDataMap 中所有元素之后,self 拥有了所有标记的值,你知道看上去是什么样。 |
6.4. 使用 ``sys.modules
6.4. 使用 ``sys.modules
与其它任何 Python 的东西一样,模块也是对象。只要导入了,总可以用全局 dictionary ``sys.modules 来得到一个模块的引用。
例 6.12. ``sys.modules 介绍
>>> import sys
>>> print '\n'.join(sys.modules.keys())
win32api
os.path
os
exceptions
__main__
ntpath
nt
sys
__builtin__
site
signal
UserDict
stat
| [1] | sys 模块包含了系统级的信息,像正在运行的 Python 的版本 (sys.version 或sys.version_info),和系统级选项,像最大允许递归的深度 (sys.getrecursionlimit() 和sys.setrecursionlimit())。 |
| [2] | sys.modules 是一个字典,它包含了从 Python 开始运行起,被导入的所有模块。键字就是模块名,键值就是模块对象。请注意除了你的程序导入的模块外还有其它模块。Python 在启动时预先装入了一些模块,如果你在一个 Python IDE 环境下,sys.modules 包含了你在 IDE 中运行的所有程序所导入的所有模块。 |
下面的例子展示了如何使用 ``sys.modules。
例 6.13. 使用 ``sys.modules
>>> import fileinfo
>>> print '\n'.join(sys.modules.keys())
win32api
os.path
os
fileinfo
exceptions
__main__
ntpath
nt
sys
__builtin__
site
signal
UserDict
stat
>>> fileinfo
<module 'fileinfo' from 'fileinfo.pyc'>
>>> sys.modules["fileinfo"]
<module 'fileinfo' from 'fileinfo.pyc'>
| [1] | 当导入新的模块,它们加入到 sys.modules 中。这就解释了为什么第二次导入相同的模块时非常的快:Python 已经在sys.modules 中装入和缓冲了,所以第二次导入仅仅对字典做了一个查询。 |
| [2] | 一旦给出任何以前导入过的模块名 (以字符串方式),通过 ``sys.modules 字典,你可以得到对模块本身的一个引用。 |
下面的例子将展示通过结合使用 __module__ 类属性和 ``sys.modules dictionary 来获取已知类所在的模块。
例 6.14. __module__ 类属性
>>> from fileinfo import MP3FileInfo
>>> MP3FileInfo.__module__
'fileinfo'
>>> sys.modules[MP3FileInfo.__module__]
<module 'fileinfo' from 'fileinfo.pyc'>
| [1] | 每个 Python 类都拥有一个内置的类属性 __module__,它定义了这个类的模块的名字。 |
| [2] | 将它与 ``sys.modules 字典复合使用,你可以得到定义了某个类的模块的引用。 |
现在准备好了,看看在样例程序 第五章 ``sys.modules介绍的fileinfo.py 中是如何使用的。这个例子显示它的一部分代码。
例 6.15. fileinfo.py 中的 ``sys.modules
def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]):
"get file info class from filename extension"
subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:]
return hasattr(module, subclass) and getattr(module, subclass) or FileInfo
| [1] | 这是一个有两个参数的函数;filename 是必须的,但 module 是可选的并且 module 的缺省值包含了 FileInfo 类。这样看上去效率低,因为你可能认为 Python 会在每次函数调用时计算这个 ``sys.modules表达式。实际上,Python 仅会对缺省表达式计算一次,是在模块导入的第一次。正如后面我们会看到的,我们永远不会用一个module参数来调用这个函数,所以module 的功能是作为一个函数级别的常量。 |
| [2] | 我们会在后面再仔细研究这一行,在我们了解了 os 模块之后。那么现在,只要相信 subclass 最终为一个类的名字就行了,像 MP3FileInfo。 |
| [3] | 你已经了解了 getattr,它可以通过名字得到一个对象的引用。hasattr 是一个补充性的函数,用来检查一个对象是否具有一个特定的属性;在本例中,用来检查一个模块是否有一个特别的类 (然而它可以用于任何类和任何属性,就像 getattr)。用英语来说,这行代码是说,“If this module has the class named by subclass then return it, otherwise return the base class FileInfo (如果这个模块有一个名为 subclass 的类,那么返回它,否则返回基类 FileInfo)”。 |
进一步阅读
- Python Tutorial 讨论了缺省参数到底在什么时候和是如何计算的。
- Python Library Reference 提供了
sys模块的文档。
6.5. 与目录共事
6.5. 与目录共事
os.path 模块有几个操作文件和目录的函数。这里,我们看看如何操作路径名和列出一个目录的内容。
例 6.16. 构造路径名
>>> import os
>>> os.path.join("c:\\music\\ap\\", "mahadeva.mp3")
'c:\\music\\ap\\mahadeva.mp3'
>>> os.path.join("c:\\music\\ap", "mahadeva.mp3")
'c:\\music\\ap\\mahadeva.mp3'
>>> os.path.expanduser("~")
'c:\\Documents and Settings\\mpilgrim\\My Documents'
>>> os.path.join(os.path.expanduser("~"), "Python")
'c:\\Documents and Settings\\mpilgrim\\My Documents\\Python'
| [1] | os.path 是一个模块的引用;使用哪一个模块要看你正运行在哪种平台上。就像 getpass 通过将 getpass 设置为一个与平台相关的函数从而封装了平台之间的不同。os 通过设置 path 封装不同的相关平台模块。 |
| [2] | os.path 的 join 函数把一个或多个部分路径名连接成一个路径名。在这个简单的例子中,它只是将字符串进行连接。(请注意在 Windows 下处理路径名是一个麻烦的事,因为反斜线字符必须被转义。) |
| [3] | 在这个几乎没有价值的例子中,在将路径名加到文件名上之前,join 将在路径名后添加额外的反斜线。当发现这一点时我高兴极了,因为当用一种新的语言创建我自已的工具包时,addSlashIfNecessary 总是我必须要写的那些愚蠢的小函数之一。在 Python 中不要 写这样的愚蠢的小函数,聪明的人已经为你考虑到了。 |
| [4] | expanduser 将对使用 ~ 来表示当前用户根目录的路径名进行扩展。在任何平台上,只要用户拥有一个根目录,它就会有效,像 Windows、UNIX 和 Mac OS X,但在 Mac OS 上无效。 |
| [5] | 将这些技术组合在一起,你可以容易地为在用户根目录下的目录和文件构造出路径名。 |
例 6.17. 分割路径名
>>> os.path.split("c:\\music\\ap\\mahadeva.mp3")
('c:\\music\\ap', 'mahadeva.mp3')
>>> (filepath, filename) = os.path.split("c:\\music\\ap\\mahadeva.mp3")
>>> filepath
'c:\\music\\ap'
>>> filename
'mahadeva.mp3'
>>> (shortname, extension) = os.path.splitext(filename)
>>> shortname
'mahadeva'
>>> extension
'.mp3'
| [1] | split 函数对一个全路径名进行分割,返回一个包含路径和文件名的 tuple。还记得我说过你可以使用多变量赋值从一个函数返回多个值吗?对,split 就是这样一个函数。 |
| [2] | 我们将 split 函数的返回值赋值给一个两个变量的 tuple。每个变量接收到返回 tuple 相对应的元素值。 |
| [3] | 第一个变量,filepath,接收到从 split 返回 tuple 的第一个元素的值,文件路径。 |
| [4] | 第二个变量,filename,接收到从 split 返回 tuple 的第二个元素的值,文件名。 |
| [5] | os.path 也包含了一个 splitext 函数,可以用来对文件名进行分割,并且返回一个包含了文件名和文件扩展名的 tuple。我们使用相同的技术来将它们赋值给独立的变量。 |
例 6.18. 列出目录
>>> os.listdir("c:\\music\\_singles\\")
['a_time_long_forgotten_con.mp3', 'hellraiser.mp3',
'kairo.mp3', 'long_way_home1.mp3', 'sidewinder.mp3',
'spinning.mp3']
>>> dirname = "c:\\"
>>> os.listdir(dirname)
['AUTOEXEC.BAT', 'boot.ini', 'CONFIG.SYS', 'cygwin',
'docbook', 'Documents and Settings', 'Incoming', 'Inetpub', 'IO.SYS',
'MSDOS.SYS', 'Music', 'NTDETECT.COM', 'ntldr', 'pagefile.sys',
'Program Files', 'Python20', 'RECYCLER',
'System Volume Information', 'TEMP', 'WINNT']
>>> [f for f in os.listdir(dirname)
... if os.path.isfile(os.path.join(dirname, f))]
['AUTOEXEC.BAT', 'boot.ini', 'CONFIG.SYS', 'IO.SYS', 'MSDOS.SYS',
'NTDETECT.COM', 'ntldr', 'pagefile.sys']
>>> [f for f in os.listdir(dirname)
... if os.path.isdir(os.path.join(dirname, f))]
['cygwin', 'docbook', 'Documents and Settings', 'Incoming',
'Inetpub', 'Music', 'Program Files', 'Python20', 'RECYCLER',
'System Volume Information', 'TEMP', 'WINNT']
| [1] | listdir 函数接收一个路径名,并返回那个目录的内容的 list。 |
| [2] | listdir 同时返回文件和文件夹,并不指出哪个是文件,哪个是文件夹。 |
| [3] | 你可以使用过滤列表和 os.path 模块的 isfile 函数,从文件夹中将文件分离出来。isfile 接收一个路径名,如果路径表示一个文件,则返回 1,否则为 0。在这里,我们使用 os.path.join 来确保得到一个全路径名,但 isfile 对部分路径 (相对于当前目录) 也是有效的。你可以使用 os.getcwd() 来得到当前目录。 |
| [4] | os.path 还有一个 isdir 函数,当路径表示一个目录,则返回 1,否则为 0。你可以使用它来得到一个目录下的子目录列表。 |
例 6.19. 在 fileinfo.py 中列出目录
def listDirectory(directory, fileExtList):
"get list of file info objects for files of particular extensions"
fileList = [os.path.normcase(f)
for f in os.listdir(directory)]
fileList = [os.path.join(directory, f)
for f in fileList
if os.path.splitext(f)[1] in fileExtList]
| [1] | os.listdir(directory) 返回在 directory 中所有文件和文件夹的一个 list。 |
| [2] | 使用 f 对 list 进行遍历,我们使用 os.path.normcase(f) 根据操作系统的缺省值对大小写进行标准化处理。normcase 是一个有用的函数,用于对大小写不敏感操作系统的一个补充。这种操作系统认为 mahadeva.mp3 和 mahadeva.MP3 是同一个文件名。例如,在 Windows 和 Mac OS 下,normcase 将把整个文件名转换为小写字母;而在 UNIX 兼容的系统下,它将返回未作修改的文件名。 |
| [3] | 再次用 f 对标准化后的 list 进行遍历,我们使用 os.path.splitext(f) 将每个文件名分割为名字和扩展名。 |
| [4] | 对每个文件,我们查看扩展名是否在我们关心的文件扩展名 list 中 (fileExtList,被传递给 listDirectory 函数)。 |
| [5] | 对每个我们所关心的文件,我们使用 os.path.join(directory, f) 来构造这个文件的全路径名,接着返回这个全路径名的 list。 |
注意 只要有可能,你就应该使用在
os和os.path中的函数进行文件、目录和路径的操作。这些模块是对平台相关模块的封装模块,所以像os.path.split这样的函数可以工作在 UNIX、Windows、Mac OS 和 Python 所支持的任一种平台上。
还有一种获得目录内容的方法。它非常强大,并使用了一些你在命令行上工作时可能已经熟悉的通配符。
例 6.20. 使用 glob 列出目录
>>> os.listdir("c:\\music\\_singles\\")
['a_time_long_forgotten_con.mp3', 'hellraiser.mp3',
'kairo.mp3', 'long_way_home1.mp3', 'sidewinder.mp3',
'spinning.mp3']
>>> import glob
>>> glob.glob('c:\\music\\_singles\\*.mp3')
['c:\\music\\_singles\\a_time_long_forgotten_con.mp3',
'c:\\music\\_singles\\hellraiser.mp3',
'c:\\music\\_singles\\kairo.mp3',
'c:\\music\\_singles\\long_way_home1.mp3',
'c:\\music\\_singles\\sidewinder.mp3',
'c:\\music\\_singles\\spinning.mp3']
>>> glob.glob('c:\\music\\_singles\\s*.mp3')
['c:\\music\\_singles\\sidewinder.mp3',
'c:\\music\\_singles\\spinning.mp3']
>>> glob.glob('c:\\music\\*\\*.mp3')
| [1] | 正如你前面看到的,os.listdir 简单地取一个目录路径,返回目录中的所有文件和子目录。 |
| [2] | glob 模块,另一方面,接受一个通配符并且返回文件的或目录的完整路径与之匹配。这个通配符是一个目录路径加上“*.mp3”,它将匹配所有的 .mp3 文件。注意返回列表的每一个元素已经包含了文件的完整路径。 |
| [3] | 如果你要查找指定目录中所有以“s”开头并以“.mp3”结尾的文件,也可以这么做。 |
| [4] | 现在考查这种情况:你有一个 music 目录,它包含几个子目录,子目录中包含一些 .mp3 文件。使用两个通配符,仅仅调用 glob 一次就可以立刻获得所有这些文件的一个 list。一个通配符是 "*.mp3" (用于匹配 .mp3 文件),另一个通配符是子目录名本身,用于匹配 c:\music 中的所有子目录。这看上去很简单,但它蕴含了强大的功能。 |
进一步阅读
- Python Knowledge Base 回答了关于
os模块的问题。 - Python Library Reference 提供了
os模块和os.path模块的文档。
6.6. 全部放在一起
6.6. 全部放在一起
再一次,所有的多米诺骨牌都放好了。我们已经看过每行代码是如何工作的了。现在往回走一步,看一下放在一起是怎么样的。
例 6.21. listDirectory
def listDirectory(directory, fileExtList):
"get list of file info objects for files of particular extensions"
fileList = [os.path.normcase(f)
for f in os.listdir(directory)]
fileList = [os.path.join(directory, f)
for f in fileList
if os.path.splitext(f)[1] in fileExtList]
def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]):
"get file info class from filename extension"
subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:]
return hasattr(module, subclass) and getattr(module, subclass) or FileInfo
return [getFileInfoClass(f)(f) for f in fileList]
| [1] | listDirectory 是整个模块主要的有趣之处。它接收一个 dictionary (在我的例子中如 c:\music\_singles\) 和一个感兴趣的文件扩展名列表 (如 ['.mp3']),接着它返回一个类实例的 list ,这些类实例的行为像 dictionary,包含了在目录中每个感兴趣文件的元数据。并且实现起来只用了几行直观的代码。 |
| [2] | 正如在前一节我们所看到的,这行代码得到一个全路径名的列表,它的元素是在 directory 中有着我们感兴趣的文件后缀 (由 fileExtList 所指定的) 的所有文件的路径名。 |
| [3] | 老学校出身的 Pascal 程序员可能对嵌套函数感到熟悉,但大部分人,当我告诉他们 Python 支持嵌套函数时,都茫然地看着我。嵌套函数,从字面理解,是定义在函数内的函数。嵌套函数 getFileInfoClass 只能在定义它的函数 listDirectory 内进行调用。正如任何其它的函数一样,不需要一个接口声明或奇怪的什么东西,只要定义函数,开始编码就行了。 |
| [4] | 既然你已经看过 os 模块了,这一行应该能理解了。它得到文件的扩展名 (os.path.splitext(filename)[1]),将其转换为大写字母 (.upper()),从圆点处进行分片 ([1:]),使用字符串格式化从其中生成一个类名。所以 c:\music\ap\mahadeva.mp3 变成 .mp3 再变成 MP3 再变成 MP3FileInfo。 |
| [5] | 在生成完处理这个文件的处理类的名字之后,我们查阅在这个模块中是否存在这个处理类。如果存在,我们返回这个类,否则我们返回基类 FileInfo。这一点很重要:这个函数返回一个类。不是类的实例,而是类本身。 |
| [6] | 对每个属于我们 “感兴趣文件” 列表 (fileList)中的文件,我们用文件名 (f) 来调用 getFileInfoClass。调用 getFileInfoClass(f) 返回一个类;我们并不知道确切是哪一个类,但是我们并不关心。接着我们创建这个类 (不管它是什么) 的一个实例,传入文件名 (又是 f) 给 __init__ 方法。正如我们在本章的前面所看到的,FileInfo 的 __init__ 方法设置了 self["name"],它将引发 __setitem__ 的调用,而 __setitem__ 在子类 (MP3FileInfo) 中被覆盖掉了,用来适当地对文件进行分析,取出文件的元数据。我们对所有感兴趣的文件进行处理,返回结果实例的一个 list。 |
请注意 listDirectory 完全是通用的。它事先不知道将得到哪种类型的文件,也不知道哪些定义好的类能够处理这些文件。它检查目录中要进行处理的文件,然后反观本身模块,了解定义了什么特别的处理类 (像 MP3FileInfo)。你可以对这个程序进行扩充,对其它类型的文件进行处理,只要用适合的名字定义类:HTMLFileInfo 用于 HTML 文件,DOCFileInfo 用于 Word .doc 文件,等等。不需要改动函数本身, listDirectory 将会对它们都进行处理,将工作交给适当的类,接着收集结果。
6.7. 小结
6.7. 小结
在 第五章 介绍的 fileinfo.py 程序现在应该完全理解了。
"""Framework for getting filetype-specific metadata.
Instantiate appropriate class with filename. Returned object acts like a
dictionary, with key-value pairs for each piece of metadata.
import fileinfo
info = fileinfo.MP3FileInfo("/music/ap/mahadeva.mp3")
print "\\n".join(["%s=%s" % (k, v) for k, v in info.items()])
Or use listDirectory function to get info on all files in a directory.
for info in fileinfo.listDirectory("/music/ap/", [".mp3"]):
...
Framework can be extended by adding classes for particular file types, e.g.
HTMLFileInfo, MPGFileInfo, DOCFileInfo. Each class is completely responsible for
parsing its files appropriately; see MP3FileInfo for example.
"""
import os
import sys
from UserDict import UserDict
def stripnulls(data):
"strip whitespace and nulls"
return data.replace("\00", "").strip()
class FileInfo(UserDict):
"store file metadata"
def __init__(self, filename=None):
UserDict.__init__(self)
self["name"] = filename
class MP3FileInfo(FileInfo):
"store ID3v1.0 MP3 tags"
tagDataMap = {"title" : ( 3, 33, stripnulls),
"artist" : ( 33, 63, stripnulls),
"album" : ( 63, 93, stripnulls),
"year" : ( 93, 97, stripnulls),
"comment" : ( 97, 126, stripnulls),
"genre" : (127, 128, ord)}
def __parse(self, filename):
"parse ID3v1.0 tags from MP3 file"
self.clear()
try:
fsock = open(filename, "rb", 0)
try:
fsock.seek(-128, 2)
tagdata = fsock.read(128)
finally:
fsock.close()
if tagdata[:3] == "TAG":
for tag, (start, end, parseFunc) in self.tagDataMap.items():
self[tag] = parseFunc(tagdata[start:end])
except IOError:
pass
def __setitem__(self, key, item):
if key == "name" and item:
self.__parse(item)
FileInfo.__setitem__(self, key, item)
def listDirectory(directory, fileExtList):
"get list of file info objects for files of particular extensions"
fileList = [os.path.normcase(f)
for f in os.listdir(directory)]
fileList = [os.path.join(directory, f)
for f in fileList
if os.path.splitext(f)[1] in fileExtList]
def getFileInfoClass(filename, module=sys.modules[FileInfo.__module__]):
"get file info class from filename extension"
subclass = "%sFileInfo" % os.path.splitext(filename)[1].upper()[1:]
return hasattr(module, subclass) and getattr(module, subclass) or FileInfo
return [getFileInfoClass(f)(f) for f in fileList]
if __name__ == "__main__":
for info in listDirectory("/music/_singles/", [".mp3"]):
print "\n".join(["%s=%s" % (k, v) for k, v in info.items()])
print
在研究下一章之前,确保你可以无困难地完成下面的事情:
- 使用
try...except来捕捉异常 - 使用
try...finally来保护额外的资源 - 读取文件
- 在一个
for循环中一次赋多个值 - 使用
os模块来满足你的跨平台文件操作的需要 - 通过将类看成对象并传入参数,动态地实例化未知类型的类
第七章 正则表达式
第七章 正则表达式
- 7.1. 概览
- 7.2. 个案研究:街道地址
- 7.3. 个案研究:罗马字母
- 7.3.1. 校验千位数
- 7.3.2. 校验百位数
- 7.4. 使用 {n,m} 语法
- 7.4.1. 校验十位数和个位数
- 7.5. 松散正则表达式
- 7.6. 个案研究:解析电话号码
- 7.7. 小结
正则表达式是搜索、替换和解析复杂字符模式的一种强大而标准的方法。如果你曾经在其他语言 (如 Perl) 中使用过它,由于它们的语法非常相似,你仅仅阅读一下 re 模块的摘要,大致了解其中可用的函数和参数就可以了。
7.1. 概览
7.1. 概览
字符串也有很多方法,可以进行搜索 (index、find 和 count)、替换 (replace) 和解析 (split),但它们仅限于处理最简单的情况。搜索方法查找单个和固定编码的子串,并且它们总是大小写敏感的。对一个字符串s,如果要进行大小写不敏感的搜索,则你必须调用 s.lower() 或 s.upper() 将 s 转换成全小写或者全大写,然后确保搜索串有着相匹配的大小写。replace 和 split方法有着类似的限制。
如果你要解决的问题利用字符串函数能够完成,你应该使用它们。它们快速、简单且容易阅读,而快速、简单、可读性强的代码可以说出很多好处。但是,如果你发现你使用了许多不同的字符串函数和 if 语句来处理一个特殊情况,或者你组合使用了 split、join 等函数而导致用一种奇怪的甚至读不下去的方式理解列表,此时,你也许需要转到正则表达式了。
尽管正则表达式语法较之普通代码相对麻烦一些,但是却可以得到更可读的结果,与用一长串字符串函数的解决方案相比要好很多。在正则表达式内部有多种方法嵌入注释,从而使之具有自文档化 (self-documenting) 的能力。
7.2. 个案研究:街道地址
7.2. 个案研究:街道地址
这一系列的例子是由我几年前日常工作中的现实问题启发而来的,当时我需要从一个老化系统中导出街道地址,在将它们导入新的系统之前,进行清理和标准化。(看,我不是只将这些东西堆到一起,它有实际的用处。)这个例子展示我如何处理这个问题。
例 7.1. 在字符串的结尾匹配
>>> s = '100 NORTH MAIN ROAD'
>>> s.replace('ROAD', 'RD.')
'100 NORTH MAIN RD.'
>>> s = '100 NORTH BROAD ROAD'
>>> s.replace('ROAD', 'RD.')
'100 NORTH BRD. RD.'
>>> s[:-4] + s[-4:].replace('ROAD', 'RD.')
'100 NORTH BROAD RD.'
>>> import re
>>> re.sub('ROAD$', 'RD.', s)
'100 NORTH BROAD RD.'
| [1] | 我的目标是将街道地址标准化,'ROAD' 通常被略写为 'RD.'。乍看起来,我以为这个太简单了,只用字符串的方法 replace 就可以了。毕竟,所有的数据都已经是大写的了,因此大小写不匹配将不是问题。并且,要搜索的串'ROAD'是一个常量,在这个迷惑的简单例子中,s.replace 的确能够胜任。 |
| [2] | 不幸的是,生活充满了特例,并且我很快就意识到这个问题。比如:'ROAD' 在地址中出现两次,一次是作为街道名称 'BROAD' 的一部分,一次是作为 'ROAD' 本身。replace 方法遇到这两处的'ROAD'并没有区别,因此都进行了替换,而我发现地址被破坏掉了。 |
| [3] | 为了解决在地址中出现多次'ROAD'子串的问题,有可能采用类似这样的方法:只在地址的最后四个字符中搜索替换 'ROAD' (s[-4:]),忽略字符串的其他部分 (s[:-4])。但是,你可能发现这已经变得不方便了。例如,该模式依赖于你要替换的字符串的长度了 (如果你要把 'STREET' 替换为 'ST.',你需要利用 s[:-6] 和 s[-6:].replace(...))。你愿意在六月个期间回来调试它们么?我本人是不愿意的。 |
| [4] | 是时候转到正则表达式了。在 Python 中,所有和正则表达式相关的功能都包含在 re 模块中。 |
| [5] | 来看第一个参数:'ROAD$'。这个正则表达式非常简单,只有当 'ROAD' 出现在一个字符串的尾部时才会匹配。字符$表示“字符串的末尾”(还有一个对应的字符,尖号^,表示“字符串的开始”)。 |
| [6] | 利用 re.sub 函数,对字符串 s 进行搜索,满足正则表达式 'ROAD$' 的用 'RD.' 替换。这样将匹配字符串 s 末尾的 'ROAD',而不会匹配属于单词 'ROAD' 一部分的 'ROAD',这是因为它是出现在 s 的中间。 |
继续我的清理地址的故事。很快我发现,在上面的例子中,仅仅匹配地址末尾的 'ROAD' 不是很好,因为不是所有的地址都包括表示街道的单词 ('ROAD');有一些直接以街道名结尾。大部分情况下,不会遇到这种情况,但是,如果街道名称为 'BROAD',那么正则表达式将会匹配 'BROAD' 的一部分为 'ROAD',而这并不是我想要的。
例 7.2. 匹配整个单词
>>> s = '100 BROAD'
>>> re.sub('ROAD$', 'RD.', s)
'100 BRD.'
>>> re.sub('\\bROAD$', 'RD.', s)
'100 BROAD'
>>> re.sub(r'\bROAD$', 'RD.', s)
'100 BROAD'
>>> s = '100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD$', 'RD.', s)
'100 BROAD ROAD APT. 3'
>>> re.sub(r'\bROAD\b', 'RD.', s)
'100 BROAD RD. APT 3'
| [1] | 我真正想要做的是,当 'ROAD' 出现在字符串的末尾,并且是作为一个独立的单词时,而不是一些长单词的一部分,才对他进行匹配。为了在正则表达式中表达这个意思,你利用 \b,它的含义是“单词的边界必须在这里”。在 Python 中,由于字符 '\' 在一个字符串中必须转义,这会变得非常麻烦。有时候,这类问题被称为“反斜线灾难”,这也是 Perl 中正则表达式比 Python 的正则表达式要相对容易的原因之一。另一方面,Perl 也混淆了正则表达式和其他语法,因此,如果你发现一个 bug,很难弄清楚究竟是一个语法错误,还是一个正则表达式错误。 |
| [2] | 为了避免反斜线灾难,你可以利用所谓的“原始字符串”,只要为字符串添加一个前缀 r 就可以了。这将告诉 Python,字符串中的所有字符都不转义;'\t' 是一个制表符,而 r'\t' 是一个真正的反斜线字符 '\',紧跟着一个字母 't'。我推荐只要处理正则表达式,就使用原始字符串;否则,事情会很快变得混乱 (并且正则表达式自己也会很快被自己搞乱了)。 |
| [3] | (一声叹息) 很不幸,我很快发现更多的与我的逻辑相矛盾的例子。在这个例子中,街道地址包含有作为整个单词的'ROAD',但是它不是在末尾,因为地址在街道命名后会有一个房间号。由于 'ROAD' 不是在每一个字符串的末尾,没有匹配上,因此调用 re.sub 没有替换任何东西,你获得的只是初始字符串,这也不是我们想要的。 |
| [4] | 为了解决这个问题,我去掉了 $ 字符,加上另一个 \b。现在,正则表达式“匹配字符串中作为整个单词出现的'ROAD'”了,不论是在末尾、开始还是中间。 |
7.3. 个案研究:罗马字母
7.3. 个案研究:罗马字母
- 7.3.1. 校验千位数
- 7.3.2. 校验百位数
你可能经常看到罗马数字,即使你没有意识到它们。你可能曾经在老电影或者电视中看到它们 (“版权所有 MCMXLVI” 而不是 “版权所有1946”),或者在某图书馆或某大学的贡献墙上看到它们 (“成立于 MDCCCLXXXVIII”而不是“成立于1888”)。你也可能在某些文献的大纲或者目录上看到它们。这是一个表示数字的系统,它实际上能够追溯到远古的罗马帝国 (因此而得名)。
在罗马数字中,利用 7 个不同字母进行重复或者组合来表达各式各样的数字。
I=1V=5X=10L=50C=100D=500M=1000
下面是关于构造罗马数字的一些通用的规则的介绍:
- 字符是叠加的。
I表示1,II表示2,而III表示3。VI表示6(字面上为逐字符相加,“5加1”),VII表示7,VIII表示8。 - 含十字符 (
I、X、C和M) 至多可以重复三次。对于4,你则需要利用下一个最大的含五字符进行减操作得到:你不能把4表示成IIII,而应表示为IV(“比5小1”)。数字40写成XL(比50小10),41写成XLI,42写成XLII,43写成XLIII,而44写成XLIV(比50小10,然后比5小1)。 - 类似地,对于数字
9,你必须利用下一个含十字符进行减操作得到:8表示为VIII,而9则表示为IX(比10小1),而不是VIIII(因为字符I不能连续重复四次)。数字90表示为XC,900表示为CM。 - 含五字符不能重复。数字
10常表示为X,而从来不用VV来表示。数字100常表示为C,也从来不表示为LL。 - 罗马数字一般从高位到低位书写,从左到右阅读,因此不同顺序的字符意义大不相同。
DC表示600;而CD是一个完全不同的数字 (为400,也就是比500小100)。CI表示101;而IC甚至不是一个合法的罗马字母 (因为你不能直接从数字100减去1;这需要写成XCIX,意思是比100小10,然后加上数字9,也就是比10小1的数字)。
7.3.1. 校验千位数
怎样校验任意一个字符串是否为一个有效的罗马数字呢?我们每次只看一位数字,由于罗马数字一般是从高位到低位书写。我们从高位开始:千位。对于大于或等于 1000 的数字,千位由一系列的字符 M 表示。
例 7.3. 校验千位数
>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')
<SRE_Match object at 0106FB58>
>>> re.search(pattern, 'MM')
<SRE_Match object at 0106C290>
>>> re.search(pattern, 'MMM')
<SRE_Match object at 0106AA38>
>>> re.search(pattern, 'MMMM')
>>> re.search(pattern, '')
<SRE_Match object at 0106F4A8>
| [1] | 这个模式有三部分:^ 表示仅在一个字符串的开始匹配其后的字符串内容。如果没有这个字符,这个模式将匹配出现在字符串任意位置上的 M,而这并不是你想要的。你想确认的是:字符串中是否出现字符 M,如果出现,则必须是在字符串的开始。M? 可选地匹配单个字符 M,由于它最多可重复出现三次,你可以在一行中匹配 0 次到 3 次字符 M。$ 字符限制模式只能够在一个字符串的结尾匹配。当和模式开头的字符 ^ 结合使用时,这意味着模式必须匹配整个串,并且在在字符 M 的前后都不能够出现其他的任意字符。 |
| [2] | re 模块的关键是一个 search 函数,该函数有两个参数,一个是正则表达式 (pattern),一个是字符串 ('M'),函数试图匹配正则表达式。如果发现一个匹配,search 函数返回一个拥有多种方法可以描述这个匹配的对象,如果没有发现匹配,search 函数返回一个 None,一个 Python 空值 (null value)。你此刻关注的唯一事情,就是模式是否匹配上,于是我们利用 search 函数的返回值了解这个事实。字符串'M' 匹配上这个正则表达式,因为第一个可选的 M 匹配上,而第二个和第三个 M 被忽略掉了。 |
| [3] | 'MM' 能匹配上是因为第一和第二个可选的 M 匹配上,而忽略掉第三个 M。 |
| [4] | 'MMM' 能匹配上因为三个 M 都匹配上了。 |
| [5] | 'MMMM' 没有匹配上。因为所有的三个 M 都匹配完了,但是正则表达式还有字符串尾部的限制 (由于字符 $),而字符串又没有结束 (因为还有第四个 M 字符),因此 search 函数返回一个 None。 |
| [6] | 有趣的是,一个空字符串也能够匹配这个正则表达式,因为所有的字符 M 都是可选的。 |
7.3.2. 校验百位数
与千位数相比,百位数识别起来要困难得多,这是因为有多种相互独立的表达方式都可以表达百位数,而具体用那种方式表达和具体的数值有关。
100=C200=CC300=CCC400=CD500=D600=DC700=DCC800=DCCC900=CM
因此有四种可能的模式:
CMCD- 零到三次出现
C字符 (出现零次表示百位数为 0) D,后面跟零个到三个C字符
后面两个模式可以结合到一起:
- 一个可选的字符
D,加上零到 3 个C字符。
这个例子显示如何有效地识别罗马数字的百位数。
例 7.4. 检验百位数
>>> import re
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$'
>>> re.search(pattern, 'MCM')
<SRE_Match object at 01070390>
>>> re.search(pattern, 'MD')
<SRE_Match object at 01073A50>
>>> re.search(pattern, 'MMMCCC')
<SRE_Match object at 010748A8>
>>> re.search(pattern, 'MCMC')
>>> re.search(pattern, '')
<SRE_Match object at 01071D98>
| [1] | 这个模式的首部和上一个模式相同,检查字符串的开始 (^),接着匹配千位数 (M?M?M?),然后才是这个模式的新内容。在括号内,定义了包含有三个互相独立的模式集合,由垂直线隔开:CM、CD 和 D?C?C?C? (D是可选字符,接着是 0 到 3 个可选的 C 字符)。正则表达式解析器依次检查这些模式 (从左到右),如果匹配上第一个模式,则忽略剩下的模式。 |
| [2] | 'MCM' 匹配上,因为第一个 M 字符匹配,第二和第三个 M 字符被忽略掉,而 CM 匹配上 (因此 CD 和 D?C?C?C? 两个模式不再考虑)。MCM 表示罗马数字1900。 |
| [3] | 'MD' 匹配上,因为第一个字符 M 匹配上,第二第三个 M 字符忽略,而模式 D?C?C?C? 匹配上 D (模式中的三个可选的字符 C 都被忽略掉了)。MD 表示罗马数字 1500。 |
| [4] | 'MMMCCC' 匹配上,因为三个 M 字符都匹配上,而模式 D?C?C?C? 匹配上 CCC (字符D是可选的,此处忽略)。MMMCCC 表示罗马数字 3300。 |
| [5] | 'MCMC' 没有匹配上。第一个 M 字符匹配上,第二第三个 M 字符忽略,接着是 CM 匹配上,但是接着是 $ 字符没有匹配,因为字符串还没有结束 (你仍然还有一个没有匹配的C字符)。C 字符也不 匹配模式 D?C?C?C? 的一部分,因为与之相互独立的模式 CM 已经匹配上。 |
| [6] | 有趣的是,一个空字符串也可以匹配这个模式,因为所有的 M 字符都是可选的,它们都被忽略,并且一个空字符串可以匹配 D?C?C?C? 模式,此处所有的字符也都是可选的,并且都被忽略。 |
哎呀!看看正则表达式能够多快变得难以理解?你仅仅表示了罗马数字的千位和百位上的数字。如果你根据类似的方法,十位数和各位数就非常简单了,因为是完全相同的模式。让我们来看表达这个模式的另一种方式吧。
7.4. 使用 {n,m} 语法
7.4. 使用 {n,m} 语法
- 7.4.1. 校验十位数和个位数
在前面的章节,你处理了相同字符可以重复三次的情况。在正则表达式中,有另外一个方式来表达这种情况,并且能提高代码的可读性。首先看看我们在前面的例子中使用的方法。
例 7.5. 老方法:每一个字符都是可选的
>>> import re
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'M')
<_sre.SRE_Match object at 0x008EE090>
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'MM')
<_sre.SRE_Match object at 0x008EEB48>
>>> pattern = '^M?M?M?$'
>>> re.search(pattern, 'MMM')
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMMM')
>>>
| [1] | 这个模式匹配串的开始,接着是第一个可选的字符 M,第二第三个 M 字符则被忽略 (这是可行的,因为它们都是可选的),最后是字符串的结尾。 |
| [2] | 这个模式匹配串的开始,接着是第一和第二个可选字符 M,而第三个 M 字符被忽略 (这是可行的,因为它们都是可选的),最后匹配字符串的结尾。 |
| [3] | 这个模式匹配字符串的开始,接着匹配所有的三个可选字符 M,最后匹配字符串的结尾。 |
| [4] | 这个模式匹配字符串的开始,接着匹配所有的三个可选字符 M,但是不能够匹配字符串的结尾 (因为还有一个未匹配的字符 M),因此不能够匹配而返回一个 None。 |
例 7.6. 一个新的方法:从 n 到 m
>>> pattern = '^M{0,3}$'
>>> re.search(pattern, 'M')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MM')
<_sre.SRE_Match object at 0x008EE090>
>>> re.search(pattern, 'MMM')
<_sre.SRE_Match object at 0x008EEDA8>
>>> re.search(pattern, 'MMMM')
>>>
| [1] | 这个模式意思是说:“匹配字符串的开始,接着匹配 0 到 3 个 M 字符,然后匹配字符串的结尾。”这里的 0 和 3 可以改成其它任何数字;如果你想要匹配至少 1 次,至多 3 次字符 M,则可以写成 M{1,3}。 |
| [2] | 这个模式匹配字符串的开始,接着匹配三个可选 M 字符中的一个,最后是字符串的结尾。 |
| [3] | 这个模式匹配字符串的开始,接着匹配三个可选 M 字符中的两个,最后是字符串的结尾。 |
| [4] | 这个模式匹配字符串的开始,接着匹配三个可选 M 字符中的三个,最后是字符串的结尾。 |
| [5] | 这个模式匹配字符串的开始,接着匹配三个可选 M 字符中的三个,但是没有匹配上 字符串的结尾。正则表达式在字符串结尾之前最多只允许匹配三次 M 字符,但是实际上有四个 M 字符,因此模式没有匹配上这个字符串,返回一个 None。 |
注意 没有一个轻松的方法来确定两个正则表达式是否等价。你能采用的最好的办法就是列出很多的测试样例,确定这两个正则表达式对所有的相关输入都有相同的输出。在本书后面的章节,将更多地讨论如何编写测试样例。
7.4.1. 校验十位数和个位数
现在我们来扩展一下关于罗马数字的正则表达式,以匹配十位数和个位数,下面的例子展示十位数的校验方法。
例 7.7. 校验十位数
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)$'
>>> re.search(pattern, 'MCMXL')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCML')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLX')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXX')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXX')
>>>
| [1] | 这个模式匹配字符串的开始,接着是第一个可选字符 M,接着是 CM,接着 XL,接着是字符串的结尾。请记住,(A|B|C) 这个语法的含义是“精确匹配 A、B 或者 C 其中的一个”。此处匹配了 XL,因此不再匹配 XC 和 L?X?X?X?,接着就匹配到字符串的结尾。MCML 表示罗马数字 1940。 |
| [2] | 这个模式匹配字符串的开始,接着是第一个可选字符 M,接着是 CM,接着 L?X?X?X?。在模式 L?X?X?X? 中,它匹配 L 字符并且跳过所有可选的 X 字符,接着匹配字符串的结尾。MCML 表示罗马数字 1950。 |
| [3] | 这个模式匹配字符串的开始,接着是第一个可选字符 M,接着是 CM,接着是可选的 L 字符和可选的第一个 X 字符,并且跳过第二第三个可选的 X 字符,接着是字符串的结尾。MCMLX 表示罗马数字 1960。 |
| [4] | 这个模式匹配字符串的开始,接着是第一个可选字符 M,接着是 CM,接着是可选的 L 字符和所有的三个可选的 X 字符,接着匹配字符串的结尾。MCMLXXX 表示罗马数字 1980。 |
| [5] | 这个模式匹配字符串的开始,接着是第一个可选字符M,接着是CM,接着是可选的 L字符和所有的三个可选的X字符,接着就未能匹配 字符串的结尾 ie,因为还有一个未匹配的X 字符。所以整个模式匹配失败并返回一个 None. MCMLXXXX 不是一个有效的罗马数字。 |
对于个位数的正则表达式有类似的表达方式,我将省略细节,直接展示结果。
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)(XC|XL|L?X?X?X?)(IX|IV|V?I?I?I?)$'
用另一种 {n,m} 语法表达这个正则表达式会如何呢?这个例子展示新的语法。
例 7.8. 用 {n,m} 语法确认罗马数字
>>> pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
>>> re.search(pattern, 'MDLV')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMDCLXVI')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMDCCCLXXXVIII')
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'I')
<_sre.SRE_Match object at 0x008EEB48>
| [1] | 这个模式匹配字符串的开始,接着匹配三个可选的 M 字符的一个,接着匹配 D?C{0,3},此处,仅仅匹配可选的字符 D 和 0 个可选字符 C。继续向前匹配,匹配 L?X{0,3},此处,匹配可选的 L 字符和 0 个可选字符 X,接着匹配 V?I{0,3},此处,匹配可选的 V 和 0 个可选字符 I,最后匹配字符串的结尾。MDLV 表示罗马数字 1555。 |
| [2] | 这个模式匹配字符串的开始,接着是三个可选的 M 字符的两个,接着匹配 D?C{0,3},此处为一个字符 D 和三个可选 C 字符中的一个,接着匹配 L?X{0,3},此处为一个 L 字符和三个可选 X 字符中的一个,接着匹配 V?I{0,3},此处为一个字符 V 和三个可选 I 字符中的一个,接着匹配字符串的结尾。MMDCLXVI 表示罗马数字 2666。 |
| [3] | 这个模式匹配字符串的开始,接着是三个可选的 M 字符的所有字符,接着匹配 D?C{0,3},此处为一个字符 D 和三个可选 C 字符中所有字符,接着匹配 L?X{0,3},此处为一个 L 字符和三个可选 X 字符中所有字符,接着匹配 V?I{0,3},此处为一个字符 V 和三个可选 I 字符中所有字符,接着匹配字符串的结尾。MMMDCCCLXXXVIII 表示罗马数字3888,这个数字是不用扩展语法可以写出的最大的罗马数字。 |
| [4] | 仔细看哪!(我像一个魔术师一样,“看仔细喽,孩子们,我将要从我的帽子中拽出一只兔子来啦!”) 这个模式匹配字符串的开始,接着匹配 3 个可选 M 字符的 0 个,接着匹配 D?C{0,3},此处,跳过可选字符 D 并匹配三个可选 C 字符的 0 个,接着匹配 L?X{0,3},此处,跳过可选字符 L 并匹配三个可选 X 字符的 0 个,接着匹配 V?I{0,3},此处跳过可选字符 V 并匹配三个可选 I 字符的一个,最后匹配字符串的结尾。哇赛! |
如果你在第一遍就跟上并理解了所讲的这些,那么你做的比我还要好。现在,你可以尝试着理解别人大规模程序里关键函数中的正则表达式了。或者想象着几个月后回头理解你自己的正则表达式。我曾经做过这样的事情,但是它并不是那么有趣。
在下一节里,你将会研究另外一种正则表达式语法,它可以使你的表达式具有更好的可维持性。
7.5. 松散正则表达式
7.5. 松散正则表达式
迄今为止,你只是处理过被我称之为“紧凑”类型的正则表达式。正如你曾看到的,它们难以阅读,即使你清楚正则表达式的含义,你也不能保证六个月以后你还能理解它。你真正所需的就是利用内联文档 (inline documentation)。
Python 允许用户利用所谓的松散正则表达式 来完成这个任务。一个松散正则表达式和一个紧凑正则表达式主要区别表现在两个方面:
- 忽略空白符。空格符,制表符,回车符不匹配它们自身,它们根本不参与匹配。(如果你想在松散正则表达式中匹配一个空格符,你必须在它前面添加一个反斜线符号对它进行转义。)
- 忽略注释。在松散正则表达式中的注释和在普通 Python 代码中的一样:开始于一个
#符号,结束于行尾。这种情况下,采用在一个多行字符串中注释,而不是在源代码中注释,它们以相同的方式工作。
用一个例子可以解释得更清楚。让我们重新来看前面的紧凑正则表达式,利用松散正则表达式重新表达。下面的例子显示实现方法。
例 7.9. 带有内联注释 (Inline Comments) 的正则表达式
>>> pattern = """
^ # beginning of string
M{0,3} # thousands - 0 to 3 M's
(CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's),
# or 500-800 (D, followed by 0 to 3 C's)
(XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's),
# or 50-80 (L, followed by 0 to 3 X's)
(IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's),
# or 5-8 (V, followed by 0 to 3 I's)
$ # end of string
"""
>>> re.search(pattern, 'M', re.VERBOSE)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MCMLXXXIX', re.VERBOSE)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'MMMDCCCLXXXVIII', re.VERBOSE)
<_sre.SRE_Match object at 0x008EEB48>
>>> re.search(pattern, 'M')
| [1] | 当使用松散正则表达式时,最重要的一件事情就是:必须传递一个额外的参数 re.VERBOSE,该参数是定义在 re 模块中的一个常量,标志着待匹配的正则表达式是一个松散正则表达式。正如你看到的,这个模式中,有很多空格 (所有的空格都被忽略),和几个注释 (所有的注释也被忽略)。如果忽略所有的空格和注释,它就和前面章节里的正则表达式完全相同,但是具有更好的可读性。 |
| [2] | 这个模式匹配字符串的开始,接着匹配三个可选 M 字符中的一个,接着匹配 CM,接着是字符 L 和三个可选 X 字符的所有字符,接着是 IX,然后是字符串的结尾。 |
| [3] | 这个模式匹配字符串的开始,接着是三个可选的 M 字符的所有字符,接着匹配 D?C{0,3},此处为一个字符 D 和三个可选 C 字符中所有字符,接着匹配 L?X{0,3},此处为一个 L 字符和三个可选 X 字符中所有字符,接着匹配 V?I{0,3},此处为一个字符 V 和三个可选 I 字符中所有字符,接着匹配字符串的结尾。 |
| [4] | 这个没有匹配。为什么呢?因为没有 re.VERBOSE 标记,所以 re.search 函数把模式作为一个紧凑正则表达式进行匹配。Python 不能自动检测一个正则表达式是为松散类型还是紧凑类型。Python 默认每一个正则表达式都是紧凑类型的,除非你显式地标明一个正则表达式为松散类型。 |
7.6. 个案研究:解析电话号码
7.6. 个案研究:解析电话号码
迄今为止,你主要是匹配整个模式,不论是匹配上,还是没有匹配上。但是正则表达式还有比这更为强大的功能。当一个模式确实 匹配上时,你可以获取模式中特定的片断,你可以发现具体匹配的位置。
这个例子来源于我遇到的另一个现实世界的问题,也是在以前的工作中遇到的。问题是:解析一个美国电话号码。客户要能 (在一个单一的区域中) 输入任何数字,然后存储区号、干线号、电话号和一个可选的独立的分机号到公司数据库里。为此,我通过网络找了很多正则表达式的例子,但是没有一个能够完全满足我的要求。
这里列举了我必须能够接受的电话号码:
800-555-1212800 555 1212800.555.1212(800) 555-12121-800-555-1212800-555-1212-1234800-555-1212x1234800-555-1212 ext. 1234work 1-(800) 555.1212 #1234
格式可真够多的!我需要知道区号是 800,干线号是 555,电话号的其他数字为 1212。对于那些有分机号的,我需要知道分机号为 1234。
让我们完成电话号码解析这个工作,这个例子展示第一步。
例 7.10. 发现数字
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})$')
>>> phonePattern.search('800-555-1212').groups()
('800', '555', '1212')
>>> phonePattern.search('800-555-1212-1234')
>>>
| [1] | 我们通常从左到右阅读正则表达式。这个正则表达式匹配字符串的开始,接着匹配 (\d{3})。\d{3} 是什么呢?好吧,{3} 的含义是“精确匹配三个数字”;这是曾在前面见到过的 {n,m} 语法的一种变形。\d 的含义是 “任何一个数字” (0 到 9)。把它们放大括号中意味着要“精确匹配三个数字位,接着把它们作为一个组保存下来,以便后面的调用”。接着匹配一个连字符,接着是另外一个精确匹配三个数字位的组,接着另外一个连字符,接着另外一个精确匹配四个数字为的组,接着匹配字符串的结尾。 |
| [2] | 为了访问正则表达式解析过程中记忆下来的多个组,我们使用 search 函数返回对象的 groups() 函数。这个函数将返回一个元组,元组中的元素就是正则表达式中定义的组。在这个例子中,定义了三个组,第一个组有三个数字位,第二个组有三个数字位,第三个组有四个数字位。 |
| [3] | 这个正则表达式不是最终的答案,因为它不能处理在电话号码结尾有分机号的情况,为此,我们需要扩展这个正则表达式。 |
例 7.11. 发现分机号
>>> phonePattern = re.compile(r'^(\d{3})-(\d{3})-(\d{4})-(\d+)$')
>>> phonePattern.search('800-555-1212-1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800 555 1212 1234')
>>>
>>> phonePattern.search('800-555-1212')
>>>
| [1] | 这个正则表达式和上一个几乎相同,正像前面的那样,匹配字符串的开始,接着匹配一个有三个数字位的组并记忆下来,接着是一个连字符,接着是一个有三个数字位的组并记忆下来,接着是一个连字符,接着是一个有四个数字位的组并记忆下来。不同的地方是你接着又匹配了另一个连字符,然后是一个有一个或者多个数字位的组并记忆下来,最后是字符串的结尾。 |
| [2] | 函数 groups() 现在返回一个有四个元素的元组,由于正则表达式中定义了四个记忆的组。 |
| [3] | 不幸的是,这个正则表达式也不是最终的答案,因为它假设电话号码的不同部分是由连字符分割的。如果一个电话号码是由空格符、逗号或者点号分割呢?你需要一个更一般的解决方案来匹配几种不同的分割类型。 |
| [4] | 啊呀!这个正则表达式不仅不能解决你想要的任何问题,反而性能更弱了,因为现在你甚至不能解析一个没有分机号的电话号码了。这根本不是你想要的,如果有分机号,你要知道分机号是什么,如果没有分机号,你仍然想要知道主电话号码的其他部分是什么。 |
下一个例子展示正则表达式处理一个电话号码内部,采用不同分隔符的情况。
例 7.12. 处理不同分隔符
>>> phonePattern = re.compile(r'^(\d{3})\D+(\d{3})\D+(\d{4})\D+(\d+)$')
>>> phonePattern.search('800 555 1212 1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212-1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('80055512121234')
>>>
>>> phonePattern.search('800-555-1212')
>>>
| [1] | 当心啦!你首先匹配字符串的开始,接着是一个三个数字位的组,接着是 \D+,这是个什么东西?好吧,\D 匹配任意字符,除了 数字位,+ 表示“1 个或者多个”,因此 \D+ 匹配一个或者多个不是数字位的字符。这就是你替换连字符为了匹配不同分隔符所用的方法。 |
| [2] | 使用 \D+ 代替 - 意味着现在你可以匹配中间是空格符分割的电话号码了。 |
| [3] | 当然,用连字符分割的电话号码也能够被识别。 |
| [4] | 不幸的是,这个正则表达式仍然不是最终答案,因为它假设电话号码一定有分隔符。如果电话号码中间没有空格符或者连字符的情况会怎样哪? |
| [4] | 我的天!这个正则表达式也没有达到我们对于分机号识别的要求。现在你共有两个问题,但是你可以利用相同的技术来解决它们。 |
下一个例子展示正则表达式处理没有 分隔符的电话号码的情况。
例 7.13. 处理没有分隔符的数字
>>> phonePattern = re.compile(r'^(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')
>>> phonePattern.search('80055512121234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800.555.1212 x1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()
('800', '555', '1212', '')
>>> phonePattern.search('(800)5551212 x1234')
>>>
| [1] | 和上一步相比,你所做的唯一变化就是把所有的 + 变成 *。在电话号码的不同部分之间不再匹配 \D+,而是匹配 \D* 了。还记得 + 的含义是“1 或者多个”吗? 好的,* 的含义是“0 或者多个”。因此,现在你应该能够解析没有分隔符的电话号码了。 |
| [2] | 你瞧,它真的可以胜任。为什么?首先匹配字符串的开始,接着是一个有三个数字位 (800) 的组,接着是 0 个非数字字符,接着是一个有三个数字位 (555) 的组,接着是 0 个非数字字符,接着是一个有四个数字位 (1212) 的组,接着是 0 个非数字字符,接着是一个有任意数字位 (1234) 的组,最后是字符串的结尾。 |
| [3] | 对于其他的变化也能够匹配:比如点号分隔符,在分机号前面既有空格符又有 x 符号的情况也能够匹配。 |
| [4] | 最后,你已经解决了长期存在的一个问题:现在分机号是可选的了。如果没有发现分机号,groups() 函数仍然返回一个有四个元素的元组,但是第四个元素只是一个空字符串。 |
| [5] | 我不喜欢做一个坏消息的传递人,此时你还没有完全结束这个问题。还有什么问题呢?当在区号前面还有一个额外的字符时,而正则表达式假设区号是一个字符串的开始,因此不能匹配。这个不是问题,你可以利用相同的技术“0 或者多个非数字字符”来跳过区号前面的字符。 |
下一个例子展示如何解决电话号码前面有其他字符的情况。
例 7.14. 处理开始字符
>>> phonePattern = re.compile(r'^\D*(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')
>>> phonePattern.search('(800)5551212 ext. 1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212').groups()
('800', '555', '1212', '')
>>> phonePattern.search('work 1-(800) 555.1212 #1234')
>>>
| [1] | 这个正则表达式和前面的几乎相同,但它在第一个记忆组 (区号) 前面匹配 \D*,0 或者多个非数字字符。注意,此处你没有记忆这些非数字字符 (它们没有被括号括起来)。如果你发现它们,只是跳过它们,接着只要匹配上就开始记忆区号。 |
| [2] | 你可以成功地解析电话号码,即使在区号前面有一个左括号。(在区号后面的右括号也已经被处理,它被看成非数字字符分隔符,由第一个记忆组后面的 \D* 匹配。) |
| [3] | 进行仔细的检查,保证你没有破坏前面能够匹配的任何情况。由于首字符是完全可选的,这个模式匹配字符串的开始,接着是 0 个非数字字符,接着是一个有三个数字字符的记忆组 (800),接着是 1 个非数字字符 (连字符),接着是一个有三个数字字符的记忆组 (555),接着是 1 个非数字字符 (连字符),接着是一个有四个数字字符的记忆组 (1212),接着是 0 个非数字字符,接着是一个有 0 个数字位的记忆组,最后是字符串的结尾。 |
| [4] | 此处是正则表达式让我产生了找一个硬东西挖出自己的眼睛的冲动。为什么这个电话号码没有匹配上?因为在它的区号前面有一个 1,但是你认为在区号前面的所有字符都是非数字字符 (\D*)。唉! |
让我们往回看一下。迄今为止,正则表达式总是从一个字符串的开始匹配。但是现在你看到了,有很多不确定的情况需要你忽略。与其尽力全部匹配它们,还不如全部跳过它们,让我们采用一个不同的方法:根本不显式地匹配字符串的开始。下面的这个例子展示这个方法。
例 7.15. 电话号码,无论何时我都要找到它
>>> phonePattern = re.compile(r'(\d{3})\D*(\d{3})\D*(\d{4})\D*(\d*)$')
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212')
('800', '555', '1212', '')
>>> phonePattern.search('80055512121234')
('800', '555', '1212', '1234')
| [1] | 注意,在这个正则表达式的开始少了一个 ^ 字符。你不再匹配字符串的开始了,也就是说,你需要用你的正则表达式匹配整个输入字符串,除此之外没有别的意思了。正则表达式引擎将要努力计算出开始匹配输入字符串的位置,并且从这个位置开始匹配。 |
| [2] | 现在你可以成功解析一个电话号码了,无论这个电话号码的首字符是不是数字,无论在电话号码各部分之间有多少任意类型的分隔符。 |
| [3] | 仔细检查,这个正则表达式仍然工作的很好。 |
| [4] | 还是能够工作。 |
看看一个正则表达式能够失控得多快?回头看看前面的例子,你还能区别它们么?
当你还能够理解这个最终答案的时候 (这个正则表达式就是最终答案,即使你发现一种它不能处理的情况,我也真的不想知道它了),在你忘记为什么你这么选择之前,让我们把它写成松散正则表达式的形式。
例 7.16. 解析电话号码 (最终版本)
>>> phonePattern = re.compile(r'''
# don't match beginning of string, number can start anywhere
(\d{3}) # area code is 3 digits (e.g. '800')
\D* # optional separator is any number of non-digits
(\d{3}) # trunk is 3 digits (e.g. '555')
\D* # optional separator
(\d{4}) # rest of number is 4 digits (e.g. '1212')
\D* # optional separator
(\d*) # extension is optional and can be any number of digits
$ # end of string
''', re.VERBOSE)
>>> phonePattern.search('work 1-(800) 555.1212 #1234').groups()
('800', '555', '1212', '1234')
>>> phonePattern.search('800-555-1212')
('800', '555', '1212', '')
| [1] | 除了被分成多行,这个正则表达式和最后一步的那个完全相同,因此它能够解析相同的输入一点也不奇怪。 |
| [2] | 进行最后的仔细检查。很好,仍然工作。你终于完成了这件任务。 |
关于正则表达式的进一步阅读
- Regular Expression HOWTO 讲解正则表达式和如何在 Python 中使用正则表达式。
- Python Library Reference 概述了
remodule。
7.7. 小结
7.7. 小结
这只是正则表达式能够完成工作的很少一部分。换句话说,即使你现在备受打击,相信我,你也不是什么也没见过了。
现在,你应该熟悉下列技巧:
^匹配字符串的开始。$匹配字符串的结尾。\b匹配一个单词的边界。\d匹配任意数字。\D匹配任意非数字字符。x?匹配一个可选的x字符 (换言之,它匹配 1 次或者 0 次x字符)。x*匹配 0 次或者多次x字符。x+匹配 1 次或者多次x字符。x{n,m}匹配x字符,至少n次,至多m次。(a|b|c)要么匹配a,要么匹配b,要么匹配c。(x)一般情况下表示一个记忆组 (remembered group)。你可以利用re.search函数返回对象的groups()函数获取它的值。
正则表达式非常强大,但是它并不能为每一个问题提供正确的解决方案。你应该学习足够多的知识,以辨别什么时候它们是合适的,什么时候它们会解决你的问题,什么时候它们产生的问题比要解决的问题还要多。
一些人,遇到一个问题时就想:“我知道,我将使用正则表达式。”现在他有两个问题了。 --Jamie Zawinski, in comp.emacs.xemacs