再过60分钟你就能了解python中的迭代器和生成器啦

231 阅读10分钟

一、迭代器:

  • 用for循环迭代的对象都是可迭代对象Iterable
  • 用next()内置函数取值的对象都是迭代器Iterator类型,表示一个惰性计算的序列
  • 可迭代对象Iterable均可通过内置函数iter(),转化为一个迭代器Iterator
  • for循环内部实现机制就是先把可迭代对象用iter()转化为迭代器,再用next()从迭代器中取值

1.理解容器概念(container)

容器是一种把多个元素组织在一起的数据结构,容器中的元素可以逐个迭代获取,可以用in,not in关键字判断元素是否包含在容器中。通常这类数据结构把所有的元素存储在内存中(也有一些特殊的存在)。再Python中常见的容器对象:

1. list ,deque, ...
2. set, frozensets, ...
3. dict, defaultdict, OrderedDict, Counter, ...
4. tuple, nametuple, ...
5. str
1.可迭代对象赋予容器一种可以供提取元素的能力
2.不是所有的容器都是可迭代的

2.可迭代对象是一种通俗的称呼(iterable)

如果给定一个list或tuple,我们可以通过for循环来遍历这个list或tuple,这种遍历我们称为迭代(Iteration)

可迭代对向 并不局限于容器,如:files,sockets等(处于打开状态)。只要是可用i返回有一个可迭代的对象都可以称之为可迭代对象,这里写的我自己看都懵逼。。。 so 还是先看实例吧:

x = [1, 2, 3]
y = iter(x)

print(next(y))
print(next(y))

print(type(x), type(y))

>>>
1
2
<class 'list'> <class 'list_iterator'>

分析:

这里x是一个可迭代对象,可迭代对象和容器一样是一种通俗的称呼,并不是指某种具体的数据类型(敲黑板,重点理解,记住这句话)。list,set,dict都是可迭代对象而y则是一个独立的迭代器,迭代器内部有一个状态,该状态用于记录当前迭代所在的位置,以方便下次迭代的时获取正确的元素。迭代器有一种具体的迭代器类型:list_iterator,set_iterator, ...。 可迭代对象实现了__iter__()和__next__()方法(python2中是next()方法,python3.x是__next__()方法),这两个方法对应内置函数iter()和next()。__iter__方法返回可迭代对象本身,这使得它既是可迭代对象同时也是一个迭代器。

3.迭代器(iterator)是一种对象

迭代器(iterator)是一种对象,它能够用来遍历标准模板库容器中的部分或全部元素,每个迭代器对象代表容器中的确定的地址。迭代器修改了常规指针的接口,所谓迭代器是一种概念上的抽象:那些行为上像迭代器的东西都可以叫做迭代器。然而迭代器有很多不同的能力,它可以把抽象容器和通用算法有机的统一起来。

对比一下二者具备的方法:

可迭代对象必须具备:
__iter__()
  
迭代器具备:
__iter__()
__next__()

1.对于迭代器来讲,有一个__next__()就够了。在使用for循环时,程序会自动调用即将被处理的迭代器对象,然后使用next()函数,直到检测到一个StopIteration异常。 2.next()内置函数就是调用对象的方法__next__(),iter()内置函数是调用对象的__iter__()方法

光看概念谁能看懂啊,还是用代码验证下吧:

直接调用next()方法

>>> L = [1, 2, 3]
>>> next(L) 
Traceback (most recent call last):
  File "<input>", line 2, in <module>
TypeError: 'list' object is not an iterator

>>> L = [1, 2, 3]
>>> print(type(L))
<class 'list'>

L列表可以用for循环取值,不能用next()取值,因此判断L是可迭代对象iterable,而不是迭代器

先使用iter(),再调用next()再试试呢

>>> L = [1, 2, 3]
>>> I = iter(L) 
>>> next(I)
1
>>> next(I)
2
>>> next(I)
3
>>> next(I)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
StopIteration   #异常
>>> print(type(I))
<class 'list_iterator'>

当列表L通过iter()进行包装后,就变成迭代器了,就可以调用next()取值了

除了上述方式可以验证,还有相应的判断函数:

可迭代对象判断

