01-迭代器和生成器

该文主要来自https://www.cnblogs.com/liwenzhou/p/9761027.html这是一个极为厉害的大佬

迭代和可迭代

什么是迭代(iteration)?

如果给定一个list或tuple,我们要想访问其中的某个元素,我们可以通过下标来,如果我们想要访问所有的元素,那我们可以用for循环来遍历这个list或者tuple,而这种遍历我们就叫做迭代。

可迭代(iterable)?

其实你已经知道,不是所有的数据类型都是可迭代的。那么可迭代的数据类型都有什么特点呢?

可以被for循环的都是可迭代的,要想可迭代,内部必须有一个__iter__方法。

下面的代码是判断我们常见的这些集合能否迭代

from collections import Iterable

l = [1,2,3,4]
t = (1,2,3,4)
d = {1:2,3:4}
s = {1,2,3,4}

print(isinstance(l,Iterable))
print('__iter__' in dir([l]))
print(isinstance(t,Iterable))
print('__iter__' in dir([t]))
print(isinstance(d,Iterable))
print('__iter__' in dir([d]))
print(isinstance(s,Iterable))
print('__iter__' in dir([s]))

# 输出
True
True
True
True
True
True
True
True

接着分析,__iter__方法做了什么事情呢?

print([1,2].__iter__())

# 结果
<list_iterator object at 0x000002BDDBE7BEE0>

执行了list([1,2])的__iter__方法,我们好像得到了一个list_iterator,现在我们又得到了一个新名词——iterator。

这是一个迭代器

迭代器协议

既什么叫“可迭代”之后,又一个历史新难题,什么叫“迭代器”?

虽然我们不知道什么叫迭代器,但是我们现在已经有一个迭代器了,这个迭代器是一个列表的迭代器。

我们来看看这个列表的迭代器比起列表来说实现了哪些新方法,这样就能揭开迭代器的神秘面纱了吧?

'''
dir([1,2].__iter__())是列表迭代器中实现的所有方法,
dir([1,2])是列表中实现的所有方法,都是以列表的形式返回给我们的,
为了看的更清楚,我们分别把他们转换成集合,然后取差集。
'''
# print(dir([1,2].__iter__()))
# print(dir([1,2]))
print(set(dir([1, 2].__iter__()))-set(dir([1, 2])))

# 结果
{'__setstate__', '__length_hint__', '__next__'}

我们看到在列表迭代器中多了三个方法,那么这三个方法都分别做了什么事呢?

iter_l = [1, 2, 3, 4, 5, 6].__iter__()
# 获取迭代器中元素的长度
print(iter_l.__length_hint__())
# 根据索引值指定从哪里开始迭代
print('*', iter_l.__setstate__(4))
# 一个一个的取值
print('**', iter_l.__next__())
print('***', iter_l.__next__())

# 结果
6
* None
** 5
*** 6

这三个方法中,能让我们一个一个取值的神奇方法是谁?

没错!就是__next__

在for循环中,就是在内部调用了__next__方法才能取到一个一个的值。

那接下来我们就用迭代器的next方法来写一个不依赖for的遍历。

l = [1,2,3,4]
l_iter = l.__iter__()
item = l_iter.__next__()
print(item)
item = l_iter.__next__()
print(item)
item = l_iter.__next__()
print(item)
item = l_iter.__next__()
print(item)
item = l_iter.__next__()
print(item)

# 结果
1
2
3
4
Traceback (most recent call last):
  File "g:\Project\DRF\test.py", line 11, in <module>
    item = l_iter.__next__()

这是一段会报错的代码,如果我们一直取next取到迭代器里已经没有元素了,就会抛出一个异常StopIteration,告诉我们,列表中已经没有有效的元素了。

这个时候,我们就要使用异常处理机制来把这个异常处理掉。

l = [1,2,3,4]
l_iter = l.__iter__()
while True:
    try:
        item = l_iter.__next__()
        print(item)
    except StopIteration:
        break

# 结果
1
2
3
4

那现在我们就使用while循环实现了原本for循环做的事情,我们是从谁那儿获取一个一个的值呀?是不是就是l_iter?好了,这个l_iter就是一个迭代器。

迭代器遵循 迭代器协议 :必须拥有__iter__方法和__next__方法。

