写在后面
本系列目标:一篇文章,不求鞭辟入里,但使得心应手。
迭代是数据处理的基石,在扫描内存无奈装载的数据集时,咱们须要一种惰性获取数据的能力(即一次获取一部分数据到内存)。在Python中,具备这种能力的对象就是迭代器。生成器是迭代器的一种非凡表现形式。
集体认为生成器是Python中最有用的高级个性之一(甚至没有之一)。尽管高级编码中应用寥寥,但随着学习深刻,会发现生成器是协程,异步等高级常识的基石。Python最有野心的asyncio库,就是用协程砌造的。
注:生成器和协程实质雷同。PEP342(Python加强提案)减少了生成器的send()办法,使其变身为协程。如此之后,生成器生成数据,协程生产数据。尽管实质雷同,然而因为从理念上说协程跟迭代没有关系,并且纠缠生成器和协程的区别与分割会引爆本人的大脑,所以应该将这两个概念辨别。此处说实质雷同意为:了解生成器原理之后,了解减少了send办法,然而实现形式简直雷同的协程会更加轻松(这段话看不懂没有关系,船到桥头天然直,学到协程天然懂)。
- Python的一致性是其最迷人的中央。理解了Python生成器,迭代器的实现。就会对Python的一致性设计有更加强烈的感知。本文读完之后,遇到面试官发问为什么列表能够迭代,字典能够迭代,甚至文本文件都能够迭代时,你就能够稳(huang)得一批。
- 浏览本文之前,如果你对Python的一致性有一些理解,如鸭子类型,或者Cpython的PyObject构造体,那真是太棒了。不过鉴于笔者深厚的文字功底,没有这些常识也不打紧。
干货儿
迭代器
在学习生成器之前,先要理解迭代器。顾名思义,迭代器即具备迭代性能的对象。在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 itemins = Iterable(1,2,3,4,5) # 1for i in ins: print(i)print('the end...')>>> # 212345the 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) # 5print('the end...')>>> # 612345...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: breakprint('the second for:')for i in ins: # 4 the second for print(i)print('the end...')>>> # the outputthe first for:12the second for:345the end...
所以咱们能够失去如下论断:
- 一个迭代器对象只能迭代一遍。屡次迭代,相当于不停对一个空迭代器调用next办法,会不停raise StopIteration异样。
- 因为迭代器实现了iter办法,并且iter办法返回了迭代器,那么迭代器也是一个可迭代对象(废话,不是可迭代对象,上述代码中如何能够用for循环迭代呢)
- 综上来说,可迭代对象和迭代器显著是一个多态的问题。迭代器是一个可迭代对象,能够迭代返回元素,因为iter返回self(即本身实例),所以只能迭代一遍,迭代到开端就会抛出异样。而每次迭代可迭代对象,iter都会返回一个新的迭代器实例。所以可迭代对象是反对屡次迭代的。比方l=[i for i in range(10)]生成的list对象就是一个可迭代对象,能够被屡次迭代。l=(i for i in range(10))生成的是一个迭代器,只能被迭代一遍。
迭代器反对
援用晦涩的Python中的原话,迭代器反对以下6个性能。因为篇幅所限,点到为止。大家只有了解了迭代器的原理,了解以下性能天然是瓜熟蒂落。
- for循环
上述代码曾经有举例,可参考
构建和扩大汇合类型
from collections improt abcclass NewIterator(abc.Iterator): pass # 放飞自我,实现新的类型
列表推导,字典推导和汇合推导
l = [i for i in range(10)] # listd = {i:i for i in range(10)} # dicts = {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 23 4
调用函数时,应用*拆包实参
def func(a, b, c): print(a, b, c)func(*[1, 2, 3]) # 会将[1, 2, 3]这个list拆开成三个实参,对应a, b, c三个形参传给func函数
- for循环
生成器
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 itemins = 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 for12345the second for12345the end...
上述代码中,可迭代对象的iter办法并没有只用了短短数行,就实现了之前Iterator迭代器性能,点赞!
yield关键字
要了解以上代码,就须要了解yield关键字,先来看以下最简略的生成器函数实现
def func(): yield 1 yield 2 yield 3ins1 = 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>123123 File "/home/disk/test/a.py", line 18, in <module> print(next(ins2))StopIteration
从以上代码能够看出:
- func是一个函数,然而调用func会返回一个生成器对象,并且通过打印的地址看,每次调用生成器函数会返回一个新的生成器对象。
- 生成器对象和迭代器对象类似,都能够被for循环迭代,都只能被迭代一遍,通过next调用,都会在生成器元素为空时raise一个StopIteration异样。
那么含有yield关键字的生成器函数体是如何执行的呢?请看如下代码: ```python 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 ``` 从上述代码及其打印后果,咱们能够得出如下论断: - \#10处代码表明,生成器函数定义与一般函数无二,只是须要蕴含有yield关键字 - \#14for 循环隐形调用next的时候,会执行到#11处,打印start,而后产出值 1返回给for循环,打印 - for 循环持续调用next,**从#11处执行到#12处**#,打印stop,而后产出值 2返回给for循环,打印 - for 循环持续调用next,**从#12处执行到#13处**#,打印next,而后产出值 3返回给for循环,打印 - for 循环持续调用next,**从#13处执行到函数尾**#,打印end,而后raise一个StopIteration,因为for循环捕捉异样,程序失常执行 - **综上所述,yield具备暂停的性能,每次迭代生成器,生成器函数体都会后退到yield语句处,并将yield之后的值抛出(无值抛None)。生成器函数作为一个工厂函数,实现了可迭代对象中iter函数的性能,能够每次产出一个新的迭代器实例。因为应用了非凡的yield关键字,它领有与区别于迭代器的新名字——生成器,它其实与迭代器并无二致**
生成器表达式
将列表推导式中的[]改为(),即为生成器表达式。返回的是一个生成器对象。个别用户列表推导然而又不须要立马产生所有值的情景中。
gen = (i for i in range(10))for i in gen: print(i)for i in gen: # 只能被生产一遍,第二遍无输入 print(i)print('the end...')>>> 0123456789the 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) >>>01234
如上所示,yield from把func作为了一个委派生成器。for循环能够通过委派生成器func间接迭代子生成器(i for i in range(5))。不过只是这个取巧远远不足以将yield from作为一个新句法退出到Python中。比起上述代码的迭代内层循环,新句法更加重要的性能是委派生成器为调用者和子生成器建设了一个管道。通过生成器的send办法就能够在管道中为两端传递音讯。如果应用此办法在程序层面控制线程行为,就会迸发出弱小的能量,它叫做协程。
写在最初
注意事项
迭代器与生成器功能强大,不过应用中还是有几点要留神:
迭代器应该实现iter办法,尽管很多时候不实现此办法页不会影响代码运行。实现此办法的最次要起因有二:
- 迭代器协定规定须要实现此办法
- 能够通过issubclass查看对象是否是迭代器
不要把可迭代对象变为迭代器。起因有二:
- 这不合乎迭代器协定规定,造就了一个四不像。
- 可迭代对象应该是能够反复遍历的,如果变为了迭代器,那么只能遍历一次。
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)>>> 0123the 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
- os.walk
心愿大家能够通过本文把握装璜器这个杀手级个性。欢送关注集体博客:药少敏的博客