今天是周一 Python 专题, 给大家带来的是 Python 当中生成器和迭代器的使用.
我当初第一次学到迭代器和生成器的时候, 并没有太在意, 只是觉得这是一种新的获取数据的方法. 对于获取数据的方法而言, 我们会一种就足够了. 但是在我后来 Python 的使用以及 TensorFlow 等学习使用当中, 我发现很多地方都用到了迭代器和生成器, 或者是直接使用, 或者是借鉴了思路. 所以我们不能掉以轻心, 今天就让我们仔细来看看, 它们到底是怎么回事.
迭代器
我们先从迭代器 [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. 和我们刚才说的一样, 我们每一次调用, 它会自动往后移动一格, 获取后面一位的数据.
这里有一点需要注意, 因为我们创建的数组当中一共只有 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 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 创建函数, 返回的结果其实就是生成器生成的数据的迭代器. 也就是说迭代器只是迭代和获取数据的, 但是并不能无中生有地创造数据. 而生成器的主要作用是创造数据, 它生成出来的数据是以迭代器的形式返回的.
举个例子, 你开了一个奶茶店, 通过奶茶店每个月可以在银行账户里获得一笔收入. 迭代器就是这个账户, 通过它你可以获得一笔一笔的收入. 而奶茶店则是一个生成器, 它产出数据, 但是是以迭代器的形式返回给你的, 也就是以银行账户的方式给你收入. 我们拿到银行卡并不知道它里面的钱是怎么赚来的, 只能看到钱, 也就是说我们并不知道迭代器背后数据的逻辑. 但是生成器我们是清楚的, 因为钱 (生产逻辑) 是我们亲自赚来的.
今天的文章就是这些, 如果觉得有所收获, 请顺手点个关注或者转发吧, 你们的举手之劳对我来说很重要.
参考资料
- [1]
- programiz: "https://www.programiz.com/python-programming/iterator"
- [2]
廖雪峰的 Python 教程: "https://www.liaoxuefeng.com/wiki/1016959663602400/1017323698112640"
来源: https://www.cnblogs.com/techflow/p/12453751.html