python迭代器与生成器

迭代器

如果给定一个list或tuple,我们可以通过for循环来遍历这个序列,这个遍历我们称之为迭代。在python中,我们通常通过for...in语句来完成。

我们知道,有些数据类型我们可以直接作用于for循环,其中一类是集合数据类型,比如list、tuple、dict、set、str等,还有一类就是generator,包括生成器和带yield的generator function。

我们把这种可以直接作用于for循环的对象统称为可迭代对象:Iterable。

from collections import Iterable
print(isinstance([1,2,3],Iterable))   #True
print(isinstance("hello",Iterable))   #True
print(isinstance((1,2,3),Iterable))   #True
print(isinstance({'a':1,'b':2},Iterable))   #True
print(isinstance(123,Iterable))       #False
print(isinstance([1,2,3],Iterator))  #False

上面这些就是可迭代的对象了,但是我们要介绍的是迭代器。对于可迭代的对象,它们都实现了__iter__()方法或__getitem__()方法,可以利用help()命令来查看,也就是说我们自定义一个类,只要实现两个方法之一,也是可迭代的对象了。

from collections import Iterable

class NUmberList():

    def __init__(self):
        super(NUmberList,self).__init__()

    def __iter__(self):
        pass

numberlist = NUmberList()
print(isinstance(numberlist,Iterable)) #True

那么什么是迭代器?可迭代对象支持内置函数iter,通过对可迭代对象调用iter函数,会返回一个迭代器,而“迭代器”支持内置函数next,通过不断对其调用next方法,会依次前进到序列中的下一个元素并将其返回,最后到达一系列结果的末尾时,会引发StopIteration异常。补充说明一点,对迭代器调用iter方法,则会返回迭代器自身。

from collections import Iterator

print(isinstance(iter([]),Iterator))  #True
print(isinstance(iter(()),Iterator))  #True
print(isinstance(iter({}),Iterator))  #True
print(iter([])) #<list_iterator object at 0x1051920f0>
L = [2,3,4]
I = iter(L)
print(next(I))
print(next(I))
print(next(I))
print(next(I))

2
3
4
Traceback (most recent call last):
  File "/Users/chen/PycharmProjects/test/pythontest/test1.py", line 6, in <module>
    print(next(I))
StopIteration

在这个手动模拟迭代的过程中,先把可迭代对象转换成迭代器,然后用next方法进行手动迭代,迭代到最后出现StopIteration异常退出。

为什么list、dict、str等数据类型不是Iterator?

这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。

Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

总结:

1、凡是可作用于for循环的对象都是Iterable类型;

2、凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;

3、集合数据类型如list、dict、str等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。

 

下面我们介绍几个循环迭代的技巧!

<1>内置函数range:用于返回一系列连续增加的整数。

for x in range(5):
    print(x,end=',')
#0,1,2,3,4,
#内置函数range在for循环中是最常用的,
#它提供了一种简单的方法,重复特定次数的动作。

S = 'asdfghjkl'
for s in S[::2]:
    print(s,end=',')
#a,d,g,j,l,
#range函数在这里没有复制字符串,不会在python中再创建一个字符串列表,
#这对于很大的字符串来说,会节约不少空间。

<2>zip:用于并行迭代多个序列

L1 = [1,2,3,4,5]
L2 = ['a','b','c','d','e']
for t in zip(L1,L2):
    print(t,end=',')
print(zip(L1,L2))
print(list(zip(L1,L2)))

#(1, 'a'),(2, 'b'),(3, 'c'),(4, 'd'),(5, 'e'),
# #<zip object at 0x10f2b9248>
#[(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e')]

内置zip函数允许我们使用for循环来并行迭代多个序列。zip使用多个序列作为参数,然后返回元组的列表,将这些列表中的并排元素一一配对。和range一样,zip在遍历时也是依次按需产生结果,而不是一次性显示所有结果。同样的,如果想一次性显示所有结果,则必须将其包含在一个list调用中,以便一次性显示所有结果。

<3>enumerate:用来同时产生偏移和元素

S = 'spam'
for t in enumerate(S):
    print(t,end='')

print(enumerate(S))
print(iter(enumerate(S)))

#(0, 's')(1, 'p')(2, 'a')(3, 'm')
#<enumerate object at 0x1050f5ee8>
#<enumerate object at 0x1050f5ee8>

有时我们在遍历的时候,既需要偏移值,又需要对应元素,那么内置函数enumerate就可以实现这个功能。

它在for循环的条件下每轮迭代返回一个包含偏移值和偏移元素的元组:(index,value)。

enumerate方法返回的也是可迭代对象,他的迭代器就是他自身。

生成器

对于列表解析式,它的优点很多,比如运行速度快、编写简单,但是有一点我们不要忘了,它是一次性生成整个列表。如果整个列表非常大,这对内存也同样会造成很大压力,想要实现内存的节约,可以将列表解析式转换为生成器表达式。

为了实现在需要的时候逐次产生结果,而不是立即产生全部的结果,python中有两种语言结构可以实现这种思路。

一个是生成器函数。外表看上去像是一个函数,但是没有用return语句一次性的返回整个结果对象列表,取而代之的是使用yield语句一次返回一个结果。另一个是生成器表达式,将列表解析方括号换成了圆括号,他们返回按需产生的一个结果对象,而不是构建一个结果列表。

