预备知识:Python规定的 迭代协议 与 迭代器协议
➹ 迭代协议:若对象定义了一个__iter__ 方法且__iter__ 方法的返回值是一个迭代器,即实现了迭代协议。
➹ 迭代器协议:若对象定义了一个__iter__ 方法和一个__next__ 方法,即实现了迭代器协议。其中,
- 对象为实现了迭代协议的对象,即__iter__ 方法的返回值是一个迭代器。
- __next__ 方法在迭代完所有数据之后,会抛出SyopIteration的错误信息。
1.定义
名称 |
|
|
---|---|---|
容器 container | ● 把多种元素组织在一起的数据结构,容器中的元素就可以逐个迭代获取。 ● 并不是一种数据类型,只是一种概念。 | 列表(list)、元组(tuple)、字符串(str)、字典(dict)、集合(set ) |
可迭代对象 iterable | ● 可迭代对象是一个类的实例对象。 ● 此类定义了__iter__()方法, __iter__()方法返回一个迭代器对象。 | 列表(list)、元组(tuple)、字符串(str)、字典(dict) |
迭代器 iterator | ● 定义一个类,此类的实例对象即为迭代器对象。 ● 类中定义了__iter__()和__next__()两个方法。 __iter__()方法返回对象本身,即 self。 __next__()方法每次返回下一个数据,如果没有数据了就抛出StopIteration异常。 | ①创建的迭代器对象 ②调用内置函数 iter() 返回一个迭代器【如:iter(可迭代对象)】 ③其他内置方法,如通过使用zip() 、enumerate()、map()、filter() 和 reversed()获得迭代器等等 |
生成器 generator | ● 包含了yield关键字的函数即为生成器函数。 ● 调用此生成器函数时将自动创建生成器对象。 【yield根据生成器类generator创建对象,生成器类的内部声明了__iter__()和__next__()方法】 PS:生成器也可认为是一种特殊的迭代器,可以把它看成迭代器的子类,是实现自己独有方法的迭代器。 | ①调用生成器函数时自动创建的生成器对象 |
2.示例与探究
自定义可迭代对象、迭代器、生成器示例代码如下:
"""定义可迭代对象的类"""
class Foo(object):
def __iter__(self):
return IT() #也可以返回生成器对象
"""定义迭代器对象的类"""
class IT(object):
def __init__(self):
self.counter = 0
def __iter__(self):
return self
def __next__(self):
self.counter += 1
if self.counter == 3:
raise StopAsyncIteration()
return self.counter
"""定义生成器函数"""
def func():
yield 1
yield 2
上述代码在以下介绍中会继续使用。
2.1可迭代对象
2.1.1 可迭代对象 使用for进行循环的原理
观察以下代码,
"""创建可迭代对象 列表"""
nums = [11, 22, 33]
for num in nums:
print(num)
运行结果:
11
12
13
"""创建可迭代对象 Foo()"""
obj = Foo()
for item in obj:
print(item)
运行结果:
1
2
两个例子都可以观察到通过for循环,可迭代对象的元素逐个被打印出来。
思考:为什么可迭代对象仅有__iter__()方法而没有定义__next__()方法,却实现了逐个输出元素的功能?
由于可迭代对象有__iter__()方法,__iter__()方法返回一个迭代器对象。使用for循环时,内部先执行被迭代对象(即可迭代对象)的__iter__()方法得到返回的一个迭代器对象,然后将此迭代器对象传入内置函数next()方法中,不断执行next(迭代器对象)
,逐个输出元素。ps:使用内置的 next(迭代器对象) 方法,这个方法会调用迭代器对象中对应的__next__()方法。(其他内置函数原理一致)
同理,当使用for循环遍历迭代器对象元素时,内部先执行被迭代对象(即迭代器对象)的__iter__()返回了对象自身再调用next函数,此过程等价于直接执行了next函数。
2.1.2 可迭代对象 通过iter()和next()输出元素
除使用for循环以外,可迭代对象也可以通过内置函数iter()和next()输出元素。
- 内置函数 iter() 的本质是 __iter__() ,也是返回一个迭代器。
- 内置函数 next() 的本质是 __next__(),也是有了迭代器之后获取元素。
示例如下:
"""创建可迭代对象 Foo()"""
obj = Foo()
"""创建迭代器"""
it = iter(obj)
"""输出元素"""
print(next(it))
print(next(it))
print(next(it))
运行结果:
1
2
Traceback (most recent call last):
File "F:/xx.py", line 30, in <module>
print(next(it))
File "F:/xx.py", line 17, in __next__
raise StopAsyncIteration()
StopAsyncIteration
2.2 迭代器对象
2.2.1 迭代器对象 分别通过__next__()和next()遍历
由于迭代器类型的__iter__()方法返回自身,__next__()方法接收自身为参数;另外,内置函数next()也是只需传入迭代器对象作为参数,本质就是__next__()方法,因此 迭代器对象不需要通过自身函数__iter__()或者内置函数iter()做一次转换,可通过自身函数__next__()方法和内置函数next()直接输出元素。示例如下:
①通过自身函数__next__()方法
"""创建迭代器对象 IT()"""
obj1 = IT()
print(obj1.__next__())
print(obj1.__next__())
print(obj1.__next__())
运行结果如下:
1
2
Traceback (most recent call last):
File "F:/xx.py", line 30, in <module>
print(next(it))
File "F:/xx.py", line 17, in __next__
raise StopAsyncIteration()
StopAsyncIteration
②内置函数next()方法
"""创建迭代器对象 IT()"""
obj1 = IT()
print(next(obj1))
print(next(obj1))
print(next(obj1))
运行结果如下:
1
2
Traceback (most recent call last):
File "F:/xx.py", line 30, in <module>
print(next(it))
File "F:/xx.py", line 17, in __next__
raise StopAsyncIteration()
StopAsyncIteration
2.2.2 为什么迭代器要实现__iter__()?
为什么迭代器的__iter__()方法 仅返回迭代器自身而不做任何处理 却依然要实现?
根据官方文档描述:
Iterators are required to have an __iter__() method that returns the iterator object itself so every iterator is also iterable and may be used in most places where other iterables are accepted.
综上,之所以迭代器(Iterator)也要求实现__iter__(),是因为很多地方接收的参数是一个可迭代对象(Iterable),如果所有的Iterator都是Iterable,那么这些用Iterable地方都可以无障碍地使用Iterator了。
2.2.3 迭代器是消费型的,怎么理解?
迭代器是有状态的,只能遍历一次,是“消费型”的,不可以“二次消费”;不属于迭代器的可迭代对象是没有状态的,每一次对可迭代对象调用iter()都会得到一个新的迭代器。示例如下:
a = iter([1,2,3])
print(a.__next__()) # 输出第一个元素
print(a.__next__()) # 输出第二个元素
print(a.__next__()) # 输出第三个元素
a = tuple(a) # 转换为元组
print("转换后的元组:",a)
运行结果如下:
1
2
3
转换后的元组: ()
通过上面__next__()方法遍历生成器对象以后,如果还需要使用该生成器对象的话,必须创建新的生成器对象!因为遍历以后,原来的生成器对象已经不存在了,输出的新元组为空。
2.3 生成器对象
2.3.1 生成器对象的创建
①自定义生成器对象创建
"""创建生成器对象 func()"""
obj2=func()
print(next(obj2))
print(next(obj2))
print(next(obj2))
运行结果如下:
Traceback (most recent call last):
File "F:/xx.py", line 9, in <module>
print(next(obj2))
StopIteration
1
2
②生成器推导式创建对象
生成器推导式的语法格式为:generator = (Expression for var in range)
,示例如下:
a = (i for i in range(3))
print(a.__next__())
print(a.__next__())
print(a.__next__())
运行结果:
0
1
2
补充:列表推导式、生成器推导式、元组推导式
★ 列表推导式:可以快速生成一个新的列表,或者根据某个列表生成满足指定需求的列表。语法格式为:list = [Expression for var in range]
★ 生成器推导式:可以快速生成一个新的生成器对象,或者根据某个可迭代对象生成满足指定需求的生成器对象。语法格式为:generator = (Expression for var in range),示例如下:a1 = (i for i in range(10)) #生成一个10以内的生成器 a2 = (i for i in range(10) if i % 2 == 0) #生成一个10以内的生成器 print(a1) print(a2)
运行结果如下:
<generator object <genexpr> at 0x000001DC03E3C900> <generator object <genexpr> at 0x000001DC03DBBD10>
★ 元组推导式:没有此概念! 如果想要使用推导式生成元组,使用 tuple() 函数将生成器推导式对象转换成元组,示例如下:
a1 = (i for i in range(10)) #生成一个10以内的生成器 a2 = (i for i in range(10) if i % 2 == 0) #生成一个10以内的生成器 print("转换后的元组为:",tuple(a1)) #转换为元组 print("转换后的列表为:",list(a2)) #转换为列表
运行结果为:
转换后的元组为: (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) 转换后的列表为: [0, 2, 4, 6, 8]
2.3.2 yield的运行原理 和 send()的使用
生成器使用 yield语句返回一个值,yield语句挂起该生成器函数的状态,保留足够的信息。对生成器函数的第二次(或第n次)调用,会跳转到函数上一次挂起的位置。因此生成器不仅“记住”了它的数据状态,还记住了程序执行的位置。
yield和return的区别在于:return是在程序中返回某个值,返回之后程序就不再往下运行了;而yield虽然在程序中也返回了某个值,但是挂起了此时的执行状态和执行位置,在后续代码出现next()函数调用时,继续从yield挂起的地方往下执行。
例子①
def foo():
print("starting...")
while True:
res = yield 4
print("res:",res)
g = foo()
print(next(g))
print("*"*20)
print(next(g))
运行结果如下:
starting...
4
********************
res: None
4
运行过程分析:
----------------------------主程序执行开始----------------------------
-
g = foo()
:因为foo函数中有yield关键字,foo函数并不会真的执行,而是先得到一个生成器g。 -
print(next(g))
:next(g) 时foo函数正式开始执行
☆print("starting...")
,输出 starting…
☆ 进入while进行第一轮循环,执行res = yield 4
,yield返回一个4(注意这里是返回而不是对res赋值,yield类似return,因此只执行了右边的yield 4
)。
----------------------------foo函数执行挂起---------------------------- -
print("*"*20)
,输出 ******************** -
print(next(g))
,next(g) 时从foo函数上面暂停位置继续执行
-----------------------foo函数挂起位置继续执行-----------------------
☆res = yield 4
,因为上述yield 4
已经将 4 返回出去了,并没有赋值给 res,只执行了左边的的res =
,因此 res 为 None
☆print("res:",res)
,输出 res: None
☆ 进入while进行第二轮循环,执行yield 4
,yield返回一个4
----------------------------foo函数执行再次挂起----------------------------
----------------------------主程序执行完毕----------------------------
例子②
生成器有三个独有的方法:send(往生成器里传入数据)、throw(往生成器里面抛异常)、close(关闭生成器)。
生成器的send方法先给生成器传值,再执行和next相同的操作( 从上次暂停的地方开始,执行到下一个yield ) 。把上例的最后一行换成 print(g.send(7))
,观察下例:
def foo():
print("starting...")
while True:
res = yield 4
print("res:",res)
g = foo()
print(next(g))
print("*"*20)
print(g.send(7))
运行结果如下:
starting...
4
********************
res: 7
4
运行结果分析:
例子①和例子②的前面代码的运行过程大致相同,不同的是例子①的运行分析步骤4部分。例子①的运行分析步骤4中提到,yield 4
已经将4返出去,并没有赋值给res;而例子②的send函数是发送一个参数值给res后调用next()方法。例子②的运行过程为:
----------------------------主程序执行开始----------------------------
-
g = foo()
:因为foo函数中有yield关键字,foo函数并不会真的执行,而是先得到一个生成器g。 -
print(next(g))
:next(g) 时foo函数正式开始执行
☆print("starting...")
,输出 starting…
☆ 进入while进行第一轮循环,执行res = yield 4
,yield返回一个4(注意这里是返回而不是对res赋值,yield类似return,因此只执行了右边的yield 4
)。
----------------------------foo函数执行挂起---------------------------- -
print("*"*20)
,输出 ******************** -
print(g.send(7))
,send(7) 时从foo函数上面暂停位置继续执行
-----------------------foo函数挂起位置继续执行-----------------------
☆res = yield 4
,因为上述yield 4
已经将 4 返回出去了,并没有赋值给 res,但 send会把7这个值赋值给res,即理解为res = 7
。由于send方法中包含next()方法,所以程序会继续向下运行。
☆print("res:",res)
,输出 res: 7
☆ 进入while进行第二轮循环,执行yield 4
,yield返回一个4
----------------------------foo函数执行再次挂起----------------------------
----------------------------主程序执行完毕----------------------------
2.3.3 yield 和 yield from 的区别
观察下列yield 和 yield from代码的运行结果:
s_list = ['a','b','c']
def generator_1(li):
yield li
def generator_2(li):
yield from li
print("yield 产生的结果")
counter = 0
for i in generator_1(s_list):
counter += 1
print(i)
print(counter)
print("yield from 产生的结果")
counter = 0
for i in generator_2(s_list):
counter += 1
print(i)
print(counter)
运行结果:
yield 产生的结果
['a', 'b', 'c']
1
yield from 产生的结果
a
b
c
3
分析:可以观察到,yield直接返回了列表s_list,且counter值为1,意味着for循环只循环了一次;而yield from逐个遍历了列表s_list的元素,且counter值为3。
因此yield和yield from后加可迭代对象时,yield from是将可迭代对象中的元素一个一个yield出来,而yield是直接yield可迭代对象。
2.4 迭代器和生成器的区别
- 迭代器是访问容器的一种方式,也就是说 容器已经出现。我们是从已有元素拓印出一份副本,只为我们此次迭代使用(从有到有的复制)。
- 生成器是自己生成元素的(从无到有的生成)。
3.判断可迭代对象、迭代器类型的方法
通过导入collections下的Iterable可迭代对象和Iterator迭代器类型,结合isinstance方法即可进行判断。示例如下:
from collections.abc import Iterable,Iterator
xxx='abcdec' #字符串是可迭代对象
print(isinstance(xxx,Iterable))
print(isinstance(xxx,Iterator))
it_xxx = iter(xxx) #转换为迭代器
print(isinstance(it_xxx,Iterable))
print(isinstance(it_xxx,Iterator))
运行结果为:
True
False
True
True
4. 应用场景
4.1 迭代器:节省内存的开销
文件对象读取有三种常用的方法:readlines、readline、迭代器读取。分别用这三种方法读取如下 script1.py文件
import sys
print(sys.path)
x = 2
print(2 ** 33)
① readlines:会将整个文件加载到内存中。常搭配for循环使用,示例如下:
for line in open('script1.py').readlines():
print(line.upper(),end='')
运行结果如下:
IMPORT SYS
PRINT(SYS.PATH)
X = 2
PRINT(2 ** 33)
但是此方法在文件较大时,往往会引发 MemoryError 内存溢出!
② readline:每次调用readline方法时,就会从文件中读取一行文本,到达文件末尾时,就会返回空字符串。
● 交互模式逐行读取文件:
>>>f = open('script1.py')
>>>f.readline()
import sys\n'
>>>f.readline()
print(sys.path)\n'
>>>f.readline()
x = 2\n'
>>>f.readline()
print(2 ** 33)\n'
>>>f.readline()
''
● 搭配循环逐行读取:
f = open('script1.py')
while True:
line = f.readline()
if not line:
break
print(line.upper(),end='')
运行结果如下:
IMPORT SYS
PRINT(SYS.PATH)
X = 2
PRINT(2 ** 33)
③迭代器读取
文件对象是一个迭代器,支持迭代协议,通过__next__()方法可逐行读取。
● 交互模式下,每次调用__next__()方法
>>>f = open('script1.py')
>>>f.__next__()
import sys\n'
>>>f.__next__()
print(sys.path)\n'
>>>f.__next__()
x = 2\n'
>>>f.__next__()
print(2 ** 33)\n'
>>>f.__next__()
Traceback (most recent call last):
...more exception text omitted...
StopIteration
● 搭配for循环逐行读取
for line in open('script1.py'):
print(line.upper(),end='')
运行结果如下:
IMPORT SYS
PRINT(SYS.PATH)
X = 2
PRINT(2 ** 33)
搭配循环的 readline方式与迭代器方式 比较:readline方式可能运行得更慢一些,因为迭代器在Python中是以C语言的速度运行的,而while循环方式则是通过Python虚拟机运行Python字节码的。一般把Python代码换成C程序代码,速度都更快。
4.2 生成器:生产者与消费者模型
模拟实现一个苹果公司生产手机以及消费者购买手机的 并发过程,这个模型的三要素是 生产者、消费者、缓冲区 ,缓冲区选用队列,其工作方式是先进先出。
import time
import random
cacheList = []
# # 设置缓冲区的最大长度, 当缓冲区到达最大长度, 那么生产者就不能再生产了
cacheListLen = 2
def is_full():
"""
判断缓冲区队列是否已经满了
"""
return len(cacheList) == cacheListLen
def Producer(name):
"""
生产者, 主要用于生产数据
"""
while True:
if not is_full():
print("生产者[%s]正在生产苹果手机....." %(name))
# 模拟生产者生产数据需要的时间, 随机休眠0~1秒,
time.sleep(random.random())
print("[%s] 已经生产苹果手机完成" %(name))
# 将生产的游戏机放入缓冲区
cacheList.append('苹果手机')
else:
print("缓存已满!请停止生产")
yield
def Consumer(name):
"""
消费者, 用于处理/消费数据
"""
print("【%s】正在准备购买苹果手机" %(name))
while True:
item = yield
print("【%s】购买%s成功" %(name,item))
if __name__ == "__main__":
# producer是一个生成器
producer = Producer("苹果公司")
next(producer)
# 列表生成式, 生成消费者
consumers = [Consumer("消费者%s" %(i+1)) for i in range(3)]
# 依次遍历所有的消费者, 给提供手机
for consumer in consumers:
if not cacheList:
print("目前商店没有手机库存.....")
else:
#如果缓冲区不满,先生产,再提供消费
if not is_full():
next(producer)
item = cacheList.pop(0)
next(consumer)
consumer.send(item)
else:
item = cacheList.pop(0)
next(consumer)
consumer.send(item)
运行结果如下:
生产者[苹果公司]正在生产苹果手机.....
[苹果公司] 已经生产苹果手机完成
生产者[苹果公司]正在生产苹果手机.....
[苹果公司] 已经生产苹果手机完成
缓存已满!请停止生产
【消费者1】正在准备购买苹果手机
【消费者1】购买苹果手机成功
生产者[苹果公司]正在生产苹果手机.....
[苹果公司] 已经生产苹果手机完成
缓存已满!请停止生产
【消费者2】正在准备购买苹果手机
【消费者2】购买苹果手机成功
生产者[苹果公司]正在生产苹果手机.....
[苹果公司] 已经生产苹果手机完成
缓存已满!请停止生产
【消费者3】正在准备购买苹果手机
【消费者3】购买苹果手机成功
该模型的好处:
- 实现了生产者与消费者的解耦和
- 平衡了生产力与消费力,就是生产者一直不停的生产,消费者可以不停的消费,因为二者不再是直接沟通的,而是跟队列沟通的。
5.补充
5.1 iter()、 __iter__()与__getitem__()的关系
- Iter()是产生迭代器的内置函数,凡是 定义有__iter__()函数的对象 或者 支持序列访问协议,也就是定义有__getitem__()函数的对象 皆可以通过 iter()内置函数 产生迭代器(iterable)对象。可迭代对象常通过iter()工厂函数产生迭代器。
- __iter__()是迭代协议函数,凡是实现__iter__方法的对象,皆是可迭代对象。迭代器对象可直接通过自身定义的__next__()函数逐个输出元素。
- __getitem__()方法可以通过使用下标获取元素(切片操作),单独实现__getitem__()的类,实例是一个可迭代对象,但不是Iterable类型。
①单独实现__getitem__()的类
单独实现__getitem__()的类,实例化一个可迭代对象。因此可以用for进行循环和内置函数iter()产生迭代器,如下例:
class Library(object):
def __init__(self):
self.books = [1, 2, 3]
self.index = -1
def __getitem__(self, i):
return self.books[i]
l = Library()
print(l[1])
print("-"*10)
for i in l:
print(i)
print("-"*10)
it = iter(l)
print(next(it))
运行结果为:
2
----------
1
2
3
----------
1
②当__iter__()、__getitem__()同时存在
class Library(object):
def __init__(self):
self.books = [1, 2, 3]
self.index = -1
def __getitem__(self, i):
return self.books[i]
def __iter__(self):
return self
# #
def __next__(self):
self.index += 1
if self.index > len(self.books)-1:
raise StopIteration()
return self.books[self.index]
l = Library()
print(next(l))
print("-"*10)
for i in l:
print(i)
运行结果为:
1
----------
2
3
可以看到,刚开始调用next()函数先输出了 1,使得调用__iter__函数返回的迭代器元素从第二位开始;后面通过for循环时依次输出的是后面元素,得知__iter__()优先于__getitem__()返回迭代器。
5.2 range与xrange
在Python 2中,range() 生成的是列表,xrange()是生成器;而在Python 3中 xrange()已不存在,而range() 生成的是可迭代对象。在此就Python 3对 range()展开讨论。
5.2.1 range对象是什么?
range()不是一个函数,而是一个可迭代对象,验证如下:
print(range(1,3))
from collections.abc import Iterable,Iterator
print(isinstance(range(5),Iterable))
运行结果为:
range(1,3) # 如果range()是函数,返回结果应该是0,1,2
True
函数语法:range(start, stop[, step])
参数说明
- start: 计数从 start 开始。默认是从 0 开始。例如range(5)等价于range(0, 5)
- stop: 计数到 stop 结束,但不包括 stop。例如:range(0, 5) 是[0, 1, 2, 3, 4]没有5
- step:步长,默认为1。例如:range(0, 5) 等价于 range(0, 5, 1)
注意点
(1)它表示的是 左闭右开 区间。
(2)它接收的 参数必须是整数,可以是负数,但不能是浮点数等其它类型。
(3)它是 不可变 的序列类型,可以进行判断元素、查找元素、切片等操作,但不能修改元素。
(4)它是 可迭代对象,但不是迭代器。
在 for 循环 遍历时,可迭代对象与迭代器的性能是一样的,即它们都是惰性求值的,在空间复杂度与时间复杂度上并无差异。不同的是可迭代对象不支持自遍历(即next()方法),而迭代器本身不支持切片(即__getitem__() 方法)。
代码示例
for n in range(5):
print(n) # 输出:0 1 2 3 4
5.2.2 range()是什么类型?
根据官方文档,有三种基本的序列类型:列表、元组和范围(range)对象。
range()是不可变的序列类型,可以进行判断元素、查找元素、切片等操作,但不能修改元素,也不支持加法拼接和乘法重复。
a = range(8)
print(a[0]==10) # 判断元素
print(a[0]) # 查找元素
print(a[2:5]) # 切片
a[2]=4 # 赋值
print(a[2])
运行结果为:
False
0
range(2, 5)
Traceback (most recent call last):
File "D:\main.py", line 5, in <module>
a[2]=4 # 赋值
TypeError: 'range' object does not support item assignment
思考:同样是不可变序列,字符串和元组支持进行加法拼接与乘法重复,为什么 range 不支持呢?
根据官方文档: range 对象仅仅表示一个遵循着严格模式的序列,而重复与拼接通常会破坏这种模式。range 相当于一个等差数列,拼接两个等差数列或重复拼接一个等差数列有违 range 本身的语法模式。
参考
15分钟彻底搞懂迭代器、可迭代对象、生成器【python迭代器】
python iter()与 __iter__()的区别
python3中的range返回的是迭代器吗
【Python】__iter__和__getitem__区别
python中yield的用法详解——最简单,最清晰的解释
python中生成器与迭代器到底有什么区别?一文带你彻底搞清楚
Python教程:Python中的元组推导式详解
python3range函数采用迭代器方式的好处_range函数用法完全解读
python中yield和yield from的区别(附代码理解)
python系列教程157——文件迭代器