Python-可迭代对象,迭代器,生成器,序列

这几个玩意比较绕,虽然平时用起来一般没什么问题,但看一些文档的时候,什么iterable, iterator一不留神就傻傻分不清楚了

注: 下文代码里会使用两个内置模块

import itertools
import collections

基本概念

  1. iterable: 可迭代对象,简单来说就是可以运行for i in obj而不报错的对象
  2. iterator: 迭代器,是用来对可迭代对象进行迭代的一个工具。一般指实现了迭代器协议(即__iter__next方法)的类实例,判断标准是assert isinstance(obj, collections.Iterator)
  3. generator: 生成器,是一种迭代器,使用yield关键字实现。(此处仅作对比,不考虑生成器的其他用法)
  4. sequence: 序列,指实现了序列协议的类实例,内置的包括: str, unicode, list, tuple, bytearray, buffer, xrange
  5. iter(obj): 一个内置函数,可以返回传入obj对应的迭代器,这里要求obj必须实现了迭代器协议或序列协议。

详细解读

iterable 可迭代对象

首先是一个令人难受的消息,可迭代对象的判别标准,到现在都没有一个优雅的方式,我们只能说:

可以运行for i in obj不报错的,或者可以运行iter(obj)而不报错的,就是可迭代对象

参见帖子: https://stackoverflow.com/questions/1952464/in-python-how-do-i-determine-if-an-object-is-iterable

这里面的逻辑是这样的: for i in obj里,会调用iter函数返回迭代器,然后使用迭代器进行迭代。iter函数会根据两个协议得到一个迭代器返回,两个协议如下:

第一种,实现了__iter__方法的,因为这个方法返回的就是迭代器,iter可以直接使用这个迭代器

第二种,实现了__getitem__,并且是数字索引,并且是从0开始依次+1的数字索引的,iter函数会构造一个迭代器返回。代码大概是这样,这里写yield是为了表述方便,实际代码应该是用C语言写的

def foo2(obj):
    i = 0
    while 1:
        try:
            yield obj.__getitem__(i)  # 这句等价于 yield obj[i]
        except IndexError:
            return
        i += 1

因为这里只except了IndexError,所以可以避免对只实现了__getitem__的mapping类对象的误判,举个例子

class ST(object):
    def __init__(self):
        self.info = 'qwer'

    def __getitem__(self, item):
        return self.info[item]

class CT(object):
    def __init__(self):
        self.info = {
            0: 'a',
            1: 'b',
            2: 'c',
        }

    def __getitem__(self, item):
        return self.info[item]

print(list(iter(ST())))  # ['q', 'w', 'e', 'r']
print(list(iter(CT())))  # 会报错 KeyError: 4

为了下面表述方便,我将实现了第一类协议的iterable称呼为一类iterable,实现了第二类协议的称呼为二类iterable

对一类iterable,可以有一种比较好的判断方式assert isinstance(obj, collections.Iterable)

还有一个需要注意的是,可迭代对象不一定是有序的。比如字符串就一定有序,集合就一定无序。

iterator 迭代器

参见官网文档: https://docs.python.org/2/library/stdtypes.html#iterator-types

文档里指出,只要实现了迭代器协议的,就算是迭代器。也就是只要正确实现了__iter__next(python3里是__next__),就是迭代器。这里要求next可以不断取到下一个,而迭代器的__iter__一定返回自身

普通迭代器在使用时,基本就是疯狂调用next,直到抛出StopIteration异常。下面代码里写yield同样只为表述方便,并不是实际代码

def foooo(obj):
    while 1:
        try:
            yield obj.next()
        except StopIteration:
            return

需要注意的很重要的一点是,迭代器是可耗尽的,也就是一次性的。举例来说,如下代码,a是一个迭代器,第一次循环的时候可以正常使用,输出结果,第二次使用时,因为迭代器已耗尽,所以什么都输出不了,这时候如果再调动一下a.next(),就必定会报StopIteration

a = [1, 2, 3].__iter__()
print a
print list(i for i in a)
print list(i for i in a)

输出结果是: 
<listiterator object at 0x109021610>
[1, 2, 3]
[]

那如果想重复使用一个迭代器,该怎么做呢,内置的itertools模块有一个tee(iterable, n=2)方法,接收一个可迭代对象,然后返回n个它的不同的迭代器,这个函数有个关于新旧迭代器的小坑,感兴趣的自己去了解吧,源码和官方文档在这 https://docs.python.org/2.7/library/itertools.html#itertools.tee ,应该已经讲的很清楚了,想看实例的话可以点 阅读原文,看我之前写的itertools模块的调用实例。搞清楚这个函数,迭代器的“耗尽”就肯定没问题了

