美文网首页
Python中的迭代器与生成器

Python中的迭代器与生成器

作者: SOLAREST | 来源:发表于2016-12-27 20:11 被阅读66次

    迭代器(Iterable)

    • 简单来说,迭代器对象(my_list)可以让以下代码正常工作:
    for i in my_list:
        ...
    
    for i in iter(my_list):
        ...
    
    for i in (v for v in my_list if v is not None):
        ...
    
    • 如果对象实现了__iter__()就可以使用迭代器。我们可以手动实现迭代协议(iterator protocol)来实现对对象的迭代操作。值得注意的是,这里的对象特指Iterator
    class Counter:
        def __init__(self, low, high):
            self.current = low
            self.high = high
    
        def __iter__(self):
            return self
    
        def __next__(self):  # Python 2: def next(self)
            if self.current > self.high:
                raise StopIteration
            else:
                self.current += 1
                return self.current - 1
    
    if __name__ == '__main__':
        c = Counter(1, 5)
        for i in c:
            print(i)
    

    在看这段代码之前,让我们先来明确一个概念:

    就Python中的迭代器而言有两个含义:第一个是Iterable,第二个是Iterator
    协议规定Iterable的__iter__()方法会返回一个Iterator,Iterator的__next__()方法(Python 2里是next())会返回下一个Iterator对象,如果迭代结束则抛出StopIteration异常。
    同时,Iterator自己也是一种Iterable,所以也需要实现Iterable的接口,也就是__iter__(),而Iterator的__iter__()只需要返回自己就行了。

    明确之后,我们来看看上述代码发生了什么。首先for语句判断对象c是一个Iterable(因为实现了__iter__())于是调用它的__iter__()方法返回了一个Iterator对象,接着for语句继续会调用它的__next__()方法(因为此时起作用的是Iterator对象)返回下一个Iterator,以此往复直到抛出StopIteration异常。

    • 但是python的for方法对未实现迭代协议的对象也进行了兼容,比如我们熟悉的:
      for key in {"a": 1, "b": 2}:
        ...
      

    这是因为对于实现了__getitem__的对象for方法会改用以下标迭代的方式:

    class Counter:
        def __init__(self, low, high):
            self.low = low
            self.high = high
    
        def __len__(self):
            return self.high - self.low
    
        def __getitem__(self, key):
            index = self.low + key
            if self.low <= index <= self.low + len(self):
                return index
            else:
                raise IndexError
    
    if __name__ == '__main__':
        c = Counter(1, 5)
        for i in c:
            print(i)
    

    这一点与Iterator本身无关,但是却属于Iterable的范畴。

    我们熟悉的Python的内建对象dictlist都是可迭代的(Iterable),但是它们满足的是序列协议(sequence protocol)故可迭代。

    实际上:

    iterable: 实现了__iter__()__getitem__()方法的对象。
    iterator: 实现了 iterator protocol(即方法:__next__()__iter__())的iterable对象。
    sequence:实现了 sequence protocol(即方法: __getitem__()__len__()),并能使用整数索引访问元素的iterable对象。

    这一点可以参考 PEP 234
    同时,附一张图说明它们之间关系:

    Iterables

    生成器(Generators)

    • 首先Generators也是一个Iterator对象,内部也实现了__iter__()__next__()方法,我们可以使用以下方法使用生成器:

      1. 在函数中使用yield
      def count_generator():
          i = 0
          while True:
              i += 1
              yield i
      
      if __name__ == '__main__':
          a = count_generator()
          print(a)               # <generator object count_generator at 0x7f2474adda40>
          print(next(a))      # 1
          print(next(a))      # 2
          print(next(a))      # 3
      
      1. 生成器表达式
      a = (i for i in range(1, 10))
      
      print(a)                    # <generator object <genexpr> at 0x01291720>
      print(next(a))           # 1
      print(next(a))           # 2
      print(next(a))           # 3
      
    • Iterator有所区别的是,Generators迭代的本质就是通过next()__next__()方法来调用send()方法(可以参考PEP 342)。我们可以用以下方式实现类似Iterable的迭代:

      c = (i for i in range(1, 10))
      for i in range(1, 10):
        print(c.send(None))
    
    • 所以既然我们可以使用send()方法来控制生成器的输出,当然我们也可以实现从外部对生成器输出的控制,来看一看下面这段没什么用的代码:
    def generater_list(i=1):
        while i < 10:
          value = (yield i)
          if value:
            i += value
          else:
            i += 1
    
    if __name__ == '__main__':
        g = generater_list()
        print(next(g))  # 1
        print(next(g))  # 2
        g.send(4)
        print(next(g))  # 7
        print(next(g))  # 8
    

    在上述代码中,send()方法,通过yield给value赋值并将函数挂起。当使用next()时,生成器函数的返回值就相应的发生了改变。

    如果需要直接调用 send(),第一次请务必 send(None) 只有这样,一个 Generator 才算是真正被激活了。我们才能进行下一步操作。

    • 为什么要使用生成器:

      1. 对内存友好

      也许当处理处理上GB的文件时,将它全部读入内存不会再是一个明智的选择,我们可以把它放到生成器函数中:

      def file_reader(file_path):
            with open(file_path, "rt") as f:
                for line in f:
                    yield line
      
      1. 实现协程

      生成器的特性十分适合完成一些调度作业,比如说下面这个例子:

        from collections import deque
        
        def count_down(n):
            while n > 0:
                print('T-minus', n)
                yield
                n -= 1
            print('Blastoff!')
      
        def count_up(n):
            x = 0
            while x < n:
                print('Counting up', x)
                yield
                x += 1
      
        class TaskScheduler:
            def __init__(self):
                self._task_queue = deque()
      
            def new_task(self, task):
                self._task_queue.append(task)
      
            def run(self):
                while self._task_queue:
                    task = self._task_queue.popleft()
                    try:
                        # Run until the next yield statement
                        next(task)
                        self._task_queue.append(task)
                    except StopIteration:
                        # Generator is no longer executing
                        pass
      
        if __name__ == '__main__':
            sched = TaskScheduler()
            sched.new_task(count_down(10))
            sched.new_task(count_down(5))
            sched.new_task(count_up(15))
            sched.run()
      

    我们运行一下这段代码,可以发现TaskScheduler类在一个循环中运行生成器集合——每个都运行到碰到yield语句为止,然后马上运行我们定义好的两个函数中的另一个。

    Reference

    1. http://manjusaka.itscoder.com/2016/09/11/something-about-yield-in-python/
    2. http://blog.chinaunix.net/uid-15174104-id-4172583.html
    3. http://www.oschina.net/translate/improve-your-python-yield-and-generators-explained
    4. https://www.zhihu.com/question/44015086
    5. 《Python Cookbook》

    相关文章

      网友评论

          本文标题:Python中的迭代器与生成器

          本文链接:https://www.haomeiwen.com/subject/atuevttx.html