python迭代器和生成器,你真的了解吗?

导读
列表a = [0,1,2,3,4,5,6,7,8,9],假如现在有个需求,需要把a中的每个元素都加1,我们该如何实现?
Way 1.

for i in a:
    i +=1

Way 2.

a = map(lambda x: x + 1, a)

Way 3.

a = [i + 1 for i in a]

通过 Way 3. 的列表生成式,我们可以直接创建一个列表,但是,受到内存限制,列表容量肯定是有限的,而且创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间,在Python中,这种一边循环一边计算的机制,称为生成器:generator

概念介绍

  • 生成器
    生成器是一个特殊的程序,可以被用作控制循环的迭代行为,python中生成器是迭代器的一种,使用yield返回值函数,每次调用yield会暂停,随后使用next()函数和send()函数恢复生成器。通俗地说,生成器类似于一个返回值为数组的函数,这个函数可以接受参数,可以被调用,但是一般的函数会一次性地返回包含了所有结果的数组,而生成器依次只能产生一个值,这样消耗的内存便大大减小(因为有时候不是所有的返回结果我们马上就要用到,所以一次性返回所有结果会造成内存资源浪费),并且允许调用函数可以很快的处理前几个返回值。因此生成器看起来像是一个函数,但是表现地像是一个迭代器。
  • 迭代器
    迭代器,其实就是循环器。迭代器包含有next方法的实现,在正确的范围内返回期待的数据,并在超出范围后抛出StopIteration的错误停止迭代。
    我们知道,可以直接作用于for循环数据类型有以下几种:
    (1) 集合型数据:list,dict,tuple,set,str等
    (2) generator,包括生成器和带yield的generator function

以上这些可直接作用于for循环的对象统称为可迭代对象 (Iterable),可以使用 isinstance() 判断对象是否为Iterable对象。

from collections import Iterable

isinstance([], Iterable) # 列表
isinstance({
    
    }, Iterable) # 字典
isinstance('python', Iterable) # 字符串
isinstance((x for x in range(10)), Iterable) # 集合
isinstance(100, Iterable) # 整数

输出:

True
True
True
True
False

Python中的生成器

在python中要创建一个生成器有很多办法,第一种最简单的方法,只需要把上边 Way 3. 的列表生成式外边的 [ ] 改成 () 即可创建一个生成器。

a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
list_a = [x + 1 for x in a]
print("list_a = ", list_a)
generator_a = (x + 1 for x in a)
print("genrator_a = ", generator_a)

输出:

list_a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
generator_a = <generator object <genexpr> at 0x00000230BEF5D048>

那么创建 list_a 和 generator_a 的区别是什么呢?从表面看就是 [ ] 和( ),但是结果却不一样,一个打印出来是列表(因为是列表生成式),而第二个打印出来却是<generator object at 0x00000230BEF5D048>,那么如何打印出来 generator_a 的每一个元素呢?
如果要一个一个地打印出generator_a里的元素,可以通过next()函数获得generator_a的下一个元素。

print(next(generator_a))
print(next(generator_a))
print(next(generator_a))
print(next(generator_a))
print(next(generator_a))
print(next(generator_a))
print(next(generator_a))
print(next(generator_a))
print(next(generator_a))
print(next(generator_a))

输出:

1
2
3
4
5
6
7
8
9
10
Traceback (most recent call last):
 
  File "generator_blog.py", line 42, in <module>
 
    print(next(generator_ex))
 
StopIteration

大家可以看到,generator保存的是算法,每次调用 next(generaotr_a) 就计算出他的下一个元素的值,直到计算出最后一个元素,没有更多的元素时,抛出 StopIteration 的错误,而且上面这样不断调用是一个不好的习惯,正确的方法是使用for循环,因为generator也是可迭代对象(Iterable)

for i in generator_a:
	print(i)

输出:

1
2
3
4
5
6
7
8
9
10

所以我们创建一个generator后,基本上永远不会调用next(),而是通过for循环来迭代,并且不需要关心 StopIteration 的错误,generator非常强大,如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。我们列举个例子。

def dfs(floors):
	n = 0
	a, b = 0, 1
	while n < floors:
		yield b
		a, b = b, a + b
		n += 1
	return 'Mission Complete'

a = dfs(10)
print(a)
print(a.__next__())
print(a.__next__())
print(a.__next__())
print(a.__next__())
print(a.__next__())

输出:

<generator object fib at 0x0000023A21A34FC0>
1
2
3
5
8