a = [1, 2, 3].__iter__()
print a
b, c = itertools.tee(a)
print b, c
print list(i for i in b)
print list(i for i in c)

输出结果是: 
<listiterator object at 0x10345e610>
<itertools.tee object at 0x103466098> <itertools.tee object at 0x1034660e0>
[1, 2, 3]
[1, 2, 3]

generator 生成器

参见官网: https://docs.python.org/2/library/stdtypes.html#generator-types

生成器也是一种迭代器,那么它和一般迭代器相比,有什么优势?

我认为优势有三点

第一点是,生成器比迭代器多实现了三个方法,send, throw和close,提供了更多操作,这里不再展开。

第二点是,生成器更方便,生成器有两种构造方式,1是函数类,用yield关键字构建,2是列表推导的时候,把列表推导最外层的[]换成(),这两种方式中__iter__next这两个函数都不需要显式实现,而是由生成器自动提供,可以让代码更pythonic,也更易读

第三点是,生成器更适合做计算,即含有复杂逻辑的迭代。我们知道普通迭代器不停next就完了,而生成器一般要求在next之前需要做各种计算和判断,然后返回合适的值。虽然这种操作使用迭代器也能实现,但跟生成器相比实在是不方便。举个最经典的计算斐波那契数列的例子,下面是迭代器实现和生成器实现。

class Fib_1(object):
    def __init__(self):
        self.n1, self.n2 = 0, 1

    def __iter__(self):
        return self

    def next(self):
        self.n1, self.n2 = self.n2, self.n1 + self.n2
        return self.n1

def fib_2():
    a, b = 0, 1
    while 1:
        a, b = b, a + b
        yield a

f1 = Fib_1()
print(f1)
assert f1 is iter(f1)
assert isinstance(f1, collections.Iterable)
assert isinstance(f1, collections.Iterator)
print(list(itertools.islice(f1, 0, 10)))

f2 = fib_2()
print(f2)
assert f2 is iter(f2)
assert isinstance(f2, collections.Iterable)
assert isinstance(f2, collections.Iterator)
print(list(itertools.islice(f2, 0, 10)))

输出结果为: 
<__main__.Fib_1 object at 0x108793810>
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
<generator object fib_2 at 0x1087a6d20>
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

其实,生成器一般不跟迭代器做比较,生成器比较的是类似列表推导一类的,先把全量数据生成出来,再依次迭代的情况,那种情况会消耗大量内存空间,而生成器消耗的空间是一定的,跟size没关系。最经典的例子是rangexrange,后者的__iter__是用的生成器实现,这样在迭代时,就不会像前者一样先把整个list算出来再迭代,而是迭代一个算一个,在数据量超大的时候,后者的优势会极为明显。

如下面两行,同样是输出0~1亿序列中的前三位,第一行会先把这一亿个数算出来,存到list里,然后再摘前三个,速度超慢还浪费空间。第二行则是一共就生成了三个,时间空间节省的不是一点半点。注意,生成器不是序列,所以不能用切片功能,我这里用的itertools.islice可以对迭代器进行切片,返回的仍然是迭代器,所以最后要用list包一层,消耗尽迭代器。

print(list(range(0, 100000000)[:3]))
print(list(itertools.islice(xrange(0, 100000000), 0, 3)))

输出结果为: 
[0, 1, 2]
[0, 1, 2]

sequence 序列

参加官网文档: https://docs.python.org/2/library/stdtypes.html#sequence-types-str-unicode-list-tuple-bytearray-buffer-xrange

序列,是指有顺序的,有数字索引的,且索引是从0到序列长度-1的,不是mapping的一种结构。python里就只有六种: str, unicode, list, tuple, bytearray, buffer, xrange,自定义的类如果想通过sequence检测,要么继承collections.Sequence,要么继承Sequence,要么使用Sequence.register(MyClass),没有像迭代器那样实现几个方法就可以成为迭代器的操作。我们常用的其实也就是字符串,列表,元组以及xrange。这个xrange本身是sequence,但它的__iter__是返回的生成器,这个需要注意下。

我们平时一般也不需要去判断一个obj是不是序列,如果非要判断的话,可以参考这个帖子: https://stackoverflow.com/questions/43566044/what-is-pythons-sequence-protocol

这些概念之间的关系

左与上的关系 iterable iterator generator
iterable iterable不是iterator
一类iterable iterable.__iter__()返回其iterator
iterator iterator是一类iterable iterator.__iter__()返回自身
generator generator是一类iterable generator是iterator generator.__iter__()返回自身(还是generator)
sequence sequence是二类iterable sequence不是iterator sequence一般不扯生成器,但xrange.__iter__()是用生成器实现的,所以说具体情况具体分析吧

文章首发于微信公众号:Woko笔记
欢迎大家来拍砖~

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

推荐阅读更多精彩内容