列表(list)是Python中最常用的内置可变序列类型,几乎所有数据处理任务都离不开它。在循环收集结果、合并多组数据、动态构建结构时,能否正确且高效地向列表添加元素,直接影响代码的可读性和运行性能。Python为列表设计了多种添加元素的手段,每种方法都有自己的适用场景和内部实现细节。理解它们之间的差异,可以避免写出冗余、低效甚至产生bug的代码。

1. append():在末尾追加单个元素
append()是最高频的列表添加方法,它接受一个参数,并将该参数作为一个整体添加到列表的末尾。操作是原地修改(in-place),返回值为None,不会创建新列表。
# 基本用法
fruits = ['apple', 'banana']
fruits.append('cherry')
print(fruits) # ['apple', 'banana', 'cherry']
# 添加任何类型的对象
fruits.append(42)
fruits.append([1, 2, 3]) # 整个列表作为一个元素
print(fruits) # ['apple', 'banana', 'cherry', 42, [1, 2, 3]]
从上面代码可以看出,当使用 append 添加一个列表时,它并不会展开这个列表,而是直接把列表对象本身当作一个新元素。如果需要将另一个序列里的元素逐个添加进去,就要用到 extend。
在性能方面,append 的时间复杂度是 O(1)(均摊情况)。Python列表底层使用动态数组实现,当频繁追加元素导致预分配空间不足时,会触发内存重分配并拷贝已有元素,但总体开销很小。因此循环中构建列表时,逐个 append 是非常高效的选择。
2. extend():批量合并可迭代对象
extend() 方法接受一个可迭代对象(如列表、元组、集合、字符串等),会遍历该可迭代对象并将其中的每个元素分别追加到原列表末尾。它同样是原地操作,返回 None。
numbers = [1, 2, 3]
numbers.extend([4, 5, 6]) # 列表元素逐个加入
print(numbers) # [1, 2, 3, 4, 5, 6]
numbers.extend((7, 8)) # 元组也可以
numbers.extend('ab') # 字符串会被拆分为字符
print(numbers) # [1, 2, 3, 4, 5, 6, 7, 8, 'a', 'b']
初学者很容易把 append 和 extend 搞混。一个简单的记忆方法是:append 增加的是“一个对象”,extend 增加的是“一组元素”。如果想把另一个列表的内容合并过来,正确的做法是 list1.extend(list2),而不是 list1.append(list2)。
extend 在内部实现上会计算待添加的可迭代对象的长度,一次性预分配足够的内存,因此比用循环 append 每个元素更加高效。当你需要将大量元素一次性合并到现有列表时,优先使用 extend。
3. insert():在指定位置插入元素
insert(index, element) 允许在列表的任意索引处插入一个新元素,该位置及之后的所有元素都会向后移动一位。如果 index 超出列表边界,Python会自动将元素放在列表首部(负索引过大时)或尾部(正索引过大时),不会抛出异常。
colors = ['red', 'green', 'blue'] colors.insert(1, 'yellow') # 在索引1处插入 print(colors) # ['red', 'yellow', 'green', 'blue'] colors.insert(10, 'purple') # 索引超出,追加到末尾 print(colors) # ['red', 'yellow', 'green', 'blue', 'purple'] colors.insert(-1, 'black') # 在倒数第一个位置之前插入 print(colors) # ['red', 'yellow', 'green', 'blue', 'black', 'purple']
insert 的时间复杂度为 O(n),因为插入点之后的所有元素都需要移动。如果频繁在列表头部插入元素(比如模拟队列的入队操作),性能开销会非常大。这种情况下建议改用 collections.deque,它针对两端操作做了优化。
4. 使用 + 运算符和 * 运算符
加号 + 运算符可以将两个列表拼接起来,返回一个全新的列表,原列表保持不变。这种方式适合需要保留原始数据、只生成一个新组合的场景。
a = [1, 2] b = [3, 4] c = a + b print(c) # [1, 2, 3, 4] print(a) # [1, 2] 原列表未改变
值得注意的是,+ 每次都会创建新对象并拷贝两个列表的所有元素,如果列表很长且频繁拼接,会产生大量内存分配和拷贝开销。对于循环中的累积拼接,更推荐先用普通列表 append 或 extend,或者使用 list comprehension。
乘号 * 也能间接实现“添加重复元素”的效果,但它更适合初始化固定长度的列表。要注意 a = [[0]*5]*3 这种写法会导致内部列表共享引用,一个元素修改会影响到所有行,正确做法是用列表推导式 [[0]*5 for _ in range(3)]。
# 初始化一个包含5个0的列表 zeros = [0] * 5 print(zeros) # [0, 0, 0, 0, 0]
5. 切片赋值:强大的原地修改能力
切片赋值是列表操作中非常灵活的一招,它可以一次性替换、插入或删除一段连续的元素。向列表中插入元素时,可以将某个位置切片为空,然后赋值一个可迭代对象。
nums = [10, 20, 30] # 在索引1的位置插入两个元素,原来索引1及之后的元素自动后移 nums[1:1] = [15, 18] print(nums) # [10, 15, 18, 20, 30] # 同时可以用切片替换一段元素 nums[2:4] = [100] # 将索引2和3的元素替换为100 print(nums) # [10, 15, 100, 30]
切片赋值在解释器内部实现为一系列元素的移动操作,当插入元素较多时,性能介于多次 insert 和一次 extend 之间。虽然它很强大,但代码可读性不如专用方法,在普通插入场景最好还是用 insert,而复杂的原地批量替换可以用切片赋值。
6. 通过解包操作合并列表
Python 3.5 引入的 * 可迭代对象解包操作(PEP 448)可以方便地在列表字面量中展开其他序列,效果类似于 extend,但会生成一个新列表。
part1 = [1, 2] part2 = [3, 4, 5] combined = [*part1, *part2, 6, 7] print(combined) # [1, 2, 3, 4, 5, 6, 7]
这种方式简洁优雅,不需要调用方法,适合在构造新列表时一次性融合多个来源的数据。因为底层仍然会遍历并拷贝每一个元素,所以时间复杂度为O(n),它不会修改原始列表。
7. 性能对比与使用建议
为了直观感受不同方法的效率差异,可以构造一个简单的测试:分别用 append 循环、extend、+ 运算符和切片赋值向列表添加100万个元素。
import timeit
# 准备数据
data = list(range(1000000))
# 测试 extend
def test_extend():
lst = []
lst.extend(data)
# 测试循环 append
def test_append_loop():
lst = []
for i in data:
lst.append(i)
# 测试 + 运算符(每次生成新列表,模拟累积拼接)
def test_plus():
lst = []
for i in data:
lst = lst + [i] # 非常慢,仅作演示
print('extend:', timeit.timeit(test_extend, number=10))
print('append loop:', timeit.timeit(test_append_loop, number=10))
# extend 一般比 append 循环略快,因为减少了函数调用开销
# + 运算符版本因每次拷贝整个列表,性能极差,不推荐
绝大多数场景下,构建列表的推荐做法如下:
- 逐个收集元素:先创建空列表,在循环中
append。 - 合并另一个序列:使用
extend或列表解包[*a, *b]。 - 固定位置插入:偶尔使用
insert,频繁头部插入改用deque。 - 需要新列表且数据量小:可以直接用
+或解包,代码更简洁。 - 复杂原地修改:用切片赋值。
8. 常见陷阱与注意事项
8.1 可变默认参数的副作用
在函数定义中使用可变对象作为默认参数(如空列表)是经典的坑。因为默认参数在函数定义时只计算一次,每次调用都会共享同一个列表对象。
def add_item(item, target=[]):
target.append(item)
return target
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] 非预期的累积
正确做法是将默认值设为 None,在函数体内创建新列表。
8.2 线程安全性
列表的 append、extend 等操作本身是原子的吗?在CPython中,由于全局解释器锁(GIL)的存在,单个列表操作如 append 是线程安全的,但组合多个操作(比如先检查长度再添加)仍可能产生竞态条件。如果确定要跨线程操作列表,建议使用 queue.Queue 或显式加锁。
8.3 复制与引用问题
当列表包含可变对象(如子列表、字典)时,使用 extend 或切片赋值时只拷贝了引用,修改内部对象会影响原数据。若需要完全独立的副本,应使用 copy.deepcopy。例如:
original = [[1, 2], [3, 4]] new_list = [] new_list.extend(original) new_list[0][0] = 99 print(original) # [[99, 2], [3, 4]] 原列表受影响
9. 总结
Python为列表添加元素提供了多种内置操作,每种操作都有其设计意图和性能特征。append 满足日常逐个追加,extend 适合批量合并,insert 处理定点插入,+ 和解包语法生成新列表,切片赋值则提供了原地修改的最大灵活性。选择合适的工具,不仅能让代码更清晰,还能有效提升程序运行效率。在实际项目中,结合数据规模、是否原地修改、内存开销等因素做出权衡,就能写出专业且可靠的Python代码。