1 迭代器与for
迭代器(iterator),在许多现代编程语言,如C++,Java等中均有出现。迭代器不是一种数据结构,而可以理解为是一种获取、访问数据的策略。在Python中,迭代器以object的形式存在,其自身并不存储数据,而是封装了获取数据的方法[即next方法(Python2)或__next__方法(Python3)]。这意味着:在Python中,所有的迭代器均可通过调用next方法,获取到迭代器的下一个数据。next方法应处理两种情况:1.迭代未结束时,根据规则返回当前位置的下一个数据;2.迭代结束时,抛出StopIteration异常,指示当前迭代器已耗尽,迭代终止。
Python中的for循环本质上即为针对迭代器使用的循环,任何数据结构在进行for循环时,均可以理解为for是在针对由此数据结构生成的一个迭代器进行循环,这包含两种情况:1.for循环的对象本身就是一个迭代器,那么此时就针对当前对象进行迭代;2.for循环的对象本身不是一个迭代器,那么for将自动对此对象调用iter函数(对于iter函数的讨论详见下文),返回当前对象的一个迭代器,然后对此迭代器进行迭代。同时,for循环将在每一次循环时,自动对迭代器调用next函数,得到下一个值,并将此值赋值给迭代变量,当迭代结束,迭代器抛出StopIteration时,for也将自动处理这个异常,结束当前的for循环。由此可见,Python的for循环,相较于C、Java、Perl等语言的“while变体形式”的三段式for循环更为高级与实用。但是,这样的for循环机制可能也是造成Python的for循环效率低下的原因之一。
此外,如果不通过for进行迭代,也可以手动多次调用next函数进行下一个值的获取,并手动处理异常。调用next函数对应于调用迭代器对象的next方法(Python2)或__next__方法(Python3)。这种用法本文不做详细讨论。
2 __iter__与委托迭代
上文中提到了iter函数,通过调用iter函数,可以得到某个对象的一个迭代器。而由于iter函数作为一种“多态性”函数(这里借用OOP术语来描述Python中的函数与特殊方法之间的关系),其可接受各种不同的数据对象,使用不同的算法返回迭代器。能够这样做的原因就在于iter函数背后所对应的__iter__方法。当对对象调用iter函数时,就相当于自动调用当前对象所对应的__iter__方法,即:
iter(xxx) 相当于 xxx.__iter__()
这样,即可通过面向对象的多态性,针对不同的对象,调用不同的__iter__方法了。
__iter__方法是迭代器协议的核心,但在很多的书籍和资料中,对__iter__方法的介绍很模糊,往往只是说明:__iter__方法应直接返回self,这是很不准确的。__iter__方法本质上对应着iter函数的调用,也就是说,这个方法将在for循环开始前被自动调用,其返回值应该为一个迭代器,这个返回的迭代器应实现__next__方法,以及在迭代结束时抛出StopIteration。这就意味着,__iter__方法的返回值有两类,一类就是self,即当前类自身就是迭代器,当返回self时,当前对象本身就还需要实现__next__方法,从而用于被for自动调用。另一种非常关键的情况是:__iter__方法也可以返回另一个可迭代对象,这样,当迭代当前对象时,实际上是迭代了__iter__方法返回的这另一个可迭代对象,这样的操作就称为委托迭代。以下通过两个例子说明自身迭代与委托迭代:
例1:自身迭代器:
class IterTest:
def __init__(self, topNum):
self.topNum = topNum
self.nowNum = -1
def __iter__(self):
return self
def __next__(self):
if self.nowNum < self.topNum:
self.nowNum += 1
return self.nowNum
else:
raise StopIteration
for i in IterTest(10):
print(i)
上述代码即定义了一个自定义的迭代器,IterTest类的实例自身就是一个迭代器,关键之处在于两点:1. __iter__方法直接返回self,也就是说,在当前类的实例被iter函数调用时,对象自身作为迭代器参与迭代,且对象自身将不停地被调用next函数,从而获取下一个值。2.next函数背后对应的就是__next__方法,此方法应实现两个功能,一是在迭代未结束时返回下一个值,二是在迭代结束时抛出StopIteration异常。实际使用中也可定义不会抛出StopIteration异常的迭代器,这样的迭代器就称为无限迭代器(infinite iterator),其在迭代中如果不加以限制,将永远不会结束。无限迭代器一般用于特殊用途,在itertools模块中有一些无限迭代器的接口定义。
以下是委托迭代的例子:
例2:委托迭代器:
class IterTest:
def __iter__(self):
return iter([1, 2, 3])
for i in IterTest():
print(i)
上述代码中,委托迭代器的__iter__方法返回的是另一个与当前对象并无关联的迭代器,在上例中,即为一个值为123的List的迭代器版本。for循环看上去迭代的是IterTest类实例,实际上迭代的却是一个与IterTest类毫不相关的list。但是,由于放在for循环当中的就是IterTest类实例,故不管迭代的是什么,IterTest类实例就是一个可迭代对象。
__iter__方法是Python迭代器协议的核心,任何一个类,只要其正确实现了此方法,返回了一个迭代器,那么这个类的实例就可以被迭代。而具体迭代什么,则由__iter__方法具体返回的内容决定。
3 __*item__与序列访问协议
__*item__包括__getitem__、__setitem__与__delitem__三个特殊方法。__delitem__是析构方法的一种,其应用场合较少,故本文不对其进行讨论,只讨论前两个特殊方法。
__getitem__方法在Python中有两个主要功能,首先,此方法是序列访问协议的核心,一个类只要实现了此方法,那么这个类的对象就可以通过类似于list的索引值语法,对当前类的一些数据进行访问。其次,__getitem__方法也是迭代器协议的备用方法,当一个类没有实现__iter__方法时,就会尝试通过__getitem__方法,按照索引值从0到溢出的顺序进行迭代访问,举例如下:
class IterTest:
numList = [1, 2, 3]
def __getitem__(self, sliceObj):
return IterTest.numList[sliceObj]
print(IterTest()[1:])
for i in IterTest():
print(i)
上例中定义了一个仅实现了__getitem__方法的类,此方法的第二参数为一个slice对象,slice是Python内部的一个特殊对象,其接受各种语法形式的切片(如[:], [1:2], [1:]等),此对象在索引访问时自动生成,且可直接放入一对方括号中进行值的访问。当一个类实现了__getitem__方法后,首先,这个类的实例可使用任何形式的切片语法,如上例中的IterTest()[1:];其次,__getitem__方法作为迭代器协议的备选方法,只实现此方法的对象也可通过for进行索引值从0到溢出的迭代。
只实现__getitem__方法的类,其通过索引值访问到的值是只读的,不可通过赋值进行修改。而如果要对值进行修改,则还需要实现__setitem__方法。__setitem__方法在绝大多数情况下的实现均很简单,只需要进行赋值行为定义即可:
def __setitem__(self, sliceObj, setValue):
IterTest.numList[sliceObj] = setValue
IterTest()[1] = 1
定义此方法后,就可以对通过索引值取出的值进行赋值了。
4 yield
yield是另一种定义迭代器的方法。而实际上,yield(包括send与Python3.5的yield from)还用于实现协程,这部分本文不予讨论,只讨论yield在迭代器中的应用。
上文中,主要讨论了OOP下的迭代器定义,而yield就可以简单理解为POP下对迭代器的定义,通过yield实现的迭代器在Python中称为生成器(Generator)。需要强调的是,生成器只是迭代器的一种,其语法上与迭代器没有任何区别。
通过yield定义生成器非常简单:如果一个函数中至少出现了一处yield语句,那么此函数的返回值将自动转换成一个生成器,即可以使用for或next进行迭代。yield在行为上类似于return,将一个值返回给外部,但不同的是,return语句具有立即退出函数调用的功能,而yield语句不会这样做,而是将函数调用过程暂停到yield之后的位置,当下一次的next方法被调用(或调用了send方法),那么函数就会在暂停处继续执行,直到又遇到了一次yield,或函数在别处退出。下面即定义了一个简单的生成器:
def yieldTest(topNum):
nowNum = 0
while nowNum < topNum:
yield nowNum
nowNum += 1
for i in yieldTest(10):
print(i)
上例的yieldTest函数,由于出现了yield,则此函数的返回值就是一个生成器。此生成器在没有达到数字上限时,会不断返回递增的数字,然后暂停,等待下一次的返回,在while循环退出后,函数结束,生成器也就随之耗尽。
5 生成器推导式
上文中讨论了如何通过含有yield的函数定义一个生成器。而Python的推导式语法系列也包含生成器推导式,可以通过简洁的形式,直接生成一个生成器。生成器推导式以一对小括号包围,中间书写推导式语法即可:
for i in (i**2 for i in range(10)):
print(i)
6 慎用迭代器
迭代器具有一些很优良的性质,如节约内存,运算速度快等。但在使用迭代器时,一定要考虑迭代器的两个最重要的隐患:
1. 迭代器是一次性的。普通的迭代器(除了文件句柄这样的特殊迭代器),如果不做特殊处理,则其都是一次性使用的,无法回退。当迭代器耗尽后,后续的next函数调用只会不断的抛出StopIteration异常。如果还要重新使用当前的迭代器,则只能通过原始数据再生成一个新的迭代器。这在某些情况下可能会导致效率低下和难以察觉的bug。
2. 对迭代器进行索引值访问是非常低效,且消耗迭代器的。故涉及到索引值访问这样的操作时,一般不应使用迭代器,而是将迭代器通过转换为list再进行操作。
2018年6月于苏州