小陈那天差点把键盘摔了。
他写了一个数据处理脚本,需要把多个来源的数据合并到一个大列表里。逻辑很简单:先创建一个空列表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']
没问题。但如果你手滑用了extend:
lst = ["hello"]
lst.extend("world")
print(lst) # ['hello', 'w', 'o', 'r', 'l', 'd']
字符串被拆成了单个字符。因为字符串是可迭代的,extend会遍历它,把每个字符当作一个元素加进去。
这种错误在处理文件路径、用户名、标签时特别常见。你以为加了一个字符串,结果列表里多了一堆字符。
字典也是坑
字典也是可迭代对象,但迭代的是键。所以:
lst = []
lst.extend({"name": "Alice", "age": 30})
print(lst) # ['name', 'age']
如果你想要的是字典本身作为一个元素,应该用append:
lst = []
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) # None
append和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]]。他想要的是扁平列表,所以应该用extend:
all_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加列表了。” 这个教训,值一下午的调试时间。