0%

迭代器与生成器

大约第三次看迭代器和生成器了,之前一直看得云里雾里,今天还是来彻底了断下。2022 年 5 月 2 日回来复习,比之前理解的好了一些。

生成器

在这之前,很多人没了解过 yield 关键字,先通过 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
class Countdown:
def __init__(self, start):
self.start = start

# Forward iterator
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1

# Reverse iterator
def __reversed__(self):
n = 1
while n <= self.start:
yield n
n += 1

# 反向迭代
for rr in reversed(Countdown(3)):
print(rr)

# 正向迭代
for rr in Countdown(3):
print(rr)

在了解 yield 的用法之后,来看一下生成器:

  • 生成器函数是一种特殊的函数;如果一个函数包含 yield 表达式,那么它是一个生成器函数;调用它会返回一个生成器。即将普通函数的 return 换成了 yield,仅在访问时有返回值,节省空间。但与普通函数不同的是,生成器函数返回的生成器只能用于迭代操作。
  • 生成器表达式也能创建生成器,如:g = (x for x in range(12))g 就是生成器,相当于把列表推导式的中括号换成了小括号,避免实例化。
1
2
3
4
5
6
7
8
9
10
11
def countdown(n):
while n > 0:
yield n
n -= 1
print('Done')

# a 是一个生成器
a = countdown(6)
# 一旦生成器函数返回退出,迭代终止
for i in a:
print(i)

现在假设,我们建立一个函数,这个函数会返回一个巨大的列表,而我们需要逐个访问。那么,如果函数生产列表中的每一个元素都需要耗费非常多的时间,或者生成所有元素需要等待很长时间,且,操作列表时会浪费巨大的内存。但使用 yield 把函数变成一个生成器函数,每次只产生一个元素,就能节省很多时间和空间的开销了。

常见的还有yield from关键字,关键字后的参数是一个可迭代对象,可以简单理解为如下的替换:

1
2
3
4
yield from x
# 等价
for i in x:
yield i

迭代器

先区分下容易搞混的一些概念:

  • 迭代器是一种对象,如 iter(f) 后得到迭代器,可以 next() 来读取下一个元素;
  • 迭代是一种操作,如 for 循环访问列表元素,可以理解为迭代;
  • 可迭代是对象的一种特性,如字典、列表和字符串等;
  • 迭代器协议指的是类需要包含一个方法 __iter__,该方法能返回一个能够逐个访问容器内所有元素的迭代器,则该容器类实现了迭代器协议。

那么我们可以根据概念先做一些简单的实验:

1
2
3
4
5
6
7
# 错误,列表不是迭代器,是可迭代对象
# 具体的类型不是迭代器,是可迭代对象。
ls = [1, 2, 3, 4]
next(ls)
# 正确,迭代器
ls = iter(ls)
next(ls)

先得到一个简单的结论,可作用于 next() 函数的对象都是迭代器类型,它们表示一个惰性计算(不在创建的时候计算所有值,而是在需要的时候才计算)的序列。

继续来看一段简单的代码,一个类提供了 __iter__ 方法,该方法能返回一个能逐步访问类中所有元素的迭代器,实现了迭代器协议,那么可以说把一个类作为一个迭代器,实例化的对象就是可迭代对象。

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
# __iter__ 方法支持迭代
class Node:
def __init__(self, value):
self._value = value
self._children = []

def __repr__(self):
return "Node({!r})".format(self._value)

def add_child(self, node):
self._children.append(node)

# 将迭代请求传递给内部的 _children 属性。
# 调用 _children.__iter__() 方法来返回对应的迭代器对象
# iter() 内建方法可以把 list、dict、str 等可迭代对象转换成迭代器
# 迭代时调用列表的 __next__()
def __iter__(self):
return iter(self._children)


root = Node(0)
child1 = Node(1)
child2 = Node(2)
root.add_child(child1)
root.add_child(child2)
# 列表迭代器
print(type(iter(root)))
for ch in root:
print(ch)

