目录

Packing and unpacking in Python (1)

前言

本人将通过两篇文章,来介绍 Python 3 中的拆包和装包。第一篇文章先介绍一下拆包与装包的基本使用方式,以及注意事项。

注意

本文中的命令行示范,都是基于以下环境:

  • 操作系统: CentOS 7
  • Python: 3.7.0 3.7.9 (更新)

 

拆包与装包的概念

拆包(unpacking)是 Python 提供的一种特殊的赋值方法,它可以将一个数据结构中的数据按照语法拆分为多个个体,并赋给单独的变量。而装包(packing)正好相反,它将多个个体,打包装在一个数据结构中。拆包和装包可以用在普通的赋值语句中,也可以用在函数参数的传递过程中。

注意
需要强调,这里的拆包和装包和 Python 的 struct 模块中的unpack/pack是两回事,后者是用来在计算机之间以及不同语言之间进行网络通讯时对数据格式的转换。

先来看两个例子。

示例一:

1
2
3
4
s1_name, s1_country, s1_height, s1_birthday = ('John', 'USA', 176, (1987, 12, 10))
print(s1_name, s1_country, s1_height, s1_birthday)
s1_name, s1_country, s1_height, (s1_year, s1_month, s1_date) = ('John', 'USA', 176, (1987, 12, 10))
print(s1_name, s1_country, s1_height, s1_year, s1_month, s1_date)

这个就是拆包的最简单的例子。等式右侧是个元祖对象,里面放着学生个人信息,左侧定义了四个变量,第四个变量可以是个单独变量,也可以是个含三个变量的元祖。等式会将右侧的元祖拆分为左侧对应的变量并赋值。这种拆包叫做元祖拆包(tuple unpacking),拆包的形式叫平行赋值。执行结果如下:

1
2
John USA 176 (1987, 12, 10)
John USA 176 1987 12 10

第一次打印的是四个变量,第四个变量对应一个元祖;第二次打印七个变量,因为赋值语句将右侧元祖拆分为七个单独变量。

示例二:

1
2
student1 = 'John', 'USA', 176, (1987, 12, 10)
print(student1)

这是个装包的例子,更简单,右侧是多个值,左侧一个变量,Python 解释器会将右侧的值放在一个 tuple 对象里,并赋给左侧变量,所以执行结果就是一个 tuple 对象:

1
('John', 'USA', 176, (1987, 12, 10))

这是关于拆包和装包最简单的例子,实际使用中,它们的功能更强大,让我们来看看其它使用方式。

 

拆包的基本用法

可迭代对象拆包

第一节中介绍的是元祖拆包(tuple unpacking)的例子,即把一个元祖拆成多个变量,实际上,拆包可以应用到任何可迭代对象上,唯一的硬性要求是:可迭代对象中的元素数量必须要跟接受这些元素的变量数量一致,除非我们用星号(*)来表示忽略多余的元素。 把拆包用于可迭代对象上,叫做可迭代对象拆包(iterable unpacking)。来看一个简单例子:

1
v1, v2, v3 = range(3)

在 Python 3 中,range() 是一个可迭代对象,这里将一个可迭代三次的对象拆包为三个变量。

再看一个例子:

1
2
k1, k2 = {1: 'a', 2: 'b'}
print(k1, k2)

执行结果:

1
1 2

因为 dict 也是可迭代对象,所以可以对它拆包,拆包后得到的变量值是 dict 的 key,而不是 value。

拆包中的占位符

有时候,把一个可迭代对象拆成多个变量,其中有些变量对我们没有意义,可以忽略,这时候可以用下划线 ‘_’ 来作为一个占位符。比如第一节的例子中,我们的程序只对学生的出生日期感兴趣,其它信息可以忽略,可以采用下面写法:

1
2
_, _, _, s1_birthday = ('John', 'USA', 176, (1987, 12, 10))
print(s1_birthday)

执行结果:

1
(1987, 12, 10)

这里没有什么特殊的技术,‘_’ 只是一个普通的变量,毕竟 Python 是可以用下划线作为变量的。

