希望通过博客园持续的更新,分享和记录Python基础知识到高级应用的点点滴滴!
第十波:第10章 充电时刻
Python语言的核心非常强大,同时还提供了更多值得一试的工具。Python的标准安装包括一组模块,称为标准库standard library、展示这些模块的工作方式,讨论如何分析它们,学习它们所提供的功能。
[10.1] 模块
已经知道如何创建和执行自己的程序,也学会了怎么用import从外部模块获取函数并且为自己的程序使用。接下来看看怎么编写自己的模块。
[10.1.1] 模块是程序
任何Python程序都可以作为模块导入。程序的名字很重要,保存的位置也很重要。在导入模块的时候,可能会看到有新文件出现---这个以.pyc为扩展名的文件时经过编译的,已经转换成Python能够更加有效地处理的文件。如果稍后导入同一个模块,python会导入.pyc文件而不是.py文件,除非.py文件已经改变---在这种情况下,会生成新的.pyc文件。删除.pyc文件不会顺海程序---必要的时候会创建新的.pyc文件。
如你所见,在导入模块的时候,其中的代码被执行了。不过如果再次导入模块,就什么都不会发生了:
>>>import hello
>>>hello world!
>>>import hello
>>>
为什么这次没用了呢?因为导入模块并不意味着在导入时执行某些操作。它们主要用于定义,比如变量、函数和类等。此外,因为只需要定义这些东西一次,导入模块多次和导入一次的效果是一样的。
[10.1.2] 模块用于定义
模块在第一次导入到程序中时被执行。真正的用处在于他们(像类一样)可以保持自己的作用域。这就意味着定义的所有类和函数以及赋值后的变量都称为模块的特性。这看起来挺复杂得,用起来却很简单。
1. 在模块中定义函数
假设我们编写了一个模块,并且将它存储为hello2.py文件。可以像下面这样导入:
>>>import hello2
模块会被执行,这意味着hello函数在模块的作用域内被定义了。因此可以通过以下方式来访问函数:
>>>hello2.hello()
hello world!
可以通过同样的方法来使用任何在模块的全局作用域中定义的名称。
为什么要这样做呢?为什么不在主程序中定义好一切呢?主要原因四代码重用code reuse。如果把代码放在模块中,就可以再多个程序中使用这些代码。为了让代码可重用,请将它模块化!
2. 在模块中增加测试代码
模块用来定义函数、类和其他一些内容,但是有些时候,在模块中添加一些检查模块本身是否正常工作的测试代码是很有用的。 举例来说,假如想要确保hello函数工作正常,你可能会将hello2模块重写为新的模块,如下代码:
#hello3.py
def hello():
print "hello,world!"
# A test
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函数中,也可以直接将它们放入if语句。但是,将测试代码放入独立的test函数会更灵活,这样做既是在把模块导入其他程序之后,仍然可以对其进行测试。如果需要编写完整的测试代码,将其放置在单独的程序中会更好。
[10.1.3] 让你的模块可用
在理想的情况下,一开始sys.path本身就应该包含正确的目录(包括你的模块的目录)。有两种方法可以做到这一点:一是将模块放置在合适的位置,另外则是告诉解释器去哪里查找需要的模块。
1. 将模块放置在正确位置
将你的模块放置在正确的位置是很容易的。只需要找出python解释器从哪里查找模块,然后将你的文件放置在那里即可。
如果机器上的python解释器是由管理员安装的,而你又没有管理员权限,可能无法将你的模块存储在python使用的目录中。这种情况下,需要使用另外一个解决方案:告诉解释器去哪里查找。
那些(称为搜索路径的)目录的列表可以在sys模块中的path变量中找到:
>>>import sys,pprint
>>>pprint.pprint(sys.path)
['C:\\Python\\Lib\\idlelib',
...,
...,
'C:\\Python\\lib\\site-packages']
提示:如果你的数据结构过大,不能在一行打印完。可以使用pprint模块中的pprint函数替代普通的print语句。pprint是个相当好的打印函数,能够提供更加智能的打印输出。
每个字符串都提供了一个放置模块的目录,解释器可以从这些目录中找到所需要的模块。尽管这些目录都可以使用,但是site-packages目录是最佳选择,因为它就是用来做这些事情的。查看你的sys.path,找到site-packages目录。只要将模块放入类似site-packages目录中,所有程序就都能将其导入了。
2. 告诉编辑器去哪里找
标准的实现方法是在PYTHONPATH环境变量中包含模块所在的目录。PYTHONPATH环境变量的内容会因为使用的操作系统不同而有差异,但从基本上来说,它于sys.path很类似---是一个目录列表。
环境变量并不是Python解释器的一部分---它们是操作系统的一部分,基本上,它相当于python变量,不过是在python解释器外设置的。在UNIX和MACOX中,可以在主目录中找到它,将下面命令添加到这个文件中,从而将~/python加入到PYTHONPATH:
export PYTHONPATH=$PYTHONPATH:~/python
注意,多个路径以冒号分隔。在windows系统中,开始菜单>设置>控制面板>双击系统图标。在“高级”选项卡,点击“环境变量”按钮,其中一栏是用户变量,另外一栏是系统变量。需要修改的是用户变量,“编辑”按钮进行编辑PYTHONPATH。
3. 命名模块
包含模块代码的文件的名字要和模块名一样。再加上.py扩展名。
[10.1.4] 包
为了组织好模块,你可以讲它们分组为包package。包基本上就是另外一类模块,有趣的地方就是它们能包含其他模块。当模块存储在文件中时,包就是模块所在的目录。为了让python将其作为包对待,它必须包含一个命名为__init__.py的文件。如果将它作为普通模块导入的话,文件的内容就是包的内容。比如有个名为constants的包,文件constants/__init__.py包括语句PI=3.14,可以像下面这么做:
import constants
print constants.PI
为了将模块放置在包内,直接把模块放在包目录内即可。
比如,如果要建立一个叫做drawing的包,其中包括名为shapes和colors的模块,就需要创建如下所示的文件和目录:
~/python/ PYTHONPATH中的目录
~/python/drawing/ 包目录(drawing包)
~/python/drawing/__init__.py 包代码(drawing模块)
~/python/drawing/colors.py colors模块
~/python/drawing/shapes.py shapes模块
假定已经将目录~/python放置在PYTHONPATH中。依照这个设置,下面的语句都是合法的:
import drawing # imports the drawing package
import drawing.colors # import the colors module
from drawing import shapes # import the shapes module
在第1条语句drawing中__init__模块的内容是可用的,但drawing和colors模块则不可用。在执行第2条语句之后,colors模块可用了,但只能通过全名drawing.colors来使用。在执行第3条语句之后,shapes模块可用,可以通过短名(也就是仅使用shapes)来使用。
[10.2] 探究模块
在讲述标准库模块前,先学会如何独立地探究模块。这种技能极有价值。
[10.2.1] 模块中有什么
探究模块最直接的方式就是在python解释器中研究他们。当然,要做的第一件事就是导入它。
>>>import copy
没有引发异常---所以它是存在的,但是它能做什么?它又有什么?
1. 使用dir
查看模块包含的内容可以使用dir函数,它会将对象(以及模块的所有函数、类、变量等)的所有特性列出。例如打印出dir(copy)的内容,一些名字以下划线开始---暗示(约定俗成)它们并不是为在模块外部使用而准备的。
>>>[n for n in dir(copy) if not n.startswith('_')]
2. __all__变量
可以列表本身获得正确答案。在完整的dir(copy)列表中,可能注意到了__all__这个名字。这个变量包含一个列表,这个列表在模块本身中被设置。
>>>copy.__all__
['Error','copy','deepcopy']
__all__列表从哪来,它为什么会在那?第一个问题容易回答,它是在copy模块内部被设置的,像下面这样:
__all__=['Error','copy','deepcopy']
那么它为什么在那呢?它定义了模块的共有接口(public interface)。更准确地说,它告诉解释器:从模块导入所有名字表示什么含义。因此,如果你使用如下代码:
from copy import *
那么你只能使用__all__变量中的3个函数。要导入PyStringMap的话,你就得显式的实现,或者导入copy然后使用copy.PyStringMap,或者使用from copy import PyStringMap。
在编写模块的时候,像设置__all__这样的技术是相当有用的。因为模块中可能会有一大堆其他程序不需要或不想要的变量、函数和类,__all__会“客气地”将它们过滤了出去。如果没有设定__all__,用import *语句默认将会输出模块中所有不以下划线开头的全局名称。
[10.2.2] 用help获取帮助
对语言的精通程度决定了对模块探究的深度。还有个标注函数能够为你提供日常所需的信息,这个函数叫做help。
>>>help(copy.copy)
help in 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__文档字符串,它是写在函数开头并且简述函数功能的字符串。这个字符串可以通过函数的__doc__特性引用。模块也可以有文档字符串(写在模块开头),类也一样(写在类开头)。
事实上前面的帮助文本是从copy函数的文档字符串中取出的:
>>>print copy.copy.__doc__
使用help与直接检查文档字符串相比,它的好处在于会获得更多信息,比如函数签名(也就是所带的参数)。试着调用help(copy)会打印出很多信息,包括copy和deepcopy之间区别的透彻的讨论(从本质来说,deepcopy(x)会将存储在x中的值作为属性进行复制,而copy(x)只是复制x,将x中的值绑定到副本的属性上)。
[10.2.3] 文档
模块信息的自然来源当然是文档。可以直接查看:
>>>print range.__doc__
这样就获得了关于range函数的精确描述。有些时候需要十分透彻地描述这些模块和函数是如何工作的。所有的这些文档都可以在Python网站(http://python.org/doc)找到。
[10.2.4] 使用源代码
阅读源代码,是学习Python最好的方式。但问题在于源代码在哪里呢?一种方案是检查sys.path,然后自己找。另外一种快捷的方法是检查模块的__file__属性:
>>>print copy.__file__
但注意一些模块并不包含任何可以阅读的Python源代码。它们可能已经融入到解释器内了(比如sys模块),或者可能是C程序语言携程的。
[10.3] 标准库:一些最爱
“充电时刻”用于描述Python丰富的标准库。模块的描述并不完全,但是会强调每个模块比较有趣的特征。
[10.3.1] sys
sys模块让你能够访问与Python解释器联系紧密的变量和函数。如下是sys模块中一些重要的函数和变量:
argv 命令行参数,包括脚本名称
exit([arg] 退出当前的程序,可选参数为给定的返回值或者错误信息
modules 映射模块名字到载入模块的字典
path 查找模块所在目录的目录名列表
platform 类似sunos5或者win32的平台标识符
stdin 标准输入流---一个类文件file-like对象
stout 标准输出流---一个类文件对象
stderr 标准错误流---一个类文件对象
变量sys.argv包括传递到python解释器的参数,包括脚本名称。
函数sys.exit可以退出当前程序。可以提供一个整数作为参数,用来标识程序是否成功运行。或者可以提供字符串参数,用作错误信息,对于用户找出程序停止运行的原因会很有用。这样,程序就会在退出的时候提供错误信息和标识程序运行失败的代码。
映射sys.modules将模块名映射到实际存在的模块上,它只应用于目前导入的模块。
sys.path是一个字符串列表,其中的每个字符串都是一个目录名。在import语句执行时,解释器就会从这些目录中查找模块。
sys.platform模块变量是解释器正在其上运行的平台的名称。
sys.stdin、sys.stdout和sys.stderr模块变量是类文件流对象。它们表示标准UNIX概念中的标准输入、标准输出和标准错误。简单来说,Python利用sys.stdin获得输入(比如用于函数input和raw_input中的输入),利用sys.stdout输出。
举例来说,思考下反序打印参数的问题。当你通过命令行调用Python脚本时,可能会在后面加上一些参数---这就是命令行参数。这些参数会放置在sys.argv列表中,脚本的名字为sys.argv[0]。反序打印这些参数如下:
# reverseargs.py
import sys
args = sys.argv[1:]
args.reverse()
print ' '.join(args)
对sys.argv进行了复制。可以修改原始的列表,但是这样做通常是不安全的,因为程序的其他部分可能也需要原始参数的sys.argv。跳过了sys.argv的第一个元素---脚本的名字。使用args.reverse()方法对列表进行反向排序,但是不能打印出这个操作的结果---这是个返回None的原地修改操作。下面是另外一种做法:
print ' '.join(reversed(sys.argvp[1:]))
[10.3.2] OS
OS模块为你提供了访问多个操作系统服务的功能。os模块包括的内容很多。os和它的子模块os.path还包括一些用于检查、构造、删除目录和文件的函数,以及一些处理路径的函数。
environ 对环境变量进行映射;
system 在子shell中执行操作系统命令
sep 路径中的分割符
pathsep 分隔路径的分隔符
linesep 行分隔符('\n','\r','\r\n')
urandom(n) 返回n字节的加密强随机数据
os.environ映射包含环境变量。比如要访问系统变量PYTHONPATH,可以使用表达式os.environ['PYTHONPATH']。这个映射也可以用来更高系统环境变量,不过并非所有系统都支持。
os.stem函数用户运行外部程序。
os.sep模块变量是用于路径名中的分隔符。
模块变量os.linesep用于文本文件的字符串分隔符。
urandom函数使用一个依赖于系统的随机数的源。
windows特有的函数---os.startfile。接受一般路径,就算包含空格也没有问题。
[10.3.3] fileinput
fileinput模块让你能够轻松地遍历文本文件的所有行。如果通过以下方式调用脚本:
$python some_script.py file1.txt file2.txt file3.txt
这样可以依次对file1到file3文件中的所有行进行遍历。比如在UNIX的管道中,使用标准的UNIX命令cat:
$cat file.txt | python some_script.py
fileinput.input是其中最重要的函数。它会返回能够用于for循环遍历的对象。如果不想使用默认行为(fileinput查找需要循环遍历的文件),那么可以给函数提供(序列形式的)一个或多个文件名。还能降inplace参数设置为真值以进行原地处理。对于要访问的每一行,需要打印出替代的内容,以返回到当前的输入文件中。在进行原地处理的时候,可选的backup参数将文件名扩展备份到通过原始文件创建的备份文件中。
input([files[,inplace[,backup]]) 便于遍历多个输入流中的行
filename() 返回当前文件的名称
lineno() 返回当前(累计)的行数
filelineno() 返回当前文件的行数
isfirstline() 检查当前行是否是文件的第一行
isstdin() 检查最后一行是否来自sys.stdin
nextfile() 关闭当前文件,移动到下一个文件
close() 关闭序列
fileinput.filename函数返回当前正在处理的文件名。
fileinput.lineno返回当前的行数,这个数值是累计的,所以在完成一个文件的处理并且开始处理下一个文件的时候,行数并不会重置,而是将上一个文件的最后行数加1作为计数的起始。
fileinput.filelineno函数返回当前处理文件的当前行数。每次处理完一个文件并且开始处理下一个文件时,行数都会重置为1,然后重新开始计数。
fileinput.isfirstline函数在当前行是当前文件的第一行时返回真值,反之返回假值。
fileinput.isstdin函数在当前文件为sys.stdin时返回真值,否则返回假值。
fileinput.nextfile函数会关闭当前文件,跳到下一个文件,跳过的行并不计。
fileinput.close函数关闭整个文件链,结束迭代。
# numberlines.py
import fileinput
for line in fileinput.input(inplace=True):
line=line.rstrip()
num=fileinput.lineno()
print '%-40s # %2i' % (line,num)
注意,要小心使用inplace参数---它很容易破坏文件。
[10.3.4] 集合、堆和双端队列
在计算机编程当中,我们可以看到很多有用的数据结构,而python支持其中相对通用的类型。例如字典(或者说散列表)、列表(或者说动态数组)是语言必不可少的一部分。其他一些数据结构尽管不是那么重要,但有些时候也能派上用场。
1. 集合
集合set在,Set类位于sets模块中。集合通过set类型的实现称为了语言的一部分,这意味着不需要导入sets模块,就可以直接创建集合。如set(range(10))。集合是由序列或者其他可迭代的对象构建的,主要用于检查成员资格,因此副本是被忽略的。和字典一样,集合元素的顺序是随意的,因此不应该以元素的顺序作为依据进行编程。
除了检查成员资格外,还可以使用标准的集合操作,比如求并集和交集。比如想要找出两个集合并集,可以使用其中一个集合的union方法或者使用按位与运算符“|”:
a=set([1,2,3])
b=set([2,3,4])
>>>a.union(b)
>>>a|b
set([1,2,3,4])
以下列出了一些其他方法和对应的运算符,方法的名称已经清楚地表明了其用途:
&、<=、>=、-、^、
如果需要一个函数,用于查找并且打印两个集合的并集,可以使用来自set类型的union方法的未绑定版本,这种做法很有用,比如结合reduce来使用:
mySets=[]
for i in range(10):
mySets.append(set(range(i,i+5)))
reduce(set.union,mySets)
集合是可变的,不能用做字典中的键。另外一个问题就是集合本身只能包含不可变值,所以也就不能包含其他集合。在实际当中,集合的集合是很常用的,通过frozenset类型,用于代表不可变的集合:
a=set()
b=set()
a.add(b) # TypeError:set objects are unhashable
a.add(frozenset(b))
frozenset构造函数创建给定集合的副本,不管是将集合作为其他集合成员还是字典的键,frozenset都很有用。
2. 堆
另外一个众所周知的数据结构是堆heap。它是优先队列的一种。使用优先队列能够以任意顺序增加对象,并且能在任何时间找到最小的元素,也就是说它比用于列表的min方法要有效率得多。
事实上,python中没有独立的堆类型---只有一个包含一些堆操作函数的模块,这个模块叫做heapq(q是queue的缩写,即队列),包含6个函数。其中4个直接和堆操作相关。你必须将列表作为堆对象本身。
heappush(heap,x) 将x入堆
heappop(heap) 将堆中最小的元素弹出
heapify(heap) 将heap属性强制应用到任意一个列表
heapreplace(heap,x) 将堆中最小的元素弹出,同时将x入堆
nlargest(n,iter) 返回iter中第n大的元素
nsmallest(n,iter) 返回iter中第n小的元素
heappush函数用于增加堆的项。注意,不能将它用于任何之前讲述的列表中---只能用于通过各种堆函数简历的列表中。原因是元素的顺序很重要(尽管看起来是随意排列,元素并不是进行严格排序的)。
from heapq import *
from random import shuffle
data=range(10)
shuffle(data)
heap=[]
for n in data:
heappush(heap,n)
[0,1,3,6,2,8,4,7,9,5]
heappush(heap,0.5)
[0,0.5,3,6,1,8,4,7,8,5,2]
元素的顺序并不像看起来那么随意。它们虽然不是严格排序的,但是也有规则的:位于i位置上的元素总比i//2位置处的元素大(反过来说就是i位置处的元素总比2*i以及2*i+1位置处的元素小)。这是底层算法的基础,而这个特性称为堆属性heap property。
heappop函数弹出最小的元素---一般来说都是在索引0处的元素,并且会确保剩余元素中最小的那个占据这个位置(保持刚才提到的堆属性)。一般来说,尽管弹出列表的第一个元素并不是很有效率得,但是这里不是问题,因为heappop在“幕后”会做一些精巧的移位操作。
heapify函数使用任意列表作为参数,并且通过尽可能少的移位操作,将其转换为合法的堆。如果没有用heappush建立堆,那么在使用heappush和heappop前应该使用这个函数。
heap=[5,8,0,3,6,7,9,1,4,2]
heapify(heap)
>>>heap
[0,1,5,3,2,7,9,8,4,6]
heapq模块中剩下的两个函数nlargest(n,iter)和nsamllest(n,iter)分别用来寻找任何可迭代对象iter中第n大或第n小的元素。你可以使用排序和分片完成这个工作,但是堆算法更快而且更有效地使用内容。
3. 双端队列(以及其他集合类型)
双端队列double-ended queue。在需要按照元素增加的顺序来移除元素时非常有用。python增加了collections模块,它包括deque类型。
双端队列通过可迭代对象(比如集合)创建,而且有些非常有用的方法。
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])
双端队列好用的原因是它能够有效地在开头(左侧)增加和弹出元素,这是在列表中无法实现的。除此之外,使用双端队列的好处还有:能够有效地旋转(rotate)元素(也就是将它们左移或者右移,使头尾相连)。双端队列对象还有extend和extendleft方法,extend和列表的extend方法差不多,extendleft则类似于appendleft、
[10.3.5] time
time模块所包括的函数能够实现以下功能:获取当前时间、操作时间和日期、从字符串读取时间以及格式化时间为字符串。日期可以用实数,或者是包含9个整数的元组。这些整数的意义如下所示,比如,元组:
(2009,1,21,12,2,56,0,21,0)
表示2008年1月21日12时2分56秒,星期一,并且是当前的第21天。
如下表Python日期元组的字段含义。
索引 字段 值
0 年
1 月
2 日
3 时
4 分
5 秒
6 周
7 儒历日
8 夏令时
time模块中最重要的函数如下所示:
asctime([tuple]) 将时间元组转换为字符串
localtime([secs]) 将秒数转换为日期元组,以本地时间为准
mktime(tuple) 将时间元组转换为本地时间
sleep(secs) 休眠(不做任何事情)secs秒
strptime(string[,format]) 将字符串解析为时间元组
time() 当前时间
函数time.asctime将当前时间格式转化为字符串:
>>>time.asctime()
'Fri Dec 22 05:21:27 2009'
函数time.localtime将实数转换为本地时间的日期元组。如果想获得全球统一时间,则可以使用gmtime。
函数time.mktime将日期元组转换为从新纪元开始计算的描述,它于localtime的功能相反。
函数time.sleep让解释器等待给定的描述。
函数time.strptime将asctime格式化过的字符串转换为日期元组。
函数time.time使用自然新纪元开始计算的描述返回当前时间。尽管每个平台的新纪元可能不同,但是你仍然可以通过计算某时间发生前后time的结果来对该时间计时,然后计算差值。
此外,python还提供了两个和时间密切相关的模块:datetime(支持日期和时间的算法)和timeit(帮助开发人员对代码段的执行时间进行计时)。
[10.3.6] random
random模块包括返回随机数的函数,可以用于模拟或者用于任何产生随机输出的程序。
事实上,所产生的数字都是伪随机数(pseudo-random),也就是说它们看起来是完全随机的,但实际上,它们以一个可预测的系统作为基础。不过,由于这个系统模块在伪装随机方面十分优秀,所以也就不必要对此过多担心了(除非为了实现强加密的目标,因为在这种情况下,这些数字就显得不够强了,无法抵抗某些特定的攻击)。如果需要真的随机性,应该使用os模块的urandom函数。random模块内的SystemRandom类也是基于同种功能,可以让数据接近真正的随机性。
这个模块中的一些重要函数如下所示:
random() 返回0≤n<1之间的随机实数n
getrandbits(n) 以长整型形式返回n个随机位
uniform(a,b) 返回实际实数n,其中a≤n
randrange([start],stop,[step]) 返回range([start],stop,[step])中的随机数
choice(seq) 从序列seq中返回随机元素
shuffle(seq[,random]) 原地制定序列seq
sample(seq,n) 从序列seq中选择n个随机且独立的元素
函数random.random是基本的随机函数之一,它只是返回0~1的伪随机数。除非这就是你想要的,否则你应该使用其他提供了额外功能的函数。random.getrandbits以长整型形式返回给定的位数。如果处理的是真正的随机事物,这个函数尤为有用。
为函数random.uniform提供两个数值参数a和b,它会返回在a~b的随机(平均分布的)实数n。所以,比如需要随机的角度值,可以使用uniform(0,360)。
调用函数range可以获得一个范围,而使用与之相同的参数来调用标准函数random.randrange则能够产生该范围内的随机整数。比如想要获得1-10的随机数,可以使用randrange(1,11),如果想要获得小于20的随机正奇数,可以使用randrange(1,20,2)。
函数random.choice从给定序列中选择随机元素。
函数random.shuffle将给定序列的元素进行随机位移,每种排列的可能性都是近似相等的。
函数random.sample从给定序列中选择给定数目的元素,同时确保元素互不相同。
从统计学的角度来说,还有些于uniform类似的函数,它们会根据其他各种不同的分布规则进行抽取,从而返回随机数。这些分布包括贝塔分布、指数分布、高斯分布,等等。
from random import *
from time import *
date1=(2008,1,1,0,0,0,-1,-1,-1)
time1=mktime(date1)
date2=(2009,1,1,0,0,0,-1,-1,-1)
time2=mktime(date2)
random_time=uniform(time1,time2)
print asctime(localtime(random_time))
[10.3.7] shelve
下一张会介绍如何在文件中存储数据。但如果只需要一个简单的存储方案,那么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']存储在键x下;
获得存储的表示,并且根据它来创建新的列表,而‘d’被添加到这个副本中。修改的版本还没有被保存!
最终,再次获得原始版本---没有‘d’!
为了正确地使用shelv模块修改存储的对象,必须将临时变量绑定到获得的副本上,并且在它被修改后重新存储这个副本:
temp=s['x']
temp.append('d')
s['x']=temp
>>>s['x']
['a','b','c','d']
python2.4之后的版本还有个解决方法:将open函数的writeback参数设为true。如果这样做,所有从shelf读取或者赋值到shelf的数据结构都会保存在内存(缓存)中,并且只有在关闭shelf的时候才写回到磁盘中。如果处理的数据不大,并且不想考虑这些问题,那么将writeback设为true的方法还是不错的。
2. 简单的数据库示例
如下代码给出了一个简单的使用shelf模块的数据库应用程序。
# database.py
import sys.shelve
def store_person(db):
"""
query user for data and store it in the shelf object
"""
pid=raw_input('Enter unique ID number: ')
person={}
person['name']=raw_input('Enter name:')
person['age']=raw_input('Enter age:')
person['phone']=raw_input('Enter phone number:')
db[pid]=person
def lookup_person(db):
"""
query user for ID and desired field.and fetch the corresponding data from the shelf object
"""
pid=raw_input('Enter ID number:')
field=raw_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=raw_input('Enter command(? for help):')
cmd=cmd.strp().lower()
return cmd
def main():
database=shelf.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函数中,只有在if __name__=='__main__'条件成立的时候才被调用,这意味着可以在其他程序中将这个程序作为模块导入,然后调用main函数;
在main函数中打开数据库shelf,然后将其作为参数传递给另外需要它的函数。当然也可以使用全局变量。不过在大多数情况下最好避免使用全局变量;
在一些值中进行读取之后,对读取的内容调用strip金额lower函数以生成一个修改后的版本。这么做的原因在于:如果提供的键与数据库存储的键相匹配,那么他们应该完全一样。如果总是对用户的输入使用strip和lower函数,那么就可以让用户随意输入大小写字母和添加空格。
使用try/finally确保数据库能正确关闭。我们永远不知道什么时候会出错。如果程序在没有正确关闭数据库的情况下终止,那么数据库文件就有可能被损坏了,这样的数据文件是毫无用处的。使用try/finally就可以避免这种情况。
[10.3.8] re
re模块包含对正则表达式regular expression的支持。学习正则的关键是一次只学习一点---查找满足特定任务需要的那部分内容,预先将它们全部记住是没有必要的。接下来将会对re模块主要特征和正则表达式的进行介绍。
1. 什么是正则表达式
正则表达式是可以匹配文本片段的模式。最简单的正则表达式就是普通字符串,可以匹配其自身。可以用这种匹配行为搜索文件中的模式,并且用计算后的值替换特定模式,或者将文本进行分段。
通配符
正则表达式可以匹配多于一个的字符串,可以使用一些特殊字符创建这类模式。比如点好(.)可以匹配任何字符(除了换行符),所以正则表达式'.ython'可以匹配字符串‘python’和‘jython’。还能匹配‘qython’、‘+ython’或者 ‘ ython’(第一个字母是空格),但是不会匹配‘cpython’或者‘ython’这样的字符串,因为点好只能匹配一个字母,而不是两个或零个。
因为它可以匹配“任何字符串”(除换行符外的任何单个字符),点好就称为通配符wildcard。
对特殊字符进行转义
在正则表达式中如果将特殊字符作为普通字符使用会遇到问题。比如,假设需要匹配字符串‘python.org’,直接用‘python.org’模式可以吗?这么做是可以的,但是这样会匹配‘pythonzorg’,这可不是所期望的结果。为了让特殊字符串表现得像普通字符一样,需要对它进行转义---可以再它前面加上反斜线。因此,在这里可以使用‘python\\.org’,这样只会匹配‘python.org’了。
为了获得re模块所需的单个反斜线,需要在字符串中使用两个反斜线---为了通过解释器进行转义。这样就需要两个级别的转义了:(1)通过解释器转义;(2)通过re模块转义。如果厌烦了使用双斜线,那么可以使用原始字符串,比如r‘python.org’。
字符集
匹配任意字符可能很有用,但有些时候需要更多的控制权。可以使用中括号括住字符串来创建字符集(character set)。字符集可以匹配它所包括的任意字符,所以‘[pj]ython’能够匹配‘python’和‘jython’,而非其他内容。可以使用范围,比如p‘[a-z]’能够匹配a到z的任意一个字符,还可以通过一个接一个的方式将范围联合起来使用。比如‘[a-zA-Z0-9]’能匹配任意大小写字母和苏子(注意字符集只能匹配一个这样的字符)。
为了反转字符集,可以再开头使用^字符,比如‘[^abc]’可以匹配任何除了a、b、c之外的字符。
选择符合子模式
在字符串的每个字符都各不相同的情况下,字符集是很好用的。但如果只想匹配字符串‘python’和‘perl’呢?取而代之的是用于选择项的特殊字符:管道符号(|)。因此,所需的模式可以写成‘python|perl’。
但是有些时候不需要对整个模式使用选择运算符---只是模式的一部分。这是可以使用圆括号括起需要的部分,或称子模式subparttern。前例可以写成‘p(ython|erl)’。
可选项和重复子模式
在子模式后面加上问号,它就编程了可选项。它可能出现在匹配字符串中,但并非必须的。例如,下面这个模式:
r'(http://)?(www\.)?python\.org'
只能匹配下列字符串:
'http://www.python.org'
'http://python.org'
'www.python.org'
'python.org'
对于上述例子,下面这些内容值得注意:
对点号进行了转义,防止它被作为通配符使用;
使用原始字符串减少所需反斜线的数量;
每个可选子模式都用圆括号括起;
可选子模式出现与否均可,而且相互独立。
问号表示子模式可以出现一次或者根本不出现。下面这些运算符允许子模式重复多次:
(pattern)*:允许模式重复0次或多次;
(pattern)+:允许模式重复1次或多次;
(pattern){m,n}:允许模式重复m~n此。
字符串的开始和结尾
在寻找子字符串时,确定子字符串位于整个字符串的开始还是结尾是很有用的。比如,只想在字符串的开头而不是其他位置匹配'ht+p',那么久可以使用脱字符(^)标记开始:‘^ht+p’会匹配‘http://python.org’以及‘httttp://python.org’。
类似的,字符串结尾用美元符号($)标识。
2. re模块的内容
re模块包含一些有用的操作正则表达式的函数。
compile(pattern[,flags]) 根据包含正则表达式的字符串创建模式对象
search(pattern,string[,flags]) 在字符串中寻找模式
match(pattern,sting[,flags]) 在字符串的开始处匹配模式
split(pattern,string[,maxsplit=0]) 根据模式的匹配项来分割字符串
findall(pattern,string) 列出字符串中模式的所有匹配项
sub(pat,repl,string[,count=0]) 将字符串中所有pat的匹配项用repl替换
escape(string) 将字符串中所有特殊正则表达式字符转义
函数re.compile将正则表达式转换为模式对象,可以实现更有效率的匹配。如果在调用search或math函数的时候使用字符串表示的正则表达式,它们也会在内部将字符串转义为正则表达式对象。使用compile完成一次转换之后,在每次使用模式的时候就不用进行转换。模式对象本身也有查找/匹配的函数。
函数re.search会在给定字符串中寻找第一个匹配给定正则表达式的字符串。一旦找到子字符串,函数就会返回MatchObject(值为True),否则返回None(值为False)。因为返回值的性质,所以该函数可以用在条件语句中,如下:
if re.search(pat,string):
print 'Found it!'
函数re.match会在给定字符串的开头匹配正则表达式。因此,re.match('p','python')返回真。而re.match('p','www.python.org')则返回假None。
函数re.spilt会根据模式的匹配项来分割字符串。使用完整的正则表达式代替了固定的分隔符字符串。返回值是子字符串的列表。maxsplit参数表示字符串最多可以分割成的不分数。
函数re.findall以列表形式返回给定模式的所有匹配项。比如查找标点符号:
pat=r'[.?\-",]+'
re.findall(pat,text)
注意,横线(-)被转义了,所以python不会将其解释为字符串范围的一部分。
函数re.sub的作用在于:使用给定的替换内容将匹配模式的子字符串替换掉。
re.escape是一个很使用的函数,可以对字符串中所有可能被解释为正则运算符的字符进行转义的应用函数。如果字符串很长且包含很多特殊字符,而你又不想输入一大堆反斜线,或者字符串来自于用户,且要用作正则表达式的一部分的时候,可以使用这个函数。
>>>re.escape('www.python.org')
'www\\.python\\.org'
>>>re.escape('But where is the ambiguity?')
'But\\ where\\ is\\ the\\ ambiguty\\?'
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
一般来说,如果组中包含诸如通配符或者重复运算符之类的特殊字符,那么你可能会对是什么与给定组实现了匹配感兴趣,比如在下面的模式中:
r'www\.(.+)\.com$'
组0包含整个字符串,而组1则包含位于'www.'和'.com'之间的所有内容。
re匹配对象的一些重要方法如下所示:
group([group1,...]) 获取给定子模式(组)的匹配项
start([group]) 返回给定组的匹配项的开始位置
end([group]) 返回给定组的匹配项的结束位置
span([group]) 返回一个组的开始和结束位置
group方法返回模式中与给定组匹配的(子)字符串。如果没有给出组号,默认组为0。如果给定一个组号,会返回单个字符串。否则会将对应给定组数的字符串作为元组返回。
start方法返回给定组匹配项的开始索引。
end类似于start,但是返回结果是结束索引加1。
方法span以元组的形式返回给定组的开始和结束位置的索引。
思考以下的例子:
>>>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的第一个例子中,只是把一个字符串用其他的内容替换掉了。用replace这个字符串方法能轻松达到同样的效果。当然,正则表达式很有用,因为它们允许以更灵活的方式搜索,同时它们也允许进行功能更强大的替换。
见证re.sub强大功能的最简单方式就是在替换字符串中使用组号。在替换内容中以‘\\n’形式出现的任何转义序列序列都会被模式中于组n匹配的字符串替换掉。例如,假设要把‘*something*’用‘something’替换掉,前者是普通文本文档中进行强调的常见方法,而后者则是相应的HTML代码。首先建立正则表达式:
>>>emphasis_pattern=r'\*([^\*]+)\*'
正则表达式很容易变得难以理解,所以为了以后能够读懂代码,使用有意义的变量名是很重要的。
现在模式已经搞定,接下来就可以使用re.sub进行替换了:
>>>re.sub(emphasis_pattern,r'\1','hello,*world*!')
'hello,world!'
将函数作为替换内容可以让替换功能变得更强大。可以对匹配的子字符串做任何事,并且可以细化处理过程,以生成替换内容。
贪婪与非贪婪模式:
重复运算符默认是贪婪greedy的,这意味着它会进行尽可能多的匹配。但是如何避免过于贪婪呢?只要使用重复运算符的非贪婪版本即可。所有的重复运算符都可以通过在其后面加上一个问号变成非贪婪版本。
这里用+?运算符代替了+,意味着模式也会像之前那样对一个或者多个通配符进行匹配,但是它会进行尽可能少的匹配,因为它是非贪婪的。
5. 找出Email的发信人
这个问题可以不使用正则表达式解决---可以使用email模块。
# 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)
6. 模板系统示例
模板是一种通过放入具体值从而得到某种已完成文本的文件。Python有一种高级的模板机制:字符串格式化。但是使用正则表达式可以让系统更加高级。
[10.3.9] 其他有趣的标准模块
下面介绍一些很酷的库:
functools:可以从这个库找到一些功能,让你能够通过部分参数来使用某个函数,稍后再为剩下的参数提供数值。Python3.0中filter和reduce包含在该模块中。
difflib:这个库让你可以计算两个序列的相似程度。还能让你从一些序列中找出和提供的原始序列“最像”的那个。difflib可以用于创建简单的搜索程序。
hashlib:通过这个模块,可以通过字符串计算小“签名”。如果为两个不同的字符串计算出了签名,几乎可以确保这两个签名完全不同。该模块应用于大文本文件,同时在加密和安全性方面有很多用途。
csv:CSV是逗号分隔值Comma-Separated Values的简写,这是一种很多程序都可以用来存储表格式数据的简单格式。它主要用于在不同程序间交换数据。使用CSV模块可以轻松写CSV文件,同时以显而易见的方式来处理这种格式的某些很难处理的地方。
timeit、profile和trace:timeit模块是衡量代码片段运行时间的工具,它有很多神秘功能,应该用它来代替time模块进行性能测试。profile模块可用于代码片段效率的全面分析。trace模块可以提供总的分析。这在写测试代码的时候很有用。
datetime:如果time模块不能满足时间追踪方面的需求,那么datetime可能就有用武之地。它支持特殊的日期和时间对象,让你能够以多种方式对它们进行构建和联合。它的接口在很多方面比time的接口要更加直观。
itertools:有很多工具用来创建和联合迭代器,还包括实现以下的功能:将可迭代的对象链接起来、创建返回无限连续整数的迭代器,从而通过重复访问可迭代对象进行循环等等。
logging:通过简单的print语句打印出程序的哪些方面很有用。如果希望对程序进行跟踪但又不想打印出太多调试内容,那么久需要将这些信息写入日志文件中了。这个模块提供了一组标准的工具,以便让开发人员管理一个或多个核心的日志文件,同时还对日志信息提供了多层次的优先级。
getopt和optparse:在UNIX中,命令行程序经常使用不同的选项或者开关运行。这些信息都可以再sys.argv中找到,但是自己要正确处理他们就没有这么简单了。针对这个问题,getopt库是个切实可行的解决方案,而optparse则更新、更强大并且更易用。
cmd:使用这个模块可以编写命令行解释器。可以自定义命令,以便让用户能够通过提示符来执行。
[10.4] 小结
本章讲述了模块的知识:如何创建、如何探究以及如何使用标准Python库中的模块。
模块:从基本上来说,模块就是子程序,它的主函数则用于定义,包括定义函数、类和变量。如果模块包含测试代码,那么久应该讲这部分代码放置在检查__name__=='__main__'是否为真的if语句中,能够在PYTHONPATH中找到的模块都可以导入。语句import foo可以导入存储在foo.py文件中的模块。
包:包是包含有其他模块的模块。包是作为包含__init__.py文件的目录来实现的。
探究模块:将模块导入交互式编辑器后,可以使用很多方法对其进行探究,比如使用dir、检查__all__变量以及使用help函数。文档和源代码是获取信息和内部机制的极好来源。
标准库:sys、os、fileinput、sets、heapq、deque、time、random、shelve、re
[10.4.1] 本章的新函数
dir(obj) 返回按字母顺序排序的属性名称列表
help([obj]) 提供交互式帮助或关于特定对象的交互式帮助信息
reload(module) 返回已经导入模块的重新载入版本