5.内建数据结构
内建数据结构
1 内建常用数据结构
1.1 官方类型层级
- 序列(Sequence)
- 字符串
str、字节序列bytes、bytearray - 列表
list、元组tuple
- 字符串
- 键值对(Mapping)
- 字典
dict
- 字典
- 集合(Set)
- 集合
set
- 集合
容器 (Container)
├── 序列 (Sequence) → 有序,可重复,支持索引
├── 映射 (Mapping) → 键值对,字典
└── 集合 (Set) → 无序,唯一,支持交并差运算
├── 可变集合 (set) → 可增删
└── 不可变集合 (frozenset) → 可哈希,可作为字典键
1.2 序列
序列是一种数据存储方式,用来存储一系列数据。在内存中,序列就是一块用来存放多个值的连续内存空间。例如整数序列 [10, 20, 30, 40],可以这样示意表示:

由于 Python 3 中一切皆对象,内存中实际的存储方式如下:
a = [10, 20, 30, 40]

从图中可以看出:序列中存储的是整数对象的地址,而不是整数对象的值。
- Python 中常用的序列结构有:字符串、列表、元组。
- Python 中常用的键值对结构有:字典。
- Python 中常用的集合结构有:集合。
上面学习的字符串就是一种序列,字符串里的很多操作在序列中同样适用。
2 列表
2.1 为什么需要列表
2.2 列表的定义
列表:用于存储任意数目、任意类型的数据集合。
列表底层用顺序表实现,类似于 Java 等语言中的数组。
列表是内置可变序列,是一段包含多个元素的有序连续内存空间。定义列表的标准语法格式:
# 伪代码:展示语法形态
列表序列名称 = [列表中的元素1, 列表中的元素2, 列表中的元素3, ...]
案例:定义一个列表,用于保存苹果、香蕉和菠萝。
list1 = ['apple', 'banana', 'pineapple']
# list 列表类型支持直接打印
print(list1)
# 打印列表的数据类型
print(type(list1))

字符串和列表都是序列类型:字符串是字符序列,列表是任意元素的序列。前面学过的许多字符串方法,在列表中也有几乎一样的用法。
列表:
- 像一个排列整齐的队伍,Python 用顺序表实现
- 列表内的个体称为元素,由若干元素组成列表
- 元素可以是任意对象(数字、字符串、对象、列表等)
- 列表内元素有顺序,可以使用索引
- 属于线性数据结构
- 使用
[]表示 - 列表是可变的
基本语法 [] 创建
a = [] # 创建一个空的列表对象
a = [10, 20, 'ym', 'fbb']
list() 创建
使用 list() 可以将任何可迭代的数据转化成列表。
# 创建一个空的列表对象
a = list()
# 字符串转列表
a = list("gaoqi,sxt")
# range 转列表
a = list(range(10))

range() 创建整数列表
range() 可以非常方便地创建整数列表,这在开发中极其有用。语法格式为:
# 伪代码:展示完整参数
range([start,] end [,step])
典型示例如下:
# range() 返回一个range对象
type(range(1, 10))
list(range(3, 15, 2))
list(range(15, 3, -1))
list(range(3, -10, -1))

推导式生成列表
这里先做简介,重点在 for 循环之后讲解。
列表推导式可以非常方便地创建列表,开发中经常使用。它涉及 for 循环和 if 语句,因此此处仅做基本介绍;控制语句一章会详细讲解更多列表推导式的细节。
# 循环创建多个元素
a = [x * 2 for x in range(5)]
# 通过 if 过滤元素
a = [x * 2 for x in range(100) if x % 9 == 0]

2.3 列表的相关操作
列表的作用是一次性存储多个数据,程序员可以对这些数据进行的操作有:增、删、改、查。

查操作
列表和字符串一样,在内存中都占用一段连续的内存空间。要访问列表中的每个元素,都可以通过索引下标的方式获取。

想获取列表中的某个元素非常简单,直接使用索引下标即可:
list1 = ['apple', 'banana', 'pineapple']
# 获取列表中的 banana
print(list1[1])

查操作的相关方法:
| 函数 | 作用 |
|---|---|
index() |
指定数据所在位置的下标,如果列表中没有指定数据,则报异常 |
count() |
统计指定数据在当前列表中出现的次数,如果列表中不存在指定元素,返回 0 |
in |
判断指定数据在某个列表序列,如果在返回 True,否则返回 False |
not in |
判断指定数据不在某个列表序列,如果不在返回 True,否则返回 False |
len() |
返回列表长度,即列表中包含元素的个数 |
index()
index() 可以获取指定元素首次出现的索引位置。语法是:
# 伪代码:展示完整参数
index(value [,start [,end]])
start 和 end 指定了搜索的范围。
# 1、查找某个元素在列表中出现的位置(索引下标)
list1 = ['apple', 'banana', 'pineapple']
print(list1.index('apple')) # 0
# print(list1.index('peach')) # 报错
a = [10, 20, 30, 40, 50, 20, 30, 20, 30]
# 从头开始搜索列表中的第一个 20
print(a.index(20))
# 从索引位置 3 开始往后搜索的第一个 20
print(a.index(20, 3))
# 从索引位置 5 到 7 这个区间,第一次出现 30 元素的位置
print(a.index(30, 5, 7))


count()
count() 可以返回指定元素在列表中出现的次数;若列表中不存在该元素,则返回 0。
举例:
# 2、count() 方法:统计元素在列表中出现的次数
list2 = ['刘备', '关羽', '张飞', '关羽', '赵云']
# 统计一下关羽这个元素在列表中出现的次数
print(list2.count('关羽'))
print(list2.count('小龙女'))

index 和 count 函数效率如何?
效率很差,因为它们都要遍历元素。
index:若查找的是列表第一个元素,时间复杂度最小为 O(1);最坏情况(目标位于末尾)为 O(n)。
count:需要遍历整个列表计数,时间复杂度为 O(n)。
二者的耗时都跟列表中数据的个数有关,元素多即「规模大」。
index 的耗时随规模增大而增加,搜索效率随规模增大而下降。
count 无论有多少元素都要完整遍历一遍,耗时同样随规模增大而增加。
性能极差,能不用则不用。做算法题时几乎不用。
使用索引访问列表元素才是列表最好的使用方式,复杂度是 O(1)。
in 与 not in
判断列表中是否存在指定元素,可以用 count() 方法:返回 0 表示不存在,返回大于 0 表示存在。
不过一般会用更简洁的 in 关键字,直接返回 True 或 False。
in 效率高吗?
不高,因为需要遍历。与 index 函数一样,只是返回值不同。
>>> # 本质是先比较地址是否一样,再比较内容
>>> [1] in [[1], [2]]
True
# 3、in 方法和 not in 方法(黑名单系统)
list3 = ['192.168.1.15', '10.1.1.100', '172.35.46.128']
if '10.1.1.100' in list3:
print('黑名单IP,禁止访问')
else:
print('正常IP,访问站点信息')

len()
len() 返回列表长度,即列表中包含元素的个数。
a = [10, 20, 30]
len(a)

len 函数的时间复杂度为 O(1)。
创建列表时,列表内部会自动创建一个变量来记录列表长度。
增操作
列表增加和删除元素时,会自动进行内存管理,大大减轻程序员的负担。但这也涉及列表元素的大量移动、效率较低。除非必要,一般只在列表尾部添加或删除元素,这会显著提高操作效率。
| 函数 | 作用 |
|---|---|
append() |
增加指定数据到列表中 |
extend() |
列表结尾追加数据,如果数据是一个序列,则将这个序列的数据逐一添加到列表 |
insert() |
指定位置新增数据 |
append()
append():在列表尾部追加元素。原地修改列表对象,是真正的尾部添加新元素,速度最快,推荐使用。
append 方法没有返回值,或者说返回值为 None——没有返回值往往意味着它就地修改了自己。
names = ['孙悟空', '唐僧', '猪八戒']
# 打印增加元素前的列表地址
print('增加元素前的列表地址', id(names))
# 在列表的尾部追加一个元素"沙僧"
names.append('沙僧')
# 打印增加元素后的列表地址
print('增加元素后的列表地址', id(names))
# 打印列表
print(names)

