python核心技术与实战,Python核心技术与实战——十五|深入了解迭代器和生成器...

我们在前面应该写过类似的代码

for i in [1,2,3,4,5]:print(i)

for in 语句看起来很直观,很便于理解,比起C++或Java早起的

for (int i = 0; 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)。

defis_iterable(param):try:

iter(param)returnTrueexceptTypeError:returnFalse

params=['1234',1234,

[1,2,3,4],

set([1,2,3,4]),

{1:1,2:2,3:3},

(1,2,3,4)

]for param inparams:print('{} is iterable?{}.'.format(param,is_iterable(param)))

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1234 isiterable?True.1234 isiterable?False.

[1, 2, 3, 4] isiterable?True.

{1, 2, 3, 4} isiterable?True.

{1: 1, 2: 2, 3: 3} isiterable?True.

(1, 2, 3, 4) is iterable?True.

输出

通过上面的代码可以发现,在给出的类型中,只有数字1234是不可迭代的,其余数据类型都可以迭代。

what is 生成器?

那么生成器又是什么呢?在很多语言中,生成器都没有相对应的模型,所以这里只需要记住一点:生成器就是懒人版的迭代器。

如果想要在迭代器中枚举他的元素,这些元素要事先生成。这里,我们看看下面的例子

importosimportpsutildefshow_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))deftest_iterator():

show_memory_info('initing iterator')

list_1= [i for i in range(100000000)]

show_memory_info('after iterator initiated')print(sum(list_1))deftest_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()

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

initing iterator memory used:7.21875MB

after iterator initiated memory used:1848.28515625MB4999999950000000intiting generator memory used:1.7109375MB

after generator initiated memory used:1.7421875MB4999999950000000after 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

如果我们想验证一下他,要怎么码代码呢?

defgenerator(k):

i= 1

whileTrue:yield i**k

i+= 1gen_1= generator(1)

gen_3= generator(3)print(gen_1)print(gen_3)defget_sum(n):

sum_1 ,sum_3=0,0for i inrange(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_3print(sum_1*sum_1,sum_3)

get_sum(10)

首先,可以注意一下generator()这个函数,他返回了一个生成器。

下面的yield可以说是程序的关键,因为函数运行到这里的时候,是会暂停的,然后跳出到next()函数处。而i**k则成了next()函数的返回值。

这样,每次next(gen)函数被调用的时候,暂停的程序就重新复活,从yield处向下继续执行,同时要注意的是,局部变量i并没有被清除掉,而是会继续累加,所以next_1和next_3是不停变化的。

所以说,这个生成器是可以无限制的一直进行下去。迭代器是一个有限的集合,而生成器则可以视为一个无限集合,我们只管调用next()就可以,生成器根据运算会自动生成新的元素返回,非常方便。

我们看看下面这段代码

defindex_normal(L,target):

result=[]for i,num inenumerate(L):if num ==target:

result.append(i)returnresultprint(index_normal([1,2,3,4,5,6,7,8,2],2))

就是获取列表中和指定元素相同的索引值组成的列表,那我们用下面的方法是不是简单的多?

defindex_generator(L,target):for i,num inenumerate(L):if num ==target:yieldiprint(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']defis_subsequence(list_1,list_2):if notlist_1:returnTrueelse:for x inlist_1:if not (x inlist_2):returnFalseelse: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']defis_subsequence(a,b):

b=iter(b)return all(i in b for i ina)print(is_subsequence(a,b))print(is_subsequence(a1,b))

看完是不是感觉一脸蒙逼?没关系,我们把他复杂化,一步一步看

a = ['a','c','d']

b= ['a','b','c','d','e']defis_subsequence(a,b):

b=iter(b)print(b)

gen= (i for i ina)print(gen)for i ingen:print(i)

gen= ((i in b)for i ina)print(gen)return all(((i in b)for i ina))print(is_subsequence(a,b))

在函数开始,先把列表b转换成了迭代器,用途后面再讲

接下来的gen=。。。比较好理解,就是产生一个生成器,这个生成器用来变量列表a,所以可以输出a里的数据。而i in b就需要好好理解了,这里是不是能联想到 for in 语句?

没错,这里的i in b大概可以等价于下面的代码

whileTrue:

val=next(b)if val ==i:yield True

所以这里就非常巧妙的利用都了生成器的特性,next()函数运行的时候,保存了当前的指针,再看看下面的示例:

b = (i for i in range(5))print(2 inb)print(6 inb)####输出####

True

False

最后的all()函数就很简单了,他用来判断一个迭代器的元素是否全为True,如果是则返回True,否则就返回False。

总结

容器是可迭代对象,可迭代对象调用iter()函数,可以得到一个迭代器,迭代器可以通过next()获得下一个元素从而支持遍历。

生成器是一种特殊的迭代器(注意反向逻辑是不成立的)。使用生成器,可以写出来更加清晰的代码,合理使用生成器可以有效降低内存使用、优化程序结构、提高程序速度。

生成器在Python2的版本上是协程的一种重要实现方式,而在3.5引入asyncawait语法糖后,生成器实现协程的方式就已经落后了。

课后思考

对于一个有限元素的生成器,如果迭代完成后,继续调用next()会发生什么呢?生成器可以遍历多次么?

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值