python迭代器和生成器

232 阅读6分钟

开篇之前,推荐一个讲述推导式生成器的视频https://www.bilibili.com/video/av18718995

容器,可迭代对象和迭代器

在 Python 中一切皆对象,对象的抽象就是类,而对象的集合就是容器。

列表(list: [0, 1, 2]),元组(tuple: (0, 1, 2)),字典(dict: {0:0, 1:1, 2:2}),集合(set: set([0, 1, 2]))都是容器。对于容器,你可以很直观地想象成多个元素在一起的单元;而不同容器的区别,正是在于内部数据结构的实现方法。然后,你就可以针对不同场景,选择不同时间和空间复杂度的容器。

所有的容器都是可迭代的(iterable)。迭代可以想象成是你去买苹果,卖家并不告诉你他有多少库存。这样,每次你都需要告诉卖家,你要一个苹果,然后卖家采取行为:要么给你拿一个苹果;要么告诉你,苹果已经卖完了。你并不需要知道,卖家在仓库是怎么摆放苹果的。

而可迭代对象,通过 iter() 函数返回一个迭代器,再通过 next() 函数就可以实现遍历。for in 语句将这个过程隐式化,所以,你只需要知道它大概做了什么就行了。又或者可以这么说通过iter()方法获得了list的迭代器对象,然后就可以通过next()方法来访问list中的元素了。当容器中,没有可访问的元素后,next()方法将会抛出一个StopIteration异常终止迭代器。字符串,列表或元组对象都可用于创建迭代器

严谨地说,迭代器(iterator)提供了一个 next 的方法。调用这个方法后,你要么得到这个容器的下一个对象,要么得到一个 StopIteration 的错误(苹果卖完了)。你不需要像列表一样指定元素的索引,因为字典和集合这样的容器并没有索引一说。比如,字典采用哈希表实现,那么你就只需要知道,next 函数可以不重复不遗漏地一个一个拿到所有元素即可。不过迭代器是有限制的,例如,不能回到开始,也无法复制一个迭代器。因此要再次进行迭代只能重新生成一个新的迭代器对象。

iter()和__next__()方法这两个方法是迭代器最基本的方法:

一个用来获得迭代器或者说是创建迭代器

一个用来获得容器中的下一个元素

迭代器示例代码:

#encoding=utf-8
li=[5,6,7]
it=iter(li)  #创建一个迭代器
print(type(li))
print(it)
print(it.__next__())
print(it.__next__())
print(it.__next__())
print(it.__next__()) #此次调用会抛出异常

#调用迭代器的另外一个方法
#encoding=utf-8
li=[5, 6, 7]
it=iter(li)
print(it)
print(next(it))
print(next(it))
print(next(it))
print(next(it))  #此次调用会抛出异常

自定义的迭代器
class  MyRange(object):
          def  __init__(self, n):
                 self.idx = 0
                 self.n = n
           def  __iter__(self):
                  return  self   #返回当前状态
            def __next__(self):
                   if self.idx < self.n:
                          val = self.idx
                           self.idx += 1
                           return val
                    else:
                           raise  StopIteration()
myRange = MyRange(3)
print(next(myRange))
print(next(myRange))
print(next(myRange))
print(next(myRange))

生成器

在python中,一边循环一边计算的机制,称为生成器。你可以这么记着:生成器是懒人版本的迭代器。

在迭代器中,如果我们想要枚举它的元素,这些元素需要实现生成。

import os
import psutil

# 显示当前 python 程序占用的内存大小
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))
    show_memory_info('after sum called')

#显示生成器的方式
def test_generator():
    show_memory_info('initing 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')

%time test_iterator()
%time test_generator()

########## 输出 ##########

initing iterator memory used: 48.9765625 MB
after iterator initiated memory used: 3920.30078125 MB
4999999950000000
after sum called memory used: 3920.3046875 MB
Wall time: 17 s
initing generator memory used: 50.359375 MB
after generator initiated memory used: 50.359375 MB
4999999950000000
after sum called memory used: 50.109375 MB
Wall time: 12.5 s

声明一个迭代器很简单,[i for i in range(100000000)]就可以生成一个包含一亿元素的列表。每个元素在生成后都会保存到内存中,你通过代码可以看到,它们占用了巨量的内存,内存不够的话就会出现 OOM 错误。

不过,我们并不需要在内存中同时保存这么多东西,比如对元素求和,我们只需要知道每个元素在相加的那一刻是多少就行了,用完就可以扔掉了。

于是,生成器的概念应运而生,在你调用 next() 函数的时候,才会生成下一个变量。生成器在 Python 的写法是用小括号括起来,(i for i in range(100000000)),即初始化了一个生成器。

这样一来,你可以清晰地看到,生成器并不会像迭代器一样占用大量内存,只有在被使用的时候才会调用。而且生成器在初始化的时候,并不需要运行一次生成操作,相比于 test_iterator() ,test_generator() 函数节省了一次生成一亿个元素的过程,因此耗时明显比迭代器短。

生成器的代码示例:

g = (x * x for x in range(10))
print(g)
print(next(g))  #0 * 0
print(next(g))  #1* 1
print(next(g))  #2 * 2
print(next(g))  #3 * 3

生成器在函数中的应用方式简单代码示例:
def  odd():
        print('step  1')
        yield 1
        print('step  2')
         yield 3
         print('step 3')
         yield 5

y = odd()
print(y)
print(next(y))
print(next(y))
print(next(y))
print(next(y))

在这段代码中,odd()这个函数,它返回了一个生成器。接下来的yield是魔术的关键。你可以简单的理解为,函数运行到这一行的时候,程序会从这里暂停,然后跳出,跳出到哪里呢?答案是next()函数。那么每个yield后面的1或3或5是干什么用的?它其实成了next()函数的返回值。

这样,每次next(y)函数被调用的时候,暂停的程序就又复活了,从yield这向下继续执行。

事实上,迭代器是一个有限集合,生成器则可以成为一个无限集合。我只管调用next(),生成器根据运算会自动生成新的元素,然后返回给你。

生成器和迭代器的区别

1.通过实现迭代器协议对应的__iter__()和__next__()方法,可以自定义迭代器类型。对于可迭代对象,for语句可以通过iter()方法获取迭代器,并且通过next方法获得容器的下一个元素。

2.生成器是一种特殊的迭代器,内部支持了生成器协议,不需要明确定义__iter__()和__next__()方法。生成器中的数据只有在调用时才会创建,可以for循环迭代访问。生成器的本质是维护了一套生成算法在生成器中,使用数据的时候,它才会实际生成一个又一个数据。

3.生成器通过生成器函数产生,生成器函数可以通过常规的def语句来定义,但是不用return返回,而是用yield一次返回一个结果。