extend() 方法
在列表尾部追加数据;若数据是一个序列,就把这个序列的数据逐一添加到列表。属于原地操作,不创建新列表对象。
list1.extend(Iterator) 等价于:
for i in iterator:
list1.append(i)
案例:
list1 = ['Tom', 'Rose', 'Jack']
list2 = ['Hack', 'Jennify']
# 列表合并前的地址
print('列表合并前的地址', id(list1))
# 使用 extend 方法两个列表进行合并
list1.extend(list2)
# 列表合并后的地址
print('列表合并后的地址', id(list1))
print(list1)

l1 = [1, 2, 3]
# extend 的参数必须为一个可迭代对象
l1.extend(1)
print(l1)

insert() 方法
使用 insert() 方法可以将指定的元素插入到列表对象的任意指定位置。这样会让插入位置后面所有的元素进行移动,会影响处理速度。涉及大量元素时,尽量避免使用。
类似发生这种移动的函数还有:remove()、pop()、del(),它们在删除非尾部元素时也会出现操作位置后面元素的移动。
# 伪代码:展示语法
insert(index, value)
作用:在 index 的位置插入元素,如果右边界越界不报错,会将元素放到末尾;左边界小于 -(len - 1) 会将元素放到第一个位置。
names = ['薛宝钗', '林黛玉', '孙悟空', '猪八戒', '沙僧']
# 在薛宝钗和林黛玉之间,插入一个新元素"贾宝玉"
names.insert(1, '贾宝玉')
print(names)
names.insert(-2, '六耳猕猴')
names.insert(-20, '玉帝')
names.insert(20, '唐僧')
print(names)

+ 运算符操作
向列表末尾追加元素,并不是真正的尾部添加元素,而是创建新的列表对象;将原列表的元素和新列表的元素依次复制到新的列表对象中。这样,会涉及大量的复制操作,对于操作大量元素不建议使用。
l1 = [20, 30]
# 添加元素前
print('添加元素前', id(l1))
l1 = l1 + [50]
# 添加元素后
print('添加元素后', id(l1))



元祖(原文写作「元祖」,标准说法是「元组」)
tu = (1, 2, 3) print(f'原地址:{id(tu)},tu的值:{tu}') new_tu = tu + (1, 2, 3) print(f'+ 操作后地址:{id(new_tu)},new_tu的值:{new_tu}') tu += (1, 2, 3) print(f'+= 操作后地址:{id(tu)},tu的值:{tu}')原地址: 2113348747584,tu的值:(1, 2, 3) + 操作后地址: 2113348747712,new_tu的值:(1, 2, 3, 1, 2, 3) += 操作后地址: 2113348747712,tu的值:(1, 2, 3, 1, 2, 3) ← 变了,新对象实测确认:元组
+=后 id 改变,证明产生了新对象——与原文结论一致。

乘法扩展
使用乘法扩展列表,生成一个新列表,新列表元素是原列表元素的多次重复。
a = ['小龙女', 100]
b = a * 3
print(a)
print(b)

适用于乘法操作的,还有:字符串、元组。例如:
c = '小龙女'
d = c * 3
print(c)
print(d)

删操作
| 函数 | 作用 |
|---|---|
del 列表[索引] |
删除列表中的某个元素 |
pop() |
删除指定下标的数据(默认为最后一个),并返回该数据 |
remove() |
移除列表中某个数据的第一个匹配项。如果不存在,抛异常 |
clear() |
清空列表,删除列表中的所有元素,返回空列表。 |
del 删除指定的列表元素
基本语法:
names = ['Tom', 'Rose', 'Jack', 'Jennify']
# 删除Rose
del names[1]
# 打印列表
print(names)

删除列表指定位置的元素:
a = [100, 200, 888, 300, 400]
del a[2]
a


pop() 方法
作用:删除指定下标的元素;若不填下标,默认删除最后一个。返回结果就是被删除的那个元素。
names = ['貂蝉', '吕布', '董卓']
del_name = names.pop()
print(del_name)
print(names)
names = ['貂蝉', '吕布', '董卓']
del_name = names.pop(1)
print(del_name)
print(names)

l1 = [1, 2, 3]
# 报索引越界异常
print(l1.pop(9))

remove() 方法
作用:删除首次出现的指定元素;若不存在该元素则抛出异常。
fruit = ['apple', 'banana', 'pineapple', 'banana']
fruit.remove('banana')
print(fruit)
fruit = ['apple', 'banana', 'pineapple', 'banana']
# 列表中没有该元素,抛异常
fruit.remove('peach')
print(fruit)

clear() 方法
清空列表,只是清除列表中的所有元素,列表本身并未被删除。
names = ['貂蝉', '吕布', '董卓']
# 随着故事的发展,人物都 game over
names.clear()
# 打印列表
print(names)

改操作
| 函数 | 作用 |
|---|---|
列表[索引] = 修改后的值 |
修改列表中的某个元素 |
reverse() |
将数据序列进行倒叙排列 |
sort() |
对列表序列进行排序 |
copy() |
对列表序列进行拷贝 |
索引修改列表内容
list1 = ['貂蝉', '大乔', '小乔', '八戒']
# 修改列表中的元素
list1[3] = '周瑜'
print(list1)
list2 = [1, 6, 5, 9, 1, 3]
list2.reverse()
print("列表倒序排列:", list2)
list3 = [10, 50, 20, 30, 1]
list3.sort() # 升序(从小到大)
print("列表升序排列", list3)
# 或
list3.sort(reverse=True) # 降序(从大到小)
print("列表降序排列", list3)
list4 = list3.copy()
print("列表复制", list4)

通过索引修改列表效率非常高,时间复杂度为 O(1)。
sort()
修改原列表、不创建新列表的排序。
对列表序列进行排序,默认升序。
# 伪代码:展示完整参数
sort(key=None, reverse=False)
-
key:排序函数,传递一个高阶函数。l1 = [1, 2, 'a', 7] l1.sort(key=str) # 类似于 if str(2) > str('a') print(l1)

reverse:False 表示升序、True 表示降序,默认为 False。
a = [20, 10, 30, 40]
# 默认是升序排列
a.sort()
print(a)
# 降序排列
a.sort(reverse=True)
print(a)
# 打乱顺序
import random
random.shuffle(a)
print(a)

sorted()
建新列表的排序。
我们也可以通过内置函数 sorted() 进行排序,这个方法返回新列表,不对原列表做修改。
a = [20, 10, 30, 40]
print('原列表地址', id(a))
# 默认升序排列
b = sorted(a)
print('升序排列列表地址', id(b))
print(b)
# 降序排列
c = sorted(a, reverse=True)
print('降序排列列表地址', id(c))
print(c)

从上面的操作可以看出,生成的列表对象 b、c 都是全新的列表对象。
reversed()
返回迭代器。
内置函数 reversed() 也支持逆序排列。与列表的 reverse() 方法不同,reversed() 不对原列表做任何修改,只返回一个逆序排列的迭代器对象。
a = [20, 10, 30, 40]
b = reversed(a)
print(b)
print(list(b))

打印 b 可见类型为 list_reverseiterator,也就是一个迭代对象。
同时用 list(b) 输出会发现它只能用一次:第一次输出了元素,第二次为空。这是因为迭代对象在第一次时已经遍历结束,第二次无法再使用。
列表相关的其他内置函数汇总
copy()
复制出一个新列表,属浅复制。
l1 = [1, 2, 'str', [1, 2]]
l2 = l1.copy()
print(id(l1))
print(id(l2))
print('修改前:')
print(l1)
print(l2)
l2[3][0] = 100
print('修改后:')
print(l1)
print(l2)

max 和 min
用于返回列表中最大和最小值。
a = [3, 10, 20, 15, 9]
print('最大值', max(a))
print('最小值', min(a))

sum
对数值型列表的所有元素进行求和操作,对非数值型列表运算则会报错。
a = [3, 10, 20, 15, 9]
sum(a)
b = ['xcz', 10, 'dd']
sum(b)

