热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

一文说清Python可迭代对象,迭代器,生成器的关系

Python的初学者可能会对以下概念感到困惑:容器可迭代对象迭代器生成器生成器表达式这篇文章将有助于加深对上述概念的理解,并梳理它们之间的异同之处。

Python的初学者可能会对以下概念感到困惑:


  • 容器
  • 可迭代对象
  • 迭代器
  • 生成器
  • 生成器表达式

这篇文章将有助于加深对上述概念的理解,并梳理它们之间的异同之处。

在这里插入图片描述


容器

容器是一种数据结构,它可收纳元素,并支持成员关系判断。它们是存储在内存中的数据结构,通常在内存中维持着元素。在Python中,它们包括:


  • list , deque,…
  • set , fronzensets,…
  • dict , defaultdict, OrderedDict, Counter,…
  • str

此处的容器,跟日常生活中容器的概念是相似的,比如一个盒子,一间房子,一艘船等。

技术上而言,当询问一个对象是否包含某个元素时,该对象就是容器。因此,可将诸如成员关系判断的方法,应用于容器中。

比如:list、sets、tuples:

>>> assert 1 in [1, 2, 3] # lists
>>> assert 4 not in [1, 2, 3]
>>> assert 1 in {1, 2, 3} # sets
>>> assert 4 not in {1, 2, 3}
>>> assert 1 in (1, 2, 3) # tuples
>>> assert 4 not in (1, 2, 3)

而dict的成员关系判断,则是判断key:

>>> d = {1: 'foo', 2: 'bar', 3: 'qux'}
>>> assert 1 in d
>>> assert 4 not in d
>>> assert 'foo' not in d # 'foo' is not a _key_ in the dict

如果判断dict的value,则可使用:

>>> assert 'foo' in d.values()

而字符串str,也是包含着元素,支持成员关系判断。因此它也属于容器:

>>> s = 'foobar'
>>> assert 'b' in s
>>> assert 'x' not in s
>>> assert 'foo' in s # a string "contains" all its substrings

实际上,当使用成员关系判断时,是调用了该对象的类方法__contains__,如果没有实现此方法,则调用对象的类方法__iter__。也就是说,大多数容器对象,都提供了这两种方法或其一。

尽管大多数容器提供了一种迭代出每个元素的能力,但并非只有容器才具有这种能力。准确而言,具有这种能力(迭代出元素)的对象,称为可迭代对象,也就是说,实现了__iter__方法的对象,为可迭代对象。

通常而言,大多数容器是可迭代对象

可通过下面例子加深理解:


  • 是容器,支持成员关系判断,但不是可迭代对象:

    class Foo:def __init__(self,item):self.item = itemdef __contains__(self, *args):print("membership testing...")return args[0] in self.itemf = Foo([1,2,3,4])
    print(1 in f) # 成员关系判断,调用了__containers__方法
    print(100 in f) # 同上
    ---for ele in f: # 迭代,调用了__iter__方法。因为没有定义,因此是不可迭代对象print(ele)

    输出:

    membership testing...
    True
    membership testing...
    False
    --
    Traceback (most recent call last):File "E:/PyProject/test02.py", line 14, in for ele in f:
    TypeError: 'Foo' object is not iterable

  • 是容器,支持成员关系判断,是可迭代对象:

    class Foo:def __init__(self,item):self.item = itemself.iter = iter(item)def __contains__(self, *args):print("membership testing...")return args[0] in self.itemdef __iter__(self):print('Ready to iter...')return self.iterf = Foo([1,2,3,4])for ele in f: # 是可迭代对象,因此可迭代print(ele)

    输出:

    Ready to iter...
    1
    2
    3
    4

容器中不是可迭代对象的类,比如Bloom filter。虽然Bloom filter可以用来检测某个元素是否包含在容器中,但是并不能从容器中获取其中的每一个值,也就是无法迭代。

因为Bloom filter压根就没把元素存储在容器中,而是通过一个散列函数映射成一个值保存在数组中。


可迭代对象

如上文所述,大多数容器都是可迭代对象。但不仅于此,如打开的文件,打开数sockets也是可迭代对象。

通常情况下,容器所包含的数据是有限的。而可迭代对象却可以是无穷的数据源

一个可迭代对象可以是任何一种对象,并不仅限于是一种数据结构。只要提供了__iter__方法的对象,都是可迭代对象

一般来说,能够返回一个迭代器的对象,都是可迭代对象。因为没有人定义了一个迭代器,但却无法迭代。

迭代器的目标,就是返回它所包含的全部元素

比如下面的例子:

>>> x = [1, 2, 3] # 可迭代对象
>>> y = iter(x)
>>> z = iter(x) # 返回了迭代器对象
>>> next(y)
1
>>> next(y)
2
>>> next(z)
1
>>> type(x)

>>> type(y)

此处,x是可迭代对象,而y和z是两个独立的迭代器实例,可以从可迭代对象x中生产出元素。可以使用内建函数next(iterator,[default])来判断一个对象是否是迭代器。

