python 可迭代对象 迭代器 生成器_Python3迭代器获取

python 可迭代对象 迭代器 生成器_Python3迭代器获取在日常提升Python基本功的时候,可能会被Python的迭代器和生成器搞晕,之前在学习和使用时,本来forin循环体和enumerate函数用的飞起,觉得自己已经彻底了解了Python的迭代特性,但接触了迭代器和生成器后,突然感觉懵逼,大概率会被可迭代、迭代器、生成器等概念搞的不知所向,本文就是结合日常项目应用,对Python的迭代概念进行系统性的全面解析,包括其底层实现原理,还有一些常见的应用,希望能帮助更多人,同时也算作给自己梳理思路了。一、基本概念二、迭代器三、生成器四、基本应用

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE稳定放心使用

初学者在日常提升Python基本功的时候,可能会被Python的迭代器和生成器搞晕,之前在学习和使用时,本来for in 循环体和enumerate函数用的飞起,觉得自己已经彻底了解了Python的迭代特性,但接触了迭代器和生成器后,突然感觉懵逼,大概率会被可迭代、迭代器、生成器等概念搞的不知所向,本文就是结合日常项目应用,对Python的迭代概念进行系统性的全面解析,包括其底层实现原理,还有一些常见的应用,希望能帮助更多人,同时也算作给自己梳理思路。

一、迭代概述

1.1 基础概念

迭代属性是Python一大特性,也才允许我们通过for  in 循环体遍历比如列表、字典等集合类型数据类型内的数据,或者用in成员函数判断某元素是否在某数据内存在、使用列表解析式等,让代码变得简洁明晰,如果想深入理解Python这一大特性,其实还需要深入了解迭代器和生成器的概念。

以下先整体介绍可迭代、迭代器、生成器的概念和相互之间的关系

  1. 可迭代:指实现了Python迭代协议,可以通过for in 循环体遍历的对象,比如list、dict等内置数据类型、迭代器、生成器
  2. 迭代器:指可以记住自己遍历位置的对象,直观体现便是可以使用next()函数返回值,迭代器只能往前,不能往后,当遍历完毕后,next(iteror)会抛出一个StopIteration异常
  3. 生成器:指使用yield的函数,生成器也是只能往前,不能往后,当遍历完毕后,next(iteror)会抛出一个StopIteration异常
  4. 三个概念的包含关系:可迭代>迭代器>生成器
  5. 迭代器和生成器,均可以通过next(obj)的方式不断返回下一个值
  6. 可迭代的对象(包括生成器),均可以通过iter(obj),转化为迭代器

1.2 判断对象是否可迭代方法

python也提供了判断是否可迭代的方法,即isinstance,代码如下

from collections import Iterable
from collections import Iterator
list1=[1,2,3]
print(isinstance(list1,Iterable)) #返回True
print(isinstance(list1,Iterator)) #返回False

1.3 迭代器和生成器的比较

  1. 迭代器是个类,且需要实现__iter__和__next__魔法函数,语法相对来说较为冗余
  2. 生成器是个使用yield的函数,相较而言,代码会更加少
  3. 在同一代码内,生成器只能遍历一次

1.4 for in循环运作原理以及其与可迭代关系

1.4.1 for in 循环运作原理

#1、只实现__getitem__
class A:
    def __init__(self):
        self.data=[1,2,3]

    def __getitem__(self,index):
        return self.data[index]
a=A()
for i in a:
    print(i)
#输出为 1、2、3

#2、实现__getitem__和__iter__
class A:
    def __init__(self):
        self.data=[1,2,3]
        self.data1=[4,5,6]
    
    def __iter__(self):
        return iter(self.data1)    

    def __getitem__(self,index):
        return self.data[index]

a=A()
for i in a:
    print(i)
#输出为 4、5、6
  1. 如以上代码所示,如果只是实现__getitem__,for in 循环体会自动调用__getitem__函数,并自动对Index从0开始自增,并将对应索引位置的值赋值给 i,直到引发IndexError错误
  2. 如果还实现了__iter__,则会忽略__getitem__,只调用__iter__,并对__iter__返回的迭代器进行成员遍历,并自动将遍历的成员逐次赋值给 i,直到引发StopIteration

1.4.2 其与可迭代关系

  1. 可迭代的对象一定可以支持for in 循环体,以及其他迭代环境,比如in成员判断、列表解析、map和reduce函数等
  2. 支持for in 循环体及迭代环境的,不一定可迭代,如1.4.1中所示,实现了__getitem__的对象

1.5 python迭代环境及对应实现介绍