切片操作
我们在前面学习字符串时,学习过字符串的切片操作,对于列表的切片操作和字符串类似。切片是 Python 序列及其重要的操作,适用于列表、元组、字符串等等。切片 Slice 操作可以让我们快速提取子列表或修改。切片的格式如下:
# 伪代码:展示完整语法
[起始偏移量 start : 终止偏移量 end [:步长 step]]
典型操作(三个量为正数的情况)如下:
| 操作和说明 | 示例 | 结果 |
|---|---|---|
[:] 提取整个列表 |
[10, 20, 30][:] |
[10,20,30] |
[start:] 从 start 索引开始到结尾 |
[10, 20, 30][1:] |
[20,30] |
[:end] 从头开始直到 end-1 |
[10, 20, 30][:2] |
[10,20] |
[start: end] 从 start 到 end-1 |
[10, 20, 30, 40][1: 3] |
[20,30] |
[start: end :step] 从 start 提取到 end-1,步长是 step |
[10, 20, 30, 40, 50, 60, 70][1: 6: 2] |
[20, 40, 60] |
其他操作(三个量为负数)的情况:
| 说明 | 示例 | 结果 |
|---|---|---|
| 倒数三个 | [10, 20, 30, 40, 50, 60, 70][-3:] |
[50,60,70] |
| 倒数第五个到倒数第三个(包左不包右) | [10, 20, 30, 40, 50, 60, 70][-5: -3] |
[30,40] |
| 步长为负,从右到左反向提取 | [10, 20, 30, 40, 50, 60, 70][::-1] |
[70, 60, 50, 40, 30, 20, 10] |
切片操作时,起始偏移量和终止偏移量不在 [-(长度), 长度 - 1] 这个范围,也不会报错。起始偏移量小于 -(长度) 则会当做 0,终止偏移量大于 长度 - 1 会被当成 长度 - 1。
例如:
# 正常输出了结果,没有报错
[10, 20, 30, 40][1: 30]

通过索引直接访问元素
我们可以通过索引直接访问元素。索引的区间在 [-列表长度, 列表长度 - 1] 这个范围。超过这个范围则会抛出异常。
a = [10, 20, 30, 40, 50, 20, 30, 20, 30]
print(a[2])
print(a[10])

2.4 列表的循环遍历
while 循环:
list1 = ['貂蝉', '大乔', '小乔']
# 定义计数器
i = 0
# 编写循环条件
while i < len(list1):
print(list1[i])
# 更新计数器
i += 1

for 循环(更推荐):
list1 = ['貂蝉', '大乔', '小乔']
for i in list1:
print(i)

2.5 列表的嵌套
列表的嵌套:列表中又包含列表,这种情况称为列表嵌套。
一维列表可以用来存储一维、线性的数据。
二维列表可以用来存储二维、表格化的数据,例如下表:
| 姓名 | 年龄 | 薪资 | 城市 |
|---|---|---|---|
| 小龙女 | 18 | 30000 | 北京 |
| 黄蓉 | 19 | 20000 | 上海 |
| 赵敏 | 20 | 10000 | 深圳 |
a = [
["小龙女", 18, 30000, "北京"],
["黄蓉", 19, 20000, "上海"],
["赵敏", 20, 10000, "深圳"],
]
print(a[1][0], a[1][1], a[1][2])

嵌套循环打印二维列表所有的数据:
a = [
["小龙女", 18, 30000, "北京"],
["黄蓉", 19, 20000, "上海"],
["赵敏", 20, 10000, "深圳"],
]
for i in a:
for j in i:
print(j, end=" ")
print()

2.6 列表的应用
下面我们通过一个双色球随机选号的例子为大家讲解列表的应用。双色球是由中国福利彩票发行管理中心发售的乐透型彩票,每注投注号码由 6 个红色球和 1 个蓝色球组成。红色球号码从 1 到 33 中选择,蓝色球号码从 1 到 16 中选择。每注需要选择 6 个红色球号码和 1 个蓝色球号码,如下所示。

下面,我们通过 Python 程序来生成一组随机号码。
"""
双色球随机选号程序
"""
import random
red_balls = [x for x in range(1, 34)]
selected_balls = []
# 添加 6 个红色球到选中列表
for _ in range(6):
# 生成随机整数代表选中的红色球的索引位置
index = random.randrange(len(red_balls))
# 将选中的球从红色球列表中移除
ball = red_balls.pop(index)
# 添加到选中列表
selected_balls.append(ball)
# 对选中的红色球排序
selected_balls.sort()
# 随机选择 1 个蓝色球
blue_ball = random.randrange(1, 17)
# 输出选中的红色球
for ball in selected_balls:
print(f'\033[031m{ball:0>2d}\033[0m', end=' ')
# 输出选中的蓝色球
print(f'\033[034m{blue_ball:0>2d}\033[0m')

我们还可以利用 random 模块提供的 sample 和 choice 函数来简化上面的代码,前者可以实现无放回随机抽样,后者可以实现随机抽取一个元素,修改后的代码如下所示。
"""
双色球随机选号程序
"""
import random
red_balls = [x for x in range(1, 34)]
blue_balls = [x for x in range(1, 17)]
# 从红色球列表中随机抽出 6 个红色球(无放回抽样)
selected_balls = random.sample(red_balls, k=6)
# 从蓝色球列表中随机抽出 1 个蓝色球
blue_ball = random.choice(blue_balls)
# 对选中的红色球排序
selected_balls.sort()
# 输出选中的红色球
for ball in selected_balls:
print(f'\033[031m{ball:0>2d}\033[0m', end=' ')
# 输出选中的蓝色球
print(f'\033[034m{blue_ball:0>2d}\033[0m')
如果要实现随机生成 N 注号码,我们只需要将上面的代码放到一个 N 次的循环中,如下所示。
import random
red_balls = [x for x in range(1, 34)]
blue_balls = [x for x in range(1, 17)]
n = int(input('生成几注号码:'))
for _ in range(n):
# 从红色球列表中随机抽出 6 个红色球(无放回抽样)
selected_balls = random.sample(red_balls, k=6)
# 从蓝色球列表中随机抽出 1 个蓝色球
blue_ball = random.choice(blue_balls)
# 对选中的红色球排序
selected_balls.sort()
# 输出选中的红色球
for ball in selected_balls:
print(f'\033[031m{ball:0>2d}\033[0m', end=' ')
# 输出选中的蓝色球
print(f'\033[034m{blue_ball:0>2d}\033[0m')

3 元组
3.1 为什么需要元组
num_list = [10, 20, 30]
num_list[0] = 100
那么在需要存储多个数据、且数据不允许更改的情况下,应该怎么办?
答:使用元组——元组可以存储多个数据,且元组内的数据不能修改。
- 列表属于可变序列,可以任意修改列表中的元素。
- 元组属于不可变序列,不能修改元组中的元素。
因此,元组没有增加元素、修改元素、删除元素的相关方法。
所以我们只需学习元组的创建和删除、元组中元素的访问和计数即可。元组支持如下操作:
- 索引访问
- 切片操作
- 连接操作
- 成员关系操作
- 比较运算操作
- 计数:元组长度
len()、最大值max()、最小值min()、求和sum()等。
3.2 元组的定义
元组特点:定义元组使用小括号,且使用逗号隔开各个数据,数据可以是不同的数据类型。
通过 () 创建元组
通过 () 创建元组,小括号可以省略。
# 创建一个空元组
a = ()
type(a)
a = (10, 20, 30)
# 可以省略括号
a = 10, 20, 30

如果元组只有一个元素,则必须后面加逗号。这是因为解释器会把 (1) 解释为整数 1,(1, ) 解释为元组。
a = (1)
print(type(a))
a = (1,)
print(type(a))
# 可省略括号
a = 1,
print(type(a))

通过 tuple() 创建元组
# 伪代码:展示语法
tuple(可迭代的对象)
# 创建一个空元组对象
b = tuple()
print(b)
b = tuple("abc")
print(b)
b = tuple(range(3))
print(b)
b = tuple([2, 3, 4])
print(b)

元组优化问题
a = tuple()
b = ()
a is b

a = tuple((1, ))
b = (1, )
a is b

3.3 元组的相关操作方法
由于元组中的数据不允许直接修改,因此其操作方法大多是查询方法。
| 函数 | 作用 |
|---|---|
元组[索引] |
根据索引下标查找元素 |
index() |
查找某个数据,如果数据存在返回对应的下标,否则报错,语法和列表、字符串的 index 方法相同 |
count() |
统计某个数据在当前元组出现的次数 |
len() |
统计元组中数据的个数 |
案例 1:修改元组中的某个元素。元组不可以修改,如果修改元组的值会抛出异常。
a = (20, 10, 30, 9, 8)
a[2] = 33

案例 2:访问元组中的某个元素。
a = (20, 10, 30, 9, 8)
print(a[1])
print(a[1: 3])
print(a[:4])

案例 3:查找某个元素在元组中出现的位置,存在则返回索引下标,不存在则直接报错。
nums = (10, 20, 30)
print(nums.index(20))

案例 4:统计某个元素在元组中出现的次数。
nums = (10, 20, 30, 50, 30)
print(nums.count(30))

案例 5:len() 主要用于求数据序列的长度,适用于字符串、列表、元组。
nums = (10, 20, 30, 50, 30)
print(len(nums))

列表的排序方法 list.sort() 会修改原列表对象,元组没有该方法。要对元组排序,只能使用内置函数 sorted(tupleObj),它会生成一个新的列表对象。
a = (20, 10, 30, 9, 8)
b = sorted(a)
print(b)

3.4 zip
zip(列表1, 列表2, …) 把多个列表对应位置的元素组合成元组,并返回这个 zip 对象。
a = [10, 20, 30]
b = [40, 50, 60, 99]
c = [70, 80, 90]
d = zip(a, b, c)
print(d)
print(list(d))

3.5 生成器推导式创建元组
从形式上看,生成器推导式与列表推导式类似,只是使用小括号。列表推导式直接生成列表对象,而生成器推导式生成的既不是列表也不是元组,而是一个生成器对象。
生成器对象可以转化成列表或元组,也可以用它的 __next__() 方法遍历,或直接作为迭代器使用。但无论哪种方式,元素访问结束后若要重新访问,必须重新创建该生成器对象。
s = (i for i in range(5))
s
print(tuple(s))
# 只能访问一次元素。第二次就为空了。需要再生成一次
print(list(s))
s = (x*2 for x in range(5))
print(s.__next__())
print(s.__next__())
print(s.__next__())
print(s.__next__())
print(s.__next__())
# 已经没有元素,再打印,抛出异常
print(s.__next__())


3.6 元组总结
- 元组的核心特点是:不可变序列。
- 元组的访问和处理速度比列表快。
- 与整数和字符串一样,元组可以作为字典的键,列表则永远不能作为字典的键使用。
4 字典
4.1 为什么需要字典
思考 1:比如要存储一个人的信息——姓名:Tom,年龄:20 周岁,性别:男——如何快速存储?
person = ['Tom', '20', '男']
思考 2:姓名、年龄、性别同属于一个人的基本特征;但如果用列表存储,它们就分散成了 3 个元素,显然不够贴合逻辑。有没有办法把它们保存在同一个元素中,让姓名、年龄、性别都作为这个元素的 3 个属性?
答:使用 Python 中的字典。
4.2 Python 中字典 (dict) 的概念
字典是键值对的可变容器(3.7+ 还同时是插入有序的,见 4.8)。字典中的每个元素都是一个键值对,包含:键对象和值对象。通过键对象可以快速获取、删除、更新对应的值对象。
列表通过下标数字找到对应的对象,字典则通过键对象找到对应的值对象。
键是任意的不可变数据,例如整数、浮点数、字符串、元组。
列表、字典、集合这些可变对象不能作为键。并且键不可重复。
值可以是任意数据,并且可重复。
特点:
- 符号为大括号(花括号)=>
{} - 数据以键值对形式出现 =>
{key: value},key 为键名、value 为值;同一字典中 key 必须唯一(类似索引下标) - 各个键值对之间用逗号隔开
定义:
# 有数据字典
dict1 = {'name': 'Tom', 'age': 20, 'gender': '男'}
# 空字典
dict2 = {}
dict3 = dict()
4.3 字典的创建
- 我们可以通过
{}、dict()来创建字典对象。
a = {'name': 'Tom', 'age': 18, 'job': 'programmer'}
# 使用 name=value 对初始化一个字典
b = dict(name='Tom', age=18, job='programmer')
# 使用可迭代对象的二元组
c = dict([["name", "Tom"], ("age", 18)])
d = {} # 空的字典对象
e = dict() # 空的字典对象
print("a", a)
print("b", b)
print("c", c)
print("d", d)
print("e", e)

dict(**kwargs) 使用 key=value 键值对对初始化一个字典。
dict(iterable, **kwarg) 使用可迭代对象和 key=value 对构造字典,不过可迭代对象的元素必须是一个二元结构。
dict(mapping, **kwarg) 使用一个字典构建另一个字典。
# 使用 name=value 对初始化一个字典
a = dict(name='Tom', age=18, job='programmer')
# 使用可迭代对象和 name=value 对构造字典
b = dict([("name", "Tom"), ("age", 18)], job='programmer')
# 使用一个字典构建另一个字典
c = dict(b, address='Beijing')
print("a", a)
print("b", b)
print("c", c)

d1 = {'name': '小龙女', 'age': 18}
# 使用一个字典构建另一个字典
d2 = dict(d1, salary=2000)
print(d2)

dict([('name', 'jack'), ('age', 18)])


把第二组键值对合并进来的正确写法是解包或 **kw 形式:
dict([('name', 'jack'), ('age', 18)], salary=2000, address='BeiJing')

还有一类错误是位置实参跟在关键字实参后面——这在任何函数调用里都是语法错误:
dict(name='小龙女', [('age', 17)])

- 通过
zip()创建字典对象。
k = ['name', 'age', 'job', 'a']
v = ['gaoqi', 18, 'teacher']
d = dict(zip(k, v))
print(d)

- 通过
fromkeys(iterable, value)创建值为空的字典。
- iterable:可迭代对象,将可迭代对象中的每一个元素作为字典的键。
- value:缺省值,如果没有设置 value 缺省值,默认将值设为 None。
k = ['name', 'age', 'job', 'a']
# 无缺省值 value
d = dict.fromkeys(k)
print(d)

k = ['name', 'age', 'job', 'a']
# 设置缺省值 value
d = dict.fromkeys(k, '我是缺省值')
print(d)

4.4 字典的增操作
给字典新增「键值对」,基本语法:
字典名称[key] = value
案例:定义一个空字典,然后添加 name、age、address 这 3 个 key。
# 1、定义一个空字典
person = {}
# 2、向字典中添加数据
person['name'] = '小龙女'
person['age'] = 18
person['address'] = '活死人墓'
# 3、使用print方法打印person字典
print(person)

4.5 字典的删操作
del 字典名称[key]
删除指定元素。
# 1、定义一个有数据的字典
person = {'name': '王大锤', 'age': 28, 'gender': 'male', 'address': '北京市海淀区'}
# 2、删除字典中的某个元素(如 gender)
del person['gender']
# 3、打印字典
print(person)

clear() 方法
清空字典中的所有 key。
# 1、定义一个有数据的字典
person = {'name': '王大锤', 'age': 28, 'gender': 'male', 'address': '北京市海淀区'}
print(person)
# 2、使用 clear() 方法清空字典
person.clear()
# 3、打印字典
print(person)

pop() 方法
pop() 删除指定键值对,并返回对应的「值对象」。
a = {'name': '小龙女', 'age': 18, 'job': 'programmer'}
b = a.pop('job')
print(a)
print(b)

popitem() 方法

4.6 字典的改操作
修改键值对
基本语法:
字典名称[key] = value
案例:定义一个字典,包含 name、age、address,修改 address 这个 key 的 value。
# 1、定义字典
person = {'name': '孙悟空', 'age': 600, 'address': '花果山'}
# 2、修改字典中的数据(address)
person['address'] = '东土大唐'
# 3、打印字典
print(person)

update()
使用 update() 将新字典中的所有键值对全部添加到旧字典对象上;若 key 重复,则直接覆盖。
a = {'name': '小龙女', 'age': 18, 'job': 'programmer'}
b = {'name': '赵敏', 'money': 1000, 'sex': '女'}
a.update(b)
print(a)

4.7 字典的查操作
① 查询方法:使用具体的某个 key 查询数据,如果未找到,则直接报错。
字典序列[key]
a = {'name': 'gaoqi', 'age': 18, 'job': 'programmer'}
print(a['name'])
print(a['sex'])

