Python基础教程第九章学习笔记——魔法方法、属性和迭代器

来源:互联网 发布:阿里云 百度云 知乎 编辑:程序博客网 时间:2024/05/16 15:12

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

Python中有些名称前面和后面都会加上两个下划线(如__future__),这种拼写表示名字有特殊含义。所有绝不要在自己的程序中使用这种名字。
Python中,由这些名字组成的集合所包含的方法称为魔法(或特殊)方法。若对象实现了这些方法中的某一个,那这个方法会在特殊情况下(确切的说是根据名字)被Python调用。几乎没有直接调用它们的必要。
本章会详细讨论一些重要的魔法方法(最重要的是__init__方法和一些处理对象访问的方法——这些方法允许你创建自己的序列或映射)。还会处理两个相关主题:属性和迭代器。

9.1 准备工作

Python对象的工作方式从Python2.2以后有了很大的改变。这种改变产生了一些影响,对刚使用者,大多改变不很重要。但需注意:尽管使用的新版Python,但一些特性(如属性和super函数)不会在老式的类上起作用。为确保类是新型的,应把赋值语句__metaclass__=type放在模块的最开始,或(直接或间接)子类化内建类(实际上是类型)object(或一些其他新式类)。
#考虑下面两个类
class NewStyle(object):             #新式类
    more_code_here
class OldStyle:                           #旧式类
    more_code_here
而如果文件以__metaclass__=type开始,那两个类型都是新式类。
还可在自己的类的作用域中对__metaclass__变量赋值——只会为这个类设定元类。元类是其他类(或类型)的类——是一个更高级的主题。
如果没有兼容之前旧版Python的需要,建议把所有类写为新式类,且用super函数这样的特性。

9.2 构造方法

首先讨论的魔法方法是:构造方法——代表着类似于init的初始化方法。但构造方法与普通方法不同之处:当一个对象被创建后,会立即调用构造方法。
>>> f = FooBar()
>>> f.init()                     #普通方法还需在调用init(),然后在调用方法f.somevar
>>> f.somevar
42
构造方法能使其简化为:
>>> f = FooBar()
在Python中创建一个构造方法容易。只要把init方法的名字从简单的init修改为魔法版本__init__即可:
class FooBar:
    def __init__(self):
        self.somevar = 42
>>> f = FooBar()
>>> f.somevar             #构造方法可直接调用,无需先调用init()
42
如果给构造方法传几个参数会发生什么呢?
class FooBar:
    def __init__(self, value=42):     #添加可变参数
        self.somevar = value
因为参数是可选的,所以可当做什么都没发生,继续下去。但要使用参数时(或不让参数可选时),会发生:
>>> f = FooBar('This is a constructor argument')
>>> f.somevar
'This is a constructor argument
Python所有魔法方法中,__init__是使用最多的一个。其还有一个魔法方法叫__del__,即析构方法——其在对象就要被垃圾回收之前调用,但发生调用具体时间不可知,尽力避免使用它。

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

如果一个方法在B类的一个实例中被调用(或一个属性被访问),但在B类中没找到该方法,那就会去它的超类A里面去找。
#考虑下面的两个类
class A:
    def hello(self):                               #A中定义一个叫hello的方法
        print "Hello, I'm A."
class B(A):                                        #B类继承A中的hello方法
    pass
类如何工作的例子:
>>> a = A()
>>> b = B()
>>> a.hello()
Hello, I'm A.
>>> b.hello()
Hello, I'm A.                     #因为B类没定义自己的hello方法,所以当hello被调用时,打印的是原始信息。

子类中增加功能的最基本的方式就是增加方法。但也可重写一些超类的方法来定义继承的行为
#B类重写hello方法
class B(A):
    def hello(self):
        print "Hello, I'm B"
