已知道如何创建和执行程序(或脚本),还知道如何使用 import 将函数从外部模块导入到程序中。
>>> import math
>>> math.sin(0)
0.0
如何编写自己的模块?
任何Python程序都可作为模块导入。
编写了代码所示的程序,并将其保存在文件hello.py中,这个文件的名称(不包括扩展名.py)将成为模块的名称。
# hello.py
print("Hello, world!")
假设这个文件存储在目录C:\python
告诉解释器去哪里查找这个模块,可执行如下命令(以Windows目录为例):
>>> import sys
>>> sys.path.append('C:/python')
提示
在UNIX中,不能直接将字符串 '~/python' 附加到 sys.path 末尾,
而必须使用完整的路径(如 '/home/yourusername/python' )。
如果你要自动创建完整的路径,可使用 sys.path.expanduser('~/python') 。
除了通常将查找的位置外,还应到目录C:\python中去查找这个模块。这样做后,就可以导入这个模块了(它存储在文件C:\python\hello.py中)。
>>> import hello
Hello, world!
导入这个模块时,执行了其中的代码。但如果再次导入它,什么事情都不会发生。
>>> import hello
>>>
模块并不是用来执行操作(如打印文本)的,而是用于定义变量、函数、类等。
鉴于定义只需做一次,因此导入模块多次和导入一次的效果相同。
让模块值得被创建的原因在于它们像类一样,有自己的作用域。
意味着在模块中定义的类和函数以及对其进行赋值的变量都将成为模块的属性。
1. 在模块中定义函数
只包含一个函数的简单模块
# hello2.py
def hello():
print("Hello, world!")
可以像下面这样导入它:
>>> hello2.hello()
Hello, world!
在模块的全局作用域内定义的名称都可像上面这样访问。
重用代码。通过将代码放在模块中,就可在多个程序中使用它们。
要让代码是可重用的,务必将其模块化!
2. 在模块中添加测试代码
模块用于定义函数和类等,但在有些情况下(实际上是经常),添加一些测试代码来检查情况是否符合预期很有用。
一个简单的模块,其中的测试代码有问题
# hello3.py
def hello():
print("Hello, world!")
# 一个测试:
hello()
如果在另一个程序中将其作为模块导入,以便能够使用函数 hello ,也将执行测试代码,就像本章的第一个 hello模块一样。
>>> import hello3
Hello, world!
>>> hello3.hello()
Hello, world!
要避免这种行为,关键是检查模块是作为程序运行还是被导入另一个程序。
为此,需要使用变量__name__ 。
>>> __name__
'__main__'
>>> hello3.__name__
'hello3'
在主程序中(包括解释器的交互式提示符),变量 name 的值是 ‘main’ ,而在导入的模块中,这个变量被设置为该模块的名称。
要让模块中测试代码的行为更合理,可将其放在一条 if 语句中。
一个包含有条件地执行的测试代码的模块
# hello4.py
def hello():
print("Hello, world!")
def test():
hello()
if __name__ == '__main__': test()
如果将这个模块作为程序运行,将执行函数 hello ;如果导入它,其行为将像普通模块一样。
>>> import hello4
>>> hello4.hello()
Hello, world!
将测试代码放在了函数 test 中。
通过将其放在一个独立的测试函数中,可在程序中导入模块并对其进行测试。
>>> hello4.test()
Hello, world!
两种办法:将模块放在正确的位置;告诉解释器到哪里去查找。
建议参阅“Python打包用户指南”:packaging.python.org。
1. 将模块放在正确的位置
只需找出Python解释器到哪里去查找模块,再将文件放在这个地方。
但如果没有管理员权限,就可能无法将模块保存到Python使用的目录中。
另一种解决方案:告诉解释器去哪里查找。
可在模块 sys 的变量 path 中找到目录列表(即搜索路径)。
>>> import sys, pprint
>>> pprint.pprint(sys.path)
['C:\\Python35\\Lib\\idlelib',
'C:\\Python35',
'C:\\Python35\\DLLs',
'C:\\Python35\\lib',
'C:\\Python35\\lib\\plat-win',
'C:\\Python35\\lib\\lib-tk',
'C:\\Python35\\lib\\site-packages']
提示
如果要打印的数据结构太大,一行容纳不下,可使用模块 pprint 中的函数 pprint (而不是普通 print 语句)。
pprint 是个卓越的打印函数,能够更妥善地打印输出。
每个字符串都表示一个位置,如果要让解释器能够找到模块,可将其放在其中任何一个位置中。
但目录site-packages是最佳的选择,因为它就是用来放置模块的。
将代码所示的模块保存到这里,但要使用另一个名称,如another_hello.py。再做如下操作:
>>> import another_hello
>>> another_hello.hello()
Hello, world!
只要模块位于类似于site-packages这样的地方,所有的程序就都能够导入它。
2. 告诉解释器到哪里去查找
将模块放在正确的位置可能不是合适的解决方案,其中的原因很多。
告诉解释器到哪里去查找模块,办法之一是直接修改 sys.path ,但这种做法不常见。标准做法是将模块所在的目录包含在环境变量 PYTHONPATH 中。
环境变量 PYTHONPATH 的内容随操作系统而异,但它基本上类似于sys.path ,也是一个目录列表。
除使用环境变量 PYTHONPATH 外,还可使用路径配置文件。
这些文件的扩展名为.pth,位于一些特殊目录中,包含要添加到 sys.path 中的目录。
为组织模块,可将其编组为包(package)。
包其实就是另一种模块,但是它们可包含其他模块。
模块存储在扩展名为.py的文件中,而包则是一个目录。
要被Python视为包,目录必须包含文件__init__.py。
要将模块加入包中,只需将模块文件放在包目录中即可。
还可以在包中嵌套其他包。
例如,
要创建一个名为 drawing 的包,其中包含模块 shapes 和 colors ,需要创建如下图所示的文件和目录(UNIX路径名)。
完成这些准备工作后,下面的语句都是合法的:
import drawing # (1) 导入drawing包
import drawing.colors # (2) 导入drawing包中的模块colors
from drawing import shapes # (3) 导入模块shapes
要探索模块,最直接的方式是使用Python解释器进行研究。
首先需要将模块导入。
假设有一个名为 copy 的标准模块。
>>> import copy
没有引发异常,说明确实有这样的模块。
1. 使用 dir
要查明模块包含哪些东西,可使用函数 dir ,它列出对象的所有属性(对于模块,它列出所有的函数、类、变量等)。
在这些名称中,有几个以下划线打头。根据约定,这意味着它们并非供外部使用。
可使用一个简单的列表推导将这些名称过滤掉。
>>> [n for n in dir(copy) if not n.startswith('_')]
['Error', 'PyStringMap', 'copy', 'deepcopy', 'dispatch_table', 'error', 'name', 't', 'weakref']
结果包含 dir(copy) 返回的不以下划线打头的名称。
2. 变量__all__
在 dir(copy) 返回的完整清单中,包含名称__all__ 。
这个变量包含一个列表,它与前面使用列表推导创建的列表类似,但是在模块内部设置的。
>>> copy.__all__['Error', 'copy', 'deepcopy']
这个 all 列表是怎么来的呢?
它是在模块 copy 中像下面这样设置的(这些代码是直接从copy.py复制而来的):
__all__ = ["Error", "copy", "deepcopy"]
为何要提供它呢?
旨在定义模块的公有接口。它告诉解释器从这个模块导入所有的名称意味着什么。
使用如下代码:
from copy import *
将只能得到变量 all 中列出的4个函数。
要导入 PyStringMap ,必须显式地:导入 copy 并使用copy.PyStringMap; 或者使用 from copy import PyStringMap 。
编写模块时,像这样设置 all 也很有用。
如果不设置 all ,则会在以 import * 方式导入时,导入所有不以下划线打头的全局名称。
有一个标准函数可提供你通常需要的所有信息,它就是 help 。
使用它获取有关函数 copy 的信息:
>>> help(copy.copy)
Help on function copy in module copy:
copy(x)
Shallow copy operation on arbitrary Python objects.
See the module's __doc__ string for more info.
帮助信息指出,函数 copy 只接受一个参数 x ,且执行的是浅复制。
在帮助信息中,还提到了模块的__doc__ 字符串。
文档字符串就是在函数开头编写的字符串,用于对函数进行说明,而函数的属性__doc__ 可能包含这个字符串。
模块也可能有文档字符串(它们位于模块的开头),而类也可能如此(位于类的开头)。
前面的帮助信息是从函数 copy 的文档字符串中提取的:
>>> print(copy.copy.__doc__)
Shallow copy operation on arbitrary Python objects.
See the module's __doc__ string for more info.
使用 help 的优点是可获取更多的信息,如函数的特征标(即它接受的参数)。
文档是有关模块信息的自然来源。
如,直接检查 range 这个函数。
>>> print(range.__doc__)
range(stop) -> range object
range(start, stop[, step]) -> range object
Return an object that produces a sequence of integers from start (inclusive)
to stop (exclusive) by step. range(i, j) produces i, i+1, i+2, ..., j-1.
start defaults to 0, and stop is omitted! range(4) produces 0, 1, 2, 3.
These are exactly the valid indices for a list of 4 elements.
When step is given, it specifies the increment (or decrement).
这样就获得了函数 range 的准确描述。
学习Python编程,最有用的文档是“Python库参考手册”,它描述了标准库中的所有模块。
要学习Python,阅读源代码是除动手编写代码外的最佳方式。
源代码在哪里查找呢?假设要阅读标准模块 copy 的代码。
一种办法是像解释器那样通过 sys.path 来查找,但更快捷的方式是查看模块的特性__file__ 。
>>> print(copy.__file__)
C:\Python35\lib\copy.py
如果列出的文件名以.pyc结尾,可打开以.py结尾的相应文件。
警告
在文本编辑器中打开标准库文件时,存在不小心修改它的风险。这可能会破坏文件。
因此关闭文件时,千万不要保存你可能对其所做的修改。
在Python中,短语“开箱即用”(batteries included)最初是由Frank Stajano提出的,指的是Python丰富的标准库。
模块 sys 让你能够访问与Python解释器紧密相关的变量和函数。
模块 fileinput 让你能够轻松地迭代一系列文本文件中的所有行。
在UNIX命令行中:
$ python some_script.py file1.txt file2.txt file3.txt
能够依次迭代文件file1.txt到file3.txt中的行。
还可在UNIX管道中对使用UNIX标准命令cat 提供给标准输入( sys.stdin )的行进行迭代。
$ cat file.txt | python some_script.py
使用模块 fileinput ,在UNIX管道中使用 cat 调用脚本的效果将与以命令行参数的方式向脚本提供文件名一样。
1. 集合
很久以前,集合是由模块 sets 中的 Set 类实现的。
在较新的版本中,集合是由内置类 set 实现的,这意味着你可直接创建集合,而无需导入模块 sets 。
>>> set(range(10))
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
可使用序列(或其他可迭代对象)来创建集合,也可使用花括号显式地指定。
注意,不能仅使用花括号来创建空集合,因为这将创建一个空字典。
>>> type({})
<class 'dict'>
相反,必须在不提供任何参数的情况下调用 set 。
集合主要用于成员资格检查,因此将忽略重复的元素:
>>> {0, 1, 2, 3, 0, 1, 2, 3, 4, 5}
{0, 1, 2, 3, 4, 5}
字典一样,集合中元素的排列顺序是不确定的,因此不能依赖于这一点。
>>> {'fee', 'fie', 'foe'}
{'foe', 'fee', 'fie'}
除成员资格检查外,还可执行各种标准集合操作,如并集和交集,为此可使用对整数执行按位操作的运算符。
例如,
要计算两个集合的并集,可对其中一个集合调用方法 union ,也可使用按位或运算符 | 。
>>> a = {1, 2, 3}
>>> b = {2, 3, 4}
>>> a.union(b)
{1, 2, 3, 4}
>>> a | b
{1, 2, 3, 4}
还有其他一些方法和对应的运算符,这些方法的名称清楚地指出了其功能:
>>> c = a & b
>>> c.issubset(a)
True
>>> c <= a
True
>>> c.issuperset(a)
False
>>> c >= a
False
>>> a.intersection(b)
{2, 3}
>>> a & b
{2, 3}
>>> a.difference(b)
{1}
>>> a - b
{1}
>>> a.symmetric_difference(b)
{1, 4}
>>> a ^ b
{1, 4}
>>> a.copy()
{1, 2, 3}
>>> a.copy() is a
False
另外,还有对应于各种就地操作的方法以及基本方法 add 和 remove 。
集合是可变的,因此不能用作字典中的键。
集合只能包含不可变(可散列)的值,因此不能包含其他集合。
frozenset 类型,它表示不可变(可散列)的集合。
>>> a = set()
>>> b = set()
>>> a.add(b)
Traceback (most recent call last):
File "" , line 1, in ?
TypeError: set objects are unhashable
>>> a.add(frozenset(b))
构造函数 frozenset 创建给定集合的副本。
2. 堆
堆(heap),它是一种优先队列。
优先队列让你能够以任意顺序添加对象,并随时(可能是在两次添加对象之间)找出(并删除)最小的元素。
只有一个包含一些堆操作函数的模块。这个模块名为heapq (其中的 q 表示队列),它包含6个函数,其中前4个与堆操作直接相关。必须使用列表来表示堆对象本身。
注意,不能将它用于普通列表,而只能用于使用各种堆函数创建的列表。
原因是元素的顺序很重要(虽然元素的排列顺序看起来有点随意,并没有严格地排序)。
>>> from heapq import *
>>> from random import shuffle
>>> data = list(range(10))
>>> shuffle(data)
>>> heap = []
>>> for n in data:
... heappush(heap, n)
...
>>> heap
[0, 1, 3, 6, 2, 8, 4, 7, 9, 5]
>>> heappush(heap, 0.5)
>>> heap
[0, 0.5, 3, 6, 1, 8, 4, 7, 9, 5, 2]
必须保证一点:位置 i 处的元素总是大于位置 i // 2 处的元素(反过来说就是小于位置 2 * i 和 2 * i + 1 处的元素)。
这是底层堆算法的基础,称为堆特征(heap property)。
>>> heappop(heap)
0
>>> heappop(heap)
0.5
>>> heappop(heap)
1
>>> heap
[2, 5, 3, 6, 9, 8, 4, 7]
>>> heap = [5, 8, 0, 3, 6, 7, 9, 1, 4, 2]
>>> heapify(heap)
>>> heap
[0, 1, 5, 3, 2, 7, 9, 8, 4, 6]
>>> heapreplace(heap, 0.5)
0
>>> heap
[0.5, 1, 5, 3, 2, 7, 9, 8, 4, 6]
>>> heapreplace(heap, 10)
0.5
>>> heap
[1, 2, 5, 3, 6, 7, 9, 8, 4, 10]
3. 双端队列(及其他集合)
在需要按添加元素的顺序进行删除时,双端队列很有用。
在模块 collections 中,包含类型deque 以及其他几个集合(collection)类型。
与集合(set)一样,双端队列也是从可迭代对象创建的,它包含多个很有用的方法。
>>> from collections import deque
>>> q = deque(range(5))
>>> q.append(5)
>>> q.appendleft(6)
>>> q
deque([6, 0, 1, 2, 3, 4, 5])
>>> q.pop()
5
>>> q.popleft()
6
>>> q.rotate(3)
>>> q
deque([2, 3, 4, 0, 1])
>>> q.rotate(-1)
>>> q
deque([3, 4, 0, 1, 2])
双端队列很有用,因为它支持在队首(左端)高效地附加和弹出元素,而使用列表无法这样做。
还可高效地旋转元素(将元素向右或向左移,并在到达一端时环绕到另一端)。
双端队列对象还包含方法 extend 和 extendleft ,其中 extend 类似于相应的列表方法,而 extendleft 类似于
appendleft 。
注意,用于 extendleft 的可迭代对象中的元素将按相反的顺序出现在双端队列中。
模块 time 包含用于获取当前时间、操作时间和日期、从字符串中读取日期、将日期格式化为字符串的函数。
秒的取值范围为0~61,这考虑到了闰一秒和闰两秒的情况。夏令时数字是一个布尔值( True或 False ),但如果你使用 -1 ,那么 mktime [将时间元组转换为时间戳(从新纪元开始后的秒数)的函数]可能得到正确的值。
还有两个较新的与时间相关的模块: datetime 和 timeit 。
datetime 提供了日期和时间算术支持,而 timeit 可帮助你计算代码段的执行时间。
模块 random 包含生成伪随机数的函数,有助于编写模拟程序或生成随机输出的程序。
要求真正的随机(如用于加密或实现与安全相关的功能),应考虑使用模块 os 中的函数 urandom 。
模块 random中的 SystemRandom 类基于的功能与 urandom 类似,可提供接近于真正随机的数据。
函数 random.random 是最基本的随机函数之一,它返回一个0~1(含)的伪随机数。
函数 random.getrandbits 以一个整数的方式返回指定数量的二进制位。
向函数 random.uniform 提供了两个数字参数 a 和 b 时,它返回一个 a ~ b (含)的随机(均匀分布的)实数。
函数 random.randrange 是生成随机整数的标准函数。为指定这个随机整数所在的范围,可像调用 range 那样给这个函数提供参数。
函数 random.choice 从给定序列中随机(均匀)地选择一个元素。
函数 random.shuffle 随机地打乱一个可变序列中的元素,并确保每种可能的排列顺序出现的概率相同。
函数 random.sample 从给定序列中随机(均匀)地选择指定数量的元素,并确保所选择元素的值各不相同。
简单的存储方案,模块 shelve 可完成大部分工作——只需提供一个文件名即可。
对于模块 shelve ,函数open 将一个文件名作为参数,并返回一个 Shelf 对象,用来存储数据。
可像操作普通字典那样操作它(只是键必须为字符串),操作完毕(并将所做的修改存盘)时,可调用其方法 close 。
1. 一个潜在的陷阱
认识到 shelve.open 返回的对象并非普通映射,如下例所示:
>>> import shelve
>>> s = shelve.open('test.dat')
>>> s['x'] = ['a', 'b', 'c']
>>> s['x'].append('d')
>>> s['x']
['a', 'b', 'c']
‘d’ 到哪里去了呢?
当你查看 shelf 对象中的元素时,将使用存储版重建该对象,而当你将一个元素赋给键时,该元素将被存储。
列表 ['a', 'b', 'c'] 被存储到 s 的 'x' 键下。
获取存储的表示,并使用它创建一个新列表,再将 'd' 附加到这个新列表末尾,但这个修改后的版本未被存储!
最后,再次获取原来的版本——其中没有 'd' 。
要正确地修改使用模块 shelve 存储的对象,必须将获取的副本赋给一个临时变量,并在修改这个副本后再次存储 :
>>> temp = s['x']
>>> temp.append('d')
>>> s['x'] = temp
>>> s['x']
['a', 'b', 'c', 'd']
另一种避免这个问题的办法:将函数 open 的参数 writeback 设置为 True 。
2. 一个简单的数据库示例
下述代码是一个使用模块 shelve 的简单数据库应用程序。
# database.py
import sys, shelve
def store_person(db):
"""
让用户输入数据并将其存储到shelf对象中
"""
pid = input('Enter unique ID number: ')
person = {}
person['name'] = input('Enter name: ')
person['age'] = input('Enter age: ')
person['phone'] = input('Enter phone number: ')
db[pid] = person
def lookup_person(db):
"""
让用户输入ID和所需的字段,并从shelf对象中获取相应的数据
"""
pid = input('Enter ID number: ')
field = input('What would you like to know? (name, age, phone) ')
field = field.strip().lower()
print(field.capitalize() + ':', db[pid][field])
def print_help():
print('The available commands are:')
print('store : Stores information about a person')
print('lookup : Looks up a person from ID number')
print('quit : Save changes and exit')
print('? : Prints this message')
def enter_command():
cmd = input('Enter command (? for help): ')
cmd = cmd.strip().lower()
return cmd
def main():
database = shelve.open('C:\\database.dat') # 你可能想修改这个名称
try:
while True:
cmd = enter_command()
if cmd == 'store':
store_person(database)
elif cmd == 'lookup':
lookup_person(database)
elif cmd == '?':
print_help()
elif cmd == 'quit':
return
finally:
database.close()
if name == '__main__': main()
所有代码都放在函数中,这提高了程序的结构化程度(一个可能的改进是将这些函数作为一个类的方法)。
主程序位于函数 main 中,这个函数仅在 name== ‘main’ 时才会被调用。这意味着可在另一个程序中将这个程序作为模块导入,再调用函数 main 。
在函数 main 中,打开一个数据库(shelf),再将其作为参数传递给其他需要它的函数。
读入一些值后,调用 strip 和 lower 来修改它们,因为仅当提供的键与存储的键完全相同时,它们才匹配。
为确保数据库得以妥善的关闭,使用了 try 和 finally 。
下面是一个示例交互过程:
Enter command (? for help): ?
The available commands are:
store : Stores information about a person
lookup : Looks up a person from ID number
quit : Save changes and exit
? : Prints this message
Enter command (? for help): store
Enter unique ID number: 001
Enter name: Mr. Gumby
Enter age: 42
Enter phone number: 555-1234
Enter command (? for help): lookup
Enter ID number: 001
What would you like to know? (name, age, phone) phone
Phone: 555-1234
Enter command (? for help): quit
退出这个程序后,来看看再次运行它时(这也许是在第二天)发生的情况。
Enter command (? for help): lookup
Enter ID number: 001
What would you like to know? (name, age, phone) name
Name: Mr. Gumby
Enter command (? for help): quit
这个程序读取前面运行它时创建的文件,该文件依然包含Mr. Gumby!
模块 re 提供了对正则表达式的支持。
1. 正则表达式是什么
正则表达式是可匹配文本片段的模式。最简单的正则表达式为普通字符串,与它自己匹配。
换而言之,正则表达式 ‘python’ 与字符串 ‘python’ 匹配。
可使用这种匹配行为来完成如下工作:
在文本中查找模式,将特定的模式替换为计算得到的值,以及将文本分割成片段。
通配符
正则表达式可与多个字符串匹配,你可使用特殊字符来创建这种正则表达式。
句点与除换行符外的任何字符都匹配,因此被称为通配符(wildcard)。
对特殊字符进行转义
普通字符只与自己匹配,但特殊字符的情况完全不同。
要让特殊字符的行为与普通字符一样,可对其进行转义:在它前面加上一个反斜杠。
注意,为表示模块 re 要求的单个反斜杠,需要在字符串中书写两个反斜杠,让解释器对其进行转义。
这里包含两层转义:解释器执行的转义和模块 re 执行的转义。
字符集
匹配任何字符很有用,但有时你需要更细致地控制。
可以用方括号将一个子串括起,创建一个所谓的字符集。
注意,字符集只能匹配一个字符。
要指定排除字符集,可在开头添加一个 ^ 字符,例如 ‘[^abc]’ 与除a、b和c外的其他任何字符都匹配。
二选一和子模式
使用字符集或通配符无法指定这样的模式,而必须使用表示二选一的特殊字符:管道字符( | )。
所需的模式为 ‘python|perl’ 。
不想将二选一运算符用于整个模式,而只想将其用于模式的一部分。为此,可将这部分(子模式)放在圆括号内。
注意,单个字符也可称为子模式。
可选模式和重复模式
通过在子模式后面加上问号,可将其指定为可选的,即可包含可不包含。
注意: 在这里,术语 匹配指的是与整个字符串匹配,而函数 match 只要求模式与字符串开头匹配。
字符串的开头和末尾
想确定字符串的开头是否与模式匹配,为此可使用脱字符( ’ ^ ’ )来指出这一点。
要指定字符串末尾,可使用美元符号( $ )。
2. 模块 re 的内容
模块 re 包含多个使用正则表达式的函数。
函数 re.compile 将用字符串表示的正则表达式转换为模式对象,以提高匹配效率。
函数 re.search 在给定字符串中查找第一个与指定正则表达式匹配的子串。
如果找到这样的子串,将返回 MatchObject (结果为真),否则返回 None (结果为假)。
函数 re.match 尝试在给定字符串开头查找与正则表达式匹配的子串,因此 re.match(‘p’,‘python’) 返回真( MatchObject ),而 re.match(‘p’, ‘www.python.org’) 返回假( None )。
函数 re.split 根据与模式匹配的子串来分割字符串。
这类似于字符串方法 split ,但使用正则表达式来指定分隔符,而不是指定固定的分隔符。
函数 re.findall 返回一个列表,其中包含所有与给定模式匹配的子串。
函数 re.sub 从左往右将与模式匹配的子串替换为指定内容。
re.escape 是一个工具函数,用于对字符串中所有可能被视为正则表达式运算符的字符进行转义。
下面的示例说明了这个函数的工作原理:
>>> re.escape('www.python.org')
'www\\.python\\.org'
>>> re.escape('But where is the ambiguity?')
'But\\ where\\ is\\ the\\ ambiguity\\?'
3. 匹配对象和编组
在模块 re 中,查找与模式匹配的子串的函数都在找到时返回 MatchObject 对象。
这种对象包含与模式匹配的子串的信息,还包含模式的哪部分与子串的哪部分匹配的信息。
这些子串部分称为编组(group)。
编组就是放在圆括号内的子模式,它们是根据左边的括号数编号的,其中编组0指的是整个模式。
因此,在下面的模式中:
'There (was a (wee) (cooper)) who (lived in Fyfe)'
包含如下编组:
0 There was a wee cooper who lived in Fyfe
1 was a wee cooper
2 wee
3 cooper
4 lived in Fyfe
re 匹配对象的一些重要方法。
方法 group 返回与模式中给定编组匹配的子串。如果没有指定编组号,则默认为0。
如果只指定了一个编组号(或使用默认值0),将只返回一个字符串;否则返回一个元组,其中包含与给定编组匹配的子串。
方法 start 返回与给定编组(默认为0,即整个模式)匹配的子串的起始索引。
方法 end 类似于 start ,但返回终止索引加1。
方法 span 返回一个元组,其中包含与给定编组(默认为0,即整个模式)匹配的子串的起始索引和终止索引。
这些方法的工作原理:
>>> m = re.match(r'www\.(.*)\..{3}', 'www.python.org')
>>> m.group(1)
'python'
>>> m.start(1)
4
>>> m.end(1)
10
>>> m.span(1)
(4, 10)
4. 替换中的组号和函数
为利用 re.sub 的强大功能,最简单的方式是在替代字符串中使用组号。
在替换字符串中,任何类似于 ‘\n’ 的转义序列都将被替换为与模式中编组 n 匹配的字符串。
例如,假设要将’*something*’ 替换为 ‘something’ ,其中前者是在纯文本文档(如电子邮件)中表示突出的普通方式,而后者是相应的HTML代码(用于网页中)。
下面先来创建一个正则表达式。
>>> emphasis_pattern = r'\*([^\*]+)\*'
创建模式后,就可使用 re.sub 来完成所需的替换了。
>>> re.sub(emphasis_pattern, r'\1', 'Hello, *world*!')
'Hello, world!'
通过将函数用作替换内容,可执行更复杂的替换。
这个函数将 MatchObject 作为唯一的参数,它返回的字符串将用作替换内容。
5. 找出发件人
包含发件人的文本行以 'From: ’ 打头,并以包含在尖括号( < 和 > )内的邮件地址结尾,要提取这两部分之间的文本。
使用模块 fileinput 。
找出发件人的程序。
# find_sender.py
import fileinput, re
pat = re.compile('From: (.*) <.*?>$')
for line in fileinput.input():
m = pat.match(line)
if m: print(m.group(1))
可像下面这样运行这个程序(假设电子邮件保存在文本文件message.eml中):
$ python find_sender.py message.eml
Foo Fie
对于这个程序,应注意如下几点。
为提高处理效率,编译了正则表达式。
将用于匹配要提取文本的子模式放在圆括号内,使其变成了一个编组。
使用了一个非贪婪模式,使其只匹配最后一对尖括号(以防姓名也包含尖括号)。
使用了美元符号指出要使用这个模式来匹配整行(直到行尾)。
使用了 if 语句来确保匹配后才提取与特定编组匹配的内容。
要列出邮件头中提及的所有邮件地址,需要创建一个只与邮件地址匹配的正则表达式,然后使用方法 findall 找出所有与之匹配的内容。
最后,提取键,将它们排序并打印出来。
import fileinput, re
pat = re.compile(r'[a-z\-\.]+@[a-z\-\.]+', re.IGNORECASE)
addresses = set()
for line in fileinput.input():
for address in pat.findall(line):
addresses.add(address)
for address in sorted(addresses):
print address
6. 模板系统示例
模板(template)是一种文件,可在其中插入具体的值来得到最终的文本。
Python提供了一种高级模板机制:字符串格式设置。
例如,可能有一个只需插入收件人姓名的邮件模板。
假设要把所有的 ‘[something]’ (字段)都替换为将 something 作为Python表达式计算得到的结果。
因此,下面的字符串:
'The sum of 7 and 9 is [7 + 9].'
应转换为:
'The sum of 7 and 9 is 16.'
还希望能够在字段中进行赋值,使得下面的字符串:
'[name="Mr. Gumby"]Hello, [name]'
转换成:
'Hello, Mr. Gumby'
可供使用的工具。
可使用正则表达式来匹配字段并提取其内容。
可使用 eval 来计算表达式字符串,并提供包含作用域的字典。可在 try / except 语句中执行这种操作。
可使用 exec 来执行语句字符串(和其他语句),并将模板的作用域存储到字典中。
可使用 re.sub 将被处理的字符串替换为计算得到的结果。
一个模板系统:
# templates.py
import fileinput, re
# 与使用方括号括起的字段匹配
field_pat = re.compile(r'\[(.+?)\]')
# 我们将把变量收集到这里:
scope = {}
# 用于调用re.sub:
def replacement(match):
code = match.group(1)
try:
# 如果字段为表达式,就返回其结果:
return str(eval(code, scope))
except SyntaxError:
# 否则在当前作用域内执行该赋值语句
# 并返回一个空字符串
return ''
# 获取所有文本并合并成一个字符串:
#(还可采用其他办法来完成这项任务,详情请参见第11章)
lines = []
for line in fileinput.input():
lines.append(line)
text = ''.join(lines)
# 替换所有与字段模式匹配的内容:
print(field_pat.sub(replacement, text))
这个程序做了如下事情。
定义一个用于匹配字段的模式。
创建一个用作模板作用域的字典。
定义一个替换函数,其功能如下。
从 match 中获取与编组1匹配的内容,并将其存储到变量 code 中。
将作用域字典作为命名空间,并尝试计算 code ,再将结果转换为字符串并返回它。
在对表达式进行求值时使用的命名空间(作用域字典)中执行这个字段,并返回一个空字符串(因为赋值语句没有结果)。
使用 fileinput 读取所有的行,将它们放在一个列表中,再将其合并成一个大型字符串。
调用 re.sub 来使用替换函数来替换所有与模式 field_pat 匹配的字段,并将结果打印出来。
测试一下这个模板系统:
[x = 2]
[y = 3]
The sum of [x] and [y] is [x + y].
看到如下输出:
The sum of 2 and 3 is 5.
argparse : 在UNIX中,运行命令行程序时常常需要指定各种选项(开关),Python解释器
就是这样的典范。
cmd : 这个模块让你能够编写类似于Python交互式解释器的命令行解释器。
csv : CSV指的是逗号分隔的值(comma-seperated values),很多应用程序(如很多电子表格程序和数据库程序)都使用这种简单格式来存储表格数据。这种格式主要用于在不同
的程序之间交换数据。
datetime : datetime 支持特殊的日期和时间对象,并让你能够以各种方式创建和合并这些对象。相比于模块 time ,模块 datetime 的接口在很多方面都更加直观。
difflib :这个库让你能够确定两个序列的相似程度,还让你能够从很多序列中找出与指定序列最为相似的序列。
enum : 枚举类型是一种只有少数几个可能取值的类型。
functools : 这个模块提供的功能是,让你能够在调用函数时只提供部分参数(部分求值,partial evaluation),以后再填充其他的参数。
hashlib : 使用这个模块可计算字符串的小型“签名”(数)。计算两个不同字符串的签名时,几乎可以肯定得到的两个签名是不同的。
itertools : 包含大量用于创建和合并迭代器(或其他可迭代对象)的工具,其中包括可以串接可迭代对象、创建返回无限连续整数的迭代器(类似于 range ,但没有上限)、反复遍历可迭代对象以及具有其他作用的函数。
logging : 使用 print 语句来确定程序中发生的情况很有用。要避免跟踪时出现大量调试输出,可将这些信息写入日志文件中。这个模块提供了一系列标准工具,可用于管理一个或多个中央日志,它还支持多种优先级不同的日志消息。
statistics : 计算一组数的平均值并不那么难,但是要正确地获得中位数,以确定总体标准偏差和样本标准偏差之间的差别。
timeit 、 profile 和 trace : 模块 timeit (和配套的命令行脚本)是一个测量代码段执行时间的工具。
度量性能时你可能应该使用模块 timeit 而不是模块 time 。
模块profile (和配套模块 pstats )可用于对代码段的效率进行更全面的分析。
模块 trace 可帮助你进行覆盖率分析(即代码的哪些部分执行了,哪些部分没有执行),这在编写测试代码时很有用。
学习参考资料:
《Python基础教程》 第3版