Bootstrap

Python学习笔记【九】——《python基础教程》:: 魔法方法、属性和迭代器

9. 魔法方法、属性和迭代器

  Python中,有的名称会在前面和后面都加上两个下划线,例如__future__,这种拼写表示名称有特殊含义,所以绝不要在自己的程序中使用这种名字。在Python中,由这些名字组成的集合所包含的方法成为魔法(或特殊)方法。若对象实现了这些方法中的某一个,那么这个方法会在特殊的情况下(根据名字)被Python调用。而几乎没有直接调用他们的必要。

9.1 准备工作

  新版的Python的一些特性(例如属性、super函数)不会再老式的类上起作用。为确保类是新型的,应该把赋值语句__metaclass__=type放在模块的最开始,或者子类化内建类object(或其他一些新式类)。
  以下两个类:

class NewStyle(object):
  #more_code_here
class OldStyle:
  #more_code_here

  两个类中,NewStyle是新式类,OldStyle是旧式类。若文件以__metaclass__=type开始,那么两个类都是新式类。
  若没有兼容之前旧版Python的需要,建议将所有类写成新式类,并使用super函数(参见9.2.3节)。

  ★Python3.0中没有“旧式”类,也不需要显示的子类化object或者将元类设置为type。所有的类都会隐式地成为object的子类——如果没有明确超类的话,会直接子类化;否则会间接子类化。

9.2 构造方法

  定义:用于类的初始化,当对象被创建后,会立即调用构造方法。
  创建:使用方法名__init__。

>>> class FooBar:
...   def __init__(self):
...     self.somevar = 42
... 
>>> f = FooBar()
>>> f.somevar
42

  可以给构造方法添加参数并设定默认值。  

>>> class FooBar:
...   def __init__(self, value=22):
...     self.somevar = value
... 
>>> f = FooBar()
>>> f.somevar
22
>>> f = FooBar("test value")
>>> f.somevar
'test value'

  ★★★与构造方法对应有析构方法__del__,在对象被回收前调用。由于调用具体时间不可知,因此使用时须谨慎。
  

9.2.1 重写一般方法和特殊的构造方法

  在子类中添加功能的最基本方法是增加方法。也可以重写一些超类的方法来自定义继承的行为。
  

>>> class A:
...   def hello(self):
...     print "I'm A."
... 
>>> class B(A):pass
... 
>>> a = A()
>>> b = B()
>>> a.hello()
I'm A.
>>> b.hello()  #B中没有找到hello()方法,回到它的超类A中找
I'm A.
>>> class B(A):
...   def hello(self):
...     print "I'm B."
... 
>>> b = B()
>>> b.hello()  #B中重写了hello()方法,会产生不同效果
I'm 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()
Aaaah...
>>> b.eat()
No, thanks!
#定义子类SongBird
>>> class SongBird(Bird):
...   def __init__(self):
...     self.sound = "Squawk!"
...   def sing(self):
...     print self.sound
... 
>>> sb = SongBird()
>>> sb.sing()  #新加的方法正常使用
Squawk!
>>> sb.eat()  #继承的eat方法产生问题。原因:构造方法被重写,但是新的构造方法中没有hungry特性
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in eat
AttributeError: SongBird instance has no attribute 'hungry'

  从上例中可以看出,子类的构造方法必须调用超类构造方法来确保基本的初始化。有两种方法可行:
  1. 调用超类构造方法的未绑定版本;
  2. 使用super函数。  

9.2.2 调用未绑定的超类构造方法

  本节为历史遗留问题。上节中提到的问题,在目前的Python版本中,使用super函数的方法简单明了。由于很多旧代码已经使用本节中的方法,因此需要了解。
  首先介绍上节中问题的解决方法:

>>> class SongBird(Bird):
...   def __init__(self):
...     Bird.__init__(self)
...     self.sound = "Sqawk!"
...   def sing(self):
...     print self.sound
... 
>>> sb = SongBird()
>>> sb.eat()
Aaaah...
>>> sb.eat()
No, thanks!

  在调用一个实例的方法时,该方法的self参数会被自动绑定到实例上(称为绑定方法)。但如果直接调用类的方法,那么没有实例会被绑定,可以自由地提供self参数,这样的方法称为未绑定方法

9.2.3 使用super函数

  super函数只适用于新式类。当前的类和对象可以作为super函数的参数使用,调用函数返回的对象的任何方法都是调用超类的方法,而不是当前类的方法。

