十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python列表与元组区别详解:可变性、性能与应用场景全解析

Python列表与元组区别详解:可变性、性能与应用场景全解析 很多学Python的朋友都有过类似的疑问列表和元组明明长得差不多为什么要搞两个容器我最早也觉得这有点多余直到用Python写过几个正式项目回头再看官方文档和底层源码才明白这对“孪生兄弟”背后设计上的深意。这篇文章不是从文档里抄定义而是从实际开发的角度把列表和元组的区别、性能差异、应用场景和踩过的坑一次说清。无论你是刚入门Python的新手还是已经写了不少脚本但没系统整理过容器类型的开发者都能按文中的思路去做选型。1. 先搞清楚列表和元组到底是什么1.1 列表可变的有序集合列表用方括号[]表示它是一组有序元素组成的容器。这里的“有序”不是说元素从小到大排序而是指元素的排列顺序会稳定保持每个元素都有固定的下标位置从0开始计数。# 最简单的列表 names [张三, 李四, 王五] # 可以混装不同类型 mixed [1, hello, 3.14, True] # 也可以是嵌套结构 matrix [[1, 2], [3, 4]]列表最大的特点就是“可变”。这意味着你可以随时往里面添加元素、删除元素、修改某个位置的值甚至对列表做整体排序。这在我们做数据收集、动态处理、消息队列这类场景里非常实用因为数据量是不断变化的我们操作的数据集合天然是“活”的。1.2 元组不可变的有序集合元组用圆括号()表示本质上也是一组有序元素组成的容器。它的顺序同样固定每个元素也有下标但它和列表有一个根本区别创建之后不能再修改。# 最简单的元组 point (1, 2) # 字符串和数字混装 person (张三, 30, 北京) # 单元素元组注意结尾必须有逗号 single (1,)我见过很多新手写single (1)然后惊讶地发现自己创建的是一个整数而不是元组。正是因为括号在Python里还承担了数学运算中的分组功能所以单个元素的元组必须加上那个逗号这是入门阶段最常见的“隐形坑”。1.3 一眼看懂的基本对比对比项列表 list元组 tuple语法方括号[1, 2]圆括号(1, 2)可变性可增删改不可修改内存占用相对更大相对更小速度稍慢稍快可哈希性不可哈希元素可哈希时可哈希典型用途动态数据集合固定结构记录这个表格只是把结论先摆出来。很多人看到表就想背结论但真正的项目里选型不是靠背结论而是靠理解这些差异背后的机制和代价。下面我逐个拆解。2. 核心区别拆解光说“一个能改一个不能改”远远不够2.1 可变性为什么这是最大的分水岭列表和元组最核心的区别就是可变性但我们要理解“可变”到底意味着什么。当你执行list.append(item)的时候Python不会新建一个列表而是在原列表的内存区域上追加元素。你可以用id()函数来观察lst [1, 2, 3] print(id(lst)) # 比如 140133411154368 lst.append(4) print(id(lst)) # 还是 140133411154368 tup (1, 2, 3) print(id(tup)) # 比如 140133411154288 tup tup (4,) print(id(tup)) # 地址变成了新的元组本身不支持修改任何看似“修改”的操作例如tup (4,)、tuple(tup[:2] (99,))都会创建一个全新的元组对象原来的元组原封不动。这个特性让元组在Python内部可以被非常安全地共享不用担心某个地方改了一下其他用同一个元组的地方数据突然变了。实际项目里这带来一个明显的好处如果你把一个列表当作参数传给函数函数内部一旦不小心执行了append或sort外部数据就跟着变了这种“外溢副作用”排查起来相当痛苦。而如果你传的是元组函数内部无论如何都不会改到原始数据代码会安全很多。2.2 性能差异元组真的更快更省内存吗很多人说元组性能比列表好这确实是真的但差距到底有多大值得用数据说话。我用一个简单的代码验证import sys import timeit lst [1, 2, 3, 4, 5] tup (1, 2, 3, 4, 5) print(sys.getsizeof(lst)) # 我这里测试得到 120注意不同版本有差异 print(sys.getsizeof(tup)) # 80原因在于列表底层为未来的动态扩展预留了一部分空间而元组创建时只需分配恰好能容纳所有元素的空间。列表之所以要“过度分配”是因为高频append操作如果每次都要重新分配内存性能会非常难看而元组创建后长度不变就不需要这个余量。再看访问速度import timeit print(timeit.timeit(lst[0], setuplst[1,2,3,4,5], number10000000)) print(timeit.timeit(tup[0], setuptup(1,2,3,4,5), number10000000))我实测的情况下元组的索引访问比列表快大约10%到20%。不过老实说在绝大多数业务代码里这种性能差异微乎其微。真正让你选择元组的理由不是因为“快那么一丢丢”而是因为它提供的不可变性和语义安全。别把性能神话了选型还是要看场景。2.3 线程安全与意外修改不可变性的隐藏价值在写多线程程序的时候不可变性会让你省很多心。多个线程同时读一个列表是安全的但如果一个线程在遍历列表、另一个线程在修改列表就可能遇到RuntimeError: dictionary changed size during iteration甚至更隐蔽的数据错乱。元组因为不可变本质上天然免疫这类问题共享给任意线程都安全。即使你暂时不写多线程不可变性在代码可维护性上也有价值。团队合作中别人拿到一个元组第一反应是“这个数据在生命周期内不会被改动”拿到列表他可能下意识觉得可以随便append。这种“意图自描述”的代码比注释可靠得多。2.4 哈希与字典键元组为什么能当字典键列表不行Python里的字典键必须是可哈希的而可哈希的对象必须满足一个条件对象在整个生命周期内哈希值不能变。列表可变哈希值一旦数据变化就会失效所以Python直接禁止列表做字典键。元组不可变只要元组里的所有元素也都不可变它就可以计算出一个稳定哈希值。# 可以用元组做字典键 location_map {(1, 2): 东门, (3, 4): 南门} print(location_map[(1, 2)]) # 东门 # 报错列表不能做字典键 # location_map {[1, 2]: 东门} # TypeError: unhashable type: list这个特性在缓存、状态表、坐标索引、复合键去重等场景里非常重要。举个实际例子如果用户用城市和日期作为条件查数据你可以直接用元组(city, date)作为字典键比拼接字符串再查询要清晰得多。有一点特别注意如果元组里包含了一个列表那这个元组仍然是不可哈希的不能做字典键bad_key (1, [2, 3]) # location_map {bad_key: x} # TypeError: unhashable type: list因为外层元组虽然不能增删元素但内层列表内容可以变元组的哈希值会跟着变所以Python规定这种元组不能哈希。3. 应用场景什么时候该用列表什么时候该用元组3.1 列表的典型场景动态数据、批量处理、栈队列列表适合那些长度和内容都在动态变化的数据。常见的有爬虫里待抓取的URL队列不断把新发现的URL追加进去抓完一个弹出一个。用户日志集合每条日志依次append最后统一分析统计。从数据库读出的记录集记录数未知需要支持追加、排序、过滤。作为栈或者队列使用append入栈pop出栈配合collections.deque做队列更高效但列表也能快速实现栈。例如我们要实现一个简单的任务管理器不断有新任务进来task_queue [] def add_task(task): task_queue.append(task) def run_next(): if task_queue: task task_queue.pop(0) print(running:, task)注意这里用pop(0)从头部取元素是O(n)操作数据量大了不推荐正确做法是使用collections.deque。这个例子只是说明列表适合承担“随时可变的容器”这个角色。列表还非常适合存储结构相同、数量可变的一组数据比如某个班级的所有学生students [ {name: 张三, score: 88}, {name: 李四, score: 76}, ] students.append({name: 王五, score: 91}) students.sort(keylambda s: s[score], reverseTrue)这里的列表长度和内容都是动态的而且需要排序、过滤、增删选列表是理所当然的。3.2 元组的典型场景固定结构、字典键、函数多返回值元组天然适合表达“这条数据本身就是一个完整的固定结构”。比如一个二维坐标(x, y)一个经纬度(longitude, latitude)一个订单里的(order_id, amount, status)这些数据的字段数量是固定的不同字段的含义不同但整体不会被修改用元组就非常妥当。函数返回多个值时Python实际上返回的就是一个元组def get_user_info(user_id): if user_id 1: return 张三, 30, 北京 name, age, city get_user_info(1) print(name, age, city) # 张三 30 北京这种通过位置访问的元组如果字段多了容易记不住每个下标是什么可以用namedtuple或者dataclass增强可读性。但在字段不多的时候轻量级元组完全够用。元组作为字典键的场景前面已经提过我再举一个更实际的例子统计每个域名每天的访问量。visit_count {} def record_visit(domain, date): key (domain, date) visit_count[key] visit_count.get(key, 0) 1这样写比用两层字典visit_count[domain][date]要简单得多也不会在嵌套字典时出现KeyError问题。元组还常用于“不可变配置”。比如一组默认参数或者一组不会被改变的状态码HTTP_SUCCESS (200, 201, 204) HTTP_REDIRECT (301, 302, 303, 307, 308) def check_response(status_code): if status_code in HTTP_SUCCESS: return ok像这种配置集合用元组可以达到“定义以后谁也不准改”的效果。3.3 混合选择列表里放元组、元组里放列表时的思考真实项目中我们经常看到“列表里放元组”比如数据库查询结果就是一行一个元组。这种结构很合理整个结果集是动态的将来可能增加行用列表承载每一行记录的结构是固定的列数不会变用元组表示。rows [ (001, 张三, 88), (002, 李四, 76), ] # 动态添加一行没问题 rows.append((003, 王五, 91))反过来“元组里放列表”就要小心了record (张三, [88, 76, 91]) record[1].append(85) # 这个操作能成功虽然元组本身不可变但它内部的列表是可变对象你仍然可以修改列表的内容。如果你想要的是“完全不可变”那元组里的所有元素也必须是不可变类型。否则你只是在“外壳”上设了道防线内部内容仍然可能被修改。所以我做设计时的习惯是如果这个数据结构内部还可能继续动态变化就整体用列表如果从语义上已经确定不会再变了再考虑元组。不要为了“用元组”而硬把一个包含列表的元组塞进去那样只会在代码维护时留下隐患。4. 实战中的高级操作列表切片、拆包与多维列表排序4.1 列表切片不要只会list[start:end]列表切片是处理有序数据时效率非常高的工具。基本语法是list[start:stop:step]三个位置都可以省略。比如data [1, 2, 3, 4, 5, 6, 7, 8] print(data[2:5]) # [3, 4, 5] print(data[:4]) # [1, 2, 3, 4] print(data[::2]) # [1, 3, 5, 7] print(data[::-1]) # [8, 7, 6, 5, 4, 3, 2, 1]step为负数时可以从右往左取data[::-1]直接得到倒序列表。这个操作写起来比reverse()更灵活因为data[::-1]会返回新列表不会修改原列表而data.reverse()是在原列表上原地反转用之前要想清楚要不要保留原顺序。切片还有一个容易被忽略的用途复制列表。copy data[:]会生成一个浅拷贝修改copy不会影响data。但注意这只对一维列表有效如果列表里包含可变对象比如子列表、字典那么“浅拷贝”只拷贝了外层引用内层对象仍然共享。这时候就需要copy.deepcopy了。切片同样支持赋值这个技巧可以用来替换一段连续区域data[2:4] [20, 30, 40] print(data) # [1, 2, 20, 30, 40, 5, 6, 7, 8]这个操作很神奇右边的元素数量可以和左边切片长度不一样Python会直接调整列表长度。我在处理数据清洗时经常用到。4.2 元组拆包与列表推导式结合元组拆包是Python里非常优雅的写法但很多人只会在函数返回时用一次性拆包实际上它还能用在循环和推导式里。pairs [(1, one), (2, two), (3, three)] for num, word in pairs: print(num, word)遍历列表里的元组时直接在循环变量里拆包比用pair[0]、pair[1]可读性好很多。利用拆包交换两个变量a, b b, a这个写法在Python里是合法的它本质上是先打包成元组(b, a)然后再拆包赋值给a, b。你不需要引入第三个临时变量。如果拆分时元素个数不匹配会遇到ValueError: too many values to unpack。但我们可以用星号表达式吸收多余部分first, *middle, last [1, 2, 3, 4, 5] print(first, middle, last) # 1 [2, 3, 4] 5这个特性在解析格式固定的数据时非常实用比如日志行前几个字段固定后面是可变参数。列表推导式也可以结合拆包来用points [(1, 2), (3, 4), (5, 6)] x_coords [x for x, y in points] print(x_coords) # [1, 3, 5]这比[p[0] for p in points]更直观特别是在x和y语义明确的时候。4.3 多维列表排序按某一列排序的几种方式在项目里经常遇到这种需求一个列表里的元素是元组或列表需要按照某个位置的字段排序。比如下面这份学生成绩表students [ (张三, 88), (李四, 76), (王五, 91), ]按成绩升序排students.sort(keylambda x: x[1]) print(students) # [(李四, 76), (张三, 88), (王五, 91)]按成绩降序students.sort(keylambda x: x[1], reverseTrue)如果不想改变原列表用sortedsorted_students sorted(students, keylambda x: x[1])当排序字段比较多时推荐用operator.itemgetter它比lambda运行得更快一些from operator import itemgetter students.sort(keyitemgetter(1)) # 先按成绩再按姓名 students.sort(keyitemgetter(1, 0))多维列表排序还需要注意如果直接用sorted(students)排序默认会按元素的第一个字段排如果第一个字段相同再按第二个字段排以此类推。这种默认行为有时候就是我们要的有时不是所以明确指定key才是更稳的做法。我自己在写数据分析脚本时经常遇到“按第3列默认是字符串、但想按数值排”的坑。比如成绩存在字符串88直接排序会按字符顺序排结果“91”排到了“88”前面。处理方式是先把字段转成数值再比较students.sort(keylambda x: int(x[1]))所以多维列表排序时不要只盯着排序方法还要先确认字段的数据类型是否符合你的预期。5. 常见问题与避坑我用Python写项目时踩过的坑5.1 元组里的列表是“可变的”这个坑怎么躲我在3.3里已经提到过这里单独拿出来说因为它真的太容易踩了。很多人以为元组创建后就完全不能变于是放心地把某个包含列表的元组传给函数结果函数内部悄悄改了列表内容数据就“莫名其妙”变了。def process(cfg): cfg[0].append(changed) settings ([base], 8080) process(settings) print(settings) # ([base, changed], 8080)这个现象的原因在于元组的不可变只是“引用不可变”它保管的变量名不能指向新对象但对象内部的状态无法控制。本质上和函数参数传递的是引用有关。要避免这个坑有两个办法设计时确保元组里的所有元素都是不可变类型比如数字、字符串、其他纯元组。如果一定要在元组里放可变对象思路要清晰这个元组保护的只是最外层外壳内部元素照样要遵守它们自己的可变性规则。我踩过一次后就给自己定了一条规矩凡是需要作为“配置项”长期存在的结构尽量使用纯不可变对象。如果数据本身要频繁变化那就别硬塞进元组。5.2 列表复制三种方式哪种才不会踩到修改原列表的坑给别人传列表时经常需要先复制一份作为快照。新手最容易犯的错误是直接b a然后改b的时候发现a也变了。这是因为赋值只是复制了引用两个变量指向同一个列表对象。正确的方式有三种a [1, 2, 3] b1 a[:] b2 list(a) b3 a.copy()这三者都是浅拷贝。对于一维列表来说够用了但如果列表里还有可变对象比如a [[1, 2], [3, 4]]浅拷贝出来的b1[0]仍然和a[0]指向同一个子列表a [[1, 2], [3, 4]] b a[:] b[0].append(99) print(a) # [[1, 2, 99], [3, 4]]这时就需要深拷贝import copy c copy.deepcopy(a) c[0].append(100) print(a) # 不受影响深拷贝是个“重型武器”性能开销比浅拷贝大很多所以不要一遇到复制就无脑深拷贝要根据嵌套可变对象的深度来决定。我的原则是只有在我确定会修改容器内部嵌套对象时才用深拷贝否则浅拷贝足够。5.3 为什么说“能用元组就别用列表”社区里经常有人建议“能用元组就别用列表”这句话有一定道理但不能机械执行。它背后的逻辑其实不是性能而是意图表达和安全性。元组能直接向阅读代码的人传递“这个数据不会变”的信号。比如我在写配置类常量时一定用元组VALID_COLORS (red, green, blue)如果这里用列表后续接手的人很可能会觉得可以往里面加颜色。用元组就少了一个让别人误解的机会。但也不能为了用元组而用元组。如果一个数据的核心行为就是动态变化比如要持续append、排序、删除、反转那列表才是正确选择。强行用元组然后每次通过重建一个新元组又麻烦又费内存。所以我的选型口诀是数据结构长度固定、内容不变优先元组。数据结构需要动态增删改用列表。作为字典键必须用元组或不可变对象。函数返回多个值用元组。同类型、可变的元素集合用列表。5.4 常见错误排查表错误提示常见原因解决办法TypeError: tuple object does not support item assignment尝试修改元组元素改成列表或重新构造元组TypeError: unhashable type: list拿列表当字典键或放入集合改成元组或保证内层元素不可变ValueError: too many values to unpack拆包时变量数不够使用星号表达式*吸收多余元素a, b 1, 2, 3但写成a, b, ...之类语法错误常见手误检查等号两侧元素个数单元素元组成了int写成了(1)少了逗号改成(1,)sort()返回了None误以为sort()返回排序后的列表原地排序用list.sort()返回新列表用sorted(list)复制列表后改了一处影响了原列表用赋值导致引用共享使用a[:]、list(a)、a.copy()或copy.deepcopy这个表格适合放在项目的代码规范文档里也可以作为新同学入门时的速查卡。根据我个人经验列表和元组的选择很多时候不是会不会写的问题而是代码设计意图的问题。我现在写代码会在变量命名之外再想一层这个容器将来会不会被改如果答案是不确定我会先默认选择元组等确实需要变更时再改成列表。这样能逼着自己把数据结构设计得更稳定也减少了不少后期调试的时间。最后再分享一个小技巧在团队代码评审时如果看到函数返回一个列表但函数内部从来没有修改过这个列表我会建议改成元组。这不是格式洁癖而是让调用方明确知道返回值是“只读快照”而不是“可操作对象”。时间长了整个项目的接口契约会清晰很多后续维护的人也不会战战兢兢地猜数据到底能不能改。这大概就是列表和元组这对“孪生兄弟”教给我的最重要的一件事。
返回列表