开篇之前,推荐一个讲述推导式生成器的视频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一次返回一个结果。