大家好,又见面了,我是你们的朋友全栈君。
初学者在日常提升Python基本功的时候,可能会被Python的迭代器和生成器搞晕,之前在学习和使用时,本来for in 循环体和enumerate函数用的飞起,觉得自己已经彻底了解了Python的迭代特性,但接触了迭代器和生成器后,突然感觉懵逼,大概率会被可迭代、迭代器、生成器等概念搞的不知所向,本文就是结合日常项目应用,对Python的迭代概念进行系统性的全面解析,包括其底层实现原理,还有一些常见的应用,希望能帮助更多人,同时也算作给自己梳理思路。
一、迭代概述
1.1 基础概念
迭代属性是Python一大特性,也才允许我们通过for in 循环体遍历比如列表、字典等集合类型数据类型内的数据,或者用in成员函数判断某元素是否在某数据内存在、使用列表解析式等,让代码变得简洁明晰,如果想深入理解Python这一大特性,其实还需要深入了解迭代器和生成器的概念。
以下先整体介绍可迭代、迭代器、生成器的概念和相互之间的关系
- 可迭代:指实现了Python迭代协议,可以通过for in 循环体遍历的对象,比如list、dict等内置数据类型、迭代器、生成器
- 迭代器:指可以记住自己遍历位置的对象,直观体现便是可以使用next()函数返回值,迭代器只能往前,不能往后,当遍历完毕后,next(iteror)会抛出一个StopIteration异常
- 生成器:指使用yield的函数,生成器也是只能往前,不能往后,当遍历完毕后,next(iteror)会抛出一个StopIteration异常
- 三个概念的包含关系:可迭代>迭代器>生成器
- 迭代器和生成器,均可以通过next(obj)的方式不断返回下一个值
- 可迭代的对象(包括生成器),均可以通过iter(obj),转化为迭代器
1.2 判断对象是否可迭代方法
python也提供了判断是否可迭代的方法,即isinstance,代码如下
代码语言:javascript复制from collections import Iterable
from collections import Iterator
list1=[1,2,3]
print(isinstance(list1,Iterable)) #返回True
print(isinstance(list1,Iterator)) #返回False
1.3 迭代器和生成器的比较
- 迭代器是个类,且需要实现__iter__和__next__魔法函数,语法相对来说较为冗余
- 生成器是个使用yield的函数,相较而言,代码会更加少
- 在同一代码内,生成器只能遍历一次
1.4 for in循环运作原理以及其与可迭代关系
1.4.1 for in 循环运作原理
代码语言:javascript复制#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
- 如以上代码所示,如果只是实现__getitem__,for in 循环体会自动调用__getitem__函数,并自动对Index从0开始自增,并将对应索引位置的值赋值给 i,直到引发IndexError错误
- 如果还实现了__iter__,则会忽略__getitem__,只调用__iter__,并对__iter__返回的迭代器进行成员遍历,并自动将遍历的成员逐次赋值给 i,直到引发StopIteration
1.4.2 其与可迭代关系
- 可迭代的对象一定可以支持for in 循环体,以及其他迭代环境,比如in成员判断、列表解析、map和reduce函数等
- 支持for in 循环体及迭代环境的,不一定可迭代,如1.4.1中所示,实现了__getitem__的对象
1.5 python迭代环境及对应实现介绍
在Python中,迭代环境到处可见,主要有:
- for in 循环
- in成员判断运算 ( x in y)
- 列表推导式[x for x in range(10]
- map和reduce函数(map(func,a))
- 列表及元组赋值语句(比如a,b=[1,2])
- 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 一致 |
- 如果实现了__iter__,则可以支持Python所有迭代环境
- 如果实现了__getitem__,也可以支持Python所有迭代环境,但是优先级和灵活度没有__iter__高,并且此时该类的对象,并不是可迭代的
- __contains__魔法函数,只对in 生效,所以如果要单独定义专有的in 运算,则最好只是实现__contains__即可
- 如果想支持next(a)函数调用,则必须实现__next__魔法函数
- 预估后续python会对迭代这块进行优化,因为现在其实整体感觉蛮混乱,如果强制可迭代,必须通过__iter__ __next__实现,反而会更加明晰些
- 建议读者:
- 实现迭代协议:就优先重载实现__iter__和__next__
- 支持in成员运算符:就优先重载实现__contains__(self,value),且只用该魔法函数支持in运算,不去支持迭代协议
- 支持索引和切片运算:就优先重载实现__getitem__(self,index),且只用该魔法函数支持索引和切片,不去支持迭代协议
二、可迭代对象
下面展开讲解如何创建一个可迭代对象及其实现原理
2.1 可迭代对象创建方式
下面演示如何创建一个可迭代对象,核心点:
- 关键是在定义类的时候,需要实现__iter__魔法函数,该函数返回一个迭代器即可
- 实现了__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 可迭代对象原理讲解
- 上面的代码,只是实现了__iter__魔法函数,但是已经可以在for in 循环体内进行遍历
- 此时,因为没有实现__next__模范函数,所以只是可迭代对象,但并不是迭代器
- 比如list数据类型,是可迭代对象,但并不是迭代器,可以观察list数据类型魔法函数,使用dir(list),其输出中有__iter__魔法函数,但并没有__next__魔法函数
三、迭代器
如一中所属,一个迭代器就是可以通过next()不断返回下一个值的对象,其本质是一个实现了支持iter()和next()方法的对象,所以,如果想创建一个迭代器,则需要定义一个类,并在该类中实现__iter__和__next__魔法函数
3.1 迭代器创建方法
下面定义一个简单的迭代器,主要是必须实现__iter__和__next__魔法函数,创建时需要注意以下问题
- __iter__必须返回一个迭代器
- __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()函数调用时,返回下一个值的
- 在使用for in 循环体,比如 for i in it遍历it时,其实调用的是__iter__魔法函数,即for i in it.__iter__()
- 在使用next(it)时,其实调用的是__next__魔法函数,即next( it.__next__())
- 一般如果定义并实现了__next__,则__iter__直接return self即可,因为此时self就是一个迭代器
- 至于如何实现每次运行next返回下一个推导值,是通过实例变量不断记录每次运行推导返回值实现的,下次运行,便可基于上次返回值及推导算法,返回下一个推导值
3.3 内置迭代器
Python的itertools库里面包含了一些生成迭代器的方法,可以生成无限迭代器、有限迭代器以及组合迭代器,具体功能不再展开,先知道,后续有用到了再进行详细了解即可,因为本身用法也比较简单。
3.4 多重迭代器
以上演示的基本都是单重迭代器,即只支持一层for in 循环遍历,因为同一个迭代器只会迭代一次,如果有多层for in 遍历,则只会迭代一层,并且多层遍历其实共用的是同一个迭代器,而内置的str、list等类型,则可以支持多重迭代,如下:
代码语言:javascript复制mylist=[1,2]
for i in string:
for j in string:
print(i,j)
#输出为
1,1
1,2
2,1
2,2
如果按照以上代码,定义自己的迭代器,则因为每次循环,都是循环的同一个迭代器,并不会产生与内置数据类型的效果
代码语言:javascript复制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
- 以上代码,因为双层for in 循环体,遍历的是同一个m迭代器,所以只会在内层迭代到2之后,便不再迭代
- 所以,如果需要支持多重迭代,且不同层的迭代,相互不受影响,需要想办法每个层的迭代都是新的迭代器,我们知道每次for in的时候,均会调用__iter__函数返回一个迭代器,所以需要在该函数内,不再返回自身,而应该返回一个新的迭代器,即创建一个迭代器对象
按照以上思路,将代码改成如下:
代码语言:javascript复制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
- 以上代码,每层for循环,均会调用__iter__函数,返回一个新的迭代器实例对象,这样多重迭代,均有独立的迭代器,就会和内置数据类型的表现基本一致
- 当然,以上代码相对比较冗余,其实可以直接在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 使用函数创建
核心点如下:
- 函数内部需要实现一个循环体,并实现返回值推导算法,并由yield返回每次推导出来的值
- yield关键词,核心作用是
- 类似return,将指定值或多个值返回给调用方
- 记录此次返回或遍历的位置,返回数值之后,挂起,知道下一次执行next函数,再重新从挂起点接着运行(类似断点的作用)
def generator():
a=0
b=1
while True:
c=a b
yield c
a,b=b,a b
g=generator()
4.1.2 使用生成器推导式
核心点如下:
- 整体规律,类似类表生成推导式
- 只是语法,由之前的[],变为()
#使用推导式,对小于10的,乘3,对于大于等于10的,乘5
#此时返回的不再是列表,而是一个生成器
g=(i*3 if i<10 else i*5 for i in range(100)
4.2 yield详解及与return对比
- 相同点:
- 均在函数体内使用,并且向调用方返回结果
- 均可返回一个值或多个值,如果是多个值,则是以元组格式返回
- 不同点:
- 包含yield的函数,调用时最终返回的是一个生成器,单纯的return函数,调用时返回的是一个值。
- return 执行并返回值后,便会直接退出函数体,该函数内存空间即回收释放
- yield执行并返回值后,不会退出函数体,而是挂起,待下次next时,再从挂起点恢复运行
- yield语句可以接受通过生成器send方法传入的参数并赋值给一个变量,以动态调整生成器的行为表现
- yield语句的返回值,可以通过from 关键词指定 返回源
- return在生成器中的作用:
- 在一个生成器函数中,如果没有 return,则默认执行至函数完毕,如果在执行过程中 return,则直接抛出 StopIteration 终止迭代
下面通过代码进行演示:
代码语言:javascript复制#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 可控文件读取
使用生成器的挂起并可重新在挂起点运行的特点,我么可以实现按需,每次读取指定大小的文件,避免因为读取文件时,因为一次性读取内容过多,导致内存溢出等问题
代码语言:javascript复制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 协程
在讲解协程应用之前,先展开讲解下进程、线程、协程概念:
- 进程指单独的一个CUP运行程序,可以简单认为一个进程就是一个独立的程序
- 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位
- 协程可以认为是在同一个线程内运行的代码
- 进程包含线程,线程包含协程
- 进程、线程的切换和调度,一般由操作系统自动完成,具体调度和切换机制较为复杂
- 同一线程下,多个协程的切换是由自己编写的代码进行控制,可以实现个性化的调度和切换需求
协程主要有以下特点:
- 协程是非抢占式特点:协程也存在着切换,这种切换是由我们用户来控制的。协程主解决的是IO的操作
- 协程有极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显
- 协程无需关心多线程锁机制,也无需关心数据共享问题:不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多
协程借助生成器实现的基本思路:
- 因为生成器通过yield,可以挂起,待下次执行时再次从挂起点恢复运行,满足切换和交替运行的特点
- 因为生成器可以通过send函数,动态的干预指定生成器的功能和表现,为实现多个协程之间协作提供了可能
下面代码简单举例用生成器实现协程的机制,后续可以根据自己实际需要,进行具体的实现
代码语言:javascript复制#让两个函数交替运行
#核心就是把两个正常的函数使用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) |
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/188249.html原文链接:https://javaforall.cn