深入理解迭代器与生成器

2018 年 10 月 12 日 • 阅读数: 115

深入理解迭代器与生成器

迭代协议

  • 迭代器是访问集合内元素的一种方式,一般用来遍历
  • 迭代器是不能返回的,迭代器提供了一种惰性的数据访问方式
  • Iterator(迭代器)是继承自Iterable(可迭代对象)的
  • Iterable实现了 iter 这个魔法方式,只要实现的该方法就是一个可迭代对象
  • Iterator有两个魔法方法 iternext ,我们都需要实现它
  • 可以直接通过python内置函数iter将一个可迭代对象转换成迭代器
'''常用的list,tuple,dict等,都是可迭代对象,但不是一个迭代器'''
from collections import Iterable,Iterator
a = [1,2,3,4,5]
print(isinstance(a,Iterable))
print(isinstance(a,Iterator))
True
False
"""
iter(iterable) -> iterator
iter(callable, sentinel) -> iterator
Get an iterator from an object.  In the first form, the argument must
supply its own iterator, or be a sequence.
In the second form, the callable is called until it returns the sentinel.
"""
iter_a = iter(a)
print(isinstance(iter_a,Iterable))
print(isinstance(iter_a,Iterator))
True
True
  • 在使用for循环遍历一个可迭代对象的时候,实际上也是调用了iter方法
  • iter方法首先会去对象里面查找有没有 iter 这个魔法方法,有就直接调用
  • 如果没有就继续查找有没有 getitem 这个魔法方法,有就调用该方法
  • 如果没有,就会抛出一个TypeError的异常

自定义迭代器

  • 最简单的可以写一个类继承Iterator
  • 当然也可以什么都不继承,然后自己实现 iternext 魔法方法
  • 我们一般不会在一般类里面实现 next 而是单独设计一个迭代器,在类里面的 iter 中返回
  • 这是python关于迭代器的设计模式规定的
class Company(object):
    def __init__(self,employee_list):
        self.employee = employee_list
    
    def __iter__(self):
        return MyIterator(self.employee)
class MyIterator(object):
    def __init__(self,employee_list):
        self.iter_list = employee_list
        self.index = 0
        
    def __iter__(self):
        return self
    
    def __next__(self):
        '''这里加入try语句的目的是:因为for语句可以处理StopIteration而不能处理IndexError'''
        try:
            word = self.iter_list[self.index]
        except IndexError:
            raise StopIteration
        self.index += 1
        return word
employee_list = ['wmm','zxy','wjl','amor']
company = Company(employee_list)
'''for循环的原理如下'''
my_itor = iter(company)
while True:
    try:
        print(next(my_itor))
    except StopIteration:
        break
wmm
zxy
wjl
amor

生成器函数

  • 函数中只要有yield关键字,就是生成器函数
  • 调用生成器函数,会返回一个生成器(generator)对象
  • 生成器对象,python在编译字节码的时候就产生了
  • 生成器也是一个迭代器
def gen_func():
    yield 'gen'
gen = gen_func()
print(type(gen))
print(isinstance(gen,Iterator))
<class 'generator'>
True

yield关键字

  • 一个函数可以包含多个yield语句
  • yield语句为惰性求值,延迟求值提供了可能
  • 在一个函数中,程序执行到yield语句的时候,程序暂停,返回yield后面表达式的值
  • 在下一次调用的时候,从yield语句暂停的地方继续执行,如此循环,直到函数执行完

斐波拉契数列

  • 斐波拉契数列可以通过很多种方法实现,但这里使用生成器来实现的话就可以大大节约内存
  • 第一种方式,通过递归实现,这种方式只能返回最终结果,不能返回整个过程
  • 第二种方式,如果数据很大,那么则需要维护一个很大的list,内存花销极大
  • 第三种方式,通过生成器的方式,可以在需要的时候调用,不需要维护大量数据
def fib(num):
    if num <= 2:
        return 1
    else:
        return fib(num-1) + fib(num-2)
fib(10)
55
def fib(num):
    re_list = []
    n,a,b = 0,0,1
    while n < num:
        re_list.append(b)
        a,b = b,a+b
        n += 1
    return re_list
fib(10)
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
def gen_fib(num):
    n,a,b = 0,0,1
    while n < num:
        yield b
        a,b = b,a+b
        n += 1
for data in gen_fib(10):
    print(data,end=' ')
1 1 2 3 5 8 13 21 34 55 

python中函数的工作原理

  • 在python解释器中会用一个 PyEval_EvalFramEx() 去指向我们定义的函数
  • 它首先会创建一个栈帧,然后使用栈帧去调用python的字节码

generator_func.png

def foo():
    bar()
def bar():
    pass
import dis
"""
python一切皆对象,栈帧对象,字节码对象
当foo调用bar,又会创建一个栈帧
所有的栈帧都是分配在堆内存上,这就决定了栈帧可以独立于调用者存在
也就意味着生成器函数可以在任意地方被暂停或调用
"""
dis.dis(foo)
  2           0 LOAD_GLOBAL              0 (bar)
              2 CALL_FUNCTION            0
              4 POP_TOP
              6 LOAD_CONST               0 (None)
              8 RETURN_VALUE

生成器原理

  • 到python解释器遇到函数内部包含了yield关键字的时候,就会返回一个生成器对象
  • 这个生成器对象,实际上是对栈帧元素再进行了一层封装
  • 它使用了两个变量来记录生成器类的一个执行情况
  • f_lasti记录的生成器上一次执行到的位置
  • f_locals记录了生成器内部的变量情况

generator_func.png

def gen_fun():
    yield 1
    print('gen')
    yield 2
    name = 'amor'
    yield 3
    age = 20
    return 'gen_fun'
gen = gen_fun()
print(gen.gi_frame.f_lasti)
print(gen.gi_frame.f_locals)
-1
{}
for data in gen:
    print('------------------')
    print('data:',data)
    print('f_lasti:',gen.gi_frame.f_lasti)
    print('f_locals:',gen.gi_frame.f_locals)
------------------
data: 1
f_lasti: 2
f_locals: {}
gen
------------------
data: 2
f_lasti: 16
f_locals: {}
------------------
data: 3
f_lasti: 26
f_locals: {'name': 'amor'}

生成器的应用场景

  • 在处理大文件的读取中的应用
  • buf相当一个缓存的作用,一开始buf为空,所以不会进入内层循环
  • 进而读取4096字节的数据到chunk中,如果未读取到数据,说明文件已为空
  • 将chunk的数据追加到buf中
  • 在内层循环判断newline分隔符是否存在,存在则找到它的下标,做切片
  • 通过yield返回一行数据,刷新buf
def myreadlines(f,newline):
    buf = ""
    while True:
        while newline in buf:
            pos = buf.index(newline)
            yield buf[:pos]
            buf = buf[pos + len(newline):]
        chunk = f.read(4096)
        if not chunk:
            yield buf
            break
        buf += chunk
with open('data/data.txt') as f:
    for line in myreadlines(f,'{|}'):
        print(line)
aaaaaaaaaaaaaaaaaaaaaa
bbbbbbbbbbbbbbbbbbbbbb
cccccccccccccccccccccc
dddddddddddddddddddddd
标签: Python生成器

召唤伊斯特瓦尔