
Python list 列表从入门到别踩坑做 Python 开发这几年如果你让我选一个最常用、最离不开的内置类型我肯定毫不犹豫选 list。写爬虫存数据、做算法题维护序列、处理接口返回的 JSON 数组、日常脚本里攒一堆待处理的文件名……list 几乎无处不在。它太常用、太基础了以至于很多人学完 append、pop 就觉得“会了”但实际一写代码就掉进各种各样的坑里切片浅拷贝改坏了原数据、循环里删元素漏删、列表推导式把代码写成天书、默认参数里塞 list 结果所有调用共享同一份数据。这篇文章就是想把 list 从底层机制到高频操作、再到实战避坑一次讲透。不管你是刚学 Python 的初学者还是写了一阵子但没系统梳理过的人我相信都值得花十分钟过一遍。1. list 到底是什么动态数组不是链表先说一个最常见的误解。很多人一听“List”下意识觉得它跟 C 的 list、Java 的 LinkedList 一样是链表结构。完全不是。Python 的 list 底层是一段连续的内存空间本质上是动态数组。你可以把 list 理解成一个大仓库里的一排带编号的货架。每个格子上存的东西并不是你放进去的对象本身而是一个指向真实对象的“门牌号”指针。所有门牌号按顺序排成一行所以访问 list[i] 时Python 可以瞬间通过首地址加偏移量跳过去时间复杂度是 O(1)。这也是 list 最大的优点按下标访问极快。但连续空间的代价是在头部插入或者删除元素时需要把后面所有元素挨个往后挪或往前挪时间复杂度是 O(n)。所以如果你频繁在 list 开头 insert那性能会非常难看。后面我会再展开讲这个时间复杂度的实际影响。1.1 为什么 list 可以装不同类型的数据很多新手第一次被 Python 惊艳到就是因为一个列表里能混着装整数、字符串、甚至另一个列表。这背后的原因就是 list 存的是“门牌号”而不是“货物本身”。每个格子都只存一个 8 字节的指针至于指针指向的是整数还是字符串list 本身根本不关心。这也解释了为什么 list 会比同等数据的 array 模块或者 NumPy 数组更耗内存。因为每个元素除了本身的数据占用的空间还要额外维护一个指针和对象头。你存 100 万个整数list 实际消耗的内存远大于一个 C 语言的 int 数组。这也是为什么做大规模数值计算时大家宁愿用 NumPy 也不用原生 list。1.2 扩容机制为什么 list 追加元素通常很快既然底层是连续内存那问题来了初始化一个空列表时Python 并不知道你以后要装多少个元素。如果每次 append 都重新申请一块“刚好够用”的内存性能会极其糟糕。Python 的实际做法是按比例扩容。当列表满了它会申请一块更大的空间大概是当前容量的 1.125 倍具体数值版本间略有差异然后把旧元素全部复制过去。复制过程是 O(n)但由于扩容不是每次 append 都发生平摊下来每次 append 的时间复杂度依然是 O(1)。这就是所谓的“均摊常数时间”。你可以自己验证一下用sys.getsizeof观察列表容量变化import sys lst [] for i in range(20): print(f长度 {i 1}: {sys.getsizeof(lst)} 字节) lst.append(i)运行后会看到列表的“内存占用”不是每次加 1而是一段一段地跳变。每次跳变就是一次扩容。这里面有个容易忽略的点列表有剩余容量时sys.getsizeof 的结果不会变。所以你会看到长度 5 和长度 8 可能占用同样大小的内存因为底层容量还没触发下一次增长。2. 创建列表的五种正确姿势创建 list 看起来太简单了但不同场景用不同写法代码质量和执行效率差很多。这节我把常用的方式全列一遍你对照自己平时的习惯看看有没有可以优化的地方。2.1 最推荐字面量写法# 最常见的性能最好的写法 nums [1, 2, 3, 4, 5] names [张三, 李四] mixed [1, hello, 3.14, True]直接写中括号字面量Python 解释器会一次性创建 list 并填入元素代码可读性最高执行效率也是所有方式里最优的。日常写代码能用字面量就优先用字面量。2.2 list() 构造函数从其他可迭代对象转换# 把字符串拆成字符列表 chars list(hello) # [h, e, l, l, o] # 把元组转成列表 t (1, 2, 3) lst list(t) # [1, 2, 3] # 把字典的键转成列表 d {a: 1, b: 2} keys list(d) # [a, b] # 把集合转成列表注意顺序不保证 s {3, 1, 2} lst list(s) # 可能是 [1, 2, 3]也可能是其他顺序这里特别提一句用 list() 转换字典时默认拿到的是键的列表。如果想拿值得用list(d.values())想拿键值对就是list(d.items())得到的是元组列表。热词里有人问“listmap字符串转list”在 Python 的世界里没有一个直接的转换函数但遇到类似的“字符串形式的列表”用ast.literal_eval可以安全解析import ast s [{name: 张三, age: 20}, {name: 李四, age: 22}] lst ast.literal_eval(s) print(lst[0][name]) # 张三2.3 range() 快速生成数字序列很多新手不知道range 本身不是一个 list而是一个惰性序列。直接 print 它得到的是range(0, 10)不是[0, 1, 2, ...]。nums list(range(10)) # [0, 1, 2, ..., 9] odds list(range(1, 20, 2)) # [1, 3, 5, ..., 19]如果只是想循环遍历一组数字其实不需要转成 list直接用 range 就行省内存。只有当你有切片、索引修改、append 等需求时才强制用 list() 包一层。2.4 列表推导式最 Pythonic 的写法列表推导式是你绕不过去的东西热词里也出现了“列表推导式与生成器”这个长期热点。它的基本语法是# 传统写法 squares [] for i in range(1, 11): squares.append(i ** 2) # 推导式写法 squares [i ** 2 for i in range(1, 11)]推导式的可读性和执行效率在简单场景下都优于 for 循环加 append。Python 底层对推导式的字节码做了专门优化速度通常比普通循环快。但记住一句话推导式只适合逻辑简单的场景。一旦你需要在循环体里写多行逻辑、做复杂判断老老实实用 for 循环不要硬拗成推导式否则代码会变成“阅兵式”别人看不懂一个月后的你自己也看不懂。2.5 乘法创建与浅拷贝陷阱# 一维列表这种方法没问题 lst [0] * 5 # [0, 0, 0, 0, 0]但如果你用同样的思路创建二维列表# 错误示范 matrix [[0] * 3] * 3 print(matrix) # [[0, 0, 0], [0, 0, 0], [0, 0, 0]] matrix[0][0] 1 print(matrix) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]这就是经典坑点。[[0] * 3] * 3外面那层乘法复制的是内部列表的引用而不是内容。三个子列表指向同一块内存改一处全变。正确做法是matrix [[0] * 3 for _ in range(3)]3. 增删改查完整操作速查与易错点这块是 list 的核心操作区。我把常用方法整理成一张表但光有表不够——下面每一行我都得说说那个方法背后藏着的坑。操作类型方法/语法说明是否原地修改追加单个元素lst.append(x)在末尾添加一个元素x 作为整体加入是返回 None追加多个元素lst.extend(iterable)把可迭代对象的元素逐个加入是返回 None指定位置插入lst.insert(index, x)在 index 位置插入 x原元素后移是返回 None按值删除lst.remove(x)删除第一个匹配的 x不存在的值报 ValueError是返回 None按下标删除lst.pop(index-1)删除并返回指定下标元素默认为最后一个是返回被删元素清空列表lst.clear()删除所有元素是返回 None按切片删除del lst[start:stop]删除切片范围内的元素是查找下标lst.index(x)返回第一个匹配元素的下标否统计出现次数lst.count(x)返回元素出现次数否判断存在x in lst返回布尔值否3.1 append 和 extend 的区别你必须刻进脑子里热词里有人问“list add一个对象”在 Python 里对应的就是 append 和 extend 的混淆问题。记住append 是把整个对象作为单个元素加入extend 是把可迭代对象里的元素逐个加入。a [1, 2, 3] a.append([4, 5]) print(a) # [1, 2, 3, [4, 5]]列表里套了一个列表 b [1, 2, 3] b.extend([4, 5]) print(b) # [1, 2, 3, 4, 5]元素被拆开加入了如果你用 append 添加一个字符串不会报错但结果大概率不是你想要的lst [1, 2] lst.append(abc) # [1, 2, abc]字符串作为一个元素 lst.extend(abc) # [1, 2, a, b, c]字符串被拆成了字符这在处理文本数据时特别容易踩中。热词里有人搜“C#线程安全list”那是在找线程安全的列表容器。Python 这边同样有类似问题多线程同时 append 同一个 listCPython 的 GIL 会帮你省去很多原子操作的烦恼但lst [x]这种复合操作不是原子的多线程下依然会有竞态条件。自己写并发代码时要么加锁要么用 queue.Queue。3.2 remove、pop、del 三兄弟很多新手会被这三个删除方式搞晕。我给你们一个选择逻辑知道元素的值、不管位置、而且只删第一个匹配项 →remove知道位置、需要拿到被删的值继续用 →pop知道位置、不需要返回值 →del lst[index]想清空列表 →lst.clear()或者del lst[:]特别注意remove(x)只删第一个匹配项不会把所有相同值都删掉。如果你想删除全部匹配项可以用列表推导式重建也可以循环删lst [1, 3, 2, 3, 4, 3] # 删除所有 3错误示范边遍历边删会漏删 # for item in lst: # if item 3: # lst.remove(item) # 正确方式一列表推导式重建 lst [item for item in lst if item ! 3] # 正确方式二while 循环反复删 lst [1, 3, 2, 3, 4, 3] while 3 in lst: lst.remove(3)个人推荐列表推导式重建简单高效。while 循环里反复用 in 判断最坏情况是 O(n²)数据量大了会很慢。3.3 排序的两种方式别搞混了lst [3, 1, 2] # 方式一原地排序不返回新列表 lst.sort() print(lst) # [1, 2, 3] # 方式二返回新列表不影响原列表 lst2 [3, 1, 2] new_lst sorted(lst2) print(lst2) # [3, 1, 2] print(new_lst) # [1, 2, 3]这就是热词里“三种修饰符修饰 listlist 中的值还能修改或删除吗”这种面试题的底仓。不管用什么修饰符关键是搞清楚哪些方法原地修改、哪些方法返回新对象。sort()是原地修改sorted()是返回新列表。同理lst.reverse()原地反转reversed(lst)返回一个迭代器。还有一个经常被忽略的点列表排序默认是升序。想降序就加参数reverseTrue。想按自定义规则排用key参数words [banana, apple, cherry, date] words.sort(keylen) # 按长度排序[date, apple, cherry, banana]4. 切片Python 里最优雅也最容易出错的语法切片是 list 的招牌特性没有哪个语言能像 Python 这样用几个冒号就把数组操作写得如此简洁。但越是简洁的语法越容易在细节上出问题。4.1 切片基本语义lst [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 基本切片 lst[2:5] # [2, 3, 4]左闭右开含头不含尾 lst[:3] # [0, 1, 2]开头省略默认从 0 开始 lst[7:] # [7, 8, 9]结尾省略默认到末尾 lst[-3:] # [7, 8, 9]负数索引从末尾往前数 lst[::2] # [0, 2, 4, 6, 8]步长为 2 lst[::-1] # [9, 8, 7, ..., 0]步长为 -1整个列表反转很多初学者对“左闭右开”不习惯觉得为什么不是lst[2:5]表示 2 到 5 都包含呢。但左闭右开有一个实际好处切片长度可以直接用 stop - start 算出来而且能跟 range() 保持一致的语义。你习惯以后会发现处理半开区间比闭区间安全得多。4.2 切片是浅拷贝一个不小心就改了原数据这是热词里“列表切片”相关搜索里最容易踩的坑。切片的本质是创建新列表但新列表里的元素是指向原列表元素的引用。对于不可变对象整数、字符串、元组因为不能被修改所以lst[:]后怎么改都不会影响原列表。但对于可变对象嵌套列表、字典情况就完全不同matrix [[1, 2], [3, 4]] copy matrix[:] copy[0][0] 999 print(matrix) # [[999, 2], [3, 4]]可以看到copy和matrix是两个不同的列表对象但它们内部的子列表指向同一块内存。这就是浅拷贝。如果你需要完全独立的副本深拷贝用 copy 模块import copy matrix [[1, 2], [3, 4]] deep_copy copy.deepcopy(matrix) deep_copy[0][0] 999 print(matrix) # [[1, 2], [3, 4]]4.3 切片赋值一个容易被忽略的“炸弹”你可能知道切片可以读取但切片也可以直接赋值用来替换列表的一部分。这个语法很强大但也容易出意外lst [0, 1, 2, 3, 4, 5] lst[1:3] [100, 200, 300] print(lst) # [0, 100, 200, 300, 3, 4, 5]注意切片赋值时右侧的列表长度不需要和左侧切片长度相等。左侧切片被替换成右侧所有元素整个列表的长度会随之变化。这跟单点赋值完全不同。用这个特性可以轻松实现“列表中间插入一段数据”lst [0, 5] lst[1:1] [1, 2, 3, 4] # 在下标 1 处插入一段 print(lst) # [0, 1, 2, 3, 4, 5]我自己写代码时这个操作偶尔会带来惊喜但更多时候是惊吓——特别是别人维护的代码里出现这种魔法语法可读性确实是大问题。如果不是必要建议少用这种“神操作”。5. 遍历与就地修改最隐蔽的运行时炸弹遍历列表同时修改列表是所有 Python 新手都会遇到、也最容易写错的场景。热词里“多选列表 删除”、“列表拖曳排序”都避不开这个底层问题。5.1 边遍历边删除为什么会出现漏删看这个经典例子lst [1, 2, 3, 4, 5, 6] for item in lst: if item % 2 0: lst.remove(item) print(lst) # 期望 [1, 3, 5]实际 [1, 3, 5]你再跑一遍试试等等这个例子在 Python 里跑结果确实是[1, 3, 5]看起来没问题。但换成另一个写法就翻车了lst [1, 2, 3, 4, 5, 6] for i in range(len(lst)): if lst[i] % 2 0: lst.pop(i)这个不仅报错而且逻辑完全乱掉。原因是当你删除一个元素后后面的元素会自动前移补位但循环的下标还在按原来的节奏递增导致某些元素被跳过。真正只会在遍历中漏删的例子是这样的lst [1, 2, 2, 3, 4] for item in lst: if item 2: lst.remove(item) print(lst) # [1, 3, 4]还有一个 2 没删干净原因就是remove删掉了第一个 2 后第二个 2 前移到了刚才第一个 2 的位置但循环已经指向下一个位置了第二个 2 被跳过。5.2 正确的删除姿势最简单的方案是倒序遍历。从后往前删删除元素只影响后面的下标而你已经遍历过的位置不受影响lst [1, 2, 2, 3, 4] for i in range(len(lst) - 1, -1, -1): if lst[i] 2: lst.pop(i) print(lst) # [1, 3, 4]第二种方案是遍历副本修改原列表lst [1, 2, 2, 3, 4] for item in lst[:]: if item 2: lst.remove(item) print(lst) # [1, 3, 4]第三种方案是直接重建列表这是最推荐、最安全、性能也最好的方式lst [1, 2, 2, 3, 4] lst [item for item in lst if item ! 2]第三种方案的特殊价值在于它不会踩任何坑而且表达非常清晰——你要的就是一个“排除掉某些元素的新列表”。你在面试里被问到“遍历时删元素”这种题直接说“我建议不修改原列表而是用列表推导式重建”面试官会对你刮目相看。5.3 enumerate同时拿下标和值想在遍历时同时拿到下标和值无数初学者会这么写lst [10, 20, 30] for i in range(len(lst)): print(i, lst[i])但在 Python 里正确姿势是 enumeratefor i, value in enumerate(lst): print(i, value)在“多选列表删除”或者“列表项拖拽排序”这类需求里enumerate 几乎是必备工具。比如你有个多选删除功能拿到的一组下标是 [1, 3, 5]如果顺序删会出问题因为删除前面的元素后后面的下标就变了。你需要从大到小排序后依次删除或者用列表推导式按下标重建indexes_to_remove {1, 3, 5} lst [value for i, value in enumerate(lst) if i not in indexes_to_remove]6. 面试高频题与综合实战李白打酒热词里有一个很有意思的词条李白打酒python。这道题在网上流传很广是一道经典编程题同时也是 list 操作的极好练习。我把题简化一下李白提着酒壶出门壶中原来有酒 2 斗。他遇到商店就加酒一倍遇店加一倍遇到花就喝掉 1 斗。最后一次遇到的必然是花而且喝完了壶里的酒。已知他一共遇到店 5 次、花 10 次问有多少种可能的遇店遇花顺序。这道题如果用 list 来实现核心思路是模拟和回溯。但我不打算在这篇文章里展开完整回溯那是递归专题的内容我想借它展示 list 的几个综合操作def check(order): wine 2 for action in order: if action 0: # 0 表示店 wine * 2 else: # 1 表示花 wine - 1 if wine 0: return False return wine 0 # 用 list 生成所有店和花的组合的一个简单示例 # 5 个 010 个 1穷举所有排列会非常大所以这道题通常用回溯 store [0] * 5 flower [1] * 10 order store flower # 之后用 itertools.permutations 做去重排列然后逐个 check这里用到的正是[0] * 5的乘法创建、拼接、以及列表作为可迭代对象被遍历。你如果能把这一整篇文章的操作吃透写这道题的模拟部分会非常顺手。6.1 经典面试题可变默认参数所有 Python 面试题里几乎必考的一道 list 题def add_item(item, lst[]): lst.append(item) return lst print(add_item(1)) # [1] print(add_item(2)) # [1, 2] print(add_item(3)) # [1, 2, 3]明明每次调用没传第二个参数为什么结果会累积因为默认参数是在函数定义时求值并缓存下来的lst[]只创建了一次所有调用共用同一个列表。正确写法是def add_item(item, lstNone): if lst is None: lst [] lst.append(item) return lst这个坑的本质还是“list 是可变对象”这件事。你如果理解了第 1 节说的“list 存的是引用”就能理解为什么默认参数不能用可变对象。6.2 list 和 array 的抉择热词里有人搜“python list和array区别”咱们也顺带讲清楚。Python 自带的 array 模块、以及第三方库 NumPy 的 ndarray底层都是同质数组存的是真实数据的紧凑排列不存指针和对象头所以内存占用远小于 list。但 array 里的元素类型必须一致你不能混装字符串和数字。list的优势是灵活、方法多、和 Python 内置语法深度集成切片、推导式、for 循环array的优势是省内存、能做真正的 C 级数组操作。做科学计算选 NumPy做通用数据处理选 list内存吃紧且数据同质才考虑 array 模块。这个选择题没有绝对答案看场景。6.3 判断列表为空别用 len()最后一个小技巧。判断一个 list 是否为空的“Pythonic 方式”# 不推荐 if len(lst) 0: pass # 推荐 if not lst: pass # 判断不为空 if lst: pass因为空列表本身就是 False 值非空列表是 True 值。直接if lst简洁、高效也是所有 Python 老手的一致习惯。7. 常见问题与排查技巧实录这部分我想把平时大家搜得最多的几个 list 相关问题集中整理一下像一份速查手册遇到问题直接翻。7.1 为什么 max、min、sum 对纯数字 list 很管用如果你的 list 里全是数字可以直接用内置函数nums [3, 6, 2, 8, 1, 9] print(max(nums)) # 9 print(min(nums)) # 1 print(sum(nums)) # 29但热词里有人问“python float数字的list相加精度丢失”这就涉及浮点数的经典问题lst [0.1, 0.2] print(sum(lst)) # 0.30000000000000004这不是 list 的问题而是二进制浮点数的固有限制。如果对精度有严格需求比如金额计算不要用 float改用decimal.Decimal。什么时候会遇到这种问题比如处理接口返回的浮点数列表求和、做统计报表时稍不注意就会在界面上展现出 0.30000000000000004 这种数字。7.2 列表里的重复元素去重去重的传统做法是用集合但集合是无序的严格说 Python 3.7 的 dict 有序但 set 仍然无序。如果你既要去重又要保持原顺序lst [3, 1, 3, 2, 1, 4] seen set() result [] for item in lst: if item not in seen: seen.add(item) result.append(item) print(result) # [3, 1, 2, 4]这段代码是面试常客体现了 list、set、遍历的综合运用。7.3 二维列表的转置热词里有人搜“鸿蒙 arkts 多选列表 删除”那是 ArkTS 的题但 Python 里处理二维 list 的常见需求是先转置。比如把行转列matrix [ [1, 2, 3], [4, 5, 6], ] transposed list(zip(*matrix)) print(transposed) # [(1, 4), (2, 5), (3, 6)]如果你想要的是列表而不是元组transposed [list(row) for row in zip(*matrix)] print(transposed) # [[1, 4], [2, 5], [3, 6]]这个技巧在数据处理时非常常用zip(*matrix) 这个星号解包操作很多老手都在用但新手往往看不懂。它就是“把 matrix 的每一行作为单独参数传给 zipzip 再把每个行的同列元素组合成新元组”。7.4 list 拼接的三种方式a [1, 2] b [3, 4] # 方式一 c a b # [1, 2, 3, 4]返回新列表 # 方式二extend a.extend(b) # 修改 a 本身a 变成 [1, 2, 3, 4] # 方式三列表推导式 c [x for x in a] [x for x in b] # 很少这么写不推荐会创建新列表适合用在需要保留原列表的场景extend 是就地修改性能更好但会改变原数据。另外注意*也可以复制列表lst [1, 2] lst2 lst * 3 print(lst2) # [1, 2, 1, 2, 1, 2]前面提到的[[0] * 3] * 3浅拷贝坑就是从这个特性延伸出来的。7.5 大数据量下 list 的性能优化如果你在处理上百万条数据的 list有几个实用的优化方向预先分配容量虽然 Python 没有公开的 reserve 接口但可以通过[None] * n预先创建指定长度的列表再按下标赋值避免反复扩容。避免在头部插入频繁在头部插入考虑用collections.deque它的左右两端插入都是 O(1)。查找频繁时转 setx in list是 O(n)x in set是 O(1)。如果你频繁做成员判断先转成 set 收益巨大。用map或推导式代替显式 for 循环尤其是在做批量转换时列表推导式通常比 for append 快 10% 到 30%。热词里有一条“oracle查询报错 maximum number of expressions in list 1000”那是 SQL 里 IN 列表超过 1000 项的报错。但在 Python 这里没有这个限制——你随便往 list 里塞多少元素它都能扛住只是内存和性能要自己心里有数。8. 我的个人实操体会从学 Python 到现在list 是我用得最多的数据结构没有之一。但有意思的是越是基础的东西越值得反复琢磨。切片底层那点浅拷贝的细节、遍历时删除的错位问题、默认参数的可变陷阱——每一条我都踩过每一次踩完都有一种“原来如此”的感觉。我给你的建议是不要把 list 当成“会用就行”的 API 集合。花半小时把每个方法的返回值是返回新列表还是原地修改并返回 None都验证一遍把切片的不同写法都打印出来看一眼。这些看起来笨拙的练习能在未来无数次调试中帮你省下大量时间。最后再分享一个小技巧调试 list 时与其 print 整个列表不如直接print(len(lst), lst[:10])既能看到长度又不会被超长列表刷屏。写脚本处理大规模数据时这个习惯真的能保住你的眼睛。