10 充电时刻


前面已经讲解了Python语言的大部分基础知识。Python的标准安装包括一组模块,称为标准库(standard library)。之前已介绍了一些模块(如math和cmath),标准库还包含其他模块。


本章将展示这些模块的工作方式,讨论如何分析他们,学习它们所提供的功能。对标准库进行概括,并着重介绍一部分有用的模块。


10.1 模块


已经学会创建和执行自己的程序(脚本)了,也学会了怎么用import从外部获取函数并为自己的程序所用:


>>> import math


>>> math.sin(0)


0.0


接下来学习一下如何编写自己的模块。


10.1.1 模块是程序


任何Python程序都可作为模块导入。


名字很重要)。


#代码清单10-1 一个简单的模块


#hello.py


print "Hello, world!"


程序保存的位置也很重要,现在假设将它保存在C:\python(windows)目录中,接着可以执行下面的代码,告诉解释器在哪里寻找模块了:


>>> import sys


#告诉解释器:除了从默认的目录中寻找之外,还需要从目录c:\python中寻找模块。


现在就可以导入自己的模块了:


>>> import hello


Hello, world!


  在导入模块的时候,可能会有新文件出现——本例中是c:\python\hello,pyc。这个以.pyc为扩展名的文件是(平台无关的)经过处理(编译)的,已将转换为Python能更加有效地处理的文件。若稍后再次导入同一个模块,Python会导入.pyc文件而不是.py文件,除非.py文件已改变——这种情况下会生成新的.pyc文件。删除.pyc文件不会损害程序(只要等效的.py文件存在即可)——必要时会创建新的.pyc文件。


上边,在导入模块的时候,其中的代码就执行了。但,再次导入该模块就什么也不会发生了:


>>> import hello


>>> 


因为导入模块并不意味着在导入时执行了某些操作(如打印文本)。它们主要用于定义,比如变量、函数和类等。此外,因为只需要定义这些东西一次,导入模块多次和导入一次的效果是一样的。


这种“只导入一次”的行为在大多数情况下是一种实质性的优化,对于以下情况尤为重要:两个模块互相导入。如果每个模块都可导入数次,那就会出问题,导入再导入。。。成了无限循环。但因为第二次导入模块时什么都不会发生,所以循环会终止。


如果坚持重新导入模块,那可使用内建的reload函数,它带有一个参数(要重新加载的模块),并返回重新载入的模块——若你在程序运行的时候更改了模块并希望这些更改反映出来,那此功能比较有用。要重新载入hello模块,可:


>>> hello = reload(hello)


Hello, World!


如果已经通过实例化bar模块中的Foo类创建了一个对象x,然后重新载入bar模块,那么不管通过什么方式都无法重新创建引用bar的对象x,x仍然是旧版本Foo类的实例(源自旧版本的bar)。如果需要x基于重新载入的模块bar中的新Foo类进行创建,那就得重新创建它。


10.1.2 模块用于定义


模块在第一次导入到程序中时被执行。真正的用处在于它们(像类一样)可以保持自己的作用域——意味着定义的所有类和函数及赋值后的变量都成为了模块的特性。


1 在模块中定义函数


假设编写了一个类似代码清单10-2的模块,并将它存储为hello2.py文件。同时,假设我们将它放置到Python解释器能找到的地方——可使用前一节中的sys.path方法,也可用10.1.3节中的常规方法。


#代码清单10-2  包含函数的简单模块


#hello2.py


def hello():


    print "Hello, world!"


导入:


>>> import hello2


模块会被执行,这意味着hello函数在模块的作用域内被定义了。因此可通过以下方式来访问函数:


>>> hello2.hello()


Hello, world!


我们可通过同样的方法来使用任何在模块的全局作用域中定义的名称。


为什么这样做?为什么不直接在主程序定义好一切?主要原因是代码重用(code reuse)。如果代码放在模块中,就可在多个程序中使用这些代码了——若编写了一个非常棒的客户端数据库,并将它放到一个叫clientdb的模块中,那就可在计费的时候、发送垃圾邮件的时候以及任何需要访问客户数据的程序中使用这个模块了。所以,为了让代码可重用,请将它模块化!(也关乎抽象)


2 在模块中增加测试代码


模块用来定义函数、类和其他一些内容,但有些时候,在模块中添加一些检查模块本身是否正常工作的测试代码是很有用的。例如想确保hello函数工作正常,你可能会将hello2模块重写为新的模块——代码清单10-3中定义的hello3


#代码清单10-3 带有问题测试代码的简单模块


#hello3.py


def hello():


    print "Hello, world!"



# A test:


hello()


看起来合理——若将其作为普通程序运行,能正常工作。但作为模块导入,然后在其他程序中使用hello函数,测试代码就会被执行:


>>> import hello3


Hello, world!                    #这不是想要的


>>> hello3.hello()


Hello, world!


避免上面情况的关键在于:“告知”模块本身是作为程序运行还是导入到其他程序。为实现这一点,需使用__name__变量:


>>> __name__           #在“主程序”(包括解释器的交互式提示符在内)中,变量__name__的值时‘__main__’


