只要你学了 Python 语言, 就不会不知道 for 循环, 也肯定用 for 循环来遍历一个列表(list), 那为什么 for 循环可以遍历 list, 而不能遍历 int 类型对象呢? 怎么让一个自定义的对象可遍历?
这篇博客中, 我们来一起探索一下这个问题, 在这个过程中, 我们会介绍到迭代器, 可迭代对象, 生成器, 更进一步的, 我们会详细介绍他们的原理, 异同.
2 迭代器与可迭代对象
在开始下面内容之前, 我们先说说标题中的 "迭代" 一词. 什么是迭代? 我认为, 迭代一个完整过程中的一个重复, 或者说每一次对过程的重复称为一次 "迭代", 而每一次迭代得到的结果会作为下一次迭代的初始值, 举一个类比来说: 一个人类家族的发展是一个完整过程, 需要经过数代人的努力, 每一代都会以接着上一代的成果继续发展, 所以每一代都是迭代.
2.1 迭代器
(1)怎么判断是否可迭代
作为一门设计语言, Python 提供了许多必要的数据类型, 例如基本数据类型 int,bool,str, 还有容器类型 list,tuple,dict,set. 这些类型当中, 有些是可迭代的, 有些不可迭代, 怎么判断呢?
在 Python 中, 我们把所有可以迭代的对象统称为可迭代对象, 有一个类专门与之对应: Iterable. 所以, 要判断一个类是否可迭代, 只要判断是否是 Iterable 类的实例即可.
- >>> from collections import Iterable
- >>> isinstance(123, Iterable)
- False
- >>> isinstance(True, Iterable)
- False
- >>> isinstance('abc', Iterable)
- True
- >>> isinstance([], Iterable)
- True
- >>> isinstance({
- }, Iterable)
- True
- >>> isinstance((), Iterable)
- True
所以, 整型, 布尔不可迭代, 字符串, 列表, 字典, 元组可迭代.
怎么让一个对象可迭代呢? 毕竟, 很多时候, 我们需要用到的对象不止 Python 内置的这些数据类型, 还有自定义的数据类型. 答案就是实现__iter__()方法, 只要一个对象定义了__iter__()方法, 那么它就是可迭代对象.
- from collections.abc import Iterable
- class A():
- def __iter__(self):
- pass
- print('A()是可迭代对象吗:',isinstance(A(),Iterable))
结果输出为:
A()是可迭代对象吗: True
瞧, 我们在__iter__()方法里面甚至没写任何东西, 反正我们在类 A 中定义则__iter__()方法, 那么, 它就是一个可迭代对象.
重要的事情说 3 遍:
只要一个对象定义了__iter__()方法, 那么它就是可迭代对象.
只要一个对象定义了__iter__()方法, 那么它就是可迭代对象.
只要一个对象定义了__iter__()方法, 那么它就是可迭代对象.
2.2 迭代器
迭代器是对可迭代对象的改造升级, 上面说过, 一个对象定义了__iter__()方法, 那么它就是可迭代对象, 进一步地, 如果一个对象同时实现了__iter__()和__next()__()方法, 那么它就是迭代器.
来, 跟我读三遍:
如果一个对象同时实现了__iter__()和__next()__()方法, 那么它就是迭代器.
如果一个对象同时实现了__iter__()和__next()__()方法, 那么它就是迭代器.
如果一个对象同时实现了__iter__()和__next()__()方法, 那么它就是迭代器.
在 Python 中, 也有一个类与迭代器对应: Iterator. 所以, 要判断一个类是否是迭代器, 只要判断是否是 Iterator 类的实例即可.
- from collections.abc import Iterable
- from collections.abc import Iterator
- class B():
- def __iter__(self):
- pass
- def __next__(self):
- pass
- print('B()是可迭代对象吗:',isinstance(B(), Iterable))
- print('B()是迭代器吗:',isinstance(B(), Iterator))
结果输出如下:
B()是可迭代对象吗: True
B()是迭代器吗: True
可见, 迭代器一定是可迭代对象, 但可迭代对象不一定是迭代器.
所以整型, 布尔一定不是迭代器, 因为他们连可迭代对象都算不上. 那么, 字符串, 列表, 字典, 元组是迭代器吗? 猜猜!
- >>> from collections.abc import Iterator
- >>> isinstance('abc', Iterator)
- False
- >>> isinstance([], Iterator)
- False
- >>> isinstance({
- }, Iterator)
- False
- >>> isinstance((), Iterator)
- False
惊不惊喜, 意不意外, 字符串, 列表, 字典, 元组都不是迭代器. 那为什么它们可以在 for 循环中遍历呢? 而且, 我想, 看到这里, 就算你已经可以在形式上区分可迭代对象和迭代器, 但是你可能会问, 这有什么卵用吗? 确实, 没多少卵用, 因为我们还不知道__iter__(),__next__()到底是个什么鬼东西.
接下来, 我们通过继续探究 for 循环的本质来解答这些问题.
2.3 for 循环的本质
说到__iter__()和__next__()方法, 就很有必要介绍一下 iter()和 next()方法了.
(1)iter()与__iter__()
__iter__()的作用是返回一个迭代器, 虽然上面说过, 只要实现了__iter__()方法就是可迭代对象, 但是, 没有实现功能 (返回迭代器) 总归是有问题的, 就像一个村长, 当选之后, 那就是村长了, 但是如果尸位素餐不做事, 那总是有问题的.
__iter__()方法毕竟是一个特殊方法, 不适合直接调用, 所以 Python 提供了 iter()方法. iter()是 Python 提供的一个内置方法, 可以不用导入, 直接调用即可.
- from collections.abc import Iterator
- class A():
- def __iter__(self):
- print('A 类的__iter__()方法被调用')
- return B()
- class B():
- def __iter__(self):
- print('B 类的__iter__()方法被调用')
- return self
- def __next__(self):
- pass
- a = A()
- print('对 A 类对象调用 iter()方法前, a 是迭代器吗:', isinstance(a, Iterator))
- a1 = iter(a)
- print('对 A 类对象调用 iter()方法后, a1 是迭代器吗:', isinstance(a1, Iterator))
- b = B()
- print('对 B 类对象调用 iter()方法前, b 是迭代器吗:', isinstance(b, Iterator))
- b1 = iter(b)
- print('对 B 类对象调用 iter()方法后, b1 是迭代器吗:', isinstance(b1, Iterator))
运行结果如下:
对 A 类对象调用 iter()方法前, a 是迭代器吗: False
A 类的__iter__()方法被调用
对 A 类对象调用 iter()方法后, a1 是迭代器吗: True
对 B 类对象调用 iter()方法前, b 是迭代器吗: True
B 类的__iter__()方法被调用
对 B 类对象调用 iter()方法后, b1 是迭代器吗: True
对于 B 类, 因为 B 类本身就是迭代器, 所以可以直接返回 B 类的实例, 也就是说 self, 当然, 你要是返回其他迭代器也没毛病. 对于类 A, 它只是一个可迭代对象,__iter__()方法需要返回一个迭代器, 所以返回了 B 类的实例, 如果返回的不是一个迭代器, 调用 iter()方法时就会报以下错误:
TypeError: iter() returned non-iterator of type 'A'
(2)next()与__next__()
__next__()的作用是返回遍历过程中的下一个元素, 如果没有下一个元素则主动抛出 StopIteration 异常. 而 next()就是 Python 提供的一个用于调用__next__()方法的内置方法.
下面, 我们通过 next()方法来遍历一个 list:
- >>> list_1 = [1, 2, 3]
- >>> next(list_1)
- Traceback (most recent call last):
- File "<pyshell#19>", line 1, in <module>
- next(list_1)
- TypeError: 'list' object is not an iterator
- >>> list_2 = iter(list_1)
- >>> next(list_2)
- 1
- >>> next(list_2)
- 2
- >>> next(list_2)
- 3
- >>> next(list_2)
- Traceback (most recent call last):
- File "<pyshell#24>", line 1, in <module>
- next(list_2)
- StopIteration
因为列表只是可迭代对象, 不是迭代器, 所以对 list_1 直接调用 next()方法会产生异常. 对 list_1 调用 iter()后就可以获得是迭代器的 list_2, 对 list_2 每一次调用 next()方法都会取出一个元素, 当没有下一个元素时继续调用 next()就抛出了 StopIteration 异常.
- >>> class A():
- def __init__(self, lst):
- self.lst = lst
- def __iter__(self):
- print('A.__iter__()方法被调用')
- return B(self.lst)
- >>> class B():
- def __init__(self, lst):
- self.lst = lst
- self.index = 0
- def __iter__(self):
- print('B.__iter__()方法被调用')
- return self
- def __next__(self):
- try:
- print('B.__next__()方法被调用')
- value = self.lst[self.index]
- self.index += 1
- return value
- except IndexError:
- raise StopIteration()
- >>> a = A([1, 2, 3])
- >>> a1 = iter(a)
A.__iter__()方法被调用
>>> next(a1)
B.__next__()方法被调用
1
>>> next(a1)
B.__next__()方法被调用
2
>>> next(a1)
B.__next__()方法被调用
3
>>> next(a1)
B.__next__()方法被调用
- Traceback (most recent call last):
- File "<pyshell#78>", line 11, in __next__
- value = self.lst[self.index]
- IndexError: list index out of range
- During handling of the above exception, another exception occurred:
- Traceback (most recent call last):
- File "<pyshell#84>", line 1, in <module>
- next(a1)
- File "<pyshell#78>", line 15, in __next__
- raise StopIteration()
- StopIteration
A 类实例化出来的实例 a 只是可迭代对象, 不是迭代器, 调用 iter()方法后, 返回了一个 B 类的实例 a1, 每次对 a1 调用 next()方法, 都用调用 B 类的__next__()方法.
接下来, 我们用 for 循环遍历一下 A 类实例:
- >>> for i in A([1, 2, 3]):
- print('for 循环中取出值:',i)
A.__iter__()方法被调用
B.__next__()方法被调用
for 循环中取出值: 1
B.__next__()方法被调用
for 循环中取出值: 2
B.__next__()方法被调用
for 循环中取出值: 3
B.__next__()方法被调用
通过 for 循环对一个可迭代对象进行迭代时, for 循环内部机制会自动通过调用 iter()方法执行可迭代对象内部定义的__iter__()方法来获取一个迭代器, 然后一次又一次得迭代过程中通过调用 next()方法执行迭代器内部定义的__next__()方法获取下一个元素, 当没有下一个元素时, for 循环自动捕获并处理 StopIteration 异常. 如果你还没明白, 请看下面用 while 循环实现 for 循环功能, 整个过程, 原理都是一样的:
- >>> a = A([1, 2, 3])
- >>> a1 = iter(a)
A.__iter__()方法被调用
- >>> while True:
- try:
- i = next(a1)
- print('for 循环中取出值:', i)
- except StopIteration:
- break
B.__next__()方法被调用
for 循环中取出值: 1
B.__next__()方法被调用
for 循环中取出值: 2
B.__next__()方法被调用
for 循环中取出值: 3
B.__next__()方法被调用
作为一个迭代器, B 类对象也可以通过 for 循环来迭代:
- >>> for i in B([1, 2, 3]):
- print('for 循环中取出值:',i)
B.__iter__()方法被调用
B.__next__()方法被调用
for 循环中取出值: 1
B.__next__()方法被调用
for 循环中取出值: 2
B.__next__()方法被调用
for 循环中取出值: 3
B.__next__()方法被调用
看出来了吗? 这就是 for 循环的本质.
3 生成器
3.1 迭代器与生成器
如果一个函数体内部使用 yield 关键字, 这个函数就称为生成器函数, 生成器函数调用时产生的对象就是生成器. 生成器是一个特殊的迭代器, 在调用该生成器函数时, Python 会自动在其内部添加__iter__()方法和__next__()方法. 把生成器传给 next() 函数时, 生成器函数会向前继续执行, 执行到函数定义体中的下一个 yield 语句时, 返回产出的值, 并在函数定义体的当前位置暂停, 下一次通过 next()方法执行生成器时, 又从上一次暂停位置继续向下......, 最终, 函数内的所有 yield 都执行完, 如果继续通过 yield 调用生成器, 则会抛出 StopIteration 异常 -- 这一点与迭代器协议一致.
- >>> from collections.abc import Iterable
- >>> from collections.abc import Iterator
- >>> def gen():
- print('第 1 次执行')
- yield 1
- print('第 2 次执行')
- yield 2
- print('第 3 次执行')
- yield 3
- >>> g = gen()
- >>> isinstance(g, Iterable)
- True
- >>> isinstance(g, Iterator)
- True
- >>> g
- <generator object gen at 0x0000021CE9A39A98>
- >>> next(g)
第 1 次执行
1
>>> next(g)
第 2 次执行
2
>>> next(g)
第 3 次执行
- 3
- >>> next(g)
- Traceback (most recent call last):
- File "<pyshell#120>", line 1, in <module>
- next(g)
- StopIteration
可以看到, 生成器的执行机制与迭代器是极其相似的, 生成器本就是迭代器, 只不过, 有些特殊. 那么, 生成器特殊在哪呢? 或者说, 有了迭代器, 为什么还要用生成器?
从上面的介绍和代码中可以看出, 生成器采用的是一种惰性计算机制, 一次调用也只会产生一个值, 它不会将所有的值一次性返回给你, 你需要一个那就调用一次 next()方法取一个值, 这样做的好处是如果元素有很多(数以亿计甚至更多), 如果用列表一次性返回所有元素, 那么会消耗很大内存, 如果我们只是想要对所有元素依次一个一个取出来处理, 那么, 使用生成器就正好, 一次返回一个, 并不会占用太大内存.
举个例子, 假设我们现在要取 1 亿以内的所有偶数, 如果用列表来实现, 代码如下:
- def fun_list():
- index = 1
- temp_list = []
- while index <100000000:
- if index % 2 == 0:
- temp_list.append(index)
- print(index)
- index += 1
- return temp_list
上面程序会先获取所有符合要求的偶数, 然后一次性返回. 如果你运行了代码, 你就会发现两个问题 -- 运行时间很长, 消耗很多内存.
有时候, 我们并不一定需要一次性获得所有的对象, 需要一个使用一个就可以, 这样的话, 可以用生成器来实现:
- >>> def fun_gen():
- index = 1
- while index <100000000:
- if index % 2 == 0:
- yield index
- index += 1
- >>> fun_gen()
- <generator object fun_gen at 0x00000222DC2F4360>
- >>> g = fun_gen()
- >>> next(g)
- 2
- >>> next(g)
- 4
- >>> next(g)
- 6
看到了吗? 对生成器没执行一次 next()方法, 就会返回一个元素, 这样的话无论在速度上还是机器性能消耗上都会好很多. 如果你还没感受到生成器的优势, 我再说一个应用场景, 假如需要取出远程数据库中的 100 万条记录进行处理, 如果一次性获取所有记录, 网络带宽, 内存都会有很大消耗, 但是如果使用生成器, 就可以取一条, 就在本地处理一条.
不过, 生成器也有不足, 正因为采用了惰性计算, 你不会知道下一个元素是什么, 更不会知道后面还有多少元素, 所以, 对于列表, 元组等结构, 我们能调用 len()方法获知长度, 但是对于生成器却不能.
总结一下迭代器与生成器的异同:
(1)生成器是一种特殊的迭代器, 拥有迭代器的所有特性;
(2)迭代器使用 return 返回值而生成器使用 yield 返回值每一次对生成器执行 next()都会在 yield 处暂停;
(3)迭代器和生成器虽然都执行 next()方法时返回下一个元素, 迭代器在实例化前就已知所有元素, 但是采用惰性计算机制, 共有多少元素, 下一个元素是什么都是未知的, 每一次对生成器对象执行 next()方法才会产生下一个元素.
3.2 生成器解析式
使用过列表解析式吗? 语法格式为:[返回值 for 元素 in 可迭代对象 if 条件]
看下面代码:
- >>> li = []
- >>> for i in range(5):
- if i%2==0:
- li.append(i**2)
- >>> li
- [0, 4, 16]
我们可以用列表解析式实现同样功能:
- >>> li = [i**2 for i in range(5) if i%2==0]
- >>> li
- [0, 4, 16]
- >>> type(li)
- <class 'list'>
很简单对不对? 简洁了很多, 返回的 li 就是一个列表. 咳咳...... 偏题了, 我们要说的是生成器解析式, 而且我相信打开我这篇博文的同学大多都熟悉列表解析式, 回归正题.
生成器解析式语法格式为:(返回值 for 元素 in 可迭代对象 if 条件)
你没看错, 跟列表解析式相比, 生成器解析式只是把方括号换成了原括号. 来感受一下:
- >>> g = (i**2 for i in range(5) if i%2==0)
- >>> g
- <generator object <genexpr> at 0x00000222DC2F4468>
- >>> next(g)
- 0
- >>> next(g)
- 4
- >>> next(g)
- 16
- >>> next(g)
- Traceback (most recent call last):
- File "<pyshell#38>", line 1, in <module>
- next(g)
- StopIteration
可以看到, 生成器解析式返回的就是一个生成器对象, 换句话说生成器解析式是生成器的一种定义方式, 这种方式简单快捷, 当然实现的功能不能太复杂.
4 总结
本文全面总结了 Python 中可迭代对象, 迭代器, 生成器知识, 我相信, 只要你认真消化我这篇博文, 就能深刻领悟迭代器生成器.
来源: https://www.cnblogs.com/chenhuabin/p/11288797.html