ARTICLE DETAIL

资讯详情

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

Python列表与元组深度对比:从底层机制到实战踩坑

Python列表与元组深度对比:从底层机制到实战踩坑 列表和元组是我日常写Python碰得最多的两种内置序列类型说它们是Python里最核心的数据容器也不为过。很多初学者学到这块就开始犯迷糊列表和元组看起来差不多都能存一堆数据、都能下标访问那为什么Python非得弄出两个到底什么时候该用哪个我见过不少写了两年Python的人还在所有场景下都用列表元组只会在函数返回多个值时被动用到。这篇把这两个数据类型完整拆一遍从底层机制到高频操作从性能差异到实际踩坑配合可以直接跑通的代码示例新手看完能直接上手已经写过一段时间的也能借这个机会查漏补缺。1. 列表Python里最活跃的“动态数组”1.1 列表的创建与访问列表在Python里是最基础的可变序列你用方括号[]就能创建一个列表也可以用内置的list()函数来做类型转换。我平时写得最多的几种创建方式# 直接字面量创建 fruits [apple, banana, cherry] # 用list()转换 nums list(range(10)) # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] chars list(hello) # [h, e, l, l, o] # 空列表 empty []关于热搜词里的一个问题“python列表是有序序列吗”这里直接给出明确答案列表是标准的有序序列。所谓有序指的是列表会按照元素插入的顺序维护它们的排列位置每一个元素都会有一个确定的索引值你可以用位置来访问它。这一点和集合set有本质区别集合是无序的不能通过位置下标来获取元素。访问列表元素的方式有正序索引和负索引两种fruits [apple, banana, cherry, durian] print(fruits[0]) # apple第一个元素 print(fruits[-1]) # durian最后一个元素负索引是Python一个非常方便的设计-1永远表示最后一个元素-2表示倒数第二个省去了“先取长度再减一”的麻烦。我第一次用C语言写类似逻辑时每次都写arr[len(arr)-1]换到Python之后直接用arr[-1]这个体验差距不是一点半点。列表能存任意类型的对象同一个列表里可以混合整数、字符串、甚至嵌套其他列表或字典mixed [1, hello, 3.14, [1, 2, 3], {name: Tom}]这种“什么都能装”的特性源于Python的列表本质上维护的是一个指针数组每个元素只是一个对象引用而不是对象本身。这和C语言或Java里的数组有本质区别那种数组要求所有元素类型一致、存储空间连续而Python的列表因为存的是引用所以天然支持异构数据代价是内存占用稍高一些。1.2 切片列表操作里的高频利器切片是列表和元组最让人舒服的语法之一。基本格式是[start:stop:step]要特别注意切片范围是左闭右开也就是包含start位置的元素但不包含stop位置的元素。nums [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] # 取前3个 print(nums[:3]) # [0, 1, 2] # 取第2到第5个索引1到4 print(nums[1:5]) # [1, 2, 3, 4] # 取最后3个 print(nums[-3:]) # [7, 8, 9] # 步长为2每隔一个取一个 print(nums[::2]) # [0, 2, 4, 6, 8] # 反转列表 print(nums[::-1]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]左闭右开的设计很多初学者会懵但习惯了会发现它逻辑很自洽。比如你想把列表分成前后两半直接写nums[:len(nums)//2]和nums[len(nums)//2:]就能严丝合缝地拼接回完整列表。range()函数也遵循同样的规则这是Python整体设计的一致性体现。切片还有一个容易被忽略的妙用复制列表。list[:]会返回一个新列表内容是原列表的浅拷贝。这在需要独立修改副本而不影响原列表时非常好用original [1, 2, 3] copy original[:] # 新列表 copy.append(4) print(original) # [1, 2, 3] —— 没变 print(copy) # [1, 2, 3, 4]切片操作返回的始终是新对象因此不会修改原列表。如果你需要原地修改列表的某一段可以直接给切片赋值nums [0, 1, 2, 3, 4] nums[1:4] [10, 20] print(nums) # [0, 10, 20, 4]这种切片赋值的灵活性是列表这种可变对象特有的能力元组因为不可变就无法做类似操作。1.3 常用方法这些方法必须刻进肌肉记忆列表的方法说多不多说少不少但有几个我几乎每天都在用建议初学者先把这些刻进肌肉记忆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是把参数当作“一个元素”塞进去extend是把参数“拆开”逐个塞进去。这个区别我见过好几个人在面试里栽过跟头。insert(index, element)在指定位置插入元素pop(index)删除指定位置元素并返回它remove(value)删除第一个匹配到的指定值nums [1, 2, 3] nums.insert(1, 99) print(nums) # [1, 99, 2, 3] last nums.pop() # 默认删除最后一个 print(last, nums) # 3 [1, 99, 2] nums.remove(99) print(nums) # [1, 2]注意pop和remove的参数语义完全不同pop接收的是索引remove接收的是值。如果你要删除的元素在列表中不存在remove会抛ValueError所以实际开发中我习惯先判断再删除或者用try/except兜底。index(value)返回元素第一次出现的索引count(value)统计元素出现的次数nums [1, 2, 2, 3, 2] print(nums.index(2)) # 1 print(nums.count(2)) # 3排序有两个途径sort()是列表的原地排序方法直接修改原列表sorted()是Python内置函数返回一个新列表。如果你还需要保留原始顺序务必用sorted()nums [3, 1, 2] nums.sort() print(nums) # [1, 2, 3] 原列表被修改 nums2 [3, 1, 2] sorted_nums sorted(nums2) print(nums2, sorted_nums) # [3, 1, 2] [1, 2, 3]配合关键字参数key可以实现自定义排序逻辑。比如按字符串长度排序、按字典里的某个键排序这种场景和“lambda表达式Python列表”这个热搜词紧密相关words [banana, apple, cherry, date] words.sort(keylambda x: len(x)) print(words) # [date, apple, banana, cherry] students [ {name: Tom, score: 88}, {name: Jerry, score: 95}, {name: Alice, score: 72}, ] students.sort(keylambda s: s[score], reverseTrue) print(students) # 按成绩从高到低排序key参数接收的是一个函数列表里的每个元素都会先经过这个函数处理然后按处理结果排序。lambda x: len(x)本质就是定义一个匿名的、接收一个参数并返回其长度的函数。你也可以用operator.itemgetter替代 lambda性能会稍微好一点但lambda的可读性在简单场景下足够。reverse()是原地反转列表注意它和在切片里用[::-1]的区别前者修改原列表后者返回新列表。1.4 列表推导式写起来很爽的语法糖列表推导式是我个人认为Python最优雅的语法之一。它用单行表达式替代多行循环把“创建一个列表并对元素做变换”这个高频操作压缩成一行代码。基本形式是[表达式 for 元素 in 可迭代对象]# 普通写法 squares [] for i in range(10): squares.append(i * i) # 推导式写法 squares [i * i for i in range(10)] print(squares) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]还可以加过滤条件ifeven_squares [i * i for i in range(20) if i % 2 0] print(even_squares) # [0, 4, 16, 36, 64, 100, 144, 196, 256, 324]嵌套写法比如生成一个3x3的矩阵matrix [[row * 3 col for col in range(3)] for row in range(3)] print(matrix) # [[0, 1, 2], [3, 4, 5], [6, 7, 8]]但我要说一个经验列表推导式不是越复杂越好。嵌套超过两层、或者条件逻辑太多的时候可读性会急剧下降。我见过有人写出五行都拆不开的推导式这种代码维护起来非常痛苦。我的原则是推导式能在一行内清晰表达的时候就使用否则宁可拆成普通循环。代码是写给人看的顺便给机器执行可读性永远排在第一位。另外一个值得了解的区别是列表推导式和生成器表达式的内存差异。列表推导式会一次性生成完整的列表如果数据量很大比如上百万个元素内存占用会很高。生成器表达式用圆括号而不是方括号它不会一次性生成全部元素而是惰性求值# 生成器表达式 gen (i * i for i in range(1000000)) print(gen) # generator object genexpr at 0x... print(next(gen)) # 0 print(next(gen)) # 1大数据量场景下能用生成器就用生成器。2. 元组不可变序列为什么不可替代2.1 元组的创建与访问元组用圆括号()表示但真正让元组成为元组的不是括号而是逗号。这是一个极其经典的陷阱t1 (1, 2, 3) # 元组 t2 1, 2, 3 # 也是元组括号可省略 print(type(t1), type(t2)) # class tuple class tuple # 陷阱一个元素的元组必须加逗号 t3 (1) # 这是整数 t4 (1,) # 这才是元组 print(type(t3)) # class int print(type(t4)) # class tuple这个坑我见过太多次了。(1)在Python解析器眼里就是一个普通的括号表达式相当于直接写了个1而(1,)才表示包含一个元素1的元组。如果以后看到某个元组变量只有括号没逗号导致类型不对先检查这里。你也可以用tuple()函数创建元组t tuple([1, 2, 3]) # (1, 2, 3) empty tuple() # ()元组同样支持索引、切片这一点和列表一致t (10, 20, 30, 40, 50) print(t[0]) # 10 print(t[-1]) # 50 print(t[1:3]) # (20, 30)但元组不支持你修改元素。尝试t[0] 100会直接抛TypeError: tuple object does not support item assignment这也是元组和列表最核心的区别。元组的“不可变性”听起来像是一种限制但在实际工程中这种限制恰恰是它的价值所在。2.2 不可变性带来的价值很多人不理解为什么Python要提供一个“不能改”的列表。不可变性至少带来三个好处第一哈希能力。元组可以作为字典的键、放进集合set里因为只有可哈希的对象才能参与这些操作。什么是可哈希简单理解就是一个对象能生成一个在其生命周期内恒定不变的哈希值。元组不可变所以它的哈希值可以稳定存在列表可变内容一变哈希值就对不上了所以Python直接禁止列表作为字典键# 元组可以做字典键 location {北京: (39.9, 116.4), 上海: (31.2, 121.5)} print(location[北京]) # (39.9, 116.4) # 列表不行会报 TypeError: unhashable type: list # d {[1, 2]: value}第二安全性。当你把一个元组传给函数时你不用担心函数内部会意外修改这个数据。特别是在多人协作的项目里有些数据是配置性的、约定好不能变的用元组就相当于加上了一道“只读”保险代码的意图一目了然。用列表的话任何拿到这个对象的人都能改动它出bug时排查范围会明显变大。第三多线程/并发安全。在多线程环境下不可变对象天然是线程安全的因为没有任何线程能修改它。虽然Python的GIL全局解释器锁让纯Python多线程并发情况下的数据竞争问题没那么严重但在写涉及共享数据的程序时能用一个不可变结构就尽量用不可变结构这是好习惯。还有一个直接的使用场景函数返回多个值本质上返回的是个元组def get_min_max(nums): return min(nums), max(nums) result get_min_max([3, 1, 4, 1, 5]) print(result) # (1, 5) print(type(result)) # class tuple我还记得第一次从C语言转Python时想返回两个值要先定义一个结构体或者用指针输出参数麻烦得要命。Python直接用一个元组就搞定了这也是元组在工程中被高频使用的原因之一。2.3 元组里的“可变态”元组的不可变性有一个重要的细节它是浅层不可变。元组里如果存了列表这个列表的元素是能被修改的t ([1, 2, 3], hello) t[0].append(4) print(t) # ([1, 2, 3, 4], hello)为什么因为元组存的是对象的引用它只保证“引用不能变”不保证“引用的对象不能变”。t[0]依然指向原来的那个列表对象只是这个列表对象里面的内容从[1,2,3]变成了[1,2,3,4]。这一点在生产环境中很容易被忽略如果你试图用元组来“冻结”一份动态数据要确保里面的元素也都是不可变的否则“冻结”就是个假象。这个特性反过来也提醒我们判断一个元组能不能作为字典键要看元组里所有元素是否都可哈希。如果一个元组里面嵌套了列表那它本身就是不可哈希的# TypeError: unhashable type: list # d {([1, 2], x): value}2.4 命名元组让元组变得更好读普通元组的短板是字段没有名字访问t[0]、t[1]这种写法在数据字段一多的时候可读性很差。collections.namedtuple就是为了解决这个问题诞生的它在普通元组的基础上给每个位置加上了字段名from collections import namedtuple Point namedtuple(Point, [x, y]) p Point(3, 5) print(p.x, p.y) # 3 5 print(p[0], p[1]) # 3 5 索引访问依然可用 # 更复杂的场景数据库查询结果用命名元组 Record namedtuple(Record, [id, name, score]) row Record(1, Tom, 88) print(f学生{row.name}的分数是{row.score})命名元组的本质还是元组所以它保持不可变性能哈希可以用作字典键。相比定义一个完整的类命名元组更轻量没有__dict__内存占用更少。在做数据处理、读取CSV行、数据库查询结果封装等场景下命名元组是性价比很高的选择。Python 3.7 还提供了 dataclasses 模块功能更强大适合需要默认值、复杂方法和继承的场合。数据类相对命名元组更“重量级”但也有自己的适用场景。选型原则很简单只是简单字段打包用命名元组需要加行为方法、属性校验用 dataclass 或普通类。3. 列表与元组的深度对比性能差在哪、什么时候用谁3.1 可变性差异背后的设计逻辑列表和元组最根本的区别就是可变性列表可变元组不可变。这个差异不是偶然的设计细节而是源于两者的定位完全不同。列表的设计目标是成为“通用容器”。它要应对各种动态场景收集数据、不断追加、按需删除、批量修改。为此它必须设计成可变的否则无法胜任这些功能。这也是为什么列表在实现上会采取“容量预分配”策略——当空间不足时会自动扩容并且会预留一部分额外空间避免每次追加都触发一次内存分配。元组的设计目标是“打包对象”。它更多是扮演一种固定的、不可被篡改的数据记录角色。想想坐标点(x, y)、RGB颜色(255, 0, 0)、一个月30天这种固定结构本质上它们就是一个“打包不可变的记录”不需要修改。如果把元组设计成可变的反而失去了稳定性和可哈希性。如果你还是觉得“可变性无所谓”想想这个场景你写了一个函数接收一个列表参数函数内部不小心调用了sort()原地排序结果调用者传入的原始列表顺序也被改了这是非常典型的毁坏性bug。换成元组后这种问题根本不会发生。不可变不是限制是保护。3.2 性能差异元组更轻更快在相同数据量下元组比列表更省内存访问速度也更快。原因在于二者的底层实现不同。列表是动态数组为了支持快速追加和插入它会预留额外的内存空间over-allocation。也就是说一个长度为5的列表底层分配的内存可能足够存8个元素。元组的大小是固定的不需要预留任何额外空间所以同样5个元素元组占用的内存一定比列表少。我们可以用sys.getsizeof实测一下import sys l [1, 2, 3, 4, 5] t (1, 2, 3, 4, 5) print(sys.getsizeof(l)) # 80不同版本可能有差异 print(sys.getsizeof(t)) # 80实际上小元组在某些情况下和列表差距不大这里要说明一下对于非常小的序列比如长度小于某个阈值Python有些优化手段会让差异不那么明显。但随着元素数量增长差距会体现出来。用一个大一点的列表测import sys l list(range(1000)) t tuple(range(1000)) print(sys.getsizeof(l)) # 8056 print(sys.getsizeof(t)) # 8040差距确实存在但没有大到“天壤之别”。真正更关键的性能差异在访问速度上因为元组不需要处理动态扩容相关的逻辑。用timeit简单测一下重复访问的时间import timeit l list(range(1000)) t tuple(range(1000)) time_list timeit.timeit(lambda: l[500], number10_000_000) time_tuple timeit.timeit(lambda: t[500], number10_000_000) print(time_list, time_tuple)实测元组访问通常比列表快5%10左右。这个差距在单个操作上无所谓但在高性能计算、大循环中积少成多就值得在意了。这也是为什么在性能敏感的代码里能用元组的地方就用元组。3.3 使用场景分析这几条原则直接照做结合我多年的开发经验给你一套非常明确的选择决策参考必须用元组的场景需要作为字典的键或放入集合因为元组可哈希列表不可以函数返回多个值时返回值本质就是一个元组不需要额外干预固定不变的配置项、常量组比如数据库连接参数、颜色RGB、地图坐标不希望被外部修改的数据记录用元组相当于加了一道“只读”锁性能敏感的只读数据序列元组更省内存更快必须用列表的场景需要动态增加、删除元素需要对数据进行排序、反转、原地修改收集程序运行中不断产生的新数据和其他API或库交互时部分库要求传入列表类型举例来说从数据库查询出来的多条记录每条记录用元组保存就很合适因为记录本身是固定结构不需要修改而收集这些记录的整体容器用列表因为你可能要对它追加记录、筛选排序。我实际处理爬虫数据时也经常这样组合每个页面解析出一批固定字段用元组表示一条数据然后再用一个列表把所有元组收起来。这样既保证了单条数据的不可篡改性又方便对整体数据做增删排序。再举个例子如果你要写一个坐标计算函数接收一个(lat, lng)坐标内部不会也不能修改这个坐标那参数类型声明为tuple就比list合理得多既能避免误改也向代码阅读者传递了“这是只读数据”的明确信号。4. 进阶技巧解包、传参和嵌套结构的实战玩法4.1 序列解包一行代码的优雅解包unpacking是Python序列最爽的特性之一它把一个序列里的元素自动“拆开”赋值给多个变量t (Tom, 25, 北京) name, age, city t print(name, age, city) # Tom 25 北京这个特性对于列表、元组、字符串、迭代器都适用。最有名的应用就是一行代码交换两个变量的值a, b 1, 2 a, b b, a print(a, b) # 2 1在C语言或者Java里交换两个变量需要一个临时变量tempPython用解包语法直接搞定语言表达力直接上一个台阶。解包还支持星号表达式可以收集多余的元素nums [1, 2, 3, 4, 5, 6] first, *middle, last nums print(first) # 1 print(middle) # [2, 3, 4, 5] print(last) # 6这个技巧在处理“首尾特殊、中间一致”的数据时非常有用。比如处理一份成绩单第一条是表头、最后一条是总分中间才是各科成绩用这一行解包就全部分开了。嵌套结构的解包也可以很优雅data [(Tom, 88), (Jerry, 95)] for name, score in data: print(name, score)这里每次循环自动把元组拆成两个变量配合for循环使用几乎成了处理列表元组数据的标配写法。4.2 在函数参数中的应用*args与**kwargs元组和列表在函数参数传递中扮演着非常重要的角色。*args可以让函数接收任意数量的位置参数在函数内部这些参数会被打包成一个元组def sum_all(*args): print(args) # args 是一个元组 total 0 for num in args: total num return total print(sum_all(1, 2, 3)) # 6 print(sum_all(1, 2, 3, 4, 5)) # 15**kwargs则接收任意数量的关键字参数打包成一个字典def show_info(**kwargs): print(kwargs) # {name: Tom, age: 25} show_info(nameTom, age25)反过来调用函数时也可以用星号把列表或元组“拆开”传入nums [1, 2, 3] print(sum_all(*nums)) # 等价于 sum_all(1, 2, 3) point {x: 1, y: 2} # 假设有函数 def f(x, y): ... # f(**point) 等价于 f(x1, y2)我经常用这个特性封装参数传递。比如读取一个配置文件里面有一组坐标点存成元组列表然后调用绘制函数时用*point解包代码干净又直观。4.3 嵌套数据结构的实际应用列表里套元组、元组里套列表、列表里套列表这种嵌套结构在处理结构化数据时无处不在。比如存一张二维表用列表作为行容器每行用列表表示各列数据table [ [name, age, city], [Tom, 25, 北京], [Jerry, 30, 上海], ] for row in table: print(row[0], row[1])如果需要处理“网格”类数据比如图像的像素矩阵、棋盘的状态列表嵌套列表即二维列表是基础解法。这里有一个特别实用的技巧生成二维列表时不能简单用乘法复制# 错误示范 matrix [[0] * 3] * 3 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)] matrix[0][0] 1 print(matrix) # [[1, 0, 0], [0, 0, 0], [0, 0, 0]]换个角度还有“浅拷贝”和“深拷贝”的问题。列表的copy()方法和[:]切片都只复制最外层的列表内部的子列表还是同一个引用。如果需要完全独立的副本用copy.deepcopyimport copy original [[1, 2], [3, 4]] shallow original.copy() deep copy.deepcopy(original) shallow[0].append(99) print(original) # [[1, 2, 99], [3, 4]] —— 浅拷贝内部的子列表还是同一个 print(deep) # [[1, 2], [3, 4]] —— 深拷贝完全独立如果你的程序要对嵌套数据做修改且不希望影响原数据“是不是同一个引用”这个坑一定要重视。很多诡异bug的根源都在这。4.4 用列表处理浮点数一维数据一个实际练习案例热搜词里有一条关于“py202.py 定义了一个6个浮点数的一维列表lt1和一个包含3个……”的内容虽然原题细节不完整但这种“给定浮点数列表做统计计算”的题目在学习和工作中太常见了。我模拟一个完整场景来演示列表的实际操作。假设lt1是一个包含6个浮点数的一维列表比如lt1 [78.5, 92.0, 85.5, 60.2, 88.8, 74.3]要求计算平均值、最大值和最小值并把列表按从大到小排序。lt1 [78.5, 92.0, 85.5, 60.2, 88.8, 74.3] # 平均值 avg sum(lt1) / len(lt1) print(f平均分{avg:.2f}) # 最大值和最小值 print(f最高分{max(lt1)}) print(f最低分{min(lt1)}) # 从大到小排序生成新列表不动原数据 sorted_desc sorted(lt1, reverseTrue) print(sorted_desc) # [92.0, 88.8, 85.5, 78.5, 74.3, 60.2]如果“包含3个”的意思是三元组描述我们可以用解包和针对列表的方法来做。比如有一个列表data里面包含三个浮点数列表分别代表三次实验的成绩想计算每次实验的平均值data [ [78.5, 92.0, 85.5, 60.2, 88.8, 74.3], [81.0, 77.5, 90.2, 69.8, 85.0, 79.9], [88.2, 83.0, 79.5, 91.2, 72.8, 86.6], ] for i, scores in enumerate(data, start1): avg sum(scores) / len(scores) print(f第{i}组成绩平均分{avg:.2f})这种遍历 统计 格式化的组合是Python日常数据处理的必修基本功列表方法、内置函数、循环、推导式全都能用上。5. 常见问题与排查技巧实录5.1 常见问题速查表我整理了一份高频报错和错误现象的速查表基本都是我平时答疑时反复遇到的错误现象可能原因解决方法IndexError: list index out of range访问了不存在的索引比如空列表取[0]先检查len()善用-1取末尾元素TypeError: tuple object does not support item assignment试图修改元组元素改用列表或重新创建一个新元组AttributeError: tuple object has no attribute append对元组调用列表专属方法确认类型需要修改数据就换成列表TypeError: unhashable type: list把列表作为字典键或集合元素改成元组或改用其他结构(1)不是元组是整数单元素元组忘加逗号写成(1,)列表赋值后一个改了另一个也变用b a复制的是引用用a[:]或list.copy()拿到副本remove()报ValueError删除的值不存在先in判断或使用try/except这张表标注的基本都是最典型的场景处理报错的第一步永远是先看类型。Python的报错信息其实已经写得很直白第二次遇到同类错误就应该能记住。5.2 避坑技巧可变对象默认参数Python里一个经典到不能再经典的坑不要用可变对象作为函数参数的默认值。def add_item(item, target[]): target.append(item) return target print(add_item(1)) # [1] print(add_item(2)) # [1, 2] —— 第二次调用时默认列表还保留着上一次的数据这个问题的根源是函数的默认参数在函数定义时就被创建了只有一份之后所有调用如果没有显式传值用的都是同一个列表对象。正确做法是用不可变对象None作为默认值函数内部再创建新列表def add_item(item, targetNone): if target is None: target [] target.append(item) return target虽然很多Python老手也会不小心踩到这个坑但这个模式其实很简单默认参数只给不可变类型可变对象的初始化放到函数体内部。把这个习惯养成之后能躲开一大批隐蔽bug。顺带提一句如果你在排查一个“多次调用结果互相影响”的函数bug优先怀疑默认参数。5.3 遍历列表时删除元素的坑遍历列表的过程中直接删除元素会导致元素下标错乱、漏删或跳删这是个非常常见的问题nums [1, 2, 3, 4, 5, 6] for n in nums: if n % 2 0: nums.remove(n) print(nums) # 期望 [1, 3, 5]实际得到 [1, 3, 5]上面这个例子结果看起来恰好对但换一组数据就会出问题nums [2, 4, 5, 6, 8] for n in nums: if n % 2 0: nums.remove(n) print(nums) # 实际得到 [4, 5, 8] —— 4和8没删掉原因是remove改变了列表长度索引跟着变化循环却还按原来的索引往后走自然就会跳过一些元素。正确的做法有三种一是遍历副本修改原列表nums [2, 4, 5, 6, 8] for n in nums[:]: if n % 2 0: nums.remove(n) print(nums) # [5]二是用列表推导式直接生成新列表nums [2, 4, 5, 6, 8] nums [n for n in nums if n % 2 ! 0] print(nums) # [5]三是倒序遍历删除nums [2, 4, 5, 6, 8] for i in range(len(nums) - 1, -1, -1): if nums[i] % 2 0: nums.pop(i) print(nums) # [5]我实际开发中最常用第二种方式简洁且不容易出错性能也好。5.4 “变量是引用”这个认知一定要建立Python里变量和对象的关系最准确的类比是“标签”而不是“盒子”。执行b a时并没有把a的数据复制一份给b而是让b和a贴到了同一个对象上。a [1, 2, 3] b a b.append(4) print(a) # [1, 2, 3, 4] —— 因为a和b指向同一个列表对象用内置函数id()可以看清对象的身份a [1, 2, 3] b a print(id(a), id(b)) # 完全相同 c a[:] print(id(a), id(c)) # 不同c是副本理解了“标签”模型之后很多以“我的变量怎么莫名其妙被改了”开头的bug就都好解释了。每次想让两个变量各自独立都必须显式地创建副本而不是简单赋值。再补充一个关于is和区别的坑。is比较的是“两个变量是不是同一个对象”比较的是“两个对象是否值相等”。对于小整数和短字符串Python有驻留机制is偶尔会返回True给人一种“用is也没问题”的错觉但换个大对象就翻车了a [1, 2, 3] b [1, 2, 3] print(a b) # True —— 值相等 print(a is b) # False —— 不是同一个对象判断值相等永远优先用判断“是否同一个对象”才用is例如检查默认参数是否被传入时用is None。5.5 我的一点调试经验优先确认类型碰到奇怪的问题第一反应先确认“这个东西到底是什么类型”。包括我在内很多bug最后定位到根因都是“我以为这变量是列表其实是元组”或者“我以为这是单个元素其实是个列表”。用一行print(type(x))往往比盯半天代码快得多。另外尽量在交互环境比如python -i或 IPython里做小实验。写完一段列表操作先跑一下看看输出对不对再放进正式代码。Python这种动态语言很多错误就是要跑一遍才能看出来早点跑、频繁跑反而比写一大段再调试省时间。我个人在实际操作中的体会是列表和元组看起来基础但所有高级的Python代码最终都要落到这些基础数据类型上。多花点时间把它们吃透后面学字典、集合、DataFrame处理都会顺利得多。最后再分享一个小建议——如果你在和别人协作开发尽量在你希望“这个数据不要被改”的地方使用元组这比任何注释都更直接有效因为代码本身就在表达意图。这个习惯我保持了多年帮我在很多项目里避免了原本可能莫名其妙的数据污染问题。
返回列表