'__main__'


>>> hello3.__name__      #在导入的模块中,这个值被设为模块的名字。


'hello3'


为了让模块的测试代码更加好用,可将其放置在if语句中,如代码清单10-4所示


#代码清单10-4 使用条件测试代码的模块


#hello4.py


def hello():


    print "Hello, world!"


def test():        #将测试代码放在了test函数中,也可直接放入if语句。但是这样更灵活(放入独立的test函数),即使把模块导入其他程序之后,仍可对其进行测试。


    hello()


if __name__ == '__main__': test()


如果将10-4作为程序运行,hello函数会被执行。而作为模块导入时,它的行为就会像普通模块一样:


>>> import hello4


>>> hello4.hello()


Hello, world!


仍可进行测试:


>>> hello4.test()


Hello, world!


10.1.3 让你的模块可用


前边,改变了sys.path——其中包含了(字符串组成的)一个目录列表,解释器在该列表中查找模块。


而在理想状况下,sys.path本身一开始就应该包含正确的目录(包括你的模块的目录)。有两种方法可以做到这一点:一是将模块放置在合适的位置,二是告诉解释器去哪里查找模块。


1. 将模块放置在正确的位置


只需要找出Python解释器从哪里查找模块,然后将自己的文件放置在那里就行。(如果机器上解释器由管理员安装,你没有管理员权限——可能无法将你的模块存储在Python使用的目录中,所以你需要另一个解决方案:告诉解释器去哪里查找)


那些(称为搜索路径的)目录的列表可以在sys模块中的path变量中找到:


>>> import sys, pprint


>>> pprint.pprint(sys.path)


['',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\User\\Scripts\\python27.zip',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\App\\appdata\\canopy-1.7.4.3348.win-x86_64\\DLLs',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\App\\appdata\\canopy-1.7.4.3348.win-x86_64\\lib',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\App\\appdata\\canopy-1.7.4.3348.win-x86_64\\lib\\plat-win',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\App\\appdata\\canopy-1.7.4.3348.win-x86_64\\lib\\lib-tk',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\App\\appdata\\canopy-1.7.4.3348.win-x86_64',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\User',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\User\\lib\\site-packages',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\User\\lib\\site-packages\\win32',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\User\\lib\\site-packages\\win32\\lib',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\User\\lib\\site-packages\\Pythonwin',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\App\\appdata',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\App\\appdata\\canopy-1.7.4.3348.win-x86_64\\lib\\site-packages\\win32',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\App\\appdata\\canopy-1.7.4.3348.win-x86_64\\lib\\site-packages\\win32\\lib',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\App\\appdata\\canopy-1.7.4.3348.win-x86_64\\lib\\site-packages\\Pythonwin',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\App\\appdata\\canopy-1.7.4.3348.win-x86_64\\lib\\site-packages\\IPython\\extensions',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\App\\appdata',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\App\\appdata\\canopy-1.7.4.3348.win-x86_64',
 'C:\\Users\\Administrator\\AppData\\Local\\Enthought\\Canopy\\App\\appdata\\canopy-1.7.4.3348.win-x86_64\\lib\\site-packages',
 'C:\\Users\\Administrator\\.ipython']


#如果你的数据结构过大,不能再一行打印完,可使用pprint模块中的pprint函数代替普通的print语句。pprint是个相当好的打印函数,能提供更加智能的打印输出。


上边每个字符串都提供了一个放置模块的目录,解释器可以从这些目录中找到所需的模块。这些目录都可以使用,但site_packages目录是最佳选择——它就是用来干这个的。只要将模块放入类似的site_packages这样的目录中,所有程序就都能将其导入了。


2. 告诉编译器去哪里找


“将模块放置在正确的位置”这个解决方案对以下几种情况可能不适用:


1) 不希望自己的模块填满Python解释器的目录


2) 没有在Python解释器目录中存储文件的权限


3) 想将模块放在其他地方


所以就需要告诉解释器去哪里找。之前的一种方法:编辑sys.path,但这不是通用的方法。标准的实现方法是在PYTHONPATH环境变量中包含模块所在的目录。


PYTHONPATH环境变量的内容会因为使用的操作系统不同而有所差异,但基本上,与sys.path很类似—— 一个目录列表。


#环境变量并不是Python解释器的一部分——是操作系统的一部分。基本上,它相当于Python变量,不过是在Python解释器外设置的:Windows系统,可使用控制面板编辑变量。依次点击:开始菜单-》设置-》控制面板。进入控制面板后,双击“系统”图标。在打开的对话框中选择“高级”选项卡,点击“环境变量”按钮。这时会弹出一个氛围上下两栏的对话框:其中一个是用户变量,另一个是系统变量,需修改前者。若已有PYTHONPATH项,那选中它,单击“编辑”按钮,进行编辑;若没有,单击“新建”按钮,然后使用PYTHONPATH作为“变量名”,输入文件目录作为“变量值”,多个目录以分号分隔。


3. 命名模块


包含模块代码的文件的名字要和模块名一样——再加上.py扩展名。Windows系统中,也可使用.pyw扩展名。


