iterable = '可迭代对象'
for x in iterable:
print(x)
可 迭 代 对 象
先记住一个简单的结论:在 Python 中,凡是能跟在 for ... in ...
后面的都是可迭代对象。
# 常见的可迭代对象
iterables = [
"123", # 字符串
[1, 2, 3], # 列表
(1, 2, 3), # 元组
{1: 'a', 2: 'b'}, # 字典
{1, 2, 3}, # 集合
]
for iterable in iterables:
print(type(iterable))
for x in iterable:
print(x, end=', ')
print('')
<class 'str'> 1, 2, 3, <class 'list'> 1, 2, 3, <class 'tuple'> 1, 2, 3, <class 'dict'> 1, 2, <class 'set'> 1, 2, 3,
既然这些不同类型的对象都能使用同样的方式来迭代,那么它们一定存在共同之处。
基于 Python 强大的内省能力,我们可以亲手找出这个共同的属性。
def common_attrs(*objs):
"""计算对象之间的共同属性"""
assert len(objs) > 0
attrs = set(dir(objs[0]))
for obj in objs[1:]:
attrs &= set(dir(obj)) # 取交集
attrs -= set(dir(object)) # 剔除基础对象的属性
return attrs
# 计算可迭代对象的共同属性
iterable_common_attrs = common_attrs(*iterables)
print(iterable_common_attrs)
{'__contains__', '__len__', '__iter__'}
看名字 __iter__
就是我们要找的特殊属性,但是为什么还有另外 2 个属性呢?
这是因为这里列举的可迭代对象都是容器类的数据结构,它们都有获取大小(__len__
)和判断是否包含(__contains__
)的方法。
除了这些容器类对象,Python 内置对象中还有其它可迭代对象,比如文件对象,StringIO 对象等。
# 文件对象也是可迭代对象
f = open('001 迭代器.ipynb', 'r')
# 加入到可迭代对象的清单中
iterables.append(f)
# 继续求一次交集
iterable_common_attrs &= set(dir(f))
print(iterable_common_attrs)
{'__iter__'}
「可迭代对象」唯一的共同接口:__iter__
__iter__
方法,对应的调用方法就是内置函数 iter()
for iterable in iterables:
print(iter(iterable))
<str_iterator object at 0x000001AD1A059AF0> <list_iterator object at 0x000001AD1A059AF0> <tuple_iterator object at 0x000001AD1A059AF0> <dict_keyiterator object at 0x000001AD1A08CF90> <set_iterator object at 0x000001AD1A094240> <_io.TextIOWrapper name='001 迭代器.ipynb' mode='r' encoding='cp936'>
输出了各种 xxx_iterator
类型的对象,这就是我们要找的迭代器!
只有最新加入的 StringIO 的结果显得有点格格不入,后面会介绍为什么。
既然所有的可迭代对象唯一的共同之处是 __iter__
方法,那么显然在迭代的时候,该方法是必然会被调用的。
则得到的迭代器对象,必然也有共同的地方,只需故技重施,找找它们之间的共同属性。
# 由可迭代对象列表得到相应的迭代器列表
iterators = [iter(iterable) for iterable in iterables]
# 故技重施,计算迭代器的共同属性
iterator_common_attrs = common_attrs(*iterators)
print(iterator_common_attrs)
{'__next__', '__iter__'}
「迭代器」有两个接口:__iter__
和 __next__
有点出乎意料,迭代器的共同属性竟然有两个。
新出现的 __next__
,从名字不难猜出它实现了迭代器的核心功能:返回下一个值。
该特殊方法同样有一个内置的函数:next()
。
重复出现的 __iter__
则让人有点疑惑,经常被人一笔带过,其实它的作用也至关重要!
actions = ['点赞', '投币', '收藏'] # 可迭代对象
action_iterator = iter(actions) # 第 1 步:构建迭代器
# 第 2 步:多次迭代
action = next(action_iterator) # 运行 3 次
print(action)
收藏
# 第 3 步:迭代结束
action = next(action_iterator)
print(action)
--------------------------------------------------------------------------- StopIteration Traceback (most recent call last) <ipython-input-12-7ee95b107d87> in <module> 1 # 第 3 步:迭代结束 ----> 2 action = next(action_iterator) 3 print(action) StopIteration:
当列表没有遍历完的时候,每次调用 next()
都返回了列表中的对象,当迭代到最后没有东西可以返回的时候,则抛出了 StopIteration
异常。
从名字不难看出,StopIteration
异常就是专门用来提示迭代器已经到头了,
这其实不是错误,而是通过异常向迭代器的使用者(例如 for
语句)发送一个信号:迭代停止了。迭代器的使用者有责任捕获并妥善处理。
至此我们已经知道了迭代的三个关键步骤:
iter(iterable)
来构建迭代器next(iterator)
来获取值StopIteration
异常来判断迭代结束## 用 while 循环模拟 for 循环迭代
# 创建迭代器
iterator = iter(actions) # 对应 可迭代对象的 __iter__ 方法
while True:
try:
# 通过迭代器获取下一个对象
print(next(iterator)) # 对应迭代器的 __next__ 方法
except StopIteration: # 捕获异常来判断结束
# print('迭代结束')
break
迭代器的 __iter__
方法作用在哪里呢❓
迭代器的基本功能:
__next__()
方法时:StopIteration
异常再添加一点额外的逻辑:
*2
之后再返回BLACK_LIST = ['白嫖', '取关']
class SuzhiIterator:
def __init__(self, actions):
self.actions = actions
self.index = 0 # 初始化索引下标
def __next__(self):
while self.index < len(self.actions):
action = self.actions[self.index]
self.index += 1 # 更新索引下标
if action in BLACK_LIST:
continue
elif '币' in action:
return action * 2
else:
return action
raise StopIteration
actions = ['点赞', '投币', '取关']
sz_iterator = SuzhiIterator(actions)
while True:
try:
print(next(sz_iterator))
except StopIteration:
break
点赞 投币投币
代码本身很简单。
正如前面的分析:
__init__
初始化时保存了可迭代对象,并且设置了初始索引位置__next__
方法不接收任何参数,永远朝着下一个移动,同时记录索引位置变化StopIteration
异常注意:
此时这个迭代器只能使用啰嗦的 while
循环来访问,还不能在 for
循环中使用:
## for 循环需要的是「可迭代对象」而不是「迭代器」
for x in SuzhiIterator(actions):
print(x)
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-14-d8309f8334d5> in <module> 1 ## for 循环需要的是「可迭代对象」而不是「迭代器」 2 ----> 3 for x in SuzhiIterator(actions): 4 print(x) TypeError: 'SuzhiIterator' object is not iterable
因为 for
循环会首先调用 __iter__
方法,而现在这个迭代器并没有定义该方法。
我们可以继续自定义一个可迭代对象的类,通过它来构建并返回自定义的迭代器:
## 一个没有存在意义的可迭代对象
class SuzhiActions:
def __init__(self, actions):
self.actions = actions
def __iter__(self):
return SuzhiIterator(self.actions)
for x in SuzhiActions(actions):
print(x)
点赞 投币投币
上面的两个自定义的类看上去还是符合我们截至目前的认知的:迭代器作为可迭代对象的辅助,由 for
循环隐藏在背后使用。
但是明显可以看出,如果说 GoodActionIterator
还有那么一丝作用,后面的 GoodActions
类完全是多余的。
此时的矛盾点在于:
我们并不是想要一个额外的可迭代对象,只是不想使用它自带的迭代器而已。
解决办法其实也很简单,就让迭代器自食其力,自己实现 __iter__
方法就好了。
既然此时自定义迭代器已经构建出来了,在 __iter__
方法中只需要 return self
即可。
class SuzhiIterator:
def __init__(self, actions):
self.actions = actions
self.index = 0 # 初始化索引下标
def __next__(self):
while self.index < len(self.actions):
action = self.actions[self.index]
self.index += 1 # 更新索引下标
if action in BLACK_LIST:
continue
elif '币' in action:
return action * 2
else:
return action
raise StopIteration
def __iter__(self):
return self
现在,直接把迭代器对象放在 for ... in
后面即可。
## 「迭代器」也是 「可迭代对象」
for x in SuzhiIterator(actions):
print(x)
点赞 投币投币
在 Python 文档中明确指出了,迭代器必须同时实现 __next__
和 __iter__
两个方法,这称之为「迭代器协议」。
根据这个协议,迭代器必须是可迭代的,换言之,「迭代器」是一种「可迭代对象」。
缺少了 __iter__
方法的迭代器是不完整的,不符合迭代器协议的要求。
所有迭代器的 __iter__
方法都只要千篇一律的 return self
即可。
按照这个思路,我们很容易形成这样的认知:
迭代器就是为了让数据结构能够快捷地遍历而定义的辅助对象。
如果只看常见的可迭代对象,这个结论确实不错。这些常见类型的迭代器都是默默地发挥作用。
for iterable in iterables:
print(iter(iterable))
<str_iterator object at 0x000001AD1A08FD30> <list_iterator object at 0x000001AD1A08FD30> <tuple_iterator object at 0x000001AD1A08FD30> <dict_keyiterator object at 0x000001AD1A15FF40> <set_iterator object at 0x000001AD1A057880> <_io.TextIOWrapper name='001 迭代器.ipynb' mode='r' encoding='cp936'>
但是别忘了,如果只是在 for
循环中使用,是无需用到迭代器的 __iter__
方法的:
## 用 while 循环模拟 for 循环迭代
# 创建迭代器
iterator = iter(actions) # 对应 可迭代对象的 __iter__ 方法
while True:
try:
# 通过迭代器获取下一个对象
print(next(iterator)) # 对应迭代器的 __next__ 方法
except StopIteration: # 捕获异常来判断结束
# print('迭代结束')
break
为什么说迭代器的 __iter__
方法是点睛之笔!
现在有两种可迭代对象:
__iter__
接口__iter__
和 __next__
接口如果迭代器没有 __iter__
方法,就不能直接用在 for
循环中:
## 针对容器类可迭代对象的循环
for x in iterable: # 背后操作:「可迭代对象」 -> 「迭代器」
pass
此种情况下,只有可迭代对象在前台露脸。
而迭代器是在背后使用默认的方式“悄悄”构建的,没有存在感,并且生命周期是和循环操作本身绑定在一起的。
而一旦迭代器实现了 __iter__
方法:
## 针对迭代器的循环
interator = iter(iterable) # 迭代器的构建过程远离循环而存在
...
for x in iterator: # 背后操作:「迭代器」 -> 「迭代器」(self)
pass
现在整个迭代过程只需要迭代器就够了!
迭代器不光是从后台走向了前台,而且直接让可迭代对象远离了循环!
现在,迭代器的构建是在明面上单独完成的,和当前循环操作解耦了。
于是乎:
于是,催生了迭代器的两个重要的应用场景。
如果迭代器不可迭代:
「for 循环」<-「迭代器(隐藏)」<-「可迭代对象」
现在,迭代器可以任意的嵌套连接:
「for 循环」<-「迭代器」<-「迭代器」<- ... <-「迭代器」<-「迭代器」<-「可迭代对象」
很多个迭代器串联起来,形成一个处理数据的管道,或者称为数据流
在这个管道中,每一次只通过一份数据,避免了一次性加载所有数据
迭代器也不仅仅只是按顺序返回数据那么简单了,它开始承担处理数据的责任
SuzhiIterator
实际实现了部分过滤器和放大器的功能。当通过迭代器获取数据的时候,远离了数据存储,渐渐地开始不关心数据到底是怎么存储的。
数据流再长总归要有源头,也就是产生数据的地方。
这些数据一定要存储在某个数据结构(如字典列表等)中吗?
from random import random
class Random:
def __iter__(self):
return self
def __next__(self):
return random()
for x in Random():
print(x)
if x > 0.8: # 避免无限循环
break
这个迭代器不但不需要存储数据,甚至连 StopIteration
都不用管。
它可以无穷尽地迭代下去,每次数据都是实时产生的,并不占用内存空间。
有经验的应该能看出来了,这不就是「生成器」嘛!
虽然该迭代器是名副其实的数据生成器,但是生成器(Generator) 在 Python 中特指包含 yield
的函数对象,这是下一章的主要内容。
__iter__
的可迭代对象¶并不是所有的可迭代对象都必须定义有 __iter__
方法。
如果一个对象没有 __iter__
方法,但是定义了 __getitem__
方法,同样是可以迭代的。
class DumbList:
def __getitem__(self, index):
if index > 5:
raise StopIteration
return index*2
dl = DumbList()
for x in dl:
print(x)
因此,不能通过检查 __iter__
方法来判断一个对象是否是可迭代的,而是应该直接使用 iter()
函数,如果不可迭代,则会抛出 TypeError
异常。
class NotIterable:
pass
ni = NotIterable()
it = iter(ni)
这里的做法更多的是为了兼容性考虑。
实际我们在编写自己的数据类型时,如果希望它是可迭代的,最好的办法还是实现 __iter__
方法。