初学者在日常提升Python基本功的时候,可能会被Python的迭代器和生成器搞晕,之前在学习和使用时,本来for in 循环体和enumerate函数用的飞起,觉得自己已经彻底了解了Python的迭代特性,但接触了迭代器和生成器后,突然感觉懵逼,大概率会被可迭代、迭代器、生成器等概念搞的不知所向,本文就是结合日常项目应用,对Python的迭代概念进行系统性的全面解析,包括其底层实现原理,还有一些常见的应用,希望能帮助更多人,同时也算作给自己梳理思路。
迭代属性是Python一大特性,也才允许我们通过for in 循环体遍历比如列表、字典等集合类型数据类型内的数据,或者用in成员函数判断某元素是否在某数据内存在、使用列表解析式等,让代码变得简洁明晰,如果想深入理解Python这一大特性,其实还需要深入了解迭代器和生成器的概念。
以下先整体介绍可迭代、迭代器、生成器的概念和相互之间的关系
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、只实现__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
在Python中,迭代环境到处可见,主要有:
以上迭代环境,都依赖于迭代协议,对应调用的魔法函数也会有不同,以下罗列下不同的迭代环境,对应的魔法函数,后续自定义类时,如果需要这个类实例对象支持相应的迭代环境,则需要实现对应的魔法函数
迭代环境 | 支持该迭代环境的实现方式 |
---|---|
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 一致 |
下面展开讲解如何创建一个可迭代对象及其实现原理
下面演示如何创建一个可迭代对象,核心点:
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)
如一中所属,一个迭代器就是可以通过next()不断返回下一个值的对象,其本质是一个实现了支持iter()和next()方法的对象,所以,如果想创建一个迭代器,则需要定义一个类,并在该类中实现__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()
下面说下,迭代器是如何支持for in 循环体遍历,又是如何在使用next()函数调用时,返回下一个值的
Python的itertools库里面包含了一些生成迭代器的方法,可以生成无限迭代器、有限迭代器以及组合迭代器,具体功能不再展开,先知道,后续有用到了再进行详细了解即可,因为本身用法也比较简单。
以上演示的基本都是单重迭代器,即只支持一层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
按照以上思路,将代码改成如下:
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
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千倍的内存占用空间
核心点如下:
def generator():
a=0
b=1
while True:
c=a+b
yield c
a,b=b,a+b
g=generator()
核心点如下:
#使用推导式,对小于10的,乘3,对于大于等于10的,乘5
#此时返回的不再是列表,而是一个生成器
g=(i*3 if i<10 else i*5 for i in range(100)
下面通过代码进行演示:
#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)
使用生成器的挂起并可重新在挂起点运行的特点,我么可以实现按需,每次读取指定大小的文件,避免因为读取文件时,因为一次性读取内容过多,导致内存溢出等问题
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
在讲解协程应用之前,先展开讲解下进程、线程、协程概念:
协程主要有以下特点:
协程借助生成器实现的基本思路:
下面代码简单举例用生成器实现协程的机制,后续可以根据自己实际需要,进行具体的实现
#让两个函数交替运行
#核心就是把两个正常的函数使用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)
其实迭代在Python中应用非常广泛,比如sum、max、min等函数,只要传入一个可迭代的对象,就可以进行工作,这极大的提高了代码的可读性和编程的简洁性。
大家在日常使用Python时,也可以观察或者思考,在需要迭代遍历对象时,是否在使用或者可使用迭代来完成
函数 | 说明 | 示例 |
---|---|---|
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) |