>>> from collections import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance((), Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance(1000, Iterable)
False

迭代器判断

>>> from collections import Iterator
>>> isinstance([x for x in range(9)], Iterator)
False
>>> isinstance((x for x in range(9)), Iterator)
True
>>> isinstance(1000, Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance([], Iterator)
False
>>> isinstance((), Iterator)
False
>>> isinstance('abc', Iterator)
False

当使用iter()将可迭代对象进行转化后,就变为迭代器了,就能够满足判断条件:

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

4. for循环内部就是先iter()再next()

综上所述,显而易见: for循环内部就是先调用iter()把可迭代对象iterable变成迭代器iterator再进行循环迭代。

实际上如代码所示

L = [1, 2, 3]
for i in L:
    pass

I = iter(L) #获取迭代器对象
while True: #循环
    try:
        x = next(I)  #获取下一值
    except StopIteration:
        break   #捕捉到该异常退出循环

5. Python的Iterator对象表示的是一个数据流

此时,还有懵逼的地方:为什么list, dict, set, str等数据类型不是Iterator? 这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()内置函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration异常。可以把这个数据流看做是一个有序序列,但是不能提前知道这个序列的长度,只能不断通过next()内置函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时才会计算。Iterator可以看做是无限大的数据流,eg:全体自然数,而list,set, str等是不可能存储全部的自然数的。

扩展 Iterator继承自Iterable,用自带的help可以很方便的看到Iterator包含__iter__()和next()方法,而Iteratble仅仅包含__iter__()。

>>> from collections  import  Iterator, Iterable
>>> help(Iterator)
Help on class Iterator in module collections.abc:

class Iterator(Iterable)
 |  Method resolution order:
 |      Iterator
 |      Iterable
 |      builtins.object
 |  
 |  Methods defined here:
 |  
 |  __iter__(self)
 |  
 |  __next__(self)
 |      Return the next item from the iterator. When exhausted, raise StopIteration


>>> help(Iterable)
Help on class Iterable in module collections.abc:

class Iterable(builtins.object)
 |  Methods defined here:
 |  
 |  __iter__(self)

二、生成器

生成器是能够返回一个迭代器的函数,其最大的作用是将输入对象返回为一个迭代器。Python中使用了迭代的概念,是因为当需要循环遍历一个较大的对象时,传统的内存载入方式会消耗大量的内存,不如需要时读取一个元素的方式更为经济快捷。 有两点要先明确: 任意生成器都是迭代器(反之,不成立) 任意生成器,都是一个可以延迟创建值的工厂(可控性) 简单说生成器对象就是一种特殊的迭代器,满足迭代器协议,可以调用next()内置函数;对生成器for 循环时,调用iter()方法返回了生成器对象,然后再不断next()迭代,而iter()和next()都是在yield内部实现的。 生成器创建方式:常见两种(列表生成式、函数关键字)

1.如何创建生成器

1.第一种方法:将列表生成式中[]改成()

通过列表生成式,可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含百万元素的列表,不仅是占用很大的内存空间,如:我们只需要访问前面的几个元素,后面大部分元素所占的空间都是浪费的。

因此,没有必要创建完整的列表(节省大量内存空间)。在Python中,我们可以采用生成器:边循环,边计算的机制—>generator

>>> L = [x*x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x*x for x in range(10))
>>> g
<generator object <genexpr> at 0x0000028F8B774200>

我们该怎么打印元素呢?

刚刚提到生成器是特殊的迭代器,可以通过next()内置函数来获得generator的下一个返回值:

>>> g = (x*x for x in range(10))
>>> g
<generator object <genexpr> at 0x0000028F8B774200>
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
16
>>> next(g)
25
>>> next(g)
36
>>> next(g)
49
>>> next(g)
64
>>> next(g)
81
>>> next(g)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
StopIteration

generator是保存的算法,每次调用next()内置函数,才能计算出下一个元素,但一直这样,是不是有种崩溃的赶脚! 那种变态的方式肯定不能用的,正因为generator也是可迭代对象,我们可以使用for循环。

>>> g = (x*x for x in range(10))
>>> for i in g:
...     print(i)
...     
0
1
4
9
16
25
36
49
64
81

这里使用for循环,没有出现异常情况,是和迭代器一样的,都是因为for循环的内部机制。

4. 第二种方法:函数中使用关键字yield

简单的说就是在函数的执行过程中,yield语句会把你需要的值返回给调用生成器的地方,然后退出函数,下一次调用生成器函数的时候又从上次中断的地方开始执行,而生成器内的所有变量参数都会被保存下来供下一次使用。

著名的斐波那契数列用列表生成式写不出来,但用函数很简单:

>>> def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        print(b)
        a, b = b, a + b
        n = n + 1
    return 'done'
>>> fib(6)
1
1
2
3
5
8
'done'

斐波那契数列的推算规则非常类似generator,也就是说上面的函数和generator无限接近了,只需要把print(b)改成yield b就行:

>>> def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

>>> f = fib(6)
>>> f
<generator object fib at 0x0000022C041D4360>

状态:函数和generator执行流程不一样。函数时顺序执行,遇到return语句或者最后一行函数语句就结束,而generator在每次执行next()时,遇到yield语句返回,再次执行时会从上次返回的yield语句处继续执行。

举个简单的生成器例子:

>>> def odd():
    print('step 1')
    yield 1
    print('step 2')
    yield(3)
    print('step 3')
    yield(5)
>>> o = odd()
>>> next(o)
step 1
1
>>> next(o)
step 2
3
>>> next(o)
step 3
5
>>> next(o)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
StopIteration

三、总结

  1. 迭代器是一个更抽象的概念,任何对象,如果它的类有 next 方法和 iter 方法返回自己本身,对于 string、list、dict、tuple 等这类容器对象,使用 for 循环遍历是很方便的。在后台 for 语句对容器对象调用 iter()函数,iter()是 python 的内置函数。iter()会返回一个定义了 next()方法的迭代器对象,它在容器中逐个访问容器内元素,next() 也是 python 的内置函数。在没有后续元素时,next()会抛出一个 StopIteration 异常。

  2. 生成器(Generator)是创建迭代器的简单而强大的工具,生成器自动实现了“迭代器协议”(即__iter__和next方法)。生成器函数在需要返回数据的时候使用 yield 语句。每次 next()被调用时,生成器会返回它脱离的位置(它记忆语句最后一次执行的位置和所有的数据值),生成器在迭代的过程中可以改变当前迭代值(用send),而修改普通迭代器的当前迭代值往往会发生异常。使用生成器表达式取代列表解析可以同时节省内存。除了创建和保存程序状态的自动方法,当发生器终结时,还会自动抛出StopIteration 异常。