Python列表高频方法全解析:从append到列表推导式实战指南
2026/9/10 19:45:32 网站建设 项目流程

从入门到实战,聊聊Python列表(List)那些高频方法

Python的列表(List)是我日常写脚本、做数据处理时用得最多的内置数据结构,没有之一。不管你是刚开始学Python,还是已经写了一段时间但总感觉对列表的掌握还差点意思,这篇内容应该都能帮到你。我尽量不绕弯子,直接讲清楚列表常用方法的用法、底层逻辑和最容易踩的坑,同时结合平时处理数据、写爬虫、做量化策略时的真实场景来展开,保证你看完能直接用起来。

列表这个数据结构,说白了就是一个有序的、可以随时增删改查的容器,元素类型可以混着放,比如整数、字符串、甚至嵌套另一个列表。这种灵活性让它成了Python里最“万能”的存储方式之一,但也正因为太灵活,很多人在使用方法时容易忽略细节,比如appendextend的区别、切片复制和copy()的区别、sortsorted的返回值陷阱,这些坑我早年都踩过,后面会一一拆开讲。

这篇文章适合的读者很明确:刚学完基础语法想进阶的人、工作中需要频繁操作列表但经常查文档的人、以及准备面试想系统梳理列表常用方法的人。我会按“创建 -> 增删改查 -> 排序复制 -> 推导式 -> 实战案例 -> 常见问题排查”这条线来讲,每个方法都有示例代码和说明,属于那种可以一边看一边抄作业的教程。

1. 列表基础操作:创建、索引与切片

1.1 创建列表的几种方式和常见误区

Python创建列表的方式很直白,最常见的就是用方括号:

# 创建空列表 empty_list = [] # 创建带初始值的列表 fruits = ['apple', 'banana', 'cherry'] # 用 list() 构造函数 numbers = list((1, 2, 3)) # 字符串转列表 chars = list('hello') print(chars) # ['h', 'e', 'l', 'l', 'o']

这里有一个很容易被忽略的点:list('hello')会把字符串拆成一个个字符。如果你想把字符串按单词切分,应该用split(),而不是list()。很多刚开始写代码的朋友在这里栽过跟头,拿着list("hello world")想要['hello', 'world'],结果得到了一堆单独字符,然后一脸懵。

还有一个创建列表的经典坑,就是用乘法复制嵌套列表:

# 看起来像是在创建二维列表 matrix = [[0] * 3] * 3 print(matrix) # [[0, 0, 0], [0, 0, 0], [0, 0, 0]] # 修改一个元素 matrix[0][0] = 1 print(matrix) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]]

看到了吗?我只改了一行,结果三行全变了。原因在于[[0] * 3] * 3先把[0, 0, 0]这个列表创建了一份,然后乘3次只是复制了“引用”,也就是说三个子列表其实指向的是同一个内存对象。正确的做法是用列表推导式:

matrix = [[0] * 3 for _ in range(3)]

这里也顺带引出一个基础概念:Python变量名存的是对象的引用,而不是对象本身。理解了这一点,后面讲浅拷贝、深拷贝就会轻松很多。

1.2 索引和切片:不只是简单的取元素

列表的索引从0开始,支持正数索引和负数索引。负数索引从-1开始,对应最后一个元素。这一点很好记:list[-1]永远是最后一个元素,list[-2]是倒数第二个。

nums = [10, 20, 30, 40, 50] print(nums[0]) # 10 print(nums[-1]) # 50 print(nums[-3]) # 30

切片是列表操作里最灵活、也最有意思的部分,语法是list[start:stop:step]。注意切片是“左闭右开”,也就是包含start对应的元素,但不包含stop对应的元素。这一点必须刻在脑子里,不然写切片时总是差一个元素。

nums = [10, 20, 30, 40, 50] # 取前三个 print(nums[:3]) # [10, 20, 30] # 取后两个 print(nums[-2:]) # [40, 50] # 隔一个取一个 print(nums[::2]) # [10, 30, 50] # 反转列表的切片写法 print(nums[::-1]) # [50, 40, 30, 20, 10]

