一、迭代器
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 定义了一个关键词:
例如:
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