这个“按需”指的是在迭代的环境中,每次迭代按需产生一个对象,因此,上述二者都不会一次性构建整个列表,从而节约了内存空间。

L = [x**2 for x in range(5)]
print(L)
#[0, 1, 4, 9, 16]
G = (x**2 for x in range(5))
print(G)
print(next(G))
print(next(G))
print(next(G))
print(next(G))
print(next(G))
print(next(G))
#<generator object <genexpr> at 0x10677fa98>
#0
#1
#4
#9
#StopIteration

如上面的程序所示,利用生成器表达式直接返回的值是generator对象的地址,如果想要输出它的值,需要调用next方法;同样也可以利用for循环来取值,而且for循环内部有捕捉异常的机制,使用起来会更方便。

def test1(num):
    for i in range(num):
        print("------%d------"%i)
        return i
        print("------%d------"%i)

def test2(num):
    for i in range(num):
        print("------%d------"%i)
        yield i
        print("------%d------"%i)

f = test1(3)
print(f)
print(type(test1))
print("="*50)

g = test2(3)
print(g)
print(type(test2))
for i in g:
    print(i)

输出结果:
------0------
0
<class 'function'>
==================================================
<generator object test2 at 0x102f89a98>
<class 'function'>
------0------
0
------0------
------1------
1
------1------
------2------
2
------2------

比较上面代码:定义的两个函数几乎一样,唯一不同之处,一个使用了return关键字,一个使用的是yield关键字。可是执行结果却有很大不同:①使用return定义的那个函数是普通函数,使用yield定义的函数是生成器函数;②只含return的函数,没有遍历所有值,return语句后的代码没有执行,直接向父函数提交返回值;含yield的函数,取到了生成器中的所有值。

以上两者的不同,只是从代码执行结果比较出来的,那么return与yield的内部执行机制有什么不同呢?

两者的对所在线程(Thread)的控制权是不一样的。在return所在的函数中,只要执行到return语句后,return之后的语句都不会执行,因为执行完return语句后,立即交出对线程的控制权,也就是说,在同一函数中,return之后的语句都不占用线程资源。

而yield生成器函数,在执行到yield语句后,会像return一样把返回值交给调用函数,不同的是,在提交完返回值之后yield整个生成器都处于暂停状态,这时候跟return一样会交出线程的控制权,yield后面的代码也不会执行,如果想执行yield后面的语句,只能再次占有线程。如果你不让它重新占有线程(即恢复运行状态)的话,它会一直处于暂停状态,yield语句后生成器就是这么“懒”,一点都不主动。你可能会有疑问,我既没有看到它暂停的状态,也没有看到将它从暂停状态恢复到运行状态?别忘了,生成器是特殊的迭代器,迭代器可以通过for语句或者next()函数进行取值,上面的代码正是有了for语句,而不需要你主动去恢复它运行状态。

那为什么说yield生成器函数强大?有什么用呢?

yield生成器函数可以实现异步,用于协程(Coroutine)多任务。在cpython解释器下的Python由于有一个GIL(全局解释器锁)的存在,使用多线程实现的多任务其实是伪多任务,因为始终是在单核CPU上执行的,不能充分发挥多核CPU。尽管协程处于线程内,但是协程比线程占用更少的资源,Python中进程+协程的组合会比进程+线程的组合更节省资源。

def test1(n):
    for i in range(n):
        yield i ** 2
for item1 in test1(5):
    print(item1)

def test2(n):
    res = []
    for i in range(n):
        res.append(i**2)
    return res
for item2 in test2(5):
    print(item2)

上面两个函数的输出都是一样的,但是生成器函数会简单一些,而且生成器函数的可读性也比较高,当我们在编写复杂的程序的时候,这两点会更加突出。

总结

1、container对象:像list、tuple、str、dict、file等对象,在数据存储的形式就像容器一样;

2、iterable对象:Python提供了不少Iterable对象,包括所有的序列类型(如列表),一些无序列类型(如字典、文件等),同时也可以通过自定义类的方法实现可迭代的类对象。所有的iterable对象,都具有共同的一个方法__iter__()

3、iterator对象:迭代器对象,不同于container的数据存储形式,iterator只存储算法的内存地址,iterator对象与iterable对象不同之处在于,iterator同时具有__iter__()和__next__()方法。尽管像列表、元祖这样的对象不具有__next__()方法,但是可以通过iter()方法或者for语句为它们返回一个迭代器对象。

4、generator对象:生成器对象是特殊的迭代器,具有迭代器一切的特点。创建generator对象一般有两种方法,一种是generator expression生成器表达式,形式上与列表、集合解析式很像,实质却完全不同;另一种创建方式yield expressions,通过自定义generator function,能够发挥生成器更大的作用。yield expressions的灵活准确应用,需要建立在深刻理解yield关键字的内部执行机制上。

5、yield表达式的异步执行,可以用于协程,实现多任务。

https://zhuanlan.zhihu.com/p/36747627

https://zhuanlan.zhihu.com/p/32787463

https://www.zhihu.com/question/20829330

https://www.liaoxuefeng.com/

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值