Blog icon indicating copy to clipboard operation
Blog copied to clipboard

迭代器和生成器

Open codcodog opened this issue 7 years ago • 0 comments

迭代器和生成器

relationships

迭代器 (Iterator)

迭代器对象:必须实现 __iter__(), __next__() 这两个特殊方法的对象,另外,又把实现这两个特殊方法统称为 迭代器协议.

其中,__iter__() 方法返回迭代器对象自身,__next__() 方法返回序列的下一个值,如果序列中没有更多元素了,则抛出 StopIteration 异常.

>>> class PowTow:
...     def __init__(self, max=0):
...             self.max = max
...             self.n = 0
...     def __iter__(self):
...             return self
...     def __next__(self):
...             if self.n <= self.max:
...                     result = 2 ** self.n
...                     self.n += 1
...                     return result
...             else:
...                     raise StopIteration
...
>>> p = PowTow(2)
>>> next(p)
1
>>> next(p)
2
>>> next(p)
4
>>> next(p)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 13, in __next__
StopIteration

注意: 迭代器只能迭代一次,每次调用 next() 方法就会向前一步,不能回退.

>>> t = PowTow(2)
>>> t
<__main__.PowTow object at 0x7f3022a4aa20>
>>> for i in t:
...     print(i)
...
1
2
4
>>> for h in t:
...     print(h)
...
>>>

for 循环,其本质就是通过不断调用 next() 函数实现的.

for x in [1, 2, 3, 4, 5]:
    pass

等价于

# 首先获得Iterator对象:
it = iter([1, 2, 3, 4, 5])
# 循环:
while True:
    try:
        # 获得下一个值:
        x = next(it)
    except StopIteration:
        # 遇到StopIteration就退出循环
        break

iterable-vs-iterator

回到迭代器无法多次使用的问题上来,从上面给出的 Demo 例子看,有两个解决方案:

  1. __iter__() 方法,重置 n 的值
  2. 将可迭代对象和迭代器分开自定义

什么是可迭代对象,看下文介绍

方案一:在 __iter__() 方法重置 n 的值

>>> class PowTow:
...     def __init__(self, max=0):
...             self.max = max
...     def __iter__(self):
...             self.n = 0
...             return self
...     def __next__(self):
...             if self.n <= self.max:
...                     result = 2 ** self.n
...                     self.n += 1
...                     return result
...             else:
...                     raise StopIteration
... 
>>> next(p)   # 这里,我们要先 iter(p) 调用 __iter__() 方法初始化 n 值
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in __next__
AttributeError: 'PowTow' object has no attribute 'n'

>>> iter(p)
<__main__.PowTow object at 0x7f3022a4ad30>
>>> next(p)
1
>>> next(p)
2
>>> next(p)
4
>>> next(p)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 13, in __next__
StopIteration

>>> m = PowTow(2)
>>> for i in m:
...     print(i)
... 
1
2
4
>>> for a in m:
...     print(a)
... 
1
2
4

第一次调用 for 循环:for i in m 的时候,在结束循环的时候,默认里已经抛出 StopIteration 异常,此时 self.n值为 3
当第二次调用 for 循环:for a in m 的时候,for 循环默认调用了 __iter__() 方法,此时 self.n 值由 3 重置为 0,因此不触发抛出 StopIteration 异常的条件,迭代器可再次使用.

方案二:将可迭代对象和迭代器分开自定义

>>> class PowTow:
...     def __init__(self, max=0):
...             self.max = max
...             self.n = 0
...     def __iter__(self):
...             return self
...     def __next__(self):
...             if self.n <= self.max:
...                     result = 2 ** self.n
...                     self.n += 1
...                     return result
...             else:
...                     raise StopIteration
... 
>>> class Pow:
...     def __init__(self, max=0):
...             self.max = max
...     def __iter__(self):
...             return PowTow(self.max)
... 
>>> p = Pow(2)
>>> for a in p:
...     print(a)
... 
1
2
4
>>> for b in p:
...     print(b)
... 
1
2
4

p 是一个可迭代对象,每次 for 循环迭代 p 的时候,__iter__() 方法都返回一个新的 PowTow 迭代器,新的迭代器的 self.n 值都是初始化值:0,不会触发抛出 StopIteration 异常的条件
这里要注意区分下:可迭代对象可以多次重复迭代使用,而迭代器,大多数情况下,只能迭代一次

可迭代对象 (Iterables)

可迭代对象:实现了 __iter__() 方法,该方法返回一个迭代器对象,或者定义了 __getitem__() 方法,可以按 index 索引的对象,并且在没有值时抛出 IndexError 异常.

可迭代对象都可以通过 iter() 方法返回一个 迭代器.
如,上面例子中的 Pow 就是一个可迭代对象.

生成器 (Generator)

生成器:其实是一个特殊的 迭代器,不过它比常规的 迭代器 更加优雅,只需要一个 yield 关键字即可.

生成器实现的斐波那契数列

>>> def fib():
...     prev, curr = 0, 1
...     yield prev
...     yield curr
...     while True:
...             prev, curr = curr, prev + curr
...             yield curr
...
>>> f = fib()
>>> a = [next(f) for i in range(3)]
>>> a
[0, 1, 1]
>>> b = [next(f) for i in range(8)]
>>> b
[2, 3, 5, 8, 13, 21, 34, 55]
>>> c = fib()
>>> d = [next(c) for j in range(13)]
>>> d
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]

注意,生成器大多数情况下也只是迭代一次(生成器是一个特殊的迭代器)

Python 中主要有两类生成器:

  • 生成器函数
  • 生成器表达式

生成器函数:指在函数体中定义了 yield 关键字的函数,例如上面例子中的 fib() 函数.

生成器表达式:类似列表推导表达式,不过它们之间有一点点不同.

# 列表推导表达式
>>> a = [i for i in range(5)]
>>> a
[0, 1, 2, 3, 4]

# 生成器表达式
>>> b = (i for i in range(5))
>>> b
<generator object <genexpr> at 0x7f3022a56518>
>>> list(b)
[0, 1, 2, 3, 4]

它们之间的区别就是,列表推导表达式,使用中括号:[ ],而生成器表达式,使用小括号:( )

有时,生成器表达式的小括号可以省略不写,例如:

>>> sum(i for i in range(5))
10

参考资料:
完全理解Python迭代对象、迭代器、生成器
Iterables vs. Iterators vs. Generators
对 Python 迭代的深入研究
Python Iterators

codcodog avatar Jan 11 '18 07:01 codcodog