下面来讨论一下range()

首先,它肯定是一个可迭代的对象,但是它是否是一个迭代器?我们来测试一下

from collections.abc import Iterator
# 查看'__next__'是不是在range()方法执行之后内部是否有__next__
print('__next__' in dir(range(12)))
# 查看'__iter__'是不是在range()方法执行之后内部是否有__iter__
print('__iter__' in dir(range(12)))

print(isinstance(range(100000000), Iterator))  

# 结果
False
True
False
验证range执行之后得到的结果不是一个迭代器

01-迭代器和生成器

迭代器一定是可迭代对象,可迭代对象未必是迭代器

为什么要有for循环

基于上面讲的列表这一大堆遍历方式,聪明的你立马看除了端倪,于是你不知死活大声喊道,你这不逗我玩呢么,有了下标的访问方式,我可以这样遍历一个列表啊

l=[1,2,3]

index=0
while index < len(l):
    print(l[index])
    index+=1

#要毛线for循环,要毛线可迭代,要毛线迭代器

没错,序列类型字符串,列表,元组都有下标,你用上述的方式访问,perfect!但是你可曾想过非序列类型像字典,集合,文件对象的感受,所以嘛,年轻人,for循环就是基于迭代器协议提供了一个统一的可以遍历所有对象的方法,即在遍历之前,先调用对象的__iter__方法将其转换成一个迭代器,然后使用迭代器协议去实现循环访问,这样所有的对象就都可以通过for循环来遍历了,而且你看到的效果也确实如此,这就是无所不能的for循环,觉悟吧,年轻人

生成器

初识生成器

我们知道的迭代器有两种:一种是调用方法直接返回的,一种是可迭代对象通过执行 iter方法得到的,迭代器有的好处是可以节省内存。

如果在某些情况下,我们也需要节省内存,就只能自己写。我们自己写的这个能实现迭代器功能的东西就叫生成器。

Python 中提供的 生成器:

  1. 生成器函数:常规函数定义,但是,使用yield语句而不是return语句返回结果。yield语句一次返回一个结果,在每个结果中间,挂起函数的状态,以便下次从它离开的地方继续执行

  2. 生成器表达式:类似于列表推导,但是,生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表

生成器Generator:

本质:迭代器 ( 所以自带了 __iter__方法和 __next__方法,不需要我们去实现)

特点:惰性运算,开发者自定义

生成器函数

一个包含yield关键字的函数就是一个生成器函数。

yield和return一样可以从函数中返回值,但是yield又不同于return,return的执行意味着程序的结束,只能返回一次,yield可以返回多次。

调用生成器函数不会得到返回的具体的值,而是得到一个生成器对象。

每一次从这个可迭代对象获取值,就能推动函数的执行,获取新的返回值。直到函数执行结束(yield像是拥有能够让函数暂停的魔力)。

def my_range():
    print('我是一个生成器函数')
    n = 0
    while 1:
        yield n
        n += 1

生成器有什么好处呢?就是不会一下子在内存中生成太多数据

接下来,我们在这个函数的基础上来写一个我们自己的range函数,实现开始和结束

def my_range2(start, stop):
    n = start
    while n < stop:
        yield n
        n += 1

再进一步,实现步长:

def my_range3(start, stop, step):
    n = start
    while n < stop:
        yield n
        n += step

生成器本质上就是个迭代器,我们根据自己的想法创造的迭代器,它当然也支持for循环:

for i in my_range3(1, 10, 2):
    print(i)

更多应用 - 生成器监听文件输入的例子

import time


def tail(filename):
    f = open(filename)
    f.seek(0, 2) #从文件末尾算起
    while True:
        line = f.readline()  # 读取文件中新的文本行
        if not line:
            time.sleep(0.1)
            continue
        yield line

tail_g = tail('tmp')
for line in tail_g:
    print(line)

send

yield可以返回值,也可以接收值。

通过生成器的send方法可以给yield传值。

def eat(name):
    print('%s要开始吃了!' % name)
    while 1:
        food = yield
        print('{}在吃{}'.format(name, food))


a = eat('alex')
a.__next__()  # 初始化,让函数暂停在yield处
a.send('包子')  # send两个作用:1.给yield传值 2.继续执行函数
a.send('饺子')

