本节主要记录一下列表生成式, 生成器和迭代器的知识点
列表生成器
首先举个例子
现在有个需求, 看列表 [0,1,2,3,4,5,6,7,8,9], 要求你把列表里面的每个值加 1, 你怎么实现呢?
方法一(简单):
- info = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
- b = []
- # for index,i in enumerate(info):
- # print(i+1)
- # b.append(i+1)
- # print(b)
- for index,i in enumerate(info):
- info[index] +=1
- print(info)
方法二(一般):
- info = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
- a = map(lambda x:x+1,info)
- print(a)
- for i in a:
- print(i)
方法三(高级):
- info = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
- a = [i+1 for i in range(10)]
- print(a)
生成器
什么是生成器?
通过列表生成式, 我们可以直接创建一个列表, 但是, 受到内存限制, 列表容量肯定是有限的, 而且创建一个包含 100 万个元素的列表, 不仅占用很大的存储空间, 如果我们仅仅需要访问前面几个元素, 那后面绝大多数元素占用的空间都白白浪费了
所以, 如果列表元素可以按照某种算法推算出来, 那我们是否可以在循环的过程中不断推算出后续的元素呢? 这样就不必创建完整的 list, 从而节省大量的空间, 在 Python 中, 这种一边循环一边计算的机制, 称为生成器: generator
生成器是一个特殊的程序, 可以被用作控制循环的迭代行为, python 中生成器是迭代器的一种, 使用 yield 返回值函数, 每次调用 yield 会暂停, 而可以使用 next()函数和 send()函数恢复生成器
生成器类似于返回值为数组的一个函数, 这个函数可以接受参数, 可以被调用, 但是, 不同于一般的函数会一次性返回包括了所有数值的数组, 生成器一次只能产生一个值, 这样消耗的内存数量将大大减小, 而且允许调用函数可以很快的处理前几个返回值, 因此生成器看起来像是一个函数, 但是表现得却像是迭代器
python 中的生成器
要创建一个 generator, 有很多种方法, 第一种方法很简单, 只有把一个列表生成式的 [] 中括号改为 () 小括号, 就创建一个 generator
举例如下:
- # 列表生成式
- lis = [x*x for x in range(10)]
- print(lis)
- # 生成器
- generator_ex = (x*x for x in range(10))
- print(generator_ex)
结果:
- [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
- <generator object <genexpr> at 0x000002A4CBF9EBA0>
那么创建 lis 和 generator_ex, 的区别是什么呢? 从表面看就是 [ ] 和(), 但是结果却不一样, 一个打印出来是列表(因为是列表生成式), 而第二个打印出来却是 < generator object <genexpr> at 0x000002A4CBF9EBA0>, 那么如何打印出来 generator_ex 的每一个元素呢?
如果要一个个打印出来, 可以通过 next()函数获得 generator 的下一个返回值:
- # 生成器
- generator_ex = (x*x for x in range(10))
- print(next(generator_ex))
- print(next(generator_ex))
- print(next(generator_ex))
- print(next(generator_ex))
- print(next(generator_ex))
- print(next(generator_ex))
- print(next(generator_ex))
- print(next(generator_ex))
- print(next(generator_ex))
- print(next(generator_ex))
- print(next(generator_ex))
结果:
- 0
- 1
- 4
- 9
- 16
- 25
- 36
- 49
- 64
- 81
- Traceback (most recent call last):
- File "列表生成式. py", line 42, in <module>
- print(next(generator_ex))
- StopIteration
大家可以看到, generator 保存的是算法, 每次调用 next(generaotr_ex)就计算出他的下一个元素的值, 直到计算出最后一个元素, 没有更多的元素时, 抛出 StopIteration 的错误, 而且上面这样不断调用是一个不好的习惯, 正确的方法是使用 for 循环, 因为 generator 也是可迭代对象:
- # 生成器
- generator_ex = (x*x for x in range(10))
- for i in generator_ex:
- print(i)
结果:
- 0
- 1
- 4
- 9
- 16
- 25
- 36
- 49
- 64
- 81
所以我们创建一个 generator 后, 基本上永远不会调用 next(), 而是通过 for 循环来迭代, 并且不需要关心 StopIteration 的错误, generator 非常强大, 如果推算的算法比较复杂, 用类似列表生成式的 for 循环无法实现的时候, 还可以用函数来实现
比如著名的斐波那契数列, 除第一个和第二个数外, 任何一个数都可以由前两个相加得到:
1,1,2,3,5,8,12,21,34.....
斐波那契数列用列表生成式写不出来, 但是, 用函数把它打印出来却狠容易:
- #fibonacci 数列
- def fib(max):
- n,a,b =0,0,1
- while n < max:
- a,b =b,a+b
- n = n+1
- return done
- a = fib(10)
- print(fib(10))
a,b = b ,a+b 其实相当于 t =a+b ,a =b ,b =t , 所以不必写显示写出临时变量 t, 就可以输出斐波那契数列的前 N 个数字上面输出的结果如下:
- 1
- 1
- 2
- 3
- 5
- 8
- 13
- 21
- 34
- 55
- 1
- 1
- 2
- 3
- 5
- 8
- 13
- 21
- 34
- 55
- done
仔细观察, 可以看出, fib 函数实际上是定义了斐波拉契数列的推算规则, 可以从第一个元素开始, 推算出后续任意的元素, 这种逻辑其实非常类似 generator
也就是说上面的函数也可以用 generator 来实现, 上面我们发现, print(b)每次函数运行都要打印, 占内存, 所以为了不占内存, 我们也可以使用生成器, 这里叫 yield 如下:
- def fib(max):
- n,a,b =0,0,1
- while n < max:
- yield b
- a,b =b,a+b
- n = n+1
- return done
- a = fib(10)
- print(fib(10))
但是返回的不再是一个值, 而是一个生成器, 和上面的例子一样, 大家可以看一下结果:
<generator object fib at 0x000001C03AC34FC0>
那么这样就不占内存了, 这里说一下 generator 和函数的执行流程, 函数是顺序执行的, 遇到 return 语句或者最后一行函数语句就返回而变成 generator 的函数, 在每次调用 next()的时候执行, 遇到 yield 语句返回, 再次被 next()调用时候从上次的返回 yield 语句处急需执行, 也就是用多少, 取多少, 不占内存
- def fib(max):
- n,a,b =0,0,1
- while n < max:
- yield b
- a,b =b,a+b
- n = n+1
- return done
- a = fib(10)
- print(fib(10))
- print(a.__next__())
- print(a.__next__())
- print(a.__next__())
- print("可以顺便干其他事情")
- print(a.__next__())
- print(a.__next__())
结果:
- <generator object fib at 0x0000023A21A34FC0>
- 1
- 1
- 2
可以顺便干其他事情
3
5
在上面 fib 的例子, 我们在循环过程中不断调用 yield, 就会不断中断当然要给循环设置一个条件来退出循环, 不然就会产生一个无限数列出来同样的, 把函数改成 generator 后, 我们基本上从来不会用 next()来获取下一个返回值, 而是直接使用 for 循环来迭代:
- def fib(max):
- n,a,b =0,0,1
- while n < max:
- yield b
- a,b =b,a+b
- n = n+1
- return done
- for i in fib(6):
- print(i)
结果:
1
1
2
3
5
8
但是用 for 循环调用 generator 时, 发现拿不到 generator 的 return 语句的返回值如果拿不到返回值, 那么就会报错, 所以为了不让报错, 就要进行异常处理, 拿到返回值, 如果想要拿到返回值, 必须捕获 StopIteration 错误, 返回值包含在 StopIteration 的 value 中:
- def fib(max):
- n,a,b =0,0,1
- while n < max:
- yield b
- a,b =b,a+b
- n = n+1
- return done
- g = fib(6)
- while True:
- try:
- x = next(g)
- print(generator: ,x)
- except StopIteration as e:
- print("生成器返回值:",e.value)
- break
结果:
- generator: 1
- generator: 1
- generator: 2
- generator: 3
- generator: 5
- generator: 8
生成器返回值: done
还可以通过 yield 实现在单线程的情况下实现并发运算的效果
- import time
- def consumer(name):
- print("%s 准备学习啦!" %name)
- while True:
- lesson = yield
- print("开始 [%s] 了,[%s]老师来讲课了!" %(lesson,name))
- def producer(name):
- c = consumer(A)
- c2 = consumer(B)
- c.__next__()
- c2.__next__()
- print("同学们开始上课 了!")
- for i in range(10):
- time.sleep(1)
- print("到了两个同学!")
- c.send(i)
- c2.send(i)
结果:
A 准备学习啦!
B 准备学习啦!
同学们开始上课 了!
到了两个同学!
开始 [0] 了,[A]老师来讲课了!
开始 [0] 了,[B]老师来讲课了!
到了两个同学!
开始 [1] 了,[A]老师来讲课了!
开始 [1] 了,[B]老师来讲课了!
到了两个同学!
开始 [2] 了,[A]老师来讲课了!
开始 [2] 了,[B]老师来讲课了!
到了两个同学!
开始 [3] 了,[A]老师来讲课了!
开始 [3] 了,[B]老师来讲课了!
到了两个同学!
开始 [4] 了,[A]老师来讲课了!
开始 [4] 了,[B]老师来讲课了!
到了两个同学!
开始 [5] 了,[A]老师来讲课了!
开始 [5] 了,[B]老师来讲课了!
到了两个同学!
开始 [6] 了,[A]老师来讲课了!
开始 [6] 了,[B]老师来讲课了!
到了两个同学!
由上面的例子我么可以发现, python 提供了两种基本的方式
生成器函数: 也是用 def 定义的, 利用关键字 yield 一次性返回一个结果, 阻塞, 重新开始
生成器表达式: 返回一个对象, 这个对象只有在需要的时候才产生结果
生成器函数
为什么叫生成器函数? 因为它随着时间的推移生成了一个数值队列一般的函数在执行完毕之后会返回一个值然后退出, 但是生成器函数会自动挂起, 然后重新拾起急需执行, 他会利用 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
- Process finished with exit code 0
生成器表达式
生成器表达式来源于迭代和列表解析的组合, 生成器和列表解析类似, 但是它使用尖括号而不是方括号
- >>> # 列表解析生成列表
- >>> [ x ** 3 for x in range(5)]
- [0, 1, 8, 27, 64]
- >>>
- >>> # 生成器表达式
- >>> (x ** 3 for x in range(5))
- <generator object <genexpr> at 0x000000000315F678>
- >>> # 两者之间转换
- >>> list(x ** 3 for x in range(5))
- [0, 1, 8, 27, 64]
一个迭代既可以被写成生成器函数, 也可以被协程生成器表达式, 均支持自动和手动迭代而且这些生成器只支持一个 active 迭代, 也就是说生成器的迭代器就是生成器本身
迭代器(迭代就是循环)
我们已经知道, 可以直接作用于 for 循环的数据类型有以下几种:
一类是集合数据类型, 如 list,tuple,dict,set,str 等
一类是 generator, 包括生成器和带 yield 的 generator function
这些可以直接作用于 for 循环的对象统称为可迭代对象: Iterable
可以使用 isinstance()判断一个对象是否为可 Iterable 对象
- >>> from collections import Iterable
- >>> isinstance([], Iterable)
- True
- >>> isinstance({}, Iterable)
- True
- >>> isinstance(abc, Iterable)
- True
- >>> isinstance((x for x in range(10)), Iterable)
- True
- >>> isinstance(100, Iterable)
- False
而生成器不但可以作用于 for 循环, 还可以被 next()函数不断调用并返回下一个值, 直到最后抛出 StopIteration 错误表示无法继续返回下一个值了
所以这里将一下迭代器
可以被 next()函数调用并不断返回下一个值的对象称为迭代器: Iterator
可以使用 isinstance()判断一个对象是否是 Iterator 对象:
- >>> from collections import Iterator
- >>> isinstance((x for x in range(10)), Iterator)
- True
- >>> isinstance([], Iterator)
- False
- >>> isinstance({}, Iterator)
- False
- >>> isinstance(abc, Iterator)
- False
生成器都是 Iterator 对象, 但 listdictstr 虽然是
Iterable(可迭代对象)
, 却不是
Iterator(迭代器)
把 listdictstr 等 Iterable 变成 Iterator 可以使用 iter()函数:
- >>> isinstance(iter([]), Iterator)
- True
- >>> isinstance(iter(abc), Iterator)
- True
你可能会问, 为什么 listdictstr 等数据类型不是 Iterator?
这是因为 Python 的 Iterator 对象表示的是一个数据流, Iterator 对象可以被 next()函数调用并不断返回下一个数据, 直到没有数据时抛出 StopIteration 错误可以把这个数据流看做是一个有序序列, 但我们却不能提前知道序列的长度, 只能不断通过 next()函数实现按需计算下一个数据, 所以 Iterator 的计算是惰性的, 只有在需要返回下一个数据时它才会计算
Iterator 甚至可以表示一个无限大的数据流, 例如全体自然数而使用 list 是永远不可能存储全体自然数的
小结:
凡是可作用于 for 循环的对象都是 Iterable 类型;
凡是可作用于 next()函数的对象都是 Iterator 类型, 它们表示一个惰性计算的序列;
集合数据类型如 listdictstr 等是 Iterable 但不是 Iterator, 不过可以通过 iter()函数获得一个 Iterator 对象
Python3 的 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 的总结
(1): 通常的 for..in... 循环中, in 后面是一个数组, 这个数组就是一个可迭代对象, 类似的还有链表, 字符串, 文件他可以是 a = [1,2,3], 也可以是 a = [x*x for x in range(3)]
它的缺点也很明显, 就是所有数据都在内存里面, 如果有海量的数据, 将会非常耗内存
(2)生成器是可以迭代的, 但是只可以读取它一次因为用的时候才生成, 比如 a = (x*x for x in range(3))!!!! 注意这里是小括号而不是方括号
(3)生成器 (generator) 能够迭代的关键是他有 next()方法, 工作原理就是通过重复调用 next()方法, 直到捕获一个异常
(4)带有 yield 的函数不再是一个普通的函数, 而是一个生成器 generator, 可用于迭代
(5)yield 是一个类似 return 的关键字, 迭代一次遇到 yield 的时候就返回 yield 后面或者右面的值而且下一次迭代的时候, 从上一次迭代遇到的 yield 后面的代码开始执行
(6)yield 就是 return 返回的一个值, 并且记住这个返回的位置下一次迭代就从这个位置开始
(7)带有 yield 的函数不仅仅是只用于 for 循环, 而且可用于某个函数的参数, 只要这个函数的参数也允许迭代参数
(8)send()和 next()的区别就在于 send 可传递参数给 yield 表达式, 这时候传递的参数就会作为 yield 表达式的值, 而 yield 的参数是返回给调用者的值, 也就是说 send 可以强行修改上一个 yield 表达式值
(9)send()和 next()都有返回值, 他们的返回值是当前迭代遇到的 yield 的时候, yield 后面表达式的值, 其实就是当前迭代 yield 后面的参数
(10)第一次调用时候必须先 next()或 send(), 否则会报错, send 后之所以为 None 是因为这时候没有上一个 yield, 所以也可以认为 next()等同于 send(None)
来源: http://www.bubuko.com/infodetail-2521994.html