for循环能够遍历列表、元组、字符串等,然而不仅仅有这些,更广泛的讲,for循环能够遍历所有可迭代对象( iterable objects )。可迭代对象包括物理序列( physical sequence )和 虚拟序列 (virtual sequence )
物理序列:指物理意义上按顺序存储的数列,例如列表、元组等
虚拟序列:指在for循环之类的迭代工具中每次产生一个结果
迭代工具(iteration tool):例如for循环、列表推导式(list comprehension)、 in 、map等。
文件迭代器(File iterator):
任何对象有 __next__ 方法并且在结束时能够抛出 StopIteration 异常的都叫做迭代器(iterator), 这类对象也能够通过for循环或者其他迭代工具一步一步的获取结果。因为所有的迭代工具在内部的工作原理都是在每次迭代中调用 __next__ 方法并且通过抓取 StopIteration 异常决定什么时间结束。
>>> f = open('/home/coder/Documents/script.py')
>>> f.__next__()
'import sys\n'
>>> f.__next__()
'print(sys.path)\n'
>>> f.__next__()
'x = 2\n'
>>> f.__next__()
'print(x ** 32)\n'
>>> f.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> for line in open('/home/coder/Documents/script.py'):
... print(line.upper(), end='')
...
IMPORT SYS
PRINT(SYS.PATH)
X = 2
PRINT(X ** 32)
第一个例子是文件对象调用 __next__ 方法输出下一行,并在结束时抛出 StopIteration 异常,这样的迭代器也能够通过迭代工具实现同样的效果,即第二个例子。这种方法是一行一行读取文件的最好方法。文件对象的readline()方法和readlines()方法,值得注意的是readline()在结束时读取空字符串,而readlines()方法一次加载完整个文件,当读取大文件时会遇到麻烦。当然,我们也可以用while实现同样的效果:
>>> while True:
... line = f.readline()
... if not line: break
... print(line.upper(), end='')
...
IMPORT SYS
PRINT(SYS.PATH)
X = 2
PRINT(X ** 32)
但是,while循环的版本是在Python虚拟机中运行的python字节代码,迭代器的版本在python内部是以C语言的速度运行的,,运行起来更快。
迭代协议(iteration protocol):
当for循环开始后,它先把可迭代对象(iterable object)传递给内建函数iter,然后获得一个迭代器(iterator)。iter 返回的对象有 next()方法。iter函数在内部运行 __iter__ 方法,就像next() 在内部运行__next__方法一样。下图是一个完整的迭代协议:
一些对象既是迭代工具(iteration context tool)也是可迭代对象,包括生成器(generator expression)、Python3.x中的map ,zip,range和一些字典方法-----为了避免一次把所有结果加载到内存中。
通过迭代协议可知,如果用for循环迭代一个列表,第一步如何做很明显:
>>> L =[1, 2, 3]
>>> I = iter(L)
>>> I.__next__()
1
>>> I.__next__()
2
>>> I.__next__()
3
>>> I.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
初始化的第一步对与文件对象来说并不需要,因为文件对象就是他们自己的迭代器,因为他们仅仅支持一次迭代,他们不能够指针倒退去多次扫描,文件有自己的next方法。
>>> f = open('/home/coder/Documents/script.py')
>>> iter(f) is f
True
>>> iter(f) is f.__iter__()
True
>>> f.__next__()
'import sys\n'
列表和其他一些内置对象并没有自己的迭代器,他们支持多重迭代,例如他们可以在嵌套循环中多重迭代并且从不同的位置开始,对于这样的对象,我们开始迭代之前必须要调用 iter
>>> L = [1, 2, 3]
>>> iter(L) is L
False
>>> L.__next__()
AttributeError: 'list' object has no attribute '__next__'
>>> I = iter(L)
>>> I.__next__()
1
>>> next(I)
2
手动迭代(Manual iteration):
>>> L = [1, 2, 3]
>>> for x in L: #自动迭代
... print(x**2,end='') #获得iter,调用__next__,捕获异常
...
149
>>> I = iter(L) #手动迭代,这一步在for内自动完成
>>> while True:
... try: #捕获异常
... x = next(I) #调用next
... except StopIteration:
... break
... print(x**2,end='')
...
149
其他可迭代对象:
在新版本的Python中,字典时可迭代的,每次自动返回一个键,这意味着
>>> D = {'a':1, 'b':2, 'c':3}
>>> for key in D:
... print(key, D[key])
...
a 1
b 2
c 3
>>> R = range(5)
>>> R # Ranges 在Python3.x中是可迭代的
range(0, 5)
>>> I = iter(R) # 通过迭代协议获取值
>>> next(I)
0
>>> next(I)
1
>>> list(range(5)) # 或者通过list()方法一次获取所有值
[0, 1, 2, 3, 4]
>>> E = enumerate('spam') # enumerate也是可迭代的
>>>> E
<enumerate object at 0x00000000029B7678>
>>> I = iter(E)
>>> next(I) # 通过迭代获取值protocol
(0, 's')
>>> next(I)
(1, 'p')
>>> list(enumerate('spam')) # 一次获得所有值
[(0, 's'), (1, 'p'), (2, 'a'), (3, 'm')]
列表推导式(list comprehension):
事实上,列表推导式并不是不可或缺的,我们可以用for逻辑块实现同样的功能,但是列表推导式看起来更简单明了,运行起来更快。当我们思考何如对一个序列中的每个元素执行相同的操作时,列表推导式就是一个很好的方法。
>>>L = [x**2 for x in range(10)]
>>> [x + y for x in 'abd' for y in 'imn']
['ai', 'am', 'an', 'bi', 'bm', 'bn', 'di', 'dm', 'dn']
列表推导式应用于文件:
>>> lines = [line.rstrip() for line in open('/home/coder/Documents/script.py')]
>>> lines
['import sys', 'print(sys.path)', 'x = 2', 'print(x ** 32)']
>>> lines = [line.rstrip() for line in open('/home/coder/Documents/script.py') if line[0] == 'p']
>>> lines
['print(sys.path)', 'print(x ** 32)']
>>> [line.rstrip() for line in open('/home/coder/Documents/script.py') if line.rstrip()[-1].isdigit()]
['x = 2']
>>> len([line for line in open('/home/coder/Documents/script.py') if line.strip != '' ])
4
Python3.X中新的可迭代对象:
Range:
在3.x中,range() 函数返回的是一个可迭代对象,它会根据需要产生结果,而不是生成一个储存在内存中列表。
>>> R = range(10)
>>> R
range(0, 10)
>>> I = iter(R)
>>> next(I)
0
>>> list(R)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
在3.x中range()仅支持 迭代、索引和len(),不支持其他序列操作
map、zip、filter:
map,zip,filter不仅能够处理迭代,而且他们返回的结果也是可迭代的,和reange不同,他们是自己的迭代器(还记得文件对象么?他也是自己的迭代器)
>>> M = map(abs,(-1, 0, 1))
>>> M
<map object at 0x7fe05c83cac8>
>>> next(M)
1
>>> next(M)
0
>>> next(M)
1
>>> next(M)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> M = map(abs,(-1, 0, 1))
>>> for x in M:
... print(x)
...
1
0
1
>>> Z = zip((1, 2, 3),(10, 20, 30))
>>> Z
<zip object at 0x7fe05c847188>
>>> list(Z)
[(1, 10), (2, 20), (3, 30)]
>>> Z = zip((1, 2, 3),(10, 20, 30))
>>> for pair in Z: print(pair)
...
(1, 10)
(2, 20)
(3, 30)
>>> Z = zip((1, 2, 3),(10, 20, 30))
>>> next(Z)
(1, 10)
以上内容来源于Learning Python 5th–Partiii— Chapter14
28092017总结:
- 每个内置工具都是从左到右使用迭代协议扫描对象
- 手动迭代分两步,第一步调用iter返回迭代器,第二部调用__next__并捕获StopIteration,有自己的迭代器的可迭代对象不需要第一步,如文件、map、zip等
- for循环之类的迭代工具能够自动完成2的第一步,所以迭代工具可以处理可迭代对象和迭代器,并且看不出来差别
- 在很多情况下当迭代时仅有一次扫描的时候,可迭代对象和迭代器没有区别,迭代器只是临时产生并被迭代工具使用
- 可迭代对象的作用是在需要的时候产生结果而不用一次吧所有的结果都加载进内存,节省空间,提高速度(此条存疑)