y和z会维持一种状态。该状态用于记录当前迭代所在的位置,以方便下次迭代的时候获取正确的元素。

总结:

如果一个可迭代对象的类支持__iter__()__next__()方法,并且__iter__()返回self,这会使得类既是一个可迭代对象,也是自己的迭代器。不过,最好返回一个与迭代器不同的对象。

也就是说,迭代时,会调用__iter__方法,返回一个新的迭代器对象。当然也可以自定义返回的对象。

最后,迭代元素时:

x = [1, 2, 3]
for elem in x:...

实际上的过程是:

在这里插入图片描述

当你反编译这段Python代码时,你会看到解释器显式地调用GET_ITER,这本质上就是调用iter(x)FOR_ITER指令会调用等效于next()去重复获取每个元素,但这并没有在字节码指令中显示出来,因为这被解释器优化过了。

>>> import dis
>>> x = [1, 2, 3]
>>> dis.dis('for _ in x: pass')1 0 SETUP_LOOP 14 (to 17)3 LOAD_NAME 0 (x)6 GET_ITER>> 7 FOR_ITER 6 (to 16)10 STORE_NAME 1 (_)13 JUMP_ABSOLUTE 7>> 16 POP_BLOCK>> 17 LOAD_CONST 0 (None)20 RETURN_VALUE

迭代器

那什么是迭代器呢?它是一个对象,当你调用next()时会有效地迭代出(生产出)一个值

任何实现了__next__()方法的对象,都是一个迭代器。因此,迭代器是一个生产值的工厂。每次你询问下一个值时,它将会知道如何计算此值,因为它维持了内部的状态。

有很多关于迭代器的例子,所有itertools函数都返回迭代器,比如生成无限的序列:

>>> from itertools import count
>>> counter = count(start=13)
>>> next(counter)
13
>>> next(counter)
14

比如从有限序列中循环返回序列:

>>> from itertools import cycle
>>> colors = cycle(['red', 'white', 'blue'])
>>> next(colors)
'red'
>>> next(colors)
'white'
>>> next(colors)
'blue'
>>> next(colors)
'red'

为了更直观地理解迭代器的内部执行过程,我们定义了一个产生斐波那契数列的迭代器