10.1.4 包


为了组织好模块,可将它们分组为包(package)。包基本上就是另一类模块(但是能包含其他模块)——当模块存储在文件中时(扩展名.py),包就是模块所在的目录。为了让Python将其作为包对待,它必须包含一个名为__init__py的文件(模块)。若将它作为普通模块导入的话,文件的内容就是包的内容。如:有个名为constants的包,文件constants/__init__.py包括语句PI=3.14,那可像下面一样做:


import constants


print constants.PI


为了将模块放置在包内,直接把模块放在包目录内即可。


#创立一个叫做drawing的包,其中包括名为shapes和colors的模块。需创建下表所示的文件和目录


C:\python                                       #PYTHONPATH中的目录


C:\python\drawing                          #包目录(drawing包)


C:\python\drawing\__init__.py        #包代码(drawing模块)


C:\python\drawing\colors.py           #colors模块


C:\python\drawing\shapes.py         #shapes模块


依上边设置,下面语句都是合法的:


import drawing                #此时drawing中__init__模块的内容是可用的,但是drawing和colors模块不可用


import drawing.colors      #colors模块可用,但只能通过全名drawing.colors来使用


from drawing import shapes    #shapes模块可用,可通过短名(shapes)来使用。


以上例子,执行顺序不是必需的。如:不必在导入包的模块前导入包本身,第二条语句可独立使用,第三条也是。还可在包之间进行嵌套。


10.2 探究模块


学会独立探究模块——标准库及网上有很多有用的模块,要学会快速理解它们。


10.2.1 模块中有什么


探究模块最直接的方式就是在Python解释器中研究它们。第一件事就是导入它们,假设你听说有个叫做copy的标准模块:


>>> import copy        #没引发异常,说明存在


接下来探讨他能做什么,有什么。


1. 使用dir


查看模块包含的内容可用dir函数——将对象(以及模块的所有函数、类、变量等)的所有特性列出。打印出dir(copy)的内容,会看到一长串的名字(一些名字以下划线开始——暗示(约定俗成)它们并不是为在模块外部使用而准备的),用列表推导式过滤掉以下划线开始的名字:


>>> [n for n in dir(copy) if not n.startswith('_')]


['Error',
 'PyStringMap',
 'copy',
 'deepcopy',
 'dispatch_table',
 'error',
 'name',
 't',
 'weakref']


2. __all__变量


__all__这个名字。这个变量包含一个列表,该列表与之前列表推导式创建的列表很类似——除了这个列表在模块本身中被设置。


>>> copy.__all__


['Error', 'copy', 'deepcopy']


列表推导式只比上边列表多了几个我用不到的名字。


__all__列表是在copy模块内部被设置的,如下(从copy.py直接复制而来的代码):


__all__ = ["Error", "copy", "deepcopy"]


__all__定义了模块的公有接口(public interface)。更准确的说,它告诉解释器;从模块导入所有名字代表什么含义。因此,若你使用如下代码:


from copy import *


那只能使用__all__变量中的3个函数。而要导入PyStringMap的话,就需要显式的实现,或导入copy然后使用copy.PyStringMap,或者使用 from copy import PyStringMap.


在编写模块时,设置类似于__all__这样的技术是相当有用的——可过滤模块中可能会有的一大堆其他程序不需要或不想要的 变量、函数和类。


若没有设定__all__,用import * 语句默认将会输出模块中所有不以下划线开头的全局名称。


10.2.2 用help获取帮助


对以上探索工作,交互式解释器是非常强大的工具,对该语言的精通程度决定了对模块探究的深度。但是,还有个标准函数能为我们提供日常所需的信息——help函数。


>>> 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__特性引用。就像之前写在函数前面的文档字符串。类也一样有(写在类前面)


>>> print copy.copy.__doc__


Shallow copy operation on arbitrary Python objects.


    See the module's __doc__ string for more info.


使用help与直接检查文档字符串相比,好处在于会获得更多信息,如函数签名(即函数所带的参数)


10.2.3 文档


模块信息的自然来源是文档。例如,提问:“range的参数是什么?”。不用查找书籍或标准Python文档,可直接查看:


>>> print range.__doc__


range(stop) -> list of integers
range(start, stop[, step]) -> list of integers


Return a list containing an arithmetic progression of integers.
range(i, j) returns [i, i+1, i+2, ..., j-1]; start (!) defaults to 0.
When step is given, it specifies the increment (or decrement).
For example, range(4) returns [0, 1, 2, 3].  The end point is omitted!
These are exactly the valid indices for a list of 4 elements.


但是,并非每个模块和函数都有不错的文档字符串(尽管都应该有),有时可能需要十分透彻地描述这些函数和模块是如何工作的。


学习Python编程最有用的文档是Python库参考——对所有标准库中的模块都有描述(http://python.org/doc/lib)


10.2.4 使用源代码


想真正理解Python语言,要了解模块,是不能脱离源代码的。阅读源代码是学习Python的最好方式。


那源代码在哪里?一种方案是检查sys.path,然后自己找。另一种快捷方式是检查模块的__file__属性:


>>> print copy.__file__


C:\Users\Administrator\AppData\Local\Enthought\Canopy\App\appdata\canopy-1.7.4.3348.win-x86_64\lib\copy.pyc #文件名以.pyc结尾,只要查看对应的以.py结尾的文件。


你可以使用代码编辑器打开copy.py,然后查看它是如何工作的。#要承担意外修改它的风险,可能会破坏它,所以关闭文件时,确保未做任何可能的修改。


注意:一些模块并不包含任何可阅读的Python源代码—可能已经融入到解释器内了(如sys模块),或可能使用C程序语言写成的。


10.3 标准库:一些最爱


本章标题:“充电时刻”(batteries included)这个短语用于描述Python丰富的标准库——安装Python之后,你就“免费”获得了很多有用的模块(充电电池)。


10.3.1 sys


sys模块让你能够访问与Python解释器联系紧密的变量和函数,下面列出一些:


argv             #命令行参数,包括脚本名称


exit([arg])      #退出当前的程序,可选参数为给定的返回值或者错误信息


modules       #映射模块名字到载入模块的字典


path              #查找模块所在目录的目录名列表


platform        #类似sunos5或win32的平台标识符


stdin             #标准输入流—— 一个类文件(file-like)对象


stdout           #标准输出流—— 一个类文件对象


stderr           #标准错误流—— 一个类文件对象


10.3.2 os


提供访问多个操作系统服务的功能。其包含内容很多,下边只列举了其中一些最有用的函数和变量。os和它的子模块os.path还包括一些用于检查、构造、删除目录和文件的函数,以及一些处理路径的函数(如os.path.split和os.path.join).


environ                            #对环境变量进行映射


system(command)          #在子shell中执行操作系统命令


sep                                  #路径中的分隔符


pathsep                           #分隔路径的分隔符


linesep                            #行分隔符(‘\n’,'\r', or '\r\n')


urandom(n)                     #返回n字节的加密强随机数据


10.3.3 fileinput

fileinput模块让你能够轻松地遍历文本文件的所有行。

10.3.4 集合、堆和双端队列


计算机编程中,有很多有用的数据结构,Python支持其中相对通用的类型(如字典(或者说散列表)、列表(或者说动态数组))。其他一些数据结构有些时候也能派上用场。


1 集合


集合(Set)在Python2.3才引入。Set类位于sets模块中。在Python2.3中,集合通过set类型的实现成为了语言的一部分,这意味着不需要导入sets模块了——直接创建集合即可:


>>> set(range(10))


{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}


集合是由序列(或其他可迭代对象)构建的,它们主要用于检查成员资格,因此副本是被忽略的:


>>> set([0, 1, 2, 3, 0, 1, 2, 3, 4])


{0, 1, 2, 3, 4}


和字典一样,集合元素的顺序是随意的,因此不应该以元素的顺序作为依据进行编程:


>>> set(['a', 'c', 'b'])


{'a', 'b', 'c'}


除检查成员资格之外,还可使用标准的集合操作,如:并集和交集,可使用方法,也可使用对整数进行位操作时使用的操作。


#找两个集合的并集,可使用其中一个集合的union方法或者使用按位与(OR)运算符“|”


>>> a = set([1, 2, 3])


>>> b = set([2, 3, 4])


>>> a.union(b)


{1, 2, 3, 4}


>>> a|b


{1, 2, 3, 4}


其还有一些其他的方法及对应的运算符。


集合是可变的,所以不能作为字典中的键。集合本身只能包含不可变(可散列的)值,所以就不能包含其他集合。而在实际中,集合的集合是很常用的,所以要解决这个问题。有frozenset类型——用于代表不可变(可散列)的集合。


>>> a = set()


>>> b = set()


>>> a.add(b)


Traceback ...


...


TypeError: ...


>>> a.add(frozenset(b))


2 堆(heap)


  堆是优先队列的一种,使用优先队列能够以任意顺序增加对象,并且能在任何时间(可能在增加对象的同时)找到(也可能是移除)最小的元素——它比用于列表的min方法有效率的多。


 Python中并没有独立的堆类型——只有一个包含一些堆操作函数的模块(heapq),包含6个函数,其中前四个直接和堆操作相关。你必须将列表作为堆对象本身。


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 heap import *


>>> from random import shuffle


>>> data = range(10)


>>> shuffle(data)


>>> heap = []


>>> for n in data:


...         heappush(heap, n)


>>>heap


[0, 2, 1, 4, 5, 9, 3, 7, 6, 8]


>>> heappush(heap, 0.5)


>>> heap


[0, 0.5, 1, 4, 2, 9, 3, 7, 6, 8, 5]   #元素排列的顺序也是有规则的:i位置处的元素总比2*i以及2*i+1位置处的元素小。这是底层堆算法的基础,此特性称为堆属性(heap property)


heappop函数弹出最小的元素—— 一般来说都是在索引0处的元素,并且会确保剩余元素中最小的那个占据这个位置(保持刚才提到的堆属性)。一般来说,尽管弹出列表的第一个元素并不是很有效率,但在这里不是问题,因为heappop在“幕后”会做一些精巧的移位操作:


>>> heappop(heap)


0


>>> heappop(heap)


0.5


>>> heappop(heap)


1


>>> heap


[2, 5, 3, 6, 9, 7, 4, 8]


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]


heapreplace函数不像其他函数那么常用。它弹出堆的最小元素,并将新元素加入。这样比调用heappop之后再调用heappush更高效。


>>> heapreplace(heap, 0.5)


0


>>>heap


[0.5, 1, 5, 3, 2, 7, 9, 8, 4, 6]


>>> heaprepalce(heap, 10)


[1, 2, 5, 3, 6, 7, 9, 8, 4, 10]


heap模块中剩下的两个函数nlargest(n,iter)和nsmallest(n,iter)分别用来寻找任何可迭代对象iter中第n大或第n小的元素。你可以使用排序(如sorted函数)和分片来完成这个工作,但是堆算法更快且更有效的使用内存且更易用。


3. 双端队列(以及其他集合类型)


双端队列(Double-ended queue,或称deque)在需要按元素增加的顺序来移除元素时非常有用。Python2.4增加了collections模块,它包含deque类型。


双端队列通过可迭代对象(如集合)创建,而且有些非常有用的方法。


>>> form 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])


双端队列好用的原因是它能够在开头(左侧)增加和弹出元素——这是列表无法实现的。


另一个好处就是:能有效的旋转(rotate)元素(也就是将它们左移或右移,使头尾相连)。


反序出现在双端队列中。


10.3.5 time


time模块中的函数可实现:获得当前时间、操作时间和日期、从字符串读取时间以及格式化时间为字符串。日期可以用实数或者是包含九个整数的元组,这些整数的意义如下所示:


元组:(2008, 1, 21, 12, 2, 56, 0, 21, 0) 表示2008年1月21日12时2分56秒,星期一,且是当年的第21天(无夏时令)。


0           年            比如2001,2003,等等


1           月            范围1—12


2           日            范围1—31


3           时            范围0—23


4           分            范围0—59


5           秒            范围0—61                         #为了应付闰秒和双闰秒


6           周            范围0—6,周一时为0


7           儒历日     范围1—366


8           夏时令     0、1或-1                            #布尔值,但是如果使用了-1,mktime(该函数将这样的元组转换为时间戳,而时间戳从新世纪开始以秒来量)就会工作正常。


time模块中最重要的函数如下:


asctime([tuple])                     #将时间元组转换为字符串


localtime([secs])                   #将秒数转换为日期元组,以本地时间为准


mktime(tuple)                        #将时间元组转换为本地时间


sleep(secs)                           #休眠(不做任何事情)secs秒


strptime(string[, format])        #将字符串解析为时间元组


time()                                    #当前时间(新世纪开始后的秒数,以UTC为准)


Python还提供了两个和时间密切相关的模块:datetime(支持日期和时间的算法)和timeit(帮助开发人员对代码段的执行时间进行计时)


10.3.6 random


random模块包括返回随机数的函数,可用于模拟或任何产生随机输出的程序。


模块中的一些重要函数:


random()                                       #返回0<=n<1之间的随机实数n, 其中0<n<=1


getrandbits(n)                               #以长整型形式返回n个随机位——处理真正的随机事务(如加密),这个函数尤其有用


uniform(a, b)                                 #返回随机实数n,其中a<=n<b


randrange([start], stop, [step])      #返回range(start,stop, step)中的随机数


randint([start], stop, [step])            #调用的是randrange(start, stop+1),所以包含上限。


choice(seq)                                   #从序列seq中返回随意元素


shuffle(seq[, random])                  #原地指定序列seq——将给定(可变)序列的元素进行随机移位,每种排列的可能性都是近似相等的


sample(seq, n)                             #从序列seq中选择n个随机且独立的元素(元素互不相同)


标准函数random.randrange能产生范围内的随机整数,如想获得1—10(包括10)的随机数,可使用randrange(1, 11)(或者使用randrange(10)+1),如果想获得小于20的随机正奇数,可使用randrange(1, 20, 2)


一些例子(使用一些time模块中的函数):


#首先获得代表时间间隔(2008年这一年)限制的实数


from random import *


from time import *


date1 = (2008, 1, 1, 0, 0, 0, -1, -1, -1)         #时间元组的方式来表示日期,使用-1表示一周中的某天、一年中的某天和夏时令,以便Python自行计算


time1 = mktime(date1)                                  #对元组调用mktime


date2 = (2009, 1, 1, 0, 0, 0, -1, -1, -1)


time2 = mktime(date2)


>>> random_time = uniform(time1, time2)                #在这一范围内均一的生成随机数(不包括上限)


>>> print asctime(localtime(random_time))               #将数字转化为易读的日期形式


Sun Jul 06 06:12:48 2008


#2 要求用户选择投掷的骰子数以及每个骰子具有的面数。投骰子机制可由randrange和for循环实现


from random import randrange


num    = input("how many dice? ")


sides  = input("How many sides per die? ")


sum = 0


for i in range(num): sum += randrange(sides)+1


print "The resurt is", sum


执行:


How many dice? 3


How many sides per die? 6


The result is 10


#3 假设有一个新建的文本文件,它的每一行代表一种运势。可使用fileinput模块将“运势”都存入列表,在进行随机选择:


#文件名 fortune.py


import fileinput, random


fortunes = list(fileinput.input())


print random.choice(fortunes)


#4 希望程序在每次敲击回车的时候都为自己发一张牌,同时还要确保不会获得相同的牌。


#首先创建一副牌 ——字符串列表


>>>values = range(1,11) + 'Jack Queen King'.split()


>>> suits = 'diamonds clubs hearts spades'.split()


>>> deck = ['%s of %s' % (v, s) for v in values for s in suits]


#现在的牌还不太适合进行游戏,让我们来看看现在的牌:


>>> from pprint import pprint


>>> pprint(deck[:12])


['1 of diamonds',
 '1 of clubs',
 '1 of hearts',
 '1 of spades',
 '2 of diamonds',
 '2 of clubs',
 '2 of hearts',
 '2 of spades',
 '3 of diamonds',
 '3 of clubs',
 '3 of hearts',
 '3 of spades']


太整齐了,解决:


>>> from random import shuffle


>>> shuffle(deck)


>>> pprint(deck[:12])


['8 of diamonds',
 '5 of spades',
 '4 of spades',
 '3 of diamonds',
 '7 of hearts',
 '5 of diamonds',
 'Jack of clubs',
 'King of hearts',
 '3 of spades',
 '4 of diamonds',
 '2 of spades',
 'Queen of diamonds']


#让Python在每次按回车的时候都给你发一张牌,直到发完为止,那只需在程序后放一个小的while循环即可。


while deck: raw_input(deck.pop())


10.3.7  shelve


若只需要一个简单的存储方案,那shelve模块可满足大部分的要求,只需为它提供文件名。shelve中唯一有趣的函数是open,在调用它的时候(使用文件名作为参数),它会返回一个shelf对象,可用它来存储内容。只需把它当做普通的字典(但是键一定要作为字符串)来操作即可,在完成工作(并且将内容存储到磁盘中)之后,调用它的close方法。


1.潜在的陷阱


意识到shelve.open函数返回的对象并不是普通的映射是很重要的:


>>> import shelve


>>> s = shelve.open('test.dat')


>>> s['x'] = ['a', 'b', 'c']                   #列表 ['a', 'b', 'c']存储在键x下


>>> s['x'].append('d')                    #获得存储的表示,并根据它来创建新的列表副本,而‘d’被添加到这个副本中。修改的版本还没有被保存!


>>> s['x']                                       最终,再次获得原始版本——没有‘d’


['a', 'b', 'c']


'd'去哪了?—— 当你在shelf对象中查找元素的时候,这个对象都会根据已经存储的版本进行重新构建,当你将元素赋给某个键时,它就被存储了。


为正确使用shelve模块修改存储对象,必须将临时变量绑定到获得的副本上,并且在它被修改后重新存储这个副本:


>>> 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(确保在最后关闭了shelf)的方法还是不错的。


2 简单的数据库示例


#简单的使用shelve模块的数据库应用程序。


#代码清单10-8  简单的数据库应用程序


#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() + ':', \                                #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 '?          : Print this message'



def enter_command():


    cmd = raw_input('Enter command (? for help): ')


    cmd = cmd.strip().lower()


    return cmd



def main():                                                           #主程序main,只有在if __name__ =='__main__'条件成立时才被调用。


    database = shelve.open('C:\\database.dat')  #you may want to change this name      #打开数据库(shelf),将其作为参数传递给其他需要他的函数。


    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函数


简单的交互过程:


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
?          : Print 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


10.3.8 re


re模块包含对正则表达书(regular expression)的支持。学习它们的关键是一次只学习一点—— (在文档中)查找满足特定任务需要的那部分内容,预先将它们全部记住是没有必要的。本节会对re模块主要特征和正则表达式进行介绍。


1 什么是正则表达式


正则表达式是可以匹配文本片段的模式。最简单的正则表达式就是普通字符串,可匹配其自身。正则表达式‘python’可匹配字符串‘python’。你可以用这种匹配行为搜索文本中的模式,并且用计算后的值替换特定模式,或将文本进行分段。


#通配符(点号)——可匹配“任何字符串”(除换行符外的任何单个字符)


点号只能匹配一个字母,而不是两个或零个。


#对特殊字符进行转义


在正则表达式中如果将特殊字符作为普通字符使用就会 遇到问题。假设需匹配字符串‘python.org’,直接使用‘python.org’可以吗?可以的,但是这样也会匹配‘pythonzorg’。为了让特殊字符表现的像普通字符一样,需要对它进行转义(escape)——在其前面加上反斜线。所以‘python\\.org’就只会匹配‘python.org’


#字符集


