社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

理解 Python 迭代对象、迭代器、生成器

Python爱好者社区 • 9 年前 • 1170 次点击  


点击上方图片报名作者微课


作者:刘志军,6年+Python使用经验, 高级开发工程师,目前在互联网医疗行业从事Web系统构架工作

个人公众号:Python之禅(微信ID:vttalk)


本文源自RQ作者的一篇博文,原文是 Iterables vs. Iterators vs. Generators,俺写的这篇文章是按照自己的理解做的参考翻译。

在了解Python的数据结构时,容器(container)、可迭代对象(iterable)、迭代器(iterator)、生成器(generator)、列表/集合/字典推导式(list,set,dict comprehension)众多概念参杂在一起,难免让初学者一头雾水,我将用一篇文章试图将这些概念以及它们之间的关系捋清楚。

容器(container)

容器是一种把多个元素组织在一起的数据结构,容器中的元素可以逐个地迭代获取,可以用 innot in关键字判断元素是否包含在容器中。通常这类数据结构把所有的元素存储在内存中(也有一些特列并不是所有的元素都放在内存)在Python中,常见的容器对象有:

  • list, deque, ....

  • set, frozensets, ....

  • dict, defaultdict, OrderedDict, Counter, ....

  • tuple, namedtuple, …

  • str

容器比较容易理解,因为你就可以把它看作是一个盒子、一栋房子、一个柜子,里面可以塞任何东西。从技术角度来说,当它可以用来询问某个元素是否包含在其中时,那么这个对象就可以认为是一个容器,比如 list,set,tuples都是容器对象:

  1. >>> assert 1 in [1, 2, 3]      # lists

  2. >>> assert 4 not in [1, 2, 3]

  3. >>> assert 1 in {1, 2, 3}      # sets

  4. >>> assert 4 not in {1, 2, 3}

  5. >>> assert 1 in (1, 2, 3)      # tuples

  6. >>> assert 4 not in (1, 2, 3)

询问某元素是否在dict中用dict的中key:

  1. >>> d = {1: 'foo', 2: 'bar', 3: 'qux'}

  2. >>> assert 1 in d

  3. >>> assert 'foo' not in d  # 'foo' 不是dict中的元素

询问某substring是否在string中:

  1. >>> s = 'foobar'

  2. >>> assert 'b' in s

  3. >>> assert 'x' not in s

  4. >>> assert 'foo' in s

尽管绝大多数容器都提供了某种方式来获取其中的每一个元素,但这并不是容器本身提供的能力,而是可迭代对象赋予了容器这种能力,当然并不是所有的容器都是可迭代的,比如:Bloom filter,虽然Bloom filter可以用来检测某个元素是否包含在容器中,但是并不能从容器中获取其中的每一个值,因为Bloom filter压根就没把元素存储在容器中,而是通过一个散列函数映射成一个值保存在数组中。

可迭代对象(iterable)

刚才说过,很多容器都是可迭代对象,此外还有更多的对象同样也是可迭代对象,比如处于打开状态的files,sockets等等。但凡是可以返回一个迭代器的对象都可称之为可迭代对象,听起来可能有点困惑,没关系,先看一个例子:

  1. >>> x = [1, 2, 3]

  2. >>> y = iter(x)

  3. >>> z = iter(x)

  4. >>> next(y)

  5. 1

  6. >>> next(y)

  7. 2

  8. >>> next(z)

  9. 1

  10. >>> type (x)

  11. <class 'list'>

  12. >>> type(y)

  13. <class 'list_iterator'>

这里 x是一个可迭代对象,可迭代对象和容器一样是一种通俗的叫法,并不是指某种具体的数据类型,list是可迭代对象,dict是可迭代对象,set也是可迭代对象。 y和 z是两个独立的迭代器,迭代器内部持有一个状态,该状态用于记录当前迭代所在的位置,以方便下次迭代的时候获取正确的元素。迭代器有一种具体的迭代器类型,比如 list_iterator, set_iterator。可迭代对象实现了 __iter__和 __next__方法(python2中是 next方法,python3是 __next__方法),这两个方法对应内置函数 iter()和 next() 。 __iter__方法返回可迭代对象本身,这使得他既是一个可迭代对象同时也是一个迭代器。

当运行代码:

  1. x = [1, 2, 3]

  2. for elem in x:

  3.    ...

实际执行情况是: 

反编译该段代码,你可以看到解释器显示地调用 GET_ITER指令,相当于调用 iter(x), FOR_ITER指令就是调用 next()方法,不断地获取迭代器中的下一个元素,但是你没法直接从指令中看出来,因为他被解释器优化过了。

  1. >>> import dis

  2. >>> x = [1, 2, 3]

  3. >>> dis.dis('for _ in x: pass')

  4.  1           0 SETUP_LOOP              14 (to 17)

  5.              3 LOAD_NAME                0 (x)

  6.              6 GET_ITER

  7.        >>    7 FOR_ITER                 6 (to 16)

  8.             10 STORE_NAME               1 (_)

  9.             13 JUMP_ABSOLUTE            7

  10.        >>   16 POP_BLOCK

  11.        >>   17 LOAD_CONST               0 (None)

  12.             20 RETURN_VALUE

