Python中的迭代器与生成器

迭代器(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》
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容