② 字典的相关查询方法
| 函数 | 作用 |
|---|---|
get(key[, default]) |
根据字典的 key 获取对应的 value 值,如果当前查找的 key 不存在则返回第二个参数(默认值),如果省略第二个参数,则返回 None |
setdefault(key[, default]) |
返回 key 对应的值 value;key 不存在,添加键值对,value 设置为 default,并返回 default;如果 default 没有设置,缺省为 None |
keys() |
返回字典所有键组成的视图对象 |
values() |
返回字典所有值组成的视图对象 |
items() |
返回可遍历的 (键, 值) 元组视图对象 |
get()
通过 get() 方法获得值。推荐使用。
优点是:指定键不存在,返回 None;也可以设定指定键不存在时默认返回的对象。推荐使用 get() 获取值对象。
案例 1:使用 get 获取字典中某个 key 的 value 值。
# 1、定义一个字典
cat = {'name': 'Tom', 'age': 5, 'address': '美国纽约'}
# 2、获取字典的相关信息
name = cat.get('name')
age = cat.get('age')
gender = cat.get('gender', 'male') # get(key, 默认值)
address = cat.get('address')
print(f'姓名:{name},年龄:{age},性别:{gender},住址:{address}')

setdefault(key, value)
setdefault(k, value) 方法用于设置 key 的默认值。
该方法接收两个参数,第 1 个参数表示 key,第 2 个参数 value 表示默认值。
如果 key 在字典中不存在,那么 setdefault() 方法会向字典中添加这个 key,并用第 2 个参数作为 key 的值。该方法会返回这个默认值。
如果未指定第 2 个参数,那么 key 的默认值是 None。如果字典中已经存在这个 key,setdefault() 不会修改 key 原来的值,而且该方法会返回 key 原来的值。
# 定义一个空字典(不要用内置类型名 dict 做变量名,这里改为 d)
d = {}
# 向字典中添加一个名为name的key,默认值是小龙女,输出结果:小龙女
print(d.setdefault('name', '小龙女'))
# 输出结果:{'name': '小龙女'}
print(d)
# 并没有改变name的值,输出结果:小龙女
print(d.setdefault('name', '黄蓉'))
# 输出结果:{'name': '小龙女'}
print(d)
# 向字典中添加一个名为age的key,默认值是None,输出结果:None
print(d.setdefault('age'))
# 输出结果:{'name': '小龙女', 'age': None}
print(d)

keys()
返回字典中的所有键对象。
案例 2:提取 person 字典中的所有 key。
# 1、定义一个字典
person = {'name': '貂蝉', 'age': 18, 'mobile': '13765022249'}
# 2、提取字典中的name、age以及mobile属性
print(type(person.keys()))
print(person.keys())

values()
返回字典中的所有值对象。
案例 3:提取 person 字典中的所有 value 值。
# 1、定义一个字典
person = {'name': '貂蝉', 'age': 18, 'mobile': '13765022249'}
# 2、提取字典中的貂蝉、18以及13765022249号码
print(type(person.values()))
print(person.values())

items()
返回可遍历的 (键, 值) 元组视图对象。
案例 4:使用 items() 方法提取数据。
# 1、定义一个字典
person = {'name': '貂蝉', 'age': 18, 'mobile': '13765022249'}
# 2、调用items方法获取数据
print(type(person.items()))
print(person.items())
# 3、结合for循环对字典中的数据进行遍历
for key, value in person.items():
print(f'{key}:{value}')



len()
返回字典中键值对的个数。
person = {'name': '貂蝉', 'age': 18, 'mobile': '13765022249'}
print(len(person))

in 成员运算
默认检测一个键是否在字典中。
person = {'name': '貂蝉', 'age': 18, 'mobile': '13765022249'}
# 默认检测键是否在字典中
print('name' in person)
# 检测值是否在字典中
print('貂蝉' in person.values())

4.8 字典的有序性
从存储机制上说,字典元素是按照 key 的 hash 值分布到哈希表中的——但这与「遍历顺序」是两回事,需要分开看:
d = {'a': 300, 'b': 20, 'c': 10, 'd': 50}
print(list(d.keys())) # 插入序:a 在前(即便 a 的值最大)
print(list(d.values()))
d2 = {'d': 50, 'c': 10, 'b': 20, 'a': 300}
print(list(d2.keys())) # 换一组数据:顺序 = 插入序,不是排序


Python 3.6 之前,在不同的机器上,甚至同一个程序分别运行 2 次,都无法确定不同 key 的先后顺序。


Python 3.6+ 会记录字典 key 的录入顺序,遍历时即按此顺序。
OrderedDict 与普通 dict 的两个实测差异:
from collections import OrderedDict
# 差异 1:相等性比较,OrderedDict 顺序敏感
od1 = OrderedDict(a=1, b=2)
od2 = OrderedDict(b=2, a=1)
print(od1 == od2) # False:顺序不同
print({'a': 1, 'b': 2} == OrderedDict(a=1, b=2)) # True:dict 混比不比顺序
# 差异 2:move_to_end(dict 没有的能力)
od = OrderedDict(a=1, b=2, c=3)
od.move_to_end('a')
print(list(od.keys()))
4.9 序列解包
序列解包可以用于元组、列表、字典,让我们能方便地对多个变量赋值。
x, y, z = (20, 30, 10)
print(f"x:{x}")
print(f"y:{y}")
print(f"z:{z}")
print("-" * 20)
(a, b, c) = (9, 8, 10)
print(f"a:{a}")
print(f"b:{b}")
print(f"c:{c}")
print("-" * 20)
[d, e, f] = [10, 20, 30]
print(f"d:{d}")
print(f"e:{e}")
print(f"f:{f}")
print("-" * 20)

序列解包用于字典时,默认对键操作;若需对键值对操作,则用 items();若需对值操作,则用 values()。
s = {'name': 'gaoqi', 'age': 18, 'job': 'teacher'}
# 默认对键进行操作
name, age, job = s
print(f"name:{name}")
print(f"age:{age}")
print(f"job:{job}")
print("-" * 20)
# 对键值对进行操作
name, age, job = s.items()
print(f"name:{name}")
print(f"age:{age}")
print(f"job:{job}")
print("-" * 20)
# 对值进行操作
name, age, job = s.values()
print(f"name:{name}")
print(f"age:{age}")
print(f"job:{job}")
print("-" * 20)

4.10 封装和解构
基本概念
Python 等式右侧出现逗号分隔的多值时,会把这几个值封装到元组中,这种操作称为封装(packing)。
t1 = 1, 2
print(type(t1)) # 什么类型
t2 = (1, 2)
print(type(t2))

Python 等式右侧是容器类型、左侧是逗号分隔的多个标识符时,右侧容器中的数据会与左侧标识符一一对应,这种操作称为解构(unpacking)。
x, y = (1, 2)
print(f"x:{x}") # 1
print(f"y:{y}") # 2

从 Python3 开始,对解构做了很大的改进,现在用起来已经非常的方便快捷。
封装和解构是非常方便的提取数据的方法,在 Python、JavaScript 等语言中应用极广。
# 交换数据(传统写法:需要临时变量)
x = 4
y = 5
temp = x
x = y
y = temp
# 封装和解构,交换(Pythonic 写法)
x = 10
y = 11
x, y = y, x
简单解构
# 左右个数相同
a, b = 1, 2
a, b = (1, 2)
a, b = [1, 2]
a, b = [10, 20]
a, b = {10, 20} # 集合(无序但可迭代)也可以解构
a, b = {'a': 10, 'b': 20} # 字典解构得到的是键
[a, b] = (1, 2)
[a, b] = 10, 20
(a, b) = {30, 40}
那么,左右个数不一致可以吗?
a, b = (10, 20, 30)

例:如何将下列所有数字都解构出来?
(1, [2, 3, 4], 5)
下面这种写法显然不对,左右个数不一致:
a, b, c, d, e = (1, [2, 3, 4], 5)

正确写法(嵌套解构):
a, [b, c, d], e = (1, [2, 3, 4], 5)
print(a, b, c, d, e)

剩余变量解构
在 Python 3.0 中增加了剩余变量解构(starred assignment)。
a, *rest, b = [1, 2, 3, 4, 5]
print(a, b)
print(type(rest), rest) # <class 'list'> [2, 3, 4]

