在掌握常量、变量、运算符、条件循环和函数之后,Python 容器三件套——列表 list、元组 tuple、字典 dict——是进入实际项目开发前必须啃下的基础。它们不只是用来“装数据”的语法糖,理解其底层存储方式、可变性边界和复杂度差异,才能应对面试追问,也才能写出更可靠的代码。
为什么需要容器
假设要保存一个班级所有学生的成绩,或者一个接口返回的多字段数据,如果每个值都用一个独立变量去存,代码会迅速膨胀且难以维护。容器的作用就是用一种统一的结构批量保存数据。Python 最常用的三种容器分别是:列表(可变序列)、元组(不可变序列)和字典(键值映射)。它们的选型直接决定了后续代码的效率和安全性。
列表 list:可变的动态数组
创建列表有多种写法,空列表可以直接用空方括号,也可以用 list() 构造,同时列表允许混合元素类型:
- a = []
- a = list()
- a = [1, 2, 3, 4]
- a = [1, 'hello', True]
复制代码
列表底层是一个指向对象的指针数组(类似 PyObject* 数组),因此不要求元素类型一致。这一点与 C 的 int arr[]、C++ 的 vector<int> 等类型固定的数组有本质区别。
下标从 0 开始,负数表示从末尾倒数。切片使用前闭后开区间,省略边界则默认到尽头。步长为负数时可以实现倒序输出:
- a = [1, 2, 3, 4]
- print(a[2]) # 3
- print(a[-1]) # 4
- print(a[1:3]) # [2, 3]
- print(a[1:]) # [2, 3, 4]
- print(a[::-1]) # [4, 3, 2, 1]
复制代码
注意,下标越界会抛出 IndexError,但切片越界不会报错,只会返回能够取到的部分。新手经常把这两者搞混。另外 a[::-1] 本质上只是改变了遍历方向,并没有引入额外反转逻辑。
列表的增删改查方法需要关注复杂度:
- a = [1, 2, 3, 4]
- a.append('hello') # 尾插,平均 O(1)
- a.insert(1, 'x') # 指定位置插入,O(n)
- print(2 in a) # True
- print(a.index(2)) # 返回下标,找不到抛 ValueError
- a.pop() # 删末尾,O(1)
- a.pop(2) # 按下标删,O(n)
- a.remove(2) # 按值删,O(n)
复制代码
append 是列表对象的方法,调用方式是 a.append(x),而 len(a) 是独立函数。这种“方法 vs 函数”的区别来源于面向对象的设计:方法是对象能够执行的操作,函数则是独立于对象的工具。
列表连接有两种常见方式,区别在于是否修改原对象:
- a = [1, 2, 3, 4]
- b = [5, 6, 7]
- print(a + b) # 生成新列表,a 和 b 不变
- a.extend(b) # 直接修改 a,把 b 拼接到末尾
复制代码
面试中经常会被问到:append 和 insert 复杂度为何差这么多?因为列表是动态数组,尾部插入只需要在末尾追加;insert 则需要把插入位置之后的元素整体后移。pop() 删末尾是 O(1),pop(i) 和 remove 需要搬移元素,因此是 O(n)。切片 a[:] 会创建新列表,但元素是引用拷贝,新旧列表指向同一批对象,属于浅拷贝,这也是一个经典考点。
元组 tuple:不可变的序列
元组的创建方式和列表类似,但它是不可变序列:
- t = ()
- t = tuple()
- t = (10, 20)
复制代码
一个容易忽略的细节是,函数返回多个值时的本质就是返回元组:
- def get_point():
- return 10, 20
- result = get_point()
- print(type(result)) # <class 'tuple'>
复制代码
元组支持下标、切片、遍历、in 判断、index、+ 连接等只读操作,但不支持修改元素,也不能增删。下面的代码会报错:
- t = (1, 2, 3, 4)
- # t[0] = 100 # TypeError: 'tuple' object does not support item assignment
复制代码
既然有了列表,为什么还需要元组?两个核心原因:第一,安全。把数据传给某个函数时,如果以元组形式传递,调用方无法修改原始数据;第二,可哈希。字典的键必须是可哈希对象,不可变的元组可以,而列表不行。
函数返回多值后可以用解包方式接收,例如 a, b = get_point();如果不关心某个返回值,可以用下划线忽略,例如 _, b = get_point()。选型建议是:数据不需要变化时优先用元组,需要增删改时用列表。
字典 dict:哈希表实现的键值映射
字典可以用花括号或 dict() 创建:
- d = {}
- d = dict()
- student = {
- 'id': 1,
- 'name': 'zhangsan'
- }
复制代码
字典本质是哈希表,key 通过哈希函数定位到存储桶,再用 key 快速取值。它适合“按名字/ID 查值”的场景,而不适合按位置索引。
字典的查增改删规则很紧凑:
- print('id' in student) # True
- print(student['id']) # 1,key 不存在抛 KeyError
- student['score'] = 90 # key 不存在则新增,存在则修改
- student.pop('score') # 按 key 删除
复制代码
遍历字典时,默认拿到的是 key,也可以显式获取 keys、values、items:
- for key in student:
- print(key, student[key])
- print(student.keys())
- print(student.values())
- print(student.items())
复制代码
合法的 key 必须是可哈希对象。int、str、tuple 都可以,list 和 dict 则不行,因为哈希表依赖 key 的哈希值定位,一旦 key 可变,哈希定位就会失效。
- print(hash(0))
- print(hash('hello'))
- print(hash(()))
- # print(hash([1, 2, 3])) # TypeError: unhashable type: 'list'
- # print(hash({'id': 1})) # TypeError: unhashable type: 'dict'
复制代码
面试中常问 dict 查找是不是 O(1)。答案是平均 O(1),但在哈希冲突严重时会退化,最坏情况下理论上是 O(n)。Python 解决哈希冲突采用开放寻址法,当负载因子超过阈值时会触发 rehash(重建哈希表),这也是为什么频繁插入大量数据时偶尔会感觉到卡顿。
另外要区分列表和字典的适用场景:按位置取用列表,按名字或 ID 取用字典。列表是序列,字典是映射,语义完全不同。
三者的横向对比
从可变性看,列表可变,元组不可变,字典的值可变但键必须不可变。底层结构上,列表是动态数组,元组是不可变数组,字典是哈希表。典型用途方面,列表适合数量未知且需要修改的序列,元组适合固定数据、函数多返回值和作为字典的键,字典适合按 key 快速查值。下标和切片方面,列表和元组都支持,字典不支持下标,而是通过 key 取值。能否作为字典的 key:列表不能,元组能,字典不能。查找复杂度上,列表用 in 是 O(n)、按下标是 O(1),元组类似,字典按 key 平均 O(1)。一句话选型:需要修改就用列表,固定不变就用元组,要按名字查找就用字典。
常见陷阱与误区
第一,切片越界不报错。a[100:200] 只会返回能取到的部分,而 a[100] 这种下标访问才会抛 IndexError。第二,方法 vs 函数。a.append(x) 是方法,len(a) 是函数,不要混淆它们的调用方式。第三,元组“不可变”的边界。元组保证的是元素指向不能变,但如果元组里存了一个列表,列表内部的元素仍然可以修改,元组本身不会变,但内容会变。第四,+ 与 extend 的副作用。+ 操作生成新列表,extend 修改原列表,在数据共享或多线程场景下要特别注意。
容器三件套是后续学习字符串处理、文件操作、pandas 数据处理以及后端开发的基础。把列表、元组、字典的底层差异和操作复杂度理解透彻,写出的 Python 代码才能既符合直觉,又经得起性能考验。 |