博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
python迭代器与生成器小结
阅读量:7044 次
发布时间:2019-06-28

本文共 7020 字,大约阅读时间需要 23 分钟。

2016.3.10关于例子解释的补充更新

源自

例子

老规矩,先上一个代码:

def add(s, x):    return s + xdef gen():    for  i in range(4):        yield ibase = gen()for n in [1, 10]:    base = (add(i, n) for i in base)print list(base)

这个东西输出可以脑补一下, 结果是[20,21,22,23], 而不是[10, 11, 12, 13]。 当时纠结了半天,一直没搞懂,后来稍微指点了一下, 突然想明白了--真够笨的,唉。。好了--正好趁机会稍微小结一下python里面的生成器。

迭代器(iterator)

要说生成器,必须首先说迭代器

区分iterable,iterator与itertion

讲到迭代器,就需要区别几个概念:iterable,iterator,itertion, 看着都差不多,其实不然。下面区分一下。

  • itertion: 就是迭代,一个接一个(one after another),是一个通用的概念,比如一个循环遍历某个数组。

  • iterable: 这个是可迭代对象,属于python的名词,范围也很广,可重复迭代,满足如下其中之一的都是iterable:

    • 可以for循环: for i in iterable

    • 可以按index索引的对象,也就是定义了__getitem__方法,比如list,str;

    • 定义了__iter__方法。可以随意返回。

    • 可以调用iter(obj)的对象,并且返回一个iterator

  • iterator: 迭代器对象,也属于python的名词,只能迭代一次。需要满足如下的迭代器协议

    • 定义了__iter__方法,但是必须返回自身

    • 定义了next方法,在python3.x是__next__用来返回下一个值,并且当没有数据了,抛出StopIteration

    • 可以保持当前的状态

首先str和listiterable 但不是iterator:

In [3]: s = 'hi'In [4]: s.__getitem__Out[4]: 
In [5]: s.next # 没有next方法---------------------------------------------------------------------------AttributeError Traceback (most recent call last)
in
()----> 1 s.nextAttributeError: 'str' object has no attribute 'next'In [6]: l = [1,2] # 同理In [7]: l.__iter__Out[7]:
In [8]: l.next---------------------------------------------------------------------------AttributeError Traceback (most recent call last)
in
()----> 1 l.nextAttributeError: 'list' object has no attribute 'next'In [9]: iter(s) is s #iter() 没有返回本身Out[9]: FalseIn [10]: iter(l) is l #同理Out[10]: False

但是对于iterator则不一样如下, 另外iterable可以支持多次迭代,而iterator在多次next之后,再次调用就会抛异常,只可以迭代一次。

In [13]: si = iter(s)In [14]: siOut[14]: 
In [15]: si.__iter__ # 有__iter__Out[15]:
In [16]: si.next #拥有nextOut[16]:
In [20]: si.__iter__() is si #__iter__返回自己Out[20]: True

这样,由这几个例子可以解释清楚这几个概念的区别。

自定义iterator 与数据分离

说到这里,迭代器对象基本出来了。下面大致说一下,如何让自定义的类的对象成为迭代器对象,其实就是定义__iter__next方法:

In [1]: %pasteclass DataIter(object):    def __init__(self, *args):        self.data = list(args)        self.ind = 0    def __iter__(self): #返回自身        return self    def next(self): # 返回数据        if self.ind == len(self.data):            raise StopIteration        else:            data = self.data[self.ind]            self.ind += 1            return data## -- End pasted text --In [9]: d  = DataIter(1,2)In [10]: for x in d: # 开始迭代   ....:     print x   ....:12In [13]: d.next() # 只能迭代一次,再次使用则会抛异常---------------------------------------------------------------------------StopIteration                             Traceback (most recent call last)----> 1 d.next()
in next(self) 10 def next(self): 11 if self.ind == len(self.data):---> 12 raise StopIteration 13 else: 14 data = self.data[self.ind]

next函数中只能向前取数据,一次取一个可以看出来,不过不能重复取数据,那这个可不可以解决呢?

我们知道iterator只能迭代一次,但是iterable对象则没有这个限制,因此我们可以把iterator从数据中分离出来,分别定义一个iterableiterator如下:

class Data(object):   # 只是iterable:可迭代对象而不iterator:迭代器    def __init__(self, *args):        self.data = list(args)    def __iter__(self):  # 并没有返回自身        return DataIterator(self)class DataIterator(object):  # iterator: 迭代器    def __init__(self, data):        self.data = data.data        self.ind = 0    def __iter__(self):        return self    def next(self):        if self.ind == len(self.data):            raise StopIteration        else:            data = self.data[self.ind]            self.ind += 1            return dataif __name__ == '__main__':    d = Data(1, 2, 3)    for x in d:        print x,    for x in d:        print x,

输出就是:

1,2,31,2,3

可以看出来数据可以复用,因为每次都返回一个DataIterator,但是数据却可以这样使用,这种实现方式很常见,比如xrange的实现便是这种数据与迭代分离的形式,但是很节省内存,如下:

In [8]: sys.getsizeof(range(1000000))Out[8]: 8000072In [9]: sys.getsizeof(xrange(1000000))Out[9]: 40

另外有个小tips, 就是为什么可以使用for 迭代迭代器对象,原因就是for替我们做了next的活,以及接收StopIteration的处理。

迭代器大概就记录到这里了,下面开始一个特殊的更加优雅的迭代器: 生成器

生成器(generator)

首先需要明确的就是生成器也是iterator迭代器,因为它遵循了迭代器协议.

两种创建方式

包含yield的函数

生成器函数跟普通函数只有一点不一样,就是把 return 换成yield,其中yield是一个语法糖,内部实现了迭代器协议,同时保持状态可以挂起。如下:

记住一点,yield是数据的生产者,而诸如for等是数据的消费者。

def gen():    print 'begin: generator'    i = 0    while True:        print 'before return ', i        yield i        i += 1        print 'after return ', ia  = gen()In [10]: a #只是返回一个对象Out[10]: 
In [11]: a.next() #开始执行begin: generatorbefore return 0Out[11]: 0In [12]: a.next()after return 1before return 1Out[12]: 1

首先看到while True 不必惊慌,它只会一个一个的执行~

看结果可以看出一点东西:

  • 调用gen()并没有真实执行函数,而是只是返回了一个生成器对象

  • 执行第一次a.next()时,才真正执行函数,执行到yield一个返回值,然后就会挂起,保持当前的名字空间等状态。然后等待下一次的调用,从yield的下一行继续执行。

还有一种情况也会执行生成器函数,就是当检索生成器的元素时,如list(generator), 说白了就是当需要数据的时候,才会执行。

In [15]: def func():   ....:     print 'begin'   ....:     for i in range(4):   ....:         yield iIn [16]: a = func()In [17]: list(a) #检索数据,开始执行beginOut[17]: [0, 1, 2, 3]

yield还有其他高级应用,后面再慢慢学习。

生成器表达式

列表生成器十分方便:如下,求10以内的奇数:

[i for i in range(10) if i % 2]

同样在python 2.4也引入了生成器表达式,而且形式非常类似,就是把[]换成了().

In [18]: a = ( i for i in range(4))In [19]: aOut[19]: 
at 0x7f40c2cfe410>In [20]: a.next()Out[20]: 0

可以看出生成器表达式创建了一个生成器,而且生有个特点就是惰性计算, 只有在被检索时候,才会被赋值。

之前有篇文章:,最后有一个例子:

def multipliers():    return (lambda x : i * x for i in range(4))  #修改成生成器print [m(2) for m in multipliers()]

这个就是说,只有在执行m(2)的时候,生成器表达式里面的for才会开始从0循环,然后接着才是i * x,因此不存在那篇文章中的问题.

惰性计算这个特点很有用,上述就是一个应用,2gua这样说的:

惰性计算想像成水龙头,需要的时候打开,接完水了关掉,这时候数据流就暂停了,再需要的时候再打开水龙头,这时候数据仍是接着输出,不需要从头开始循环

个人理解就是就是可以利用生成器来作为数据管道使用,当被检索的时候,每次拿出一个数据,然后向下面传递,传到最后,再拿第二个数据,在下面的例子中会详细说明。

