本篇文章来介绍如何优雅地遍历可迭代对象,举个例子:
data = ["古明地觉", "芙兰朵露", "雾雨魔理沙"]
for item in data:
print(item)
"""
古明地觉
芙兰朵露
雾雨魔理沙
"""
遍历一个可迭代对象,可以使用 for 循环,每次会从可迭代对象中迭代出一个元素。当迭代完毕时,抛出 StopIteration,然后 for 循环捕获,终止循环。
当然,可迭代对象对内部的元素没有要求,可以指向任意的对象。
data = [("古明地觉", "女", "地灵殿"),
("琪露诺", "女", "雾之湖"),
("芙兰朵露", "女", "红魔馆")]
for item in data:
print(item)
"""
('古明地觉', '女', '地灵殿')
('琪露诺', '女', '雾之湖')
('芙兰朵露', '女', '红魔馆')
"""
此时迭代出来的元素就是一个个的元组,如果想获取元组里面的元素,那么可以通过索引的方式获取,比如 item[0]。但是基于索引的话,代码可读性不高,于是你可能会这么做。
data = [("古明地觉", "女", "地灵殿"),
("琪露诺", "女", "雾之湖"),
("芙兰朵露", "女", "红魔馆")]
for item in data:
name, gender, address = item
print(name, gender, address)
"""
古明地觉 女 地灵殿
琪露诺 女 雾之湖
芙兰朵露 女 红魔馆
"""
通过这种方式,代码的可读性变得更高了一些。但实际上,这段代码有点冗余,我们可以简化一下:
data = [("古明地觉", "女", "地灵殿"),
("琪露诺", "女", "雾之湖"),
("芙兰朵露", "女", "红魔馆")]
# name, gender, address 周围的小括号可以省略
for (name, gender, address) in data:
print(name, gender, address)
"""
古明地觉 女 地灵殿
琪露诺 女 雾之湖
芙兰朵露 女 红魔馆
"""
for 后面可以跟一个循环变量,也可以跟多个循环变量组成的元组。如果 for 后面跟的是一个普通的变量,那么可迭代对象里面的元素迭代出来之后会直接赋值给该变量。
如果 for 后面跟的是多个变量组成的元组,那么可迭代对象里迭代出来的元素必须仍是一个可迭代对象,并且迭代出来的每一个可迭代对象里面的元素个数,都必须和 for 后面的元组里的变量个数相同。最后进行解包,按照顺序将值分别赋给 for 后面的变量,这里就是 name, gender, address。
那么问题来了,这两种迭代方式有什么不同呢?
# 第一种迭代方式
for item in data:
name, gender, address = item
print(name, gender, address)
# 第二种迭代方式
for name, gender, address in data:
print(name, gender, address)
我们看一下字节码就清楚了,字节码面前没有秘密:
# 第一种迭代方式对应的字节码
# 加载变量 data
0 LOAD_NAME 0 (data)
# 获取可迭代对象对应的迭代器
2 GET_ITER
# 将元素迭代出来
4 FOR_ITER 13 (to 32)
# 赋值给变量 item
6 STORE_NAME 1 (item)
# 加载变量 item,item 一定也指向一个可迭代对象
8 LOAD_NAME 1 (item)
# 解包
10 UNPACK_SEQUENCE 3
# 按照顺序将里面的值赋给变量 name, gender, address
12 STORE_NAME 2 (name)
14 STORE_NAME 3 (gender)
16 STORE_NAME 4 (address)
# 第二种迭代方式对应的字节码
0 LOAD_NAME 0 (data)
2 GET_ITER
4 FOR_ITER 11 (to 28)
6 UNPACK_SEQUENCE 3
# 前面三条字节码没有区别
# 但是这里将元素迭代出来之后,直接就解包了
8 STORE_NAME 1 (name)
10 STORE_NAME 2 (gender)
12 STORE_NAME 3 (address)
所以这两种方式没有本质上的区别,只是第一种方式在将元素迭代出来之后需要单独用一个变量保存,然后加载变量,最后进行解包;而第二种方式在将元素迭代出来之后,直接就解包了。因此虽然效果是一样的,但是第二种方式要稍微快一点点,因为它少执行了两条指令。
另外,还有一种特殊情况:
data = [[1], [2], [3], [4]]
# for 后面是一个变量
for item in data:
print(item)
"""
[1]
[2]
[3]
[4]
"""
# for 后面是包含一个变量的元组
for item, in data:
print(item)
"""
1
2
3
4
"""
由于 data 里面的元素也是列表,所以 for 后面仍然可以跟一个元组,迭代的时候会自动解包。只是当元组里面只有一个元素的时候,需要在第一个元素的后面加上一个逗号,什么意思呢?举个例子:
data = [[1], [2], [3], [4]]
# 这里虽然给 item 加上了括号,但它仍然不是一个元组
for (item) in data:
print(item)
"""
[1]
[2]
[3]
[4]
"""
# 如果元组里面只有一个元素
# 那么第一个元素后面必须要有一个逗号
# 否则解释器会认为这个括号只是起到一个限定优先级的作用
for (item,) in data:
print(item)
"""
1
2
3
4
"""
# 再举个栗子
a, b, c = 3, 2, 4
# 此时 a + b 周围的括号只是起到了一个限定作用
# 用于提高 a + b 的优先级
print((a + b) * c) # 20
# 但如果是这样的话,就不同了
# 此时和 c 相乘的不再是整数,而是一个元组
print((a + b,) * c) # (5, 5, 5, 5)
当然啦,变量赋值也是同样的道理,因为每一次 for 循环本质上也是一次变量赋值。
numbers = (99, 96, 100)
a, b, c = numbers
print(a, b, c) # 99 96 100
# 也可以显式地使用括号括起来
(a, b, c) = (99, 96, 100)
print(a, b, c) # 99 96 100
# 如果变量名字比较长,那么还可以换行写
(
a,
b,
c
) = numbers
print(a, b, c) # 99 96 100
# 当可迭代对象只包含一个元素时,也是同理
numbers = (88,)
(a,) = numbers
print(a) # 88
# 赋值的时候,元组周围的小括号可以不要
a, = numbers
print(a) # 88
最后还有一个神奇的地方,在赋值的时候,多个变量不仅可以组成一个元组,还可以组成一个列表,举个例子:
numbers = (99, 96, 100)
[a, b, c] = numbers
print(a, b, c) # 99 96 100
# 如果是列表的话,当只有一个元素的时候,就不需要逗号了
numbers = [88]
[a] = numbers
print(a) # 88
# for 循环的时候也是同理
data = [("古明地觉", "女", "地灵殿"),
("琪露诺", "女", "雾之湖"),
("芙兰朵露", "女", "红魔馆")]
for [name, gender, place] in data:
print(name, gender, place)
"""
古明地觉 女 地灵殿
琪露诺 女 雾之湖
芙兰朵露 女 红魔馆
"""
当然啦,无论多个变量组成的是元组还是列表,字节码都没有区别。只是我们更习惯写成元组,并且将元组周围的小括号省略掉。
另外可迭代对象也是可以嵌套的,举个例子:
data = [("古明地觉", ("女", "地灵殿")),
("琪露诺", ("女", "雾之湖")),
("芙兰朵露", ("女", "红魔馆"))]
# 每个可迭代对象内部只有两个元素,所以在迭代的时候
# for 后面的元组或列表里面也只能有两个变量
for name, gender_address in data:
print(name, gender_address)
"""
古明地觉 ('女', '地灵殿')
琪露诺 ('女', '雾之湖')
芙兰朵露 ('女', '红魔馆')
"""
# 于是聪明的你可能想到了
for name, (gender, address) in data:
print(name, gender, address)
"""
古明地觉 女 地灵殿
琪露诺 女 雾之湖
芙兰朵露 女 红魔馆
"""
# 使用列表也是可以的
for [name, (gender, address)] in data:
print(name, gender, address)
"""
古明地觉 女 地灵殿
琪露诺 女 雾之湖
芙兰朵露 女 红魔馆
"""
# 以下几种方式也是可以的
"""
for [name, [gender, address]] in data:
print(name, gender, address)
for (name, [gender, address]) in data:
print(name, gender, address)
for (name, (gender, address)) in data:
print(name, gender, address)
"""
并且嵌套的可迭代对象的数量也是任意的,举个例子:
data = [("古明地觉", ("女",), ("地灵殿",)),
("琪露诺", ("女",), ("雾之湖",)),
("芙兰朵露", ("女",), ("红魔馆",))]
for name, gender, address in data:
print(name, gender, address)
"""
古明地觉 ('女',) ('地灵殿',)
琪露诺 ('女',) ('雾之湖',)
芙兰朵露 ('女',) ('红魔馆',)
"""
for name, [gender], [address] in data:
print(name, gender, address)
"""
古明地觉 女 地灵殿
琪露诺 女 雾之湖
芙兰朵露 女 红魔馆
"""
for name, (gender,), (address,) in data:
print(name, gender, address)
"""
古明地觉 女 地灵殿
琪露诺 女 雾之湖
芙兰朵露 女 红魔馆
"""
# 变量赋值也是同理
numbers = [[[3]]]
a = numbers
print(a) # [[[3]]]
a, = numbers
print(a) # [[3]]
((a,),) = numbers
print(a) # [3]
(((a,),),) = numbers
print(a) # 3
[[[a]]] = numbers
print(a) # 3
# 再来一个恶心人的,当然啦,这个做法没啥意义
# 只是想表明可迭代对象之间的嵌套是非常自由的
numbers = [[[3], [[[[4]], 5], 6]], 7]
(((a,), ((((b,),), c), d)), e) = numbers
print(a, b, c, d, e) # 3 4 5 6 7
最后再来介绍一个高级特性,不过介绍之前先来看看上面的迭代方式有什么缺陷:
data = [
(1, 2, 3, 4),
(5, 6),
(7, 8, 9)
]
如果是这种情况的话,那么 for 循环在遍历的时候,要使用几个变量去遍历呢?两个、三个、还是四个呢?我们先用三个变量看看:
data = [
(1, 2, 3, 4),
(5, 6),
(7, 8, 9)
]
for a, b, c in data:
print(a, b, c)
"""
Traceback (most recent call last):
File "...", line 6, in <module>
for a, b, c in data:
ValueError: too many values to unpack (expected 3)
"""
很明显它报错了,所以这种方式有一个缺陷,就是它除了要求可迭代对象里面的元素也是可迭代对象之外,还要满足它们内部的值的个数都相等,并且个数已知。
但是问题来了,如果我在遍历的时候,只想拿到里面的第一个值和最后一个值,该怎么办呢?
data = [
(1, 2, 3, 4),
(5, 6),
(7, 8, 9)
]
for item in data:
print(item[0], item[-1])
"""
1 4
5 6
7 9
"""
首先上面这种方式肯定是可以的,但还有没有另外的方式呢?显然是有的。
data = [
(1, 2, 3, 4),
(5, 6),
(7, 8, 9)
]
for first, *middle, last in data:
print(first, middle, last)
"""
1 [2, 3] 4
5 [] 6
7 [8] 9
"""
在迭代的时候,第一个值会赋给 first,这没有问题。然后是 middle,它的前面加上了一个 *,那么 middle 就会变成一个列表,这个类似正则的贪婪匹配,会不断地匹配值。而 *middle 后面还有一个 last,因此 *middle 就会匹配到倒数第二个值为止,最后一个值留给 last。
我们再举几个例子:
data = [
(1, 2, 3, 4, 5),
(6, 7, 8, 9, 10),
(11, 12, 13, 14, 15)
]
# 第 1 个值给 a、剩余的 4 个值给 b
for a, *b in data:
print(a, b)
"""
1 [2, 3, 4, 5]
6 [7, 8, 9, 10]
11 [12, 13, 14, 15]
"""
# 第 1 个值给 a、第 2 个值给 b,剩余的 3 个值给 c
for a, b, *c in data:
print(a, b, c)
"""
1 2 [3, 4, 5]
6 7 [8, 9, 10]
11 12 [13, 14, 15]
"""
# 第 1 个值给 a、第 2 个值给 b
# 倒数第 1 个值给 d,剩余的值给 c
for a, b, *c, d in data:
print(a, b, c, d)
"""
1 2 [3, 4] 5
6 7 [8, 9] 10
11 12 [13, 14] 15
"""
# 倒数第 1 个值给 b,前面的值给 a
for *a, b in data:
print(a, b)
"""
[1, 2, 3, 4] 5
[6, 7, 8, 9] 10
[11, 12, 13, 14] 15
"""
# 每次迭代的元素内部只有 5 个值,所以 b 是一个空列表
for a, *b, c, d, e, f in data:
print(a, b, c, d, e, f)
"""
1 [] 2 3 4 5
6 [] 7 8 9 10
11 [] 12 13 14 15
"""
# 所有的值都给 a,但是需要注意:
# 如果出现了 *,那么 for 后面的变量必须组成一个元组或列表
# 所以如果是 for *a in data: 会报出语法错误
# 必须是 for *a, in data: 或者 for [*a] in data:
for *a, in data:
print(a)
"""
[1, 2, 3, 4, 5]
[6, 7, 8, 9, 10]
[11, 12, 13, 14, 15]
"""
另外还有一个约定或者说规范,如果在遍历的时候,有一部分的值我们不需要,那么可以使用下划线代替。比如我们只需要第一个值和倒数第二个值,那么遍历的时候就可以像下面这么做:
for a, *_, b, _ in data:
pass
当然啦,* 不仅可以在 for 循环的时候用,普通的变量赋值也是可以使用的,一样的道理。
在赋值的时候, * 最多只能出现一次,否则会报出语法错误。
以上就是可迭代对象的遍历,是不是很有趣呢?