Python 中的序列解包

序列解包在 Python 中非常重要并且常用,以简短又可读的代码完成复杂的功能。

最近在看《Python 3 学习笔记》上卷,有非常多的收获,感谢作者完成了这么优秀的作品。里面就有关于序列解包的解读,获益良多,记录下来。

简单操作

序列解包展开所有的元素,继而分别与多个名字关联。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
## 对 list 对象进行解包
In [337]: a, b, c = [1, 2, 3]
In [338]: a, b, c
Out[338]: (1, 2, 3)
In [339]: a
Out[339]: 1
In [340]: b
Out[340]: 2
In [341]: c
Out[341]: 3
## tuple
In [342]: a, b, c = (1, 2, 3)
In [343]: a
Out[343]: 1
## str
In [344]: a, b, c = "xyz"
In [345]: a, b, c
Out[345]: ('x', 'y', 'z')
## range
In [346]: a, b, c = range(3)
In [347]: a, b, c
Out[347]: (0, 1, 2)
## 右值表达式构建元组对象([1, 2], (3, 4))
In [348]: a, b = [1, 2], (3, 4)
In [349]: a
Out[349]: [1, 2]
In [350]: b
Out[350]: (3, 4)

解包操作还可以用来交换表变量,无须借助第三方,这也是 Python 的一个语法糖。

1
2
3
4
5
6
7
8
9
10
11
In [351]: a = 1
In [352]: b = 2
In [353]: a, b = b, a
In [354]: a
Out[354]: 2
In [355]: b
Out[355]: 1

对三个以内的变量交换,编译器优化成ROT指令,直接交换栈帧数据,而不是构建元组。栈帧指的是编译器用来实现过程/函数调用的一种数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
## 三个变量
In [356]: dis.dis(compile("a, b, c = c, b, a", "", "exec"))
1 0 LOAD_NAME 0 (c)
2 LOAD_NAME 1 (b)
4 LOAD_NAME 2 (a)
6 ROT_THREE ## 使用 ROT 指令直接操作栈帧数据
8 ROT_TWO
10 STORE_NAME 2 (a)
12 STORE_NAME 1 (b)
14 STORE_NAME 0 (c)
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
## 三个以上
In [357]: dis.dis(compile("a, b, c, d = d, c, b, a", "", "exec"))
1 0 LOAD_NAME 0 (d)
2 LOAD_NAME 1 (c)
4 LOAD_NAME 2 (b)
6 LOAD_NAME 3 (a)
8 BUILD_TUPLE 4 ## 构建元组,解包后分别赋值
10 UNPACK_SEQUENCE 4
12 STORE_NAME 3 (a)
14 STORE_NAME 2 (b)
16 STORE_NAME 1 (c)
18 STORE_NAME 0 (d)
20 LOAD_CONST 0 (None)
22 RETURN_VALUE

支持深度嵌套展开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
In [358]: a, (b, c) = 1, [10, 20]
In [359]: a, b, c
Out[359]: (1, 10, 20)
In [362]: a, ((b, c), (d, e)) = 1, [(10, 20), "ab"]
In [363]: a, b, c, d, e
Out[363]: (1, 10, 20, 'a', 'b')
In [364]: a, _, b, _, c = "a0b0c"
In [365]: a, b, c
Out[365]: ('a', 'b', 'c')

星号收集

当序列元素与名字数量不等时,解包出错。为此,Python 3 专门对其进行了扩展。在名字前面添加星号,表示收纳所有的剩余元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
In [366]: a, *b, c = range(5)
In [367]: a, b, c
Out[367]: (0, [1, 2, 3], 4)
In [368]: *a, b, c = range(5)
In [369]: a, b, c
Out[369]: ([0, 1, 2], 3, 4)
In [370]: a, b, *c = range(5)
In [371]: a, b, c
Out[371]: (0, 1, [2, 3, 4])
In [372]: a, *b, c = 1, 2
In [373]: a, b, c
Out[373]: (1, [], 2) # 收集不到数据,返回空列表

解包操作优先保障对非收集名字(也就是不带星号的名字)的赋值,所以元素不能少于非收集名字的数目。

另外需注意的是,星号只能有一个,否则无法界定收集边界。

1
2
3
4
5
In [374]: a, *b, c, d = 1, 2
ValueError: not enough values to unpack (expected at least 3, got 2)
In [375]: a, *b, *c = range(5)
SyntaxError: two starred expressions in assignment

星号收集不能单独出现:要么与其他名字一起,要么放入列表或元组内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
In [376]: *a = 1, 2
SyntaxError: starred assignment target must be in a list or tuple
In [377]: [*a] = 1, 2
In [378]: a
Out[378]: [1, 2]
In [379]: (*a, ) = 1, 2 # 注意元组要加逗号!
In [380]: a
Out[380]: [1, 2]
In [381]: for a, *b in ["abc", (1, 2, 3)]:
...: print (a, b)
...:
a ['b', 'c']
1 [2, 3]

星号展开

星号可以用来展开可迭代对象。

1
2
3
4
5
6
7
8
In [382]: a = (1, 2)
In [383]: b = "ab"
In [384]: c = range(10, 13)
In [385]: [*a, *b, *c]
Out[385]: [1, 2, 'a', 'b', 10, 11, 12]

对于字典,单星号展开主键,双星号展开键值。

1
2
3
4
5
6
7
In [386]: d = {"a": 1, "b": 2}
In [387]: [*d]
Out[387]: ['a', 'b']
In [390]: {"c": 3, **d}
Out[390]: {'a': 1, 'b': 2, 'c': 3}

还可以用于函数调用,将单个对象分解成多个实参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In [391]: def test(a, b, c):
...: print(locals())
...:
In [392]: test(*range(3))
{'c': 2, 'b': 1, 'a': 0}
In [393]: test(*[1, 2], 3)
{'c': 3, 'b': 2, 'a': 1}
In [394]: a = {"a": 1, "c": 3}
In [395]: b = {"b": 2}
In [396]: test(**a, **b)
{'c': 3, 'b': 2, 'a': 1}