稍微总结下,如果一个类想被用于 for ... in 循环,类似 list 那样,就必须实现一个 __iter__ 方法,该方法返回一个迭代器。然后 for 循环就会通过 next(迭代器) 不断调用迭代对象的 __next__ 方法拿到循环的下一个值,直到遇到 StopIteration 错误时退出循环。以斐波那契数列为例,写一个 Fib 类并实例化一个可迭代对象,不在依赖列表的迭代方法,可以作用于 for 循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1 # 初始化两个计数器a,b

# 这个类实现了 next 方法,因此 iter 返回自己
# 因为实例化的对象本身可迭代
def __iter__(self):
return self

def __next__(self):
# 计算下一个值
self.a, self.b = self.b, self.a + self.b
# 退出循环的条件
if self.a > 100000:
raise StopIteration()
# 返回下一个值
return self.a

a = Fib()
for n in a:
print(n)

这里有个小细节,iter(a) 的类型并不是迭代器类型,因为 iter(a) 调用 __iter__ 方法返回的是对象本身,也就是 Fib 这个类。因此,iter() 的结果并不一定是迭代器类型,但一定可迭代。

此外,我们还可以反向迭代,但要求对象的大小预先可以确定或者对象实现了 __reversed__() 的特殊方法

1
2
3
4
# 对象的大小可预先确定或者对象实现了 __reversed__() 的特殊方法
a = [1, 2, 3, 4]
for x in reversed(a):
print(x)

如果两者都不符合,那必须先将对象转换为一个列表。要注意的是如果可迭代对象元素很多的话,将其预先转换为一个列表要消耗大量的内存。

1
2
3
f = open('somefile')
for line in reversed(list(f)):
print(line, end='')

总结

最后总结一下,生成器中 __iter____next__ 这两个函数都不需要显式实现,而是由生成器自动提供,可以让代码更 pythonic,也更易读。任何一个生成器都会定义一个名为 __next__ 的方法,这个方法要么返回迭代的下一项,要么引起结束迭代的异常 StopIteration。生成器也是用在迭代操作中,因此它有和迭代器一样的特性,因此我们可以说生成器是迭代器,迭代器不一定是生成器。

而迭代器必须同时实现 __iter____next__ 方法,而迭代时的 next() 函数的本质就是调用对象的 __next____next__ 方法包含了用户自定义的推导算法,这是迭代器对象的本质。

结合上面的总结做一个小例子,如果需要在生成器函数中访问其他属性时,可以将它实现为一个类,这个类将 __iter__ 方法实现为生成器函数,而生成器函数会再次自动提供 __iter____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
34
from collections import deque


# 生成器暴露外部状态给用户
class linehistory:
def __init__(self, text, histlen=3):
self.text = text
self.history = deque(maxlen=histlen)

# 生成器函数,自动实现 iter 和 next
def __iter__(self):
for lineno, line in enumerate(self.text, 1):
self.history.append((lineno, line))
yield line

def clear(self):
self.history.clear()


with open('demo.txt') as f:
# 创建一个实例对象,可以访问内部属性值
lines = linehistory(f)
for line in lines:
# 创建实例对象后,于可以访问内部属性值, 比如 history 属性或者是 clear() 方法。
for lineno, hline in lines.history:
print('{}:{}'.format(lineno, hline), end='')


f = open("demo.txt")
lines = linehistory(f)
# 调用对象的 iter 方法
# 而 iter 方法实现为生成器函数,因此返回生成器
t = iter(lines)
print(next(t))

参考

站在巨人的肩膀上,我们能更好的前行。

  1. https://python3-cookbook.readthedocs.io/zh_CN/latest/chapters/p04_iterators_and_generators.html
  2. https://liam.page/2017/06/30/understanding-yield-in-python/
  3. https://www.liaoxuefeng.com/wiki/1016959663602400/1017590712115904
感谢上学期间打赏我的朋友们。赛博乞讨:我,秦始皇,打钱。

欢迎订阅我的文章