Python进阶话题杂谈(一)迭代器与迭代器协议

1  迭代器与for

迭代器(iterator),在许多现代编程语言,如C++,Java等中均有出现。迭代器不是一种数据结构,而可以理解为是一种获取、访问数据的策略。在Python中,迭代器以object的形式存在,其自身并不存储数据,而是封装了获取数据的方法[即next方法(Python2)或__next__方法(Python3)]。这意味着:在Python中,所有的迭代器均可通过调用next方法,获取到迭代器的下一个数据。next方法应处理两种情况:1.迭代未结束时,根据规则返回当前位置的下一个数据;2.迭代结束时,抛出StopIteration异常,指示当前迭代器已耗尽,迭代终止。

Python中的for循环本质上即为针对迭代器使用的循环,任何数据结构在进行for循环时,均可以理解为for是在针对由此数据结构生成的一个迭代器进行循环,这包含两种情况:1.for循环的对象本身就是一个迭代器,那么此时就针对当前对象进行迭代;2.for循环的对象本身不是一个迭代器,那么for将自动对此对象调用iter函数(对于iter函数的讨论详见下文),返回当前对象的一个迭代器,然后对此迭代器进行迭代。同时,for循环将在每一次循环时,自动对迭代器调用next函数,得到下一个值,并将此值赋值给迭代变量,当迭代结束,迭代器抛出StopIteration时,for也将自动处理这个异常,结束当前的for循环。由此可见,Python的for循环,相较于C、Java、Perl等语言的“while变体形式”的三段式for循环更为高级与实用。但是,这样的for循环机制可能也是造成Python的for循环效率低下的原因之一。

此外,如果不通过for进行迭代,也可以手动多次调用next函数进行下一个值的获取,并手动处理异常。调用next函数对应于调用迭代器对象的next方法(Python2)或__next__方法(Python3)。这种用法本文不做详细讨论。

2  __iter__与委托迭代

上文中提到了iter函数,通过调用iter函数,可以得到某个对象的一个迭代器。而由于iter函数作为一种“多态性”函数(这里借用OOP术语来描述Python中的函数与特殊方法之间的关系),其可接受各种不同的数据对象,使用不同的算法返回迭代器。能够这样做的原因就在于iter函数背后所对应的__iter__方法。当对对象调用iter函数时,就相当于自动调用当前对象所对应的__iter__方法,即:

iter(xxx) 相当于 xxx.__iter__()

这样,即可通过面向对象的多态性,针对不同的对象,调用不同的__iter__方法了。

__iter__方法是迭代器协议的核心,但在很多的书籍和资料中,对__iter__方法的介绍很模糊,往往只是说明:__iter__方法应直接返回self,这是很不准确的。__iter__方法本质上对应着iter函数的调用,也就是说,这个方法将在for循环开始前被自动调用,其返回值应该为一个迭代器,这个返回的迭代器应实现__next__方法,以及在迭代结束时抛出StopIteration。这就意味着,__iter__方法的返回值有两类,一类就是self,即当前类自身就是迭代器,当返回self时,当前对象本身就还需要实现__next__方法,从而用于被for自动调用。另一种非常关键的情况是:__iter__方法也可以返回另一个可迭代对象,这样,当迭代当前对象时,实际上是迭代了__iter__方法返回的这另一个可迭代对象,这样的操作就称为委托迭代。以下通过两个例子说明自身迭代与委托迭代:

例1:自身迭代器:

class IterTest:

    def __init__(self, topNum):

        self.topNum = topNum

        self.nowNum = -1

    def __iter__(self):

        return self

    def __next__(self):

        if self.nowNum < self.topNum:

            self.nowNum += 1

            return self.nowNum

        else:

            raise StopIteration

for i in IterTest(10):

    print(i)

上述代码即定义了一个自定义的迭代器,IterTest类的实例自身就是一个迭代器,关键之处在于两点:1. __iter__方法直接返回self,也就是说,在当前类的实例被iter函数调用时,对象自身作为迭代器参与迭代,且对象自身将不停地被调用next函数,从而获取下一个值。2.next函数背后对应的就是__next__方法,此方法应实现两个功能,一是在迭代未结束时返回下一个值,二是在迭代结束时抛出StopIteration异常。实际使用中也可定义不会抛出StopIteration异常的迭代器,这样的迭代器就称为无限迭代器(infinite iterator),其在迭代中如果不加以限制,将永远不会结束。无限迭代器一般用于特殊用途,在itertools模块中有一些无限迭代器的接口定义。

以下是委托迭代的例子:

例2:委托迭代器:

class IterTest:

    def __iter__(self):

        return iter([1, 2, 3])

for i in IterTest():

    print(i)

上述代码中,委托迭代器的__iter__方法返回的是另一个与当前对象并无关联的迭代器,在上例中,即为一个值为123的List的迭代器版本。for循环看上去迭代的是IterTest类实例,实际上迭代的却是一个与IterTest类毫不相关的list。但是,由于放在for循环当中的就是IterTest类实例,故不管迭代的是什么,IterTest类实例就是一个可迭代对象。

