迭代器和生成器原理

《Python3高级核心技术97讲,bobby》学习笔记,第八章:迭代器和生成器原理。
411阅读 · 2020-6-14 23:17发布

8.1 迭代协议

  • 迭代器是访问集合内元素的一种方式,一般用来遍历数据。
  • 迭代器和以下标的访问方式不一样,迭代器是不能返回的,迭代器提供了一种惰性访问数据的方式(访问数据的时候才计算)。
  • __iter__魔法方法就是迭代协议。
  • 可迭代对象(Iterable)只需要实现__iter__魔法方法。
  • 迭代器(Iterator)继承了Iterable,实现__next__魔法方法和__iter__方法。
  • 抽象基类代码:

    class Iterable(Protocol[_T_co]):
          @abstractmethod
          def __iter__(self) -> Iterator[_T_co]: ...
    
      class Iterator(Iterable[_T_co], Protocol[_T_co]):
          @abstractmethod
          def __next__(self) -> _T_co: ...
          def __iter__(self) -> Iterator[_T_co]: ...
    

8.2 什么是迭代器和可迭代对象

  • 使用iter()方法时,如果目标对象没有实现__iter__魔法函数,则会使用__getitem__返回一个iterator对象。
  • 迭代器会在内部维护一个index用于计数。
  • 迭代的逻辑交给迭代器完成,是一种迭代器设计模式(使可迭代对象无需维护迭代器中的内置变量)。
  • 可迭代对象实现迭代器代码示例:

    from collections.abc import Iterator
      class Company(object):
          def __init__(self,employee_list):
              self.employee = employee_list
    
          def __iter__(self):
              return MyIterator(self.employee)
    
      class MyIterator(Iterator):
          def __init__(self, employee_list):
              self.iter_list = employee_list
              self.index = 0
    
          def __next__(self):
              try:
                  word = self.iter_list[self.index]
              except IndexError:
                  raise StopIteration
              self.index += 1
              return word
    
      if __name__ == "__main__":
          company = Company(["tom", "jack", "marry"])
          for item in company:
              print(item)
    

8.3 生成器函数的使用

  • 函数中包含yield,就是生成器函数。
  • yield返回的是生成器对象generator。
  • 生成器对象在python编译字节码的时候就产生的。
  • 执行时会从上一次的yield处继续执行。
  • 生成器函数可以return一个值(早期版本python不行)。
  • 使用生成器函数实现斐波拉契:

    def gen_fib(index):
          n,a,b = 0,0,1
          while n<index:
              yield b
              a,b = b,a+b
              n +=1
    
      for data in gen_fib(10):
          print(data)
    

8.4 生成器的原理

函数运行原理

def foo():
    bar()
def bar():
    pass
  • python解释器由C编写,它会使用C语言函数PyEval_EvalFramEx执行foo函数。
  • 首先会创建一个栈帧(stack frame),该栈帧也是一个对象。
  • 此时foo函数也是字节码对象。
  • 可以通过内置函数dis的dis()方法查看对象字节码。
    import dis
      print(dis.dis(foo))
    
  • 在栈帧的上下文中,运行字节码,字节码全局唯一。
  • foo函数调用子函数bar时,会再次创建一个栈帧,将函数的控制权交给栈帧对象。
  • 在新创建的栈帧中运行bar字节码。
  • 此时拥有两个栈帧,所有的栈帧都分配在堆内存中(堆的特点是不释放则一直存在在内存中)。
  • 所以说明栈帧独立于调用者存在(原调用者后面不存在也可以)。

生成器函数利用了python函数调用过程(栈帧都分配在堆内存上的特性)

  • python解释器在遇到yield关键字,就会标记函数为生成器函数。
  • 当调用生成器函数时会返回生成器对象(实际是对frame做了封装)。
  • 生成器对象的gi_frame.f_lasti存放字节码位置,gi_frame.f_locals存放值(因此生成器可以找到上一次执行的位置)。

  • 生成器分配在堆内存中,只要拿到生成器对象(栈帧对象),就可以在任何地方控制生成器(也是实现协程的基础)。

8.5 使用生成器的UserList

  • collcetions的UserList是用python实现的list。(list是C语言写的)
  • UserList继承于Sequence,Sequence中实现了iter魔法函数。示例:

    class Sequence(Reversible, Collection):
    
          """All the operations on a read-only sequence.
    
          Concrete subclasses must override __new__ or __init__,
          __getitem__, and __len__.
          """
    
          __slots__ = ()
    
          @abstractmethod
          def __getitem__(self, index):
              raise IndexError
    
          def __iter__(self):
              i = 0
              try:
                  while True:
                      v = self[i]
                      yield v
                      i += 1
              except IndexError:
                  return
    
          ...省略...
    

8.6 使用生成器读取大文件

  • 文件500g,只有一行,使用{|}分割。示例代码:

    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('input.txt') as f:
          for line in myreadlines(f,"{|}"):
              print(line)