切片还有一个隐藏能力:可以赋值,用来替换或插入元素。

nums = [10, 20, 30, 40, 50] nums[1:3] = [99, 98, 97] print(nums) # [10, 99, 98, 97, 40, 50]

这里要补充一下,切片返回的是一个新的列表(浅拷贝),所以用切片复制列表在某种程度上是安全的。但它只是浅拷贝,如果列表内部还有嵌套列表,那嵌套列表依然共享引用,这一点后面在讲到复制陷阱时还会再强调。

2. 列表的增删改查:高频方法逐个拆解

2.1 增加元素:append、extend、insert的区别

列表增加元素的方法有appendextendinsert三个,很多人搞不清前两个的区别,其实一句话就能说透: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通常用于添加可迭代对象,比如列表、元组、字符串、集合。如果extend一个字符串,它会按字符拆分:

c = [1, 2] c.extend('ab') print(c) # [1, 2, 'a', 'b']

insert有两个参数,第一个是插入位置,第二个是插入元素:

d = [1, 2, 3] d.insert(1, 'x') print(d) # [1, 'x', 2, 3]

我的实操经验是,insert在列表头部插入元素时效率比较低,因为涉及到所有元素的移位。如果需要频繁从头部插入,优先考虑collections.deque,它的appendleft是 O(1) 的。这个细节在数据量小的时候无所谓,但处理几万条以上数据时就会明显感觉到差别。

2.2 删除元素:remove、pop、del、clear各有用处

remove按值删除第一个匹配的元素,如果值不存在会抛出ValueError

nums = [1, 2, 3, 2, 4] nums.remove(2) print(nums) # [1, 3, 2, 4]

注意,remove只删除第一个匹配的2,后面的2还留着。如果要删除所有匹配元素,可以用循环或列表推导式。

pop按索引删除并返回被删除的元素,如果不传索引,默认删除最后一个:

nums = [1, 2, 3] last = nums.pop() print(last) # 3 print(nums) # [1, 2] first = nums.pop(0) print(first) # 1

pop(0)remove一样,时间复杂度是 O(n),因为删除后所有后续元素都要往前移动。如果频繁删除头部元素,同样建议用deque

del是Python的通用删除语句,可以按索引删除、按切片删除,甚至直接删除整个列表:

nums = [1, 2, 3, 4, 5] del nums[0] print(nums) # [2, 3, 4, 5] del nums[1:3] print(nums) # [2, 5] del nums # 整个列表被删除

clear方法更简单,直接清空列表所有元素,但列表对象本身还在:

nums = [1, 2, 3] nums.clear() print(nums) # []

2.3 查找与计数:index、count、in 的组合用法

index返回元素第一次出现的索引,如果元素不存在会抛ValueError。所以用的时候最好先判断或者捕获异常,这一点实际写代码时很关键:

nums = [10, 20, 30, 20, 40] nums.index(20) # 1 nums.index(20, 2) # 从索引2开始找,结果是3 # 安全写法 if 20 in nums: print(nums.index(20))

count统计元素出现的次数:

nums = [1, 2, 2, 3, 2] print(nums.count(2)) # 3

innot in用来判断元素是否存在。很多人会忽略的一点是,列表的in判断是线性扫描,时间复杂度 O(n)。如果数据量很大且需要频繁做存在性判断,最好把列表转成集合再判断,集合的in是 O(1)。我处理去重类需求时经常这么做,效果立竿见影。

3. 排序与反转:sort、sorted、reverse的细节和陷阱

3.1 sort方法和sorted函数到底怎么选

这是我在面试别人时特别喜欢问的一个点,也是实际工作中很容易用混的地方。简单来说:

  • list.sort()是列表的方法,原地排序,不返回新列表(返回None)。
  • sorted(iterable)是内置函数,排序后返回新列表,原列表不变。
nums = [3, 1, 2] nums.sort() print(nums) # [1, 2, 3],原列表被修改
nums = [3, 1, 2] new_nums = sorted(nums) print(nums) # [3, 1, 2],原列表不变 print(new_nums) # [1, 2, 3],返回新列表

很多新手在这里写了nums = nums.sort(),然后发现nums变成了None,这就是没记住sort是原地操作、返回None导致的。反过来,如果写nums = sorted(nums),那就没问题,因为sorted返回了新列表。

两个方法都支持key参数,用来指定排序依据,这是最常用的高级用法:

words = ['banana', 'apple', 'cherry', 'date'] words.sort(key=len) print(words) # ['date', 'apple', 'banana', 'cherry'],按长度排序

key还可以配合lambda、函数、甚至内置方法,比如按字符串最后一个字母排序:

words = ['banana', 'apple', 'cherry'] words.sort(key=lambda s: s[-1]) print(words) # ['banana', 'apple', 'cherry']

3.2 复杂对象排序:用key参数实现按字典、按元组元素排序

key参数最大的价值在于能对复杂数据排序。假设你有一个包含字典的列表,每个字典代表一个商品,包含名称和价格,想按价格从低到高排序:

products = [ {'name': 'apple', 'price': 5}, {'name': 'banana', 'price': 3}, {'name': 'cherry', 'price': 8}, ] products.sort(key=lambda p: p['price']) print(products) # [{'name': 'banana', 'price': 3}, {'name': 'apple', 'price': 5}, {'name': 'cherry', 'price': 8}]

这个写法的效率很高,因为key函数只会对每个元素调用一次,然后Python基于这些 key 值做排序。类似地,如果你有一个元组列表,想按元组的第二个元素排序:

pairs = [('b', 2), ('a', 3), ('c', 1)] pairs.sort(key=lambda x: x[1]) print(pairs) # [('c', 1), ('b', 2), ('a', 3)]

还有一种更取巧的写法,利用operator.itemgetter,性能和可读性都不错:

from operator import itemgetter pairs.sort(key=itemgetter(1))

itemgetterlambda快一些,尤其在数据量大的时候更明显。它还可以接收多个参数,实现先按第一个字段排、再按第二个字段排的效果。

3.3 reverse和反转的两种方式

reverse()是原地反转,返回None,注意和[::-1]的区别:[::-1]是生成一个新列表,原列表不变。

nums = [1, 2, 3] nums.reverse() print(nums) # [3, 2, 1]

配合sort反向排序,可以直接用reverse=True参数,而不是排序后再reverse,效率上更优:

nums = [3, 1, 2] nums.sort(reverse=True) print(nums) # [3, 2, 1]

4. 列表推导式与遍历技巧:少写循环的写法

4.1 列表推导式的基本语法和进阶写法

列表推导式是Python里我最喜欢的一个特性,没有之一。它用一行代码代替多行循环,既能生成新列表,也能做筛选和变换。基本语法是[表达式 for 变量 in 可迭代对象 if 条件]

最简单的例子,生成1到10的平方:

squares = [x ** 2 for x in range(1, 11)] print(squares) # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

筛选偶数:

evens = [x for x in range(1, 11) if x % 2 == 0] print(evens) # [2, 4, 6, 8, 10]

嵌套循环也可以用推导式实现。比如生成一个坐标点列表:

points = [(x, y) for x in range(3) for y in range(3)] print(points) # [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]

一般经验是,如果推导式嵌套超过两层,可读性会明显下降,这时候老老实实写循环反而更好维护。技术是为了做事,不是为了炫技。

4.2 列表推导式与map、filter的对比

Python内置了mapfilter,它们也可以用来对列表做变换和筛选,但在大部分场景下,列表推导式是更直观的选择。对比例子:

nums = [1, 2, 3, 4] # 用 map squares_map = list(map(lambda x: x ** 2, nums)) # 用列表推导式 squares_comp = [x ** 2 for x in nums]

