14. 生成器和迭代器

14.1. 生成器

列表作为一个容器,其所占内存大小和元素个数成正比,也即每增加一个元素,那么内存中就要分配一块区域来存储它。我们看一个例子:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import sys

list0 = [1] * 100
list1 = [1] * 1000000

print(sys.getsizeof(list0))
print(sys.getsizeof(list1))

>>>
864
8000064

list1 列表元素个数是 list0 元素个数的 10000 倍,所占内存也大约大 10000 倍。

如果我们要处理更多元素,那么所占内存就呈线性增大,所以受到内存限制,列表容量是有限的。通常我们并不会一次处理所有元素,而只是集中在其中的某些相邻的元素上。所以如果列表元素可以用某种算法用已知量推导出来,就不必一次创建所有的元素。这种边循环边计算的机制,称为生成器(generator),生成器是用时间换空间的典型实例。

生成器通常由两种方式生成,用小括号()表示的生成器表达式(generator expression)和生成器函数(generator function)。

14.1.1. 生成器表达式

在生成列表和字典时,可以通过推导表达式完成。只要把推导表达式中的中括号换成小括号就成了生成器表达式。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
list0 = [x * x for x in range(5)]
print(list0)

list_generator0 = (x * x for x in range(5))
print(list_generator0)

list_generator1 = (x * x for x in range(5000000))
print(sys.getsizeof(list_generator0))
print(sys.getsizeof(list_generator1))

>>>
[0, 1, 4, 9, 16]
<generator object <genexpr> at 0x000002C7B9955B48>
88
88

显然生成器对象的大小不会因为生成元素的上限个数而增大,此外不能够像列表被打印出来,那么如何获取通过生成器获取每一个元素呢? 借助 Python 内建方法 next():

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
list_generator0 = (x * x for x in range(3))
print(next(list_generator0))
print(next(list_generator0))
print(next(list_generator0))
print(next(list_generator0)) # 触发 StopIteration 异常

>>>
0
1
4
StopIteration

每次调用 next() 方法时,总是根据最后的值和生成器给出的生成方法来计算下一个值。直到最后一个元素,抛出 StopIteration 异常。generator 也是可迭代对象,通常不会使用 next() 来逐个获取元素,而是使用 for in,它自动在遇到 StopIteration 异常时结束循环。

0
1
2
3
4
5
6
7
8
9
list_generator0 = (x * x for x in range(3))
print(isinstance(list_generator0, Iterable))
for i in list_generator0:
    print(i)

>>>
True
0
1
4

14.1.2. 生成器函数

通过生成器表达式来生成 generator 是有局限的,比如斐波那契数列用表达式写不出来,复杂的处理需要生成器函数完成。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def fibonacci(n):
    i, j = 0, 1

    while(i < n):
        print(i, end=' ')
        i, j = j, i + j

fibonacci(5)
print(type(fibonacci))
>>>
0 1 1 2 3 <class 'function'>

很容易写出打印斐波那契数列的函数,参数表示生成的元素个数。有时候我们不需要打结果一个个打印出来,而是要把这种推导算法封装起来,把 fibonacci() 函数变成一个生成器函数。只需要把 print 这一行替换为 yielb i 即可。

如果一个函数定义中包含 yield 表达式,那么这个函数就不再是一个普通函数,而是一个生成器函数。yield 语句类似 return 会返回一个值,但它会记住这个返回的位置,下次 next() 迭代就从这个位置下一行继续执行。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def fib_generator(n):
    i, j = 0, 1

    while(i < n):
        yield i
        i, j = j, i + j

print(type(fib_generator))
print(type(fib_generator(5)))

>>>
<class 'function'>
<class 'generator'>

生成器函数并不是生成器,它运行返回后的结果才是生成器。

0
1
2
3
4
5
generator = fib_generator(5)
for i in generator:
    print(i, end=' ')

>>>
0 1 1 2 3

