Python基础:08.迭代器与生成器

一、迭代

通过for循环来遍历list、tuple或char,称为迭代

1
2
3
4
5
6
7
8
9
list1 = [1,2,3,4,5,6,7,8,9]
tuple1 = (1,2,3,4,5,6)
char1 = 'python'
for i in list1[0:2]:
print(i)
for i in tuple1[0:2]:
print(i)
for i in char1[0:2]:
print(i)

1
2
1
2
p
y

dict默认是以key进行迭代。如果要迭代value,可以用for value in dict.values()。同时迭代key和value,可以用for k, v in d.items()

1
2
3
4
5
6
7
8
9
dict1 = {1:'python',
2:'java',
3:'c++'}
for key in dict1: # 不一定非要用key这个变量,一种规范
print(key)
for value in dict1.values(): # 注意values和()易写漏
print(value)
for k,v in dict1.items():
print(k,v)

1
2
3
python
java
c++
1 python
2 java
3 c++

二、可迭代对象(Iterable)

能用for循环进行迭代的对象就是可迭代对象,判断一个对象是可迭代对象的方法:通过collections.abc模块的Iterable类型判断

可迭代的对象:str,list,tuple,dict,set,文件对象

1
2
3
from collections.abc import Iterable
print(isinstance('abc', Iterable)) # str是否可迭代
print(isinstance(123, Iterable)) # 整数是否可迭代

True
False

如果要给list加下标,可以用Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身

1
2
for i, value in enumerate(['A', 'B', 'C']):
print(i, value)

0 A
1 B
2 C

三、列表生成式

学完了可迭代对象,就可以学习一个很强大的工具,列表生成式,他是Python内置的非常简单却强大的可以用来创建list的生成式,即在可迭代对象中将满足条件的元素生成为一个列表,条件可以省略

其结构为:[x表达式 for x in 可迭代对象 if 判断语句] ,它与下面的for循环是等价的

1
2
3
4
result = []
for val in collection:
if condition:
result.append(expr)

使用:

1
[x * x for x in range(1,11)]