在Python中,迭代环境到处可见,主要有:

  1. for in 循环
  2. in成员判断运算 ( x in y)
  3. 列表推导式[x for x in range(10]
  4. map和reduce函数(map(func,a))
  5. 列表及元组赋值语句(比如a,b=[1,2])
  6. next()

以上迭代环境,都依赖于迭代协议,对应调用的魔法函数也会有不同,以下罗列下不同的迭代环境,对应的魔法函数,后续自定义类时,如果需要这个类实例对象支持相应的迭代环境,则需要实现对应的魔法函数

迭代环境 支持该迭代环境的实现方式
 for in 循环

1、可只是实现__iter__魔法函数,该魔法函数返回一个迭代器对象

2、可只是实现__getitem__(self,index)魔法函数,该魔法函数每次循环均会对index从0自增

3、如果两个都实现了,则会调用__iter__

in 成员判断

1、可只是实现__contains__(self,value)魔法函数,in 运算符,会自动将该函数返回值转化为对应布尔值

2、可只是实现__iter__魔法函数

3、可只是实现__getitem__(self,index)魔法函数

4、一般判断先使用__contains__,再用__iter__,再用__getitem__,为标准起见,最好用__contains__单独支持 in 成员判断

列表推导式

与for in 一致

map和reduce函数 与for in 一致
列表及元组赋值语句

与for in 一致

  1. 如果实现了__iter__,则可以支持Python所有迭代环境
  2. 如果实现了__getitem__,也可以支持Python所有迭代环境,但是优先级和灵活度没有__iter__高,并且此时该类的对象,并不是可迭代的
  3. __contains__魔法函数,只对in 生效,所以如果要单独定义专有的in 运算,则最好只是实现__contains__即可
  4. 如果想支持next(a)函数调用,则必须实现__next__魔法函数
  5. 预估后续python会对迭代这块进行优化,因为现在其实整体感觉蛮混乱,如果强制可迭代,必须通过__iter__+__next__实现,反而会更加明晰些
  6. 建议读者:
    1. 实现迭代协议:就优先重载实现__iter__和__next__
    2. 支持in成员运算符:就优先重载实现__contains__(self,value),且只用该魔法函数支持in运算,不去支持迭代协议
    3. 支持索引和切片运算:就优先重载实现__getitem__(self,index),且只用该魔法函数支持索引和切片,不去支持迭代协议

二、可迭代对象

下面展开讲解如何创建一个可迭代对象及其实现原理

2.1 可迭代对象创建方式

下面演示如何创建一个可迭代对象,核心点:

  1. 关键是在定义类的时候,需要实现__iter__魔法函数,该函数返回一个迭代器即可
  2. 实现了__iter__,也即实现了Python的迭代协议
class Myiter:
    def __init__(self):
        self.a=1
    def __iter__(self):
        a=[1,2,3,4]
        return iter(a)
#此种实现的方式,不是一个迭代器,但是可迭代,即可通过for in 循环体进行遍历
it=Myiter()
for i in it:
    print(it)

2.2 可迭代对象原理讲解

  1. 上面的代码,只是实现了__iter__魔法函数,但是已经可以在for in 循环体内进行遍历
  2. 此时,因为没有实现__next__模范函数,所以只是可迭代对象,但并不是迭代器
  3. 比如list数据类型,是可迭代对象,但并不是迭代器,可以观察list数据类型魔法函数,使用dir(list),其输出中有__iter__魔法函数,但并没有__next__魔法函数

三、迭代器

如一中所属,一个迭代器就是可以通过next()不断返回下一个值的对象,其本质是一个实现了支持iter()和next()方法的对象,所以,如果想创建一个迭代器,则需要定义一个类,并在该类中实现__iter__和__next__魔法函数

3.1 迭代器创建方法

下面定义一个简单的迭代器,主要是必须实现__iter__和__next__魔法函数,创建时需要注意以下问题

  1. __iter__必须返回一个迭代器
  2. __next__实现数值推演算法
class Myiter:
    #一般在初始时,传入或者初始化一些实例变量值,便于在__next__中使用
    def __init__(self):
        self.a=1
    #该函数必须返回一个迭代器
    def __iter__(self):
        return self
    #该值返回每次next调用,需要返回的下一个值,其实也就是实现数值推演算法
    def __next__(self):
        self.a+=1
        return self.a

#下面实例化一个迭代器
it=Myiter()

3.2 迭代器原理讲解

下面说下,迭代器是如何支持for in 循环体遍历,又是如何在使用next()函数调用时,返回下一个值的

  1. 在使用for in 循环体,比如 for i in it遍历it时,其实调用的是__iter__魔法函数,即for i in it.__iter__()
  2. 在使用next(it)时,其实调用的是__next__魔法函数,即next( it.__next__())
  3. 一般如果定义并实现了__next__,则__iter__直接return self即可,因为此时self就是一个迭代器
  4. 至于如何实现每次运行next返回下一个推导值,是通过实例变量不断记录每次运行推导返回值实现的,下次运行,便可基于上次返回值及推导算法,返回下一个推导值

3.3 内置迭代器

Python的itertools库里面包含了一些生成迭代器的方法,可以生成无限迭代器、有限迭代器以及组合迭代器,具体功能不再展开,先知道,后续有用到了再进行详细了解即可,因为本身用法也比较简单。

3.4 多重迭代器

以上演示的基本都是单重迭代器,即只支持一层for in 循环遍历,因为同一个迭代器只会迭代一次,如果有多层for in 遍历,则只会迭代一层,并且多层遍历其实共用的是同一个迭代器,而内置的str、list等类型,则可以支持多重迭代,如下:

mylist=[1,2]
for i in string:
    for j in string:
        print(i,j)
#输出为
1,1
1,2
2,1
2,2

如果按照以上代码,定义自己的迭代器,则因为每次循环,都是循环的同一个迭代器,并不会产生与内置数据类型的效果

class myit:
    def __init__(self):
        self.index=0
        self.data=[1,2]
    def __iter__(self):
        return self
    def __next__(self):
        self.index+=1
        if self.index>len(self.data):
            raise StopIteration
        return self.data[self.index-1]
m=myit()
for i in m:
    for j in m:
        print(i,j)
#输出
1,2
  1. 以上代码,因为双层for in 循环体,遍历的是同一个m迭代器,所以只会在内层迭代到2之后,便不再迭代
  2. 所以,如果需要支持多重迭代,且不同层的迭代,相互不受影响,需要想办法每个层的迭代都是新的迭代器,我们知道每次for in的时候,均会调用__iter__函数返回一个迭代器,所以需要在该函数内,不再返回自身,而应该返回一个新的迭代器,即创建一个迭代器对象

按照以上思路,将代码改成如下:

class A:
    def __init__(self):
        self.index=0
        self.data=[1,2]

    def __iter__(self):
        return self

    def __next__(self):
        self.index+=1
        if self.index>len(self.data):
            raise StopIteration
        return self.data[self.index-1]

class B:
    def __iter__(self):
        return A()
b=B()

for i in b:
    for j in b:
        print(i,j)
#输出
1,1
1,2
2,1
2,2
  1. 以上代码,每层for循环,均会调用__iter__函数,返回一个新的迭代器实例对象,这样多重迭代,均有独立的迭代器,就会和内置数据类型的表现基本一致
  2. 当然,以上代码相对比较冗余,其实可以直接在A类中的__iter__函数内,不要返回self,而是创建一个新的实例对象即可
class A:
    def __init__(self):
        self.index=0
        self.data=[1,2]
    def __iter__(self):
        return A()
    def __next__(self):
        self.index+=1
        if self.index>len(self.data):
            raise StopIteration
        return self.data[self.index-1]

四、生成器

生成器本质是一个使用了yield返回值的函数,支持使用next()函数不断返回下一个值,同时支持使用send函数向生成器发送消息

生成的这个特性,为解决 无限个变量和有限内存之间矛盾的问题,提供了解决方案,或者为优化内存使用效率提供了途径

因为比如一个包含1万个变量的列表,和一个包含推导算法的生成器,其内存占用空间,可能前者是后者的几个数量级倍数,比如下面的

a=[i for i in range(10000)] #运行sys.getsizeof(a)后,为87616

a=(i for i in range(10000))#运行sys.getsizeof(a)后,为112,直接减少了8千倍的内存占用空间

4.1 生成器创建方法

4.1.1 使用函数创建

核心点如下:

  1. 函数内部需要实现一个循环体,并实现返回值推导算法,并由yield返回每次推导出来的值
  2. yield关键词,核心作用是
    1. 类似return,将指定值或多个值返回给调用方
    2. 记录此次返回或遍历的位置,返回数值之后,挂起,知道下一次执行next函数,再重新从挂起点接着运行(类似断点的作用)
def generator():
    a=0
    b=1
    while True:
        c=a+b
        yield c
        a,b=b,a+b
g=generator()

4.1.2 使用生成器推导式

核心点如下:

  1. 整体规律,类似类表生成推导式
  2. 只是语法,由之前的[],变为()
#使用推导式,对小于10的,乘3,对于大于等于10的,乘5
#此时返回的不再是列表,而是一个生成器
g=(i*3 if i<10 else i*5 for i in range(100)

4.2 yield详解及与return对比

  1. 相同点:
    1. 均在函数体内使用,并且向调用方返回结果
    2. 均可返回一个值或多个值,如果是多个值,则是以元组格式返回
  2. 不同点:
    1. 包含yield的函数,调用时最终返回的是一个生成器,单纯的return函数,调用时返回的是一个值。
    2. return 执行并返回值后,便会直接退出函数体,该函数内存空间即回收释放
    3. yield执行并返回值后,不会退出函数体,而是挂起,待下次next时,再从挂起点恢复运行
    4. yield语句可以接受通过生成器send方法传入的参数并赋值给一个变量,以动态调整生成器的行为表现
    5. yield语句的返回值,可以通过from 关键词指定 返回源
  3. return在生成器中的作用:
    1. 在一个生成器函数中,如果没有 return,则默认执行至函数完毕,如果在执行过程中 return,则直接抛出 StopIteration 终止迭代

下面通过代码进行演示:

#1、yield和return共存
def gene(maxcount):
    a,b=0,1
    count=1
    while True:
        if count>maxcount:
            #直接退出函数体
            return 
        else:
            #返回值后,函数在该处挂起,下次再从该处恢复运行
            yield a+b
            a,b=b,a+b
            count+=1

#2、yield接受通过send传入的参数并动态调整生成器行为
#
def gene(maxcount):
    a,b=0,1
    count=1
    while True:
        if count>maxcount:
            return 
        else:
            msg=yield a+b
            if msg=='stop':
                return
            a,b=b,a+b
            count+=1
g=gene(10)
next(g)
g.send('msg') #生成器终止,并抛出一个StopIteration异常

#3、通过from关键词,接受另外一个生成器,并通过该生成器返回值
#此处只是做展示,大家知道即可,后续如果有类似场景,可以想起来可以这么搞就行
gene1=(i for i in range(10))
def gene2(gene):
    yield from gene
g=gene2(gene1)

五、基本应用举例

5.1 可控文件读取

使用生成器的挂起并可重新在挂起点运行的特点,我么可以实现按需,每次读取指定大小的文件,避免因为读取文件时,因为一次性读取内容过多,导致内存溢出等问题

def read_file(fpath): 
    BLOCK_SIZE = 1024 
    with open(fpath, 'rb') as f: 
        while True: 
            block = f.read(BLOCK_SIZE) 
            if block: 
                yield block 
            else: 
                return

5.2 协程

在讲解协程应用之前,先展开讲解下进程、线程、协程概念:

  1. 进程指单独的一个CUP运行程序,可以简单认为一个进程就是一个独立的程序
  2. 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
  3. 协程可以认为是在同一个线程内运行的代码
  4. 进程包含线程,线程包含协程
  5. 进程、线程的切换和调度,一般由操作系统自动完成,具体调度和切换机制较为复杂
  6. 同一线程下,多个协程的切换是由自己编写的代码进行控制,可以实现个性化的调度和切换需求

协程主要有以下特点:

  1. 协程是非抢占式特点:协程也存在着切换,这种切换是由我们用户来控制的。协程主解决的是IO的操作
  2. 协程有极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显
  3. 协程无需关心多线程锁机制,也无需关心数据共享问题:不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多

协程借助生成器实现的基本思路:

  1. 因为生成器通过yield,可以挂起,待下次执行时再次从挂起点恢复运行,满足切换和交替运行的特点
  2. 因为生成器可以通过send函数,动态的干预指定生成器的功能和表现,为实现多个协程之间协作提供了可能

下面代码简单举例用生成器实现协程的机制,后续可以根据自己实际需要,进行具体的实现

#让两个函数交替运行
#核心就是把两个正常的函数使用yield变为生成器函数,然后交替使用其next调用即可
def task1(times):
	for i in range(times):
		print('task1 done the :{} time'.format(i+1))
		yield
def task2(times):
	for i in range(times):
		print('task2 done the :{} time'.format(i+1))
		yield	
gene1=task1(5)
gene2=task2(5)
for i in range(100):
	next(gene1)
	next(gene2)

5.3 迭代在其他地方的应用表现

其实迭代在Python中应用非常广泛,比如sum、max、min等函数,只要传入一个可迭代的对象,就可以进行工作,这极大的提高了代码的可读性和编程的简洁性。

大家在日常使用Python时,也可以观察或者思考,在需要迭代遍历对象时,是否在使用或者可使用迭代来完成

5.4 常用内置迭代工具

函数 说明 示例
zip(seq1,seq2,seq3,…)

1、将多个序列按位打包成元组,最后返回一个由这些元组组成的序列

2、其返回的结果,本质是一个迭代器,可以尽量减少对内存的占用

seq1=[1,2,3];seq2=[4,5,6];seq3=[7,8,9]

zip(seq1,seq2,seq3)

map(func,seq)

1、对seq序列遍历,并对其每个元素传入func函数

2、其返回的结果,本质是一个迭代器,可以尽量减少对内存的占用

def func(a):

      return a+1

seq=[1,2,3]

map(func,seq)

filter(func,seq)

1、对seq序列遍历,并对齐每个元素传入func函数,最后只返回为真的值

2、其返回的结果,本质是一个迭代器,可以尽量减少对内存的占用

def func(a):

      return a+1

seq=[1,2,3]

filter(func,seq)

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/188249.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号