>>> __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__() #__init__方法能以普通的方式被调用
...     self.sound = "Squawk!"
...   def sing(self):
...     print self.sound
... 
>>> sb = SongBird()
>>> sb.eat()
Aaaah...
>>> sb.eat()
No, thanks!

  ★Python3.0中,super函数可以不用任何参数进行调用

  super函数实际上很智能,即使类已经继承多个超类,它只需要使用一次super函数(要确保所有的超类的构造方法都是用了super函数)。在一些含糊情况下使用旧式类会很别扭(例如两超类共同继承了一个超类),但新式类和super函数会自动处理。内部具体工作原理不需要理解,但必须知道的是:大多情况下,调用超类的未绑定构造方法是更好的选择。
  super类的返回是一个super对象,负责进行方法解析,当对其特性进行访问时,它会查找所有的超类(以及超类的超类),直到找到所需的特性为止(或引发一个AttributeError异常)。

9.3 成员访问

  本节中介绍的魔法方法可以创建类似序列或映射的对象。
  ★规则(protocol)在Python中用于描述管理某种形式的行为的规则。规则说明了应实现何种方法和这些方法应该做什么。
  ★Python中的多态性是基于对象的行为。在其他语言中,对象可能被要求属于某一个类,或者被要求实现某个接口,但Python中只是简单地要求它遵守几个给定的规则。

9.3.1 基本的序列和映射规则

  序列和映射是成员的集合。为了实现他们的基本行为(规则),若对象不可变,则需要两个魔法方法,若对象可变,需要四个魔法方法:
  __len__(self):返回集合中所含项目的个数。对于序列,指元素个数。对于映射,指key-value对的个数。若返回值为0(未使用__nonzero__来重写该行为),对象被作为Boolean类型的假值(空列表、空元组、空字符串、空字典)处理。
  __getitem__(self,key):返回给定key对应的value。对序列,key指0~n-1的整数(或负数),n是序列长度;对映射,key可以是任意种类。
  __setitem__(self,key,value):按一定方式存储和key有关的value。只适用于可修改的对象。
  __delitem__(self,key):该方法在对部分对象使用del语句时被调用,用于删除与key相关的元素。只适用于可修改的对象。
  对上述方法有以下附加要求:
  1. 对序列,若键是负整数,则从末尾开始计数。即X[-n]=X[len(x)-n]。
  2. 若key是错误的类型,会引发TypeError异常。
  3. 若序列的索引类型正确但是超出范围,会引发IndexError异常。
  
  实践一个无穷算术序列的例子:

>>> def checkIndex(key): #检查key是否有效。key应为非负整数,若不是整数,引发TypeError;若是负数,引发IndexError
...   if not isinstance(key, (int, long)): 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):   #获取算术序列的某项
...     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   #修改后的值保存到changed
... 
>>> s = ArithmeticSequence(1, 2)
>>> s[4]
9
>>> s[4]=2
>>> s[4]
2
>>> s[5]
11

  上例中没有实现__del__是希望删除项的操作是非法的:

>>> del s[3]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: __delitem__

  上例中实现了对序列索引有效性检查

>>> s["four"]  #索引类型错误,引发TypeError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __getitem__
  File "<stdin>", line 2, in checkIndex
TypeError
>>> s[-2]  #索引超出有效范围,引发IndexError
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in __getitem__
  File "<stdin>", line 3, in checkIndex
IndexError

  ★isinstance函数应尽量避免使用,因为类/类型的检查和Python中多态的目标背道而驰。由于Python语言明确规定索引必须为(长)整数,上面代码才会如此使用。遵守标准是适用类型检查的正当理由之一。
  

9.3.2 子类化列表、字典和字符串

  标准库有3个关于序列和映射规则(UserList、UserString和UserDict)可以立即使用的实现,在较新版本的Python中,可以子类化内建类型。若希望实现一个和内建列表行为相似的序列,可以使用子类list。
  下面创建一个带有访问计数的列表。

>>> 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依赖于它的子类化超类的行为。CounterList没有重写的任何方法都能被直接使用。在被重写的方法中,使用super方法调用相应超类方法,在__init__中添加特性counter的初始化行为,在__getitem__中更新counter特性。