此时:
>>> b = B()
>>> b.hello()
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!'
示例:
>>> b = Bird()
>>> b.eat()
Aaaah...
>>> b.eat()
No, thanks!
#考虑子类SongBird,添加唱歌行为
class SongBird(Bird):
    def __init__(self): 
        self.sound = 'Squawk!'                    # 构造方法被重写,但新的构造方法没有任何关于初始化hungry特性的代码。
    def sing(self):
        print self.sound
示例:
>>> sb = SongBird()
>>> sb.sing()
Squawk!
SongBird是Bird的一个子类,继承了eat方法,但调用eat方法会产生一个问题:
>>> sb.eat()
Traceback ...
...
...
AttributeError: SongBird instance has no attribute 'hungry'
错误是:SongBird没有hungry特性。这是因为在SongBird中,构造方法被重写,但新的构造方法没有任何关于初始化hungry特性的代码。为达到预期效果,SongBird的构造方法必须调用其超类Bird的构造方法来确保进行基本的初始化——两种方法:调用超类构造方法的未绑定版本或使用super函数—后边两节会介绍。

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

目前版本Python内,使用super函数的方法简单明了。但是很多已完成的代码还是使用本节介绍的方法——是一个了解绑定和未绑定方法之间区别的好例子。
#调用超类的构造方法很容易(很有用)
#解决上节末尾提出的问题
class SongBird(Bird):
    def __init__(self):
        Bird.__init__(self)                        #相比只多加了这一行代码
        self.sound = 'Squawk!'
    def sing(self):
        print self.sound
运行:
>>> sb = SongBird()
>>> sb.sing()
Squawk!
>>> sb.eat()
Aaaah...
>>> sb.eat()
No, thanks!
在调用一个实例的方法时,该方法的self参数会被自动绑定到实例上(绑定方法)。但直接调用类的方法(如Bird.__init__),没有实例被绑定,所以可自由提供需要的self参数。此方法称为未绑定方法。通过将当前的实例作为self参数提供给未绑定方法,SongBird就能使用其超类构造方法的所有实现,此时属性hungry能被设置。

9.2.3 使用super函数

super函数只能在新式类中使用,应使用新式类。当前的类和对象可作为super函数的参数使用,调用函数返回的对象的任何方法都是调用超类的方法,而不是当前类的方法。那就可以不用在SongBird的构造方法中使用Bird,而直接使用super(SongBird,self)。除此之外,__init__方法能以一个普通的(绑定)方式被调用
#Bird例子的更新
__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):
        super(SongBird,self).__init__()                       #相比只改了这一行代码
        self.sound = 'Squawk!'
    def sing(self):
        print self.sound
运行结果(与旧式版本一样):
>>> sb = SongBird()
>>> sb.sing()
Squawk!
>>> sb.eat()
Aaaah...
>>> sb.eat()
No, thanks!

9.3 成员访问

介绍另一个有用的魔法方法集合,它可创建行为类似于序列或映射的对象。
基本的序列和映射规则很简单,但要实现它们的全部功能就需要实现很多魔法函数(但也有一些捷径)

9.3.1 基本的序列和映射规则

序列和映射是对象的集合。为实现他们基本的行为(规则),如果对象是不可变的——需要使用两个魔法方法,如果可变——需要使用4个魔法方法。
__len__(self):返回集合中所含项目的数量。对序列—元素的个数;对映射—键-值对的数量。若__len__返回0(且未实现重写该行为的__nonzero__),对象会被当做一个布尔变量中的假值(空列表、元组、字符串和字典也一样)进行处理
__getitem__(self.key):返回与所给键对应的值。对序列—键应是一个0~n-1的整数(或像后边所说的负数),n为序列长度;对映射—可使用任何种类的键。
__setitem__(self,key,value):按一定方式存储和key相关的value,该值随后可使用__getitem__来获取。只能为可修改的对象定义此方法。
__delitem__(self,key):对一部对象使用del语句时被调用,同时必须删除和元素相关的键。为可修改的对象定义(不删除全部对象,只删除一些需移除的元素)
上述方法的附加要求:
对一个序列,若键是负整数,那要从末尾开始计数(x[-n]和x[len(x)-n]是一样的);
如果键是不合适的类型(如,对序列使用字符串作为键),会引发一个TypeError异常;
若序列的索引是正确的类型,但超出了范围,引发一个IndexError异常
#实践——若创建一个无穷序列,会发生什么:
def checkIndex(key):
"""
    所给的键是能接受的索引吗?

    为了能被接受,键应该是一个非负的整数。若它不是整数,会引发TypeError;若是负数,会引发IndexError(因为序列无限长)
    """
    if not isinstance(key, (int, long)): raise TypeError                 #Python语言规范明确指出索引必须是(长)整数,所以才用isinstance函数——应尽量避免使用(多态违背)
    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                 #保存更改后的值