__iter__方法是Python迭代器协议的核心,任何一个类,只要其正确实现了此方法,返回了一个迭代器,那么这个类的实例就可以被迭代。而具体迭代什么,则由__iter__方法具体返回的内容决定。

3  __*item__与序列访问协议

__*item__包括__getitem__、__setitem__与__delitem__三个特殊方法。__delitem__是析构方法的一种,其应用场合较少,故本文不对其进行讨论,只讨论前两个特殊方法。

__getitem__方法在Python中有两个主要功能,首先,此方法是序列访问协议的核心,一个类只要实现了此方法,那么这个类的对象就可以通过类似于list的索引值语法,对当前类的一些数据进行访问。其次,__getitem__方法也是迭代器协议的备用方法,当一个类没有实现__iter__方法时,就会尝试通过__getitem__方法,按照索引值从0到溢出的顺序进行迭代访问,举例如下:

class IterTest:

    numList = [1, 2, 3]

    def __getitem__(self, sliceObj):

        return IterTest.numList[sliceObj]

print(IterTest()[1:])

for i in IterTest():

    print(i)

上例中定义了一个仅实现了__getitem__方法的类,此方法的第二参数为一个slice对象,slice是Python内部的一个特殊对象,其接受各种语法形式的切片(如[:], [1:2], [1:]等),此对象在索引访问时自动生成,且可直接放入一对方括号中进行值的访问。当一个类实现了__getitem__方法后,首先,这个类的实例可使用任何形式的切片语法,如上例中的IterTest()[1:];其次,__getitem__方法作为迭代器协议的备选方法,只实现此方法的对象也可通过for进行索引值从0到溢出的迭代。

只实现__getitem__方法的类,其通过索引值访问到的值是只读的,不可通过赋值进行修改。而如果要对值进行修改,则还需要实现__setitem__方法。__setitem__方法在绝大多数情况下的实现均很简单,只需要进行赋值行为定义即可:

def __setitem__(self, sliceObj, setValue):

IterTest.numList[sliceObj] = setValue

    IterTest()[1] = 1

    定义此方法后,就可以对通过索引值取出的值进行赋值了。

4  yield

yield是另一种定义迭代器的方法。而实际上,yield(包括send与Python3.5的yield from)还用于实现协程,这部分本文不予讨论,只讨论yield在迭代器中的应用。

上文中,主要讨论了OOP下的迭代器定义,而yield就可以简单理解为POP下对迭代器的定义,通过yield实现的迭代器在Python中称为生成器(Generator)。需要强调的是,生成器只是迭代器的一种,其语法上与迭代器没有任何区别。

通过yield定义生成器非常简单:如果一个函数中至少出现了一处yield语句,那么此函数的返回值将自动转换成一个生成器,即可以使用for或next进行迭代。yield在行为上类似于return,将一个值返回给外部,但不同的是,return语句具有立即退出函数调用的功能,而yield语句不会这样做,而是将函数调用过程暂停到yield之后的位置,当下一次的next方法被调用(或调用了send方法),那么函数就会在暂停处继续执行,直到又遇到了一次yield,或函数在别处退出。下面即定义了一个简单的生成器:

def yieldTest(topNum):

    nowNum = 0

    while nowNum < topNum:

        yield nowNum

        nowNum += 1

for i in yieldTest(10):

    print(i)

上例的yieldTest函数,由于出现了yield,则此函数的返回值就是一个生成器。此生成器在没有达到数字上限时,会不断返回递增的数字,然后暂停,等待下一次的返回,在while循环退出后,函数结束,生成器也就随之耗尽。

5  生成器推导式

上文中讨论了如何通过含有yield的函数定义一个生成器。而Python的推导式语法系列也包含生成器推导式,可以通过简洁的形式,直接生成一个生成器。生成器推导式以一对小括号包围,中间书写推导式语法即可:

for i in (i**2 for i in range(10)):

    print(i)

6  慎用迭代器

迭代器具有一些很优良的性质,如节约内存,运算速度快等。但在使用迭代器时,一定要考虑迭代器的两个最重要的隐患:

1. 迭代器是一次性的。普通的迭代器(除了文件句柄这样的特殊迭代器),如果不做特殊处理,则其都是一次性使用的,无法回退。当迭代器耗尽后,后续的next函数调用只会不断的抛出StopIteration异常。如果还要重新使用当前的迭代器,则只能通过原始数据再生成一个新的迭代器。这在某些情况下可能会导致效率低下和难以察觉的bug。

2. 对迭代器进行索引值访问是非常低效,且消耗迭代器的。故涉及到索引值访问这样的操作时,一般不应使用迭代器,而是将迭代器通过转换为list再进行操作。


2018年6月于苏州

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

推荐阅读更多精彩内容