>>> cl = CounterList(range(10))
>>> cl
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> cl.reverse()
>>> cl
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
>>> del cl[3:6]
>>> cl
[9, 8, 7, 3, 2, 1, 0]
>>> cl.counter #列表元素没有被访问
0
>>> cl[4] + cl[2] #列表元素被访问2次
9
>>> cl.counter
2

9.4 更多魔力

  更多特殊函数请参考《Python参考手册》中的3.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
... 
>>> r = Rectangle()
>>> r.width = 10
>>> r.height = 5
>>> r.getSize() #使用getSize()获取size属性
(10, 5)
>>> r.setSize((150, 100)) #使用setSize()设置size属性
>>> r.width
150

  上例中,size是width和height构成的元组,为假象特性,getSize和getSize是它的访问器方法。若想改变类的实现,将size变成真正的特性,动态计算width和height,这样要把它们放到访问器方法中去,那么使用这个类的程序都需要重写。
  Python可以隐藏访问器方法,让所有特性看起来一样。这些通过访问其定义的特性成为属性
  Python中有两种创建属性的机制。主要讨论新的机制:只在新式类中使用的property函数,然后说明如何用特殊方法实现属性。

9.5.1 property 函数

  参考上节中的例子,使用property函数创建size属性。

>>> __metaclass__=type  #若不是新式类,property创建的属性读取可以工作,赋值不一定(取决于Python的版本)
>>> 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属性
>>> r = Rectangle()
>>> r.width = 10
>>> r.height = 5
>>> r.size    #size可以作为普通属性被访问
(10, 5)
>>> r.size = (150, 100)
>>> r.width
150

  实际上,property函数可以用4个参数调用。property(fget, fset, fdel, doc),其中fget是读取特性的方法,fset是设置特性的方法,fdel是删除特性的方法,doc是文档字符串。
  
  ★★★property函数的工作原理
  property函数不是一个真正的函数——它是是利用有很多特殊方法的类,正式这些方法完成了所有的工作。涉及地方法有__get__、__set__和__delete__,这三个方法结合定义了描述符规则。实现了其中任何一个方法的对象叫描述符(descriptor)。描述符的特殊在于他们如何被访问。例如,程序读取一个特性时(尤其在实例中访问该特性,但该特性在类中定义时),如果该特性被绑定到实现了__get__方法的对象上,那么会调用__get__方法,而不只是简单的返回对象。This is, in fact, the mechanism underlying properties, bound methods, static and class methods (see the following section for more information), and super(呵呵,翻译得很别扭)。关于描述符规则的全面介绍可参考Raymond Hettinger的How-To Guide for Descriptors

9.5.2 静态方法和类成员方法

  静态方法和类成员方法分别在创建时被装入Staticmethod类型和Classmethod类型的对象中。
  静态方法的定义没有self参数,且能够被类本身直接调用。
  类成员方法定义时需要名为cls的类似于self的参数,类成员方法可以直接用类的具体对象调用,cls参数是自动被绑定到类的。

>>> __metaclass__=type
>>> 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)
... 

  上述代码使用手动包装和替换方法实现静态和类成员方法。在Python2.4中,引入装饰器(decorators)语法(可用于任何可调用对象:方法、函数)。使用@操作符,在方法(函数)上方列出装饰器,指定一个或更多装饰器(多个装饰在应用时的顺序与指定顺序相反)。上述代码可修改为:

>>> __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.cmeth()
This is a class method of <class '__main__.MyClass'>

  静态方法和类成员方法在大多情况下可以使用函数或者绑定方法代替。但不可忽视静态方法和类成员方法的应用(比如工厂函数)。
 

9.5.3 __getattr__、__setattr__和它的朋友们

  可以拦截对象的所有特性访问,可用旧式类实现属性。为访问特性的时候可以执行代码,需要使用以下魔法方法(旧式类中只需要后3个):
  ■ __getattribute__(self, name):当特性name被访问时自动被调用(只适用新式类)。
  ■ __getattr__(self, name):当特性name被访问且对象没有相应特性是被自动调用。
  ■ __setattr__(self, name, value):当试图给特性name赋值时会被自动调用。
  ■ __delattr__(self, name):当时图删除特性name是被自动调用。
  
  尽管和使用property函数相比有点复杂(某些方面效率低),但这些特殊方法很强大,因为可对处理很多属性的方法进行再编码。
  采用Rectangle说明:

>>> class Rectangle:
...   def __init__(self):
...     self.width = 0
...     self.height = 0
...   def __setattr__(self, name, value):   #考虑两种情况:size和非size特性
...     if name =="size":   
...       self.width, self.height = value
...     else:   #若非size特性,为了避免__setattr__被再次调用(导致死循环),使用__dict__方法代替普通的特性赋值操作
...       self.__dict__[name] = value   #__dict__包含一个字典,字典里是所有实例的属性。
...   def __getattr__(self, name):  #若希望类和hasattr或者getattr这样内建函数一起使用,__getattr__方法就很重要
...     if name == "size":
...       return self.width, self.height
...     else:   #若不是size属性,引发AttributeError,表示特性不存在
...       raise AttributeError
... 

  ★★★死循环陷阱和__setattr__有关,另一个陷阱和__getattribute__(__getattribute__拦截所有特性的访问,包括__dict__)有关。访问__getattribute__中的self相关特性时,使用超类的__getattribute__方法是唯一安全的途径。

9.6 迭代器

  __iter__,这个方法是迭代器规则(iterator protocol)的基础。

9.6.1 迭代器规则

  迭代的意思是重复做一些事很多次。
  __iter__方法返回一个迭代器(iterator),所谓迭代器就是具有next方法的对象。调用next方法时,迭代器回放会他的下一个值。如果next方法被调用,但迭代器没有值可以返回,会引发StopIteration异常。
  ★Python3.0中,迭代器对象应实现__next__方法,而非next。新的内建函数next可以用于访问这个方法。即next(it)等同于Python3.0之前的it.next()。
  与迭代器相比,列表会占用太多内存。以斐波那契数列为例:

SyntaxError: invalid syntax
>>> 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__会放到其他的会在for循环中使用的对象中。例如:

>>> fibs = Fibs()
>>> for f in fibs:
...   if f > 1000:  #查找斐波那契数列中大于1000的最小数
...     print f
...     break
... 
1597

  ★正式说法:一个实现了__iter__方法的对象是可迭代的,一个实现了next方法的对象是迭代器

  内建函数iter可以从壳迭代的对象中获得迭代器。

>>> it = iter([1, 2, 3, 4])
>>> it.next()
1
>>> it.next()
2
>>> it.next()
3
>>> it.next()
4

9.6.2 从迭代器得到序列

  在大部分能使用序列的情况下(除了索引或分片等操作中),能使用迭代器(可迭代对象)替换。参照下面的例子:

>>> 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)  #使用list构造方法显示将迭代器转化为列表
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

9.7 生成器

  生成器是Python新引入的概念,也叫简单生成器。它和迭代器可能是近几年来引入的最强大的两个特性。
  生成器是一种用普通函数语法定义的迭代器。

9.7.1 创建生成器

  以创建一个展开嵌套列表的函数为例:

#参数nested是一个嵌套的列表
>>> nested = [[1, 2], [3, 4], [5]]
>>> def flatten(nested):  #首先迭代嵌套列表中的子列表,然后按顺序迭代子列表中的元素
...   for sublist in nested:
...     for element in sublist:
...       yield element 
... 
#通过在生成器上迭代来使用所有的值
>>> for num in flatten(nested):
...   print num
... 
1
2
3
4
5
>>> list(flatten(nested))
[1, 2, 3, 4, 5]

  flatten函数使用yield语句,任何包含了yield语句的函数成为生成器
  yield语句与普通函数区别在于:它不像return那样返回值,而是每次产生多个值。每次产生一个值(使用yield语句),函数就会被冻结:即函数停在那点等待被激活。函数被激活后就从停止的那点开始执行。

★★★循环生成器
  Python2.4引入了列表推导式的概念,生成器推导式(生成器表达式)和列表推导式的工作方式类似,只不过返回的不是列表而是生成器(并且不会立刻进行循环)。所返回的生成器允许你像下面这样一步一步地进行计算:

>>> g = ((i+2)**2 for i in range(2, 27)) 
>>> g.next()
16
>>> g.next()
25

  列表推导式使用[],生成器推导式使用()。简单例子中推荐使用列表推导式,若希望将可迭代对象“打包”,推荐使用生成器推导式,因为列表推导式会立刻实例化列表,失去了迭代的优势。
  另外,生成器推导式可以在调用处的()内直接使用。例如:

>>> sum(i**2 for i in range(10))
285

9.7.2 递归生成器

  上节创建的生成器只能处理两层嵌套,为了处理嵌套使用了两个for循环。若处理任意层嵌套,则需要采用递归(recursion)生成器。例如:

#考虑两种情况:基本情况和需要递归的情况
>>> def flatten(nested):
...   try:
...     for sublist in nested:  
...       for element in flatten(sublist):  #递归情况:sublist是列表,继续展开
...         yield element   
...   except TypeError: #基本情况:nested是一个元素,引发TypeError异常,生成器产生一个元素
...     yield nested
... 
>>> list(flatten([[[1], 2], 3, 4, [5, [6, 7]], 8]))
[1, 2, 3, 4, 5, 6, 7, 8]

  ★上述例子中,不应在flatten中对类似于字符串的对象进行迭代,原因如下:
  1. 需实现的是将类似于字符串的对象当成原子值,而不是硬背展开的序列;
  2. 对他们进行迭代会导致无穷递归,因为一个字符串的第一个元素是另一个长度为1的字符串,而长度为1的字符串的第一个元素就是字符串本身。
  
  为了解决上述问题,添加对字符串的检测。

>>> def flatten(nested):
...   try:
...     try: nested + ""    #与字符串连接,检测TypeError判断是否为字符串
...     except TypeError:pass   #出现TypeError说明不是字符串,则继续递归操作
...     else:raise TypeError    #未出现TypeError说明是字符串,向上层try/except传递TypeError
...     for sublist in nested:
...       for element in flatten(sublist):
...         yield element
...   except TypeError: #若检测nested是字符串或单个元素,则返回nested
...     yield nested
... 
>>> list(flatten(["foo", ["bar", ["baz"]], [[1], 2], 3, 4, [5, [6, 7]], 8]))
['foo', 'bar', 'baz', 1, 2, 3, 4, 5, 6, 7, 8]

  ★上述代码没有检测nested是否是字符串(isinstance),只是检测nested的行为是不是像字符串。

9.7.3 通用生成器

  生成器是有两部分组成:
  ★生成器的函数:使用def语句定义,包含yield的函数。每次请求一个值,会执行生成器函数,直到遇到一个yield或者return语句。
  ★生成器的迭代器:生成器函数的返回部分。生成器函数执行到yield或者return语句时返回。yield语句意味着生成一个值,返回一个迭代器;return语句意味着生成器停止执行,不再生成任何东西。

9.7.4 生成器方法

 send方法

  生成器的新属性(Python2.5之后)是在开始运行后为生成器提供值的能力。表现为生成器和“外部世界”交流的渠道,需要注意以下两点。
  ■ 外部作用域访问生成器的send方法,类似访问next方法,只不过前者使用一个参数。
  ■ 内部则挂起生成器,yield现在作为表达式而不是语句使用,换句话说,当生成器重新运行时,yield方法返回一个值,也就是外部通过send方法发送的值。若next方法被使用,那么yield方法返回None。
  NOTE:使用send方法只有在生成器挂起之后才有意义。如果在此之前需要给生成器提供更多信息,那么只需使用生成器函数的参数。
  ★若想对刚启动的生成器使用send方法,可以将None作为参数进行调用。
  参考下例:

>>> def repeater(value):
...   while True:
...     new = (yield value) #使用yield的返回值时,建议闭合yield表达式
...     if new is not None: value = new
... 
>>> r = repeater(42) 
>>> r.next()
42
>>> r.next()
42
>>> r.send(44) #当向repeater发送44,repeater内的new被赋值为44
44
>>> r.next()
44
 throw方法

  用于在生成器内(yield表达式中)引发一个异常,使用异常类型调用,还有可选的值以及回溯对象

 close方法

  用于停止生成器。
  close方法是建立在异常的基础上的(需要时可有Python垃圾收集器调用),它在yield运行处引发一个GeneratorExit异常。所以如果需要在生成器内进行代码清理的,可以将yield语句放在try/finally语句中。如果需要,可以捕捉GeneratorExit异常,但随后需要将其重新引发、引发另一异常或者直接返回。若在生成器的close方法调用后通过生成器生成一个值会导致RuntimeError异常

9.7.5 模拟生成器

  生成器在旧版本的Python中是不可用的。本节介绍使用普通函数模拟生成器。
  将flatten生成器用普通函数重写示例:

>>> def flatten(nested): #返回nested解嵌套后的元素列表
...   result = []   
...   try:
...     try: nested + ""
...     except TypeError: pass
...     else: raise TypeError
...     for sublist in nested:
...       for element in flatten(sublist):
...         result.append(element)
...   except TypeError:
...     result.append(nested)
...   return result
... 
>>> flatten(["foo", ["bar", ["baz"]], [[1], 2], 3, 4, [5, [6, 7]], 8])
['foo', 'bar', 'baz', 1, 2, 3, 4, 5, 6, 7, 8]

9.8 八皇后问题

9.8.1 生成器和回溯

  生成器是逐渐产生结果的复杂递归算法的理想实现工具。
  ■ 若没有生成器,算法是需要额外参数传递的半成品方案,递归调用可以在这个方案上建立。
  ■ 若使用生成器,所有递归调用只要创建自己的yield部分。

  一些应用中,答案需要做很多次选择才能得出。为了解决上述问题,需要使用回溯策略,这种策略在解决需要尝试每种组合,直到找到一种解决方案的问题时很有用。这类问题按照下面为代码的方式解决:

#伪代码(当不知道可能性的层数,可以使用递归)1层所有的可能性:
  第2层所有的可能性:
    ……
      第n层所有的可能性:
        可行吗?

9.8.2 问题

  谜题:有一个棋盘和8个要放到上面的皇后。唯一的要求是皇后之间不能形成威胁。
  原理:这是经典的回溯问题——尝试放置第1个皇后(在第1行),然后放置第2个,以此类推。若发现不能放置下一个皇后,回溯到上一步,试着将皇后放到其他的位置。知道尝试完所有可能或找到解决方案。
  问题拓展:假设任意数目的皇后比较符合实际生活中的回溯问题,接下来给出解决方案。

9.8.3 状态表示

  可以使用元组(或列表)表示一个可能的解决方案。每个元组中元素表示相应行皇后的位置(state[0]==3表示在第1行的皇后是在第4列)。

9.8.4 寻找冲突

  为了找到一种没有冲突的设置,首先定义冲突是什么,使用函数conflict表达:

#已知的皇后位置state被传递给conflict函数,判断下一行的皇后位置nextX(列)会不会有新的冲突
>>> def conflict(state, nextX):
...   nextY = len(state)    #nextY:垂直位置(行)
...   for i in range(nextY):
...     if abs(state[i] - nextX) in (0, nextY - i): #0:nextX与第i行皇后在同一列
...       return True                               #nextY-i:nextX与第i行皇后在一条对角线上
...   return False

9.8.5 基本情况

  考虑基本的情况:最后一个皇后
  你想让她做什么?它要能根据其他皇后的位置生成自己能占据的所有位置(maybe none)。使用函数queens表达:

#若只剩一个皇后没有放置,那么遍历它的所有可能的位置,并返回没有冲突发生的位置
>>> def queens(num, state): #num:皇后总数;state:存放前面皇后的位置信息的元组
...   if len(state) == num - 1:
...     for pos in range(num):
...       if not conflict(state, pos):
...         yield pos
>>> list(queens(4, (1,3,0))) #假设有4个皇后,前3个分别放在1、3、0号位置
[2]

9.8.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
... 
>>> list(queens(4))
[(1, 3, 0, 2), (2, 0, 3, 1)]
>>> list(queens(5))
[(0, 2, 4, 1, 3), (0, 3, 1, 4, 2), (1, 3, 0, 2, 4), (1, 4, 2, 0, 3), (2, 0, 3, 1, 4), (2, 4, 1, 3, 0), (3, 0, 2, 4, 1), (3, 1, 4, 2, 0), (4, 1, 3, 0, 2), (4, 2, 0, 3, 1)]

  使用上述算法解决8皇后问题:

>>> for solution in queens(8):
...   print solution
... 
(0, 4, 7, 5, 2, 6, 1, 3)
(0, 5, 7, 2, 6, 3, 1, 4)
……
(7, 2, 0, 5, 1, 4, 6, 3)
(7, 3, 0, 2, 5, 1, 6, 4)

9.8.7 打包

  结束八皇后问题之前,尝试将输出处理得更容易理解一些,容易发现错误。

>>> def prettyprint(solution):
...   def line(pos, length=len(solution)):
...     return "." * (pos) + "X" + "." * (length-pos-1)
...   for pos in solution:
...     print line(pos)
... 
>>> import random
>>> prettyprint(random.choice(list(queens(8))))
.......X
...X....
X.......
..X.....
.....X..
.X......
......X.
....X...
;