Python的列表操作把我坑惨了,原来append和extend的区别这么大
小陈那天差点把键盘摔了。他写了一个数据处理脚本需要把多个来源的数据合并到一个大列表里。逻辑很简单先创建一个空列表all_data然后遍历每个数据源把里面的每条记录加进去。代码大概长这样all_data [] source1 [1, 2, 3] source2 [4, 5, 6] all_data.append(source1) all_data.append(source2) print(all_data)他以为会得到[1, 2, 3, 4, 5, 6]。结果终端打印出来的是[[1, 2, 3], [4, 5, 6]]列表里套了两个列表完全不是他想要的扁平结构。他盯着屏幕愣了几秒然后改成extend问题瞬间解决。他叹了口气“就差了四个字母结果天差地别。”这个坑太常见了。append和extend看起来都是往列表里加东西但加的方式完全不同。用错了数据结构就会悄悄变形而且往往不会报错直到下游代码崩溃你才发现。先看一个最直观的对比我们把两个方法放在一起跑一遍lst1 [1, 2, 3] lst2 [4, 5, 6] lst1.append(lst2) print(append 之后:, lst1) lst3 [1, 2, 3] lst4 [4, 5, 6] lst3.extend(lst4) print(extend 之后:, lst3)输出append 之后: [1, 2, 3, [4, 5, 6]] extend 之后: [1, 2, 3, 4, 5, 6]append把lst2当成一个整体塞进了lst1的末尾。lst1的长度从3变成4第四个元素是[4, 5, 6]这个列表对象。extend把lst4里的每个元素逐个取出来依次追加到lst3末尾。lst3的长度从3变成6六个元素都是独立的整数。用一个生活场景来理解你有一个水果篮里面已经装了苹果。现在你手里有一袋橘子。append的做法是把整袋橘子直接扔进水果篮。篮子里现在有一个苹果和一袋橘子。extend的做法是把袋子打开把里面的橘子一个一个拿出来放进水果篮。篮子里现在有一个苹果和若干个橘子。区别一目了然。append只加一个东西append的签名是list.append(x)。它接受任意一个对象x把它作为单个元素追加到列表末尾。列表长度只增加1。这个“任意对象”包括整数、字符串、列表、字典、元组、自定义对象甚至另一个列表。不管x是什么append都把它当作一个整体。lst [] lst.append(1) # [1] lst.append(hello) # [1, hello] lst.append([2, 3]) # [1, hello, [2, 3]] lst.append({a: 1}) # [1, hello, [2, 3], {a: 1}]每次调用列表长度加1。元素就是传入的那个对象本身。extend拆开可迭代对象逐个添加extend的签名是list.extend(iterable)。它接受一个可迭代对象遍历它把每个元素依次追加到列表末尾。列表长度增加的是可迭代对象的长度。lst [] lst.extend([1, 2, 3]) # [1, 2, 3] lst.extend(abc) # [1, 2, 3, a, b, c] lst.extend((4, 5)) # [1, 2, 3, a, b, c, 4, 5] lst.extend({x: 1, y: 2}) # 添加的是键[x, y]注意最后一行字典是可迭代对象但迭代它得到的是键不是键值对。所以extend一个字典会把它的键逐个加进去。如果你想要值得用dict.values()。extend要求参数必须是可迭代的。如果传一个整数会直接报错lst [] lst.extend(5)TypeError: int object is not iterable而append不会报错它接受任何对象。为什么字符串是重灾区字符串是最容易被误用的可迭代对象。假设你想把一个字符串作为一个整体添加到列表里lst [hello] lst.append(world) print(lst) # [hello, world]没问题。但如果你手滑用了extendlst [hello] lst.extend(world) print(lst) # [hello, w, o, r, l, d]字符串被拆成了单个字符。因为字符串是可迭代的extend会遍历它把每个字符当作一个元素加进去。这种错误在处理文件路径、用户名、标签时特别常见。你以为加了一个字符串结果列表里多了一堆字符。字典也是坑字典也是可迭代对象但迭代的是键。所以lst [] lst.extend({name: Alice, age: 30}) print(lst) # [name, age]如果你想要的是字典本身作为一个元素应该用appendlst [] lst.append({name: Alice, age: 30}) print(lst) # [{name: Alice, age: 30}]如果你想把字典的值加进去应该用extend(d.values())。性能差异extend通常更快除了语义不同extend在性能上也通常优于多次append。因为extend是在C层面实现的它知道要添加多少个元素可以一次性调整列表的容量减少内存重新分配的次数。而循环调用append每次都可能触发容量检查虽然Python的列表有动态扩容机制但反复调用仍有开销。我们来实测一下import time # 用 append 逐个添加 start time.time() lst [] for i in range(1000000): lst.append(i) print(append 耗时:, time.time() - start) # 用 extend 一次性添加 start time.time() lst [] lst.extend(range(1000000)) print(extend 耗时:, time.time() - start)在我的机器上append循环大约0.08秒extend大约0.02秒。差距明显。当然如果你已经有现成的列表直接extend比写循环append更简洁也更快。那和呢列表还支持运算符和运算符它们也能合并列表。a [1, 2] b [3, 4] c a b # [1, 2, 3, 4] a b # a 变成 [1, 2, 3, 4]会创建一个新列表原列表不变。是原地修改等价于extend。注意和extend一样会把右边的可迭代对象拆开。但要求两边都是列表不能直接加字符串或元组。a [1, 2] a abc # 等价于 extend得到 [1, 2, a, b, c] a a abc # 报错列表不能和字符串相加所以如果你想把一个字符串拆成字符加进去用或extend如果你想把字符串作为一个整体用append。切片赋值也能实现类似效果还有一种不常见但很强大的方式切片赋值。lst [1, 2, 3] lst[len(lst):] [4, 5, 6] print(lst) # [1, 2, 3, 4, 5, 6]这等价于extend。但可读性不如extend一般不建议这样写。一个容易忽略的细节extend 可以接受任何可迭代对象extend的参数不一定是列表。它可以是任何可迭代对象元组、集合、生成器、range、字符串、字典的键视图甚至是你自己实现的迭代器。lst [] lst.extend(range(3)) # [0, 1, 2] lst.extend({10, 20}) # 顺序不确定集合是无序的 lst.extend(x for x in [7, 8]) # [..., 7, 8]这意味着你可以直接把一个生成器传给extend它会边迭代边添加。这在处理大数据时很有用不需要先构建一个中间列表。append 和 extend 的返回值都是 None这是一个小细节但容易导致链式调用出错。lst [1, 2] result lst.append(3) print(result) # Noneappend和extend都是原地修改列表不返回新列表。所以你不能这样写lst [1, 2].append(3) # lst 是 None这是新手常犯的错误。记住它们修改原列表返回None。什么时候用 append什么时候用 extend判断标准很简单你想加一个东西还是想加一堆东西加一个东西用append。无论这个东西是整数、字符串、列表还是字典都作为一个整体。加一堆东西用extend。这一堆东西必须是一个可迭代对象里面的每个元素都会被逐个添加。如果你有一个列表source想把它里面的元素合并到target里用target.extend(source)。如果你想把source本身作为target的一个元素用target.append(source)。回到小陈的代码小陈原本的代码all_data [] source1 [1, 2, 3] source2 [4, 5, 6] all_data.append(source1) all_data.append(source2)得到的是[[1, 2, 3], [4, 5, 6]]。他想要的是扁平列表所以应该用extendall_data [] all_data.extend(source1) all_data.extend(source2)得到[1, 2, 3, 4, 5, 6]。如果数据源很多也可以写成循环all_data [] for source in sources: all_data.extend(source)或者更Pythonic的写法all_data [item for source in sources for item in source]但extend在性能上通常更好尤其是数据量大时。一个更隐蔽的坑append 添加的是引用最后提一个和append相关但容易被忽略的问题append添加的是对象的引用不是副本。lst [] inner [1, 2] lst.append(inner) inner.append(3) print(lst) # [[1, 2, 3]]修改inner之后lst里的那个元素也跟着变了因为它们指向同一个列表对象。如果你想要一个独立的副本需要显式复制lst.append(inner.copy())这个问题在循环中批量append时尤其危险。比如lst [] for i in range(3): row [i] * 3 lst.append(row)每次循环都新建了row所以没问题。但如果你重用同一个列表lst [] row [0, 0] for i in range(3): row[0] i lst.append(row) print(lst)输出是[[2, 0], [2, 0], [2, 0]]因为三个元素都指向同一个row。这是另一个常见的列表陷阱和append/extend的区别无关但同样值得警惕。总结append和extend的区别本质上是“加一个”和“加一串”的区别。append(x)把x作为单个元素追加列表长度加1。x可以是任何对象。extend(iterable)遍历iterable把每个元素逐个追加列表长度增加len(iterable)。iterable必须是可迭代对象。用错了列表结构就会变形。append一个列表会得到嵌套列表extend一个字符串会得到一堆字符。记住那个水果篮的比喻append是整袋扔进去extend是倒出来一个个放。想清楚你要的是哪种就不会再被坑了。小陈后来在代码注释里写了一句话“合并列表用extend添加单个用append。别再用append加列表了。” 这个教训值一下午的调试时间。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →