第九章:魔法方法、属性和迭代器
1:魔法方法:比如 __future__
2:构造方法
介绍的第一个魔法是构造方法。构造方法和其他普通方法不同的地方在于,当一个对象被创建后,会立即调用构造方法:
>>> class FooBar: def __init__(self): self.somevar=42 >>> f=FooBar() >>> f.somevar 42
Python还有个魔法方法叫__del__,也就是析构方法。它在对象就要被垃圾回收之前调用。但由于发生调用的具体时间是不可知的,所以避免使用它
1)重写一般方法和特殊的构造方法
重写是继承机制中的一个重要内容,对于构造方法尤其重要。构造方法用来初始化新创建对象的状态,子类不仅要拥有自己的初始化代码,还要拥有超类的初始化代码。虽然重写机制对于所有方法都是一样的,但是:如果一个类的构造方法被重写,那么就需要调用超类的构造方法,否则对象可能不会被正确的初始化。
__metaclass__ = type #super函数只在新式类中起作用 class Bird: def __init__(self): self.hungry = True def eat(self): if self.hungry: print('Aaaah...') self.hungry = False else: print('No,thanks!') class SongBird(Bird): def __init__(self): #在调用一个实例的方法时,该方法的self参数会被自动绑定到实例上(这称为绑定方法) #如果直接调用类的方法(如Bird.__init__),就没有实例会被绑定。这样可以自由提供需要的self参数 #通过将当前的实例作为self参数提供给未绑定方法,SongBird就能使用其超类构造方法的所有实现,也就是说hungry属性能被设置 #Bird.__init__(self) super(SongBird, self).__init__() self.sound = 'Squawk!' def sing(self): print(self.sound)
super函数比在超类中直接调用未绑定方法更直观。另外super函数也很智能,即使类已经继承多个超类,它也只需要使用一次super函数(但要确保所有的超类的构造方法都使用了super函数)。super函数实际返回了一个super对象,这个对象负责进行方法解析,当对其特性进行访问时,它会查找所有的超类(以及超类的超类),直到找到为止(或引发一个AttributeError异常)
3:成员访问
规则(protocol),它是管理某种形式的行为的规则。这与接口有点类似。规则说明了应该实现何种方法和这些方法应该做什么。因为python中的多态性是基于对象的行为的(而不是基于祖先,例如它所属的类或者超类)。这是一个重要的概念:在其他的语言中对象可能被要求属于某一个类,或者被要求实现某个接口,但python中只是简单地要求它遵守几个给定的规则。比如成为一个序列,你所需要做的只是遵循序列的规则
1)基本的序列和映射规则
序列和映射是对象的集合。为了实现它们基本的行为(规则),如果对象是不可变的,那么就需要使用两个魔法方法,如果是可变的则需要使用4个:
(1) __len__(self):这个方法应该返回集合中所含项目的数量。对序列而言就是元素的个数,对于映射则是键值对的数量。如果__len__返回0(并且没有实现重写该行为的__nonzero__),对象会被当作一个布尔变量中的假值(空的列表,元组,字符串和字典也一样)进行处理
(2) __getitem__(self,key):这个方法返回与所给键对应的值。对于序列,键应该是一个0~n-1的整数(或者负数),n是序列长度。对映射,可以使用任何种类的键
(3) __setitem__(self,key,value):这个方法按一定的方式存储和key相关的value。只能为可修改的对象定义这个方法
(4) __delitem__(self,key):这个方法在对一部分对象使用del语句时被调用,同时必须删除和元素相关的键。这个方法也是为可修改对象定义的(并不是删除全部的对象,而只删除一些需要移除的元素)
对于这些方法的附加要求:
(1) 对于一个序列来说,如果键是负整数,那么要从末尾开始计数。换句话说就是x[-n]和x[len(x)-n]是一样的
(2) 如果键是不合适的类型(如对序列使用字符串作为键),会引发一个TypeError异常
(3) 如果序列索引超过范围,引发一个IndexError异常
创建一个无穷序列:
def checkIndex(key): """ 所给的键是能接收的索引吗? 为了能被接受,键应该是一个非负的整数。如果它不是一个整数,会引发TypeError, 如果是负数,则会引发IndexError(因为序列是无限长的)。 """ #python3.x去掉了long类型,只有一种整型:int #if not isinstance(key, (int,long)):raise TypeError if not isinstance(key, int):raise TypeError if key < 0 : raise IndexError class ArithmeticSequence: def __init__(self, start = 0, step = 1): """ 初始化算术序列 起始值——序列中的第一个值 步长——两个相邻值之间的差别 改变——用户修改的值的字典 """ self.start = start #保存开始值 self.step = step #保存步长值 self.changed = {} #没有项被修改 def __getitem__(self, key): """ Get an item from the arithmetic sequence """ checkIndex(key) try:return self.changed[key] #修改了吗? except KeyError: #否则... return self.start + key*self.step #...计算值 def __setitem__(self, key, value): """ 修改算术序列中的一个项 """ checkIndex(key) self.changed[key] = value #保存更改后的值
这里没有实现__del__方法,是因为我希望删除元素是非法的。这个类没有__len__方法,因为它是无限长的
2)子类化列表,字典和字符串
标准库有3个关于序列和映射规则(UserList、UserString、UserDict)。如果希望实现一个和内建列表行为相似的序列,可以使用子类list
注意:当子类化一个内建类型——比如list的时候,也就间接地将object子类化了。因此的类就自动成为新式类,这就意味着可以使用像super函数这样的特性了
实例——带有访问计数的列表:
class CounterList(list): def __init__(self,*args): super(CounterList,self).__init__(*args) self.counter=0 def __getitem__(self, index): self.counter += 1 return super(CounterList,self).__getitem__(index) >>> c1 = CounterList(range(10)) >>> c1 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> c1.reverse() >>> c1 [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] >>> del c1[3:6] >>> c1 [9, 8, 7, 3, 2, 1, 0] >>> c1.counter 0 >>> c1[4] + c1[2] 9 >>> c1.counter 2
4:更多魔力——参加文档:http://docs.python.org/3/library/index.html
5:属性
class Rectangle: def __init__(self): self.width = 0 self.height = 0 def setSize(self,size): self.width,self.height = size def getSize(self): return self.width,self.height >>> r =Rectangle() >>> r.width=10 >>> r.height=5 >>> r.getSize() (10, 5) >>> r.setSize((150,100)) >>> r.width 150
Python能隐藏访问器方法,让所有特性看起来一样,这些通过访问器定义的特性成为属性
1)property函数(只在新式类中使用)
__metaclass__ = type class Rectangle: def __init__(self): self.width = 0 self.height = 0 def setSize(self, size): self.width,self.height = size def getSize(self): return self.width, self.height size = property(getSize, setSize) >>> r=Rectangle() >>> r.width = 10 >>> r.height = 5 >>> r.size (10, 5) >>> r.size = 150,100 >>> r.width 150
在新版的Rectangle中,property函数创建了一个属性,其中访问器函数被用作参数(先是取值,然后是赋值),这个属性命名为size。
property函数可以用0、1、2、3、4个参数来调用。如果没有参数,产生的属性既不可读,也不可写。如果值使用一个参数调用(一个取值方法),产生的属性是只读的。第3个参数(可选)是一个用于删除特性的方法(它不要参数)。第4个参数(可选)是一个文档字符串。property的4个参数分别被叫做fget、fset、fdel和doc
2)静态方法和类成员方法
静态方法和类成员方法在创建时分别被装入Staticmethod类型和Classmethod类型的对象中。静态方法的定义没有self参数,并且能够被类本身直接调用。类方法在定义时需要名为cls的类似于self的参数,类成员方法可以直接用类的具体对象调用。但cls参数是自动被绑定到类的:
class MyClass: def smeth(): print('This is a static method') smeth = staticmethod(smeth) def cmeth(cls): print('This is a class method of ', cls) cmeth = classmethod(cmeth)
python为staticmethod、classmethod这样的包装方法引入了一个叫装饰器(decorators)的新语法(它能对任何可调用的对象进行包装,既能用于方法也能用于函数(多个装饰器在应用时的顺序与指定顺序相反)
class MyClass2: @staticmethod def smeth(): print('This is a static method') @classmethod def cmeth(cls): print('This is a class method of ', cls)
3)__getattr__、__setattr__和它的朋友们
拦截(intercept)对象的所有特性访问是可能的,这样可以用旧式类实现属性(因为property方法不能使用)。为在访问特性的时候可以执行代码,必须使用一些魔法方法。下面4种方法提供了需要的功能(在旧式类中只需要后3个)
__getattribute__(self,name):当特性那么被访问时自动被调用(只能在新式类中使用)
__getattr__(self,name):当特性name被访问且对象没有相应的特性时被自动调用
__setattr__(self,name,value):当试图给特性name赋值时会被自动调用
__delattr__(self,name):当试图删除特性name时被自动调用
尽管和property相比更复杂(某些方面效率更低),但这些特殊方法很强大,因为可以对处理很多属性的方法进行再编码
class Rectangle2: def __init__(self): self.width = 0 self.height = 0 def __setattr__(self,name,value): if name == 'size' : self.width,self.height = value else: self.__dict__[name] = value def __getattr__(self,name): if name == 'size' : return self.width,self.height else: raise AttributeError
注意:
__setattr__方法所涉及的特性不是size时也会被调用。因此,如果属性是size,就像前面一样操作,否则就要使用特殊方法__dict__,该特殊方法包含一个字典,字典里是所有实例的属性。为避免__setattr__方法被再次调用(这样会使程序陷入死循环),__dict__方法被用代替普通的特性赋值操作
__getattr__方法只在普通的特性没有被找到时调用。就是说如果给定的名字不是size,这个方法会引发一个AttributeError异常
__getattribute__拦截所有特性的访问(在新式类中),也拦截对__dict__的访问。访问__getattribute__中与self相关的特性时,使用超类的__getattribute__方法(使用super函数)是唯一安全的途径
6:迭代器——__iter__,这个方法是迭代器规则(iterator protocol)的基础
1)迭代器规则:迭代的意思是重复做一些事很多次——就像在循环中做的那样。目前为止只是在for循环中对序列和字典进行迭代,但是实际上也能对其他对象迭代:实现__iter__方法的对象
__iter__方法返回一个迭代器(iterator),所谓的迭代器就是具有next方法(这个方法在调用时不需要任何参数)的对象。在调用next方法时,迭代器会返回它的下一个值。如果next方法被调用,但迭代器没有值可以返回,就会引发一个StopIteration异常
注意:Python3.0中,迭代器对象应该实现__next__方法,而不是next。而新的内建函数next可以用于访问这个方法。换句话说:next(it)等于3.0前的it.next()
迭代规则的关键是什么?为什么不使用列表?因为列表的杀伤力太大。如果有可以一个接一个地计算值的函数,那么在使用时可能是计算一个值时获取一个值——而不是通过列表一次性获取所有的值(如果有很多值,列表就会占用太多内存)。
>>> class Fibs: def __init__(self): self.a = 0 self.b = 1 def __next__(self):#python2.x => def next(self): self.a,self.b = self.b, self.a + self.b return self.a def __iter__(self): return self >>> fibs = Fibs() >>> for f in fibs: if f > 1000: print(f) break 1597
注意:迭代器实现了__iter__方法,这个方法实际返回迭代器本身。很多情况下,__iter__会放到其他的会再for循环中使用的对象中。这样程序就能返回所需的迭代器—— 一个实现了__iter__方法的对象是可迭代的,一个实现了next方法的对象则是迭代器
内建函数iter可以从可迭代的对象中获得迭代器:
it = iter([1,2,3]) #it.next() >>> next(it) 1 >>> next(it) 2
2)从迭代器得到序列:除了在迭代器和可迭代对象进行迭代外,还能把他们转换为序列。在大部分能使用序列的情况下(除了在索引或者分片等操作中),能使用迭代器(或可迭代对象)替换。
有用的例子:使用list构造方法显式地将迭代器转化为列表
class TestIterator: value = 0 def __next__(self): self.value += 1 if self.value > 0 : raise StopIteration return self.value def __iter__(self): return self
7:生成器——是一种用普通的函数语法定义的迭代器:yield
1)创建生成器:
>>> def flatten(nested): for sublist in nested: for element in sublist: yield element >>> nested = [[1,2],[3,4],[5]] #注意5必须要用[]括起来,否则就是int类型,而不是列表。int类型是不能迭代的 >>> for num in flatten(nested): print(num) 1 2 3 4 5
任何包含 yield 语句的函数称为:生成器。每次产生一个值(yield语句),函数就会被冻结:即函数停在那点等待被激活。还是被激活后就从停止那点开始执行
循环生成器:生成器推导式(或称生成器表达式)和列表推导式的工作方式类似,只不过返回的不是列表而是生成器(并且不会立刻进行循环),所以返回的生成器允许你像下面这样一步步地进行运算:
>>> g = ((i+2)**2 for i in range(2,27)) >>> next(g) 16
和列表推导式不同的就是普通圆括号的使用方式,在这样简单的例子中,还是推荐使用列表推导式。但如果行为将可迭代对象(例如生成大量的值)“打包”,那么最好不要使用列表推导式,因为它会立刻实例化一个列表,从而丧失迭代的优势。
更妙的地方在于生成器推导式可以在当前的圆括号内直接使用:
>>> sum(i**2 for i in range(10)) 285
2)递归生成器:上一节创建的生成器只能处理两层嵌套,为了处理嵌套使用了两个for循环。如果要处理任意层的嵌套怎么办?例如,可能要使用来表示树形结构,就必须用递归(recursion)
def flatten(nested): try: for sublist in nested: for element in flatten(sublist): yield element except TypeError: yield nested >>> list(flatten([[[1],2],3,4,[5,[6,7]],8])) [1, 2, 3, 4, 5, 6, 7, 8]
注意:不应该在flatten函数中对类似于字符串的对象进行迭代,首先,需要实现的是将类似字符串的对象当成原子值,而不是当成应被展开的序列。其次,对它们进行迭代实际上会导致无穷递归,因为一个字符串的第一个元素是另一个长度为1的字符串,而长度为1的字符串的第一个元素就是字符串本身
为处理这种情况,则必须在生成器的开始处添加一个检查语句。将传入对象和一个字符串拼接,看会不会出现TypeError,这是检查一个对象是不是类似字符串的最简单、最快速的方法(也可以使用isinstance来检查):
def flatten(nested): try: #不要迭代类似字符串的对象: try:nested + '' except TypeError:pass else:raise TypeError for sublist in nested: for element in flatten(sublist): yield element except TypeError: yield nested >>> list(flatten(['foo',['bar', ['baz']]])) ['foo', 'bar', 'baz']
3)通用生成器:生成器由两部分组成:生成器的函数和生成器的迭代器。生成器的函数是用def语句定义,包含yield部分。生成器的迭代器是这个函数返回的部分:
>>> def simple_generator(): yield 1 >>> simple_generator <function simple_generator at 0x0300C660> >>> simple_generator() <generator object simple_generator at 0x02F88AF8>
4)生成器方法:生成器的新属性(2.5中引入)是在开始运行后为生成器提供值的能力。表现为生成器和“外部世界”进行交流的渠道,要注意下面两点:
A:外部作用域访问生成器的send方法,就像访问next方法一样,只不过前者使用一个参数(要发送的“消息”——任意对象)
B:在内部则挂起生成器,yield现在作为表达式而不是语句使用,即,当生成器重新运行的时候,yield方法返回一个值,也就是外部通过send方法发送的值。如果next方法被使用,那么yield方法返回None
注意,使用send方法(而不是next方法)只有在生成器挂起之后才有意义(意即yield函数第一次被执行之后)。如果想对刚刚启动的生成器使用send方法,那么可以讲None作为其参数进行调用:
def repeater(value): while True: new = (yield value) #安全起见,使用返回值的时候,要闭合yield表达式 if new is not None:value = new >>> r=repeater(42) >>> next(r) 42 >>> r.send("Hello,world!") 'Hello,world!'
生成器还有其他两个表达式:
throw 方法:(使用异常类型调用,还有可选的值以及回溯对象)用于在生成器内引发一个异常(在yield表达式中)
close 方法:(调用时不用参数)用于停止生成器(在需要的时候也会由Python垃圾收集器调用),也是建立在异常的基础上的。它在yield运行处引发一个GeneratorExit异常。试着在生成器的close方法被调用后再通过生成器生成一个值会导致RuntimeError异常
5)模拟生成器:使用普通的函数模拟生成器
def flatten(nested): result = [] #First Step try: #不要迭代类似字符串的对象 try:nested + '' except TypeError:pass else:raise TypeError for sublist in nested: for element in flatten(sublist): result.append(element) #用此句代码代替原来的 yield some_expression except TypeError: result.append(nested) return result #在函数末尾,添加这个语句 >>> list(flatten(['foo',['bar', ['baz']]])) ['foo', 'bar', 'baz']
8:八皇后问题
1)生成器和回溯:生成器是逐渐产生结果的复杂递归算法的理想实现工具。回溯策略在解决需要尝试每种组合,直到找到一种解决方案的问题时很有用:
#伪代码 第1层所有的可能性: 第2层所有的可能性: ...... 第n层所有的可能性: 可行吗?
为直接使用for循环来实现,就必须知道会遇到的具体判断层数,如果无法得知层数信息,就使用递归
2)问题:有一个棋盘和8个要放到上面的皇后,唯一要求是皇后之间不能形成威胁
这是一个典型的回溯问题:首先尝试放置第一个皇后(在第一行),然后放置第2个,以此类推。如果发现不能放置下一个皇后,就回溯上一步,试着将皇后放到其他的位置。最后,或者尝试完所有的可能或者找到解决方案
3)状态表示:为了表示一个可能的解决方案(或方案的一部分),可以使用元组(或列表:如果序列很小而且是静态的,元组是更好的选择)。每个元组中元素都指示相应行的皇后的位置(也就是列)。如果state[0]==3就表示第一行的皇后在第四列。
4)寻找冲突:为了找到没有冲突的位置,首先必须定义冲突是什么。把它定义成一个函数:
我们假设i=5 ,那么state[i]就是棋盘第6行的皇后的位置了, 那么abs(state[i]-nextX)就是当前皇后和6号皇后的水平位置间隔, 很明显,当间隔是0的时候也就是在同一列时,被吃 ;假设当前皇后是第7行,那么abs(state[i]-nextX)=abs(5-6)=1 = nextY-i=7-6,也就是说两者行列间隔均为等间隔,那就是对角线,被吃。
def conflict(state, nextX): nextY = len(state) for i in range(nextY): if abs(state[i] - nextX) in (0, nextY - i): #0表示在同一列,nextY - i表示对角线 return True return False
已知的皇后的位置被传递给conflict函数(以状态元组state的形式),然后由函数判断下一个的皇后的位置会不会有新的冲突。
参数nextX表示下一个皇后的水平位置(x坐标或列),nextY表示垂直位置(y坐标或行)。这个函数对对前面的每个皇后的位置做一个简单的检查,如果下一个皇后和前面的皇后有同样的水平位置,或者是在一条对角线上,就会发生冲突,接着返回True,否则返回False
5)基本情况
从基本情况开始:最后一个皇后。假设你想找出所有可能的解决方案,这样一来,它能根据其他皇后的位置生成自己能占据的所有位置(可能没有)
def queens(num, state): if len(state) == num - 1: for pos in range(num): if not conflict(state, pos): yield pos
用人类语言描述:如果只剩一个皇后没有位置,那么遍历它的所有可能位置,并且返回没有冲突发生的位置。num是皇后的总数,state是存放前面皇后的位置信息的元组
6)需要递归的情况:完成基本情况后,递归函数会假定(通过归纳)所有的来自底层(有更高编号的皇后)的结果都是正确的。因此需要做的就是为前面的queen函数的实现中的if语句增加else
def queens(num=8,state=()): for pos in range(num): if not conflict(state,pos): if len(state) == num-1: yield (pos,) else: for result in queens(num, state+(pos,)): yield (pos,) + result
(1)函数的定义,num是所有皇后的数量,state是用来存储语句被放置的皇后的位置信息的元组
(2)对于当前放置的皇后来说,要对前面放过的皇后的列进行判断,看能不能放下
(3)pos这个位置是可以放置的话
(4,5)如果当前的皇后已经是最后一个要放置的皇后的话,就可以直接给出她的位置了,并放进state元组里
(6,7,8)如果不是最后一个皇后,那么不但要给出当前皇后可放的pos,同时还要在此基础上继续计算下面的皇后的pos,直到进入(4,5)条件,那么所有方案就生成了
打印任意一种解决方案:
def prettyprint(solution): def line(pos, length=len(solution)): return '. '*pos + 'Q ' + '. '*(length-pos-1) for pos in solution: print(line(pos)) >>> import random >>> lst = list(queens(8)) >>> printResult(random.choice(lst))
>>> len(list(queens(8)))
92
小结
旧式类和新式类:提供如super函数和property函数等新特性,为创建新式类,必须直接或间接子类化object,或者设置__metaclass__属性
魔法方法:名字是以双下划线开始和结束。和函数只有很小的不同,但其中的大部分方法在某些情况下被Python自动调用(如__init__在对象被创建后调用)
构造方法:__init__
重写:一个类能通过实现方法来重写它超类中定义的这些方法和属性。可以从超类(旧式类)直接调用未绑定的版本或者使用super函数(新式类)
序列和映射:创建自己的序列或者映射需要实现所有的序列或是映射规则的方法,包括__getitem__和__setitem__这样的特殊方法。也可以通过子类化list(或UserList)和dict(或UserDict)
迭代器:迭代器是带有next方法的简单对象。当没有值可供迭代时,next方法就会引发StopIteration异常。可迭代对象有一个返回迭代器的__iter__方法,它能像序列那样在for循环中使用。一般的,迭代器本身也是可迭代的,即迭代器有返回它自己的next方法
生成器:生成器函数(或方法)是包含了关键字yield的函数(或方法)。当被调用时,生成器函数返回一个生成器(一种特殊的迭代器)。可以使用send、throw、close方法让活动生成器与外界交互
八皇后问题:使用生成器可轻松解决这个问题。问题描述是如何在棋盘上放置8个皇后,使其不会互相攻击
第十章:充电时刻
1:模块——python的标准按照包括一组模块,称为标准库(standard libraty)
1)模块是程序:任何程序都可以作为模块导入。
#hello.py print("Hello,world!")
假设将它保存在C:\python(windows)或 ~/python(UNIX/Mac OS X)目录中,接着就可以执行下面的代码,告诉解释器在哪里寻找模块了(windows):
>>> import sys >>> sys.path.append('c:/python')
在UNIX系统中,不能只是简单的将字符串'~/python'添加到sys.path中,必须使用完整的路径(如:/home/yourusername/python).如果希望这个操作自动化,可以使用:sys.path.expanduser('~python')
除了从目录中寻找外,还需要从目录c:\python中寻找模块:
>>> import hello Hello, world!
在导入模块后,会看到新文件出现:hello.pyc。这个以.pyc为扩展名的文件是(平台无关的)编译后的、已经转换成python能更加有效处理的文件
再次导入模块,就不会输出“Hello,world!”。因为导入模块并不意味着在导入时执行某些操作,它主要用于定义,比如变量、函数和类等
这种“只导入一次”(import-only-once)行为是一种实质性优化,对于两个模块互相导入的情况时尤其重要
2)模块用于定义
综上所述,模块在第一次导入到程序时被执行。真正的用处在于它们(像类一样)可以保持自己的作用域。这就意味着定义的所有类和函数以及赋值后的变量都变成了模块的特性。
(1)在模块中定义函数:为了让代码可重用,请将它模块化
(2)在模块中增加测试代码:__name__变量:“告知”模块本身是作为程序运行还是导入到其他程序
#hello.py def hello(): print('Hello,world!') def test(): hello() if __name__ = '__main__':test()
3)让你的模块可用:
(1)将模块放置在正确位置:找出python解释器从哪里查找模块,然后将自己的文件放置在那里即可
>>> import sys, pprint >>> pprint.pprint(sys.path) ['', 'C:\\Python33\\Lib\\idlelib', 'C:\\Windows\\system32\\python33.zip', 'C:\\Python33\\DLLs', 'C:\\Python33\\lib', 'C:\\Python33', 'C:\\Python33\\lib\\site-packages']
如果数据结构过大,不能在一行内打印完,可以用pprint模块中的pprint函数替代普通的print语句
尽管上面的目录都可用,但site-packages是最佳选择,因为它就是用来做这些事的(注意放置进去的模块文件名应类似:another_hello.py)。
(2)告诉编译器去哪里找:
上面的方案可能不适用于:不希望将自己的模块填满python解释器的目录、没有权限、想将模块放在其他地方
标准实现方法:在 PYTHONPATH 环境变量中包含模块所在的目录。PYTHONPATH环境变量的内容会因操作系统不同而有差异
4)包:为组织好模块,可以讲它们分组为包(package)。包基本上是另外一类模块,有趣的地方就是它们能包含其他模块。当模块存储在文件中时(扩展名为.py),包就是模块所在的目录。为了让python将其作为包对待,它必须包含一个命名为__init__py的文件(模块)。
比如要建立一个叫:drawing的包,其中包括名为shapes和colors的模块,你就需要重建下标中所示的文件和目录:
2:探究模块
1)模块中有什么
(1)使用dir:
>>> import copy >>> dir(copy) ['Error', 'PyStringMap', '_EmptyClass', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__initializing__', '__loader__', '__name__', '__package__', '_copy_dispatch', '_copy_immutable', '_copy_with_constructor', '_copy_with_copy_method', '_deepcopy_atomic', '_deepcopy_dict', '_deepcopy_dispatch', '_deepcopy_list', '_deepcopy_method', '_deepcopy_tuple', '_keep_alive', '_reconstruct', 'builtins', 'copy', 'deepcopy', 'dispatch_table', 'error', 'name', 't', 'weakref']
一些名字以下划线开始——约定成俗它们并不是为在模块外部使用准备的:
>>> [n for n in dir(copy) if not n.startswith('_')] ['Error', 'PyStringMap', 'builtins', 'copy', 'deepcopy', 'dispatch_table', 'error', 'name', 't', 'weakref']
(2)__all__变量:定义了模块的共有接口(public interface)
>>> copy.__all__ ['Error', 'copy', 'deepcopy']
用import *语句也将默认输出模块中所有不以下划线开头的全局名称:
from copy import *
2)用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. >>> print(copy.copy.__doc__) Shallow copy operation on arbitrary Python objects. See the module's __doc__ string for more info.
deepcopy(x)会将存储在x中的值作为属性进行复制,而copy(x)只是复制x,将x中的值绑定到副本的属性上
3)文档:模块的信息来源是文档(下面获取了关于range函数的精确描述)
print(range.__doc__)
4)使用源代码(检查模块的__file__属性):一些源代码是C
>>> print(copy.__file__) C:\Python33\lib\copy.py
3:标准库
1)sys——这个模块能让你访问与Python解释器联系紧密的变量和函数:
函数sys.exit 可以退出当前程序(如果在try/finally中调用,finally子句的内容仍然会被执行)。你可以提供一个整数作为参数,用来标识程序是否成功运行——UNIX的一个惯例。多数情况使用整数的默认值:0表示成功。也可以提供字符串参数,用作错误信息
>>> import sys >>> args = sys.argv[1:] >>> args.reverse() >>> print(' '.join(args)) #不能打印出操作结果——这是个返回None的原地修改操作 >>> print(' '.join(reversed(sys.argv[1:]))
2)os——提供了访问多个操作系统服务的功能
sep模块变量是用于路径中的分隔符。UNIX中的标准分隔符是“/”,windows中是“\\”
pathsep用于分隔路径名:UNIX使用“:”,windows使用“;”,Mac OS 使用“::”
linesep用于文本文件的字符串分隔符。UNIX(以及Mac OS)为一个换行符(\n),Mac OS中为单个回车符(\r),而windows为两者组合(\r\n)
如启动网络浏览器程序:
>>> os.system('/user/bin/firefox') #UNIX >>> os.system(r'c:\"program Files"\"Mozilla Firefox"\firefox.exe')
上面将Program Files和Mozilla Firefox放入引号中,否则DOS(它负责处理这个命令)就会因为空格处停下来(对于在PYTHONPATH中设定的目录来说,这点同样重要)。windows特有的函数:
os.startfile(r'C:\Program Files (x86)\Mozilla Firefox\firefox.exe')
大多数情况下,os.system函数很有用,但是对于启动浏览器这样特定的任务来说有更好的解决方案:webbrowser模块。它也包括open函数,它可以启动Web浏览器访问给定的URL
>>> import webbrowser >>> webbrowser.open('http://www.python.org')
3)fileinput——fileinput模块能让你轻松遍历文本文件的所有行。如果通过以下方式调用脚本(假设在UNIX命令行下):
$ python some_script.py file1.txt file2.txt file3.txt
这样就可以依次对file1,2,3文件中的所有行进行遍历。还能对提供给标准输入(sys.stdin)的文件进行遍历。如在UNIX管道中,使用标准的UNIX命令cat:
$ cat file.txt | python some_script.py
如果使用fileinput模块,在UNIX管道中使用cat来调用脚本的效果和将文件名作为命令行参数提供给脚本是一样的。
fileinput.input是其中最重要的函数。它会返回能够用于for循环遍历的对象。如果不想使用默认行为(fileinput查找需要循环遍历的文件),那么可以给函数提供(序列形式的)一个或多个文件名。还能将inplace参数设为真值(inplace=True)以进行原地处理。在进行原地处理时,可选的backup参数将文件名扩展备份到通过原始文件创建的备份文件中。
fileinput.lineno函数返回当前行得行数。这个数值是累计的,所以在完成一个文件的处理并且开始处理下一个文件的时候,行数并不会重置
fileinput.filelineno函数返回当前处理文件的当前行数。每次处理完一个文件并且开始处理下一个文件时,行数会重置为1
fileinput.isfirstline函数在当前行是当前文件的第一行时返回真值,否则返回假值
fileinput.isstdin函数在当前文件为sys.stdin时返回真值,否则返回假值
fileinput.nextfile函数会关闭当前文件,跳到下一个文件,跳过的行不计
file.close函数关闭整个文件链,结束迭代
实例:为Python脚本添加行号:
import fileinput for line in fileinput.input(inplace=True): line = line.rstrip() num = fileinput.lineno() print('%-40s # %2i ' % (line,num))
4)集合、堆和双端队列
python支持通用的数据结构类型,例如字典(或者说散列表)、列表(或者说动态数组),还有一些不常用的:
(1)集合:集合是由序列(或者其他可迭代的对象)构建的。它们主要用于检查成员资格,因此副本是被忽略的
>>> set(range(10)) {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} >>> set([0,1,2,3,0,1,2,3,4,5]) {0, 1, 2, 3, 4, 5}
和字典一样,集合元素顺序也是随意的。除了检查成员资格,还可以使用标准的集合操作,如:
add、remove、union、issubset、issupperset、intersetction、difference、symmetric_difference、copy
如果需要一个函数,用于查找并且打印两个集合的并集,可以用来自set类型的union方法的未绑定版本
>>> import functools >>> mySets=[] >>> for i in range(10): mySets.append(set(range(i,i+5))) >>> functools.reduce(set.union,mySets) {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}
集合是可变的,所以不能用作字典中的键。集合本身只能包含不可变的(可散列的)值,所以也就不能包含其他集合。实际应用中,集合的集合是很有用的,有个frozenset类型,用于代表不可变(可散列)的集合:
>>> a=set() >>> b=set() >>> a.add(frozenset(b))
(2)堆:heap,它是优先队列的一种。使用优先队列能够以任何顺序增加对象,并且能在任何时间(可能在增加对象的同时)找到(也可能是移除)最小的元素,也就是说它比用于列表的min方法更有效率
事实上,python中并没有独立的堆类型——只有一个包含一些堆操作函数的模块,这个模块叫做heapq(q是queue的缩写:队列)。你必须将列表作为堆对象本身:
>>> from heapq import * >>> from random import shuffle >>> data = list(range(10)) >>> shuffle(data) >>> heap = [] >>> for n in data: heappush(heap,n) >>> heap [0, 1, 6, 4, 2, 7, 8, 5, 9, 3]
元素的顺序虽然不是严格排序的,但是也有规则:位于i位置上的元素总比i//2位置处的元素大(反过来说就是i位置处的元素总比 2*i 及 2*i+1 处的元素小。这是底层堆算法的基础,这个特性被称为堆属性(heap property)
heappop这个函数弹出最小的元素——一般都是索引0处的元素,并且确保剩余元素中最小的那个占据这个位置(保持堆属性)
heapify函数使用任意列表作为参数,并且通过尽可能少的移位操作,将其转换为合法的堆(事实上是应用到了刚才提到的堆属性)。如果没有用heappush建立堆,那么在使用heappush和heappop前应该使用这个函数:
>>> from heapq import * >>> 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更高效:
>>> 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]
nlargest(n,iter)和nsmallest(n,iter)分别用来寻找任何可迭代对象iter中第n大或第n小的元素(比使用排序:如sorted函数和分片来完成这个工作,但堆算法更快而且更有效的使用内存)
(3)双端队列(以及其他集合类型)
双端队列(Double-ended queue,或称deque)在需要按照元素增加的顺序来移除元素时非常有用。deque类型在collections模块中(Python2.5中的collections模块只包含deque类型和defaultdict类型——为不存在的键提供默认值的字典,未来可能会加入二叉树B-Tree和斐波拉契堆Fibonacci heap)
双端队列通过可迭代对象(如集合)创建:
>>> 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])
双端队列好用的原因是它能够有效地在开头(左侧)增加和弹出元素,这是在列表中无法实现的。它还能旋转(rotate)元素(也就是将它们左移或右移,使头尾相连)。还有extend和extendleft方法,extendleft使用的可迭代对象中的元素会反序出现在双端队列中
5)time——time模块包括以前功能函数:获取当前时间、操作时间日期、从字符串读取时间以及格式化时间为字符串
日期可以用实数(从”新纪元(UNIX是1970)“的1月1日0点开始计算到现在的秒数),或是包含有9个整数的元组:
(2008,1,21,12,2,56,0,21,0)
>>> import time >>> time.asctime() 'Wed Jan 23 16:00:38 2013'
6)random——random模块包括返回随机数的函数(事实上,所产生的数字都是伪随机数(pseudo-random)
函数random.random是最基本的随机函数,它只是返回0~1的伪随机数n。
random.getrandbits以长整型形式返回给定的位数(二进制数),如果处理的是真正的随机事务(如加密),这个函数尤其有用
random.uniform(a,b)返回a~b的随机(平均分布的)实数n。如需要随机的角度值,可以用:uniform(0,360)。(还有些与uniform类似的函数,它们会根据其他各种不同的分布规则进行抽取,从而返回随机数。这些分布包括:贝塔分布、指数分布、高斯分布等)
randrange一个范围内的随机整数。比如想要获得1~10(包括10)的随机数,可以使用randrange(1,11)。获得小于20的随机正奇数:randrange(1,20,2)
random.shuffle将给定(可变)序列的元素进行随机移位,每种排列的可能性都是近似相等的
random.sample从给定序列中(均一地)选择给定数目的元素,同时确保元素互不相同
>>> values = list(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[1:12]) ['9 of clubs', '2 of diamonds', 'Queen of diamonds', '3 of diamonds', '3 of spades', '1 of hearts', 'King of spades', '7 of clubs', '4 of clubs', '4 of spades', '6 of clubs']
7)shelve——需要一个简单的在文件中存储数据的方案,shelve模块可以满足大部分需要。
(1)shelve.open函数,使用文件名作为参数,它会返回一个Shelf对象,可以用它来存储内容。只需要把它当做普通的字典(但键一定要作为字符串)来操作即可,在完成工作(并且将内容存储到磁盘中)之后,调用它的close方法
潜在的陷阱:注意shelve.open函数返回的对象并不是普通的映射,如下面的例子:
>>> import shelve >>> s = shelve.open('test.dat') >>> s['x'] = ['a','b','c'] >>> s['x'].append('d') >>> s['x'] ['a', 'b', 'c']
'd'哪去了?当你在shelve对象中查找元素时,这个对象都会根据已经存储的版本进行重新构建,当你将元素赋给某个键的时候,它就被存储了,上例执行操作:
A:列表['a','b','c']存储在键x下
B:获得存储的表示,并且根据它来创建新的列表,而'd'被添加到这个副本中。修改的版本还没被保存
C:再次获得原始版本——没有'd'
为正确使用shelve模块修改存储的对象,必须将临时变量绑定在获得的副本上,并且在它被修改后重新存储这个副本:
>>> temp = s['x'] >>> temp.append('d') >>> s['x'] = temp >>> s['x'] ['a', 'b', 'c', 'd']
python2.4后的版本还有个解决方法:将open函数的writeback参数设为true。如果这样做,所有从shelf读取或者赋值到shelf的数据结构都会保存在内存(缓存)中,并且只有在关闭shelf的时候才写回到磁盘中。
实例:简单的数据库应用程序
import sys, shelve def store_person(db): """ Query user for data and store it in the shelf object """ 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): """ Query user for ID and desired field,and fetch the corresponding data from the shelf object """ pid = input('Enter ID number: ') field = input('What would you like to know?(name,age,phone)') filed = 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('D:\\database.dat') try: while True: cmd = enter_command() if cmd == 'store': store_person(database) elif cmd == 'lookuo': lookup_person(database) elif cmd == '?': print_help() elif cmd == 'quit': return finally: database.close() #主程序放在main函数中,只有在if __name__ == '__main__'时才被调用。 #这意味着可以在其他程序中将这个程序作为模块导入,然后调用main函数 if __name__ == '__main__' : main()
8)re模块——正则表达式(regular expression)
(1)通配符:如点号
对特殊字符进行转义:python\\.org
字符集:[a-zA-z0-9]匹配任意大小写字母和数字。反转字符集,可以在开头使用^字符:[^abc]匹配除a、b、c之外的字符
(2)
函数re.compile将正则表达式(以字符串书写的)转换为模式对象,可以实现更有效率的匹配。如果调用search或match函数的时候使用字符串表示的正则表达式,它们也会在内部将字符串转换为正则表达式对象。使用compile完成一次转换后,在每次使用模式的时候就不用进行转换。模式对象本身也有查找/匹配函数,就像方法一样,所以re.search(pat,string)(pat是用字符串表示的正则表达式)等价于pat.search(string)(pat是compile创建的模式对象)
函数re.search会再给定字符串中寻找第一个匹配给定正则表达式的字符串。找到返回MatchObject(值为True),否则返回None
函数match会再给定字符串的开头匹配正则表达式。因此,match('p','python')返回真(即匹配对象MatchObject),而re.match('p','www.python')返回假。如果要求模式匹配整个字符串,可以在模式的结尾加上美元符号。美元符号会对字符串的末尾进行匹配,从而“顺延”了整个匹配
函数re.split会根据模式的匹配项来分割字符串。如允许用任意长度的逗号和空格序列来分割字符串:
>>> import re >>> some_text = 'alpha. beta....gamma delta' >>> re.split('[. ]+', some_text) ['alpha', 'beta', 'gamma', 'delta']
如果模式包含小括号,那么括起来的字符组合会散布在分割后的子字符串之间。例如,re.split('o(o)', 'foobar')会生成['f','o','bar']
>>> re.split('o(o)', 'foobar') ['f', 'o', 'bar'] >>> re.split('o', 'foobar') ['f', '', 'bar'] >>> re.split('o(b)', 'foobar') ['fo', 'b', 'ar'] >>> re.split('o(a)', 'foobar') ['foobar']
从上述例子可以看出,返回值是子字符串的列表。maxsplit参数表示字符串最多可以分割成的部分数:
>>> re.split('[. ]+', some_text, maxsplit=2) ['alpha', 'beta', 'gamma delta'] >>> re.split('[. ]+', some_text, maxsplit=1) ['alpha', 'beta....gamma delta']
函数re.findall 以列表形式返回给定模式的所有匹配项。
函数re.sub作用:使用给定的替换内容将匹配模式的子字符串(最左端并且非重叠的子字符串)替换掉:
>>> pat = '{name}' >>> text = 'Dear {name}...' >>> re.sub(pat,'Mr.Gumby',text) 'Dear Mr.Gumby...'
re.escape是个很实用的函数,对字符串中所有可能被解释为正则运算符的字符进行转义
>>> re.escape('www.python.com') 'www\\.python\\.com' >>> re.escape('But where is the ambiguity?') 'But\\ where\\ is\\ the\\ ambiguity\\?'
(3)匹配对象和组
找到匹配项时,返回的都是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
>>> 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函数中使用VERBOSE标志。它允许在模式中添加空白(空白字符、tab、换行符等),re则会忽略它们——除非将其放在字符类或者用反斜线转义。也可以在正则表达式中添加注释:
>>> #emphasis_pattern = r'\*([^\*]+)\*' >>> emphasis_pattern = re.compile(r''' \* #Beginning emphasis tag -- an asterisk ( #Begin group fro capturing phrase [^\*]+ #Capture anything except asterisks ) #End group \* #Ending emphasis tag ''', re.VERBOSE) >>> re.sub(emphasis_pattern, r'<em>\1</em>', 'Hello, *world*!') 'Hello, <em>world</em>!'
注意:重复运算符默认是贪婪的(greedy),这意味着它将尽可能多的东西据为己有
>>> import re >>> emphasis_pattern = r'\*(.+)\*' >>> re.sub(emphasis_pattern, r'<em>\1</em>', '*This* is *it*!') '<em>This* is *it</em>!'
如何避免过于贪婪,事实上非常简单,只要使用重复运算符的非贪婪版本。所有的重复运算符都可以通过在其后面加上一个问号变成非贪婪版本:
>>> emphasis_pattern = r'\*\*(.+?)\*\*' >>> re.sub(emphasis_pattern, r'<em>\1</em>', '**This** is **it**!') '<em>This</em> is <em>it</em>!'
这里用+?运算符代替了+,意味着模式也会像之前那样对一个或多个通配符进行匹配,但它会进行尽可能少的匹配,因为它是非贪婪的。它仅会在到达'\*\*'的下一个匹配项之前匹配最少的内容——也就是在模式的结尾进行匹配
(5)找出Email的发信人
#find_sender.py import fileinput,re ''' 用compile函数处理了正则表达式,让处理过程更有效率 将需要取出的子模式放在圆括号中作为组 使用非贪婪模式对邮件地址进行匹配,那么只有最后一对尖括号符合要求(当名字也包含了尖括号的情况下) 使用美元符号表明要匹配整行 使用if语句确保在试图从特定组中取出匹配内容之前,的确进行了匹配 ''' pat = re.compile('From: (.*)<.*?>$') for line in fileinput.input(): m = pat.match(line) if m : print(m.group(1))
为了列出头部信息中所有的Email地址,需要建立只匹配Email地址的正则表达式,然后用findall方法寻找每行出现的匹配项,为避免重复,将地址保存在集合中
pat = re.compile(r'[a-z\-\.]+@[a-z\-\.]+', re.IGNORECASE) address = set() for line in fileinput.input(): for address in pat.findall(line): address.add(address) for address in sorted(address): print(address)
(6)模板系统示例
python有一种高级的模板机制:字符串格式。但是使用正则表达式可以让系统更加高级:
#templates.py import fileinput, re #匹配中括号里的字段: field_pat = re.compile(r'\[(.+?)\]') #我们将变量收集到这里: scop = {} #用于re.sub中: def replacement(match): code = match.group(1) try: #如果字段可以求值,返回它: return str(eval(code, scope)) except SyntaxError: #否则执行相同作用域内的赋值语句 exec(code,scop) #返回空字符串 return '' #将所有文本以一个字符串的形式获取(还有其他办法,参见第11章): lines = [] for line in fileinput.input(): lines.append(line) text = ''.join(lines) #将field模式的所有匹配项都替换掉 print(field_pat.sub(replacement, text))
9)其他标准模板
(1)functools:filter和reduce包含在该模块中
(2)difflib:这个库让你可以计算两个序列的相似程度
(3)hashlib:
(4)csv:CSV是逗号分隔值(Comma-Separated Values)的缩写
(5)timeit、profile和trace:
(6)datetime:
(7)itertools:
(8)logging:
(9)getopt和optparse:
(10)cmd:使用这个模块可编写命令行解释器
小结
模块:基本来说,模块就是子程序,它的主函数则用于定义,包括定义函数、类和变量。如果模块包含测试代码,则应该将这部分代码放置在检查__name__='__main__'是否为真的if语句中。
包:包是包含有其他模块的模块。包是作为包含__init__.py文件的目录来实现的
标准库