[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

1. 表达式用法

列表生成式中的表达式有多种用法

(1)可以调用方法

比如知道可迭代对象L中只有字符串的话,就可以使用字符串的方法

1
2
L = ['Hello', 'World', 'IBM', 'Apple']  
[x.lower() for x in L if len(x)>3] # 字符串变成小写,且筛选长度大于3的字符串

[‘hello’, ‘world’, ‘apple’]

1
2
3
# 练习
L = ['Hello', 'World', 18, 'Apple', None] # 把列表中字符串提取出来生成新列表,并小写
[x.lower() for x in L if isinstance(x, str) == 1] # isinstance函数可以判断一个变量是不是对应的类型

[‘hello’, ‘world’, ‘apple’]

(2)使用三元表达式

x表达式可以使用三元表达式进一步确定x的值,首先回顾下python里的三元表达式

1
条件为真时的结果 if 判段的条件 else 条件为假时的结果

务必注意:因为涉及到if-else,要小心 else 语句放在 for 语句前后位置的区别,for语句前面必须是一个表达式,它必须根据元素计算出一个结果,可迭代对象后面才是一个筛选条件。因此需要将完整的三元表达式放在for的前面,for后面可以有if,但不能有else,列表生成式的语法不支持for后面有else

正确示范:

1
2
3

# x if x % 2 == 0 else 0 能够根据x算出精确的结果
[x if x % 2 == 0 else 0 for x in range(1, 11)]

[0, 2, 0, 4, 0, 6, 0, 8, 0, 10]

1
2
# 如果只是想筛选2的整数倍,可以把if条件放在后面,不需要三元表达式
[x for x in range(1, 11) if x % 2 == 0 ]

[2, 4, 6, 8, 10]

错误示范:

1
2
# 只筛选偶数,筛选条件中不能放else,否则不能达到筛选的效果
[x for x in range(1, 11) if x % 2 == 0 else 0]

SyntaxError: invalid syntax

1
2
# 前面的这个表达式必须要能根据x算出结果,但此时少else,表达模糊,计算不出结果,并不是正确的三元表达式
[x if x % 2 == 0 for x in range(1, 11)]

SyntaxError: invalid syntax

2. 可迭代对象用法

列表生成式的for循环可以使用两个甚至多个变量

1
2
d = {'x': 'A', 'y': 'B', 'z': 'C' }
[a + '=' + b for a,b in d.items()]

[‘x=A’, ‘y=B’, ‘z=C’]

3. 嵌套列表生成式

当在表达式中出现两个对象,或者是嵌套的列表等情况就可以使用两层循环,嵌套列表生成式执行的顺序一定是和写成for循环的顺序相同,一般也就是先执行第一个for循环。三层及以上的用的不多,可读性较差

1
[m * n for m in range(1,5) for n in range(2,4)]

[2, 3, 4, 6, 6, 9, 8, 12]

1
[m + n for m in 'ABC' for n in 'XY']  # 先A 再XY  先B 再XY 先C 再XY

[‘AX’, ‘AY’, ‘BX’, ‘BY’, ‘CX’, ‘CY’]

1
2
3
4
5
6
7
list1 = [(1,2,3),(4,5,6),(7,8,9)]  # 想将次嵌套式的列表扁平化为一个一维的列表
# 如果采用普通for循环
list_demo = []
for x in list1:
for y in x:
list_demo.append(y)
list_demo

[1, 2, 3, 4, 5, 6, 7, 8, 9]

1
2
3
4
list1 = [(1,2,3),(4,5,6),(7,8,9)]  # 想将次嵌套式的列表扁平化为一个一维的列表
# 如果采用嵌套列表生成式,更简洁
list_demo = [y for x in list1 for y in x] # 谨记嵌套列表生成式执行的顺序一定是和写成for循环的顺序相同
list_demo

[1, 2, 3, 4, 5, 6, 7, 8, 9]

还有一个情况是列表生成式中的列表生成式,他与嵌套列表生成式是不同的,它是先执行后面的for语句,即把后面的for循环放在最外层循环
他是在列表中生成列表,注意区分

1
2
3
list1 = [(1,2,3),(4,5,6),(7,8,9)]
list_demo1 = [[y for y in x ]for x in list1]
print(list_demo1)

[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

1
2
3
4
5
6
matrix = [           
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
]
[[row[i] for row in matrix] for i in range(4)] # 将3X4的矩阵列表转换为4X3列表

[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

1
list(zip(*matrix)) # 也可以通过zip函数解包

[(1, 5, 9), (2, 6, 10), (3, 7, 11), (4, 8, 12)]

1
2
3
4
transposed = []
for i in range(4):
transposed.append([row[i] for row in matrix])
transposed

[[1, 5, 9], [2, 6, 10], [3, 7, 11], [4, 8, 12]]

4. 集合生成式

把列表生成式的[]换为{}就成为集合生成式

1
2
3
strings = ['a','as','bat','car','dove','python']
unique_strings = {len(x) for x in strings} # 将列表中字符串的长度转为集合
unique_strings

{1, 2, 3, 4, 6}

1
2
# 也可以用map函数到达同样的效果
set(map(len,strings))

{1, 2, 3, 4, 6}

5. 字典生成式

除了把列表生成式的[]换为{},在最开始的表达式中需要写成键值对的形式,这样就成为了字典生成式

1
2
3
strings = ['a','as','bat','car','dove','python']
dict_strings = {key:value for key,value in enumerate(strings)} # 使用enumerate函数创造索引值
dict_strings

{0: ‘a’, 1: ‘as’, 2: ‘bat’, 3: ‘car’, 4: ‘dove’, 5: ‘python’}

使用小括号包裹生成式会变成生成器对象,而不是元组生成式

四、迭代器(Iterator)

迭代器指的是迭代取值的工具,每次调用迭代器会返回自身的下一个元素,它表示的是一个数据流,可以看做是一个有序序列,但不能提前知道序列长度,只有在需要返回下一个数据时它才会计算。所以Iterator的计算是惰性的,和列表等集合数据类型不同

迭代器必须有两个基本的方法:

  • iter() ,这是可迭代对象的内置函数,将可迭代对象转变为迭代器
  • next() 可以输出迭代器的下一个元素

从类角度讲,任何实现了__iter__()__next__()方法的对象都是迭代器

为了防止出现无限循环的情况,StopIteration 异常用于标识迭代的完成,没有数据时抛出StopIteration异常,这并不是错误,只是提醒迭代完成

1
2
3
4
5
6
list1 = [1,2,3]
iter1 = iter(list1) # 将列表转为迭代器
print(next(iter1)) # 输出迭代器下一个元素
print(next(iter1))
print(next(iter1))
print(next(iter1))

1
2
3

StopIteration Traceback (most recent call last)
in ()
4 print(next(iter1))
5 print(next(iter1))
—-> 6 print(next(iter1))

迭代器一定是可迭代对象,但可迭代对象不一定是迭代器,如list、dict、str等是Iterable但不是Iterator,需要经过iter()函数转换 ,常用的列表生成式就是迭代器

可以用collections.abc模块的Iterator类型判断一个对象是否是Iterator对象

1
2
3
4
5
6
7
8
from collections.abc import Iterable,Iterator
list1 = [1,2,3]
iter1 = iter(list1) # 将列表转为迭代器
print(isinstance(list1, Iterable))
print(isinstance(list1, Iterator)) # 列表不是迭代器
print(isinstance(iter1, Iterable))
print(isinstance(iter1, Iterator))
print(isinstance((x for x in range(10)), Iterator)) # 列表生成式是迭代器

True
False
True
True
True

一般更常用的是用for循环遍历迭代器内容,这也是for循环的底层原理。for循环本质就是基于迭代器:for 循环在处理这些数据前,会调用 iter() 方法,将这些数据转化为一个迭代器,然后调用迭代器的 next() 方法,并捕获StopIteration异常,也就实现了遍历完所有数据就会结束,并不会抛出这个异常 。所以从for循环的角度,但凡可以被for循环取值的对象就是可迭代对象

1
2
3
4
list1 = [1,2,3]
iter1 = iter(list1) # 将列表转为迭代器
for i in iter1: # 也常用
print(i)

1
2
3

迭代器的优缺点:

  • 优点:
    • 提供了一种通用不依赖索引的迭代取值方式
    • 同一时刻在内存中只存在一个值,更节省内存
  • 缺点:
    • 取值不如按照索引的方式灵活,不能取指定的某一个值,只能往后取,不能往前取
    • 无法预测迭代器的长度

五、生成器(Generator)

迭代器解决了特大序列占内存的问题,但当生成序列的算法较为复杂时,需要一个能更简洁代码的迭代器,而不是一直定义类,这就有了生成器(个人理解)

在 Python 中,将使用了yield语句的函数赋给一个对象,这个对象被称为生成器

生成器和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成生成器的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行,所以需要给生成器设置一个条件来退出循环,不然就会无限产生内容出来。生成器本质上也是迭代器

所以,生成器仅仅保存了一套生成数值的算法,并且没有让这个算法现在就开始执行,而是什么时候调它,它什么时候开始计算一个新的值并返回,是一种懒汉式的思想。

同时也要注意,使用了yield语句的函数并不是生成器,它只是函数,必须要将它赋给一个对象,这个对象才是生成器

1
2
3
4
5
6
7
8
9
10
11
12
def fib(max):   # 斐波那契数列
n, a, b = 0, 0, 1
while n < max:
yield b # 遇到yield语句函数返回
a, b = b, a + b # 再次执行的时候从此执行
n = n + 1
return 'done'
g = fib(6)
print(type(fib)) # fib是一个特殊函数,仍然是一个函数
print(type(g)) # 将该函数赋给一个对象,这个对象成了generator
for i in g: # 迭代得到生成器元素
print(i) # 拿不到函数的return返回值

<class ‘function’>
<class ‘generator’>
1
1
2
3
5
8

值得注意的是,上面的代码中要想得到函数的return返回值,是做不到的,因为yield就返回了,所以如果想要得到返回值,必须捕获StopIteration错误,函数的返回值就包含在StopIteration的value中。而若只使用for循环或者list()函数,python并不会抛出这个异常,所以需要使用while循环加上next()函数。因此,编写和应用生成器时,看是否需要返回值,来选择用for/list()还是next()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def fib(max):   # 斐波那契数列
n, a, b = 0, 0, 1
while n < max:
yield b # 遇到yield语句函数返回
a, b = b, a + b # 再次执行的时候从此执行
n = n + 1
return 'done'
g = fib(6)
while True:
try:
print(next(g))
except StopIteration as e: # 捕获StopIteration错误
print('Generator return value:', e.value) # 返回值在异常的value中
break # 不加break,便会一直打印返回值

1
1
2
3
5
8
Generator return value: done

另外,只要把一个列表生成式的[]改成(),也能创建一个generator

1
2
g = (x * x for x in range(10))
type(g)

generator

迭代器和生成器都常被用于使用list()函数来生成列表

在写接受任意序列类型(列表、元组、N维数组),甚至是一个迭代器的函数时,可以先检查这个对象是否是列表,如果不是就将其转为列表

1
2
3
4
5
6
from collections.abc import Iterable

x = (1,2,3)
if not isinstance(x,list) and isinstance(x,Iterable):
x = list(x)
print(x)

[1, 2, 3]


Python基础:08.迭代器与生成器
http://jswanyu.github.io/2021/09/12/Python/Python基础:08.迭代器与生成器/
作者
万宇
发布于
2021年9月12日
许可协议