拆包中星号修饰的变量

Python 3 中新增了一个关于拆包的功能:可以用星号(*)修饰的变量接受多个拆包后的元素。以前在 Python 2 中,函数的形参可以用星号修饰,比如*args,表示接受多个实参并放在 args 元祖中。Python 3 把这个概念扩展到了平行赋值中。比如下面的代码,

示例三:

1
2
3
4
5
6
a, b, *rest = range(5)
print(a, b, rest)
a, b, *rest = range(3)
print(a, b, rest)
a, b, *rest = range(2)
print(a, b, rest)

执行结果:

1
2
3
0 1 [2, 3, 4]
0 1 [2]
0 1 []

星号修饰的变量可以出现在左侧任意位置,比如:

1
2
a, *rest, b = range(5)
print(a, rest, b)

执行结果:

1
0 [1, 2, 3] 4

这里需要记住三点:

  • 星号修饰的变量,是个 list 类型,存放着匹配的元素,可以为空列表。
  • 左侧变量中,只能有一个星号修饰的变量。
  • 左侧如果只用一个星号修饰的变量,需要逗号跟随。

要理解第二点,你可以把等式的写法看作是正则表达式的匹配过程,星号修饰的变量可以匹配任意多个元素,直到左侧和右侧完全匹配。如果左侧有两个星号修饰的变量,就会引起歧义。比如:

1
a, *rest1, b, *rest2 = range(5)

如果这个语句合法,那么四个变量的值会有多种结果:

1
2
3
4
0 [1] 2 [3, 4]
0 [1, 2] 3 [4]
0 [1, 2, 3] 4 []
....

编程语言的设计,保证了同一语句,不可能有歧义,所以上述会引起歧义的、两个星号修饰的语法,是不合法的。

关于第三点,是因为等式左侧实际上是一个 tuple,当有多个元素的时候可以省去括号,但当只有一个元素的时候,需要用逗号表示它是一个 tuple,否则 Python 会把它解释成单一变量,如:

1
2
3
*all, = range(5)
print(all)
*all = range(5)

执行结果:

1
2
3
[0, 1, 2, 3, 4]
...
SyntaxError: starred assignment target must be in a list or tuple

嵌套对象拆包

关于嵌套对象拆包,其实在第一节中的例子已经展示过了:

1
2
s1_name, s1_country, s1_height, s1_birthday = ('John', 'USA', 176, (1987, 12, 10))
s1_name, s1_country, s1_height, (s1_year, s1_month, s1_date) = ('John', 'USA', 176, (1987, 12, 10))

等式右侧就是一个嵌套对象,元祖对象嵌套着另一个元祖对象,左侧的变量,只要符合表达式本身的嵌套结构,就可以正确解析并赋值。

 

装包的基本用法

比起拆包,装包更多的用法是在函数的形参和实参中,而装包的基本用法,已经在第一节中举过例子,其实就是一个简单的赋值语句:

1
2
student1 = 'John', 'USA', 176, (1987, 12, 10)
print(student1)

等式右侧其实就是一个 tuple,因为在没有二义性的时候,tuple 的括号是可以省略的。整个表达式可以理解成把一个 tuple 对象赋值给左侧的变量,所以输出:

1
('John', 'USA', 176, (1987, 12, 10))

另一种方法是巧妙地用星号拆包的方法来装包:

1
2
*student1, = 'John', 'USA', 176, (1987, 12, 10)
print(student1)

拆包中星号修饰的变量 一节中我们介绍过这种拆包方法,这种方法把右侧的 tuple 对象拆成左侧唯一一个 list 对象 student1,所以你可以把它看成是装包行为。执行结果和上一个例子的区别在于这里的 student1 变量是个 list:

1
['John', 'USA', 176, (1987, 12, 10)]

关于装包以及拆包,用在函数形参和实参中更高级的用法,本人将在 第二篇文章 中详细介绍。

 


- 全文完 -

相关文章

「 您的赞赏是激励我创作和分享的最大动力! 」