目录
- 一、四种内置数据结构总览
- [二、列表 list:可变的有序容器](#二、列表 list:可变的有序容器)
- [三、元组 tuple:不可变的"常量序列"](#三、元组 tuple:不可变的"常量序列")
- [四、字典 dict:键值对的查找表](#四、字典 dict:键值对的查找表)
- [五、序列:list / tuple / str 的共同语言](#五、序列:list / tuple / str 的共同语言)
- [六、集合 set:去重与集合运算](#六、集合 set:去重与集合运算)
- [七、引用陷阱:= 是"贴标签"不是"复制"](#七、引用陷阱:= 是"贴标签"不是"复制")
一、四种内置数据结构总览
为什么 Python 不直接给一个"万能容器"?因为不同场景对数据的增删频率、查找方式、是否允许改动要求完全不同。专门的结构各管一摊,用对了既快又清晰。
| 结构 | 可变 | 有序 | 元素约束 | 典型用途 |
|---|---|---|---|---|
list 列表 |
✅ | ✅ | 任意对象 | 动态有序集合(购物车、待办列表) |
tuple 元组 |
❌ | ✅ | 任意对象 | 不可变配置、函数多返回值 |
dict 字典 |
✅(值) | ❌ | 键必须不可变且唯一 | 键值映射(联系人、配置项) |
set 集合 |
✅ | ❌ | 元素必须可哈希(不可变) | 去重、交集/并集/差集运算 |
一句话记忆:列表管"排队",元组管"定值",字典管"查名字",集合管"去重和算关系"。
二、列表 list:可变的有序容器
为什么用它
程序里最常见的需求就是"维护一组会变化、且保持先后次序的数据"------购物清单、日志、待处理任务。列表同时满足两点:有序 (按索引取)和可变(原地增删改),所以最常用。
创建与基本操作
python
# data_structures/list_demo.py
fruits = ['pear', 'orange', 'apple', 'banana']
# 增:末尾追加 / 指定位置插入
fruits.append('melon') # ['pear', 'orange', 'apple', 'banana', 'melon']
fruits.insert(0, 'berry') # 插到索引 0:['berry', 'pear', ...]
# 删:三种姿势
fruits.pop() # 默认删最后一个,返回被删元素
fruits.pop(1) # 删索引 1
fruits.remove('apple') # 删第一个匹配的值,找不到抛 ValueError
del fruits[2] # 按索引删,等价于 pop(2) 但不返回值
# 改:直接按索引赋值
fruits[0] = 'red apple'
# 查与排序
print(fruits[2]) # 索引访问
fruits.sort() # 原地排序(返回 None)
fruits.reverse() # 原地反转
要点:
list用方括号[]创建,元素类型可以混搭(但不推荐混,除非真需要)。append是 O(1),insert(0, ...)是 O(n)(要挪后面所有元素),高频头插别用列表。sort()/reverse()是原地操作 ,不返回新列表------写成x = fruits.sort()会得到None。
常用方法速查
| 方法 | 作用 | 备注 |
|---|---|---|
append(x) |
末尾追加 | O(1) |
insert(i, x) |
索引 i 处插入 | O(n) |
pop([i]) |
删索引 i(默认末尾) | 返回被删元素 |
remove(x) |
删第一个值为 x 的项 | 找不到报错 |
index(x) |
返回 x 首次出现的索引 | 找不到报错 |
count(x) |
统计 x 出现次数 | --- |
sort() / reverse() |
原地排序 / 反转 | 不返回新列表 |
clear() |
清空 | --- |
小结 :列表是"可变有序"的万金油容器。记住
append快、insert(0)慢、sort/reverse原地不动返回None这三点,就避开了 80% 的坑。
三、元组 tuple:不可变的"常量序列"
为什么有 list 还要 tuple
列表什么都能改,但有些数据就不该被改 :一个二维坐标 (x, y)、一份固定配置、数据库读出来的一行记录。用元组包起来,等于给数据上一道"完整性约束"------谁改谁报错,解释器帮你盯着。另外,元组因为不可变,可以当作字典的键,列表不行。
创建与陷阱
python
# data_structures/tuple_demo.py
zoo = ('tiger', 'lion', 'zebra') # 括号可省,但建议保留,语义更清楚
empty = () # 空元组
single = ('deer',) # 单元素元组:逗号不能省!
# 下面这行不是元组,而是带括号的字符串 'deer'
not_a_tuple = ('deer')
print(zoo[2]) # 'zebra',索引访问和列表一样
for animal in zoo:
print(animal)
single = ('deer',) 这个逗号是最容易写错的地方 :少了逗号,Python 只当它是普通括号表达式,type(single) 会是 str 而不是 tuple。
小结 :元组 = 不可变列表。需要"定值"或要当字典键时用它;单元素别忘了加尾逗号
('x',)。
四、字典 dict:键值对的查找表
为什么用它
如果用列表存"名字→电话",查一个人得从头遍历,慢。字典按键直接定位值,查找接近 O(1)------本质是哈希表。所有"通过某个标识取对应信息"的场景(联系人、配置、缓存、JSON)都该用字典。
创建与增删改查
python
# data_structures/dict_demo.py
contact = {'z3': '119', 'l4': '110', 'w5': '114'}
# 增 / 改:键存在就覆盖,不存在就新增
contact['l7'] = '120'
contact['l7'] = '111'
# 查:直接索引键不存在会抛 KeyError,优先用 get
print(contact['l4']) # '110'
print(contact.get('l4')) # '110'
print(contact.get('lao8', 'not exists')) # 键不存在返回默认值,不报错
# 删
contact.pop('l7') # 返回被删的值
# del contact['l7'] # 等价,但不返回值
# 迭代:键值对 / 只键 / 只值
for k, v in contact.items():
print(k, v)
for k in contact.keys():
print(k)
for v in contact.values():
print(v)
要点:
- 键必须不可变且唯一 :键可以是
str/int/tuple等可哈希对象,不能是list;重复键后者覆盖前者。 - 排序:字典本身无序,要按 key 排序只能
for k in sorted(contact): print(k, contact[k])。 - 值没有限制,可以是列表、字典甚至函数。
小结 :字典是"用名字查东西"的标准结构。键要不可变、取值优先
get防KeyError,这是字典的两大铁律。
五、序列:list / tuple / str 的共同语言
为什么单独讲"序列"
列表、元组、字符串看上去不一样,但都遵守同一套"序列协议":能索引、能切片、能测成员、能拼接、能求长度。理解"序列"这个抽象,这三种类型不用分别背------学会一个,三个都会。
python
# data_structures/sequence_demo.py
zoo = ('tiger', 'lion', 'zebra', 'elephant', 'fox', 'wolf', 'puma')
# 1. 资格测试
'tiger' in zoo # True
'lion' not in zoo # False
# 2. 索引(负索引从末尾算)
zoo[3] # 'elephant'
zoo[-1] # 'puma'
# 3. 切片 [start:end:step],左闭右开
zoo[2:5] # ('zebra', 'elephant', 'fox')
zoo[2:] # 从 2 到结尾
zoo[:5] # 从 0 到 5
zoo[2:-1] # 去掉头尾
zoo[-3:-5:-1] # 负步长反向切:('wolf', 'fox')
zoo[2:5:2] # 每隔一个取:('zebra', 'fox')
# 4. 拼接与重复(都返回新对象,原序列不变)
zoo + ('cat', 'dog') # 拼接
zoo * 2 # 重复
len(zoo) # 7
字符串同样是序列,上面所有操作照用:'abcdefg'[2:-1:2]、'hi' * 3、'c' in 'abc'。
小结 :序列是 list/tuple/str 的共同抽象。切片
[start:end:step]左闭右开、可负;+拼接、*重复都返回新对象,不动原数据。
六、集合 set:去重与集合运算
为什么用它
要"去掉重复元素"或做"哪些人既在 A 组又在 B 组"这类关系运算,用列表得手写循环,又慢又丑。集合天生无序、元素唯一,专为去重 和数学集合运算 设计;而且 in 成员测试是 O(1),比列表的 O(n) 快得多。
python
# data_structures/set_demo.py
bri = set(['brazil', 'russia', 'india'])
bric = bri.copy() # 复制一份,避免改到原集合
bric.add('China') # 添加元素
# 集合运算
bric & bri # 交集:两集合共有
bric | bri # 并集:全部合起来
bric - bri # 差集:bric 有而 bri 没有
bric.issuperset(bri) # bric 是否包含 bri 的全部
# 资格测试(比 list 快)
'China' in bric # True
bric.remove('india') # 元素必须存在,否则抛 KeyError
bric.discard('india') # 不存在也不报错,更稳妥
bric.clear()
要点:集合元素必须可哈希(不可变) ,所以 set([1, 2, 3]) 行,set([[1, 2], [3, 4]]) 报错(列表不可哈希)。去重最常用的一招:unique = list(set(some_list))。
小结 :集合管"去重 + 关系运算"。
&交、|并、-差、in测试 O(1);元素必须可哈希,删元素用discard比remove安全。
七、引用陷阱:= 是"贴标签"不是"复制"
为什么这是个坑
Python 的变量不是"盒子",而是贴在数据对象上的标签 。写 b = a 时,a 和 b 指向同一块内存。对不可变对象(数字、字符串)无所谓,反正改不了;但对列表、字典这类可变容器,通过一个名字改了,另一个名字看到的全跟着变------这种"幽灵联动"是新手 bug 的重灾区。
python
# data_structures/reference_demo.py
original = ['apple', 'banana', 'pear']
alias = original # alias 和 original 指向同一个列表对象
del original[0]
print(alias) # ['banana', 'pear'] ------ 跟着变了!
# 正确复制:切片创建独立副本
copy = original[:]
# 或 copy = original.copy() / copy = list(original)
del original[0]
print(copy) # ['banana', 'pear'] ------ 不受影响
复制可变对象的几种正确姿势:
- 列表:
new = old[:]或new = old.copy() - 字典:
new = old.copy() - 通用深拷贝:
import copy; new = copy.deepcopy(old)(对象里还套对象时用)
小结 :
=是绑定不是复制。可变对象要独立副本,用切片[:]或.copy();嵌套结构用copy.deepcopy。
数据结构本身不难,难在看到需求时选对结构:要顺序和改动就用 list,要定值就用 tuple,要按名查就用 dict,要去重算关系就用 set。把这四个选对了,代码自然又短又清楚------后面写 FastAPI 的入参校验、响应模型、缓存,全都是这四种结构在干活。