《Python基础教程》第9章 魔方方法、属性和迭代器

本章会(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 接下来学习什么

测试、扩展、打包和一些项目的具体实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,050评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,538评论 1 306
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,673评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,622评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,047评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,974评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,129评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,893评论 0 209
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,654评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,828评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,297评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,619评论 3 262
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,326评论 3 243
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,176评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,975评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,118评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,909评论 2 278

推荐阅读更多精彩内容

  • 我们在学习web前端的路程起步时总是疑问,我们如何更好的遍历元素呢?迭代器和生成器是什么?今天为大家带上与精彩的E...
    侬姝沁儿阅读 3,231评论 0 6
  • 本文翻译自Functional Programming Howto 本文将介绍Python中函数式编程的特性。在对...
    大蟒传奇阅读 2,563评论 4 14
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,111评论 18 139
  • 你不知道JS:异步 第四章:生成器(Generators) 在第二章,我们明确了采用回调表示异步流的两个关键缺点:...
    purple_force阅读 919评论 0 2
  • 感情从来是相互的,我失去的同样你也失去,我有什么好值得难过的。 我们都在改变,你早就不是过去的你,我也不是曾经的我...
    yf2880阅读 235评论 0 1