标识符 rest 将尽可能收集剩余的数据组成一个列表。
a, *rest = [1, 2, 3, 4, 5]
print(a, rest)
a, *rest, b = [1, 2, 3, 4, 5]
print(a, rest, b) # 内容是什么?
*rest, b = [1, 2, 3, 4, 5]
print(rest, b) # 内容是什么?

*rest = [1, 2, 3, 4, 5]

a, *r1, *r2, b = [1, 2, 3, 4, 5]

a, *_, b = [1, 2, 3, 4, 5]
print(_)

_, *b, _ = [1, 2, 3]
print(b)

_ 是合法的标识符,它在这里没有什么可读性,作用就是表示不关心这个变量的值。有人把它称作丢弃变量。
(另外在交互式解释器里,_ 会自动保存上一次表达式的结果,注意两者不要混用。)
4.11 字典核心底层原理(重要)
字典对象的核心是散列表(哈希表)。散列表是一个稀疏数组(总是有空白元素的数组),数组的每个单元叫做 bucket(桶)。
每个 bucket 有两部分:一个是键对象的引用,一个是值对象的引用。
由于所有 bucket 结构和大小一致,我们可以通过偏移量来读取指定 bucket。

将一个键值对放进字典的底层过程
a = {}
a["name"] = "小龙女"
假设字典 a 对象创建完后,数组长度为 8:

我们要把键值对 "name": "小龙女" 放进字典 a,第一步需要计算键 name 的散列值。Python 中可用 hash() 计算。
由于数组长度为 8,我们可以拿计算出的散列值的最右边 3 位数字作为偏移量,即 "101",十进制是数字 5。我们查看偏移量 5,对应的 bucket 是否为空。如果为空,则将键值对放进去。如果不为空,则依次取右边 3 位作为偏移量,即 "100",十进制是数字 4。再查看偏移量为 4 的 bucket 是否为空。直到找到为空的 bucket 将键值对放进去。流程图如下:


扩容
Python 会根据散列表的拥挤程度扩容。
扩容指的是:创建更大的数组,将原有内容拷贝到新数组中。装载元素数接近容量的 2/3 时,数组就会扩容。
根据键查找「键值对」的底层过程
明白了键值对如何存储到数组后,根据键对象取到值对象就很好理解了。
a = {"name": "gaoqi"}
print(a.get("name"))
调用 a.get("name"),就是根据键 name 查找到「键值对」,从而找到值对象 gaoqi。
第一步,我们仍然要计算 name 对象的散列值(同样地,具体哈希值每次运行都不同,见前文勘误)。
和存储的底层流程算法一致,也是依次取散列值的不同位置的数字。假设数组长度为 8,我们可以拿计算出的散列值的最右边 3 位数字作为偏移量,即 "101",十进制是数字 5。我们查看偏移量 5,对应的 bucket 是否为空。如果为空,则返回 None。如果不为空,则将这个 bucket 的键对象计算对应散列值,和我们的散列值进行比较,如果相等,则将对应「值对象」返回。如果不相等,则再依次取其他几位数字,重新计算偏移量。依次取完后,仍然没有找到,则返回 None。流程图如下:

用法总结
-
键必须可散列。
- 数字、字符串、元组,都是可散列的。
- 自定义对象需要支持下面三点:
- 支持
hash()函数。 - 支持通过
__eq__()方法检测相等性。 - 若
a == b为真,则hash(a) == hash(b)也为真。
- 支持
-
字典在内存中开销巨大,典型的空间换时间。
-
键查询速度很快,平均 O(1)。
-
不要在遍历字典的同时进行字典的增删操作。
4.12 综合案例:通讯录管理系统
需求:开发一个通讯录的管理系统,主要用于实现存储班级中同学的信息(姓名、年龄、电话)。
知识点:列表、字典、死循环
延伸:在 Python 中,我们可以使用字典来保存一个人的基本信息。但是如果想保存多个人的信息,我们必须把列表和字典结合起来。
students = [0, 1, 2]
student = {'name': '刘备', 'age': 18, 'mobile': '10086'}
# 组装:
students = [
{'name': '刘备', 'age': 18, 'mobile': '10086'},
{'name': '关羽', 'age': 17, 'mobile': '10000'},
{'name': '张飞', 'age': 16, 'mobile': '10010'}
]
为什么需要死循环:
当我们选中某个菜单时,功能一旦执行完毕,则整个程序就执行结束了。为了保存程序可以一直运行下去,可以模拟死循环的效果,让程序一直运行下去。
while True:
...
要求:正常情况下,通讯录管理系统应该有 4 个功能:增删改查。
# 1、定义一个列表,将来用于存储所有学员的通讯信息
students = []
# 2、打印功能菜单
print('-' * 40)
print('欢迎使用传智教育通讯录管理系统V1.0')
print('[1] 增加学员信息')
print('[2] 删除学员信息')
print('[3] 退出系统')
print('-' * 40)
while True:
# 3、提示用户进行相关操作
user_num = int(input('请输入您要进行的操作编号:'))
if user_num == 1:
# 4、提示用户输入学员的信息
student = {}
student['name'] = input('请输入学员的姓名:')
student['age'] = int(input('请输入学员的年龄:'))
student['mobile'] = input('请输入学员的电话:')
# 5、把学员信息保存在列表中
students.append(student)
print(students)
elif user_num == 2:
name = input('请输入要删除的学员姓名:')
# 6、遍历所有学员信息,找到即删并跳出
for i in students:
if i['name'] == name:
# 从列表中删除整个学员(字典)
students.remove(i)
print('删除成功')
break
else:
# for 循环正常走完(没被 break)说明没找到
print('您要删除的学员信息不存在')
elif user_num == 3:
print('感谢您使用传智教育通讯录管理系统V1.0')
break
else:
print('输入错误,请重新输入要操作的编号')
5 集合
5.1 什么是集合
集合是无序可变容器,元素不能重复。
实际上,集合底层是字典实现,集合的所有元素都是字典中的键对象****,因此是不能重复且唯一的。
集合(set)是一个无序、不重复元素的序列。
- 天生去重
- 无序(按哈希值分布,不保证任何顺序)
5.2 集合的定义
在 Python 中,可以用一对花括号 {} 或 set() 方法定义集合;但如果要定义空集合,则只能用 set()。
用 set() 可以把列表、元组等可迭代对象转成集合;若原数据存在重复,则只保留一个。
# 定义一个集合
s1 = {10, 20, 30, 40, 50}
print(s1)
print(type(s1))
# 定义一个集合:集合中存在相同的数据
s2 = {'刘备', '曹操', '孙权', '曹操'}
print(s2)
print(type(s2))
print('################')
# 使用 set() 方法将可迭代对象转换成集合
# 如果原来数据存在重复数据,则只保留一个。
l1 = [1, 2, 3, 3, 5, 6, 6]
tu = 1, 2, 3, 3, 5, 6, 6
s3 = set(l1)
s4 = set(tu)
print(s3)
print(s4)
print('################')
# 定义空集合
s5 = {}
s6 = set()
print(type(s5))
print(type(s6))

5.3 集合操作的相关方法(增删查)
集合的增操作
add()方法:向集合中增加一个元素(单一)。
students = set()
students.add('小龙女')
students.add('赵敏')
print(students)

update()方法:向集合中增加可迭代类型的数据(字符串、列表、元组、字典)。
students = set()
list1 = ['刘备', '关羽', '赵云']
students.update(list1)
print(students)

students = set()
students.add('刘德华')
students.add('黎明')
# 使用 update 新增元素
students.update('蔡徐坤')
print(students)

集合的删操作
remove()方法:删除集合中的指定数据,如果数据不存在则报错。discard()方法:删除集合中的指定数据,如果数据不存在也不会报错。pop()方法:随机删除集合中的某个数据,并返回这个数据。
# 1、定义一个集合
products = {'萝卜', '白菜', '水蜜桃', '奥利奥', '西红柿', '凤梨'}
# 2、使用 remove 方法删除白菜这个元素
products.remove('白菜')
print(products)
# 3、使用 discard 方法删除未知元素(不报错)
products.discard('玉米')
print(products)
# 4、使用 pop 方法随机删除某个元素
del_product = products.pop()
print(del_product)
print(products)
# 5、使用 remove 方法删除不存在元素(报 KeyError)
products.remove('小龙女')

