希望通过博客园持续的更新,分享和记录Python基础知识到高级应用的点点滴滴!
第九波:第9章 魔法方法、属性和迭代器
在Python中,有的名称会在前面和后面都加上两个下划线,这种写法很特别。已经出现过一些这样的名称(比如__future__),这种拼写表示名字有特殊含义,所有绝不要在自己的程序中使用这种名字。
在Python中,由这些名字组成的集合所包含的方法称为魔法方法(或称特殊方法)。如果对象实现了这些方法中的某一个,那么这个方法会在特殊的情况下被Python调用,而几乎没有直接调用它们的必要。
讨论一些重要的魔法方法(最重要的是__init__方法和一些处理对象访问的方法,这些方法允许你创建自己的序列或者是映射)。还会处理两个相关的主题:属性(在以前版本的Python中通过魔法方法来处理,现在则通过property函数)和迭代器(使用魔法方法__iter__来允许迭代器在for循环中使用)。
[9.1] 准备工作
在Python3.0中没有“旧式”的类,也不需要显式地子类化object或者将元类设置为type(__metaclass__=type)。所有的类都会隐式地称为object的子类---如果没有明确超类的话,就会直接子类化;否则会间接子类化。
[9.2] 构造方法
首先要讨论的第一个魔法方法是构造方法。构造方法是一个很奇特的名字,它代表着类似那种名为init的初始化方法。但构造方法和其他普通方法不同的地方在于,当一个对象被创建后,会立即调用构造方法。
例如:
>>> f=FooBar()
>>> f.init()
构造方法能让它简化成如下形式:
>>> f=FooBar()
在Python中创建一个构造方法很容易。只要把init方法的名字从简单的init修改为魔法版本__init__即可:
class FooBar():
def __init__(self):
self.somevar=42
在Python所有的魔法方法中,__init__hi使用最多的一个。有一个魔法方法叫做__del__,也就是析构方法。它在对象就要被垃圾回收之前调用,但发生调用的具体时间是不可知的。所以建议读者尽力避免使用__del__函数。
[9.2.1] 重写一般方法和特殊的构造方法
每个类都可能拥有一个或者多个超类,它们从超类那里继承行为方式。如果一个方法在B类的一个实例中被调用(或一个属性被访问),但在B类中没有找到该方法,那么就会去他的超类A里面找。
class A:
def hello(self):
print "Hello,I'm A!"
class B(A):
pass
A类定义了一个叫做hello的方法,被B类继承。因为B类没有没有定义自己的hello方法,所以当hello被调用时,就会去超类A里面找hello方法。
在子类中增加功能的最基本的方式就是增加方法。但是也可以重写一些超类的方法来自定义继承的行为。B类也能重写这个方法。比如下面的例子中B类的定义被修改了。
class B(A):
def hello(self):
print "Hello,I'm B"
重写是继承机制中的一个重要内容,对于构造方法尤其重要。构造方法用来初始化新创建对象的状态,大多数子类不仅要拥有自己的初始化代码,还要拥有超类的初始化代码。虽然重写的机制对于所有方法来说都是一样的,但是当处理构造方法比重写普通方法时,更可能遇到特别的问题:如果一个类的构造方法被重写,那么就需要调用超类的构造方法,否则对象可能不会被正确的初始化。
考虑下面的Bird类:
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.sound='Squawk!'
def sing(self):
print self.sound
因为SongBird是Bird的一个子类,它集成了eat方法,但如果调用eat方法就会产生一个问题:
>>>sb.eat()
AttributeError:SongBird instance has no attribute 'hungry'
异常很清楚地说明了错误:SongBird没有hungry特性。原因是这样的:在SongBird中,构造方法被重写,但新的构造方法没有任何关于初始化hungry特性的代码。为了达到预期的效果,SongBird的构造方法必须调用其超类Bird的构造方法来确保进行基本的初始化。有两种方法能达到这个目的:调用超类构造方法的未绑定版本,或者使用super函数。
[9.2.2] 调用未绑定的超类构造方法
其实调用茶类的构造方法很容易,也很有用。下面给出上一节末尾提出的问题的解决方法。
class SongBird(Bird):
def __init__(self):
Bird.__init__(self)
self.sound='Squawk!'
def sing(self):
print self.sound
SongBird类中只添加了一行代码Bird.__init__(self)。为什么会有这样的结果呢?在调用一个实例的方法时,该方法的self参数会被自动绑定到实例上(这称为绑定方法)。但如果直接调用类的方法(比如Bird.__init__),那么久没有实例会被绑定。这样就可以自由地提供需要的self参数。这样的方法称为未绑定方法。
通过将当前的实例作为self参数提供给未绑定方法,SongBird就能够使用其超类构造方法的所有实现,也就是说属性hungry能被设置。
[9.2.3] 使用super函数
当前的类和对象可以作为super函数的参数使用,调用函数返回的对象的任何方法都是调用超类的方法,而不是当前类的方法。那么就可以不用再SongBird的构造方法中使用Bird,而直接使用super(SongBird,self)。
__metaclass__=type
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):
super(SongBird,self).__init__()
self.sound='Squawk!'
def sing(self):
print self.sound
[9.3] 成员访问
本节会讨论一个有用的魔法方法集合,它可以创建行为类似于序列或映射的对象。
基本的序列和映射的规则很简单,但如果要实现它们全部功能就需要实现很多魔法函数。
[9.3.1] 基本的序列和映射规则
序列和映射是对象的集合。为了实现它们基本的行为,如果对象是不可变的,那么久需要使用两个魔法方法,如果是可变的则需要使用4个。
__len__(self):这个方法应该返回集合中所含项目的数量。对于序列来说,着就是元素的个数;对于映射来说,则是键-值对的数量。如果__len__返回0,对象会被当做一个布尔变量中的假值(空的列表,元组,字符串和字典也一样)进行处理。
__getitem__(self,key):这个方法返回与所给键对应的值。对于一个序列,键应该是一个0~n-1的整数,n是序列的长度;对于映射来说,可以使用任何种类的键。
__setitem__(self,key,value):这个方法应该按一定的方式存储和key相关的value。该值随后可使用__getitem__(self,key)来获取。当然只能为可以修改的对象定义这个方法。
__delitem__(self,key):这个方法在对一部分对象使用del语句时被调用,同时必须删除和元素相关的键。这个方法也是为可修改的对象定义的。
对这些方法的附加要求:
对一个序列来说,如果键是负整数,那么要从末尾开始计数。换句话说,x[-n]和[len(x)-n]是一样的;
如果键是不合适的类型,会引发一个TypeError异常;
如果序列的索引是正确的类型,但超出了范围,应该引发一个IndexError异常。
下例创建一个无穷序列:
def checkIndex(key):
"""
所给的键是能接受的索引嘛?
为了能被接受,键应该是一个非负的整数,如果它不是一个整数,会引发TypeError;如果它是负数,则会引发IndexError
"""
if not isinstance(key,(int,long)):raise TypeError
if key<0:raise IndexError
class ArithmeticSequence:
def __init__(self,start=0,setp=1):
"""
初始化算术序列
初始值---序列中的第一个值
步长---两个相邻值之间的差别
改变---用户修改的值的字典
"""
self.strt=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.strp
def __setitem__(self,key,value):
"""
修改算术序列中的一个项
"""
checkIndex(key)
self.changed[key]=value
[9.3.2] 子类化列表,字典和字符串
到目前为止已经介绍了基本的序列、映射规则的4中方法。关键词是继承,能继承的时候为什么还要全部实现呢?标准库有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)
CounterList类严重依赖于它的子类化超类list的行为。CounterList类没有重写任何的方法,都能被直接使用。在两个被重写的方法中,super方法被用来调用相应的超类的方法。只在__init__中添加了所需的初始化counter特性的行为,并在__getitem__中更新了counter特性。
[9.4] 更多的魔力
魔法名称的用途有很多。大部分的特殊方法都是为高级的用法准备的。
[9.5] 属性
曾经提到过访问器方法。访问器是一个简单的方法,它能够使用getHeight、setHeight这样的名字来得到或者重绑定一些特性(可能是类的私有属性)。如果在访问给定的特性时必须要采取一些行动,那么像这样的封装状态变量(特性)就很重要。
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
在上面例子中,getSize和setSize方法一个名为size的假想特性的访问器方法。size是由width和height构成的元组。Python能隐藏访问器方法,让所有特性看起来一样。这些通过访问器定义的特性被称为属性。
在Python中有两种创建属性的机制。主要讨论新的机制在新式类中使用的property函数,然后简单地说明一下如何使用特殊方法实现属性。
[9.5.1] property函数
property函数的使用很简单,像上节的Rectangle那样的类,只要增加一行代码:
__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)
property函数创建了一个属性,其中访问器函数被用作参数(先是取值,然后是赋值),这个属性名为size。这样一来不再需要担心是怎么实现的了,可以用同样的方法处理width、height和size。
[9.5.2] 静态方法和类成员方法
静态方法和类成员方法分别在创建时分别被装入Staticmethod类型和Classmethod类型的对象中。静态方法的定义没有self参数,且能够被类本身直接调用。类方法在定义时需要名为cls的类似于self的参数,类成员方法可以直接用类的具体对象调用。但cls参数是自动被绑定到类的。
请看下面的例子:
__metaclass_=type
class MyClass:
def smeth():
print 'this is a static method'
semth=staticmethod(smeth)
def cmeth(cls):
print 'this is a class method of', cls
cmeth=classmethod(cmeth)
手动包装和替换方法的技术看起来有点单调。为这样的包装方法引入了一个叫做装饰器的新语法(它能够对任何可调用的对象进行包装,既能够用于方法也能用于函数)。使用@操作符,在方法(或函数)的上方将装饰器列出,从而制定一个或者更多的装饰器(多个装饰器在应用时的顺序与制定顺序相反)。
__metaclass__=type
class MyClass:
@staticmethod
def smeth():
print 'this is a static method'
@classmethod
def cmeth(cls):
print 'this is a class method of',cls
定义了这些方法以后,就可以像下面的例子那样使用(例子中没有实例化类):
>>>MyClass.smeth()
this is a static method
>>>MyClass.cemth()
This is a class method of <class '__main__.MyClass'>
静态方法和类成员方法在Python中并不是向来都很重要,主要的原因使得爱不分情况下可以使用函数或者绑定方法代替。
[9.5.3] __getattr__、__setattr__和它的朋友们
拦截对象的所有特性访问是可能的,为了在访问特性的时候可以执行代码,必须使用一些魔法方法。
__getattribute__(self,name):当特性name被访问时自动被调用。
__getattr__(self,name):当特性name被访问且对象没有响应的特性时被自动调用。
__setattr__(self,name,value):当试图给特性name赋值时会被自动调用
__delattr__(self,name):当试图删除特性name时被自动调用。
[9.6] 迭代器
只讨论一个特殊方法__iter__,这个方法时迭代器规则的基础。
[9.6.1] 迭代器规则
迭代的意思是重复做一些事情很多次。实际上也能对其他的对象进行迭代:实现__iter__方法的对象。
__iter__方法返回一个迭代器iterator,所谓的迭代器就是具有next方法(这个方法在调用时不需要任何参数)的对象。在调用next方法时,迭代器会返回它的下一个值。如果next方法被调用,但迭代器没有值可以返回,就会引发一个StopIteration异常。
迭代规则的关键是什么?为什么不适用列表?因为列表的杀伤力太大。如果有可以一个接一个地计算值的函数,那么在使用时可能是计算一个值时获取一个值 --- 而不是通过列表一次性获取所有值。如果有很多值,列表就会占用太多的内存。但还有其他的理由:使用迭代器更通用、更简单、更优雅。
这里的“列表”是一个斐波那契数列,使用的迭代器如下:
class Fibs:
def __init__(self):
self.a=0
self.b=1
def next(self):
self.a,self.b=self.b,self.a+self.b
return self.a
def __iter__(self):
return self
注意,迭代器实现了__iter__方法,这个方法实际上返回迭代器本身。很多情况下,__iter__会放到其他的会在for循环中使用的对象中。这样一来,程序就能返回所需的迭代器。
首先,产生一个Fibs对象:
>>>fibs=Fibs()
>>>for f in fibs:
if f>1000:
print f
break
[9.6.2] 从迭代器得到序列
除了在迭代器和可迭代对象上进行迭代外,还能把他们转换为序列。关于这个的一个很有用的例子是使用list构造方法显式地将迭代器转化为列表:
class TestIterator:
value=0
def next(self):
self.value+=1
if self.value>10:raise StopIteration
return self.value
def __iter__(self):
return self
>>>ti=TestIterator()
>>>list(ti)
[1,2,3,4,5,6,7,8,9,10]
[9.7] 生成器
它和迭代器可能是近几年来引入的最强大的两个特性。但是,生成器的概念则要更高级一些。要理解它是如何工作以及它有什么用处。生成器可以帮助写出非常优雅的代码。
生成器是一种普通的函数语法定义的迭代器。先看看怎么创建和使用生成器,然后再了解一下它的内部机制。
[9.7.1] 创建生成器
创建一个生成器就像创建函数一样简单。首先创建一个展开嵌套列表的函数,参数是一个列表,和下面这个很像:
nested=[[1,2],[3,4],[5]]
这是一个列表的列表。函数应该按顺序打印出列表中的数字,解决的办法如下:
def flatten(nested):
for sublist in nested:
for element in sublist:
yield element
首先迭代提供的嵌套列表中的所有子列表,然后按顺序迭代子列表中的元素。这里的yield语句是新知识。任何包含yield语句的函数称为生成器。除了名字不同以外,它的行为和普通的函数也有很大的差别。这就在于它不是像return那样返回值,而是每次产生多个值。每次缠身一个值(使用yield语句),函数就会被冻结:即函数停在那点等待被激活。函数被激活后就从停止的那点开始执行。
[9.7.2] 递归生成器
如果要处理任意层的嵌套该怎么办?例如,可能要使用来表示树形结构。每层嵌套需要增加一个for循环,但因为不知道有几层嵌套,所以必须把解决方法变得更灵活,现在是求助于递归的时候了。
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
当flatten被调用时,有两种可能性(处理递归时大部分都是有两种情况):基本情况和需要递归的情况。
[9.7.3] 通用生成器
生成器是一个包含yield关键字的函数,当它被调用时,在函数体重的代码不会被执行,而会返回一个迭代器。每次请求一个值,就会执行生成器中的代码,知道遇到一个yield或者return语句。yield语句意味着应该生成一个值, return语句意味着生成器要停止执行。
生成器由两部分组成:生成器的函数和生成器的迭代器。生成器的函数使用def语句定义的,包含yield的部分,生成器的迭代器是这个函数返回的部分。两个实体经常被当做一个,合起来叫做生成器。
[9.7.4] 生成器方法
[9.7.5] 模拟生成器
[9.8] 八皇后问题
介绍如何使用生成器解决经典的编程问题。
[9.8.1] 生成器和回溯
生成器是逐渐产生结果的复杂递归算法的理想实现工具。没有生成器的话,算法就需要一个座位额外参数传递的半成品方案,这样递归调用就可以在这个方案上建立起来。如果使用生成器,那么所有的递归调用只要创建自己的yield部分。相同的策略也可以用在遍历图和树形结构中。
在一些应用程序中,答案必须做很多次选择才能得出。并且程序不只是在一个层面上而必须在递归的每个层面上做出选择。拿生活中的例子打个比方,首先想象一下你要出席一个很重要的回忆,但你不知道在哪儿开会,在你面前有两扇门,开会的地点就在其中的一扇门后,于是有人挑了左边的进入,然后又发现两扇门。后来再选了左边的门,结果却错了,于是回溯到刚才的两扇门那里,并选择右边的门,结果还是错的,于是再次回溯,知道回到了开始点,再在哪里选择右边的门。
这样的回溯策略在解决需要尝试每种组合,知道找到一种解决方案的问题时很有用。这类问题能按照下面伪代码的方式解决:
# 伪代码
第1层所有的可能性:
第2层所有的可能性:
......
第n层所有的可能性:
可行吗?
为了直接使用for循环来实现,就必须知道会遇到的具体判断层数,如果无法得知层数信息,那么可以使用递归。
[9.8.2] 问题
这是一个深受喜爱的计算机科学谜题:有一个棋盘和8个要放在上面的皇后。唯一的要求是皇后之间不能形成威胁。也就是说,必须把他们放置成每个皇后都不能吃掉其他皇后的状态。怎样才能做到呢?皇后要如何放置呢?
这是一个典型的回溯问题:首先尝试放置第1个皇后,然后尝试第2个,以此类推。如果发现不能放置下一个皇后,就回溯到上一步,试着将皇后放到其他的位置。最后,或者尝试完所有的可能或者找到解决方案。
问题会告知,棋盘上只有8个皇后,但我们假设有任意数目的皇后(这样就更符合实际生活中的回溯问题),怎么解决?对于这个问题有更高效的解决方案,可以查看http://www.cit.gu.edu.au/~sosic/nqueens.html可以找到关于各种解决方案的简单介绍。
[9.8.3] 状态表示
为了表示一个可能的解决方案,可以使用元组。每个元组中元素都指示相应行的皇后的位置。如果state[0]==3.那么就表示第1行的皇后在第4列。当在某一个递归的层面时,只能知道上一行皇后的位置,因此需要一个长度小于8的状态元组(或者小于皇后的数目)。
使用列表来代替元组表示状态也是可行的。具体使用哪个只是一个习惯的问题。一般来说,如果序列很小而且是静态的,元组是一个好的选择。
[9.8.4] 寻找冲突
首选从一些简单的抽象开始。为了找到一种没有冲突的设置(没有皇后会被其他的皇后吃掉),首先必须定义冲突是什么。
已知的皇后的位置被传递给conflict函数,然后由函数判断下一个的皇后的位置会不会有新的冲突:
def conflict(state,nextX):
nextY=len(state)
for i in range(nextY):
if abs(state[i]-nextX) in (0,nextY-i):
return True
return False
参数nextX代表下一个皇后的水平位置(x坐标或列),nextY代表垂直位置(y坐标或行)。这个函数对前面的每个皇后的位置做一个简单的检查。如果下一个皇后和前面的皇后有同样的水平位置,或者是在同一条对角线上,就会发生冲突,接着返回True。如果没有这样的冲突发生,那么返回False,不太容易理解的是下面的表达式:
abs(state[i]-nextX) in (0,nextY-i)
如果下一个皇后和正在考虑的前一个皇后的水平距离为0(列相同)或者等于垂直距离(在一条对角线上)就返回True,否则返回False。
[9.8.5] 基本情况
八皇后问题的实现虽然有点不太好实现,但如果使用生成器就没有什么难的了。如过不习惯使用递归,那么最好不要自己动手解决这个entire。需要注意的是这个解决方案的效率不是很高,因此如果皇后的数目很多的话,运行起来就会有点慢。
从基本的情况开始:最后一个皇后。你想让它做什么?假设你想找出所有可能的解决方案,这样一来,它能根据其他皇后的位置生成它自己能占据的所有位置。能把这样的情况直接描绘出。
def queens(nums,state):
if len(state)==num-1:
for pos in range(num):
if not conflict(state,pos):
yield pos
用人类的语言来描述,它的意思是:如果只剩下一个皇后没有放置。那么便利它的所有可能的位置,并且返回没有冲突发生的位置。num参数是皇后的总数。state参数是存在前面皇后的位置信息的元组。
[9.8.6] 需要递归的情况
现在让我们看看解决方案中的递归部分。完成基本情况后,递归函数会嘉定(通过归纳)所有的来自低层的结果都是正确的。因此需要做的就是为前面的queen函数的实现中if语句增加else子句。
那么递归调用会得到什么结果呢?你想得到所有低层皇后的位置,对吗?假设位置信息作为一个元组返回。在这种情况下,需要修改基本情况也返回一个元组。
这样一来,程序从前面的皇后得到了包含位置信息的元组,并且要为后面的皇后提供当前皇后的每种合法的位置信息。为了让程序继续运行下去,接下来需要做的就是把当前的位置信息添加到元组中并传递给后面的皇后。
...
else:
for pos in range(num):
if not conflict(state,pos):
for result in queens(num,state+(pos,)):
yield (pos,)+result
for pos和if not conflict部分和前面的代码相同,因此可以稍微简化一下代码。添加一些默认的参数。
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
生成器quees能给出所有的解决方案(那就是放置皇后的所有的合法方法):
>>>list(queens(3))
[]
>>>for solution in queens(8):
print solution
>>>len(queens(8))
92
如果用8个皇后做参数来运行queens,有92种方案。
[9.8.7] 打包
在结束八皇后问题之前,试着将输出处理得更容易理解一些。清理输出总是一个好的习惯,因为这样很容易发现错误。
def prettyprint(solution):
def line(pos,length=len(solution)):
return '. ' * (pos) +'X ' +'. ' * len(length-pos-1)
for pos in solution:
print line(pos)
注意prettyprint中创建了一个小的助手函数,之所以将其放在prettyprint内,是因为门假设在外面的任何地方都不会用到它。
[9.9] 小结
介绍了很多魔法方法,下面来总结一下:
旧式类和新式类:创建新式类,直接或间接子类化object。或者设置__metaclass__属性也可以。
魔法方法:python中有一些特殊的方法(名字是以双下划线开始和结束的)。这些方法和函数只有很小的不同,但其中的大部分方法在某些情况下被Python自动调用(比如__init__在对象被创建后调用)。
构造方法:这是面向对象的语言共有的,可能要为自己写的每个类实现构造方法。构造方法被命名为__init__并且在对象呗创建后立即自动调用。
重写:一个类能通过实现方法来重写它的超类中定义的这些方法和树形。如果新方法要调用重写版本的方法,可以从超类直接调用未绑定的版本或者使用super函数。
序列和映射:创建自己的序列或映射需要实现所有的序列和映射规则得方法,包括__getitem__和__setitem__这样特殊的方法。通过子类化list和dict能节省很多工作。
迭代器:迭代器是带有next方法的简单对象。迭代器能在一系列的值上进行迭代。当没有值可供迭代时,next方法就会引发StopIteration=异常。可迭代对象有一个返回迭代器的__iter__方法,它能像序列那样在for循环中使用。一般来说,迭代器本身也是可迭代的,即迭代器有返回它自己的next方法。
生成器:生成器函数(或者方法)是包含了关键字yield的函数。当被调用时,生成器函数返回一个生成器。可以使用send、throw和close方法让活动生成器和外界交互。
八皇后问题:使用生成器可以很轻松地解决。问题描述的是如何在棋盘上放置8个皇后,使其不会互相攻击。