Python列表合并六种方法详解:从+到itertools.chain的性能与应用场景
1. 从“合并”说起为什么Python列表合并值得深究刚接触Python那会儿处理列表合并我脑子里就一个号。后来项目做多了数据量上来了才发现这里面门道不少。比如你手头有两个各含十万个元素的列表用直接相加和你用extend()方法在内存和时间开销上可能天差地别。再比如你想合并几个列表但要去掉重复项或者合并的同时还要进行一些计算这时候一个简单的操作选择直接决定了代码的简洁度和运行效率。列表list作为Python中最基础、最常用的数据结构之一几乎贯穿了每一个脚本、每一个项目。数据清洗、结果汇总、批量处理都离不开列表的合并操作。表面上看不就是把几个列表拼在一起吗但“怎么拼”却是一个融合了性能考量、代码可读性、内存管理和特定场景需求的综合问题。网上教程往往只罗列几种语法却很少告诉你在什么情况下该用哪一种背后的原理是什么踩过哪些坑今天我们就抛开那些浅尝辄止的教程深入聊聊Python中合并列表的六种核心方法。我不会只给你语法糖我会带你看看每种方法在Python解释器里是怎么工作的它们各自适合什么场景以及我在实际开发中因为选错方法而掉进去的那些“坑”。无论你是正在刷题的学生还是处理数据的分析师或是构建后端服务的工程师这些细节都能让你的代码更健壮、更高效。2. 方法一加法运算符—— 最直观的“连接”号大概是所有人学会的第一种列表合并方式它非常符合直觉把两个列表连起来。list1 [1, 2, 3] list2 [4, 5, 6] merged_list list1 list2 print(merged_list) # 输出: [1, 2, 3, 4, 5, 6]原理与行为拆解 当你使用list1 list2时Python解释器会执行一个“连接”concatenation操作。关键点在于这个操作会创建一个全新的列表对象。新列表merged_list的内存空间是独立于list1和list2的它依次包含了list1和list2中所有元素的引用对于不可变对象如整数、字符串是值的拷贝对于可变对象是引用的拷贝。原始的list1和list2本身没有任何变化。性能分析与适用场景时间复杂度O(nm)其中n和m分别是两个列表的长度。因为它需要遍历两个列表的所有元素并将每个元素的引用复制到新列表。内存开销最高。因为它创建了一个全新的列表占用了额外的内存空间大小等于两个原列表之和。适用场景需要保留原始列表当你不想或不能修改任何一个原始列表时是最安全的选择。函数式编程风格中常用。合并次数极少对于一次性合并且列表规模不大比如几百几千个元素的情况其简洁性优势明显。链式合并可以连续使用如list1 list2 list3代码清晰。避坑经验大列表慎用合并两个非常大的列表例如各100万个元素时操作会瞬间申请一块巨大的连续内存可能导致内存使用峰值陡增在内存受限的环境如Web服务器、嵌入式设备中需要警惕。循环内的灾难千万不要在循环里反复用来合并列表这是一个经典的低性能写法。# 错误示范性能极差 result [] for i in range(10000): new_data [i] * 10 # 假设每次生成一个小列表 result result new_data # 每次循环都创建全新的大列表上面这段代码的时间复杂度接近O(n²)因为每次循环result都在增长复制成本越来越高。正确做法是使用extend()或列表推导式下文会讲。3. 方法二extend()方法 —— 原地扩展的“效率之王”如果说是“新建住宅小区”那么extend()就是“在老城区扩建”。它直接在原有列表的末尾一次性添加另一个可迭代对象的所有元素。list1 [1, 2, 3] list2 [4, 5, 6] list1.extend(list2) print(list1) # 输出: [1, 2, 3, 4, 5, 6] print(list2) # 输出: [4, 5, 6] (list2未被修改)原理与行为拆解extend()是一个原地in-place操作方法。它修改调用它的列表list1将其内部存储元素的数组容量进行可能需要的扩容然后将list2中所有元素的引用追加到list1的数组末尾。它不返回一个新列表而是返回None。这也是新手常踩的坑new_list list1.extend(list2)结果new_list是None。性能分析与适用场景时间复杂度平均情况下的摊销时间复杂度为O(m)其中m是待扩展列表的长度。因为列表的扩容机制over-allocation使得大多数情况下追加操作是O(1)只有偶尔扩容时需要O(n)时间。内存开销较低。主要在list1容量不足需要扩容时申请一块比当前稍大的新内存并迁移数据。相比总是创建全新列表extend()的内存利用更经济。适用场景循环中累积数据这是extend()的绝对主场。上面提到的循环合并问题用它完美解决。# 正确示范高性能 result [] for i in range(10000): new_data [i] * 10 result.extend(new_data) # 原地扩展高效明确需要修改原列表当你的逻辑就是要把所有东西都塞进第一个列表时。合并多个列表可以链式调用如list1.extend(list2); list1.extend(list3)。避坑经验返回值不是列表务必记住extend()返回None。如果你需要保留原列表可以先复制一份list1_copy list1.copy(); list1_copy.extend(list2)。参数必须是可迭代对象extend()可以接受任何可迭代对象不仅是列表元组、集合、字符串甚至生成器都可以。list1 [1, 2] list1.extend((3, 4)) # 元组 list1.extend({5, 6}) # 集合 (注意顺序可能不定) list1.extend(ab) # 字符串会追加[a, b] print(list1) # 输出可能是 [1, 2, 3, 4, 5, 6, a, b]与append()的区别这是另一个关键点。append()是把整个参数作为一个单个元素添加到列表末尾。list1 [1, 2] list1.append([3, 4]) # 把[3,4]这个列表当成一个元素添加 print(list1) # 输出: [1, 2, [3, 4]] # 嵌套列表 list1.extend([3, 4]) # 把[3,4]列表中的元素3和4添加进去 print(list1) # 输出: [1, 2, [3, 4], 3, 4]想添加多个元素时用extend想添加一个整体对象时用append。4. 方法三*运算符 ——extend()的语法糖*运算符就地乘法赋值用于列表时有一个特殊用途重复自身。但当它与另一个列表相加时其行为就等同于extend()。list1 [1, 2, 3] list2 [4, 5, 6] list1 list2 # 等价于 list1.extend(list2) print(list1) # 输出: [1, 2, 3, 4, 5, 6]原理与行为拆解 对于可变序列如列表运算符会调用其__iadd__魔法方法该方法在列表中的实现就是调用extend()然后返回self即列表自身。所以list1 list2完全等价于list1.extend(list2)同样是原地修改。性能分析与适用场景 其性能特征与extend()完全一致。那为什么还要有两种写法代码风格与可读性写起来更简洁特别是当list1的名字很长时。它更像一个“增强赋值”操作意图是“把右边加到左边上”语义非常直接。适用场景所有适合使用extend()的场景都可以用替代取决于你的编码习惯。我个人在脚本中更常用因为它更短在写供他人调用的库函数时可能会用extend()因为方法名更明确。一个重要的细微差别 虽然对于列表和extend()等价但对于元组tuple这样的不可变序列会创建一个新对象因为它无法原地修改。这提醒我们讨论这些操作时必须结合具体的数据类型。5. 方法四itertools.chain()—— 处理超大规模数据的“懒人”利器当你需要合并的列表不是两个而是很多个或者列表非常大以至于你不想在内存中同时存在两份完整数据时itertools.chain()就派上用场了。它来自Python强大的itertools模块。import itertools list1 [1, 2, 3] list2 [4, 5, 6] list3 [7, 8, 9] # chain() 返回一个迭代器 chained itertools.chain(list1, list2, list3) print(chained) # 输出: itertools.chain object at 0x... print(list(chained)) # 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9] # 注意list(chained)消费了迭代器再次打印就是空列表了原理与行为拆解chain()函数接受多个可迭代对象作为参数返回一个迭代器iterator。这个迭代器非常“懒”lazy它不会立即复制任何数据。当你遍历这个迭代器时例如通过for循环或list()转换它会依次从第一个可迭代对象中产出元素耗尽了再转向下一个如此往复。在整个过程中原始数据没有被复制到一个新的大列表中。性能分析与适用场景时间复杂度遍历的总时间复杂度是O(N)N是所有列表元素总数。但它的启动开销极小因为它不进行数据复制。内存开销极低。它只存储对原始可迭代对象的引用和当前遍历的状态内存消耗是常数级别的与合并的列表总大小无关。这是它最大的优势。适用场景内存敏感型操作处理日志文件、大型数据集时数据无法一次性装入内存chain()可以让你像操作一个连续流一样处理它们。只需要遍历一次如果你合并列表的目的只是为了进行一次遍历例如搜索、统计、过滤那么chain()是最佳选择避免了创建中间大列表的浪费。合并大量可迭代对象chain.from_iterable()变体可以接受一个可迭代对象其元素本身也是可迭代对象非常适合处理嵌套结构。list_of_lists [[1, 2], [3, 4], [5, 6]] # 用 chain.from_iterable 展平 flat list(itertools.chain.from_iterable(list_of_lists)) print(flat) # 输出: [1, 2, 3, 4, 5, 6]避坑经验迭代器只能消费一次chain()返回的是迭代器像list()、for循环这样的操作会“耗尽”它。如果你需要多次使用合并后的结果必须先将其转换为列表或元组保存起来result list(chained)。但这就失去了节省内存的意义所以需要权衡。不适用于随机访问迭代器不支持下标操作如result[10]。如果你需要随机访问元素必须转换为列表。与生成器表达式结合chain()的威力在于可以和生成器表达式强强联合实现极其高效的数据管道。# 假设有三个生成器每个产生大量数据 def gen1(): yield from range(1000000) def gen2(): yield from range(1000000, 2000000) # 合并处理内存友好 mega_gen itertools.chain(gen1(), gen2()) for item in mega_gen: if some_condition(item): process(item)6. 方法五列表推导式与for循环 —— 灵活控制的“手术刀”前面几种方法都是“整体合并”但有时我们的需求更精细合并的同时要过滤、要转换、要有条件地选择元素。这时列表推导式List Comprehension和普通的for循环就是你的手术刀。列表推导式合并list1 [1, 2, 3, 10] list2 [4, 5, 6, 10] # 合并但只取偶数 merged_evens [x for lst in [list1, list2] for x in lst if x % 2 0] print(merged_evens) # 输出: [2, 10, 4, 6, 10] # 合并并给每个元素加1 merged_plus_one [x1 for lst in [list1, list2] for x in lst] print(merged_plus_one) # 输出: [2, 3, 4, 11, 5, 6, 7, 11]for循环合并list1 [1, 2, 3] list2 [4, 5, 6] merged [] for lst in (list1, list2): # 遍历要合并的列表 for item in lst: # 遍历每个列表的元素 # 这里可以加入复杂的逻辑 if item % 2 ! 0: # 例如只合并奇数 merged.append(item) print(merged) # 输出: [1, 3, 5]原理与行为拆解 这两种方式本质都是手动控制合并过程。列表推导式是语法糖底层也是循环但通常执行效率比等价的for循环稍高因为其循环操作在Python解释器的C语言层面实现。它们都明确地创建了一个新列表并按照你定义的规则填充元素。性能分析与适用场景时间复杂度O(N * C)其中N是总元素数C是你在循环体内操作的复杂度。对于简单的合并无过滤转换就是O(N)。内存开销与操作类似创建了新列表。但你可以通过生成器表达式Generator Expression来获得类似itertools.chain()的惰性求值特性。# 生成器表达式不立即创建列表 gen (x for lst in [list1, list2] for x in lst if x 2) for value in gen: print(value) # 惰性计算适用场景合并伴随数据清洗/转换这是其核心优势。在数据预处理中非常常见。条件性合并只合并符合特定条件的元素。需要高度自定义合并逻辑比如根据元素的值决定放入哪个子列表或者进行复杂的映射。避坑经验可读性陷阱过于复杂的嵌套列表推导式会严重影响可读性。当逻辑超过两层循环或包含复杂条件时考虑拆分成多行或使用普通的for循环。变量作用域在列表推导式中循环变量如上例中的lst和x会泄露到当前作用域Python 3中有所改善但最好避免依赖。在普通for循环中则不会。性能不是唯一考量对于简单的合并extend()通常比手写循环更快。但当逻辑复杂时列表推导式的清晰和速度优势就体现出来了。规则是先写对人友好的代码在性能瓶颈被证实后再优化。7. 方法六解包操作符*—— Python 3.5的“优雅魔法”Python 3.5引入了PEP 448扩展了可迭代对象解包操作符*的使用场景让它成为合并列表的一种非常优雅的方式。list1 [1, 2, 3] list2 [4, 5, 6] list3 [7, 8, 9] # 在列表字面量中使用 * 解包 merged_list [*list1, *list2, *list3] print(merged_list) # 输出: [1, 2, 3, 4, 5, 6, 7, 8, 9] # 也可以和普通元素混用 merged_with_extra [0, *list1, 3.5, *list2, 100] print(merged_with_extra) # 输出: [0, 1, 2, 3, 3.5, 4, 5, 6, 100]原理与行为拆解*操作符在列表字面量中会将紧随其后的可迭代对象“解包”unpack将其所有元素作为独立项插入到当前位置。[*list1, *list2]在效果上等同于list(list1) list(list2)它会创建一个新列表。性能分析与适用场景时间复杂度与内存开销与运算符类似需要创建新列表并复制所有元素因此时间和空间复杂度都是O(N)。适用场景合并多个列表代码极其简洁当需要合并的列表数量固定且不多时这种写法比多次extend()或链式更清晰。在合并中插入固定元素如上例所示可以轻松地在列表之间插入任意字面量元素这是其他方法难以一步完成的。函数调用时合并参数*解包在函数传参时也非常有用。def my_func(a, b, c, d): return a b c d list_args [2, 3] result my_func(1, *list_args, 4) # 等价于 my_func(1, 2, 3, 4) print(result) # 输出: 10避坑经验版本要求确保你的Python版本在3.5以上。可读性与滥用虽然[*a, *b, *c]很酷但如果列表变量名很长或者解包层级太深可能会降低可读性。对于动态数量的列表使用itertools.chain()或循环extend()可能更合适。与字典解包**区分在字典字面量中**用于解包字典。不要混淆。8. 实战场景下的方法选型决策指南知道了所有工具关键是怎么选。下面我结合几个典型场景给出我的选择策略。场景一数据预处理流水线需要过滤和转换你从多个CSV文件读取数据每个文件读成一个列表需要合并并且清洗掉无效值如None同时将所有数字转换为浮点数。选择列表推导式或生成器表达式。理由合并、过滤、转换三步合一逻辑清晰且生成器表达式能节省内存。all_data [] for file in csv_files: data read_csv(file) # 假设返回一个列表 cleaned [float(x) for x in data if x is not None] all_data.extend(cleaned) # 或者用 如果文件很大可以用生成器表达式惰性处理def process_files(csv_files): for file in csv_files: data read_csv(file) yield from (float(x) for x in data if x is not None) # 使用时 for processed_item in process_files(csv_files): # 处理每个清洗后的数据场景二合并大规模日志流进行分析你有来自多个服务器的实时日志流可视为无限长的迭代器需要合并成一个流进行实时关键词监控。选择itertools.chain()。理由日志流可能无限大无法装入内存。chain()可以创建一个虚拟的合并流无需等待所有数据。import itertools def log_generator(server_id): # 模拟从服务器不断产生日志 while True: yield fLog from server {server_id}: {get_next_log()} # 合并三个服务器的日志流 combined_log_stream itertools.chain(log_generator(1), log_generator(2), log_generator(3)) for log_entry in combined_log_stream: if ERROR in log_entry: alert_ops(log_entry)场景三在Web请求中合并多个API返回的列表一个后端接口需要调用三个内部微服务获取数据每个返回一个JSON列表需要合并后返回给前端。数据量适中每个列表几百条。选择解包操作符*或。理由代码简洁明了意图清晰。数据量不大内存和性能不是瓶颈。data_from_svc_a call_service_a() # 返回列表 data_from_svc_b call_service_b() data_from_svc_c call_service_c() # 方案1使用 * 解包 (Python 3.5) final_response { status: success, data: [*data_from_svc_a, *data_from_svc_b, *data_from_svc_c] } # 方案2使用 (更兼容) final_response { status: success, data: data_from_svc_a data_from_svc_b data_from_svc_c }场景四在循环中不断累积中间结果你在一个算法中例如图遍历、动态规划需要不断将新发现的一批节点添加到一个总列表中。选择extend()或。理由这是原地操作避免在每次循环中创建大量临时新列表性能最佳。all_nodes [] while frontier: # frontier是待探索的节点集合 new_nodes explore(frontier) # 探索返回新节点列表 all_nodes.extend(new_nodes) # 关键原地扩展 frontier update_frontier(new_nodes)决策流程图简化版需要惰性合并/内存极度敏感吗是 - 用itertools.chain()。合并的同时需要复杂过滤或转换吗是 - 用列表推导式或生成器表达式。需要修改原列表吗是 - 用extend()或。需要保留原列表吗是 - 用或[*a, *b]。合并的列表数量固定且少追求代码简洁吗是 - 用[*a, *b, *c](Py3.5)。其他一般情况- 优先考虑extend()/效率好或意图清晰。9. 性能实测与背后的原理剖析理论说了很多是骡子是马拉出来遛遛。我们写个小脚本用timeit模块粗略对比一下几种方法在合并两个列表时的性能。注意性能测试结果会因Python版本、操作系统、硬件和列表大小而异但相对趋势是有参考价值的。import timeit import itertools list_small list(range(1000)) list_big list(range(10000)) def test_plus(): return list_small list_big def test_extend(): # 注意为了公平测试每次需要复制原列表因为extend是原地修改 a list_small.copy() a.extend(list_big) return a def test_inplace_add(): a list_small.copy() a list_big return a def test_chain(): return list(itertools.chain(list_small, list_big)) def test_unpack(): return [*list_small, *list_big] # 测试函数 def run_perf_test(func_name, func): # 执行1000次取平均时间 time_taken timeit.timeit(func, number1000, globalsglobals()) print(f{func_name:15} - 耗时: {time_taken:.5f} 秒 (1000次平均)) print(合并 list_small(1000) 和 list_big(10000):) run_perf_test( Operator, test_plus) run_perf_test(extend(), test_extend) run_perf_test( Operator, test_inplace_add) run_perf_test(chain()list(), test_chain) run_perf_test(* Unpack, test_unpack)在我的环境中Python 3.9可能得到类似下面的结果合并 list_small(1000) 和 list_big(10000): Operator - 耗时: 0.045 秒 (1000次平均) extend() - 耗时: 0.032 秒 (1000次平均) Operator - 耗时: 0.031 秒 (1000次平均) chain()list() - 耗时: 0.048 秒 (1000次平均) * Unpack - 耗时: 0.047 秒 (1000次平均)结果解读与原理联系extend()和最快这印证了它们是原地操作主要开销是扩容和元素引用复制避免了创建全新列表对象的开销。和解包*稍慢它们都需要创建一个全新的列表对象这个对象创建和初始化的成本是可观的。chain()list()最慢这里慢的原因在于list()需要遍历迭代器并构建列表相当于做了两遍工作chain组织遍历list执行遍历并构建。但这并不意味着chain()不好这个测试场景对chain()不公平因为我们强制它立即生成列表。chain()的真正优势在于惰性遍历。如果我们只是遍历而不需要列表它的效率是最高的。背后的Python实现原理列表的存储Python列表本质上是一个指向堆内存的指针数组PyObject**。合并操作的核心就是复制这些指针。操作会调用list_concat函数它计算总长度分配一个新列表然后分两次内存复制memcpy将两个原列表的数据区拷贝过来。extend()操作会先检查待扩展列表的剩余空间是否足够不够则进行扩容。扩容策略通常是申请(new_size 3) (new_size 9 ? 3 : 6)大小的额外空间这就是所谓的“过度分配”以平摊后续多次追加的成本。然后通过循环赋值来追加元素。itertools.chain它本身是一个轻量级的迭代器类只保存了对输入可迭代对象的引用。它的__next__方法会依次从每个输入中取元素。理解这些底层机制就能明白为什么在特定场景下选择特定方法如此重要。对于追求极致性能的代码段这些选择的影响会被放大。10. 进阶话题与常见陷阱掌握了基本方法后我们再看几个更深层次的问题和容易踩的坑。陷阱一嵌套列表的“浅合并”与“深合并”我们之前讨论的合并都是“浅合并”shallow merge。如果列表里存放的是可变对象如其他列表、字典合并操作复制的是引用而不是对象本身。list1 [[1, 2], 3] list2 [[4, 5], 6] merged list1 list2 # 浅合并 print(merged) # 输出: [[1, 2], 3, [4, 5], 6] # 修改 merged 中的子列表会影响原列表 merged[0][0] 99 print(merged) # 输出: [[99, 2], 3, [4, 5], 6] print(list1) # 输出: [[99, 2], 3] # 原列表也被改了解决方案如果需要完全独立的副本即“深合并”需要使用copy模块的deepcopy函数但这通常非常耗时且并非所有对象都支持深拷贝。在大多数情况下意识到这是引用复制并谨慎操作就足够了。如果确实需要合并值对于简单嵌套可以使用列表推导式进行一层拷贝merged_deep [sublist.copy() for sublist in list1] [sublist.copy() for sublist in list2]陷阱二合并时去重与保序有时合并列表需要去除重复元素。直接用set是最快的但会丢失元素顺序和原列表中的可哈希性要求列表本身不可哈希。list1 [2, 1, 3, 2] list2 [3, 4, 4, 5] # 简单去重但顺序丢失 merged_unique_set list(set(list1 list2)) print(merged_unique_set) # 输出可能是 [1, 2, 3, 4, 5] # 保序去重 (Python 3.7 字典保序可简化) from collections import OrderedDict # Python 3.6及以下用这个 merged_unique_ordered list(dict.fromkeys(list1 list2).keys()) print(merged_unique_ordered) # 输出: [2, 1, 3, 4, 5] # 保留首次出现的顺序陷阱三与sum()函数的误用有人可能会想到用sum()来合并列表sum([list1, list2], [])。千万不要这样做虽然语法上可行但它的性能是灾难性的O(n²)因为sum每次迭代都会创建一个新列表。进阶使用functools.reduce()进行函数式合并对于函数式编程爱好者可以用reduce来合并多个列表from functools import reduce import operator lists [[1, 2], [3, 4], [5, 6]] merged reduce(operator.add, lists) # 等价于 [] [1,2] [3,4] [5,6] print(merged) # 输出: [1, 2, 3, 4, 5, 6]但请注意这本质上也是多次使用存在和循环中使用一样的性能问题。更高效的做法是结合itertools.chainmerged list(itertools.chain.from_iterable(lists))最后我个人的一个习惯是在写代码时如果只是简单的合并我优先写a b因为它短如果代码是给别人看的或者逻辑稍复杂我会写a.extend(b)意图更明确当处理的数据流可能很大时我的第一反应就是itertools.chain。这些选择没有绝对的对错只有是否适合当下的场景。理解每种方法背后的代价才能写出既优雅又高效的代码。