以上:实现的是一个算术序列——每个元素都比它前面的元素大一个常数,第一个值由构造方法参数start(默认为0)给出,步长由step设定(默认1)。用户能通过名为changed的方法将特例规则保存在字典中,从而修改一些元素的值,若元素未被修改,就计算self start + key*self step的值。
使用上例:
>>> s = ArithmeticSequence(1,2)
>>> s[4]
9
>>> s[4] = 2
>>> s[4]
2
>>> s[5]
11
没实现__del__方法是因为我希望删除元素是非法的:
>>> del s[4]
Traceback ...
...
AttributrError: ...
这个类中没__len__方法,因为它是无限长的。
若使用一个非法类型的索引,就会引发TypeError异常;若索引类型正确但超出范围(本例子中为负数),会引发IndexError异常:
>>> s["four"]
Traceback ... 
... 
TypeError
>>> s[-42]
Traceback ...
...
IndexError                     #索引检查是通过用户自定义的checkIndex函数实现的

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

已介绍基本的序列/映射规则的四种方法,官方语言规范也推荐实现其他的特殊方法和普通方法,包括9.6结中的__iter__方法。
实现所有这些方法是繁重工作且难做好。若只想在一个操作中自定义行为,那其他方法不必实现(程序员懒惰)——关键词是继承。标准库有三个关于序列和映射规则(UserList、UserString和UserDict)可立即实现使用,较新版Python中,可子类化内建类型。
所以,若希望实现一个和内建列表行为相似的序列,可使用子类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)的行为,其没有重写任何的方法(和append、extend、index一样)都能直接使用。在两个被重写的方法中,super方法被用来调用相应的超类的方法,只在__init__中添加了所需的初始化counter特性的行为,并在__getitem__中更新了counter特性。
运行例子:
>>> 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
可见:CounterList在很多方面和列表作用一个,但它有一个counter特性(被初始化为0),每次列表元素被访问时,它都会自增,所以执行 c1[4] + c1[2]后,值自增两次,变为2

9.4 更多魔力

魔法名称的用途有很多——大部分特殊方法都是为高级用法准备的。可模拟数字、让对象像函数那样被调用、影响对象的比较等

9.5 属性

第七章曾提过访问器方法。访问器是一个简单的方法,能使用getHeight、setHeight这样的名字来得到或重新绑定一些特性。
若在访问给定特性时必须要采取一些行动,那像这样的封装状态变量(特性)就很重要。如:
#Rectangle类
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
上例中,getSize和setSize方法是一个名为size的假想特性的访问器方法,size是由width和height构成的元组。可随意用别的方法替换这里的函数,如:计算面积、对角线长度,此时代码没错,但是有缺陷。
程序员使用这个类时不应该还要考虑它是怎么实现的(封装)。若要改变类的实现,将size变为一个真正的特性,此时width和height可动态算出;那就要把他们放到一个访问器方法中去,并任何使用这个类的程序都必须重写。客户代码(使用代码的代码)应能够用同样的方式对待所有特性。
解决:可把所有的属性放到访问器方法中,但若有很多简单的特性就不现实了(笨,要写很多访问器方法—除返回或设置特性啥也不做了);复制加粘贴式或切割代码式的编程方式很糟糕;Python能隐藏访问器方法,让所有特性看起来一样——这些通过访问器定义的特性被称为属性。
Python中有两种创建属性机制,主要介绍只在新式类中使用的property函数,简单介绍如何使用特殊方法实现属性。