迭代器(iterator)

那么什么迭代器呢?它是一个带状态的对象,他能在你调用 next()方法的时候返回容器中的下一个值,任何实现了 __next__()(python2中实现 next())方法的对象都是迭代器,至于它是如何实现的这并不重要。

所以,迭代器就是实现了工厂模式的对象,它在你每次你询问要下一个值的时候给你返回。有很多关于迭代器的例子,比如 itertools函数返回的都是迭代器对象。

生成无限序列:

  1. >>> from itertools import count

  2. >>> counter = count(start=13)

  3. >>> next(counter)

  4. 13

  5. >>> next(counter)

  6. 14

从一个有限序列中生成无限序列:

  1. >>> from itertools import cycle

  2. >>> colors = cycle(['red', 'white', 'blue'])

  3. >>> next(colors)

  4. 'red'

  5. >>> next(colors)

  6. 'white'

  7. >>> next(colors)

  8. 'blue'

  9. >>> next(colors)

  10. 'red'

从无限的序列中生成有限序列:

  1. >>> from itertools import islice

  2. >>> colors = cycle(['red', 'white', 'blue'])  # infinite

  3. >>> limited = islice( colors, 0, 4)            # finite

  4. >>> for x in limited:                        

  5. ...     print(x)

  6. red

  7. white

  8. blue

  9. red

为了更直观地感受迭代器内部的执行过程,我们自定义一个迭代器,以斐波那契数列为例:

  1. class Fib:

  2.    def __init__(self):

  3.        self.prev = 0

  4.        self.curr = 1

  5.    def __iter__(self):

  6.        return self

  7.    def __next__(self):

  8.        value = self.curr

  9.        self.curr += self.prev

  10.        self.prev = value

  11.        return value

  12. >>> f = Fib ()

  13. >>> list(islice(f, 0, 10))

  14. [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Fib既是一个可迭代对象(因为它实现了 __iter__方法),又是一个迭代器(因为实现了 __next__方法)。实例变量 prev和 curr用户维护迭代器内部的状态。每次调用 next()方法的时候做两件事:

  1. 为下一次调用 next()方法修改状态

  2. 为当前这次调用生成返回结果

迭代器就像一个懒加载的工厂,等到有人需要的时候才给它生成值返回,没调用的时候就处于休眠状态等待下一次调用。

生成器(generator)

生成器算得上是Python语言中最吸引人的特性之一,生成器其实是一种特殊的迭代器,不过这种迭代器更加优雅。它不需要再像上面的类一样写 __iter__()和 __next__()方法了,只需要一个 yiled关键字。 生成器一定是迭代器(反之不成立),因此任何生成器也是以一种懒加载的模式生成值。用生成器来实现斐波那契数列的例子是:

  1. def fib():

  2.    prev, curr = 0, 1

  3.    while True:

  4.        yield curr

  5.        prev, curr = curr, curr + prev

  6. >>> f = fib()

  7. >>> list(islice(f, 0, 10))

  8. [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

fib就是一个普通的python函数,它特需的地方在于函数体中没有 return关键字,函数的返回值是一个生成器对象。当执行 f=fib()返回的是一个生成器对象,此时函数体中的代码并不会执行,只有显示或隐示地调用next的时候才会真正执行里面的代码。

生成器在Python中是一个非常强大的编程结构,可以用更少地中间变量写流式代码,此外,相比其它容器对象它更能节省内存和CPU,当然它可以用更少的代码来实现相似的功能。现在就可以动手重构你的代码了,但凡看到类似:

  1. def something():

  2.    result = []

  3.    for ... in ...:

  4.        result.append(x)

  5.    return result

都可以用生成器函数来替换:

  1. def iter_something():

  2.    for ... in ...:

  3.        yield x

生成器表达式(generator expression)

生成器表达式是列表推倒式的生成器版本,看起来像列表推导式,但是它返回的是一个生成器对象而不是列表对象。

  1. >>> a = (x*x for x in range(10))

  2. >>> a

  3. <generator object <genexpr> at 0x401f08>

  4. >>> sum(a)

  5. 285

Python是一门非常独特的编程语言,它不仅简单易学而且非常强大,有过编程经验的程序员第一次接触Python会大呼”居然可以这样玩”,从此路转粉,“The Zen of Python”被Pythoneer视为编程教条,究竟什么样的代码才称得上地道呢?我们怎样才能写出地道的Python代码?

点击阅读原文或扫码咨询


今天看啥 - 高品质阅读平台
本文地址:http://www.jintiankansha.me/t/ubXg7NzEiS
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/2936