迭代器与生成器

文章发布时间:

最后更新时间:

文章总字数:
1.9k

预计阅读时间:
8 分钟

一.迭代器(iterator)

1.1 迭代器创建

迭代器只能往前取值,不会后退

  • 用iter函数可以将一个可迭代对象变成一个可迭代对象的迭代器

  • next函数可以获取迭代器的下一个值,取决于上一个next

1
2
3
4
5
6
arr = [1,2,3,4]
# 将列表转换为迭代器
a = iter(arr)
# 看看是啥样的
print(a)
print(type(a))

结果:

1
2
<list_iterator object at 0x0000014AB07015A0>
<class 'list_iterator'>

next用法:

1
2
3
4
5
6
# next用法
arr = [1,2,3,4]
a = iter(arr)
re = next(a)
print(re)
print(next(a))

结果:

1
2
1
2

  还有一个容易疏漏的点,next会固定当前迭代器中元素的角标,用for循环的话会直接从上一个next取的元素的下一个元素开始循环。说白了,for循环就是更多次数的next。迭代器是不可后退的,next或者for循环迭代完后再用next会报错,再用for里面的代码不会运行。

  而其他可迭代对象其实用for时运用了魔术方法,将其返回个迭代器,每for一次就调用一次魔术方法,所以这些可迭代对象(如列表)可以反复使用for循环。

1
2
3
4
5
6
7
8
9
10
L = [1,2,3,4,5,6]
l = iter(L)
print(next(l))
for i in l:
print(i)
print('-----------')

for i in l:
print(i)
print('-----------')

结果:

1
2
3
4
5
6
7
8
9
10
11
1
2
-----------
3
-----------
4
-----------
5
-----------
6
-----------

1.2 自定义迭可代对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A:
def __init__(self):
self.x = 1

# 自定义iter魔术方法,将对象返回为一个可迭代对象
def __iter__(self):
return self

# 自定义next魔术方法,返回迭代的下一个值
def __next__(self):
return 100

a = A()
for i in a:
print(i)

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
100 100

...

100 100 100 100

---------------------------------------------------------------------------
KeyboardInterrupt Traceback (most recent call last)
Cell In[9], line 15
13 a = A()
14 for i in a:
---> 15 print(i)

File c:\Users\Clocky7\anaconda3\envs\start1\Lib\site-packages\ipykernel\iostream.py:664, in OutStream.write(self, string)
655 def write(self, string: str) -> Optional[int]: # type:ignore[override]
656 """Write to current stream after encoding if necessary
657
658 Returns
(...) 662
663 """
--> 664 parent = self.parent_header
666 if not isinstance(string, str):
667 msg = f"write() argument must be str, not {type(string)}" # type:ignore[unreachable]

File c:\Users\Clocky7\anaconda3\envs\start1\Lib\site-packages\ipykernel\iostream.py:505, in OutStream.parent_header(self)
502 msg = "echo argument must be a file-like object"
503 raise ValueError(msg)
--> 505 @property
506 def parent_header(self):
507 try:
508 # asyncio-specific
509 return self._parent_header.get()

KeyboardInterrupt:

  自定义了iter魔术方法后,对象变成了一个可迭代对象,for循环会调用__next__方法,每次迭代都会返回自定义的返回值。由此可见,尽管是数字,只要自定义了iter魔术方法就可以让它变成一个可迭代对象。而next的返回值自定义后会作为每次迭代的返回值。for循环就很容易陷入死循环,因为next没有设置打断的条件。

接下来看看自定义next的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class A:
def __init__(self):
self.x = 1
def __iter__(self):
return self

# 自定义next魔术方法,给出迭代的下一个值
def __next__(self):
if self.x < 5:
self.x += 1
return self.x
# 并给出打断条件
else:
raise StopIteration


a = A()
# 将可迭代对象a转换为迭代器
iterator = iter(a)

print(next(iterator))
print(next(iterator))

#换个行
print()

# 来看看之前的死循环变成什么了
for i in iterator:
print(i)

# 猜猜为什么要写这一块代码
for el in a:
print(el)

结果:

