迭代器(ITERATOR)和生成器(GENERATOR)

前言


迭代器和生成器可能对于一些人来说知道是什么东东,但是并没有比较深入的了解,那么今天,就跟随我来了解一下这两者的概念,关系及优点。我将使用python中的迭代器和生成器作为演示,如果你不懂python没关系,明白了概念,剩下的就只是编程语言的差异了!这一点很关键,再啰嗦一句,不要为了编程而编程,也要明白一些概念性的东西,编程语言只是工具!

从循环开始说起


想必大家在学习编程的时候,肯定学到过for循环,while循环,do...while循环等等,那么我们为什么需要循环操作呢?因为有些时候我们希望计算机为我们重复的执行同样的操作,比如我有一个“数组”,里面存储了100个同学的id,那么我则会对这个数组进行循环操作,然后挨个输出。当然还有很多其他地方需要循环操作,这里我只是举个例子。

所以,循环操作是计算机编程语言中必不可少的组成部分,那么请大家用几秒钟时间回想一下,我们之前曾经写过的循环操作for循环,while循环。我们往往需要初始化一个变量i,还得声明一个条件比如i<100,然后循环完每一步之后做什么,比如(下方伪代码):

for(i = 0; i < 100; i++) {
    
}

我们可以很容易的用这种循环来遍历一个数组,希望大家学过数据结构,因为数组在内存中的存储是连续的!我们可以通过数组的“下标”(其实是相对于数组第一个元素的位置)来进行访问数组中的元素,所以在很多时候,我们通过for循环来遍历数组(下方伪代码):

for(i = 0; i < arrLength; i++) {
    
}

那么如果我现在问你,你怎么进行遍历一个没有在内存中连续存储的“数据结构”呢,比如python中的“字典”,javascript中的”对象“,又比如你自己写了一个”树“结构的类,想遍历整个树的节点?那么传统的for循环,while循环就无法发挥他们的作用了,这个时候我们就应该引入”迭代器“了。

所以,”迭代器“其实目的也是为了”循环“,更严谨一些,是为了“遍历”,你可以把迭代器看成比普通循环更高级别的工具,普通循环能搞定的迭代器也能搞定,普通循环搞不定的迭代器还能搞定,并且使用迭代器比普通循环效率更高,这个我们后面说到生成器的时候会提到。

迭代(iteration)/可迭代(iterable)/迭代器(iterator)


我想大多数人可能和我一样,刚开始对这些概念/名词都很模糊,那么让我们一起弄明白他们。

大家先要知道“协议”(protocol)的意思,其实协议是用来“规范/标准化”你“创造的东西”的。比如,你开天辟地的发明了一种东西叫做“吧啦哔哩”,你给小明说:“小明,给我发一个吧啦哔哩过来”,如果小明不知道啥叫“吧啦哔哩”,那么小明会直接懵逼的。这时候你就要定一个“协议”如下:

  1. "吧啦哔哩"一共有10个字
  2. "吧啦哔哩"开头和结尾都是"#"号 (占两个字)
  3. "吧啦哔哩"最后四位是"blbl"
  4. 其他随便

那么我们根据这个协议,可以很轻易的构造出“吧啦哔哩”来:#1234blbl# 或者 #8888blbl#

同样,我们根据这份协议,就可以用来检测你得到的是不是“吧啦哔哩”,#1234blbl# -> 是,#1234blbl!-> 不是

迭代(iteration)

明白了上面的东西,下面我们就开始“迭代”之旅,迭代顾名思义,就是重复的的既定的任务,直到完成。所以,为了完成迭代,我们需要一个迭代器!那么什么是迭代器呢?来看看迭代器的协议吧

迭代器协议 iterator protocol

从前有个人发明了迭代器,为了让大家明白什么是迭代器,他就写了这个协议,那么协议的内容简而言之就是一句话:如果一个对象包括一个叫"next"(python3 为__next__)的方法,那么这个对象就叫做“迭代器”。

好了,那么我们根据这个协议可以创建一个迭代器(iterator)

class Counter:
    def __init__(self):
        self.index = 0

    def __next__(self):
        i = self.index
        if i < 10:
            self.index += 1
            return i

这个Counter就是一个迭代器,但是目前它没有什么太大的作用,因为我们不可能每次通过手动调用__next__方法来进行操作。

好消息是,很多编程软件为我们提供了一个“语法糖”(syntactic sugar),让这个语法糖来替我们反复执行__next__方法,比如python中的"for.. in",但是,为了让这个反复执行的过程停下来,我们同样需要定义一个终止信号,在python中,终止信号就是抛出一个StopIteration的“例外”(exception),来告知我们的语法糖:”好啦,没东西可以迭代了,可以停了“,这样迭代就终止了。

所以我们再进一步规范一下我们创建的迭代器成如下形式:

class Counter:
    def __init__(self):
        self.index = 0

    def __next__(self):
        i = self.index
        if i < 10:
            self.index += 1
            return i
        else:
            raise StopIteration

好了,我们来试一下:

counter = Counter()

for i in counter:
    print(i)

不妙,报错了。。

TypeError: 'Counter' object is not iterable

错误显示说:这个Counter对象不是可迭代的!这是什么意思呢?

原来,为了使用这个for..in 迭代语法糖,我们需要在in后面放可以迭代的“迭代器”,什么是可以迭代?你可以认为就是可以使用for..in语法糖,让语法糖帮你重复调用next方法就好了。如果不可以迭代,那么for..in这个语法糖就无法为我们自动调用next方法。

