210 lines
9.3 KiB
Markdown
210 lines
9.3 KiB
Markdown
## 第012课:常用数据结构之元组
|
||
|
||
上一节课为大家讲解了Python中的列表,它是一种容器型数据类型,我们可以通过定义列表类型的变量来保存和操作多个元素。当然,Python中容器型的数据类型肯定不止列表一种,接下来我们为大家讲解另一种重要的容器型数据类型,它的名字叫元组(tuple)。
|
||
|
||
### 定义和使用元组
|
||
|
||
在Python中,元组也是多个元素按照一定的顺序构成的序列。元组和列表的不同之处在于,元组是不可变类型,这就意味着元组类型的变量一旦定义,其中的元素不能再添加或删除,而且元素的值也不能进行修改。定义元组通常使用`()`字面量语法,也建议大家使用这种方式来创建元组。元组类型支持的运算符跟列表是一样。下面的代码演示了元组的定义和运算。
|
||
|
||
```Python
|
||
# 定义一个三元组
|
||
t1 = (30, 10, 55)
|
||
# 定义一个四元组
|
||
t2 = ('骆昊', 40, True, '四川成都')
|
||
|
||
# 查看变量的类型
|
||
print(type(t1), type(t2)) # <class 'tuple'> <class 'tuple'>
|
||
# 查看元组中元素的数量
|
||
print(len(t1), len(t2)) # 3 4
|
||
|
||
# 通过索引运算获取元组中的元素
|
||
print(t1[0], t1[-3]) # 30 30
|
||
print(t2[3], t2[-1]) # 四川成都 四川成都
|
||
|
||
# 循环遍历元组中的元素
|
||
for member in t2:
|
||
print(member)
|
||
|
||
# 成员运算
|
||
print(100 in t1) # False
|
||
print(40 in t2) # True
|
||
|
||
# 拼接
|
||
t3 = t1 + t2
|
||
print(t3) # (30, 10, 55, '骆昊', 40, True, '四川成都')
|
||
|
||
# 切片
|
||
print(t3[::3]) # (30, '骆昊', '四川成都')
|
||
|
||
# 比较运算
|
||
print(t1 == t3) # False
|
||
print(t1 >= t3) # False
|
||
print(t1 < (30, 11, 55)) # True
|
||
```
|
||
|
||
一个元组中如果有两个元素,我们就称之为二元组;一个元组中如果五个元素,我们就称之为五元组。需要提醒大家注意的是,`()`表示空元组,但是如果元组中只有一个元素,需要加上一个逗号,否则`()`就不是代表元组的字面量语法,而是改变运算优先级的圆括号,所以`('hello', )`和`(100, )`才是一元组,而`('hello')`和`(100)`只是字符串和整数。我们可以通过下面的代码来加以验证。
|
||
|
||
```Python
|
||
# 空元组
|
||
a = ()
|
||
print(type(a)) # <class 'tuple'>
|
||
# 不是元组
|
||
b = ('hello')
|
||
print(type(b)) # <class 'str'>
|
||
c = (100)
|
||
print(type(c)) # <class 'int'>
|
||
# 一元组
|
||
d = ('hello', )
|
||
print(type(d)) # <class 'tuple'>
|
||
e = (100, )
|
||
print(type(e)) # <class 'tuple'>
|
||
```
|
||
|
||
### 元组的应用场景
|
||
|
||
讲到这里,相信大家一定迫切的想知道元组有哪些应用场景,我们给大家举几个例子。
|
||
|
||
#### 例子1:打包和解包操作。
|
||
|
||
当我们把多个用逗号分隔的值赋给一个变量时,多个值会打包成一个元组类型;当我们把一个元组赋值给多个变量时,元组会解包成多个值然后分别赋给对应的变量,如下面的代码所示。
|
||
|
||
```Python
|
||
# 打包
|
||
a = 1, 10, 100
|
||
print(type(a), a) # <class 'tuple'> (1, 10, 100)
|
||
# 解包
|
||
i, j, k = a
|
||
print(i, j, k) # 1 10 100
|
||
```
|
||
|
||
在解包时,如果解包出来的元素个数和变量个数不对应,会引发`ValueError`异常,错误信息为:`too many values to unpack`(解包的值太多)或`not enough values to unpack`(解包的值不足)。
|
||
|
||
```Python
|
||
a = 1, 10, 100, 1000
|
||
# i, j, k = a # ValueError: too many values to unpack (expected 3)
|
||
# i, j, k, l, m, n = a # ValueError: not enough values to unpack (expected 6, got 4)
|
||
```
|
||
|
||
有一种解决变量个数少于元素的个数方法,就是使用星号表达式,我们之前讲函数的可变参数时使用过星号表达式。有了星号表达式,我们就可以让一个变量接收多个值,代码如下所示。需要注意的是,用星号表达式修饰的变量会变成一个列表,列表中有0个或多个元素。还有在解包语法中,星号表达式只能出现一次。
|
||
|
||
```Python
|
||
a = 1, 10, 100, 1000
|
||
i, j, *k = a
|
||
print(i, j, k) # 1 10 [100, 1000]
|
||
i, *j, k = a
|
||
print(i, j, k) # 1 [10, 100] 1000
|
||
*i, j, k = a
|
||
print(i, j, k) # [1, 10] 100 1000
|
||
*i, j = a
|
||
print(i, j) # [1, 10, 100] 1000
|
||
i, *j = a
|
||
print(i, j) # 1 [10, 100, 1000]
|
||
i, j, k, *l = a
|
||
print(i, j, k, l) # 1 10 100 [1000]
|
||
i, j, k, l, *m = a
|
||
print(i, j, k, l, m) # 1 10 100 1000 []
|
||
```
|
||
|
||
需要说明一点,解包语法对所有的序列都成立,这就意味着对字符串、列表以及我们之前讲到的`range`函数返回的范围序列都可以使用解包语法。大家可以尝试运行下面的代码,看看会出现怎样的结果。
|
||
|
||
```Python
|
||
a, b, *c = range(1, 10)
|
||
print(a, b, c)
|
||
a, b, c = [1, 10, 100]
|
||
print(a, b, c)
|
||
a, *b, c = 'hello'
|
||
print(a, b, c)
|
||
```
|
||
|
||
现在我们可以反过来思考一下函数的可变参数,可变参数其实就是将多个参数打包成了一个元组,可以通过下面的代码来证明这一点。
|
||
|
||
```Python
|
||
def add(*args):
|
||
print(type(args), args)
|
||
total = 0
|
||
for val in args:
|
||
total += val
|
||
return total
|
||
|
||
|
||
add(1, 10, 20) # <class 'tuple'> (1, 10, 20)
|
||
add(1, 2, 3, 4, 5) # <class 'tuple'> (1, 2, 3, 4, 5)
|
||
```
|
||
|
||
#### 例子2:交换两个变量的值。
|
||
|
||
交换两个变量的值是编程语言中的一个经典案例,在很多编程语言中,交换两个变量的值都需要借助一个中间变量才能做到,如果不用中间变量就需要使用比较晦涩的位运算来实现。在Python中,交换两个变量`a`和`b`的值只需要使用如下所示的代码。
|
||
|
||
```Python
|
||
a, b = b, a
|
||
```
|
||
|
||
同理,如果要将三个变量`a`、`b`、`c`的值互换,即`b`赋给`a`,`c`赋给`b`,`a`赋给`c`,也可以如法炮制。
|
||
|
||
```Python
|
||
a, b, c = b, c, a
|
||
```
|
||
|
||
需要说明的是,上面并没有用到打包和解包语法,Python的字节码指令中有`ROT_TWO`和`ROT_THREE`这样的指令可以实现这个操作,效率是非常高的。但是如果有多于三个变量的值要依次互换,这个时候没有直接可用的字节码指令,执行的原理就是我们上面讲解的打包和解包操作。
|
||
|
||
#### 例子3:让函数返回多个值。
|
||
|
||
有的时候一个函数执行完成后可能需要返回多个值,这个时候元组类型应该是比较方便的选择。例如,编写一个找出列表中最大值和最小的函数。
|
||
|
||
```Python
|
||
def find_max_and_min(items):
|
||
"""找出列表中最大和最小的元素
|
||
:param items: 列表
|
||
:return: 最大和最小元素构成的二元组
|
||
"""
|
||
max_one, min_one = items[0], items[0]
|
||
for item in items:
|
||
if item > max_one:
|
||
max_one = item
|
||
elif item < min_one:
|
||
min_one = item
|
||
return max_one, min_one
|
||
```
|
||
|
||
上面函数的`return`语句中有两个值,这两个值会组装成一个二元组然后返回。所以调用`find_max_and_min`函数会得到这个二元组,如果愿意也可以通过解包语法将二元组中的两个值分别赋给两个变量。
|
||
|
||
### 元组和列表的比较
|
||
|
||
这里还有一个非常值得探讨的问题,Python中已经有了列表类型,为什么还需要元组这样的类型呢?这个问题对于初学者来说似乎有点困难,不过没有关系,我们先抛出观点,大家可以一边学习一边慢慢体会。
|
||
|
||
1. 元组是不可变类型,**不可变类型更适合多线程环境**,因为它降低了并发访问变量的同步化开销。关于这一点,我们会在后面讲解多线程的时候为大家详细论述。
|
||
|
||
2. 元组是不可变类型,通常**不可变类型在创建时间和占用空间上面都优于对应的可变类型**。我们可以使用`sys`模块的`getsizeof`函数来检查保存相同元素的元组和列表各自占用了多少内存空间。我们也可以使用`timeit`模块的`timeit`函数来看看创建保存相同元素的元组和列表各自花费的时间,代码如下所示。
|
||
|
||
```Python
|
||
import sys
|
||
import timeit
|
||
|
||
a = list(range(100000))
|
||
b = tuple(range(100000))
|
||
print(sys.getsizeof(a), sys.getsizeof(b)) # 900120 800056
|
||
|
||
print(timeit.timeit('[1, 2, 3, 4, 5, 6, 7, 8, 9]'))
|
||
print(timeit.timeit('(1, 2, 3, 4, 5, 6, 7, 8, 9)'))
|
||
```
|
||
|
||
3. Python中的元组和列表是可以相互转换的,我们可以通过下面的代码来做到。
|
||
|
||
```Python
|
||
# 将元组转换成列表
|
||
info = ('骆昊', 175, True, '四川成都')
|
||
print(list(info)) # ['骆昊', 175, True, '四川成都']
|
||
# 将列表转换成元组
|
||
fruits = ['apple', 'banana', 'orange']
|
||
print(tuple(fruits)) # ('apple', 'banana', 'orange')
|
||
```
|
||
|
||
### 简单的总结
|
||
|
||
**列表和元组都是容器型的数据类型**,即一个变量可以保存多个数据。**列表是可变数据类型**,**元组是不可变数据类型**,所以列表添加元素、删除元素、清空、排序等方法对于元组来说是不成立的。但是列表和元组都可以进行**拼接**、**成员运算**、**索引和切片**这些操作,就如同之前讲到的字符串类型一样,因为字符串就是字符按一定顺序构成的序列,在这一点上三者并没有什么区别。我们**推荐大家使用列表的生成式语法来创建列表**,它很好用,也是Python中非常有特色的语法。
|
||
|
||
> **温馨提示**:学习中如果遇到困难,可以加**QQ交流群**询问。
|
||
>
|
||
> 付费群:**789050736**,群一直保留,供大家学习交流讨论问题。
|
||
>
|
||
> 免费群:**151669801**,仅供入门新手提问,定期清理群成员。 |