集合中的查操作
in:判断某个元素是否在集合中,如果在,则返回 True,否则返回 False。not in:判断某个元素不在集合中,如果不在,则返回 True,否则返回 False。
# 定义一个set集合
s1 = {'刘帅', '英标', '高源'}
# 判断刘帅是否在s1集合中
if '刘帅' in s1:
print('刘帅在s1集合中')
else:
print('刘帅没有出现在s1集合中')

- 集合的遍历操作
for i in 集合:
print(i)
5.4 集合中的交集、并集与差集特性
在 Python 中,我们可以使用 & 来求两个集合的交集:

在 Python 中,我们可以使用 | 来求两个集合的并集:

在 Python 中,我们可以使用 - 来求两个集合的差集:

# 求集合中的交集、并集、差集
s1 = {'刘备', '关羽', '张飞', '貂蝉'}
s2 = {'袁绍', '吕布', '曹操', '貂蝉'}
# 求两个集合中的交集
print(s1 & s2)
# 求两个集合中的并集
print(s1 | s2)
# 求两个集合中的差集
print(s1 - s2)
print(s2 - s1)

5.5 核心特性对比
| 特性 | 列表 (Sequence) | 字典 (Mapping) | 集合 (Set) |
|---|---|---|---|
| 是否有序 | ✅ 插入顺序 | ✅ 插入顺序 (3.7+) | ❌ 无序(按哈希值分布) |
| 元素重复 | ✅ 允许 | 键唯一,值可重复 | ❌ 自动去重 |
| 索引访问 | ✅ lst[0] |
✅ dct[key] |
❌ 只能迭代或成员判断 |
| 可变性 | ✅ 可变 | ✅ 可变 | set 可变 / frozenset 不可变 |
| 底层实现 | 动态数组 | 哈希表 | 哈希表(只存键,无值) |
5.6 底层实现说明
集合和字典的底层均基于哈希表。
- 字典:每个条目存储
(hash, key, value)三元组。 - 集合:每个条目只存储
(hash, key),因为无需关联值。
正是由于哈希表的特性,集合拥有 O(1) 平均时间复杂度的插入、删除和成员检测,远超列表的 O(n)。
5.7 典型应用场景
# 1. 快速去重
lst = [1, 2, 2, 3]
unique = list(set(lst))
print(unique) # [1, 2, 3](顺序不保证)
# 2. 数学集合运算
a = {1, 2, 3}
b = {2, 3, 4}
print(a & b) # 交集 {2, 3}
print(a | b) # 并集 {1, 2, 3, 4}
print(a - b) # 差集 {1}
# 3. 极速成员检查(比列表快数百倍)
large_set = set(range(10**6))
if 999999 in large_set: # 哈希查找,极快
print('found')
5.8 特殊子类型:不可变集合 frozenset
由于普通 set 是可变的(内部哈希值可能随增删变化),它不能作为字典的键或放入另一个集合。此时需使用 frozenset:
d = {{1, 2}: "value"}
# 正确:frozenset 可哈希,可以做键
d = {frozenset({1, 2}): "value"}
print(d[frozenset({2, 1})]) # 值相等即可取出
5.9 总结
集合是基于哈希表的无序、唯一元素容器,虽不支持索引访问,但在去重、成员检测、数学关系运算场景下具有不可替代的性能优势。
6 序列中的公共方法
6.1 什么是公共方法
所谓公共方法,就是能操作大部分数据序列的方法。
6.2 常见公共方法 1
| 运算符 | 描述 | 支持的容器类型 |
|---|---|---|
+ |
合并 | 字符串、列表、元组 |
* |
复制 | 字符串、列表、元组 |
in |
元素是否存在 | 字符串、列表、元组、字典、集合 |
not in |
元素是否不存在 | 字符串、列表、元组、字典、集合 |
案例 1:+ 合并。
# 1、+ 加号,代表两个序列之间的连接与整合
str1 = 'hello'
str2 = 'world'
print(str1 + str2)
# 2、定义两个列表,对其数据进行整合
list1 = ['刘备', '关羽']
list2 = ['诸葛亮', '赵云']
print(list1 + list2)
# 3、定义两个元组,对其数据进行整合
tuple1 = (10, 20)
tuple2 = (30, 40)
print(tuple1 + tuple2)

案例 2:* 复制。
# 1、字符串与乘号的关系
print('-' * 40)
print('传智教育Python管理系统V1.0')
print('-' * 40)
# 2、列表与乘号的关系
list1 = ['*']
print(list1 * 10)
# 3、元组与乘号的关系
tuple1 = (10,)
print(tuple1 * 10)

案例 3:in 与 not in 方法。
ips = ['192.168.10.11', '10.1.1.100', '172.15.184.31']
if '10.1.1.100' in ips:
print('列表中元素已存在')
else:
print('列表中元素不存在')

6.3 常见公共方法 2
| 函数 | 描述 |
|---|---|
len() |
计算容器中元素个数 |
del 或 del() |
根据索引下标删除指定元素 |
max() |
返回容器中元素最大值 |
min() |
返回容器中元素最小值 |
range(start, end, step) |
生成从 start 到 end(不包含)的数字,步长为 step,供 for 循环使用 |
enumerate() |
将一个可遍历的数据对象组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中 |
案例 1:len() 获取字符串、列表、元组、字典、集合的长度。
# 定义一个字符串
str1 = 'hello world'
print(f'字符串的长度为{len(str1)}')
# 定义一个列表
list1 = [10, 20, 30, 40, 50]
print(f'列表的长度为{len(list1)}')
# 定义一个元组
tuple1 = (1, 2, 3)
print(f'元组的长度为{len(tuple1)}')
# 定义一个字典
dict1 = {'name': '哆啦A梦', 'gender': 'male', 'address': '东京'}
print(f'字典的长度为{len(dict1)}')
# 定义一个集合
set1 = {10, 20, 30}
print(f'集合的长度为{len(set1)}')

案例 2:del 方法,用于删除序列中指定的元素(根据索引下标)。
# 定义一个列表
list1 = ['吕布', '董卓', '貂蝉']
# 使用 del 方法删除董卓
del list1[1]
print(list1)
# 定义一个字典
dict1 = {'name': '白龙马', 'age': 23, 'address': '东海龙宫'}
# 使用 del 方法删除 age
del dict1['age']
print(dict1)
del (dict1['name'])
print(dict1)

案例 3:求某个序列中元素的最大值和最小值(原文用 input() 交互输入,这里改为固定数据以便给出实跑输出)。
list1 = [15, 42, 8]
max_num = max(list1)
min_num = min(list1)
print(f'最大值:{max_num}')
print(f'最小值:{min_num}')

案例 4:enumerate(),把一个序列类型中的每个元素构造成 (index, value) 结构,然后结合 for 循环进行遍历。
list1 = [10, 20, 30, 40, 50]
n = 1
for i in list1:
print(f'第{n}个数:{i}')
n += 1
print('-' * 40)
for key, value in enumerate(list1):
print(f'第{key + 1}个数:{value}')

6.4 序列类型之间的相互转换
list() 方法:把某个序列类型的数据转化为列表。
# 1、定义元组类型的序列
tuple1 = (10, 20, 30)
print(list(tuple1))
# 2、定义一个集合类型的序列
set1 = {'a', 'b', 'c', 'd'}
print(list(set1))
# 3、定义一个字典
dict1 = {'name': '刘备', 'age': 18, 'address': '蜀中'}
print(list(dict1))

tuple() 方法:把某个序列类型的数据转化为元组。
# 定义一个列表类型的数据
list1 = ['a', 'b', 'c', 'd']
print(tuple(list1))
# 定义一个集合类型的数据
set1 = {10, 20, 30, 40}
print(tuple(set1))

set() 方法:将某个序列转换成集合(但是要注意两件事 => ① 集合可以快速完成列表去重 ② 集合不支持下标)。
# 定义一个列表类型的数据
list1 = ['a', 'b', 'c', 'd', 'a']
print(set(list1))
# 定义一个元组类型的数据
tuple1 = (10, 20, 30, 40)
print(set(tuple1))

7 元组的不可变与字典键对象的不可变
元组的不可变和字典键对象的不可变一样吗?
tu = ('a', 1, [1, 2, 3])
print(tu)

d = {'a': 1, 1: 2, [1, 2, 3]: 3}

