Python 中的迭代器
9.6 迭代器
自从第4章4.2.6节出现了“可迭代的”(Iterable)一词之后,就不断遇到具有此特征的对象,比如列表、字符串、字典等。判断一个对象是否是可迭代的,主要看它是否具有 __iter__()
方法。
>>> hasattr(list, '__iter__')
True
>>> hasattr(int, '__iter__')
False
此外,在第4章4.3.3节还首次遇到了“迭代器”这个名词,此后它也反复出现,例如:
>>> map(lambda x, y: x*y, [1, 2, 3], [4, 5, 6])
<map object at 0x7f86d1969c70>
函数 map()
所返回的对象就是迭代器(Iterator)。
显然,“迭代器”一定是“可迭代的”,但“可迭代的”对象,不一定是“迭代器”。
定义迭代器的一种最简单的方式是用内置函数 iter()
。
>>> lst = [1, 2, 3, 4]
>>> iter_lst = iter(lst)
>>> iter_lst
<list_iterator object at 0x7f86d194ccd0>
从返回结果中可以看出,iter_lst
引用的是迭代器对象。那么,iter_lst
和 lst
有什么异同?
>>> hasattr(lst, '__iter__')
True
>>> hasattr(iter_lst, '__iter__')
True
首先,它们都有 __iter__()
方法,即它们都是可迭代的。但差异也是明显的:
>>> hasattr(lst, '__next__')
False
>>> hasattr(iter_lst, '__next__')
True
有无 __next__()
方法是 iter_lst
和 lst
两个对象的区别,迭代器必须有 __next__()
方法,它的作用是什么?
>>> iter_lst.__next__()
1
>>> iter_lst.__next__()
2
>>> iter_lst.__next__()
3
>>> iter_lst.__next__()
4
>>> iter_lst.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
每执行一次 __next__()
方法,就将 iter_lst
中的一个成员呈现出来,也就是将该成员读入到内存——这是迭代器的特点,也是与列表、字典等可迭代对象的不同之处。迭代器 iter_lst
比列表 lst
节省内存。对于迭代器对象,内存中虽然已经有了它,但对象的成员没有占用内存空间。而列表一经创建之后,其所有成员已经被读入了内存。
要想使用迭代器的成员,必须执行迭代器的 __next__()
方法,且需要多少个成员就执行多少次(按照从左向右的方向排序),当读取到最后一个成员之后,到了迭代器结束位置,再执行 __next__()
方法,就抛出 StopIteration
异常。
一个一个地执行 __next__()
方法太麻烦,替代方法就是使用循环语句。如接续上述操作(这句话很重要,必须是接续上面的操作,才能有下面操作的效果),继续执行:
>>> for i in iter_lst: print(i)
...
没有任何打印结果——如果打印出了 iter_lst
的成员,说明你一定搞错了或者没有按照前面的要求做。
为什么会没有任何结果?甚至连异常信息都没有。
这又是 iter_lst
与 lst
的另一项区别。在迭代器中,有一个“指针”(注意,这里加了引号),它指到哪个成员,在执行 __next__()
方法时就将该成员读入内存,“指针”随后指向下一个成员。当最后一个成员被读入内存后,“指针”也移动到了迭代器的最后。但是,这个“指针”不会自动“掉头”回到最开始的位置,犹如“过河的卒子”。
再来看前面的操作,在执行若干次 iter_lst.__next__()
后,“指针”已经移动到了 iter_lst
最后。若用 for 循环再次试图从 iter_lst
读取成员,但“指针”不走回头路,所以什么也读不到。
另外,没有抛出异常,其原因在于 for 循环会自动捕获 StopIteration
异常信息,并进行处理——后面会看到这个效果。
>>> iter_lst = iter(lst) # (1)
>>> for i in iter_lst: print(i) # (2)
...
1
2
3
4
>>> for i in iter_lst: print(i) # (3)
...
注释(1)重新生成一个迭代器,然后用注释(2)的 for 循环读取其成员。如果再次循环(如注释(3)所示),也没有读到内容,原因同前。
列表 lst
则不然,可以反复多次使用循环语句读取其成员,每次都“不走空”。
>>> for i in lst: print(i, end=",")
...
1,2,3,4,>>>
>>> for i in lst: print(i, end=",")
...
1,2,3,4,>>>
使用迭代器的 __next__()
方法能够将迭代器成员读入内存,在 Python 中还有一个内置函数也实现此功能,即 next()
函数。以下是接续注释(3)操作:
>>> next(iter_lst)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
此处抛出异常的原因与执行 iter_lst.__next__()
抛出异常一样。
>>> iter_lst = iter(lst)
>>> next(iter_lst)
1
>>> next(iter_lst)
2
>>> iter_lst.__next__()
3
内置函数 next()
就是迭代器的 __next__()
方法的函数实现,如同内置函数 len()
实现了 __len__()
方法一样(参阅9.3节)。
从第6章6.3节学习了 for 循环之后,它就经常出现在程序中,现在要基于对迭代器的理解,从更深层次研究 for 循环。
>>> lst
[1, 2, 3, 4]
>>> import dis
>>> dis.dis("for i in lst: pass")
1 0 LOAD_NAME 0 (lst)
2 GET_ITER
>> 4 FOR_ITER 4 (to 10)
6 STORE_NAME 1 (i)
8 JUMP_ABSOLUTE 4
>> 10 LOAD_CONST 0 (None)
12 RETURN_VALUE
此处使用的 dis
模块是 Python 标准库一员,其作用是将 Python 代码反汇编成字节码,字节码类似汇编指令,一个 Python 语句会对应若干字节码指令,虚拟机一条一条执行字节码指令,从而完成程序执行。
通过 dis.dis("for i in lst: pass")
所得到的就是执行 for 循环的字节码。主要看其中的两处:
GET_ITER
,其作用等同于iter(lst)
;FOR_ITER
,其相当于使用了next()
,依次获取每个成员。
这说明 for 循环会先将被循环对象转化为迭代器,然后调用 __next__()
方法(即使用 next()
函数)依次获取每个成员。
前面操作中使用的迭代器是用 iter()
函数生成,注意该函数的参数必须是可迭代对象,或者说这个函数只能将可迭代对象转化为迭代器。“任何对象都可以自定义”,这是我们从第8章以来已经逐步确立的观念。那么如何自定义一个迭代器?
#coding:utf-8
'''
filename: iterator.py
'''
class MyRange:
def __init__(self, n):
self.i = 1
self.n = n
def __iter__(self):
return self
def __next__(self):
if self.i <= self.n:
i = self.i
self.i += 1
return i
else:
raise StopIteration()
if __name__ == "__main__":
print(f"range(7): {list(range(7))}")
print(f"MyRange(7): {[i for i in MyRange(7)]}")
执行结果:
% python iterator.py
range(7): [0, 1, 2, 3, 4, 5, 6]
MyRange(7): [1, 2, 3, 4, 5, 6, 7]
此处定义的类 MyRange
的实例对象有点类似 range()
函数返回的对象,但二者也有差别,通过比较执行结果很容易看出来。造成此区别的操作之一是在类 MyRange
的初始化方法中以 self.i = 1
确定以整数 1
作为计数起点,而不是 0
。
另外,在 __next__()
方法中以 self.i <= self.n
作为判断条件(注意等号),从而将实例化参数值也包含在了迭代器返回值范围。
再观察类 MyRange
内的方法,__iter__()
和 __next__()
是迭代器的标志,在类中定义了这两个方法,就得到了能生成迭代器的类。
在第7章7.1.2节曾经写过斐波那契数列函数。这里再次编写一个实现斐波那契数的程序,只不过是用迭代器实现。
参考代码如下:
#coding: utf-8
'''
filename: fibsiterator.py
'''
class Fibs:
def __init__(self, max):
self.max = max
self.a = 0
self.b = 1
def __iter__(self):
return self
def __next__(self):
fib = self.a
if fib > self.max:
raise StopIteration
self.a, self.b = self.b, self.a + self.b
return fib
if __name__ == "__main__":
fibs = Fibs(100000) # (4)
lst = [ fibs.__next__() for i in range(10)]
print(lst)
运行结果如下:
% python fibsiterator.py
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
注释(4)利用定义的类 Fibs
创建的斐波那契数列对象,包含了很多项(读者还可以设置更大的参数)。但是,因为迭代器的特点,那些斐波那契数在执行后面的列表解析之前没有一个被读入内存。只有当执行了后面的列表解析,才有指定数量的数字被读入内存,并组合成了一个列表。
在 Python 标准库中,还有一个与迭代器密切相关的模块 itertools
,在此也简要给予介绍。
>>> import itertools
>>> counter = itertools.count(start = 7)
变量 counter
所引用的对象是从整数 7
开始,步长为 1
的迭代器对象,即通过它可以得到从 7
开始的整数。
>>> next(counter)
7
>>> next(counter)
8
>>> next(counter)
9
其实,counter
是生成器(参阅9.7节),读者可以通过帮助文档查阅其详细说明。
除了能“线性迭代”之外,还能创建“循环迭代”的迭代器。
>>> colors = itertools.cycle(["red", "yellow", "green"])
>>> next(colors)
'red'
>>> next(colors)
'yellow'
>>> next(colors)
'green'
>>> next(colors)
'red'
>>> next(colors)
'yellow'
现在得到的 colors
迭代器就能实现“循环迭代”。
★ 自学建议 随着掌握的知识越来越多,可能有一种跃跃欲试的感觉,急切地盼望着开发一个“改变世界”的程序。 心情可以理解,行动还要谨慎。如果有了一个创意,特别建议先用自己所熟悉的自然语言写出来,并用严格的逻辑描述解决问题的过程。写完之后,还要反复斟酌,检查自己的逻辑是否有问题。这是编程的基础。
相关文章
- 图像处理工具Python扩展库,你了解吗?
- 十个常用的损失函数解释以及Python代码实现
- 30 个数据科学工作中必备的 Python 包
- 如何在 Windows 上安装 Python
- 几行 Python 代码就可以提取数百个时间序列特征
- 使用Python快速搭建接口自动化测试脚本实战总结
- 哪种编程语言最适合开发网页抓取工具?
- 不要在 Python 中使用循环,这些方法其实更棒!
- 震惊!用Python探索《红楼梦》的人物关系!
- 如何最简单、通俗地理解Python模块?
- 酷炫,Python实现交通数据可视化!
- 为什么急于寻找Python的替代者?
- 30 个数据工程必备的Python 包
- 去字节面试被面这题能答上来吗?谈谈你对时间轮的理解?
- 火山引擎在行为分析场景下的 ClickHouse JOIN 优化
- 用Python爬取了某宝1166家月饼数据进行可视化分析,终于找到最好吃的月饼~
- 在 Linux 上试试这个基于 Python 的文件管理器
- Python列表解析式到底该怎么用?
- 如何快速把你的 Python 代码变为 API
- 十个Python初学者常犯的错误