【python】迭代器&生成器

源Link:http://www.cnblogs.com/huxi/archive/2011/07/01/2095931.html

迭代器

迭代器是访问集合元素的一种方式。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退,不过这也没什么, 因为人们很少在迭代途中往后退。另外,迭代器的一大优点是不要求事先准备好整个迭代过程中所有的元素。迭代器仅仅在迭代到某个元素时才计算该元素,而在这之前或之后,元素可以不存在或者被销毁。这个特点使得它特别适合用于遍历一些巨大的或是无限的集合,比如几个G的文件

特点:

  1. 访问者不需要关心迭代器内部的结构,仅需通过next()方法不断去取下一个内容
  2. 不能随机访问集合中的某个值 ,只能从头到尾依次访问
  3. 访问到一半时不能往回退
  4. 便于循环比较大的数据集合,节省内存
class listiterator(object)
 |  Methods defined here:
 |  
 |  __getattribute__(...)
 |      x.__getattribute__('name') <==> x.name
 |  
 |  __iter__(...)
 |      x.__iter__() <==> iter(x)
 |  
 |  __length_hint__(...)
 |      Private method returning an estimate of len(list(it)).
 |  
 |  next(...)
 |      x.next() -> the next value, or raise StopIteration

listiterator
View Code

生成一个迭代器:

#在list中有个__iter__方法,就是个迭代器,其类里必须有个next方法。需要通过遍历来具体输出

others = iter(['liupeng','tony','jack','rain']) #iter代表生成的是个迭代器。

print(others)

#Result(下面结果为一个迭代器(list_iteratior))

<list_iterator object at 0x7efd6f8cbcc0>

#others[2]  # 假如我们想取'jack'这个值,如果是列表或者元组的话,通过下标就可以读取元素。但是迭代器不可以只能通过__next__()持续迭代下去到这个值才能被取出。直接用下标取值会出现一个“TypeError: 'list_iterator' object is not subscriptable”的报错。

print(others.__next__())          #迭代器一次只能迭代一个值,而且是从头开始迭代。不会重复迭代。要想读取到我们想找到的'jack',只能在第三次迭代中取出。
print(others.__next__())
print(others.__next__())
print(others.__next__())
print(others.__next__())          # 上述我们自己创建的迭代器中一共只有4个元素,如果超过迭代器元素的数量继续迭代的话,它会自动返回“StopIteration”的信息来告诉你已经到终点了。

#Result
liupeng
tony
jack
rain
Traceback (most recent call last):
  File "/home/liupeng/PycharmProjects/untitled/zhengzebiaoda.py", line 80, in <module>
    print(others.__next__())      # 上述我们自己创建的迭代器中一共只有4个元素,如果超过迭代器元素的数量继续迭代的话,它会自动返回“StopIteration”的信息来告诉你已经到终点了。
StopIteration
 
obj = iter([11,22,33,44,55,66,77,88,99,90])      #

for n in obj:
    print(n)

#Result   (利用for 循环取出iter中每个元素。)
11
22
33
44
55
66
77
88
99
90

#下面这种情况其实是抛出Stoplteration异常。事实上,Python正是根据是否检查到这个异常来决定是否停止迭代的。 
#这种做法与迭代前手动检查是否越界相比各有优点。但Python的做法总有一些利用异常进行流程控制的嫌疑。 
#了解了这些情况以后,我们就能使用迭代器进行遍历了。 

obj = iter([11,22,33,44,55,66,77,88,99,90])

try:
    while True:
        val = obj.__next__()
        print (val)

except StopIteration:
    pass

#另外在使用迭代器的循环可以避开索引,但有时候我们还是需要索引来进行一些操作的。这时候内建函数enumerate就派上用场咯,它能在iter函数的结果前加上索引,以元组返回,用起来就像这样:

obj =iter(['liupeng','tony','jack','rain'])

for i in enumerate(obj):
    print(i)

#Result
(0, 'liupeng')
(1, 'tony')
(2, 'jack')
(3, 'rain')
实例1:
obj = iter([11,22,33,44,55,66,77,88,99,90])

while True:
    val = obj.__next__()
    print(val)