9.5.1 property函数

property函数的使用很简单。只需在类的前面增加一行代码(子类化object,或使用__metaclass__=type语句):
__metaclass__ = type

class MyClass:
    
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。
运行例子:
>>> r = Rectangle()
>>> r.width = 10
>>> r.height = 5
>>> r.size
(10, 5)
>>> r.size = 150, 100
>>> r.width
150
size特性仍取决于getSize和setSize中的计算,但看起来就像普通的属性一样。
实际上,property函数可用0、1、2、3或者4个参数来调用。如果没有参数,产生的属性既不可读,也不可写。如果只使用一个参数调用(一个取值方法),产生的属性是只读的。第三个参数(可选)是一个用于删除特性的方法(它不要参数)。第四个参数(可选)是一个文档字符串。property的4个参数分别被叫做fget、fset、fdel和doc—。如果想要一个属性是只写的,并且有一个文档字符串,能使用它们作为关键字参数。

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():
        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():
        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'>
静态方法和类成员方法在Python中并不是向来都很重要,主要是因为大部分情况下可使用函数或绑定方法替代。两种方法的应用(如工厂函数)不可忽视,可考虑新技术。

9.5.3 __getattr__、__setattr__和它的朋友们

拦截(intercept)对象的所有特性访问是可能的,这样可以用旧式类实现属性(因为property方法不能使用)。为了在访问特性的时候可以执行代码,必须使用一些魔法方法。以下四种方法提供了需要的功能(旧式类中只需要后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):
        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
此版本的类需要注意增加的管理细节。以下两点需引起重视:
1, __setattr__方法在所涉及到的特性不是size时也会被调用。所以,这个方法必须把两方面都考虑进去:如果属性是size,那就像原来一样执行操作,否则就要使用特殊方法__dict__(该特殊方法包含一个字典,字典里面是所有实例的属性。)。为避免__setattr__方法再次调用(使程序陷入死循环),__dict__方法用来代替普通的特性赋值操作。
2,__getattr__方法只在普通的特性没有被找到的时候调用:若给定的名字不是size,这个特性不存在,此方法会引发一个AttributeError异常。如果希望类和hasattr或者getattr这样的内建函数一起正确的工作,__getattr__方法就很重要。若使用的是size属性,那就会使用在前面的实现中找到的表达式。
__getattribute__拦截所有特性的访问(在新式类中),也拦截对__dict__的访问!访问__getattribute__中与Self相关的特性时,使用超类的__getattribute__方法(使用super函数)是唯一安全的途径。

9.6 迭代器

讨论特殊方法——__iter__,此方法是迭代器规则(iterator protocol)的基础

9.6.1 迭代器规则

迭代:重复做一件事很多次——像循环中做的那样。到现在为止只是在for循环中对序列和字典进行迭代,但实际也能对其他的对象进行迭代:实现__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循环中使用的对象中。这样,程序就能返回所需的迭代器。此外,当迭代器实现它自己的__iter__方法,然后就能直接在for循环中使用迭代其本身了。
一个实现了__iter__方法的对象是可迭代的,一个实现了next方法的对象是迭代器。
运行:
#首先产生一个Fibs对象
>>> fibs = Fibs()
#可在for循环中使用该对象——如查找在斐波那契数列中比1000大的数中的最小的数
>>> for f in fibs:
...         if f > 1000:
                 print f
                 break
...
1597
内建函数iter可以从可迭代的对象中获得迭代器。
>>> 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()
>>> list(ti)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

9.7 生成器—也称为简单生成器

生成器是Python新引入的概念。可帮助我们写出非常优雅的代码(编写任何代码时不使用生成器也可以)。
生成器是一种用普通的函数语法定义的迭代器工作方式可用例子很好地展现。
学习:怎么创建和使用生成器,了解它的内部机制。

