本章会(1)讨论一些重要的魔法方法,(2)然后会讨论两个相关的主题:属性和迭代器,(3)最后介绍一个相关的示例来处理一些稍微复杂的问题。
魔法方法或者又称为特殊方法(个人挺不习惯魔法方法的翻译,叫法搞的跟拍科幻片似的,因此后面统一叫做特殊方法),是指被包含下划线的方法或者所能调用到的方法的统称,这些通常会在特殊的情况下调用,并且基本没有手动调用他们的必要。
9.1 注备工作
Python的老版本和新版本在定义类的时候会有些差别,为了统一以新式类的方式来定义类,可以在文件首行添加一行代码来来解决这个问题。个人在2.7的版本上进行学习暂时不存在这个问题,跳过不深究。
9.2 构造方法
构造方法就是类似于init的初始化方法,差别在于一个对象被构建好以后会自动调用构造方法。在Python中创建一个构造方法如下(使用_init_):
class FooBar:
def __init__(self):
self.value = 42
9.2.1 重写一般方法和特殊的构造方法
如果类B继承自类A,类B的某个方法被调用的时候会首先去B中寻找该方法,如果找不到则会到父类中去寻找:
class A():
def hello(self):
print "Hello, I am A!"
class B(A):
pass
a = A()
a.hello()
b = B()
b.hello()
$ python test.py
Hello, I am A!
Hello, I am A!
因为B类没有hello方法,所以当hello被调用时,原始信息就被打印出来。现在类B重写了该方法,再进行调用就能产生不一样的效果:
class A():
def hello(self):
print "Hello, I am A!"
class B(A):
def hello(self):
print "Hello, I am !"
a = A()
a.hello()
b = B()
b.hello()
$ python test.py
Hello, I am A!
Hello, I am B!
重写方法对于类很重要,尤其对构造方法,但重写父类的构造方法需要调用超类的构造方法,否则对象可能不能被正确的初始化。看如下的例子:
class Bird():
def __init__(self):
self.hungry = True
def eat(self):
if self.hungry:
print 'Aaaah...'
self.hungry = False
else:
print 'No, thanks!'
b = Bird()
b.eat()
b.eat()
$ python test.py
Aaaah...
No, thanks!
鸟类在吃过东西以后,就不再饥饿。现在考虑添加SingBird,它添加了唱歌的行为:
class SingBird():
def __init__(self):
self.sound = 'Squawk'
def sing(self):
print self.sound
sb = SingBird()
sb.sing()
$ python test.py
Squawk
但是如果直接调用eat
就会产生问题:
Traceback (most recent call last):
File "test.py", line 20, in <module>
sb.eat()
AttributeError: SingBird instance has no attribute 'eat'
为了解决该问题,必须调用超类的构造方法保证正常的初始化,后面两节分别介绍了两种做法。
9.2.2 调用未绑定的超类构造方法
如下为调用未绑定的超类构造方法,属于历史遗留写法:
class SingBird(Bird):
def __init__(self):
Bird.__init__(self)
self.sound = 'Squawk'
def sing(self):
print self.sound
9.2.3 使用super函数
使用super函数的写法:
__metaclass__ = type#super类只能在新式类中使用
class SingBird(Bird):
def __init__(self):
super(SingBird, self).__init__()
self.sound = 'Squawk'
def sing(self):
print self.sound
9.3 成员访问
序列和映射是对象的集合,为了实现它们的基本行为,我们要实现一些魔法方法。如果集合中的对象是不可变的那么实现两个魔方方法即可,否则要实现四个魔法方法。
- __len__(self) 如果返回0,该对象被当做布尔变量使用时是假
- __getitem__(self, key) 如果是序列,key是0~n-1的整数,如果是映射,key可能会是任何类型。
- __setitem__(self, key, value)
- __delitem__(self, key)
9.3.1 基本的序列和映射规则
让我们实践一把,创建一个无穷序列:
def checkKey(key):
if not isinstance(key, (int, long)):
raise TypeError
elif key < 0:
raise IndexError
class InfinitSequence():
def __init__(self, start=0, step=1):
self.start = 0
self.step = 1
self.changed = {}
def __getitem__(self, key):
checkKey(key)
try:
return self.changed[key]
except KeyError:
return self.start + key * self.step
def __setitem__(self, key, value):
checkKey(key)
self.changed[key] = value
sequence = InfinitSequence(1, 2)
print sequence[4] #4
sequence[4] = 2
print sequence[4] #2
print sequence[5] #5
因为序列是无穷的,所以没有实现_len_方法,因此之前上面所说的实现两个或者四个魔法方法是可选的,甚至有些方法对于你实现数据结构根本不需要,因此不需要实现。
9.3.2 子类化列表、字典和字符串
要模拟基本的序列/映射要实现其他的特殊方法和普通方法,这是一项繁琐的工作,好在我们有继承。如下实现一个和内建列表行为相似的序列,可以使用子类内建类型list:
__metaclass__=type
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))
print c1 #[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
c1.reverse()
print c1 #[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
del c1[3:6]
print c1 #[9, 8, 7, 3, 2, 1, 0]
print c1.counter #0
print c1[4] + c1[2] #9
print c1.counter #2
9.4 更多魔力
还有很多特殊方法不详述,参考Python参考手册。
9.5 属性
访问器方法是一个简单的方法,能够使用类似于getHeight何setHeight这样的名字绑定一些属性,看如下例子中的Recantagle类:
__metaclass__=type
class Recantagle(list):
def __init__(self):
self.height = 0
self.width = 0
def getSize(self):
return self.width, self.height
def setSize(self, size):
self.width, self.height = size
r = Recantagle() #(0, 0)
print r.getSize()
r.setSize((200,400))
print r.getSize() #(200, 400)
我们知道方法的实现是在getSize里面,更好的做法是隐藏访问方法。Python中有两种机制,后面主要讨论新式类中使用的property函数,以及类方法和静态方法,然后简单介绍如何使用特殊方法实现属性。
9.5.1 property属性
property函数的使用很简单,只需要将访问器函数用作参数将结果赋值给一个名为size的属性。可以用同样的方式处理width和height:
__metaclass__=type
class Recantagle(list):
def __init__(self):
self.height = 0
self.width = 0
def getSize(self):
return self.width, self.height
def setSize(self, size):
self.width, self.height = size
size = property(getSize, setSize)
r = Recantagle()
print size
property的参数可以用0-4个(此处只提及,使用查询文档),在新式类中应该使用property函数而不是访问器方法。
9.5.2 静态方法和类成员方法
静态方法是创建时被装入Staticmethod类型的特征,静态方法没有self属性,能够被类本身直接调用。
类成员方法是创建时被装入Classmethod类型的特征,类方法在定义时需要cls(类似于self的参数),类成员方法可以直接用类的对象调用。
请看下面的例子:
__metaclass__=type
class MyClass():
def smeth():
print 'This is static method'
smeth = staticmethod(smeth)
def cmeth(cls):
print 'This is class method of ', cls
cmeth = classmethod(cmeth)
MyClass.cmeth() #This is class method of <class '__main__.MyClass'>
c = MyClass()
c.cmeth() #This is class method of <class '__main__.MyClass'>
MyClass.smeth() #This is static method
也可以用装饰器来定义静态方法和类成员方法:
__metaclass__=type
class MyClass():
@staticmethod
def smeth():
print 'This is static method'
@classmethod
def cmeth(cls):
print 'This is class method of ', cls
MyClass.cmeth() #This is class method of <class '__main__.MyClass'>
c = MyClass()
c.cmeth() #This is class method of <class '__main__.MyClass'>
MyClass.smeth() #This is static method
静态方法和类成员方法虽然目前还没有大量运功过,以后可以在工厂函数中用到。
9.5.3 __getattr__、 __setattr__和它的朋友们
在不使用property属性之前,如下四个方法提供了拦截对对象特性的功能:
- __getattribute__(self, name) 当name被访问时自动被调用,只能在新式类中使用
- __getattr__(self, name) 当name被访问时且对象没有相应的可直接访问的特性时候调用
- __setattr__(self, name, value) 当试图给特性赋值时候会自动调用
- __delattr__(self, name) 但试图删除特性name时自动调用
尽管相对property使用起来稍微复杂,但是还是很强大的:
__metaclass__=type
class Recantagle(list):
def __init__(self):
self.height = 0
self.width = 0
def __getattr__(self, name):
if name == 'size':
return self.width, self.height
else:
raise AttributeError
def __setattr__(self, name, value):
if name == 'size':
self.width, self.height = value
else:
self.__dict__[name] = value
r = Recantagle()
print r.size #(0, 0)
r.size = (100, 200)
print r.size #(100, 200)
9.6 迭代器
此处只讨论一个特殊的迭代器方法__iter__,这个方法是迭代器规则的基础。
9.6.1 迭代器的规则
迭代意思是重复做一件事情能够很多次,到目前为止我们使用了for循环对序列和字典进行迭代,本章介绍的是对实现了__iter__方法的对象进行迭代。
__iter__方法会返回一个迭代器,迭代器是一个有next方法的对象,调用迭代器的next方法时会返回它的下一个值,如果没有值返回就会引发一个StopIteration异常。
使用迭代器的好处是什么? 相比较使用列表一次性获得所有的元素的值占用大量内存而言,迭代显得更加简单优雅,如下的例子“斐波拉契数列”是一个使用列表的例子,否则列表长度必须无限:
如下是一个使用迭代去的例子:
__metaclass__=type
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
fibs = Fibs()
for f in fibs:
if f > 1000:
print f
break
内建函数可以从可迭代对象中直接获得迭代器,比如:
>>> it = iter([1,2,3])
>>> it.next()
1
>>> it.next()
2
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()
print 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:
print element
flatten(nested)
现在要把它改写成生成器的方式:
nested = [[1,2], [3, 4], [5]]
def flatten(nested):
for sublist in nested:
for element in sublist:
yield element
for number in flatten(nested):
print number
唯一的区别在于yield语句,任何包含yield的语句被称作生成器,它和普通函数的差别在于普通函数直接返回值,而生成器每次生成一个值后函数会被冻结,等到再次被调用的时候又会被激活,被激活后函数会从之前被冻结的点开始执行。
9.7.2 递归生成器
上面的例子中嵌套有两层,因此使用两个for循环。通过求助于递归,可以遍历多层级的嵌套结构(树):
def flatten(nested):
try:
for sublist in nested:
for element in flatten(sublist):
yield element
except:
yield nested
print list(flatten([[[1], 2], 3, 4, [5, [6, 7]], 8])) # [1, 2, 3, 4, 5, 6, 7, 8]
flatten被调用时,两种情况:nested为一个元素,进而引发TypeError异常,生成器继而生成一个元素;nested为一个列表,那么就会进入到第二个for循环,遍历flatten(sublist)从而实现递归(这里面运用到了深度遍历的思想)。
有一种情况是需要特别进行处理的,如果遍历的元素中包含了字符串,我们当然不希望对字符串进行遍历。对一个元素是否是字符串的最好的检测办法是直接将元素和字符串拼接看是否发生错误。因此改写了的代码如下所示(颇有点巧妙):
def flatten(nested):
try:
try:
nested + ''
except TypeError:
pass
else:
raise TypeError
for sublist in nested:
for element in flatten(sublist):
yield element
except:
yield nested
print list(flatten([[[1], 2], 3, 4, [5, [6, 7]], 8])) # [1, 2, 3, 4, 5, 6, 7, 8]
假定nested是字符串,那么在内层try检测中一定不会导致TypeError,因此在else中手动诱发TypeError好交给外层处理;如果不是字符串且不是普通的单个元素(不然进入不到第一个try子句里),那么会触发内层的TypeError,不需要进行处理而是直接跳过好让后面的for中递归内容执行。
9.7.3 通用生成器
生成器是由两部分组成的:生成器的函数和生成器的迭代器。用于def语句定义的包含yield部分的就是生成器的函数,生成器的迭代器则是这个函数的返回部分。两个实体经常被当做一个,合作生成器。如下一个简单例子方便理解这两个概念:
def simple_generator():
yield 1
print simple_generator #<function simple_generator at 0x1004edaa0>
print simple_generator() #<generator object simple_generator at 0x1004eb140>
9.7.4 生成器方法
从2.5开始Python中引入了属性能够为生成器提供值,这样生成器就能与外部进行交互:
- 外部作用域访问生成器的send方法,就像使用next一样,只不过前者需要一个参数
- 当生成器运行时候,返回通过send发送的值,如果next方法被使用则放回None
如下是一个简单例子:
def repeater(value):
while True:
new = (yield value)
if new is not None:
value = new
r = repeater(42)
print r.next()
9.7.5 模拟生成器
生成器在比较旧的版本中是不可用的,可以使用代码来模拟生成器。考虑到目前还没有使用到生成器的需求,如何模拟也暂且先跳过,有需要再回来翻看。
9.8 八皇后问题
该节会使用生成器来解决经典的编程问题。
9.8.1 生成器和回溯
略。
9.8.2 问题
简述:8*8的棋盘中,放置八个皇后使得他们相互之间不会相互威胁(即不在同一条横线竖线斜线上)。
9.8.3 状态表示
八个皇后的状态可以用一个长度为8的元组表示,比如state[0]=3表示第1行的皇后放在第4列。
9.8.4 寻找冲突
可以把冲突的判定定义为一个函数:
def conflict(state, nextX):
nextY = len(state) #2
for i in range(nextY): #2
if abs(nextX - state[i]) in (0, nextY - i):
return True
return False
nextY为下一个皇后的竖直位置(nextX和nextY的含义理解了就容易了),然后遍历所有已经放置好的皇后,如果新的位置和放置好的皇后位置在同一个竖直位置上或者斜线上,那么就返回True,否则返回False。
9.8.5 基本情况
这里先把问题简化为n皇后,假定前面n-1个皇后的位置都已经找到了,你只需要处理最后一步,你现在需要做的是根据前面n-1个皇后的位置找到第n个皇后所能放置的所有位置。如下用代码进行表示:
def queens(nums, state):
if len(state) == nums - 1:
for pos in range(nums):
if not conflict(state, pos):
yield pos
list(queens(4, [1, 3, 0])) # [2]
/Users/appledev072/Desktop/Screen Shot 2015-01-30 at 2.50.19 PM.png
9.8.6 需要递归的情况
def conflict(state, nextX):
nextY = len(state) #2
for i in range(nextY): #2
if abs(nextX - state[i]) in (0, nextY - i):
return True
return False
def queens(nums = 8, state = ()):
for pos in range(nums):
if not conflict(state, pos):
if len(state) == nums - 1:
yield (pos, )
else:
for result in queens(nums, state + (pos,)):
yield (pos,) + result
print list(queens(3)) #[]
print list(queens(4)) #[(1, 3, 0, 2), (2, 0, 3, 1)]
print len(list(queens(8))) #92
内层部分相当于我们之前所处理的情况,区别在于现在变成了元组,即该部分会获得所有最后一个皇后的位置并返回给父级调用(内层for循环中的queens),并跟后面7皇后的位置结果进行合并。以此类推。
9.8.7 打包
可以将八皇后的结果输出的稍微好理解一点,这样也有有助于发现错误:
import random
def prettyprint(solution):
def line(pos, length = len(solution)):
return '. ' * (pos) + 'X ' + '. ' * (length - pos - 1)
for pos in solution:
print line(pos)
prettyprint(random.choice(list(queens(8))))
9.9 小结
- 旧式类和新式类
- 魔法方法
- 构造方法
- 重写
- 序列和映射
- 迭代器
- 生成器
- 八皇后问题
9.9.1 本章的新函数
- iter(obj)
- property(fget, fset, fdel, doc)
- super(class, obj)
9.9.2 接下来学习什么
测试、扩展、打包和一些项目的具体实现。