GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • 卡码笔记-最强八股文
  • 计算机基础
  • Python
  • C++
  • Java
  • Go
  • ChatGPT充值
  • Claude充值
  • 国内使用Claude
  • 中转站推荐
  • 🔥大模型🔥
  • 大模型面经
  • Java面经
  • C++面经

This view

  • Original page
  • New address

GeezerVison is showing notes.kamacoder.com — Python列表和元组的区别:可变性、哈希、内存占用与使用场景

# Python列表和元组有什么区别?

面试官问:“Python中的列表和元组有什么区别?”

很多录友答完“列表可变、元组不可变”就停了。面试官接着问一句“那元组一定能当字典键吗”“元组里放列表,列表能改吗”,答案就开始含糊。这道题的关键是讲清不可变到底约束了什么,以及它怎样影响哈希、内存和使用场景。可变与不可变的通用规则见《Python中的可变对象和不可变对象有什么区别?》。

# 简要回答

列表list和元组tuple都是有序序列,都支持索引、切片、遍历和in判断。核心区别是:列表可变,可以原地增删改元素;元组不可变,创建后不能增删元素,也不能把某个位置换成别的对象。

这个区别带来几个直接结果。元组的元素都可哈希时,元组本身才可哈希,可以作为字典键或集合元素;列表永远不可哈希。在CPython中,元组没有为扩容预留空间,同样元素数量下通常比列表占用更少内存。

语义上,列表适合放数量会变化的同类数据,例如用户列表;元组适合放结构固定的一组值,例如坐标、数据库的一行记录、函数返回的多个值。

# 详细回答

# 原地修改与创建新对象

列表的append、索引赋值、sort都会原地修改同一个对象;元组没有这些修改方法,想“改”只能创建新元组:

courses = ["Python", "Java"]
alias = courses
courses.append("Go")
print(alias)              # ['Python', 'Java', 'Go'],alias看到同一个列表的变化

point = (3, 5)
# point[0] = 10           # TypeError: 'tuple' object does not support item assignment
moved = (10,) + point[1:]
print(point, moved)       # (3, 5) (10, 5)
1
2
3
4
5
6
7
8
9

alias和courses指向同一个列表,append修改的是这个列表本身,所以两个名字都能看到"Go"。元组point没有被修改,moved是拼接出来的新元组。

# 元组不可变,不等于里面的对象不可变

元组只保证每个位置保存的引用不变,不管被引用的对象自己能不能变:

record = ("Alice", [90, 85])
record[1].append(100)
print(record)             # ('Alice', [90, 85, 100])

# record[1] = [60]        # TypeError,不能把这个位置换成别的对象
print(hash(("Alice", 90)))     # 正常输出一个整数
# hash(record)            # TypeError: unhashable type: 'list'
1
2
3
4
5
6
7

record[1]仍然是原来那个列表,只是列表内容变了,元组的约束没有被打破。也正因为里面有可变的列表,record的内容可能随时变化,所以它不能计算哈希,不能当字典键。

元组引用与内部可变对象

图里要区分两层:元组这一层的“槽位引用”被锁死,替换会报错;槽位指向的列表那一层仍然可变,append可以成功。哈希要求整个值都稳定,只要有一层能变,元组就不能哈希。

# 内存占用与性能

在CPython中,列表为了让append的均摊时间复杂度保持O(1),扩容时会多申请一些空间;元组长度固定,不需要预留。以64位的CPython 3.13为例:

import sys

print(sys.getsizeof([1, 2, 3, 4, 5]))   # 104
print(sys.getsizeof((1, 2, 3, 4, 5)))   # 80
1
2
3
4

sys.getsizeof只统计容器本身,不包含元素对象,具体数值随版本和平台变化。可以确认的趋势是:同样元素数量下,元组通常更省内存;CPython还会把全部由常量组成的元组字面量直接放进代码对象,创建时开销更小。

但不要把“元组更快”当成换掉列表的理由。两者按下标访问都是O(1),真实程序的耗时通常不在这里。先按语义选类型,再考虑这点性能差异。

# 怎么选

对比项 列表 list 元组 tuple
可变性 可变,支持增删改 不可变,不能替换元素
可哈希 不可哈希 元素都可哈希时可哈希
内存(CPython) 有扩容预留 长度固定,通常更省
典型语义 数量会变的同类数据 结构固定的一组值
常见场景 待办任务、查询结果集合 坐标、字典的复合键、多返回值

一个实用的判断方法:如果每个位置都有固定含义(第0位是姓名,第1位是年龄),用元组;如果每个元素地位相同、数量会变,用列表。字典键要用复合值时,例如(城市, 日期),只能用元组,字典对键的要求见《Python字典和集合有什么区别?》。

# 知识拓展

面试官可能追问:

Q1:函数return a, b返回的是什么?

返回的是一个元组,括号可以省略,真正构成元组的是逗号。调用方写name, age = get_user()时,是把元组解包给两个名字。解包和星号收集的更多写法见《Python切片和序列解包怎么用?》。

Q2:只有一个元素的元组怎么写?

要写成(42,)。(42)里的括号只是分组,结果还是整数42。空元组写作()或tuple()。

Q3:tuple(some_list)会把里面的列表也变成不可变吗?

不会。转换只创建一个新的元组容器,元素引用和原列表里的完全相同。原列表中如果有嵌套列表,转换后它依旧是同一个可变列表,修改会同时体现在原列表和新元组里。

Q4:元组+=为什么不报错?

t += (3,)对元组来说是创建一个新元组,再让t重新绑定过去,原元组没变。如果另一个名字也指向原元组,它看到的还是旧内容。列表+=则是原地扩展,和extend效果一样,所有指向该列表的名字都会看到变化。

← Python字符串为什么不可变? Python字典和集合有什么区别? →

评论

验证登录状态...

You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.