迭代器与生成器

一、迭代器

1.1 定义

在讲迭代之前,先搞清楚这些名词:

  • 循环(loop),指的是在满足条件的情况下,重复执行同一段代码。比如,while 语句。
  • 迭代(iterate),指的是按照某种顺序逐个访问列表中的每一项。比如,for 语句。
  • 递归(recursion),指的是一个函数不断调用自身的行为。比如,以编程方式输出著名的斐波纳契数列。
  • 遍历(traversal),指的是按照一定的规则访问树形结构中的每个节点,而且每个节点都只访问一次。

能够用循环语句之类的方法来一个一个读取元素的对象,就称之为可迭代的对象。

可迭代对象有:

一类是:list、tuple、dict、set、str,这些都是普通的迭代器。

一类是:generator,这个属于特殊的迭代器,又称生成器。

用来循环的如 for 就被称之为迭代工具。

用严格点的语言说:所谓迭代工具,就是能够按照一定顺序扫描迭代对象的每个元素(按照从左到右的顺序)。

显然,除了 for 之外,还有别的可以称作迭代工具。

下面介绍一个内置函数 iter()

iter()作用是获取可迭代对象的迭代器,常与 next() 配合使用。

lis=[1,2,3]
it=iter(lis)
while True:
   print(it.__next__())
// 运行一次,从 lis 中获取一个结果,按顺序执行。
1
2
3
StopIteration

// 以上还可以这样写:
while True:
   print(next(it))

当迭代对象 it 被迭代结束,即每个元素都读取了一遍之后,指针就移动到了最后一个元素的后面。如果再访问,指针并没有自动返回到首位置,而是仍然停留在末位置,所以报 StopIteration,想要再开始,需要重新载入迭代对象。所以,当我在上面重新进行迭代对象赋值之后,又可以继续了。这在 for 等类型的迭代工具中是没有的。

下面再看一个例子:

f = open(' 并发编程 / 协程 /file.txt', encoding='utf-8')
next(f)
大千世界,无奇不有。
next(f)
天地中央,有个曾用一剑劈出天河瀑布的读书人,人间最得意。
next(f)
东海崖畔,有个不愿飞升枯坐山巅的无名道人,只愿清风拂面。
next(f)
西方净土,有个喜欢给人说故事的老和尚,豢养有九条天龙。
next(f)
蛮荒南疆,有个目盲画师,驱使与山岳等高的金甲傀儡,搬动十万大山,铺就一幅锦绣图画。
next(f)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

由此可见,文件是天然的可迭代对象,不需要用 iter() 转换了。

1.2 应用

判断一个对象是否可迭代:

可以使用 isinstance() 判断一个对象是否是 Iterable 对象:

isinstance() 函数来判断一个对象是否是一个已知的类型,类似 type()。

from collections.abc import Iterable

In [1]: isinstance([], Iterable)
Out[1]: True

In [52]: isinstance({}, Iterable)
Out[2]: True

In [3]: isinstance('abc', Iterable)
Out[3]: True

In [4]: isinstance(100, Iterable)
Out[4]: False

iter() 与 next()

我们可以通过 iter() 函数获取这些可迭代对象的迭代器。然后我们可以对获取到的迭代器不断使用 next()函数来获取下一条数据。iter() 函数实际上就是调用了可迭代对象的 __iter__ 方法。

>>>li = [11, 22, 33, 44, 55]
>>>li_iter = iter(li)

>>>next(li_iter)
11

>>>next(li_iter)
22
>>> next(li_iter)
33
>>> next(li_iter)
44
>>> next(li_iter)
55
>>> next(li_iter)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

注意,当我们已经迭代完最后一个数据之后,再次调用 next()函数会抛出 StopIteration 的异常,来告诉我们所有数据都已迭代完成,不用再执行 next()函数了。

for…in…循环的本质

for item in Iterable 循环的本质就是先通过 iter() 函数获取可迭代对象 Iterable 的迭代器,然后对获取到的迭代器不断调用 next() 方法来获取下一个值并将其赋值给 item,当遇到 StopIteration 的异常后循环结束。

迭代器作用:

节省内存空间!

不用再将所有要迭代的数据都一次性缓存下来供后续依次读取,而是用到再读取。通过再次调用 next()读取下一个数据。

二、生成器

生成器(英文:generator)是一类特殊的迭代器。

我们在实现一个迭代器时,关于当前迭代到的状态需要我们自己记录,进而才能根据当前状态生成下一个数据。为了达到记录当前状态,并配合 next() 函数进行迭代使用,我们可以采用更简便的语法,即 生成器(generator)

2.1 简单的生成器

g = (x*x for x in range(4))
print(g)
// 输出结果
<generator object <genexpr> at 0x00000233A8DA2D68>

这是不是跟列表解析很类似呢?仔细观察,它不是列表,如果这样的得到的才是列表:

my_list = [x*x for x in range(4)]
print(my_list)
// 输出结果
[0, 1, 4, 9]

以上两的区别在于是 [] 还是 (),虽然是细小的差别,但是结果完全不一样。

g = (x*x for x in range(4))
print(list(g))
// 输出结果
[0, 1, 4, 9]

这样生成器又变成了列表!

对生成器调用 next()方法:

In [1]: next(g)
Out[1]: 0

In [2]: next(g)
Out[2]: 1

In [3]: next(g)
Out[3]: 4

In [4]: next(g)
Out[4]: 9

In [5]: next(g)
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-24-380e167d6934> in <module>()
----> 1 next(my_generator)

难道生成器就是把列表解析中的 [] 换成 () 就行了吗?这仅仅是生成器的一种表现形式和使用方法罢了,仿照列表解析式的命名,可以称之为“生成器解析式”(或者:生成器推导式、生成器表达式)。

2.2 高级生成器:

上述中通过生成器解析式得到的生成器,掩盖了生成器的一些细节,并且适用领域也有限。下面就要剖析生成器的内部,深入理解这个魔法工具。

python 定义了一个关键词:

yield
作为生成器的标志。

例如:

def g():
    yield 0
    yield 1
    yield 2
gen=g()
print(gen)
// 输出结果
<generator object g at 0xb7200edc>

使用了 yield 代替了return,就把函数 g 变成了一个生成器。

查看生成器中的方法:

dir(gen)
输出如下
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'next', 'send', 'throw']

在这里看到了__iter__()next(),也可以说明它是迭代器。

我们把含有 yield 语句的函数称作生成器。生成器是一种用普通函数语法定义的迭代器。通过上面的例子可以看出,这个生成器(也是迭代器),在定义过程中并没有像上节迭代器那样写 __inter__()next(),而是只要用了 yield 语句,那个普通函数就神奇般地成为了生成器,也就具备了迭代器的功能特性。

2.2.1 yield 与 return 区别

首先看一个普通含 return 的函数:

def r_return(n):
    print(" 进入了函数 ")
    while n > 0:
        print(" 返回内容之前 ")
        return n
        n -= 1
        print(" 返回内容之后 ")

r=r_return(3)
print(r)
// 输出结果如下
进入了函数
返回内容之前
3

遇到 return 关键词之后,函数体就停止运行了。

下面将 return 改为 yield

def y_yield(n):
    print(" 进入了函数 ")
    while n > 0:
        print(" 返回内容之前 ")
        yield n
        n -= 1
        print(" 返回内容之后 ")

r = y_yield(3)
print(next(r))
print(next(r))
print(next(r))
// 输出结果如下

进入了函数
返回内容之前
3

返回内容之后
返回内容之前
2

返回内容之后
返回内容之前
1

一般的函数,都是止于 return。作为生成器的函数,由于有了 yield,则会遇到它挂起,如果还有 return,遇到它就直接抛出 SoptIteration 异常而中止迭代。

程序执行的逻辑是:遇到 yield 即返回 n,并挂起等待,直到在执行下一个next() 时,再从挂起的地方继续开始运行……

2.2.2 接收参数的生成器

再看一个例子深入了解生成器的工作原理:

def foo():
    print("starting...")
    while True:
        res = yield ' 到我这了 '
        print("res:", res)

f = foo()
print(next(f))
print("---" *10)
print(next(f))

// 输出结果如下:
starting...
到我这了
------------------------------
res: None
到我这了

它的运行逻辑是:

从Ⅰ部分开始运行,第 1 行将函数实例化,再依次运行第 2 行,next(f) 即运行一次 f, f 就是 foo() ,foo() 里面有 yield ,因此他就是一个生成器,跳转到第Ⅱ部分,到 foo() 函数里面,运行第 1 行,输出 “starting…’’ ,接下来进入到 while 循环里面, 执行第 2 行,yield 返回一个 ‘’到我这了’’ 直接给到 foo() , 输出 ‘’到我这了’’,而 res 只能接受到 None ,此时不再进一步执行,yield 挂起,在第 2 行执行完毕被挂起,第一个 next(f)执行完毕。

回到第Ⅰ部分,执行第 3 行,输出 ‘’——————————‘’ ,接着执行第 4 行,又是一个 next(f) ,再跳到第Ⅱ部分,直接进入到第 3 行,(从哪挂起,从哪里开始),输出 ‘’res: None’’ ,while True 还是成立,继续在循环体内运行,执行第 2 行,将 ‘’到我这了’’ yield 出去给到 foo() , 输出 ‘’到我这了’’,res 又只能接受到 None ,此时不再进一步执行,yield 挂起,第二个 next(f)执行完毕。

最后:程序结束。

再看一个例子:

def yield_test(n):
    yield n*2
    print('n=' , n)
    print('do something.')
    print('end.')

#调用生成器
for i in yield_test(5):
    print(i, ',')
    print('next')

// 输出结果如下:
10 ,
next
n= 5
do something.
end.

如果你已经明白了第一个例子,那么这个就不难理解了!

这个循环体只执行了两次。

只有触发调用生成器返回值 yield 表达式时,才会在生成器内部产生位置记录和循环迭代(如果有可供循环的可迭代值的话),如果调用生成器 yield_test()返回值 yield 表达式时,生成器 yield_test()的返回值 yield 表达式只有一次可迭代值,因为生成器可迭代值只能迭代一次的特性,生成器将不再循环,继续执行生成器剩余代码直至结束。


欢迎各位看官及技术大佬前来交流指导呀,可以邮件至 jqiange@yeah.net