本系列目的:一篇文章,不求鞭辟入里,但使驾轻就熟。python
迭代是数据处理的基石,在扫描内存没法装载的数据集时,咱们须要一种惰性获取数据的能力(即一次获取一部分数据到内存)。在Python中,具备这种能力的对象就是迭代器。生成器是迭代器的一种特殊表现形式。git
我的认为生成器是Python中最有用的高级特性之一(甚至没有之一)。虽然初级编码中使用寥寥,但随着学习深刻,会发现生成器是协程,异步等高级知识的基石。Python最有野心的asyncio库,就是用协程砌造的。github
注:生成器和协程本质相同。PEP342(Python加强提案)增长了生成器的send()方法,使其变身为协程。如此以后,生成器生成数据,协程消费数据。虽然本质相同,可是因为从理念上说协程跟迭代没有关系,而且纠缠生成器和协程的区别与联系会引爆本身的大脑,因此应该将这两个概念区分。此处说本质相赞成为:理解生成器原理以后,理解增长了send方法,可是实现方式几乎相同的协程会更加轻松(这段话看不懂没有关系,船到桥头天然直,学到协程天然懂)。面试
Python的一致性是其最迷人的地方。了解了Python生成器,迭代器的实现。就会对Python的一致性设计有更增强烈的感知。本文读完以后,遇到面试官提问为何列表能够迭代,字典能够迭代,甚至文本文件均可以迭代时,你就能够稳(huang)得一批。异步
阅读本文以前,若是你对Python的一致性有一些了解,如鸭子类型,或者Cpython的PyObject结构体,那真是太棒了。不过鉴于笔者深厚的文字功底,没有这些知识也不打紧。async
迭代器函数
在学习生成器以前,先要了解迭代器。顾名思义,迭代器即具备迭代功能的对象。在Python中,能够认为迭代器能够经过不断迭代,产生出一个又一个的对象。学习
可迭代对象和迭代器编码
Python的一致性是靠协议支撑的。一个对象只要遵循如下协议,它就是一个可迭代对象或迭代器。线程
Python中的一个对象,若是实现了iter方法,而且iter方法返回一个迭代器,那么它就是可迭代对象。若是实现了iter和next方法,而且iter方法返回一个迭代器,那么它就是迭代器(有点绕,按住不表,继续学习)。
注:若是对象实现了__getitem__方法,而且索引从0开始,那么也是可迭代对象。此hack为兼容性考虑。只需切记,若是你要实现可迭代对象和可迭代器,那么请遵循以上协议。
可迭代对象的iter返回迭代器,迭代器的iter方法返回自身(也是迭代器),迭代器的next方法实现迭代功能,不断返回下一个元素,或者在元素为空时raise一个StopIteration终止迭代。
可迭代对象与迭代器的关系
话很少说,上代码。
class Iterable: def __init__(self, *args): self.items = args def __iter__(self): return Iterator(self.items) class Iterator: def __init__(self, items): self.items = items self.index = 0 def __iter__(self): return self def __next__(self): try: item = self.items[self.index] except IndexError: raise StopIteration() self.index += 1 return item ins = Iterable(1,2,3,4,5) # 1 for i in ins: print(i) print('the end...') >>> # 2 1 2 3 4 5 the end ...
上述代码中,实现了可迭代对象Iterable和迭代器Iterator。遵循协议规定,Iterable实现了iter方法,且iter方法返回迭代器Iterator实例,迭代器实现了iter方法和next方法,iter返回自身(即sel,迭代器自己f),next方法返回迭代器中的元素或者引起StopIteration异常。运行上述代码,会看到#2处的输出。
经过上述代码迭代一个对象显得十分啰嗦。好比在Iterable中,iter必需要返回一个迭代器。为何不能直接用Iterator迭代元素呢?假设咱们经过迭代器来迭代元素,将上述代码中的#1处以下代码:
ins = Iterator([1,2,3,4,5]) for i in ins: # 3 print(i) for i in ins: # 4 print(i) next(ins) # 5 print('the end...') >>> # 6 1 2 3 4 5 ... File "/home/disk/test/a.py", line 20, in __next__ # 7 raise StopIteration() the end...
运行上述代码,会看到#6处的输出。疑惑的是,#3和#4处运行了两次for循环,结果只打印一遍全部元素。解释以下:
上述代码中,ins是一个Iterator迭代器对象。那么ins符合迭代器协议:每次调用next,会返回下一个元素,直到迭代器元素为空,raise一个StopIteration异常。
#3处第一次经过for循环迭代ins,至关于不断调用ins的next方法,不断返回下一个元素,输出如#6所示。当元素为空时,迭代器raise了StopIterator。而这个异常会被for循环捕获,不会暴露给用户,因此咱们就认为数据迭代完成,而且没有出现异常。
迭代器ins内的元素已经被#3处的for循环消耗完,而且raise了StopIteration(只不过被for循环捕获静默处理,没有暴露给用户)。此时ins已是元素消耗殆尽的“空”状态。在#4处第二次经过for循环迭代ins,由于ins内的元素为空,继续调用ins的next方法,那么仍是会raise一个StopIteration,并且又被for循环静默处理,因此没有异常,也没有输出。
接下来,#5处经过next方法获取ins的下一个元素,同上,继续raise一个StopIteration异常。因为此处经过next调用而不是for循环,异常不会被处理,因此抛出到用户层面,即#7输出。
从新编写上述代码中#3处for循环和#4处for循环,能够看到对应输出验证了咱们的结论。第一次for循环在迭代到元素为2时跳出循环,第二次for循环继续迭代同一个迭代器,那么会继续上次迭代器结束位置继续迭代元素。代码以下:
ins = Iterator([1,2,3,4,5]) print('the first for:') for i in ins: # 3 the first for print(i) if i == 2: break print('the second for:') for i in ins: # 4 the second for print(i) print('the end...') >>> # the output the first for: 1 2 the second for: 3 4 5 the end...
因此咱们能够获得以下结论:
迭代器支持
引用流畅的Python中的原话,迭代器支持如下6个功能。因为篇幅所限,点到为止。你们只要理解了迭代器的原理,理解如下功能天然是水到渠成。
for循环
上述代码已经有举例,可参考
构建和扩展集合类型
from collections improt abc class NewIterator(abc.Iterator): pass # 放飞自我,实现新的类型
列表推导,字典推导和集合推导
l = [i for i in range(10)] # list d = {i:i for i in range(10)} # dict s = {i for i in range(10)} # set
遍历文本文件
with open ('a.txt') as f: for line in f: print(line)
元祖拆包
for i, j in [(1, 2), (3, 4)]: print(i, j) >>> 1 2 3 4
调用函数时,使用*拆包实参
def func(a, b, c): print(a, b, c) func(*[1, 2, 3]) # 会将[1, 2, 3]这个list拆开成三个实参,对应a, b, c三个形参传给func函数
生成器
Python之禅曾经说过,simple is better than complex。鉴于以上代码中迭代器复杂的实现方式。Python提供了一个更加pythonic的实现方式——生成器。生成器函数就是含有yield关键字的函数(目前这种说法是正确的,以后会学到yield from等句法,那么这个说法就就须要更正了),生成器对象就是调用生成器函数返回的对象。
生成器的实现
将上述代码修改成生成器实现,以下:
class Iterable: def __init__(self, *args): self.items = args def __iter__(self): # 8 for item in self.items: yield item ins = Iterable(1, 2, 3, 4, 5) print('the first for') for i in ins: print(i) print('the second for') for i in ins: print(i) print('the end...') >>> # 9 the first for 1 2 3 4 5 the second for 1 2 3 4 5 the end...
上述代码中,可迭代对象的iter方法并无只用了短短数行,就完成了以前Iterator迭代器功能,点赞!
yield关键字
要理解以上代码,就须要理解yield关键字,先来看如下最简单的生成器函数实现
def func(): yield 1 yield 2 yield 3 ins1 = func() ins2 = func() print(func) print(ins1) print(ins2) for i in ins1: print(i) for i in ins1: print(i) print(next(ins2)) print(next(ins2)) print(next(ins2)) print(next(ins2)) >>> <function func at 0x7fcb1e4bde18> <generator object func at 0x7fcb1cc7c0a0> <generator object func at 0x7fcb1cc7c0f8> 1 2 3 1 2 3 File "/home/disk/test/a.py", line 18, in <module> print(next(ins2)) StopIteration
从以上代码能够看出:
那么含有yield关键字的生成器函数体是如何执行的呢?请看以下代码:
def f_gen(): # 10 print('start') yield 1 # 11 print('stop') yield 2 # 12 print('next') yield 3 # 13 print('end') for i in f_gen(): # 14 print(i) >>> start 1 stop 2 next 3 end
从上述代码及其打印结果,咱们能够得出以下结论:
生成器表达式
将列表推导式中的[]改成(),即为生成器表达式。返回的是一个生成器对象。通常用户列表推导可是又不须要立马产生全部值的情景中。
gen = (i for i in range(10)) for i in gen: print(i) for i in gen: # 只能被消费一遍,第二遍无输出 print(i) print('the end...') >>> 0 1 2 3 4 5 6 7 8 9 the end...
itertools
python的内置模块itertools提供了对生成器的诸多支持。这里列举一个,其它支持请看文档
gen = itertools.count(1, 2) # 从1开始,步长为2,不断产生数值 >>> next(gen) 1 >>> next(gen) 3 >>> next(gen) 5 >>> next(gen) 7 >>> next(gen) 9 >>> next(gen) 11
yield from 关键字
yield from 是python3.3中出现的新句法。yield from句法能够实现委派生成器。
def func(): yield from (i for i in range(5)) gen = func() for i in gen: print(i) >>> 0 1 2 3 4
如上所示,yield from把func做为了一个委派生成器。for循环能够经过委派生成器func直接迭代子生成器(i for i in range(5))。不过只是这个取巧远远不足以将yield from做为一个新句法加入到Python中。比起上述代码的迭代内层循环,新句法更加剧要的功能是委派生成器为调用者和子生成器创建了一个管道。经过生成器的send方法就能够在管道中为两端传递消息。若是使用此方法在程序层面控制线程行为,就会迸发出强大的能量,它叫作协程。
注意事项
迭代器与生成器功能强大,不过使用中仍是有几点要注意:
tips
我的以为迭代器有趣的点
os.walk
os.walk迭代器能够深度遍历目录,是个大杀器,你值得拥有,快去试试吧。
iter
iter能够接受两个位置参数:callable和flag。callable()能够不断产出值,若是等于flag,则终止。以下是一个小例子
gen = (i for i in range(10)) for i in iter(lambda: next(gen), 4): # 执行ntext(gen), 不断返回生成器中的值,等于4则中止 print(i) >>> 0 1 2 3 the end...
yield能够接收值
yield能够接收send发送的值。以下代码中,#16处send的值,会传给#15中的yield,而后赋值给res。
def func(): res = yield 1 #15 print(res) f = func() f.send(None) # 预激 f.send(5) # 16
但愿你们能够经过本文掌握装饰器这个杀手级特性。欢迎关注我的博客:药少敏的博客