14.1.3. 生成器的本质

任何一个生成器都会定义一个名为 __next__ 的方法,这个方法要在最后一个元素之后需抛出 StopIteration 异常。next() 函数的本质就是调用对象的 __next__()。这个方法要么返回迭代的下一项,要么引起结束迭代的异常 StopIteration,下面的示例揭示了生成器的本质。

 0
 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
class FibGenerator():
    def __init__(self, n):
        self.__n = n

        self.__s0 = 0
        self.__s1 = 1
        self.__count = 0

    def __next__(self):  # 用于内建函数 next()
       if self.__count < self.__n:
           ret = self.__s0
           self.__s0, self.__s1 = self.__s1, (self.__s0 + self.__s1)
           self.__count += 1

           return ret
       else:
           raise StopIteration

    def __iter__(self):  # 用于 for 循环语句
       return self

fg = FibGenerator(5)
print(type(fg))
print(isinstance(fg, Iterable))

for i in fg:
    print(i, end=' ')

>>>
<class '__main__.FibGenerator'>
True
0 1 1 2 3

示例中如果没有定义 __iter__() 方法则只能使用 next() 函数进行迭代,当它定义后,就可以使用 for 和 in 语句访问了,同时定义了这两种方法的对象称为迭代器(Iterator)。

14.2. 迭代和迭代对象

在 Python 中通过 for in 对对象进行遍历的操作被称为迭代(Iteration),可以进行迭代操作的对象被称为可迭代 (Iterable) 对象,例如字符串,列表和元组。如何判断一个对象是否可迭代呢?

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from collections import Iterable

print(isinstance(1, Iterable))
print(isinstance('abc', Iterable))
print(isinstance([1, 2, 3], Iterable))
print(isinstance({'name': 'val'}, Iterable))

print(isinstance(range(10), Iterable))
print(type(range(10)))

>>>
False
True
True
True
True
<class 'range'>

除了常见的基本数据类型是可迭代对象外,文件对象,管道对象以及更复杂的生成器等也是可迭代对象。

14.3. 迭代器

14.3.1. 可迭代对象和迭代器

如 list、tuple、dict、set、str、range、enumerate 等这些可以直接用于 for 循环的对象称为可迭代(Iterable)对象,也即它们是可迭代的。但是生成器不但可以作用于 for 和 in 语句,还可以被 next() 函数不断调用并返回下一个值,直到最后抛出 StopIteration 错误,它是一个迭代器(Iterator)。

  • 可迭代对象,需要提供 __iter__()方法,否则不能被 for 语句处理。
  • 迭代器必须同时实现 __iter__() 和 __next__()方法,__next__() 方法包含了用户自定义的推导算法,这是迭代器对象的本质。生成器表达式和生成器函数产生生成器时,会自动生成名为 __iter__ 和 __next__ 的方法。参考 Python 迭代对象
 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
list_generator0 = (x * x for x in range(3))
print('__iter__' in dir(list_generator0))
print('__next__' in dir(list_generator0))

fg = fib_generator(5)
print('__iter__' in dir(fg))
print('__next__' in dir(fg))

>>>
True
True
True
True

如同判断对象是否可迭代一样,可以使用isinstance()判断一个对象是否是 Iterator 对象:

0
1
2
3
4
5
6
list_generator0 = (x * x for x in range(3))
isinstance(list_generator0, Iterator)
isinstance(list_generator0, Iterable)

>>>
True
True

14.3.2. 迭代对象类型判断

另外一种方式是通过 iter() 函数来判断,这种方法是最准确的(可迭代对象并不一定是 Iterable 或者 Iterator 类的实例),后面会详解 iter()内建函数的作用。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 判断可迭代对象
def is_iterable(obj):
    status = True
    try:
      iter(obj)
    except TypeError:
      status = False

  return status

# 判断迭代器对象
def is_iterator(obj):
  return is_iterable(obj) and obj is iter(obj)

