本文始发于我的公众号:TechFlow,原创不易,求个关注node
今天是周一Python专题,给你们带来的是Python当中生成器和迭代器的使用。python
我当初第一次学到迭代器和生成器的时候,并无太在乎,只是以为这是一种新的获取数据的方法。对于获取数据的方法而言,咱们会一种就足够了。可是在我后来Python的使用以及TensorFlow等学习使用当中,我发现不少地方都用到了迭代器和生成器,或者是直接使用,或者是借鉴了思路。因此咱们不能掉以轻心,今天就让咱们仔细来看看,它们究竟是怎么回事。web
咱们先从迭代器[1]开始入手,迭代器并非Python独有的概念,在C++和Java当中都有iterator的概念,二者的使用也都差很少。迭代器主要解决了一个问题,在一个复杂场景下,获取数据怎么尽量简便。数组
咱们来假设一个场景,假设咱们从某个数据源获取了一批数据。而后咱们须要调用前一万条生成一个结果,获得结果以后,咱们要将剩下的数据交给另外一个调用方去处理。这个过程看起来很是日常,可是隐藏了两个问题,第一个问题是若是咱们能保证第一次处理的时候,每次都是使用一万条还好说,若是咱们使用的条数是一个动态的值呢?显然,咱们须要一个变量来记录咱们究竟用了多少条数据,和这批数据的状态。其次,若是这个数据量很大会存在一个数据传输的问题。咱们每次都要将一大批数据传来传去,显然会消耗不少资源。数据结构
还有一个场景是若是咱们开发的是一个比较复杂的数据结构,好比一棵多叉树,下游想要遍历它的时候,必需要了解它的实现原理才行。这显然也不太友好。编辑器
迭代器的出现正是针对以上这些问题,它的含义也很简单,有点像是咱们遍历链表的时候用到的cur的指针。永远指向当前的位置,永远知道下一个位置在哪里。函数
咱们先从简单的元素迭代器开始了解它的用途,咱们都知道Python当中经典的几个容器:list, tuple和dict。它们都是一个可迭代对象,咱们能够直接使用关键字iter获取一个对应的迭代器。工具
咱们来看一个例子:学习
arr = [1, 3, 4, 5, 9]
it = iter(arr)
print(next(it))
print(next(it))
复制代码
这是一个很是经典的例子,咱们首先定义了一个数组,而后经过iter关键字获取了一个读取它的迭代器。有了迭代器以后咱们能够经过next关键字获取迭代器当中的下一个元素,咱们一共调用了两次next,第一次输出的结果是1,第二次的结果是3。和咱们刚才说的同样,咱们每一次调用,它会自动日后移动一格,获取后面一位的数据。flex
这里有一点须要注意,由于咱们建立的数组当中一共只有5个元素,若是咱们调用it的次数超过5次,那么会引起超界,Python的解释器会抛出StopIteration的error。
除了使用next,咱们也可使用for循环来迭代它:
for i in it:
print(i)
复制代码
这种用法就和咱们用for循环遍历元素是同样的。
官方的迭代器的用法就这么多,这也不是它的主要用法,它最主要的用法是咱们本身建立迭代器。和以前介绍Python自定义排序的时候的思路同样,咱们为类添加上__iter__方法和__next__方法便可。
其中__iter__方法用来初始化并返回迭代器,关于它的解释比较复杂。在Python当中迭代有两个概念一个是iterable,一个是iterator。协议规定iteratble的__iter__方法会返回一个iterator。而iterator自己也是一个iterable对象,天然也须要实现__iter__方法。
我知道这么说可能听不太明白,我举个例子,好比说员工和老板,员工没有审批权限,只能转达给老板。咱们把员工比喻成iterable对象,老板比喻成iterator。
员工面临一个问题的时候没有权限处理,只能找来老板决定。也就是最终决定的是老板,但若是是老板本身发现的问题,他彻底能够本身就解决了,不须要再去找其余人。因此说咱们用iter调用iterable对象的__iter__的时候,会获得一个iterator,也就是调用员工返回老板,而后经过调用iterator的__next__来进行迭代。
到这里也就清楚了,只有iterator有__next__方法,而iterable没有,而且__iter__返回的是一个iterator。然而咱们定义的已是iterator了,它同时也是一个iterable对象,因此调用__iter__时只须要返回self就行了。__next__方法很简单,对应迭代器的next方法,用来返回下一个迭代的元素。
咱们来看一个例子:
class PowTwo:
"""Class to implement an iterator
of powers of two"""
def __init__(self, max = 0):
self.max = max
def __iter__(self):
self.n = 0
return self
def __next__(self):
if self.n <= self.max:
result = 2 ** self.n
self.n += 1
return result
else:
raise StopIteration
复制代码
这是一个简单的生成2的幂的迭代器,咱们在__iter__里为self.n初始化为0,而后返回自身。在__next__里判断有没有迭代结束,若是结束的话抛出一个异常。
咱们来看使用它的例子:
>>> a = PowTwo(4)
>>> i = iter(a)
>>> next(i)
1
>>> next(i)
2
>>> next(i)
4
>>> next(i)
8
>>> next(i)
16
>>> next(i)
Traceback (most recent call last):
...
StopIteration
复制代码
咱们也能够用for循环来迭代它:
>>> for i in PowTwo(5):
... print(i)
...
1
2
4
8
16
32
复制代码
迭代器除了能够迭代一个容器或者是像上面这样自定义迭代方法以外,还能够用来迭代生成器。下面就让咱们一块儿来看下生成器的概念。
生成器的概念和迭代器相辅相成,迭代器是生成一个遍历数据的迭代工具,而生成器则是数据生成工具。
举个很简单的例子,好比说斐波那契数列咱们都知道,从第三个数开始等于前面两个数的和。好比咱们想获取100万个斐波那契数列,按照传统的方法咱们须要开辟一个长度是一百万的数组,而后按照斐波那契数列的定义一个一个地计算。显然这样会消耗大量的空间,有没有办法咱们和迭代器那样构建一个生成数据的方法,咱们每次调用获取下一个结果呢?这样咱们要多少数据就调用多少次就能够了,从根本上解决了存储的问题。
下面咱们来看怎么定义一个生成器。
最简单的方法真的很简单,和咱们建立list基本上如出一辙。
在Python当中,咱们常常这样初始化一个数组:
arr = [i * 3 for i in range(10)]
复制代码
也就是说咱们把循环放在list的定义当中,这样Python会自动执行里面的循环,而后将全部循环的结果进行二次计算后写入到list当中去。咱们稍微变形一下,就获得了一个最简单的生成器。
g = (i * 3 for i in range(10))
print(next(g))
复制代码
看清楚了吗,其实和list没什么差异,只是咱们将最外层的括号从[]换成了()。
这种方法你们应该都能看懂,可是可能会有一个疑惑。咱们这样作的意义是什么呢?这样和上面用[]定义有什么区别呢?
实际上是有区别的,若是没有区别,那么咱们用生成器也就没有意义了。它的区别也就是生成器的意义,简单来讲,咱们前文中已经说过了当定义一个list的时候,Python会自动将for循环执行一遍,而后将结果写入进list当中。可是生成器不会,虽然咱们也用到了for循环,可是它只是起到了限制个数的做用,在执行完这一步以后,Python并不会将for循环执行结束。只有咱们每次调用next,才会触发它进行一次循环。
不相信的同窗能够试试,看看运行一下下面两个语句的区别:
g = (i for i in range(1000000000))
g = [i for i in range(1000000000)]
复制代码
若是奇怪的事情发生了,不妨再回到文章来思考一下。
上面介绍的方法虽然简单,可是不太实用,由于不少时候咱们想要的数据构造方法会比较复杂,很难用这种形式展示出来。
因此Python当中还为咱们提供了一种构造生成器的方法,相比起来要稍微复杂一点点,可是也很好用。咱们来看一个例子:
def gtr(n):
for i in range(n):
yield i
复制代码
从代码上来看,咱们好像定义了一个函数,某种程度上能够这么理解,可是它返回的结果并非一个值,而是一个生成器[2]。
若是你真的去试了,你会获得一个generator类型的实例,这也是Python自带的生成器的实例。
再仔细观察一下,你会发现这个函数当中的关键字和通常的不太同样,它没有使用return,而是使用了yield。yield和return在很大程度上很接近,可是又有些不一样。
相同点是当咱们执行到yield时,和return同样会将yield以后的内容返回给调用方。好比上面代码当中写到yield i,那么咱们运行next的时候就会获取到这个i。
不一样的地方是,当咱们下一次再次执行的时候,会继续从yield处开始往下执行。有些相似于递归的时候,底层的递归执行结束回到上层的状况。所以若是咱们要获取多个值,须要在生成器当中使用循环。举个例子:
def test():
n = 0
while True:
if n < 3:
yield n
n += 1
else:
yield 10
if __name__ == '__main__':
t = test()
for i in range(10):
print(next(t))
复制代码
咱们若是执行上面这段代码,前三个数是0,1和2,从第四个数开始一直是10。若是你能看懂这个例子,必定能明白yield的含义。
接下来要介绍的yield from和yield用法差很少,也是从生成器返回一个结果,而且下次执行的时候从返回的位置开始继续执行。
可是它有一点和yield不一样,咱们来看一个经典的例子。
def g1():
yield range(5)
def g2():
yield from range(5)
it1 = g1()
it2 = g2()
for x in it1:
print(x)
for x in it2:
print(x)
复制代码
这二者打印出来的结果是同样的,可是逻辑彻底不一样。在第一个生成器g1当中,直接经过yield返回了一个迭代器。也就是说咱们for循环执行的实际上是range(5),而第二个生成器g2则经过yield from获取了range(5)这个迭代器当中的值进行的返回。
也就是说yield from能够返回一个迭代器或者是生成器执行next以后的结果。
最后,咱们来看一个yield from使用的一个经典场景:二叉树的遍历:
class Node:
def __init__(self, key):
self.key = key
self.lchild = None
self.rchild = None
self.iterated = False
self.father = None
def iterate(self):
if self.lchild is not None:
yield from self.lchild.iterate()
yield self.key
if self.rchild is not None:
yield from self.rchild.iterate()
复制代码
在这个代码当中咱们定义了二叉树当中的一个节点,以及它对应的迭代方法。因为咱们用到了yield来返回结果,因此iterate方法本质是一个生成器。再来看iterate方法内部,咱们经过yield from调用了iterate,因此咱们在执行的时候,它会自动继续解析node.lchild的iterate,也就是说咱们经过yield from实现了递归。
当咱们建好树以后,能够直接使用root.iterate来遍历整棵树。
class Tree:
def __init__(self):
#建树过程
self.root = Node(4)
self.root.lchild = Node(3)
self.root.lchild.father = self.root
self.root.rchild = Node(5)
self.root.rchild.father = self.root
self.root.lchild.lchild = Node(1)
self.root.lchild.lchild.father = self.root.lchild
self.root.rchild.rchild = Node(7)
self.root.rchild.rchild.father = self.root.rchild
def iterate(self):
yield from self.root.iterate()
复制代码
经过yield from,咱们能够很轻松地利用递归的思路来实现树上的生成器。从而能够很方便地以生成器的思路来遍历树上全部的元素。
到这里,关于Python当中迭代器和生成器的知识就算是讲完了,这二者的概念有些接近,可是又不彻底同样,不少初学者容易搞混淆。
其实能够这么理解,迭代器和生成器遍历元素的方式是同样的,都是经过调用next来获取下一个元素。咱们经过yield建立函数,返回的结果其实就是生成器生成的数据的迭代器。也就是说迭代器只是迭代和获取数据的,可是并不能无中生有地创造数据。而生成器的主要做用是创造数据,它生成出来的数据是以迭代器的形式返回的。
举个例子,你开了一个奶茶店,经过奶茶店每月能够在银行帐户里得到一笔收入。迭代器就是这个帐户,经过它你能够得到一笔一笔的收入。而奶茶店则是一个生成器,它产出数据,可是是以迭代器的形式返回给你的,也就是以银行帐户的方式给你收入。咱们拿到银行卡并不知道它里面的钱是怎么赚来的,只能看到钱,也就是说咱们并不知道迭代器背后数据的逻辑。可是生成器咱们是清楚的,由于钱(生产逻辑)是咱们亲自赚来的。
今天的文章就是这些,若是以为有所收获,请顺手点个关注或者转发吧,大家的举手之劳对我来讲很重要。
programiz: "https://www.programiz.com/python-programming/iterator"
[2]廖雪峰的Python教程: "https://www.liaoxuefeng.com/wiki/1016959663602400/1017323698112640"