thtl

导航

迭代器和生成器

迭代器

什么叫可迭代对象呢,就是遵循迭代器协议的对象叫可迭代对象,迭代器协议有哪些呢?

1:只可向后不可向前

2:必须具有__next__()方法

(以上都只是讲个大概,基本上就是这样,最重要的是第二点)

for循环是基于可迭代对象的next()方法来迭代的,也就是基于迭代器的协议来实现的,所有for循环和强大,强大到甚至连文件对象都可以迭代,可问题来了,你看下面的例子,l 这个列表可以被for迭代开始 l 这个列表并没有__next__()方法,这是怎么回事呢?这是否意味着列表不是可迭代对象呢?  答案:是的,列表元组字符串字典集合之类的全部不是可迭代对象

l = [1,2,3,4]
for each in l:
    print(each)

结果
1
2
3
4

 

 现在来讲一下原理,可迭代对象<==>迭代器对象,因为for循环非常强大,它在迭代列表元组字符串这些东西时,会调用列表元组字符串的__iter__()方法,生成一个对应的可迭代对象,就是迭代器对象,然后就可迭代啦,意思就是说只要能被for循环迭代的非可迭代对象必须有__iter__()方法,下面来测试一下

a = [1,2,3,4]
s = a.__iter__()
s.__next__()
print(s)

结果
<list_iterator object at 0x0000018442E892E8>

 

 //next()函数,这个系统内置的函数其实在调用时是调用了迭代器对象的__next__()方法

 生成器

生成器是自动变成迭代器对象的,不需要认为的再调用__iter__()方法,有两种方式写生成器,1:生成器函数,2:生成器表达式

先说生成器函数,只要将我们平时写函数时的 return 改成 yield,有个小地方不一样的是,函数的return只可以一次,但是yield可以多次,每一次的yield相当于生成器函数的一个状态,而且生成器函数没有省内存这么一说,因为和普通函数比起来一样,都是函数所以都先存在内存之中,生成器表达式才有省内存这么一说,下面会详细讲,当然生成器函数也是有优点的,对于调用普通函数,是先将函数全部执行加载完之后再执行下面主程序的操作,但对于生成器函数而言,可以一边执行加载到内存一边用你目前加载到内存的数据,这样效率就高一点

def func():
    yield 1
    yield 2
    yield 3
g = func()
print(g)
print(g.__next__())
print(g.__next__())
print(g.__next__())

结果
<generator object func at 0x0000025CEBB7CFC0>
1
2
3

def func():
    print('one time')
    yield 1
    print('two times')
    yield 2
    print('three times')
    yield 3
    print('that is all')
g = func()//生成器函数没有执行,只是返回一个生成器先,并且状态为最开始,生成器函数真正的执行是需要 __next__()方法的
print(g.__next__())//回到生成器的函数中从记录的状态开始执行,到第一个状态然后暂时挂起函数并记录下状态(也就是记录到执行完了yield 1 这条语句  yield相当于状态语句)
print(g.__next__())//重新回到生成器函数中,从记录的状态开始执行也就是第一个状态之后(yield 1 这条语句之后),执行到第二个状态并记录下来挂起函数
print(g.__next__())//重新回到生成器函数中,从记录的状态开始执行也就是第二个状态之后(yield 2 这条语句之后),执行到第三个状态并记录下来挂起函数
//由于后面没有再继续调用生成器函数,所以 print('that is all') 这条语句没有被执行
结果
one time
1
two times
2
three times
3

母鸡生蛋传说
def func():
    ret = []
    for each in range(1000):
        ret.append('鸡蛋%s' % str(each+1))
    return ret
ret = func()
people1=ret[0]
print(people1)
people2=ret[1]
print(people2)

结果
鸡蛋1
鸡蛋2
//这个方法是怎么样呢,是先让母鸡将1000给蛋全部生完,然后拿个篮子装起来,然后再将篮子里的鸡蛋分给后面1000给正在等着领鸡蛋的人,那这样效率是不是低

换种方法,能不能母鸡生一个就分一个鸡蛋给正在等待的人,那是不是人排队领鸡蛋所需的时间就短了,  假设生一个鸡蛋要1s领一个鸡蛋要1s 对于第1000个人来说前两种方法都一样,都要
等2000s  但对前面的999个人来说,第一个人对于第一种方法需要1001s才领完,对于第二种方法只需要2s,所以对于前999个人来说速度都变慢了,对于第1000个人来说速度没变,那这样
用第二种方法是不是效率就高了,而且还不需要篮子将1000个鸡蛋装起来,生一个给一个,根本不需要装起来(对于内存来说第一种方法就占内存,因为需要将数据全部存起来先,对于第二种
方法就不占内存,因为是生成器函数,调用一次__next__()方法就生成一个数据,调用第二次时生成第二个数据,第一次的数据已经处理完不需要存起来,一开始这个生成器函数是一个数据
都没有生成的,是当你调用生成器的__next__()方法时才开始生成数据,而且是调用一次生成一个),这样的话效率就会高一点
def func():
    for each in range(1000):
        yield '鸡蛋%s' % str(each+1)
    return ret
ret = func()
people1=ret.__next__()
print(people1)
people2=ret.__next__()
print(people2)