d = {'a': 1, 1: 2, ([1, 2, 3], ): 3}
print(d)

同样是要求「不可变」,但元组可以放列表,字典却不行。简单直接的回答是:
- 元组不可变:指元组自身容器内元素的引用关系不可变(不能增删改元素)。
- 字典不可变:在 Python 标准类型中,字典本身都是可变的;所谓「不可变」通常指字典的键(Key)必须是不可变对象。
下面从三个维度拆解它们的区别。
hash 的详细讲解:hash详解
7.1 核心区别速查表
| 对比维度 | 元组 (tuple) |
字典 (dict) |
|---|---|---|
| 自身容器状态 | 不可变 (Immutable) | 可变 (Mutable) |
| 能否增删元素 | ❌ 不能 append / pop |
✅ 可以 d['new'] = val / del |
| 能否修改元素值 | ❌ tup[0] = x 报错 |
✅ d['key'] = new_val |
| 对内部元素的要求 | 无限制(可以是可变列表) | 键必须可哈希(不可变或其子类) |
7.2 元组的不可变:浅层不可变
元组的不可变是针对元组这个容器本身的引用结构,而不是针对它内部包含的对象。
# 元组本身不可变:不能修改引用的指向
t = (1, 2, 3)
# t[0] = 4 # TypeError: 'tuple' object does not support item assignment
# 但如果元组里放的是可变对象(如列表),该对象的内容是可以改变的
t_with_list = (1, [2, 3], 4)
t_with_list[1].append(5)
print(t_with_list) # (1, [2, 3, 5], 4) —— 元组的内容“变了”,但引用的地址没变
本质:元组的不可变是指指针数组只读。元组存储的是对象的引用,它保证不改变这些引用指向哪里,但管不了被指向对象内部发生了什么。
7.3 字典的不可变:针对键的要求
在 Python 标准库中,dict 本身是完全可变的。「字典不可变」这一说法,通常是指:
Python 规定:字典的键(Key)必须是可哈希(hashable)的对象。
为什么键必须不可变?
因为字典底层基于哈希表。写入 d[key] = value 时,Python 会计算 hash(key) 来决定存放位置。
- 若
key是可变对象(比如列表),它内容一变,哈希值就变了。 - 哈希值一变,字典就找不到这个键原本存在的位置,会导致内存泄漏或查找错误。
错误示例 vs 正确示例
# 1. 错误:试图用可变对象做键
bad_dict = {}
key = [1, 2] # 列表是可变对象,没有 __hash__ 方法
# bad_dict[key] = 3 # TypeError: unhashable type: 'list'
# 2. 正确:元组做键(前提是元组内部全为不可变对象)
good_dict = {}
key_tuple = (1, 2) # 元组不可变,且内部只有数字,是可哈希的
good_dict[key_tuple] = "OK"
print(good_dict[key_tuple])
# 3. 陷阱:元组内部有可变对象,依然不能做键
tricky_key = (1, [2, 3])
# hash(tricky_key) # TypeError: unhashable type: 'list'
特殊情况:不可变字典视图 MappingProxyType
如果确实需要一个只读、不可变的字典视图(防止他人修改配置),Python 标准库提供了 types.MappingProxyType:
from types import MappingProxyType
original = {"debug": True, "timeout": 30}
read_only = MappingProxyType(original)
print(read_only["debug"])
# read_only["debug"] = False # TypeError: 'mappingproxy' object does not support item assignment
# 注意:这个只读是代理视图,如果修改原字典,视图也会跟着变
original["timeout"] = 99
print(read_only["timeout"])
7.4 总结
| 场景 | 解释 |
|---|---|
| 元组不可变 | 锁住的是结构。列表是书架,可以随便抽换书;元组是焊死的钢筋架,架子本身不能动,但架子上如果挂了个篮子(列表),篮子里可以放东西。 |
| 字典键的要求 | 锁住的是标签。字典像图书馆的检索卡片,卡片的编号(哈希值)必须永久有效且唯一。可变对象像活页笔记本,页码总在变,没法当检索卡用。 |
| 字典本身 | 图书馆本身的书架布局(键值对映射关系)是可以随时重新规划调整的,是可变的。 |
7.5 延伸:Python 3.7+ 字典的「顺序性」
现代 Python 字典是插入有序的,但这属于元素顺序,与可变性无关。字典依然可以随时增加和删除键值对。
8 各个数据结构比较
线性数据结构(有序,可索引):list、tuple、str
非线性数据结构(无序,不可索引):dict、set
如果内存中是线性列表,你要做的操作里若用到了遍历,时间复杂度为 O(n),n 越大效率越低,耗时可达微秒到毫秒级——例如 index、count 方法。
如果内存中是集合(或字典),则无论数据规模多大,检索元素的时间都不随规模变化,时间复杂度为 O(1),效率极高,可达纳秒(ns)级——例如 d[key]、in。
9 术语表(重要英文单词)
9.1 容器与结构
| 术语 | 全拼 / 说明 | 释义 |
|---|---|---|
| mapping | 映射 | 键到值的对应关系;dict 是 Python 的内置映射类型 |
| hashable | — | 可哈希;有稳定 hash() 值且可比较相等的对象,能做字典键/集合元素 |
| hash table | 散列表/哈希表 | 按 hash(key) 计算存储位置的表结构,O(1) 平均查找 |
| bucket | 桶 | 哈希表中的单个存储单元,存放键(和值)的引用 |
| open addressing | 开放寻址 | 哈希冲突时按探测序列找下一个空槽的策略,CPython 字典采用 |
| collision | 碰撞 | 不同键算出同一槽位;探测法解决 |
| load factor | 装载因子 | 已占槽位/总槽位;dict 约 2/3 触发扩容 |
| compact dict | 紧凑字典 | 3.6+ CPython 布局:indices 数组 + 连续 entries,省内存且保插入序 |
| view | 视图 | keys()/values()/items() 返回的动态窗口,字典变它跟着变 |
| insertion order | 插入顺序 | 元素被放入的先后顺序;3.7+ dict 遍历顺序的保证 |
| LIFO | Last In, First Out | 后进先出;3.7+ dict.popitem() 弹出最后插入的键值对 |
| immutable | 不可变 | 创建后不能增删改;tuple/str/frozenset 属于此类 |
| shallow immutability | 浅层不可变 | 容器引用关系只读,但内部可变对象的内容仍可改 |
| packing | 封装 | 右侧多值自动打包成元组,如 t = 1, 2 |
| unpacking | 解包/解构 | 把容器元素一次性赋给多个变量,如 a, b = (1, 2) |
| starred assignment | 星号赋值 | a, *rest, b = [...],剩余元素收集为列表 |
9.2 方法与函数
| 术语 | 全拼 / 说明 | 释义 |
|---|---|---|
fromkeys() |
— | 用给定的键序列建字典,所有键共用同一个默认值 |
setdefault() |
— | 取键值;键不存在则插入默认值并返回 |
popitem() |
— | 弹出键值对;3.7+ 按 LIFO 弹出最后插入项 |
update() |
— | 批量合并键值对,键冲突时右方覆盖 |
keys() / values() / items() |
— | 分别返回键/值/键值对的视图对象 |
discard() |
— | 删除指定元素;不存在不报错 |
remove() |
— | 删除指定元素;不存在抛 KeyError |
add() |
— | 向集合加入单个元素 |
| frozenset | frozen set,冻结集合 | 不可变集合,可哈希,能做字典键或放进另一个集合 |
zip() |
— | 多个可迭代对象按位置配对成元组,长度取最短 |
enumerate() |
— | 生成 (index, value) 对;start 参数指定起始序号 |
MappingProxyType |
— | types 模块的只读字典代理视图 |
OrderedDict |
— | collections 中的有序字典;move_to_end()、顺序敏感相等性是它相对 dict 的增量 |
10 系列导航
11 参考链接
- Python 3 官方文档(中文)
- 内置类型 — dict / set / frozenset
dict.popitem的 LIFO 说明- 字典视图对象
collections.OrderedDicttypes.MappingProxyType- PEP 372 — 有序字典
- PEP 448 — 更宽松的解包语法(星号表达式)
- PEP 584 — 字典合并运算符
| - Time complexity(CPython 各容器操作复杂度)
分类:Python

浙公网安备 33010602011771号