可迭代对象、迭代器对象和生成器
什么是迭代?
每一次过程的重复称为一次迭代,而每一次迭代的结果作为下一代初始值,单纯的重复不是迭代。
# 迭代取值
my_list = [1, 2, 3, 4, 5, 6]
index = 0
while index < len(my_list):
print(my_list[index])
index += 1
上述while循环就是一个迭代过程,满足重复,每次打印之后将新的值赋值给index作为下一次循环的索引,知道达到结束条件后取完所有的值。
一 可迭代对象
通过索引的方式进行迭代取值,实现简单,但是仅适用于有序的数据类型,像dict和set等非有序数据类型,必须找到一种不依赖索引取值的方法,所以就有了迭代器。
了解迭代器之前,先要来了解一个概念,什么是可迭代对象(iterable),有内置方法__iter__方法的就是可迭代对象,字符串、列表、字典、元组、集合、打开的文件都是可迭代对象(文件本身也是一个迭代器对象)。
# 可迭代对象
'jasper'.__iter__()
[1, 2].__iter__()
{'name': 'jasper'}.__iter__()
(1,).__iter__()
{1, }.__iter__()
二 迭代器对象
可迭代对象调用__iter__方法返回的就是一个迭代器对象(iterator),迭代器对象内置方法有__iter__和__next__,迭代器对象调用iter方法得到的还是迭代器本身,而调用next方法返回的迭代器中的一个值。迭代器是python提供的一种不依赖索引取值的方式,无论是有序数据类型还是无序数据类型都可以进行取值。
# 迭代器对象
name = 'jasper' # 可迭代对象iterable
res = name.__iter__() # 迭代器对象iterator
print(res) # <str_iterator object at 0x000001E943A230D0>
print(res.__next__()) # j
print(res.__next__()) # a
print(res.__next__()) # s
print(res.__next__()) # p
print(res.__next__()) # e
print(res.__next__()) # r
print(res.__next__()) # StopIteration 抛出异常 代表无值可取 迭代停止
name1 = 'jack'
print(name1.__iter__().__next__()) # j 每次产生都是不一样的iterator object
print(name1.__iter__().__next__()) # j
print(name1.__iter__().__next__()) # j
print(name1.__iter__().__next__()) # j
三 迭代器优缺点
优点:
- 为有序和无序的数据类型提供了统一的取值方式
- 迭代器对象是一个数据流,可以只在需要时才去调用next方法来计算出一个值,对于迭代器本身来说,迭代器只占用内存中一块内存,存放的是迭代器的内存地址,那么这个迭代器就可以存放很大的数据流而不占用多的空间,相对于其他容器类型,如果有很多个数据值,需要把每个数据放到内存中,可能会造成内存的溢出。
缺点:
- 只能全部取完值才可以获取迭代器的长度
- 不能取下一个值,如果要再次迭代同一个对象,必须创建一个新的迭代器对象,如果两个循环使用了同一个迭代器,那么只有先运行的循环可以取到值。
# 两个循环中使用一个迭代器 只有一个可以取到值
l = [1, 2, 3, 4, 5]
res = l.__iter__()
count = 0
while count < len(l):
print(res.__next__())
count += 1
count1 = 0
while count1 < len(l):
print(res.__next__()) # StopIteration
count += 1
三 for循环原理
for i in range(5):
print(i)
# 原理
res = range(5).__iter__() # range(5)有__iter__方法 说明是一个iterable object
# 调用iter方法后变为iterator object
print(res.__next__()) # 0
print(res.__next__()) # 1
print(res.__next__()) # 2
print(res.__next__()) # 3
print(res.__next__()) # 4
print(res.__next__()) # StopIteration 迭代停止
for循环的语法是for 变量名 in 可迭代对象:
for循环的原理就是将iterable object调用__iter__方法,转换成iterator object,在调用iterator的__next__方法,将返回值赋值给变量名,完成一次迭代,周而复始,直至捕捉到StopIteration异常,迭代停止。
四 异常处理
4.1 什么是异常
程序运行时报错就是异常,程序一旦出现异常,如果没有处理,程序的运行就会终止。
异常的类型:NameError、TypeError、IndexError、SyntaxError、KeyError等
异常的分类:
- 语法错误
这种错误是不应该犯的,应在程序运行前就改正。 - 逻辑错误
出现错误时修改即可
4.2 异常操作
为了保证程序的可靠性和容错性,即在程序遇到错误时有相应的处理机制让程序不崩溃掉,我们需要对程序进行异常处理。
4.2.1 语法
# 语法
"""
try:
被检测代码块
except 异常类型 as e:
检测到异常执行的代码
"""
4.2.2 实际操作
利用异常处理用while循环实现for循环功能
# while循环实现for循环功能
my_list = [1, 2, 3, 4, 5]
res = my_list.__iter__()
while True:
try:
print(res.__next__())
except StopIteration: # 当捕获到StopIteration异常时结束循环
break
本来程序一旦异常,程序就会立刻结束,有了异常处理之后,在捕获到异常类型时,就会执行except的内部代码,其余代码正常运行。
4.2.3 异常操作补充
1.当一个程序需要捕捉不同类型的异常时,需要使用多个except,类似于if分支,在执行一个except分支后,就不会执行另外的分支。
try:
被检测代码块
except 异常类型1 as e:
检测到异常1执行的代码
except 异常类型2 as e:
检测到异常2执行的代码
except 异常类型3 as e:
检测到异常3执行的代码
except 异常类型4 as e:
检测到异常4执行的代码
2.当我们想让不同类型的异常采用一种逻辑代码处理 可以将异常类型写在一个元组中
try:
被检测代码块
except (TypeError, IndexError, NameErroe...) as e:
检测到异常执行的代码
3.当我们想捕获所有的异常,就可以采用万能异常类型(Exception)
my_list = [1,2,3,4]
try:
my_list[5]
except NameError:
print('name error')
except Exception:
print('Error') # Error
4.在多分支的except后还可以跟一个else(必须跟在except后且不能单独存在),再有在else前所有的except都没执行才会执行else
try:
name = 'jasper'
except IndexError:
print('NameError')
else:
print('except不执行才会执行')
5.try还可以和finally连用,无论被检测的代码是否触发异常,都会执行finally的子代码。
try:
name
except NameError:
print('NameError')
finally:
print('except执不执行都会执行')
6.如果想主动抛出异常,就需要使用到raise关键字
name = input('>>>:').strip()
if name == 'jasper':
raise Exception('Error')
else:
print('...')
7.断言语句assert
assert isinstance(1, str)
表达式如果不成立,就会抛出AssertionError
总结:异常捕获能少用就少用 被try检测的代码越少越好
五 生成器对象
5.1 生成器对象使用
某函数体代码包含yield关键字,调用函数,不会执行函数,而是返回一个生成器对象(generator),生成器有__iter__和__next__方法,所以生成器对象本质就是一个迭代器对象。
自定义一个range方法
# 自定义range方法
def my_range(start, end=None, step=1):
if step < 0:
if start < end:
return
else:
while start > end:
yield start
start += step
else:
if not end:
end = start
start = 0
while start < end:
yield start
start += step
res = my_range(5)
print(res) # <generator object my_range at 0x000001C3D4D0C890>
所以可以使用next方法使函数运行
print(res.__next__()) # 0
print(res.__next__()) # 1
print(res.__next__()) # 2
print(res.__next__()) # 3
print(res.__next__()) # 4
print(res.__next__()) # StopIteration
生成器是迭代器,那么肯定是可迭代对象,就可以使用for循环
for i in my_range(5):
print(i)
# 输出 0\n1\n2\n3\n4\n
5.2 yield其他用法
def eat():
print('打算吃什么')
while True:
food = yield
print(f'打算吃{food}')
res = eat()
print(res) # <generator object eat at 0x00000217922CC890>
res.__next__() # 打算吃什么 让函数停在yield
res.send('烤鸡') # 打算吃烤鸡
res.send('烤鸭') # 打算吃烤鸭
函数内有yield关键字,调用一次函数,转换成生成器对象,调用__next__方法让函数停在food = yield位置,等待调用send方法为其传值,并自动调用__next__方法。
5.3 生成器表达式
语法:(表达式 for 变量名 in 可迭代对象)
返回值就是一个生成器对象
res = (i**2 for i in range(5) if i != 2)
print(res) # <generator object <genexpr> at 0x000002209C626970>
print(list(res)) # [0, 1, 9, 16]
六 索引取值与迭代取值差异
- 索引取值
优点:可以重复随意获取任意一个值
缺点:只能针对有序容器类型,无序的取不了 - 迭代取值
优点:提供了统一的取值方式,省空间
缺点:不能倒着取,取完在想取需生成一个新的迭代器