有时你需要更多的控制权,可使用中括号括住字符串来创建字符集(character set)。字符集可匹配它所包含的任意字符,所以‘[py]thon’能够匹配‘python’和‘jython’,而非其他内容。可使用范围,如‘[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’会匹配‘http://python.org’及(‘htttttp://python.org’),但是不匹配‘www.http.org’。类似的,字符串结尾用美元符号($)标识。


2 re模块的内容


re模块包含的最重要的一些函数如下:


compile(pattern[, flags])                      # 根据包含正则表达式的字符串创建模式对象


search(pattern, string[, flags])             # 在字符串中寻找模式


match(pattern, string[, flags])              #在字符串的最开始除匹配模式


split(pattern, string[, maxsplit=0])        #根据模式的匹配项来分割字符串


findall(pattern, string)                          #列出字符串中模式的所有匹配项


sub(pat, repl, string[, count=0])           #将字符串中所有pat的匹配项用repl替换


escape(string)                                     #将字符串中所有特殊正则表达式字符转义


函数re.compile将正则表达式(以字符串书写的)转换为模式对象,可实现更有效率的匹配。


函数re.search会在给定字符串中寻找第一个匹配给定正则表达式的子字符串。一但找到子字符串,函数就返回MatchObject(True),否则返回None(False),所以该函数可用在条件语句中:


if re.search(pat, string):


    print 'Found it!'


函数re.match会在给定字符串的开头匹配正则表达式。所以,match('p', 'python')返回真;而re.match('p', 'www.python.org')返回假。


函数re.split会根据模式的匹配项来分割字符串。类似于字符串方法split,不过是用完整的正则表达式代替了固定的分隔符字符串。如字符串方法split允许使用字符串‘.’的匹配项来分割字符串,而 re.split则允许用任意长度的逗号和空格序列来分割字符串:


>>> some_text = 'alpha, beta,,,,gamma delta'


>>> re.split('[, ]+', some_text)


['alpha', 'beta', 'gamma', 'delta']


>>> re.split('o(o)', 'foobar)               #若模式包含小括号,那括起来的字符组合会散布在分割后的子字符串之间。


['f', 'o', 'bar']                                      #返回值是子字符串的列表


maxsplit参数表示字符串最多可分割成的部分数:


>>> re.split('[, ]+', some_text, maxsplit=2)


['alpha', 'beta', 'gamma delta']


>>> re.split('[, ]+', some_text, maxsplit=1)


['alpha', 'beta,,,,gamma delta'


函数re.findall以列表形式返回给定模式的所有匹配项。如,在字符串中查找所有的单词:


>>> pat = '[a-zA-Z]+'


>>> text = '"Hm... Err -- are you sure?" he said, sounding insecure.'


>>> re.findall(pat, text)


['Hm', 'Err', 'are', 'you', 'sure', 'he', 'said', 'sounding', 'insecure']


或者查找标点符号:


>>> pat = r'[.?\-",]+'             #横线被转义,python不会将其解释为字符范围的一部分


>>> re.findall(pat, text)


['"', '...', '---', '?"', ',', '.']


函数re.sub的作用:使用给定的替换内容将匹配模式的子字符串(最左端并且非重叠的子字符串)替换掉。


>>> pat = '{name}'


>>> text = 'Dear {name}...'


>>> re.sub(pat, 'Mr Gumby', text)


'Dear Mr Gumby...'


re.escape是一个很有用的函数,可对字符串中所有可能被解释为正则运算符的字符进行转义的应用函数。当字符串很长且包含很多特殊字符,且不想输入一大堆反斜线;或字符串来自用户(如通过raw_input函数获取的内容),且要用作正则表达式的一部分时。可使用这个函数。


>>> 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


如果组中包含诸如通配符或者重复运算符之类的特殊字符,那可能会对是什么与给定组实现了匹配感兴趣,如在下面模式中:


r'www\.(.+)\.com$'


组0包含整个字符串,而组1则包含位于‘www.’和'.com'之间的所有内容。像这样创建模式的话,就可以取出字符串中最感兴趣的部分了。


re匹配对象的一些重要方法:


group([group1, ...])     #获取给定子模式(组)的匹配项


start([group])              #返回给定组的匹配项的开始位置(默认为0,即整个模式)


end([group])               #返回给定组的匹配项的结束位置(不包括组的结束位置,返回结果是结束索引+1)


span([group])             #返回一个组的开始和结束位置(以元组(start,end)的形式返回给定组的开始和结束位置的索引(默认为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的第一个例子中,只把一个字符串用其他内容替换掉了。用replace字符串方法能达到同样的效果。但是,正则表达式允许以更灵活的方式搜索,同时也允许进行功能更强大的替换。


首先建立正则表达式:


>>> emphasis_pattern = r'\*([^\*]+)\*'


需注意的是:正则表达式很容易变得难以理解,所以为了让其他人(包括自己)在以后能读懂代码,使用有意义的变量名(或加上一两句注释)是很重要的。让正则表达式更易读的方式是在re函数中使用VERBOSE标志——允许在模式中添加空白(空白字符、tab、换行符、等等),re会忽略他们(除非将其放在字符类或者用反斜线转义)。也可在冗长的正则式中添加注释。


>>>emphasis_pattern = re.compile(r'''


...            \*                       #Beginning emphasis tag -- an asterisk


...            (                        #Begin group for capturing phrase


...            [^\*]+                 #Capture anything except asterisks


...            )                        #End group


...            \*                       #Ending emphasis tag


...            ''', re.VERBOSE)


...


接下来可使用re.sub进行替换:


>>> re.sub(emphasis_pattern, r'<em>\1</em>', 'Hello, *world*!')


'Hello, <em>world</em>!


将函数作为替换内容可让替换功能变得更加强大。MatchObject将作为函数的唯一参数,返回的字符串将会作为替换内容。换句话说,可以对匹配的子字符串做任何事,并且可以细化处理过程,以生成替换内容。正则表达式可有无数的应用。


贪婪和非贪婪模式


重复运算符默认是贪婪(greedy)的—会进行尽可能多的匹配。如,假设重写了刚才用到的程序,以使用下面的模式:


>>> emphasis_pattern = r'\*(.+)\*'            #会匹配:星号加上一个或多个字符,再加上一个星号的字符串。


>>> re.sub(emphasis_pattern, r'<em>\1</em>', '*This* is *it*!')


'<em>This* is *it</em>!'          #模式匹配了从开始星号到结束星号之间的所有内容——包括中间的两个星号:贪婪的—将尽可能多的东西据为己有。


当你知道某个特定字母不合法的时候,前面的解决方案(使用字符集匹配任何不是星号的内容[^\*]+)才是可行的。但是,如果使用‘**something**’表示强调呢?现在所强调部分包括单个星号已经不是问题了,但是如何避免过于贪婪?


很简单,只需使用重复运算符的非贪婪版本(在其后面加上一个问号):


>>> emphasis_pattern = r'\*\*(.+?)\*\*'                   #+?替换了+,模式还会像原来那样对一个或多个通配符进行匹配,但会进行尽可能少匹配,是非贪婪的。


>>> re.sub(emphasis_pattern, r'<em>\1</em>', '**This** is **it**!')


'<em>This</em> is <em>it</em>!'


5. 找出Email的发件人


将Email存为文本文件,文件的头部包含了一大堆与邮件内容无关的信息,如代码清单10-9所示


#代码清单10-9  一组(虚构的)Email头部信息


From foo@bar.baz Thu Dec 20 01:22:50 2008


Return_Path: <foo@bar.baz>


Received: from xyzzy42.bar.com (xyzzy.bar.baz [123.456.789.42])


    by frozz.bozz.floop (8.9.3/8.9.3) with ESMTP id BAA25436


    for <magnus@bozz.floop>: Thu, 20 Dec 2004 01:22:50 +0100 (MET)


Received: from [43.253.124.23] by bar.baz


    (InterMail vM.4.01.03.27 201-229-121-127-20010626) with ESMTP


    id <20041220002242.ADASD123.bar.baz@[43.253.124.23]>;


    Thu, 20 Dec 2004 00:22:42 +0000


User-Agent: Microsoft-outlook-Express-Macintosh-Edition/5.02.2022


Date: Wed, 19 Dec 2008 17:22:42 -0700


Subject: Re: Spam


From: Foo Fie <foo@bar.baz>


To: Magnus Lie Hetland <magnus@bozz.floop>


CC: <Mr.Gumby@bar.baz>


Message-ID: <B8467D62.84F%foo@baz.com>


In-Reply-To: <20041219013308.A2655@bozz.floop>


Mime-version: 1.0


Content - type: text/plain; charset="US-ASCII"


Content - transfer-encoding: 7bit


Status: RO


Content-Length: 55


Lines: 6



So long, and thanks for all the spam!



Yours,



Foo Fie


#试着找出这封邮件是谁发的(如果直接看邮件可直接指出),找出通用模式。


#怎么把发件人的名字取出而不带Email地址?或如何将头部信息中包含的Email地址列示出来?


#代码清单10-10 寻找发件人的程序


#首先找出发件人:包含发件人的文本行以字符串“From: ”作为开始,以放置在尖括号(<>)中的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)





10.3.9 其他有趣的标准模块




10.4 小结


本章主讲了模块的知识:如何创建、如何探究及如何使用python的标准库中的模块。


模块: 从基本上来说,模块就是子程序,它的主函数则用于定义(定义函数、类和变量)。若模块包含了测试代码,那应该将这部分代码放置在检查__name__=='__main__'是否为真的if语句中。能够在PYTHONPATH中找到的模块都可以导入。语句import foo 可以导入存储在foo.py文件中的模块。


包: 包是包含在其他模块中的模块。包是作为包含__init__.py文件的目录来实现的。


探究模块:将模块导入交互式编辑器后,可使用很多方法对其进行探究。如使用dir、检查__all__变量及使用help函数。文档和源代码是获取信息和内部机制的极好来源。


标准库:python包含了一些模块,总称标准库。


本章介绍了其中很多模块。


10.4.1 本章新函数


dir(obj)                      #返回按字母顺序排列的属性名称列表


help([obj])                 #提供交互式帮助或关于特定对象的交互式帮助信息


reload(module)         #返回已经导入模块的重新载入版本,在python3.0中废除?