Python 生成器、迭代器、可迭代对象

1. 生成器(Generator)

1.1 生成器的创建方式1

1.2 生成器的创建方式2

1.3 生成器的特点总结

1.4 生成器的应用场景

2. 迭代器(Iterator)

3. 可迭代对象(Iterable)

4. 总结

 

 

1. 生成器(Generator)

通过列表生成式,我们可以直接创建一个列表。但由于受到内存限制,列表容量肯定是有限的。并且,如果创建一个包含了100万个元素的列表,却仅仅需要访问前面几个元素,那么后面绝大多数元素占用的空间都白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。

在Python中,这种一边循环一边计算的机制,称为生成器(generator)。

 

1.1 生成器的创建方式1

创建一个生成器,有多种方法。第一种方法很简单,只要把一个列表生成式的 [ ] 改成 ( ) 。

1 >>> l = [x*2 for x in range(5)]
2 >>> l
3 [0, 2, 4, 6, 8]
4 >>> g = (x*2 for x in range(5))
5 >>> g
6 <generator object <genexpr> at 0x0313F6B8>

我们可以直接打印出列表的每一个元素,但我们怎么打印出生成器的每一个元素呢?如果要一个一个打印出来,可以通过 next() 函数获得生成器的下一个返回值:

 1 >>> next(g)  # 也可以使用 g.__next__()
 2 0
 3 >>> next(g)
 4 2
 5 >>> next(g)
 6 4
 7 >>> next(g)
 8 6
 9 >>> next(g)
10 8
11 >>> next(g)
12 Traceback (most recent call last):
13   File "<stdin>", line 1, in <module>
14 StopIteration

生成器保存的是算法,每次调用 next(G) ,就计算出 G 的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出 StopIteration 的异常。

当然,这种不断调用 next() 实在是太变态了,正确的方法是使用 for 循环,因为生成器也是可迭代对象。

所以,我们创建了一个生成器后,基本上永远不会调用 next() ,而是通过 for 循环来迭代它,并且不需要关心 StopIteration 异常。

1 >>> g = (x*2 for x in range(5))
2 >>> for i in g:
3 ...     print(i)
4 ...
5 0
6 2
7 4
8 6
9 8

 

1.2 生成器的创建方式2

generator 非常强大。如果推算的算法比较复杂,用类似列表生成式的 for 循环无法实现的时候,还可以用函数来实现。

比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:

1, 1, 2, 3, 5, 8, 13, 21, 34, ...

斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:

 1 >>> def fib(times):
 2 ...     n = 0
 3 ...     a, b = 0, 1
 4 ...     while n < times:
 5 ...         print(b)
 6 ...         a, b = b, a+b
 7 ...         n += 1
 8 ...     return "done"
 9 ...
10 >>> fib(5)
11 1
12 1
13 2
14 3
15 5
16 'done'

仔细观察,可以看出,fib() 函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似 generator。

也就是说,上面的函数和 generator 仅一步之遥。要把 fib() 函数变成 generator,只需要把 print(b) 改为 yield b 就可以了:

 1 >>> def fib(times):
 2 ...     n = 0
 3 ...     a, b = 0, 1
 4 ...     while n < times:
 5 ...         yield b
 6 ...         a, b = b, a+b
 7 ...         n += 1
 8 ...     return "done"
 9 ...
10 >>> f = fib(5)
11 >>> next(f)
12 1
13 >>> next(f)
14 1
15 >>> next(f)
16 2
17 >>> next(f)
18 3
19 >>> next(f)
20 5
21 >>> next(f)
22 Traceback (most recent call last):
23   File "<stdin>", line 1, in <module>
24 StopIteration: done

在上面 fib 的例子,我们在循环过程中不断调用 yield ,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。

同样的,把函数改成 generator 后,我们基本上从来不会用 next() 来获取下一个返回值,而是直接使用 for 循环来迭代:

1 >>> for i in fib(5):
2 ...     print(i)
3 ...
4 1
5 1
6 2
7 3
8 5
9 >>>

但是在用 for 循环调用 generator 时,发现拿不到 generator 的 return 语句的返回值。如果想要拿到返回值,必须捕获 StopIteration 错误,返回值包含在 StopIteration 的 value 中:

 1 >>> g = fib(5)
 2 >>>
 3 >>> while True:
 4 ...     try:
 5 ...         x = next(g)
 6 ...         print("value: {}".format(x))
 7 ...     except StopIteration as msg:
 8 ...         print("生成器的返回值为:{}".format(msg.value))
 9 ...         break
10 ...
11 value: 1
12 value: 1
13 value: 2
14 value: 3
15 value: 5
16 生成器的返回值为:done

