迭代

迭代这个概念,在很多编程语言当中都是存在的。说白了,就是对一个『可迭代对象』进行遍历的过程。如 for 循环,while 循环等等,都是对一个对象进行迭代操作。那么这个『可迭代对象』到底是什么呢?

可迭代对象

简单来说,可迭代对象就是一个具有 __next__方法的对象。当这个对象被用在 for 循环等一系列迭代的场景的时候,这个方法就会起到相应的作用。如,python 当中的文件对象想按照逐行的顺序来进行迭代的话,有以下几种方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
	# 1
	for line in open('test.py'):
		print(line.upper(), end='')
		
	# 2
	for line in open('test.py').readlines():
		print(line.upper(), end='')
		
	# 3
	
	while True:
		line = file.readLine()
		if not line:
			beak;
		print(line.upper(), end='')

首先第一种方式,应该是迭代一个文件对象的最优选择:

  1. 该对象在循环中自动调用__next__方法,逐行读取文件,不会浪费内存
  2. 调用迭代器,在 python 中几乎是以 C 语言的速度在执行的

第二种方式,一个明显的缺点,就是 readlines 方法将文件中所有的内存一次性都加载到了内存中,形成了一个以每一行内容为一个元素的字符串列表。如果文本内容过大超过了机身内存的大小,很可能会出现意想不到的问题。

第三种,虽然没有内存方面的问题,但是比起在 for 循环使用迭代器进行处理,while 循环是在python 虚拟机当中运行 python 的字节码,所以在速度上,相较第一种中方式,还是差了一些。

迭代器

关于迭代的第三个概念,我们之前说过,迭代是一种行为,可迭代对象是可以在其上进行迭代行为的一个对象,也就是一个具有__next__方法的对象。之所以对于可迭代对象有这样的一个定义,是因为之前讨论的文件对象比较特殊,文件对象本身是自己的一个迭代器。也就是说,真正具有__next__方法的对象应该是迭代器,而不是我们之前所谓的可迭代对象。可迭代对象的范围应该比迭代器更大,以列表举例,它本身是没有迭代器的,但是列表依然是 python 当中可迭代的对象之一,那么列表这个对象的__next__方法从何而来呢?是通过一个叫做 iter 的方法得到的,该方法接受一个对象,返回一个具有next方法的迭代器,next方法在内部会调用迭代器的__next__方法。

所以,for 循环在处理列表这类可以迭代但是本身不是迭代器的对象时,都会将列表这个对象传递给内置函数 iter,得到一个具有 next 方法的迭代器,然后每次循环调用next方法来迭代对象。

除了之前说的列表和文件对象之外,在遍历一个字典的 key 的集合时,也可以使用迭代器来进行操作。之前的 python 版本中,在遍历字典键的时候都会使用keys()方法来获取一个字典键的列表,使用 for 循环迭代。目前,可以直接使用for key in dict的形式来进行迭代。

对于可迭代对象,需要提及一点,python2.x 和 python3.x版本中,对于 zip 和 range 两个函数生成的结果是有很大不同的。2.x 版本中 zip 和 range 生成的都是一个 list,但是在3.x 版本中生成的则是一个可迭代的对象。也就是说在3.x 版本中,如果你想得到 zip 和 range 两个函数调用结果内的元素的话,都需要额外的迭代操作,而不能一次性直接得到最终结果。这种惰性求值的理念,非常适合当 zip 和 range 生成的结果数据量都比较大的时候。

除了上面说到的,range,zip 等内置函数生成的可迭代对象在不同版本有一些不同之处之外。range 生成的可迭代对象和 zip, filter 等函数生成的可迭代对象也有不小的差异。对于 zip, filter, map等内置函数生成的可迭代对象,他们都是自带迭代器的,和上面我们说到的文件对象是一样的。他们返回的结果,在一些迭代的场景下,如 for 循环中,可以直接进行迭代行为,而不需要把他们调用的结果传递给他 iter 函数获取迭代器之后再进行迭代。这就引发了一个问题,zip 等函数生成的迭代器只能够使用一次,也就是说,必须从头迭代到尾,没有其他机会在可迭代对象的基础上,生成多个不同位置的迭代器,进行迭代。说起来可能比较抽象,看一下例程应该会清楚很多

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# running in python3.x
>>> a = zip((1,2), (3, 4))
>>> type(a)
<class 'zip'>
>>> ita1 = iter(a)
>>> ita2 = iter(a)
>>> type(ita1)
<class 'zip'>
>>> ita1.__next__()
(1, 3)
>>> ita2.__next__()
(2, 4)


>>> b = range(1, 4)
>>> type(b)
<class 'range'>
>>> itb1 = iter(b)
>>> itb2 = iter(b)
>>> itb1.__next__()
1
>>> itb1.__next__()
2
>>> itb2.__next__()
1
>>> itb2.__next__()
2
>>>

例程当中对于 zip 和 range 生成的可迭代对象分别创建了两个迭代器,zip 返回的可迭代对象本身就是一个迭代器,所以在整个迭代范围内,即使创建了两个,但是看起来行为使用的确是同一个迭代器。因为例程的本意是想两个迭代器的迭代步调不一致的。对于 range 返回的可迭代对象就比较符合我们预期的行为,range 调用返回的对象并不是一个迭代器,所以需要通过 iter 函数来构建,在这种情形下,每个迭代器都是独立的,所以可以有属于自己的迭代位置。