9.7.1 创建生成器

就像创建函数一样简单。例子:首先创建一个展开嵌套列表的函数。参数是一个列表——和下面这个很像:
nested = [[1, 2], [3, 4], [5]]               #嵌套列表——列表的列表
函数应该按顺序打印出列表中的数字,如下:
def flatten(nested):
    for sublist in nested:                     #迭代提供的嵌套列表的所有子列表
        for element in sublist:               #按顺序迭代子列表中的元素
            yield element                        #yield语句是新知识,任何包含yield语句的函数称为生成器
yield语句的行为和普通的函数有很大的差别——它不像return那样返回值,而是每次产生多个值。每次产生一个值(使用yield语句),函数就会被冻结(函数停在那点等待被激活)。函数激活后就从停止的那点开始执行。
#现在可通过在生成器上迭代来使用所有的值
>>> nested = [[1, 2], [3, 4], [5]]
>>> for num in flatten(nested):
             print num
...
1
2
3
4
5
或者
>>> list(flatten(nested))
[1, 2, 3, 4, 5]
循环生成器
第五章讲了列表推导式(利用其它列表创建新列表),其使用的是方括号([])。如果使用圆括号(()),就会得到生成器推导式(或称生成器表达式)——其和列表推导式的工作方式类似,但是返回的是生成器而不是列表(且不会立刻进行循环),返回的生成器允许一步一步地进行计算,如:
>>> g = ((i + 2)**2 for i in range(2, 27))
>>> g.next()
16
简单例子中还是推荐使用列表推导式。但如果希望将可迭代对象(如生成大量的值)“打包”,那最好不要用列表推导式,因为它会立即实例化一个列表,从而丧失迭代的优势。
#还有,生成器推导式可在当前的圆括号内直接使用(如在函数调用中),不用额外增加另一对圆括号
sum(i**2 for i in range(10))     列表推导式需要加方括号: sum([i**2 for i in range(10)])

9.7.2 递归生成器

上一节的生成器只能处理两层嵌套——为处理嵌套使用了两个for循环。若要处理任意层嵌套怎么办?如,可能要使用生成器来表示树形结构(也可用于特定的树类,原理一样)。每层嵌套需要增加一个for循环,但因为不知道有几层嵌套,所以必须把解决方案变得更灵活——求助于递归(recursion):
def flatten(nested):
    try:
        for sublist in nested:
            for element in flatten(sublist):
                yield element
    except TypeError:
        yield nested
当flatten被调用时有两种可能性(处理递归时大部分都是两种情况):基本情况和需要递归的情况。
基本情况:函数被告知展开一个元素(如一个数字),会引发一个TypeError异常(因为试图对一个数字进行迭代),生成器产生一个元素。
需要递归的情况:函数被告知展开一个列表(或其他可迭代对象),程序必须遍历所有的子列表(一些可能不是列表),并对他们调用flatten。然后使用另一个for循环来产生被展开的子列表中的所有元素。
>>> list(flatten([[[1], 2], 3, 4, [5, [6, 7]], 8]))
[1, 2, 3, 4, 5, 6, 7, 8]
这样做只有一个问题:如果nested是一个类似于字符串的对象(字符串、Unicode、UserString,等等),那它就是一个序列,不会引发TypeError,但其实你不想对这样的对象进行迭代。
不应再flatten函数中对类似于字符串的对象进行迭代,因为:1) 需要实现的是将类似于字符串的对象当成原子值,而不是当成应被展开的序列。2) 对他们进行迭代实际会导致无穷递归——一个字符串的第一个元素是另一个长度为1的字符串,而长度为1的字符串的第一个元素就是字符串本身。
为处理上述情况,必须在生成器的开始处添加一个检查语句。试着将传入的对象和一个字符串拼接,看会不会出现TypeError——是检查一个对象是不是类似于字符串的最简单、最快速的方法。
#加入了检查语句的生成器
def flatten(nested):
    try:
        #不要迭代类似于字符串的对象
        try: nested + ' '
        except TypeError: pass                   #如果表达式nested + '' 引发了一个TypeError,他就会被忽略。
        else: raise TypeError                        #若没有引发TypeError,那内层try语句的else子句就会引发一个它自己的TypeError异常。
        for sublist in nested:
            for element in flatten(sublist):
                yield element
    except TypeError:
        yield nested