# 结果 
alex要开始吃了!
alex在吃包子
alex在吃饺子

yield可以同时返回值和接收值。

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total/count


g_avg = averager()
next(g_avg) # 这里是为了激活生成器,要让代码走到yield才能既取值又传值
print(g_avg.send(10))
print(g_avg.send(30))
print(g_avg.send(5))

# 结果
10.0
20.0
15.0

计算移动平均值(2)_预激协程的装饰器

def init(func):  #在调用被装饰生成器函数的时候首先用next激活生成器
    def inner(*args,**kwargs):
        g = func(*args,**kwargs)
        next(g)
        return g
    return inner

@init
def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total/count


g_avg = averager()
# next(g_avg)   在装饰器中执行了next方法
print(g_avg.send(10))
print(g_avg.send(30))
print(g_avg.send(5))

# 结果
10.0
20.0
15.0

这一块儿涉及到装饰器,可能有点难理解,可以去看我的关于装饰器的博客,也在python杂记系列

yield from

def gen1():
    for c in 'AB':
        yield c
    for i in range(3):
        yield i

print(list(gen1()))

def gen2():
    yield from 'AB'
    yield from range(3)

print(list(gen2()))

# 结果
['A', 'B', 0, 1, 2]
['A', 'B', 0, 1, 2]

列表推导式和生成器表达式

老男孩由于峰哥的强势加盟很快走上了上市之路,alex思来想去决定下几个鸡蛋来报答峰哥

egg_list = ['鸡蛋%s' % i for i in range(10)]  # 列表解析
print(egg_list)

# 峰哥瞅着alex下的一筐鸡蛋,捂住了鼻子,说了句:哥,你还是给我只母鸡吧,我自己回家下

laomuji = ('鸡蛋%s' % i for i in range(10))  # 生成器表达式
print(laomuji)
print(next(laomuji))  # next本质就是调用__next__
print(laomuji.__next__())
print(next(laomuji))

# 结果
['鸡蛋0', '鸡蛋1', '鸡蛋2', '鸡蛋3', '鸡蛋4', '鸡蛋5', '鸡蛋6', '鸡蛋7', '鸡蛋8', '鸡蛋9']
<generator object <genexpr> at 0x000001F73C4EAF90>
鸡蛋0
鸡蛋1
鸡蛋2

总结:

  1. 把列表解析的[]换成()得到的就是生成器表达式
  2. 列表解析与生成器表达式都是一种便利的编程方式,只不过生成器表达式更节省内存
  3. Python不但使用迭代器协议,让for循环变得更加通用。大部分内置函数,也是使用迭代器协议访问对象的。例如, sum函数是Python的内置函数,该函数使用迭代器协议访问对象,而生成器实现了迭代器协议,所以,我们可以直接这样计算一系列值的和:
sum(x ** 2 for x in range(4))

而不用多此一举的先构造一个列表:

sum([x ** 2 for x in range(4)]) 

01-迭代器和生成器

一道有点难度的面试题

下面这段代码来自原博客,但是原博客并没有写多余的东西

源代码,生成器表达式

def add(a, b):
    return a + b


def func():
    for j in range(4):
        yield j


g = func()  # g(0,1,2,3)
for n in [1, 10]:
    g = (add(n, i) for i in g)


print(list(g))

# 执行结果
[20, 21, 22, 23]

修改源代码,将生成器表达式改成列表解析

def add(a, b):
    return a + b


def func():
    for j in range(4):
        yield j


g = func()  # g(0,1,2,3)
for n in [1, 10]:
    g = [add(n, i) for i in g]  # 这里把之前的括号改成了方括号


print(list(g))

# 结果
[11, 12, 13, 14]

可以看到,这段代码就一对括号的区别,结果就是天壤之别。这里面很值得探究。下面就这个差异进行分析。

g = (add(n, i) for i in g)

这段代码执行完以后,g变成了什么?一段已经开始执行取数的代码,也就是说他还没固定,他只是一段待执行代码块,当我们真的要取数的时候,他才会执行,返回四个数字给我们。

g = [add(n, i) for i in g]

这段代码在执行的过程中,g最后变成了一个列表,里面有四个数,也就是说,这四个数已经确定了,列表固定了。