send() 方法

 send 方法与 next 方法一样可以调用 yield 的结果,并把 send() 中的参数传递作为 yield 整体的返回值。

>>> def test():
...     i = 0
...     while i < 5:
...             temp = yield i
...             print(temp)
...             i += 1
...
>>> t = test()
>>> next(t)
0
>>> next(t)
None
1
>>> next(t)
None
2
>>> t.send("haha")  # send与next一样可以调用 yield i 的结果,并把"haha"传递作为 yield i 整体的返回值,即赋值给了temp
haha
3
>>> next(t)  # 注意,此时并没有为 yield i 传递返回值,因此temp又为None
None
4

注意第一次调用就使用 send() 时 :

 1 >>> def test():
 2 ...     i = 0
 3 ...     while i < 5:
 4 ...             temp = yield i
 5 ...             print(temp)
 6 ...             i += 1
 7 ...
 8 >>> t = test()
 9 >>> t.send("haha")  # 由于先计算=号右边的表达式,并且调取完结果后会进行停止,无法将"haha"传递赋值给=号左边的temp,因此报错
10 Traceback (most recent call last):
11   File "<stdin>", line 1, in <module>
12 TypeError: can't send non-None value to a just-started generator
13 >>> t.send(None)  # 若要第一次调用就使用send,那么就传递None。send(None)等价于next()
14 0

 

1.3 生成器的特点总结

生成器是这样的一种函数,它记住上一次返回(调用)时所在函数体中的位置,而上次调用时的所有局部变量都保持不变。

生成器不仅记住了它的数据状态,还记住了它在流程控制构造(在命令式编程中,这种构造不只是数据值)中的位置。

生成器的特点:

  • 节约内存。
  • 迭代下一次调用时,函数内部所使用的参数都是从第一次调用时就开始保留下来的,而不是新创建的。

 

1.4 生成器的应用场景

使用生成器完成多任务(即协程):

 1 def test1():
 2     while 1:
 3         print("----1----")
 4         yield None    
 5     
 6 def test2():
 7     while 1:
 8         print("----2----")
 9         yield None
10         
11 t1 = test1()
12 t2 = test2()
13 
14 while 1:
15     next(t1)
16     next(t2)

执行结果:

----1----
----2----
----1----
----2----
----1----
----2----
……

 

2. 迭代器(Iterator)

可以被 next() 函数调用并不断返回下一个值的对象,统称为迭代器(Iterator)。迭代器是一个可以记住遍历的位置的对象。

迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完,迭代器只能往前访问,不能退后访问。

判断是否是迭代器对象

 1 >>> from collections import Iterator
 2 >>> g = (x for x in range(5))
 3 >>> type(g)
 4 <class 'generator'>
 5 >>> isinstance(g, Iterator)  # 生成器是迭代器对象
 6 True
 7 >>> isinstance([], Iterator)
 8 False
 9 >>> isinstance("abc", Iterator)
10 False

 

3. 可迭代对象(Iterable)

可以直接作用于 for 循环的对象统称为可迭代对象(Iterable)。大致有以下两种:

  • 一种是如 list、tuple、dict、set、str 等数据类型。
  • 一种是生成器(两种创建方式均是)。

判断是否是可迭代对象

 1 >>> from collections import Iterable
 2 >>> isinstance([], Iterable)
 3 True
 4 >>> isinstance({}, Iterable)
 5 True
 6 >>> isinstance("abc", Iterable)
 7 True
 8 >>> isinstance((x for x in range(5)), Iterable) 
 9 True
10 >>> isinstance(100, Iterable)
11 False

生成器不仅可以作用于 for 循环,还可以被 next() 函数不断调用并返回下一个值,直到最后抛出 StopIteration 错误表示无法继续返回下一个值了。

iter() 函数

生成器都是迭代器对象(Iterator),但 list、tuple 等数据类型虽然是可迭代对象(Iterable),但不是 Iterator。

若想将 list、tuple 等可迭代对象变成迭代器对象,可以使用 iter() 函数:

1 >>> isinstance(iter([]), Iterator)
2 True
3 >>> isinstance(iter({}), Iterator)
4 True
5 >>> isinstance(iter("abc"), Iterator)
6 True

 

4. 总结

  • 可迭代对象(Iterable):可以直接作用于 for 循环的对象。
  • 迭代器(Iterator):可以作用于 next() 函数的对象。例如生成器(generator)就属于迭代器对象。
  • list、tuple、str 等数据类型虽然是可迭代对象,但不是迭代器,不过可以使用 iter() 函数变成一个迭代器对象。

 

posted @ 2020-02-24 20:46  Juno3550  阅读(288)  评论(0编辑  收藏  举报