例子:
>>> list(flatten(['foo', ['bar', ['baz']]]))
['foo', 'bar', 'baz']
上边的代码没有进行类型检查(没有测试nested是否是一个字符串——可通过isinstance函数完成检查),只是检查nested的行为是不是像一个字符串(通过和字符串拼接来检查)。

9.7.3 通用生成器

生成器是一个包含yield关键字的函数。当它被调用时,在函数体中的代码不会被执行,而是返回一个迭代器。每次请求一个值,就会执行生成器中的代码,直到遇到一个yield或return语句。yield语句意味着应该生成一个值,return语句意味着生成器要停止执行(不在生成任何东西,return语句只有在生成器中使用时才能进行无参数调用)。
生成器是由两部分组成:生成器的函数和生成器的迭代器。生成器的函数是用def语句定义的,包含yield的部分;生成器的迭代器是这个函数返回的部分。一种不是很准确的说法:两个实体经常被当做一个,合起来叫做生成器。
def simple_generator():
      yield 1
>>> simple_generator
<function simple_generator at 153b44>
>>> simple_generator()
<generator object at 1510b0>           
生成器的函数返回的迭代器可以像其他的迭代器那样使用

9.7.4 生成器方法

生成器的新属性是在开始运行之后为生成器提供值的能力。表现为生成器和“外部世界”进行交流的渠道,要注意以下两点:
1)外部作用域访问生成器的send方法,就像访问next方法一样,只不过前者使用一个参数(要发送的“消息”——任意对象)
2)在内部则挂起生成器,yield现在作为表达式而不是语句使用。即:当生成器重新运行的时候,yield方法返回一个值——外部通过send方法发送的值。如果next方法 被使用,那yield方法返回None。#使用send方法(不是next方法)只有在生成器挂起之后才有意义(也就是在yield函数第一次被执行之后)。如果在此之前需要给生成器提供更多信息,那只需使用生成器函数的参数。(若真想对刚启动的生成器使用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.send("Hello, world!")
"Hello, world!"
生成器还有其他两个方法:
1)throw方法(使用异常类型调用,还有可选的值及回溯对象)用于在生成器内引发一个异常(在yield表达式中)。
2)close方法(调用时不用参数)用于停止生成器。
close方法(在需要时也会由Python垃圾收集器调用)也是建立在异常的基础上的。它在yield运行处引发一个GeneratorExit异常,所以如果需要在生成器内进行代码清理的话,则可以将yield语句放在try/finally语句中。如果需要还可以捕捉GeneratorExit异常,但随后必须将其重新引发(可能在清理之后)、引发另外一个异常或直接返回。
试着在生成器的close方法被调用之后在通过生成器生成一个值则会导致RuntimeError异常。

9.7.5 模拟生成器

生成器在旧版本的Python中是不可用的,所以下面介绍如何使用普通的函数模拟生成器。
先从生成器的代码开始。
首先将下面语句放在函数体的开始处:
           result = []           #如果代码已经使用了result这个名字,那应该用其他名字代替(一个更具描述性的名字)
然后,将下面这种形式的代码:
           yield some_expression
           用下面的语句替换:
           result.append(some_expression)
最后在函数的末尾添加一下语句:
           return result
这个版本可能不适用于所有生成器,但对大多数生成器是可行的(如,不能用于一个无限的生成器——当然不能把它的值放入列表中)。
#flatten生成器用普通函数重写版本
def flatten(nested):
    result = []
    try:
        #不要迭代类似于字符串的对象
        try: nested + ' '
        except TypeError: pass                   #如果表达式nested + '' 引发了一个TypeError,他就会被忽略。
        else: raise TypeError                        #若没有引发TypeError,那内层try语句的else子句就会引发一个它自己的TypeError异常。
        for sublist in nested:
            for element in flatten(sublist):
                result.append(element)
    except TypeError:
        result.append(nested)
    return result