一定要明白上述两段代码的根本差异,生成器表达式只是一段待执行的代码,子元素还没生出来呢,最后什么样谁也不知道,而列表已经固定了,内部已经填充了数字。我又重复了一遍,记住这个概念。

现在我们逐段分析,先看列表推导式的代码

def add(a, b):
    return a + b


def func():
    for j in range(4):
        yield j


g = func()  # 在这里这个g最后会产生(0,1,2,3)四个数字没什么好争议的。但是这个时候g还是一个生成器,并非列表

for n in [1, 10]:
    g = [add(n, i) for i in g] 
'''
在for循环中,
g = [add(n, i) for i in g] 
执行了两遍,第一遍,因为是[]这个括号,代码会立刻执行,生成一个列表
g已经变成了一个列表,此时内部内容是[1,2,3,4]
执行第二遍,n=10,这个时候逐个与g子元素相加
g变成了另一个列表,内部内容为[11,12,13,14]
'''

print(list(g))

再来看生成器模式的代码

def add(a, b):
    return a + b


def func():
    for j in range(4):
        yield j


g = func()  # 到这里,g还是一个生成器,
for n in [1, 10]:
    g = (add(n, i) for i in g)
'''
在for循环中,
g = (add(n, i) for i in g)
执行了两遍,但是由于这里采用的是(),所以每次执行完了,g最后还是一个生成器,我说过,生成器就是一段待执行的取数代码,但是因为g这个代码块的地址发生了改变,因此每次g发生改变,我都将其名字改一下,方便梳理思路
第一遍执行变成了这样
g1 = (add(n, i) for i in g)
第二遍执行完了
g2 = (add(n, i) for i in g1)
那么最后整体的取数逻辑变成了什么样
g2 = (add(n, i) for i in (add(n, i) for i in g1))
'''

print(list(g))
'''
到了这里要开始取数了
print(list(g))实际上变成了什么print(list(g2))
也就是说,前面,我叫你取数,你不取,现在你必须给我取,不管你取数逻辑变成了什么样,你也要给我吐出数字来。
list向g2索要第一个数
g2 = (add(n, i) for i in g1)
这段代码从左往右执行,因为经过了两次循环,n的值已经确定下来了,就是10,
g2要执行一个add操作,n已经有了,就是10,i呢,i还没取出来呢,于是g2就找g1索要i
g1 = (add(n, i) for i in g)
再次从左往右执行,n=10,可是i呢,给我吐出来,于是g1又去找g索要i
g = func() ,这段代码里面完全没有别的值,影响最终的结果,所以,第一次返回一个0
于是在g1这里完成了add(10,0)这个操作,这个操作得到一个10,于是g1说,给你,
g2得到了10,于是他要继续自己未完成的操作,那就是add(10,10),于是就有了第一个数字20。
然后list继续向g2索要第二个值,
。。。
就这样,得到了最终的结果是
[20, 21, 22, 23]

这叫什么?欠的早晚要还
'''

从上面的分析我们得到一个很重要的结论,生成器是节约了内存,用的时候我才给你,先欠着,但是欠的早晚要还,因此使用生成器的时候最好不要掺杂会变化的变量,不然很可能会出事故。得到奇奇怪怪的结果。生成器也尽量不要嵌套生成器,一不留神就把自己给绕晕了。

到了这里你可能对生成器就是一段待执行的代码没有深刻的认识,那么接下来的例子会让你彻底搞懂

def demo():
    for i in range(4):
        yield i


g1 = demo()
g2 = demo()
g3 = [1, 2, 3, 4]


print('g1第一次取数', list(g1))
print('g1第二次取数', list(g1))
print('g2第一次取数', list(g2))
print('g3第一次取数', list(g3))
print('g3第二次取数', list(g3))

# 结果
g1第一次取数 [0, 1, 2, 3]
g1第二次取数 []
g2第一次取数 [0, 1, 2, 3]
g3第一次取数 [1, 2, 3, 4]
g3第二次取数 [1, 2, 3, 4]

g1只能取一轮,想要再用一次,只能从头开始执行,g2就是从头开始执行的,而g3完全不需要,因为内部固定了。

上一篇:Linux_CMD_FOR_FILE&FOLDER


下一篇:在static静态变量中使用注入方式