从循环说起
顺序,分支,循环是编程语言的三大逻辑结构,在Python中都得到了支持,而Python更是为循环结构提供了非常便利的语法:for ... in ...
刚从C语言转入Python的同学可能倾向于写索引下标式的循环,例如下面的代码像遍历C中的数组一样遍历了一个Python中的列表:
>>> colors = ['black', 'white', 'red', 'blue'] >>> for i in range(len(colors)): ... print(colors[i]) ... black white red blue
但如果将列表(list)替换为集合(set),这个方法就不奏效了:
>>> colors = set(['black', 'white', 'red', 'blue']) >>> for i in range(len(colors)): ... print(colors[i]) ... Traceback (most recent call last): File "<stdin>", line 2, in <module> TypeError: 'set' object does not support indexing
这是因为set不像list,string,tuple这些结构,其中的元素从逻辑上讲本身是没有序的,与之类似的还有dict,因而Python在设计set这个内置数据结构时,并没有实现其下标索引。但遍历集合这个操作本身是合理并且常见的,我们可以如下实现对集合的遍历:
>>> colors = set(['black', 'white', 'red', 'blue']) >>> for color in colors: ... print(color) ... red blue black white
至于元素输出的顺序为何是这样,这与set的具体实现有关,不在本文的讨论范围内,本文要探究的是,这种for...in...
循环到底是如何运作的。
轮到你出场了,迭代器
想知道for循环的运作模式,首先要介绍迭代器(iterator)的概念。迭代器并不是Python独有的概念,事实上,在C++,Java等其他语言中,都可以看到迭代器的身影,站在更高的角度,迭代器不是一个语言特性,而是一种设计模式,它提升了语言的抽象能力和代码的服用,减少程序员的心智负担。为证明这一点,下面我们就以Python中的迭代器进行说明。
在Python中,迭代器泛指一类实现了迭代器协议的对象,具体来说,任何实现了__next__
函数的对象都是迭代器,该函数可以通过Python的内置函数next
进行调用,该函数的逻辑应该返回数据结构中的下一个迭代对象或抛出StopIteration
异常。以下代码是列表迭代器的一个示例,我们暂时不关心该列表迭代器是怎么来的。
>>> colors = ['black', 'white', 'red', 'blue'] >>> color_iterator = iter(colors) >>> next(color_iterator) 'black' >>> next(color_iterator) 'white' >>> next(color_iterator) 'red' >>> next(color_iterator) 'blue' >>> next(color_iterator) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
可以看到,每次对迭代器调用next
函数,迭代器或返回应被迭代的下一个对象,或抛出一个StopIteration
异常,上层的调用者可通过捕获该异常得知迭代已经结束。
他来了,可迭代对象
可迭代对象(Iterable)是另一个重要概念,顾名思义,它应该泛指满足可以被迭代,即拥有迭代器的一类对象。在Python的迭代器协议里,任何实现了__iter__
函数的对象都是可迭代对象,该函数可以通过Python的内置函数iter
调用,该函数逻辑上应该返回一个迭代器对象,即返回一个实现了__next__
函数的对象。我们还是以列表作为例子。
>>> colors = ['black', 'white', 'red', 'blue'] >>> type(colors) <class 'list'> >>> color_iterator = iter(colors) >>> type(color_iterator) <class 'list_iterator'>
可以看到,colors
和color_iterator
是两个不同的对象,后者是前者的__iter__
函数返回的一个新对象。
为什么要这么复杂?
你可能已经在问了,为什么要制定这么复杂的所谓协议?是的,对于每一个类来说,按照规定实现__next__
和__iter__
方法听上去很麻烦。但是,当很多类都遵循这个协议麻烦一点的时候,对于调用者而言,事情开始变得简单。换句话说,当所有逻辑上可以被迭代的对象都告诉你,”我已经按照迭代器协议实现了协议里要求的方法“,作为调用者,你不必再关心他们怎么实现的,你可以用一个统一的方式去迭代他们,无论这是一个列表,一个字符串,一个集合,还是一个字典。
事实上,for...in...
就是这么做的。
揭秘for循环
Python的for...in...
循环实际上等价于以下代码
colors = ['black', 'white', 'red', 'blue'] for color in colors: print(color) # 等价于 color_iterator = iter(colors) while True: try: color = next(color_iterator) print(color) except StopIteration: break
当我们毫无顾忌地对列表/字符串/集合/字典/...各式各样的数据结构for...in...
时,Python并不是像魔法师一样天然地知道这些对象该怎么去遍历,而是按部就班地用iter
取出迭代器,用next
取出下一个迭代对象,如果捕获StopIteration
,就停止迭代。所以,这些功劳来自于这些对象严格地遵从了迭代器协议。
自己实现一个可迭代对象
当我们自定义类时,是不是也应该考虑调用者的感受?下面我们就自己实现一个符合迭代器协议的类。
class MyIterable(): def __init__(self): self.data = ['I', 'love', 'python'] def __iter__(self): self.index = 0 return self def __next__(self): if self.index < len(self.data): ret = self.data[self.index] self.index += 1 return ret else: raise StopIteration a = MyIterable() for x in a: print(x) ''' 执行结果: I love Python '''
解释一下:MyIterable
的数据存放在成员变量data
中,即实现了__next__
的方法,所以本身就是一个迭代器,因而它的__iter__
只需要返回self
,但在返回之前,将成员变量index
归0,意味着从头开始迭代。在__next__
中,如果index
比元素总数少,则返回该下标索引的数据,并使得索引自增,否则就抛出StopIteration
表示迭代结束。
可以看到,在调用者看来,MyIterable
的数据内部是如何存放的,一共有多少数据,该怎么访问他们,这些都不用关心,只要MyIterable
老老实实地遵守了迭代器协议,一句for...in...
就能进行遍历。
总结
我们通过深入解析Pyhton中for
循环的工作机制,接触了迭代器,可迭代对象这些Python中乃至通用编程中重要的概念。可以说,迭代器具体来说是语言所规定的一种协议,一个具体的对象实现了协议所规定的接口后,就能让自己看上去是一个迭代器,使用者使用起来也只需要按照接口去使用,减少了心智负担;更抽象来说,迭代器是一种设计模式,对于遍历这种编程中非常常用的操作,通过指定某种模式和规则,让代码的开发和调用都变得更加规范,更加方便。
迭代器在Python中的使用当然不止for
循环这么简单,在生成器等更高级的语法糖中,迭代器也扮演至关重要的角色,欢迎大家持续关注!
获取最新文章更新,欢迎关注我的个人公众号:StackOverflow Daily