>>> class fib:
... def __init__(self):
... self.prev = 0
... self.curr = 1
...
... def __iter__(self):
... return self
...
... def __next__(self):
... value = self.curr
... self.curr += self.prev
... self.prev = value
... return value
...
>>> f = fib()
>>> list(islice(f, 0, 10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

需要注意的是,这个类既是可迭代对象,也是迭代器。因为它实现了__iter__()__next__()方法。

此迭代器内部的状态由precurr的实例变量来维持,并用于调用迭代器时产生的下一个序列。每次调用next(),会做两件事:


  1. 为下一次next()调用而修改状态
  2. 生产出当前调用的结果

从类的外部来看,迭代器相当于一个懒惰的工厂。它只有在需要值的时候才生产出值。当生产出一个值后,将停止生产直到你下一次调用它。这就是懒加载。


生成器

生成器可以算得上是Python中最吸引人的语言特性之一,它实际上是一种特殊的迭代器,但更加优雅。

一个生成器允许你编写一个类似于斐波那契数列的迭代器,但它简洁优雅的语法允许你无需提供__iter__()__next__()方法。

可以理解为,使用yield关键字的函数,就是生成器函数

因此可以概括为:


  • 生成器也是迭代器。
  • 生成器是一个懒加载的工厂函数。

下面的代码使用生成器实现了斐波那契数列的工厂函数:

>>> def fib():
... prev, curr = 0, 1
... while True:
... yield curr
... prev, curr = curr, prev + curr
...
>>> f = fib()
>>> list(islice(f, 0, 10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

代码说明:


  1. fib是一个普通的Python函数,但它没有包含return关键字。
  2. fib的函数返回值是一个生成器(迭代器,工厂函数)
  3. 当f = fib() 被调用,生成器被实例化并返回。此时并没有执行任何代码,也就是说prev,curr = 0, 1并没有执行。
  4. 生成器实例被islice()包装,这也是一个迭代器,因此最初时是空闲状态,没有任何事情发生。
  5. 然后,迭代器被list()包装,它是一个消费者,消费它所包含的参数并从中构建成 一个列表的形式。此时,将在islice()实例中调用next()方法,并在f中调用next()方法。
  6. 此时,代码开始真正执行,进入到循环中,直到遇到yield,产生一个值后,将再次进入空闲状态。
  7. 产生的值传递给islice(),并生产出来。list增加值1到列表中。
  8. 然后循环往复,直到输出列表的长度为10个元素
  9. 求第11个值时,islice()将引发StopIteration异常,表明已到达末尾,并且list将返回结果:list 10个项。其中包含前10个斐波那契数。 请注意,生成器没有收到第11个next()调用。 实际上,它不会再次使用,以后会被垃圾回收。

生成器是程序结构中非常有效的工具,它允许你使用很少的中间变量和数据结构来编写流式代码。并且,它们在CPU和内存的表现中更为高效,代码更为简洁。

但凡看到下面的代码,都可用生成器重构:

def something():result = []for ... in ...:result.append(x)return result

生成器:

def iter_something():for ... in ...:yield x# def something(): # Only if you really need a list structure
# return list(iter_something())

生成器表达式

在Python中的生成器有两类:生成器函数和生成器表达式。

生成器函数是拥有yield关键字的函数。

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

比如列表解析式,集合解析式和字典解析式:

>>> numbers = [1, 2, 3, 4, 5, 6]
>>> [x * x for x in numbers]
[1, 4, 9, 16, 25, 36]>>> {x * x for x in numbers}
{1, 4, 36, 9, 16, 25}>>> {x: x * x for x in numbers}
{1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36}

生成器表达式:

>>> a = (x*x for x in range(10))
>>> a
at 0x401f08>
>>> sum(a) ## 已经消费完a里面的值
285
>>> a = (x*x for x in range(10))
>>> next(a) ## 开始消费
0
>>> list(a)
[1,2,3,4,5,6,7,8,9]

总结:

在实际使用过程汇总,如果需要延迟计算,懒加载的场景,使用生成器更加节省内存资源,而且CPU利用效率更高。

参考:https://nvie.com/posts/iterators-vs-generators/


推荐阅读
  • Python自动化处理:从Word文档提取内容并生成带水印的PDF
    本文介绍如何利用Python实现从特定网站下载Word文档,去除水印并添加自定义水印,最终将文档转换为PDF格式。该方法适用于批量处理和自动化需求。 ... [详细]
  • 1.如何在运行状态查看源代码?查看函数的源代码,我们通常会使用IDE来完成。比如在PyCharm中,你可以Ctrl+鼠标点击进入函数的源代码。那如果没有IDE呢?当我们想使用一个函 ... [详细]
  • XNA 3.0 游戏编程:从 XML 文件加载数据
    本文介绍如何在 XNA 3.0 游戏项目中从 XML 文件加载数据。我们将探讨如何将 XML 数据序列化为二进制文件,并通过内容管道加载到游戏中。此外,还会涉及自定义类型读取器和写入器的实现。 ... [详细]
  • 本文详细解析了Python中的os和sys模块,介绍了它们的功能、常用方法及其在实际编程中的应用。 ... [详细]
  • 掌握远程执行Linux脚本和命令的技巧
    本文将详细介绍如何利用Python的Paramiko库实现远程执行Linux脚本和命令,帮助读者快速掌握这一实用技能。通过具体的示例和详尽的解释,让初学者也能轻松上手。 ... [详细]
  • 根据最新发布的《互联网人才趋势报告》,尽管大量IT从业者已转向Python开发,但随着人工智能和大数据领域的迅猛发展,仍存在巨大的人才缺口。本文将详细介绍如何使用Python编写一个简单的爬虫程序,并提供完整的代码示例。 ... [详细]
  • Python 异步编程:深入理解 asyncio 库(上)
    本文介绍了 Python 3.4 版本引入的标准库 asyncio,该库为异步 IO 提供了强大的支持。我们将探讨为什么需要 asyncio,以及它如何简化并发编程的复杂性,并详细介绍其核心概念和使用方法。 ... [详细]
  • 深入解析Android自定义View面试题
    本文探讨了Android Launcher开发中自定义View的重要性,并通过一道经典的面试题,帮助开发者更好地理解自定义View的实现细节。文章不仅涵盖了基础知识,还提供了实际操作建议。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 本文详细介绍如何使用Python进行配置文件的读写操作,涵盖常见的配置文件格式(如INI、JSON、TOML和YAML),并提供具体的代码示例。 ... [详细]
  • 本文介绍了Java并发库中的阻塞队列(BlockingQueue)及其典型应用场景。通过具体实例,展示了如何利用LinkedBlockingQueue实现线程间高效、安全的数据传递,并结合线程池和原子类优化性能。 ... [详细]
  • 本文深入探讨 MyBatis 中动态 SQL 的使用方法,包括 if/where、trim 自定义字符串截取规则、choose 分支选择、封装查询和修改条件的 where/set 标签、批量处理的 foreach 标签以及内置参数和 bind 的用法。 ... [详细]
  • CMake跨平台开发实践
    本文介绍如何使用CMake支持不同平台的代码编译。通过一个简单的示例,我们将展示如何编写CMakeLists.txt以适应Linux和Windows平台,并实现跨平台的函数调用。 ... [详细]
  • UNP 第9章:主机名与地址转换
    本章探讨了用于在主机名和数值地址之间进行转换的函数,如gethostbyname和gethostbyaddr。此外,还介绍了getservbyname和getservbyport函数,用于在服务器名和端口号之间进行转换。 ... [详细]
  • 2023年京东Android面试真题解析与经验分享
    本文由一位拥有6年Android开发经验的工程师撰写,详细解析了京东面试中常见的技术问题。涵盖引用传递、Handler机制、ListView优化、多线程控制及ANR处理等核心知识点。 ... [详细]
author-avatar
金婉jessica氵_573
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有