其实本质跟迭代器差不多,不一次性把数据都那过来,需要的时候,才拿。

回到例子

看到这里,开始的例子应该大概可以有点清晰了,

核心语句就是:

def gen():    for i in range(4):        yield ifor n in [1, 10]:    base = (add(i, n) for i in base)

之前的解释有点瑕疵,容易误导对生成器的理解:

在执行list(base)的时候,开始检索,然后生成器开始运算了。关键是,这个循环次数是2,也就是说,有两次生成器表达式的过程。必须牢牢把握住这一点。生成器返回去开始运算,n = 10而不是1没问题吧,这个在上面提到的文章中已经提到了,就是add(i+n)绑定的是n这个变量,而不是它当时的数值。然后首先是第一次生成器表达式的执行过程:base = (10 + 0, 10 + 1, 10 + 2, 10 +3),这是第一次循环的结果(形象表示,其实已经计算出来了(10,11,12,3)),然后第二次,base = (10 + 10, 11 + 10, 12 + 10, 13 + 10) ,终于得到结果了[20, 21, 22, 23].

新思路

这个可以以管道的思路来理解,首先gen()函数是第一个生成器,下一个是第一次循环的base = (add(i, n) for i in base),最后一个生成器是第二次循环的base = (add(i, n) for i in base)
这样就相当于三个管道依次连接,但是水(数据)还没有流过,现在到了list(base),就相当于驱动器,打开了水的开关,这时候,按照管道的顺序,由第一个产生一个数据,yield 0,然后第一个管道关闭。
之后传递给第二个管道就是第一次循环,此时执行了add(0, 10),然后水继续流,到第二次循环,再执行add(10, 10),此时到管道尾巴了,此时产生了第一个数据20,然后第一个管道再开放:yield 1, 流程跟上面的一样,依次产生21,22,23; 直到没有数据。
把代码改一下容易理解:

def gen():    for i in range(4):        yield i  #  第一个管道base = (add(i, 10) for i in base) #  第二个管道base = (add(i, 10) for i in base) #  第三个管道list(base) #  开关驱动器

具体执行过程可以在上:

之前的解释被误导的原因是,可能会误以为是在第二个管道就把gen()执行完毕了,其实不是这样的。
这种写法的好处显而易见:内存占用低。在数据量极大的时候,用list就只能爆内存,而用生成器模式则完全不用担心

小结

概括

主要介绍了大概这样几点:

  • iterable,iteratoritertion的概念

  • 迭代器协议

    • 自定义可迭代对象与迭代器分离,保证数据复用

  • 生成器: 特殊的迭代器,内部实现了迭代器协议

其实这一块, 那几个概念搞清楚, ,这个很关键, 搞懂了后面就水到渠成了。而且对之前的知识也有很多加深。

比如常见list就是iteratoriteable分离实现的,本身是可迭代对象,但不是迭代器, 类似与xrange,但是又不同。
越来越明白,看源码的重要性了。

参考

转载地址:http://olhal.baihongyu.com/

你可能感兴趣的文章
Springboot 之 引入Thymeleaf
查看>>
webpack学习笔记2 起步
查看>>
注解全解析
查看>>
map函数的使用技巧
查看>>
Laravel5.2 自定义类引入和命名空间问题
查看>>
express使用req对象获取HTTP请求的参数
查看>>
React小技巧: 使用Context跨组件树传递数据
查看>>
Objective-C设计模式解析-迭代器
查看>>
keepalived搭建zabbix server双机高可用
查看>>
怎样使用和设置.babelrc
查看>>
我的世界:一个村落(其二)
查看>>
Longest Increasing Path in a Matrix
查看>>
Node项目之需求收集平台(一)- 基本介绍
查看>>
ArchSummit北京2015 | “新人”的技术约战
查看>>
Microsoft宣布正式发布Linux on ASE
查看>>
Elm提供的语言级响应性
查看>>
微服务通信策略
查看>>
InfoQ 趋势报告:技术文化\u0026方法2019年实践状况
查看>>
Entity Framework Core 2.0的槽点
查看>>
甲骨文解散Java Mission Control团队事件新进展
查看>>