所以说,为了使用for..in语法糖来进行迭代我们的迭代器,你必须让你的迭代器可迭代(有点绕。。哈哈)。

这句话有两层含义:

  1. 为了使用for..in语法糖,你必须让你的迭代器可迭代
  2. 你如果不适用for..in语法糖,你就不必让你的迭代器可迭代,你可以自己写一个语法糖,不断地调用next方法,当遇到StopIteration例外的时候停止罢了。

好了,我们现在明白了,通常来讲,当我们要创建了一个迭代器时,我们还“必须”(注意是必须)让迭代器可迭代,这样理解:因为一个不可迭代的迭代器是没有意义的!

所以,注意!从现在开始到文章结束,我所说的“迭代器”都是“可迭代”的迭代器!

那么怎么让我的迭代器可迭代呢?同样,来看什么是“可迭代协议”(iterable protocol)

可迭代协议 iterable protocol

在python中,为了使一个”对象“可迭代:

  1. 这个迭代器必须同时包含另一个方法叫做“__iter__”
  2. 这个"__iter__"方法还得返回一个”迭代器“(可迭代)

请注意,上面我说的是:为了使一个”对象“可迭代,这里,对象可以指我们刚刚创建的”Counter“迭代器,也可以是其他的对象。

来个栗子:
为了使我们刚才创建的Counter迭代器对象“可迭代”,那么:

  1. 我们就在这个Counter对象里面添加一个叫__iter__的方法 (可迭代化操作)
  2. 让这个__iter__方法返回一个“可迭代的迭代器” (这里就是自己了!)
class Counter:
    def __init__(self):
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        i = self.index
        if i < 10:
            self.index += 1
            return i
        else:
            raise StopIteration

counter = Counter()
for i in counter:
    print(i)

Cool! 这个时候我们得到了0,1,2,3,4,5,6,7,8,9的迭代!

这里简单说一些执行步骤,当我们使用for..in语法糖的时候,它先调用__iter__方法,得到返回的迭代器,然后连续调用该迭代器的__next__方法,知道遇到StopIteration例外。

我上面也提到了,我们不仅可以使迭代器“可迭代”,我们也可以使普通的对象“可迭代”,只需给该对象添加一个__iter__的方法,然后返回一个可迭代的迭代器就好了!

这里顺便插一句!在python中,我们可以使用"iter"这个函数来返回一个“可迭代的迭代器”。

比如:

x = iter([1, 2, 3])
print(x) #<list_iterator object at 0x10c828550>
x.__next__() # 返回 1
x.__next__() # 返回 2
x.__next__() # 返回 3
x.__next__() # 返回 StopIteration

所以,我们可以让一个普通对象可迭代,而不一定非得是迭代器。

class Name:
    def __iter__(self):
        return iter(['zhangsan', 'lisi', 'wangwu'])

name = Name()
for n in name:
    print(n)

不错!我们得到了zhangsan, lisi, wangwu

现在逻辑不是很复杂的情况之下,这种创建迭代器的方式还是能够接受的,但是如果逻辑复杂,以及用这种模式多了,每次这么定义就不是很方便,于是为了“简化”创建迭代器的过程,“生成器”generator就出现了。

生成器generator


生成器的出现,就是为了简化创建迭代器的繁杂,同时又要保证逻辑的清晰,说到底生成器就是为了更方便我们使用迭代器而生的,生成器的特性如下:

  1. 生成器的样子就是一个普通的函数,只不过return关键词被yield取代了
  2. 当调用这个“函数”的时候,它会立即返回一个迭代器,而不立即执行函数内容,直到调用其返回迭代器的next方法是才开始执行,直到遇到yield语句暂停。
  3. 继续调用生成器返回的迭代器的next方法,恢复函数执行,直到再次遇到yield语句
  4. 如此反复,一直到遇到StopIteration

看如下例子:

def gFun():
    print('before hello')
    yield 'hello'
    print('after hello')

a = gFun() # 调用生成器函数,返回一个迭代器并赋给a

print(a) # <generator object gFun at 0x104cd2a40> 得到一个生成器对象(迭代器)
print(a.__next__())
# before hello
# hello
print(a.__next__())
# after hello
# StopIteration

同时因为调用生成器函数返回的是一个迭代器,所以我们可以使用for..in语法糖对其进行迭代操作:

a = gFun()
for x in a:
    print(x)

迭代返回了before hello, hello, after hello

使用迭代器/生成器的好处


首先快速看一段代码:

def firstn(n):
    num, nums = 0, []
    while num < n:
        nums.append(num)
        num += 1
        return nums

sum_of_first_n = sum(firstn(1000000))

这段代码定一个了一个函数firstn,该函数接受一个参数n,返回n之前所有的整数,最后对这些整数进行求和。

这个代码使用了我们传统的while循环,如果接受的参数n比较小还好,但是当接受的参数很大时,对内存的消耗就凸显出来了,因为在执行该函数的过程中,nums这个大的列表会全部存在于内存中。并且求和运算只有当nums列表完全构建完成之后才可以进行运算,效率也高。

而用迭代器(生成器)的方法则会大大提高效率,一方面每次next循环都会yield出一个值,供sum函数累加使用,这样就不用占用很大的内存,另一方面,使用迭代器/生成器也不用完全等到前n个数全部遍历完再进行累加,效率更高!

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

推荐阅读更多精彩内容