9.8 八皇后问题

学习了所有的魔法方法,所以接下来要将其应用于实践了——如何使用生成器解决经典的编程问题。

9.8.1 生成器和回溯

生成器是逐渐产生结果的复杂递归算法的理想实现工具。若没有生成器,算法就需要一个作为额外参数传递的半成品方案,这样递归调用就可以在这个方案上建立起来。如果使用生成器,那所有的递归调用只要创建自己的yield部分。前一个递归版本的flatten程序中使用的就是后一种做法,相同的策略也可用在遍历(Traverse)图和树形结构中。
    在一些问题中,答案必须在多次选择后才能得出。且程序不止是在一个层面上而是必须在递归的每个层面上做出选择。如:要参加会议,面前有两道门,开会地点在其中一扇门后边,有人挑左进,结果又发现两道门。然后又选左,结果错了,于是回溯到刚才的两扇门那,选右,结果还是错的;于是再次回溯,直到回到开始点,再选择右边的门。
     这样的回溯策略在解决需尝试每种组合,直到找到一种解决方案的问题时很有用。这类问题能按照下面伪代码的方法解决:
#伪代码
第一层所有可能性:
    第二层所有可能性
        。。。。
            第n层可能性:
                可行吗?
    以上若直接使用for循环来实现,就必须知道会遇到的具体判断层数;若无法得知层数信息,那可使用递归。

9.8.2 问题

    在一个棋盘上放置八个皇后,唯一要求:皇后之间不能相互威胁(放置成每个皇后都不能吃掉其他皇后的状态)。如何做到?皇后要如何放置?

    这是一个典型的回溯问题:首先尝试放置第一个皇后(在第一行),然后放置第二个,以此类推。若 发现不能放置下一个皇后,就回溯到上一步,试着将皇后放到其他位置。直到尝试完所有的可能性或找到解决方案。
    问题告知了棋盘上只有8个皇后,但我们假设有任意数目的皇后(更符合实际生活中的回溯问题),怎么解决?

9.8.3 状态表示

    为了表示一个可能的解决方案(或者方案的一部分),可以使用元组(或列表)。每个元组中的元素都指示相应行的皇后的位置(也就是列)。如果state[0]==3,那就表示第1行的皇后是在第4列。当在某一个递归的层面(一个具体的行)时,只能知道上一行皇后的位置,因此需要一个长度小于8的状态元组(或者小于皇后的数目)。

9.8.4 寻找冲突

    首先从一些简单的抽象开始。为找到一种没有冲突的设置,首先必须定义冲突是什么(将其定义为函数conflict)。
    已知的皇后的位置被传递给conflict函数(以状态元组的形式),然后由函数判断下一个的皇后的位置会不会有新的冲突:
def conflict(state,nextX):
    nextY = len(state)
    for i in range(nextY):
        if abs(state[i]-nextX) in (0, nextY-i):     #如果下一个皇后和正在考虑的前一个皇后的水平距离为0(列相同)或等于垂直距离(在一条对角线上)
            return True
    return False
其中参数nextX代表下一个皇后的水平位置(X坐标或列),nextY代表垂直位置(Y坐标或行)。
这个函数对前面的每个皇后的位置做一个简单的检查,如果下一个皇后和前面的皇后有同样的水平位置或在一条对角线上,就会发生冲突,从而返回True。如果没有发生冲突,那就返回False。

9.8.5 基本情况

#此方案的效率不是很高——若皇后数目很多,运行起来会有点慢。
从基本情况开始:最后一个皇后,想让她做什么?假设想找出所有可能的解决方案——能根据其他皇后的位置生成它自己能占据的所有位置(可能没有)。
#如果只剩一个皇后没有放置,那遍历它的所有可能的位置,并且返回没有发生冲突的位置。
def queens(num, state):
    if len(state) == num - 1:
        for pos in range(num):
            if not conflict(state, pos):
                yield pos