#Result   (while循环的话判断为真,赋一个变量。然后把每次通过obj.__next__()的结果赋值给变量打印出来。当循环超过iter中的值后条件就为Talse,那么就会报StopIteration的错误。意思就是已经没值了到底了)
22
44
66
88
90
Traceback (most recent call last):
  File "/home/liupeng/PycharmProjects/untitled/zhengzebiaoda.py", line 77, in <module>
    val = obj.__next__()
StopIteration
实例2:

使用迭代器一个显而易见的好处就是:每次只从对象中读取一条数据,不会造成内存的过大开销。

例如:
/* 把文件一次加载到内存中,然后逐行打印。当文件很大时,这个方法的内存开销就很大了 */
for line in open("test.txt").readlines():
    print line

/* 这是最简单也是运行速度最快的写法,他并没显式的读取文件,而是利用迭代器每次读取下一行 */
for line in open("test.txt"):   #use file iterators
    print line

 生成器

定义:一个函数调用时返回一个迭代器,那这个函数就叫做生成器(generator),如果函数中包含yield语法,那这个函数就会变成生成器

  • 生成器是一个特殊的程序,可以被用作控制循环的迭代行为
  • 生成器类似于返回值为数组的一个函数,这个函数可以接收参数,可以被调用,但是,不同于一般的函数会一次性返回包含了所有数值的数组,生成器一次 只产生一个值,这样消耗的内粗数量大大减少,而且允许调用函数可以很快的开始处理前几个返回值。因此,生成器看起来像一个函数但是表现的却像一个迭代器。
python中的生成器
python提供了两种基本的方式。
  • 生成器函数:也是用def来定义,利用关键字yield一次返回一个结果,阻塞,重新开始
  • 生成器表达式:返回一个对象,这个对象只有在需要的时候才产生结果

下面详细讲解。

生成器函数

为什么叫生成器函数?因为他随着时间的推移生成了一个数值队列。一般的函数在执行完毕之后会返回一个值然后退出,但是生成器函数会自动挂起,然后重新拾起继续执行,他会利用yield关键字关起函数,给调用者返回一个值,同时保留了当前的足够多的状态,可以使函数继续执行。生成器和迭代协议是密切相关的,可迭代的对象都有一个__next()__成员方法,这个方法要么返回迭代的下一项,要么引起异常结束迭代。
为了支持迭代协议,拥有yield语句的函数被编译为生成器,这类函数被调用时返回一个生成器对象,返回的对象支持迭代接口,即成员方法__next()__继续从中断处执行执行。
看下面的例子:

def creat_counter(n):
    print('create counter')

    while True:
        yield n
        print('increment n')
        n += 1

cnt = creat_counter(2)
print(cnt)

print(next(cnt))
print(next(cnt))
print(next(cnt))

 分析一下这个例子:

  • 在create_counter函数中出现了关键字yield,预示着这个函数每次只产生一个结果值,这个函数返回一个生成器(通过第一行输出可以看出来),用来产生连续的n值
  • 在创造生成器实例的时候,只需要像普通函数一样调用就可以,但是这个调用却不会执行这个函数,这个可以通过输出看出来
  • next()函数将生成器对象作为自己的参数,在第一次调用的时候,他执行了create_counter()函数到yield语句,返回产生的值2
  • 我们重复的调用next()函数,每次他都会从上次被挂起的地方开始执行,直到再次遇到了yield关键字

为了更加深刻的理解,我们再举一个例子。

#coding
def cube(n):
    for i in range(n):
        yield i ** 3

for i in cube(5):
    print i

#output
1
27

所以从理解函数的角度出发我们可以将yield类比为return,但是功能确实完全不同,在for循环中,会自动遵循迭代规则,每次调用next()函数,所以上面的结果不难理解。

生成器表达式:

生成器表达式来自于迭代和列表解析的组合,生成器表达式和列表解析类似,但是他使用尖括号而不是方括号括起来的。如下代码:

>>> # 列表解析生成列表
>>> [ x ** 3 for x in range(5)]
[0, 1, 8, 27, 64]
>>> 
>>> # 生成器表达式
>>> (x ** 3 for x in range(5))
<generator object <genexpr> at 0x000000000315F678>
>>> # 两者之间转换
>>> list(x ** 3 for x in range(5))
[0, 1, 8, 27, 64]

 就操作而言,生成器表如果使用大量的next()函数会显得十分不方便,for循环会自动出发next函数,所以可以按下面方式使用:

>>> for n in (x ** 3 for x in range(5)):
    print('%s, %s' % (n, n * n))

    
0, 0
1, 1
8, 64
27, 729
64, 4096

两者比较

一个迭代既可以被写成生成器函数,也可以被协程生成器表达式,均支持自动和手动迭代。而且这些生成器只支持一个active迭代,也就是说生成器的迭代器就是生成器本身。

协程与yield表达式

yield语句还有更给力的功能,作为一个语句出现在赋值运算符的右边,接受一个值,或同时生成一个值并接受一个值。

def recv():
    print ('Are your ready:')
    while True:
        n=yield
        print ('总共用了 %s 秒'%n)

c = recv()
c.__next__()
c.send(100)
c.send(300)

以这种方式使用yield语句的函数称为协程。在这个例子中,对于__next__的初始调用是必不可少的,这样协程才能执行可通向第一个yield表 达式的语句。在这里协程会挂起,等待相关生成器对象send()方法给它发送一个值。传递给send()的值由协程中的yield表达式返回。

协程的运行一般是无限期的,使用方法close()可以显式的关闭它。

如果yield表达式中提供了值,协程可以使用yield语句同时接收和发出返回值。

def split_line():
    print('ready to split')
    result = None

    while True:
        line = yield result
        result = line.split()


s = split_line()
s.__next__()
print(s.send('1,2,3'))

 注意:理解这个例子中的先后顺序非常重要。首个next()方法让协程执行到yield result,这将返回result的值None。在接下来的send()调用中,接收到的值被放到line中并拆分到result中。send()方法 的返回值就是下一条yield语句的值。也就是说,send()方法可以将一个值传递给yield表达式,但是其返回值来自下一个yield表达式,而不 是接收send()传递的值的yield表达式。

如果你想用send()方法来开启协程的执行,必须先send一个None值,因为这时候是没有yield语句来接受值的,否则就会抛出异常。

>>> s=split_line()
>>> s.send('1 2 3')
TypeError: can't send non-None value to a just-started generator
>>> s=split_line()
>>> s.send(None)
ready to split

使用生成器与协程

乍看之下,如何使用生成器和协程解决实际问题似乎并不明显。但在解决系统、网络和分布式计算方面的某些问题时,生成器和协程特别有用。实际上,yield已经成为Python最强大的关键字之一。

比如,要建立一个处理文件的管道:

import os,sys
def default_next(func):
    def start(*args,**kwargs):
        f=func(*args,**kwargs)
        f.__next__()
        return f
    return start
@default_next
def find_files(target):
    topdir=yield
    while True:
        for path,dirname,filelist in os.walk(topdir):
            for filename in filelist:
                target.send(os.path.join(path,filename))
 
@default_next
def opener(target):
    while True:
        name=yield
        f=open(name)
        target.send(f)
     
@default_next
def catch(target):
    while True:
        f=yield
        for line in f:
            target.send(line)
             
@default_next
def printer():
    while True:
        line=yield
        print line

 然后将这些协程连接起来,就可以创建一个数据流处理管道了:

finder=find_files(opener(catch(printer())))
finder.send(toppath)

 程序的执行完全由将数据发送到第一个协程find_files()中来驱动,协程管道会永远保持活动状态,直到它显式的调用close()。

总之,生成器的功能非常强大。协程可以用于实现某种形式的并发。在某些类型的应用程序中,可以用一个任务调度器和一些生成器或协程实现协作式用户空 间多线程,即greenlet。yield的威力将在协程,协同式多任务处理(cooperative multitasking),以及异步IO中得到真正的体现。

有的时候真的是“眼观千遍,不如手动一遍”来的记忆犹新。

此文截取多篇博客中的案例结合而制,同时以上代码都已经验证过有效。

“今天的努力都是明天别人对你的膜拜,今天的停滞就是明天别人对你的唾弃!“



原文地址:https://www.cnblogs.com/liupengpengg/p/5607601.html