可迭代对象、迭代器对象和生成器对象
可迭代对象iterable
字面意思:
- 可迭代:更新迭代。迭代是一个重复的过程,每次重复是基于上一次的结果而继续的,每次都有新的内容。
- 可迭代对象:可以进行循环更新的一个实实在在的值。
专业角度:
内部含有'__iter__'方法的对象。
str.__iter__
list.__iter__
set.__iter__
dict.__iter__
tuple.__iter__
优点:
1、存储的数据能直接显示,比较直观。
2、拥有的方法比较多,操作方便。
缺点:
1、占内存,有多少值就会占用多少内存。
2、只能通过索引,key取值。
for循环能进行取值是因为在底层做了转换,将可迭代对象转换成迭代器,然后取值。
迭代器对象iterator
字面角度:
迭代器指的是迭代取值的工具(数据结构)。
专业角度:
内置有__next__方法和__iter__方法的对象。
f = open('a.txt',mode'rt',encoding='utf-8')
print('__iter__' in dir(f),'__next__' in dir(f))
f.close()
True True
文件对象就是迭代器,这是为了避免当一个文件过大时,占用过多内存,所以Python做的优化。
优点:
1、提供一种不依赖索引的通用迭代取值方案。
2、惰性计算,节省内存。每次在内存中仅存在一个值。
缺点:
- 1、速度慢,以时间换空间。
- 2、取值麻烦。
先将可迭代对象调用__iter__方法或iter()函数转换成迭代器,然后调用__next__方法或next()函数取值。
li = [11,22,33]
li_obj = li.__iter__()
print(li_obj.__next__())
print(li_obj.__next__())
print(li_obj.__next__())
print(li_obj.__next__())
每调用一次__next__就会取一次值,当值都取完后,再调用__next__会抛StopIteration异常。
StopIteration
11
22
33
- 3、没有__len__方法,无法预测值的长度,只能取到抛出异常才知道已经取完。
s1 = {11,22,33}
obj = iter(s1)
print('__len__' in dir(obj))
False
- 4、一次性取值。
li = [11,22,33]
li_obj = iter(li)
for i in li_obj:
print(i)
print('====分隔符====')
for i in li_obj:
print(i)
在第一个for循环取完值后,第二次for循环已经取不出来值了。所以结果为:
11
22
33
====分隔符====
除非重新调用iter()转换成迭代器,就可以重新取值。
li_obj2 = iter(li)
for i in li_obj:
print(i)
11
22
33
====分隔符====
11
22
33
使用while循环模拟for循环对迭代器取值
1、调用可迭代对象.__iter__方法,得到一个迭代器对象。
2、迭代器调用next(),获取一个值。
3、循环取值,直到抛出StopIteration异常,捕捉异常然后结束循环。
s1 = {11,22,33}
obj = iter(s1)
while 1:
try:
print(next(obj))
except StopIteration: # 当捕捉到StopIteration异常时就break结束循环。
break
将可迭代对象转换为迭代器就可以实现不依赖索引或key也能取值的目的。
可迭代对象转换为迭代器对象
可迭代对象调用__iter__方法或iter()函数会转换成迭代器。每次转换得到的都是一个新的迭代器。
li = [11,22,33]
li_obj = iter(li)
li_obj2 = iter(li)
print(li_obj is li_obj2) # 两次都得到一个新的迭代器。
False
两个迭代器都可以取值。
for i in li_obj:
print(i)
for i in li_obj2:
print(i)
11
22
33
11
22
33
如果是迭代器对象调用__iter__方法或iter()函数,所得到的结果为迭代器对象本身。这么设计是为了方便for循环的工作机制,使迭代器对象无论调用__iter__方法多少次,得到的结果都是迭代器对象自身。
li = [11,22,33]
li_obj = iter(li)
li_obj2 = li_obj.__iter__()
li_obj3 = li_obj2.__iter__().__iter__()
print(li_obj is li_obj2 is li_obj3)
# 无论调用多少次,结果还是迭代器自身。
True
for 循环工作原理
1、调用可迭代对象.__iter__方法,得到一个迭代器对象。
2、调用next(迭代器),将返回值赋值给in前面的变量。
3、循环取值,直到抛出StopIteration异常,for就会捕捉异常然后结束循环。
s1 = {11,22,33}
for i in s1:
print(i)
33
11
22
由于迭代器对象无论调用__iter__方法多少次,得到的结果都是迭代器对象自身,所以for循环无论迭代的是可迭代对象还是一个迭代器对象,都可以直接调用该对象的__iter__方法,简化了for循环的设计思路。
内置函数dir()
获取一个对象的所有内置方法,以列表的形式返回。
l = [1,2,3]
print(dir(l))
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
判断一个对象是否是可迭代对象。
s = 'abc'
print('__iter__' in dir(s))
True
生成器generator
生成器的本质就是迭代器。
区别:
生成器是我们自己用Python代码构建的数据结构。迭代器是Python提供的,或者调用iter()转换得来的。
def f():
yield 10
print('__iter__' in dir(f()) and '__next__' in dir(f()))
True
获取生成器的两种方式:
1、yield构建生成器函数。
2、生成器表达式。
1、yield构建生成器函数。
函数体内但凡出现yield关键字,调用函数将不会触发函数体代码的运行,而是会返回一个生成器对象。
def func():
print(111)
yield 1
print(222)
yield 3
ret = func()
print(ret)
print(next(ret))
<generator object func at 0x0000000002338B30>
111
1
特点:
-
只要函数中有yield那么就是生成器函数。
-
生成器函数加括号()并不会直接执行函数体代码,而是返回一个生成器对象。
def func(): print(111) yield 'aaa' obj = func() print(obj) # 并不会打印111 <generator object func at 0x00000000025487B0>
-
每调用一次生成器函数都会获得一个新的生成器。
def func(): print(111) yield 'aaa' obj = func() obj2 = func() # 会得到一个新的生成器. print(obj is obj2) False
-
函数中可以存在多个yield,并且yield不会终止函数。
-
通过next取出yield返回的值,并会保留上次取值的位置。一个next对应一个yield,多了会报错。
也就是说,yield可以暂停函数,然后我们可以拿到返回值进行处理后,用next方法再次触发函数代码的运行,协程方面有应用。
return与yield的区别:
相同点:在返回值反面用法一样。
不同点:yield可以返回多次值,而return只能返回一次。
用生成器函数手撸个range功能。
def my_range(start,stop,step=1):
while start < stop:
yield start
start += step
for i in my_range(0,10):
print(i)
0
2
4
6
8
用生成器函数手撸个无限取值的功能。
def get_num():
num = 0
while 1:
yield num
num += 1
obj = get_num()
print(next(obj))
print(next(obj))
print(next(obj))
结果为:
0
1
2
每调用一次生成器函数都会获得一个新的生成器,如果用下述写法,每次得到的是个新生成器,永远是取0:
def get_num():
num = 0
while 1:
yield num
num += 1
print(next(get_num()))
print(next(get_num()))
0
0
2、生成器表达式。
首先讲列表推导式:能用一行代码构建一个比较复杂有规律的列表。
格式:
1、循环模式:
l = [表达式 for 变量 in iterable]
# 多层嵌套
l = [表达式 for 变量 in iterable for 变量 in iterable ...]
2、筛选模式:
l = [表达式 for 变量 in iterable if 条件 for 变量 in iterable]
超过三层循环才能构建成功的,不建议使用列表推导式。查找错误(debug模式)不行。
# 常规创建
li = []
for i in range(1,10):
if i > 3:
li.append(i)
# [4, 5, 6, 7, 8, 9]
# 列表推导式:
li = [i for i in range(1,10) if i > 3]
print(li)
# [4, 5, 6, 7, 8, 9]
生成器表达式与列表推导式的写法几乎一模一样。将列表推导式的方括号改为圆括号。也有筛选模式,循环模式,多层嵌套。
实例:
l = (i for i in range(10))
print(next(l))
print(next(l))
print(next(l))
0
1
2
字典推导式:
l1 = ['jay','jj','meet']
l2 = ['周杰伦','林俊杰','元宝']
dic = {l1[i]:l2[i] for i in range(len(l1))}
print(dic)
# {'jay': '周杰伦', 'jj': '林俊杰', 'meet': '元宝'}
items = [('a',1),('b',2),('c',3)]
dic = {key:value for key,value in items if value > 1 }
print(dic)
# {'b': 2, 'c': 3}
集合推导式:
s1 = {i for i in range(1,4)}
print(s1)
{1, 2, 3}
并没有元组推导式,因为有列表推导式就可以使用tuple()将列表转换成元组。
li = tuple([i for i in range(1,10) if i > 3])
print(li)
(4, 5, 6, 7, 8, 9)
表达式应用:
计算一个文件内有多少个字符。
with open('user.txt',mode='r',encoding='utf-8') as f:
# 常规方法,读取每一行计算字符数,然后再累加
d = 0
for line in f:
d += len(line)
print(d)
# 可使用列表推导式简化,但文件行数若过多,会造成列表元素过多,占用大量内存空间
d = sum([len(line) for line in f])
print(d)
# 使用生成器表达式,每次只会占用一块内存空间
d = sum((len(line) for line in f))
print(d)
# 函数的括号和生成器表达式的括号可以合并。
d = sum(len(line) for line in f)
print(d)