结果
鸡蛋1
鸡蛋2

 

 //三元表达式,看例子自己回忆

name = 'djh'
a = '帅哥' if name == 'djh' else 'sb'
print(a)

结果
帅哥

 

 //列表的解析式

s = []
for each in range(10):
    s.append("帅哥%s" % str(each+1))
print(s)

结果
['帅哥1', '帅哥2', '帅哥3', '帅哥4', '帅哥5', '帅哥6', '帅哥7', '帅哥8', '帅哥9', '帅哥10']

列表解析式可以很简单的达到目的

s = ['帅哥%s' % str(each+1) for each in range(10)]
print(s)

结果
['帅哥1', '帅哥2', '帅哥3', '帅哥4', '帅哥5', '帅哥6', '帅哥7', '帅哥8', '帅哥9', '帅哥10']

还可以这么玩
s = ['帅哥%s' % str(each+1) for each in range(10) if each >5]
print(s)

结果
['帅哥7', '帅哥8', '帅哥9', '帅哥10']

 

现在说一下生成器表达式,自己感受啦,其实就是列表解析式的格式,只不过因为返回的是有个生成器不是一个列表所将以 [] 变成了 () 而已

s = ('帅哥%s' % str(each+1) for each in range(3))
print(s)
print(s.__next__())
print(s.__next__())
print(s.__next__())

结果
<generator object <genexpr> at 0x000002339848CFC0>
帅哥1
帅哥2
帅哥3

 

需要强调的一个地方是,用生成器省内存,以生成器表达式为例子,生成器表达式都是在调用到某个值时才生成这个值,没调用到时是不生成的,并且在使用结束之后由于没有变量指定该值,会被python的回收机制给回收,所以根本不占内存,但是如果是用列表的形式的话,它会先在内存中将该列表的所以元素给存起来先,然后调用时直接调用,这样就很占内存,下面举例说明

print(sum(list(range(1000000000000000000))))
sum()函数的工作原理也给予迭代器协议工作的,第一步先在内存里生成 list(range(1000000000000000000)) 这么大的一个列表,将其每个元素全部存在内存中先,这样就占用了十分
巨大的内存,甚至可能机器卡死,第二步再生成 list(range(1000000000000000000)) 的迭代器对象,当然这步不占内存,但如果换个写法
print(sum(each for each in range(10000000000000000)))  你看,传入的是一个生成器,过程是这样的,sum()函数是把全部元素加起来嘛,当它要加第一个元素时,会去找这个
生成器,也就是这个each for each in range(10000000000000000))表达式,生成第一个数(我猜测可能还会有个负责记录的变量,记录到目前为止已经生成到哪个了),当加第二个
数时它又会去找回这个each for each in range(10000000000000000))表达式生成第二个数,第一个数用完了并且没有变量去指向所以会被回收,后面的以此类推,那这个过程就不像
上面的那个方法先在内存里把全部元素存起来那么费内存

 

 //send()方法,有一个参数,必填,是专属于生成器的方法,迭代器没有这个方法,send()方法其实主要是用在生成器函数里的,它是调用生成器函数让其继续往下走,并且将其参数的值返回给生成器函数里的yield。

def func():
    print("the fist")
    fist = yield 1
    print(2,fist)
    second = yield
a = func()
res = a.__next__()//之所以一开始不用send()方法是因为一开始状态并没有停在yield语句那里,所以send()的参数不知道给谁接收所以一开始都是先用__next__()方法
print(res)
res = a.send(None)//将send()的参数返回给当前状态停靠的那个yield(也就是yield 1),然后将当前的yield赋值给fist
print(res)

结果
the fist
1
2 None
None

 

下面给个单线程模拟实现并发的程序,就是两个独立的程序之间不需要人为的显示的调用就可以跳来跳去。

举例吃包子,做包子和吃包子应该是并行的两个程序对吧,你见过哪家店是把包子全部做完然后再卖给食客吃的,全都是一边做一边卖

import time
def consumer(name):
    print("%s已经进入餐馆准备放开肚皮吃包子了" % name)
    while True:
        baozi = yield None
        time.sleep(0.5)
        print("%s已经吃了包子%s" %(name,baozi))

def product_baozi():
    c1 = consumer('djh')
    c2 = consumer('DJH')
    c1.__next__()
    c2.__next__()
    for each in range(10):
        time.sleep(0.5)
        c1.send(each)
        c2.send(each)

product_baozi()

结果
djh已经进入餐馆准备放开肚皮吃包子了
DJH已经进入餐馆准备放开肚皮吃包子了
djh已经吃了包子0
DJH已经吃了包子0
djh已经吃了包子1
DJH已经吃了包子1
djh已经吃了包子2
DJH已经吃了包子2
djh已经吃了包子3
DJH已经吃了包子3
djh已经吃了包子4
DJH已经吃了包子4
djh已经吃了包子5
DJH已经吃了包子5
djh已经吃了包子6
DJH已经吃了包子6
djh已经吃了包子7
DJH已经吃了包子7
djh已经吃了包子8
DJH已经吃了包子8
djh已经吃了包子9
DJH已经吃了包子9

 

posted on 2018-08-04 19:47  thtl  阅读(75)  评论(0编辑  收藏  举报