map生成的是迭代器对象,所以要用list()转成列表。map的优点是延迟计算,处理超大集合时节省内存,但如果你只是构建一个小列表,直接用推导式更清晰。

filter的用法和map类似:

nums = [1, 2, 3, 4] # 用 filter evens_filter = list(filter(lambda x: x % 2 == 0, nums)) # 用列表推导式 evens_comp = [x for x in nums if x % 2 == 0]

结论是:mapfilter在配合函数式编程风格时有用,但日常开发用列表推导式就够了。我个人的习惯是,除非性能测试明确说明mapfilter有优势,否则一律用推导式,代码的可读性决定了后期的维护成本。

4.3 enumerate和zip:遍历时同时拿索引和多个列表

enumerate是我遍历列表时最常用的函数,它可以同时返回索引和元素,避免手动维护一个计数变量:

fruits = ['apple', 'banana', 'cherry'] for index, fruit in enumerate(fruits): print(index, fruit) # 0 apple # 1 banana # 2 cherry

enumerate还支持第二个参数,定义起始索引:

for index, fruit in enumerate(fruits, start=1): print(index, fruit) # 1 apple # 2 banana # 3 cherry

zip用于同时遍历多个列表,把对应位置的元素打包成元组:

names = ['Alice', 'Bob', 'Charlie'] scores = [85, 92, 78] for name, score in zip(names, scores): print(f'{name}: {score}') # Alice: 85 # Bob: 92 # Charlie: 78

zip在数据对齐时非常高效。注意如果两个列表长度不一致,zip默认以短的为准。如果想以长的为准并用默认值填充,需要用到itertools.zip_longest,这个在数据分析场景中偶尔会用到。

4.4 遍历时能不能修改列表

这是经典陷阱:在遍历列表的同时删除元素,会导致索引错位,漏掉一些元素。

nums = [1, 2, 3, 4, 5] for num in nums: if num % 2 == 0: nums.remove(num) print(nums) # [1, 3, 5]

看似结果正确,但如果换个例子就会出问题:

nums = [2, 4, 5, 6] for num in nums: if num % 2 == 0: nums.remove(num) print(nums) # [4, 5]

为什么会漏掉4?因为删除2后,4往前移了一位,循环内部索引自动加1,导致4被跳过了。这是面试题里非常经典的一个坑。

正确的做法是遍历列表的副本,或者用列表推导式直接生成新列表:

nums = [2, 4, 5, 6] nums = [num for num in nums if num % 2 != 0] print(nums) # [5]

还有一个笨但可靠的方法是倒序遍历:

nums = [2, 4, 5, 6] for i in range(len(nums) - 1, -1, -1): if nums[i] % 2 == 0: nums.pop(i) print(nums) # [5]

倒序删除不会影响前面元素的索引,所以不会漏元素。我个人最推荐列表推导式,因为它简洁、快、不易出错。

5. 复制与浅拷贝深拷贝:copy方法背后的内存逻辑

5.1 直接赋值、切片复制、copy() 的区别

在Python里,list2 = list1并不是复制列表,而是让list2list1指向同一个列表对象。修改任何一个,另一个也会变:

a = [1, 2, 3] b = a b.append(4) print(a) # [1, 2, 3, 4] a也变了

如果你只想复制内容,而不是共享引用,有三种常见方式:

a = [1, 2, 3] # 方式1:切片 b = a[:] # 方式2:copy方法 c = a.copy() # 方式3:list构造函数 d = list(a) b.append(4) print(a) # [1, 2, 3] a不变 print(b) # [1, 2, 3, 4]

这三种都是浅拷贝,对于一层列表来说够用了。但列表里如果还有嵌套列表,浅拷贝只会复制外层列表,内层列表还是共享同一个对象。

a = [[1, 2], [3, 4]] b = a.copy() b[0][0] = 99 print(a) # [[99, 2], [3, 4]] a的内部列表也被改了

5.2 什么时候必须用copy模块的deepcopy

如果列表是多层嵌套,且需要完全独立复制,必须用copy.deepcopy()

