导读
列表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返回的一个值,并且记住这个返回的位置。下一次迭代就从这个位置开始;