由上述分析可知,只要一个对象是迭代器,那么它一定是可迭代对象,反过来不成立。

14.3.3. 生成迭代器

iter() 内建方法可以把list、dict、str等可迭代对象转换成迭代器。

0
1
2
3
4
5
6
list0 = [0, 1, 2]
iter0 = iter(list0)

print(type(iter0))

>>>
<class 'list_iterator'>

除字典外,一个对象只要实现了 __getitem__() 方法,就认为它是序列类型,序列类型总是可迭代的,循环作用在序列类型上的本质参考 索引访问和循环

对于序列类型,字典,还有更复杂的可迭代类型如 range,Python 内建了对应的迭代器对它们进行迭代操作,它们无需实现 __next__() 方法,iter() 函数会返回对应的内建迭代器。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
print(type(iter("")))
print(type(iter([])))

print(type(iter({})))
print(type(iter({}.values())))

print(type(iter(range(5))))

>>>
<class 'str_iterator'>
<class 'list_iterator'>
<class 'dict_keyiterator'>
<class 'dict_valueiterator'>
<class 'range_iterator'>

需要强调,可迭代对象并不一定是 Iterable 或者 Iterator 类的实例,可以参考 索引访问和循环

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
......

print(isinstance(rwstr, Iterator))
print(isinstance(rwstr, Iterable))

print(is_iterable(rwstr))
print(is_iterator(rwstr))

>>>
False
False
True
False

而对于迭代器类型来说,iter() 函数直接执行对象中的 __iter__()函数并返回,循环操作的实质如下所示:

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
for element in iterable:
    # do something with element

# 等价于如下操作
# create an iterator object from that iterable
iter_obj = iter(iterable)

# infinite loop
while True:
    try:
        # get the next item
        element = next(iter_obj)
        # do something with element
    except StopIteration:
        # if StopIteration is raised, break from loop
        break
iter(iterable) -> iterator
iter(callable, sentinel) -> iterator

查看 iter() 函数定义,它的第二种用法比较特殊,如果提供了第二个参数 sentinel,那么第一个参数必须是一个可调用对象,比如函数。下面示例用于实现读取到指定的行:

0
1
2
3
4
5
6
7
8
# 读取到 SystemExit\n 的前一行
with open('test.txt', 'r', encoding="utf-8") as fp:
    for line in iter(fp.readline, 'SystemExit\n'):
        print(line)

# 只处理 1 和 2
list0 = [1, 2, 3]
for i in iter(list0.pop, 3):
  print(i)

14.3.4. 无限迭代器

所谓无限迭代器,也即是没有限制,永不抛出 StopIteration 异常。下面是一个奇数生成器,可以无限制地生成奇数。

 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class OddIter:

    def __iter__(self):
        self.num = 1
        return self

    def __next__(self):
        num = self.num
        self.num += 2
        return num

for i in OddIter():
    if i > 5:       # 处理无限生成器需要退出分支
        break
    print(i, end=' ')

>>>
1 3 5

14.3.5. 惰性计算

惰性计算又称为惰性求值(Lazy Evaluation),是一个计算机编程中的概念,它的目的是要最小化计算机要做的工作,尽可能延迟表达式求值。延迟求值特别用于函数式编程语言中。在使用延迟求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值。惰性计算的最重要的好处是它可以构造一个无限的数据类型。

具有惰性计算特点的序列称为惰性序列,Python 中的迭代器就是一个惰性序列,调用 iter() 返回一个 iterator 并赋值给一个变量后不会立即进行求值,而是当你用到其中某些元素的时候才去求某元素的值。

惰性计算还可以在大规模数据处理中平滑处理时间,提高内存使用率。当处理大规模数据时,一次性进行处理往往是不方便的。

很多语言对惰性计算提供了支持,比如 Java,Scala,当然 Python 也不例外,它是函数式编程语言的一大特点。