import copy a = [[1, 2], [3, 4]] b = copy.deepcopy(a) b[0][0] = 99 print(a) # [[1, 2], [3, 4]] a完全不受影响 print(b) # [[99, 2], [3, 4]]

deepcopy会递归复制所有层级,代价是速度慢、内存占用高。所以在使用时想清楚:如果列表只有一层,用copy()就够了,没必要用deepcopy。如果列表嵌套层级很深但不需要完全独立(比如你只读不改),也别用deepcopy,那是性能杀手。

6. 实战案例:从去重、词频统计到复杂数据排序

6.1 列表去重的几种写法,哪种最高效

列表去重是面试高频题,也是日常开发常遇到的需求。最简单的写法是用集合:

nums = [1, 2, 2, 3, 3, 3] unique = list(set(nums)) print(unique) # [1, 2, 3]

但这种方法会改变元素顺序(集合是无序的)。如果需要保留原始顺序,可以用循环配合辅助集合:

nums = [3, 1, 2, 3, 2, 1] seen = set() unique = [] for num in nums: if num not in seen: seen.add(num) unique.append(num) print(unique) # [3, 1, 2]

Python 3.7+ 中dict保持插入顺序,所以还有一种更简洁的写法:

nums = [3, 1, 2, 3, 2, 1] unique = list(dict.fromkeys(nums)) print(unique) # [3, 1, 2]

如果列表元素是字典等不可哈希对象,集合方式就不能用了,需要自己写判断逻辑。实战中我用dict.fromkeys比较多,因为它保留了顺序,而且性能很好。

6.2 词频统计:列表、字典和collections.Counter

词频统计是文本处理中的经典场景。先看基础版,用字典手动统计:

words = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'] word_count = {} for word in words: word_count[word] = word_count.get(word, 0) + 1 print(word_count) # {'apple': 3, 'banana': 2, 'orange': 1}

dict.get(word, 0)的写法比if word in word_count更简洁,也是Python社区常见的风格。不过更推荐直接用collections.Counter,它本身就是专为计数设计的:

from collections import Counter words = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'] word_count = Counter(words) print(word_count) # Counter({'apple': 3, 'banana': 2, 'orange': 1}) # 取出现次数最多的前2个 print(word_count.most_common(2)) # [('apple', 3), ('banana', 2)]

Counter还有updatesubtractelements等方法,功能很全。如果你要统计的数据量很大,写成Counter(words)一行就能搞定,比手写循环省事很多。

6.3 按优先级排序多个字段

实际开发里经常遇到按多个字段排序的需求。比如一个学生列表,每个学生有语文、数学、英语三门成绩,想按总分降序,总分相同按数学成绩降序排序:

students = [ {'name': 'Alice', 'chinese': 90, 'math': 85, 'english': 78}, {'name': 'Bob', 'chinese': 85, 'math': 90, 'english': 88}, {'name': 'Charlie', 'chinese': 90, 'math': 92, 'english': 70}, ] # 先按数学成绩降序,再按总分降序 students.sort( key=lambda s: (s['chinese'] + s['math'] + s['english'], s['math']), reverse=True )

这里有个容易出错的地方:如果多个排序字段的方向不一致(比如总分升序但数学降序),直接传reverse=True会让所有字段都反向。这种情况下有两种解决办法:一是把数字取负,二是用functools.cmp_to_key写自定义比较函数。

# 总分升序,数学降序 students.sort(key=lambda s: (s['chinese'] + s['math'] + s['english'], -s['math']))

用负数取反是很Pythonic的写法,前提是字段是数字。如果是字符串字段,就需要用更复杂的方案了。我处理这类需求时,优先把排序逻辑拆清楚,避免在一个lambda里堆太多逻辑,那样后面维护时会很痛苦。

6.4 列表分组与结构化处理

把一个大列表按某种规则分组,也是高频需求。比如有一批订单记录,想按商品名称分组,把相同商品的订单汇总到一个列表里:

