热门标签 | 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/


推荐阅读
  • 本文节选自《NLTK基础教程——用NLTK和Python库构建机器学习应用》一书的第1章第1.2节,作者Nitin Hardeniya。本文将带领读者快速了解Python的基础知识,为后续的机器学习应用打下坚实的基础。 ... [详细]
  • 浅析python实现布隆过滤器及Redis中的缓存穿透原理_python
    本文带你了解了位图的实现,布隆过滤器的原理及Python中的使用,以及布隆过滤器如何应对Redis中的缓存穿透,相信你对布隆过滤 ... [详细]
  • 普通树(每个节点可以有任意数量的子节点)级序遍历 ... [详细]
  • 目录预备知识导包构建数据集神经网络结构训练测试精度可视化计算模型精度损失可视化输出网络结构信息训练神经网络定义参数载入数据载入神经网络结构、损失及优化训练及测试损失、精度可视化qu ... [详细]
  • 更新vuex的数据为什么用mutation?
    更新vuex的数据为什么用mutation?,Go语言社区,Golang程序员人脉社 ... [详细]
  • 本文详细介绍了Java反射机制的基本概念、获取Class对象的方法、反射的主要功能及其在实际开发中的应用。通过具体示例,帮助读者更好地理解和使用Java反射。 ... [详细]
  • Spring Boot 中配置全局文件上传路径并实现文件上传功能
    本文介绍如何在 Spring Boot 项目中配置全局文件上传路径,并通过读取配置项实现文件上传功能。通过这种方式,可以更好地管理和维护文件路径。 ... [详细]
  • 本文介绍了在 Java 编程中遇到的一个常见错误:对象无法转换为 long 类型,并提供了详细的解决方案。 ... [详细]
  • 如何在Java中使用DButils类
    这期内容当中小编将会给大家带来有关如何在Java中使用DButils类,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。D ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 检查在所有可能的“?”替换中,给定的二进制字符串中是否出现子字符串“10”带 1 或 0 ... [详细]
  • 本文详细解析了一种实用的函数,用于从URL中提取查询参数。该函数通过处理URL中的搜索部分,能够高效地获取并解析出所需的参数值,适用于各种Web开发场景。 ... [详细]
  • 使用Maven JAR插件将单个或多个文件及其依赖项合并为一个可引用的JAR包
    本文介绍了如何利用Maven中的maven-assembly-plugin插件将单个或多个Java文件及其依赖项打包成一个可引用的JAR文件。首先,需要创建一个新的Maven项目,并将待打包的Java文件复制到该项目中。通过配置maven-assembly-plugin,可以实现将所有文件及其依赖项合并为一个独立的JAR包,方便在其他项目中引用和使用。此外,该方法还支持自定义装配描述符,以满足不同场景下的需求。 ... [详细]
  • 在Android应用开发中,实现与MySQL数据库的连接是一项重要的技术任务。本文详细介绍了Android连接MySQL数据库的操作流程和技术要点。首先,Android平台提供了SQLiteOpenHelper类作为数据库辅助工具,用于创建或打开数据库。开发者可以通过继承并扩展该类,实现对数据库的初始化和版本管理。此外,文章还探讨了使用第三方库如Retrofit或Volley进行网络请求,以及如何通过JSON格式交换数据,确保与MySQL服务器的高效通信。 ... [详细]
  • 本文探讨了利用Python实现高效语音识别技术的方法。通过使用先进的语音处理库和算法,本文详细介绍了如何构建一个准确且高效的语音识别系统。提供的代码示例和实验结果展示了该方法在实际应用中的优越性能。相关文件可从以下链接下载:链接:https://pan.baidu.com/s/1RWNVHuXMQleOrEi5vig_bQ,提取码:p57s。 ... [详细]
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社区 版权所有