1
2
3
4
5
2
3

4
5

  可见设置打断条件后for循环就会被打断,就跳出死循环了。而最后一块代码,我想说明的是,因为我们在next函数中每次迭代都让a++,不管是转换为迭代器还是就用b对象,实际上都是一个对象。其中的a值可能早就已经超出打断条件,因此无法继续迭代。

正确用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 正确用法

class A:
def __init__(self):
self.x = 1
def __iter__(self):
return self


def __next__(self):
if self.x < 5:
self.x += 1
return self.x

else:
# 在每次达到打断条件时重置x,达到可以重复迭代的效果
self.x = 1
raise StopIteration


a = A()
# 将可迭代对象a转换为迭代器
iterator = iter(a)

print(next(iterator))
print(next(iterator))

#换个行
print()


for i in iterator:
print(i)


#换个行
print()



# 又猜猜为啥写着行代码
print(next(iterator))




#换个行
print()




for el in a:
print(el)

结果:

1
2
3
4
5
6
7
8
9
10
11
2
3

4
5

2

3
4
5

  因为每次超出迭代范围后,跟迭代有密切相关的 self.x 被重置,因此就可以反复使用for循环了。但其本质还是只有一个对象,因此转换后的迭代器与对象a同时进行迭代时,这俩的进度是一样的,因为同一个对象a中 self.x 进度一致。或者也可以重新创建一个对象再进行迭代。

1.3 迭代器大题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class mydata:
pass

# 补全上面的代码

data = mydata([1,2,3,4,5,6,7,.......100])
data_loader = iter(data)


# 要求每次都返回新的10条数据,且为一个列表
re = next(data_loader)
print(re)

re1 = next(data_loader)
print(re1)

re2 = next(data_loader)
print(re2)

这个问题就类似日后会解决的机器学习中的处理图片,分批次处理而不是一次性传入。

下面是解答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class mydata:
def __init__(self,data,batch_size =10):

self.data = data
self.index = 0
# 这里的batch_size是每次迭代返回的样本数,默认为10
self.batch_size = batch_size

def __iter__(self):
return self

def __next__(self):
if self.index < len(self.data):
# 核心代码
self.index += 10
# 用切片实现批量读取并返回列表
return self.data[self.index-self.batch_size:self.index]
else:
self.index = 0
raise StopIteration

# 补全上面的代码

data = mydata([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30])
data_loader = iter(data)


# 要求每次都返回新的10条数据,且为一个列表
re = next(data_loader)
print(re)

re1 = next(data_loader)
print(re1)

re2 = next(data_loader)
print(re2)

# 再试试for循环
for i in data_loader:
print(i)

结果:

1
2
3
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
[11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
[21, 22, 23, 24, 25, 26, 27, 28, 29, 30]

最关键的就是通过切片返回一个列表,利用设置的batch_size进行操作,使得这个列表返回的元素都是新的10个。

二.生成器(Generator)

  • 是一种特殊的迭代器,通过函数定义,用 yield 语句生成值。生成器可以自动实现迭代协议,无需手动实现 iter() 和 next()

  • 比手动实现迭代器更易写

  • 惰性计算:生成器在每次调用 next() 时生成一个值,而不是一次性生成所有值

  • yield:暂停函数执行并其后的表达式结果,保留函数的状态,以便下一次继续执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def fn():

print(1)
# yield 表示其后的表达式作为这一次函数的执行结果,然后停止这个函数
yield 100

print(2)
# 第二次调用后则会从这里开始执行
yield 200

print(3)
yield 300
print("执行完毕")

f = fn()
print(next(f))

# 换个行
print()

print(next(f))

# 换个行
print()

print(next(f))

结果:

1
2
3
4
5
6
7
8
1
100

2
200

3
300

  由此可见,虽然生成器中没有 iter 和 next 方法,但是实际上,其是自动生成的。而且生成器天生就是一个迭代器,可以使用 next 方法。yield 出现时,其后的代码都暂时不执行,只返回yield 后面的值,当再次(进行迭代)调用 next 时,从 yield 后面的代码开始执行,直到遇到下一个 yield。