提纲:生成器 ⫋ \subsetneqq ⫋ 迭代器 ⫋ \subsetneqq ⫋ 可迭代对象
1. 可迭代对象:
可迭代对象包括:
- 迭代器 (包括生成器)。
- 字符串 str,列表 list, 字典 dict,元组 tuple,集合 set。
- 实现了 __iter__ 方法的对象。
判断一个变量是否为可迭代对象:
from collections import Iterable
isinstance(变量, Iterable)
测试代码如下:
from collections import Iterable
l1 = [1, 2, 3]
t1 = (1, 2, 3)
d1 = {'key1': 'value1'}
str1 = 'James'
set1 = {'a', 'b', 'c'}
class MyObject:
def __init__(self):
pass
class MyIter:
def __init__(self):
pass
def __iter__(self):
yield 1
a = MyObject()
b = MyIter()
print(isinstance(l1, Iterable))
print(isinstance(t1, Iterable))
print(isinstance(d1, Iterable))
print(isinstance(str1, Iterable))
print(isinstance(set1, Iterable))
print(isinstance(a, Iterable))
print(isinstance(b, Iterable))
运行结果如下:
特别注意变量 a 和 b,a 是 MyObject 的一个实例,但是由于没有实现 __iter__ 方法,所以不是一个可迭代对象,而 b 是 MyIter 的一个实例并且实现了 __iter__ 方法,所以就是一个可迭代对象。
2. 迭代器
迭代器可以通过 next() 方法不断重复获取下一个值,直到所有元素全部输出完之后,返回 StopIteration才停止。同时实现在 __iter__ 和 __next__ 两个函数的对象,就是迭代器。其中 __iter__ 方法需要返回一个迭代器, 而 __next__ 方法返回下一个返回值或者 StopIteration。
判断是否为迭代器:
from collections import Iterator
isinstance(变量, Iterator)
测试代码如下:
from collections import Iterable
from collections import Iterator
l1 = [1, 2, 3]
t1 = (1, 2, 3)
d1 = {'key1': 'value1'}
str1 = 'James'
set1 = {'a', 'b', 'c'}
class MyObject:
def __init__(self):
pass
class MyIterable:
def __init__(self):
pass
def __iter__(self):
yield 1
class MyIterator:
def __init__(self):
pass
def __iter__(self):
yield 1
def __next__(self):
return self
a = MyObject()
b = MyIterable()
c = MyIterator()
print(isinstance(l1, Iterable))
print(isinstance(t1, Iterable))
print(isinstance(d1, Iterable))
print(isinstance(str1, Iterable))
print(isinstance(set1, Iterable))
print(isinstance(a, Iterable))
print(isinstance(b, Iterable))
print(isinstance(c, Iterable))
print('********************')
print(isinstance(l1, Iterator))
print(isinstance(t1, Iterator))
print(isinstance(d1, Iterator))
print(isinstance(str1, Iterator))
print(isinstance(set1, Iterator))
print(isinstance(a, Iterator))
print(isinstance(b, Iterator))
print(isinstance(c, Iterator))
结果如下:
主要关注第二部分,发现字符串 str,列表 list, 字典 dict,元组 tuple,集合 set,没有实现 __iter__ MyObject 和仅实现 __iter__ 的 MyIterable 都不是迭代器,只有实现了 __iter__ 和 __next__ 的 MyIterator 才是迭代器。
对于可迭代对象可以使用 iter() 转为迭代器:
l2 = iter(l1)
t2 = iter(t1)
d2 = iter(d1)
str2 = iter(str1)
set2 = iter(set1)
b2 = iter(b)
print('*************************')
print(isinstance(l2, Iterator))
print(isinstance(t2, Iterator))
print(isinstance(d2, Iterator))
print(isinstance(str2, Iterator))
print(isinstance(set2, Iterator))
print(isinstance(b2, Iterator))
结果如下:
迭代器的优点:
- 提供了一种不依赖于索引的取值方式
- 惰性计算,节省内存
这里的节省内存是指 迭代器在迭代过程中,不会一次性把可迭代对象的所有元素都加载到内存中,仅仅是在迭代至某个元素时才加载该元素,而在这之前或之后,元素可以不存在或者被销毁。这个特点就使得迭代器适合用于遍历一些巨大的 或是 无限的集合。
迭代器的缺点:
- 取值不如按照索引取值来的方便(索引可以直接定位某一个值,迭代器不行,只能一个一个地取下去)
- 迭代器只能往后迭代,不能回退(执行 __next__ 方法只能向后,不能向前)
- 无法获取迭代器的长度
3. 生成器
生成器是一种特殊的迭代器,不过实现更加简单。实现的方式有两种:
- 生成器表达式
- 带有 yield 关键字的函数
生成器作为一种特殊的迭代器,相比于一般迭代器更加优雅。它不需要再像上面的类一样写 __iter__ 和 __next__ 方法了,只需要一个 yield 关键字或生成器表达式。
def fib(n):
i, a, b = 0, 0, 1
while i < n:
yield b
a, b = b, a + b
i = i + 1
g1 = (x * x for x in range(1, 11))
g2 = fib(6)
print('********************')
print(isinstance(g1, Iterator))
print(isinstance(g2, Iterator))
for i in range(10):
print(next(g1))
for i in range(6):
print(next(g2))
结果如下:
Python的yield关键字的作用,就是把一个普通的函数变成生成器。当一个函数内出现yield关键字后,就会变异为生成器,其行为与普通函数不同。
从动态的角度,生成器在运行过程中:
- 当生成器函数被调用的时候,生成器函数不执行内部的任何代码,直接立即返回一个迭代器。
- 当所返回的迭代器第一次调用 next 的时候,生成器函数从头开始执行,如果遇到了执行 yield x,next立即返回 yield 的值 x。
- 当所返回的迭代器继续调用next的时候,生成器函数从上次yield语句的下一句开始执行,直到遇到下一次执行yield。
- 任何时候遇到函数结尾,或者 return 语句,抛出 StopIteration 异常。
这里关注一下第三点:
def g():
print('L1')
yield 1
print('L2')
yield 2
print('L3')
yield 3
print('L4')
it = iter(g())
print('............')
v = next(it)
print(v)
v = next(it)
print(v)
v = next(it)
print(v)
v = next(it)
print(v)
结果如下:
可见 it = iter(g()) 语句之后生成器函数不执行内部的任何代码,直接立即返回一个迭代器直到第一次执行 next 这个时候才执行了 print(’…’),就从头开始执行到 yield 语句,然后状态挂起,等下一次执行 next 时,从上次的 yield 语句处继续执行。
4. 惰性计算
我查找的资料上面对于生成器的惰性计算都是认可的,但有的认为迭代器不是惰性计算,有的认为是惰性计算。实践出真理,我动手试验了一下
import os
import psutil
import gc
class MyIterator2:
def __init__(self, n):
self.i = -1
self.n = n
def __iter__(self):
return self
def __next__(self):
while (self.i + 1) < self.n:
self.i = self.i + 1
return self.i
class MyIterator3:
def __init__(self, n):
self.i = -1
self.n = n
def __iter__(self):
return self
def __next__(self):
while (self.i + 1) < self.n:
self.i = self.i + 1
yield self.i
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: {} M'.format(hint, memory))
def test_iterable():
show_memory_info('initing iterable')
my_iterable = [x * x for x in range(100000000)]
show_memory_info('after iterable initiated')
print('********')
def test_iterator1():
show_memory_info('initing iterator1')
my_iterable = [x * x for x in range(100000000)]
my_iterator1 = iter(my_iterable)
del my_iterable
gc.collect()
print(next(my_iterator1))
show_memory_info('after iterator1 initiated')
print('********')
def test_iterator2():
show_memory_info('initing iterator2')
my_iterator2 = MyIterator2(100000000)
show_memory_info('after iterator2 initiated')
print('********')
def test_iterator3():
show_memory_info('initing iterator3')
my_iterator3 = MyIterator3(100000000)
show_memory_info('after iterator3 initiated')
print('********')
def test_generator():
show_memory_info('initing generator')
my_generator = (x * x for x in range(100000000))
show_memory_info('after generator initiated')
print('********')
if __name__ == '__main__':
test_iterable()
test_iterator1()
test_iterator2()
test_iterator3()
test_generator()
结果如下:
1. 对于可迭代对象,显然不是惰性的,占用了大量内存。
2. 这个迭代器是用 iter() 方法将一个可迭代对象转为一个迭代器,结果还是占用了大量内存,我一开始推测是由于一开始的可迭代对象占用了大量内存。但是我发现删除可迭代对象并回收内存之后还是占有了大量内存,因此我判定用这种方式生成的迭代器不是惰性的。
3. 手动定义 __iter__ 和 __next__ 生成的迭代器确实是惰性的,占用的内存大大减少。
4. 一开始我在解决生成器的内存减少是 yield 关键字的问题,结合 3 和 4 的对比,发现即使是 return 语句也是惰性的,所以惰性的关键不是 yield 的语句。
5. 生成器的惰性也是显然地。
5. 迭代器和生成器只供消耗一次
x = [0, 1, 2, 3, 4]
y = iter([0, 1, 2, 3, 4])
z = (i for i in range(5))
print(tuple(x))
print(tuple(x))
print(tuple(y))
print(tuple(y))
print(tuple(z))
print(tuple(z))
结果如下:
可以看到可迭代对象可以重复使用,而迭代器和生产器只供消耗一次。
6 iter() 产生的迭代器
def scale(data, factor):
for j in range(len(data)):
data[j] *= factor
a_list = [1, 2, 3, 4, 5]
iterator1 = iter(a_list)
print(next(iterator1))
print(next(iterator1))
print(next(iterator1))
scale(a_list, 10)
print(next(iterator1))
print(next(iterator1))
结果如下:
iter() 产生的迭代器保存原始列表的当前索引,该索引指向下一个元素。因此,如果原始列表的内容在迭代器构造之后但在迭代完成之前被修改,迭代器将报告原始列表的更新内容。
总结
- 生成器 ⫋ \subsetneqq ⫋ 迭代器 ⫋ \subsetneqq ⫋ 可迭代对象。
- 任何可迭代对象都可以使用 for…in 语句。实际上在运行 for…in 时,由解释器帮助我们对可迭代独享调用了 iter() 方法,得到一个迭代器,然后每次使用 next 取一个值出来。
- 以列表 list 为例,list 本身只有 __iter__ 方法,因此只是一个可迭代对象,但是执行 for…in 的时候,调用 __iter__ 方法 返回一个迭代器,此迭代器有 __next__ 方法,正在用来执行 for…in…
- 迭代器的适用的场景:不关心元素的随机访问,元素的个数不可提前预测。
- 使用 iter(可迭代对象) 生成的迭代器不是惰性的,而严格手动定义 __iter__ 和 __next__ 方法的迭代器是惰性的,且与使用 return 语句和 yield 语句无关。
- 生成器是惰性的。
- 生成器使用 yield 语句更加优雅,但本身就是一种特殊的迭代器。
- 有的资料提到的生成器的延时操作可以用惰性计算理解:延迟操作就是在需要的时候才产生结果,不是立即产生结果,显然和惰性计算本质是类似的。