orders = [ {'product': 'apple', 'amount': 5}, {'product': 'banana', 'amount': 3}, {'product': 'apple', 'amount': 8}, {'product': 'cherry', 'amount': 2}, ] grouped = {} for order in orders: product = order['product'] grouped.setdefault(product, []).append(order) # 或者用 defaultdict from collections import defaultdict grouped = defaultdict(list) for order in orders: grouped[order['product']].append(order) for product, items in grouped.items(): total = sum(item['amount'] for item in items) print(product, total) # apple 13 # banana 3 # cherry 2

setdefaultdefaultdict都需要掌握。defaultdict(list)的写法在遍历时更自然,不会因为键不存在而报错。

如果你需要的是按条件把列表拆成多个子列表,比如把大于10和小于等于10的分开,列表推导式可以做,但多次遍历效率不高。数据量不大时无所谓,数据量大时可以考虑用单次遍历分组:

nums = [3, 15, 7, 22, 9, 30] big, small = [], [] for num in nums: if num > 10: big.append(num) else: small.append(num)

7. 常见问题排查:遇到这些情况应该怎么排查和解决

7.1 修改了列表但其他变量也变了

如果你用b = a这种方式复制列表,修改ba也会变。这不是Bug,是Python变量引用模型导致的。解决方法是使用a.copy()a[:]。我发现很多人第一次遇到这个现象时特别困惑,一句话解释就是:变量名是标签,不是盒子。b = a的意思是给同一个列表对象贴了第二张标签,不是创建了一个新盒子。

7.2 列表删除元素时漏删

前面已经讲过,遍历时用remove会漏元素。解决方案是:遍历副本、用推导式、倒序遍历。这是一个非常高频的排查点,面试中考这个问题的频率也特别高。如果你在牛客、LeetCode上刷题,遇到“删除列表中的指定元素”这类题目,一定要记得这个坑。

7.3 检查元素是否存在的性能问题

当列表很大且需要频繁判断元素是否在列表中时,用in是O(n)的,会导致程序明显变慢。解决办法是把列表转成set,把判断变成O(1)。但前提是元素是可哈希的。这个优化在爬虫去重场景里我几乎天天用,比如维护一个已处理URL的集合,如果URL很多,用列表存和用集合存,性能差距是数量级的。

7.4 列表排序后数据错乱或顺序不是预期

排序结果和自己设想的不一样,通常是没有理解sort是原地修改,或者在key函数里写了错误的逻辑。排查方式很简单:先用小数据列表打印每一步结果,确认key函数返回的值是否符合预期。在lambda里加打印是一种直接粗暴但有效的排查方式。

7.5 列表索引越界

访问不存在的索引会报IndexError。如果你不确定索引是否有效,可以先判断长度,或者用try...except捕获异常。另外,pop()在空列表上调用也会报IndexError,所以在删除前要确认列表不为空。

7.6 列表、元组、集合、字典怎么选

最后补充一个高频问题:listtuplesetdict到底应该怎么选。简单区分标准:

  • 需要有序、可修改、允许重复:用list
  • 需要有序、只读、可哈希:用tuple
  • 需要去重、无序、快速判断存在:用set
  • 需要键值映射、快速查找:用dict

这个选型标准在绝大多数场景下都够用。我处理量化交易策略数据时,日线、分钟线数据用listDataFrame,去重和集合判断用set,指标映射用dict。每种数据结构都有它的定位,混着用反而容易出问题。

我个人在实际操作中的一个体会是:列表方法本身并不复杂,难点在于理解Python的引用模型、可变对象的机制,以及不同方法在不同场景下的性能差异。把这些底层逻辑搞清楚了,列表用法基本就融会贯通了。还有一个小技巧想分享给大家:在写代码之前先想清楚,我是要“原地修改”还是“生成新列表”,这两个方向决定了应该用哪一类方法,也能帮你避免很多莫名的Bug。希望这篇内容能让你对Python列表有一个系统且扎实的理解,日常写代码时少踩点坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询