Python核心技术与实战——十四|深入了解迭代器和生成器
Posted 银色的音色
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python核心技术与实战——十四|深入了解迭代器和生成器相关的知识,希望对你有一定的参考价值。
我们在前面应该写过类似的代码
for i in [1,2,3,4,5]: print(i)
for in 语句看起来很直观,很便于理解,比起C++或Java早起的
for (int i = 0; i<n;i++) printf("d ",a[i])
是不是简洁清晰的多。但是我们有没有想过Python在处理for in语句的时候,具体发生了什么吗?什么样的对象可以被for in用来枚举呢?
所以,这一节我们就深入到Python的容器类型实现底层看一看,了解一下迭代器和生成器。
前面用过的容器、可迭代对象和迭代器
容器这个概念还是比较容易理解的,我们说过,Python中一切皆为对象,对象的抽象就是类,而对象的集合就是容器。
l = [0,1,2] t = (0,1,2) d = {0:0,1:1,2:2}
s = set([0,1,2])
上面的列表,元组、字典和集合都是容器。对于容器,我们可以很直观的想象成很多个元素在一起的单元;而不同容器的区别,正式在于内部数据结构的实现方法。然后我们就可以针对不同的场景,选择不同时间复杂度和空间复杂度的容器。
所有的容器都是可迭代的(iterable)。这里的迭代,和枚举是完全不同的,这里的迭代可以想象成去买个苹果,卖家并声明他有多少库存,这样,每次去买一个苹果的时候卖家采取的行为不外乎给你拿一个苹果,要么就是告诉你苹果已经卖完了,所以你并不需要知道卖家是怎么在仓库内存放苹果的。
严谨 都说,迭代器(iterator)提供了一个next的方法,调用这个方法后,要么叨叨容器的下一个对象,要么得到一个StopIteration的错误。我们并不需要想列表一样指定元素的索引,因为字典和集合是没有索引的说法的(字典采用哈希表实现)。我们只需要知道,next函数可以不重复不遗漏的拿到所有的元素就可以了。
而可迭代对象,是通过iter()函数返回一个迭代器,在通过next()函数就可以实现遍历。for in语句将这个过程隐式化,我们只需要大概知道他怎么做就行了。
我们再看看下面的代码,主要展示了如何判断一个对象是否可迭代。当然还有一种用法,是isinstance(obj,Iterable)。
def is_iterable(param): try: iter(param) return True except TypeError: return False params = [ ‘1234‘, 1234, [1,2,3,4], set([1,2,3,4]), {1:1,2:2,3:3}, (1,2,3,4) ] for param in params: print(‘{} is iterable?{}.‘.format(param,is_iterable(param)))
1234 is iterable?True. 1234 is iterable?False. [1, 2, 3, 4] is iterable?True. {1, 2, 3, 4} is iterable?True. {1: 1, 2: 2, 3: 3} is iterable?True. (1, 2, 3, 4) is iterable?True.
通过上面的代码可以发现,在给出的类型中,只有数字1234是不可迭代的,其余数据类型都可以迭代。
what is 生成器?
那么生成器又是什么呢?在很多语言中,生成器都没有相对应的模型,所以这里只需要记住一点:生成器就是懒人版的迭代器。
如果想要在迭代器中枚举他的元素,这些元素要事先生成。这里,我们看看下面的例子
import os import psutil def show_memory_info(hint): pid = os.getpid() p = psutil.Process(pid) info = p.memory_full_info() memory = info.uss / 1024. /1024 print(‘{} memory used:{}MB‘.format(hint,memory)) def test_iterator(): show_memory_info(‘initing iterator‘) list_1 = [i for i in range(100000000)] show_memory_info(‘after iterator initiated‘) print(sum(list_1)) def test_generator(): show_memory_info(‘intiting generator‘) list_2 = (i for i in range(100000000)) show_memory_info(‘after generator initiated‘) print(sum(list_2)) show_memory_info(‘after sum called‘) test_iterator() test_generator()
initing iterator memory used:7.21875MB after iterator initiated memory used:1848.28515625MB 4999999950000000 intiting generator memory used:1.7109375MB after generator initiated memory used:1.7421875MB 4999999950000000 after sum called memory used:2.109375MB
我们用[i for i in range(100000000)]声明了一个包含一个亿元素的列表(声明了一个迭代器),每个元素在生成以后都保存在内存里,通过代码可以发现他们占用了巨大的内存空间,如果内存不够的话就直接OOM错误了。
不过,我们并不需要在内存中同事保存这么多东西,比方元素求和,我们只需要知道每个元素相加的那一刻是多少就可以了,用完扔掉即可。
于是,生成器在这里就体现出作用了。在我们调用next()函数的时候,才会生成下一个变量,生成器在Python中的写法是用小括号括起来:
l = (i for i in range(100000000))
这样一来,可以清晰的看到生成器是不会像迭代器一样占用大量内存的,只有在被使用的时候才会调用,而且生成器在初始化的时候,并不需要一次生成操作,相比于例子中的第一个测试函数,第二个函数节省了一次生成一亿个元素的过程,因此耗时明显变短。
此外,生成器并不是单单节省了时间和计算机资源,我们可以看看下面的例子。
生成器还有什么作用?
数学中有一个恒等式:
(1+2+3+...+n)^2 = 1^3+2^3+3^3+...+n^3
如果我们想验证一下他,要怎么码代码呢?
def generator(k): i = 1 while True: yield i**k i += 1 gen_1 = generator(1) gen_3 = generator(3) print(gen_1) print(gen_3) def get_sum(n): sum_1 ,sum_3 = 0,0 for i in range(n): next_1 = next(gen_1) next_3 = next(gen_3) print(‘next_1 = {},next_3 = {}‘.format(next_1,next_3)) sum_1 += next_1 sum_3 += next_3 print(sum_1*sum_1,sum_3) get_sum(10)
首先,可以注意一下generator()这个函数,他返回了一个生成器。
下面的yield可以说是程序的关键,因为函数运行到这里的时候,是会暂停的,然后跳出到next()函数处。而i**k则成了next()函数的返回值。
这样,每次next(gen)函数被调用的时候,暂停的程序就重新复活,从yield处向下继续执行,同时要注意的是,局部变量i并没有被清除掉,而是会继续累加,所以next_1和next_3是不停变化的。
所以说,这个生成器是可以无限制的一直进行下去。迭代器是一个有限的集合,而生成器则可以视为一个无限集合,我们只管调用next()就可以,生成器根据运算会自动生成新的元素返回,非常方便。
我们看看下面这段代码
def index_normal(L,target): result = [] for i,num in enumerate(L): if num == target: result.append(i) return result print(index_normal([1,2,3,4,5,6,7,8,2],2))
就是获取列表中和指定元素相同的索引值组成的列表,那我们用下面的方法是不是简单的多?
def index_generator(L,target): for i,num in enumerate(L): if num == target: yield i print(list(index_generator([1,2,3,4,5,6,7,8,2],2)))
到这里就不用多做解释了,唯一需要强调的是index_generator会返回一个Generator对象,需要用list转换为列表后才能打印。
这里有个事情要强调:
在Python的语言规范中,用更少,更加清晰的代码实现相同的功能一直是被推崇的办法。因为这样能够很有效的提高代码的可读性,减少出错概率,也能方便他人快速准确的理解作者的意图。但是,这里的“更少”的前提是清晰,而非使用更多的魔术操作,即便减少了代码反而增加了阅读的难度。
回归正题,我们看一看这样的问题,给定两个序列,判定第一个序列是不是第二个序列的子序列。(子序列,一个列表的元素在第二个列表中按顺序出现,注意按顺序,比方[1,3,5]是[1,2,3,4,5]的子序列,而[1,5,3]就不是)
a = [‘a‘,‘c‘,‘d‘] b = [‘a‘,‘b‘,‘c‘,‘d‘,‘e‘] def is_subsequence(list_1,list_2): if not list_1: return True else: for x in list_1: if not (x in list_2): return False else: if is_subsequence(list_1[list_1.index(x)+1:],list_2[list_2.index(x)+1:]): return ‘list_1 is subsequence of list_2‘ else: return ‘list_1 is not subsequence of list_2‘ print(is_subsequence(a,b))
上面的代码就是用了叫做‘贪心算法’的常规算法,我们维护两个指针指向两个列表的最开始,然后对第二个列表一路扫过去,如果某个数字和第一个指针指的一样,那么就把第一个指针前进一步。直到一个指针移出第一个序列。
那么我们要用生成器和迭代器的方法该怎么实现呢?
我们先看一个极简版的
a = [‘a‘,‘c‘,‘d‘] b = [‘a‘,‘b‘,‘c‘,‘d‘,‘e‘] a1 = [‘a‘,‘d‘,‘c‘] def is_subsequence(a,b): b = iter(b) return all(i in b for i in a) print(is_subsequence(a,b)) print(is_subsequence(a1,b))
看完是不是感觉一脸蒙逼?没关系,我们把他复杂化,一步一步看
a = [‘a‘,‘c‘,‘d‘] b = [‘a‘,‘b‘,‘c‘,‘d‘,‘e‘] def is_subsequence(a,b): b = iter(b) print(b) gen = (i for i in a) print(gen) for i in gen: print(i) gen = ((i in b)for i in a) print(gen) return all(((i in b)for i in a)) print(is_subsequence(a,b))
在函数开始,先把列表b转换成了迭代器,用途后面再讲
接下来的gen=。。。比较好理解,就是产生一个生成器,这个生成器用来变量列表a,所以可以输出a里的数据。而i in b就需要好好理解了,这里是不是能联想到 for in 语句?
没错,这里的i in b大概可以等价于下面的代码
while True: val = next(b) if val == i: yield True
所以这里就非常巧妙的利用都了生成器的特性,next()函数运行的时候,保存了当前的指针,再看看下面的示例:
b = (i for i in range(5)) print(2 in b) print(6 in b) ####输出#### True False
最后的all()函数就很简单了,他用来判断一个迭代器的元素是否全为True,如果是则返回True,否则就返回False。
总结
容器是可迭代对象,可迭代对象调用iter()函数,可以得到一个迭代器,迭代器可以通过next()获得下一个元素从而支持遍历。
生成器是一种特殊的迭代器(注意反向逻辑是不成立的)。使用生成器,可以写出来更加清晰的代码,合理使用生成器可以有效降低内存使用、优化程序结构、提高程序速度。
生成器在Python2的版本上是协程的一种重要实现方式,而在3.5引入asyncawait语法糖后,生成器实现协程的方式就已经落后了。
课后思考
对于一个有限元素的生成器,如果迭代完成后,继续调用next()会发生什么呢?生成器可以遍历多次么?
以上是关于Python核心技术与实战——十四|深入了解迭代器和生成器的主要内容,如果未能解决你的问题,请参考以下文章
《Python自然语言处理实战:核心技术与算法》PDF新书推介,附带链接地址
Python核心技术与实战——二一|巧用上下文管理器和with语句精简代码