我们可以感受到generator的执行流程,只在每次调用__next__()的时候执行一次,且遇到yield语句返回,再次执行__next__()的时候从上次返回的地方继续执行,就这样每次执行便在之前的基础上进行迭代计算,用多少,取多少,而不是一次性地把结果全反馈给我们,使得内存占用效率最大化。
上边也说过了,在使用generator的时候,我们基本不用next方法一个一个的取出结果,因为generator是可迭代对象,所以我们让它作用于for循环。

for i in dfs(6):
	print(i)

输出:

1
2
3
5
8

上边说了挺多关于python生成器的内容,总结一下,python提供了两种基本的创建生成器的方式。

  • 生成器函数
    这种方式就是我们上边举得函数 dfs 的例子,生成器函数随着时间的推移生成了一个数值队列。一般的函数在执行完毕之后会返回一个值然后退出,但是生成器函数会自动挂起,然后重新拾起急需执行,他会利用yield关键字关起函数,给调用者返回一个值,同时保留了当前的足够多的状态,可以使函数继续执行,生成器和迭代协议是密切相关的,迭代器都有一个__next__()__成员方法,这个方法要么返回迭代的下一项,要么引起异常结束迭代。
# 函数有了yield之后,函数名+()就变成了生成器
# return在生成器中代表生成器的中止,直接报错
# next的作用是唤醒并继续执行
# send的作用是唤醒并继续执行,发送一个信息到生成器内部
'''生成器'''
 
def create_counter(n):
    print("create_counter")
    while True:
        yield n
        print("increment n")
        n +=1
 
gen = create_counter(2)
print(gen)
print(next(gen))
print(next(gen))

输出:

<generator object create_counter at 0x0000023A1694A938>
create_counter
2
increment n
3

从输出结果的顺序上,我们可以了解yield的工作机制

  • 生成器表达式
    这种方式就是我们上边列表生成器的方法。生成器表达式来源于迭代和列表解析的组合,生成器和列表解析类似,但是它使用尖括号而不是方括号。
    一个迭代既可以被写成生成器函数,也可以被协程生成器表达式,均支持自动和手动迭代。而且这些生成器只支持一个active迭代,也就是说生成器的迭代器就是生成器本身。

Python中的迭代器

上边我们说过,生成器都是Iterator对象。然而,list、dict、str虽然是Iterable(可迭代对象),却不是Iterator(迭代器),但是可以使用iter()函数将它们转为Iterator。

isinstance(iter([]), Iterator)
isinstance(iter('abc'), Iterator)

输出:

True
True

为什么list、dict、str等数据类型是Iterable而不是Iterator?这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。
Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

小结:

凡是可作用于for循环的对象都是Iterable类型;
凡是可作用于next()函数的对象都是Iterator类型,它们表示一个惰性计算的序列;
集合数据类型如list、dict、str等是Iterable但不是Iterator,不过可以通过iter()函数获得一个Iterator对象。

在Python 3中,for循环本质上也是通过不断地调用next()函数来实现的。举例:

for x in [1, 2, 3, 4, 5]:
    pass

这两句就等价于下边的程序:

# 首先获得Iterator对象:
it = iter([1, 2, 3, 4, 5])
# 循环:
while True:
    try:
        # 获得下一个值:
        x = next(it)
    except StopIteration:
        # 遇到StopIteration就退出循环
        break

最后再对yield做一个总结

  • 通常的for…in…循环中,in后面是一个数组,这个数组就是一个可迭代对象,类似的还有链表,字符串,文件。他可以是a = [1,2,3],也可以是a = [x*x for x in range(3)]。它的缺点也很明显,就是所有数据都在内存里面,如果有海量的数据,将会非常耗内存;
  • 生成器是可以迭代的,但是每次只可以读取它一次。因为要取出使用的时候才生成,比如a = (x*x for x in range(3))。注意这里是小括号而不是方括号。 ---->>>列表生成式创建迭代器的方法;
  • 生成器(generator)能够迭代的关键是他有next()方法,工作原理就是通过重复调用next()方法,直到捕获一个异常;
  • 带有yield的函数不再是一个普通的函数,而是一个生成器generator,可用于迭代;
  • yield是一个类似return 的关键字,迭代一次遇到yield的时候就返回yield后面或者右面的值。而且下一次迭代的时候,从上一次迭代遇到的yield后面的代码开始执行;
  • yield就是return返回的一个值,并且记住这个返回的位置。下一次迭代就从这个位置开始;

猜你喜欢

转载自blog.csdn.net/Just_do_myself/article/details/118569039