其中num参数是皇后的总数,state参数是存放前面皇后的位置信息的元组。
假设有四个皇后,前三个位置分别放在1,3,0号位置(每个皇后占据了一行,并且未知的标号已经到了最大):
>>> list(queens(4, (1, 3, 0)))
[2]            #此种情况下只有一个位置是可行的。

9.8.6 需要递归的情况

  完成基本情况后,递归函数会假设(通过归纳)所有来自低层(有更高编号的皇后)的结果都是正确的。因此需要做的就是为前面的queen函数中实现的if语句增加else子句。
  递归调用会得到什么结果呢?目的是想得到所有低层皇后的位置,所以假设将位置信息作为一个元组返回——此时,需要修改基本情况也返回一个元组(长度为1)。
  这样,程序从前面的皇后得到了包含位置信息的元组,且要为后面皇后提供当前皇后的每种合法的位置信息。
  所以为了让程序运行下去,接下来需要做的就是把当前的位置信息添加到元组中并传递给后面的皇后。
def queens(num, state):
    if len(state) == num - 1:
        for pos in range(num):
            if not conflict(state, pos):
                yield pos
    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
生成器queens能给出所有的解决方案(放置皇后的所有合法方法):
>>> list(queens(3))
[]
>>> list(queens(4))
[(1, 3, 0, 2), (2, 0, 3, 1)]
>>> 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)
>>> len(list(queens(8)))
92

9.8.7 打包

试着将输出处理的更容易理解一点。清理输出总是一个好习惯——容易发现错误。
def prettyprint(solution):
    def line(pos, length=len(solution)):              #一个小的助手函数——假设外面任何地方都不会用到它,所以放在prettyprint函数中
         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 . . . . .

9.9 小结

旧式类和新式类: Python中类的工作方式正在发生改变。新式类在2.2版本中被引入——提供了一些新特征(如使用super函数和property函数,旧式类不能)。为创建新式类,必须直接或间接子类化object,或者设置__metaclass__属性。
魔法方法:Python中的一些特殊方法(名字以双下划线开始和结束的)。这些方法和函数只有很小的不同,但其中大多数方法在某些情况下被Python自动调用(如__init__在对象被创建后调用)。
构造方法:这是面向对象的语言共有的,可能要为自己写的每个类实现构造方法。构造方法被命名为__init__并且在对象被创建后立即自动调用。
重写:一个类能通过实现方法来重写它的超类中定义的这些方法和属性。如果新方法要调用重写版本的方法,可以从超类(旧式类)直接调用未绑定的版本或使用super函数(新式类)。
序列和映射:创建自己的序列或者映射需要实现所有的序列或是映射规则的方法,包括__getitem__和__setitem__这样的特殊方法。通过子类化list(或UserList)和dict(或UserDict)能节省很多工作
迭代器:迭代器是带有next方法的简单对象。迭代器能在一系列的值上进行迭代。当没有值可供迭代时,next方法就会引发StopIterration异常。可迭代对象有一个返回迭代器的__iter__方法,它能像序列那样在for循环中使用。一般,迭代器本身也是可迭代的,即迭代器有返回它自己的next方法。
生成器:生成器函数(或方法)是包含了关键字yield的函数(或方法)。当被调用时,生成器函数返回一个生成器(一种特殊的迭代器)。可使用send、throw、close方法让活动生成器和外界交互。
八皇后问题:使用生成器可轻松解决。如何在棋盘上放置8个皇后,使其不会相互攻击。

9.9.1 本章新函数

iter(obj)                                  #从一个可迭代对象得到迭代器
property(fget, fset, fdel, doc)   #返回一个属性,所有的参数都是可选的
super(class, obj)                    #返回一个类的超类的绑定实例
阅读全文
0 0
原创粉丝点击