Python数据结构踩坑指南:从list到set,这些坑我都替你踩过了

35 阅读7分钟

你有没有遇到过这种情况:

# 写了个简单的去重功能
items = [1, 2, 3, 2, 4, 3, 5]
unique_items = []
for item in items:
    if item not in unique_items:  # 你猜这个循环跑了多少次?
        unique_items.append(item)

运行后发现数据量一大,程序慢得像乌龟爬行。然后你百度了一圈,才知道Python有个叫set的东西...

别笑,这种坑我踩过,你可能也踩过。

今天咱们就来彻底搞懂Python的四大金刚:列表、元组、字典、集合。

先搞懂一个灵魂问题:为什么需要这么多数据结构?

想象你在整理衣柜:

  • 列表(list):就像挂衣服的杆子,可以随便加衣服、取衣服,还能调整顺序
  • 元组(tuple):就像打包好的行李箱,装好后就不能改了,但跑得快
  • 字典(dict):就像带标签的抽屉,按标签找东西超快
  • 集合(set):就像去重神器,自动帮你去掉重复的东西

不同的场景用不同的工具,这才是编程的艺术!

列表(List):最灵活但也最容易踩坑

列表是Python中最常用的数据结构,但也是最容易写出低效代码的地方。

坑1:频繁的append操作

# ❌ 很多人这么写
result = []
data = range(100000)
for i in data:
    result.append(i * 2)  # 每次append都可能触发内存重分配

# ✅ 大神这么写
data = range(100000)
result = [i * 2 for i in data]  # 一次性分配内存,速度快几倍

为什么差异这么大?列表的append操作有个"秘密":

当列表空间不够时,Python会重新分配一块更大的内存,然后把旧数据复制过去。数据量大的时候,这个过程就很耗时。

坑2:用in操作符找元素

# ❌ O(n)时间复杂度,数据大了就是灾难
users = ['alice', 'bob', 'charlie', 'david', 'eve']
if 'zebra' in users:  # 最坏情况下要遍历整个列表
    print('找到了')

# ✅ 如果需要频繁查找,考虑用set
user_set = {'alice', 'bob', 'charlie', 'david', 'eve'}
if 'zebra' in user_set:  # O(1)时间复杂度
    print('找到了')

记住一个简单的原则:列表适合增删改,不适合频繁查找

坑3:修改正在遍历的列表

# ❌ 这会跳过一些元素
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers:
    if num % 2 == 0:
        numbers.remove(num)  # 删除元素后,后面的元素会前移
# 结果:[1, 3, 5]  期望:[1, 3, 5]?好像对?
# 但如果改成删除奇数...
for num in numbers:
    if num % 2 == 1:
        numbers.remove(num)
# 结果:[2, 4, 6] 还是 [2, 6]?答案是后者!

# ✅ 正确姿势:创建新列表或用列表推导
numbers = [1, 2, 3, 4, 5, 6]
odd_numbers = [num for num in numbers if num % 2 == 0]

元组(Tuple):不可变就是它的超能力

很多人觉得元组就是"不能修改的列表",这个理解太片面了。

为什么需要不可变?

想象一下:

# 元组可以作为字典的键
location_cache = {
    (39.9042, 116.4074): "北京",
    (31.2304, 121.4737): "上海"
}

# 列表就不行
# location_cache = {
#     [39.9042, 116.4074]: "北京"  # TypeError: unhashable type: 'list'
# }

不可变=可哈希=可以作为字典的键,这才是元组的真正价值!

元组的性能优势

import timeit

# 创建100万元素的列表和元组
list_time = timeit.timeit('[1] * 1000000', number=100)
tuple_time = timeit.timeit('(1,) * 1000000', number=100)

print(f'列表创建时间: {list_time:.4f}s')
print(f'元组创建时间: {tuple_time:.4f}s')

你会发现元组的创建和访问速度都比列表快,因为Python不需要为元组预留扩展空间。

元组的"可变"陷阱

# ❌ 看起来不可变,但里面的东西可能变
t = (1, [2, 3], 4)
t[1].append(5)  # 这居然可以!
print(t)  # (1, [2, 3, 5], 4)

# 解释:元组不可变的是引用,不是引用指向的对象
# 这就像你家的地址不能变,但家里可以装修

字典(Dict):查找的艺术

字典是Python中最强大的数据结构之一,但很多开发者只用到了它的皮毛。

坑1:KeyError的恐惧

# ❌ 每次都要检查key存在
user_scores = {'alice': 95, 'bob': 88}
score = user_scores['charlie'] if 'charlie' in user_scores else 0

# ✅ 用get方法更优雅
score = user_scores.get('charlie', 0)  # 找不到就返回默认值

# ✅ 更高级:用defaultdict
from collections import defaultdict
word_count = defaultdict(int)  # 默认值为0
word_count['hello'] += 1  # 不用先检查key存在

坑2:遍历字典的低效姿势

# ❌ 先遍历key再取value
user_data = {'name': 'Alice', 'age': 25, 'city': 'Beijing'}
for key in user_data.keys():
    print(key, user_data[key])  # 两次查找

# ✅ 直接遍历items
for key, value in user_data.items():  # 一次遍历获取所有
    print(key, value)

字典的高级玩法

# 字典合并(Python 3.9+)
dict1 = {'a': 1, 'b': 2}
dict2 = {'b': 3, 'c': 4}
merged = dict1 | dict2  # {'a': 1, 'b': 3, 'c': 4}

# 字典推导式
squares = {x: x**2 for x in range(5)}  # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# 嵌套字典访问
data = {'user': {'profile': {'name': 'Alice'}}}
name = data.get('user', {}).get('profile', {}).get('name', 'Unknown')

集合(Set):去重和交并差的王者

集合是Python中最被低估的数据结构,很多人甚至不知道它的存在。

集合的超能力:去重

# ❌ 用列表去重,代码又长又慢
items = [1, 2, 3, 2, 4, 3, 5, 1, 6]
unique_items = []
for item in items:
    if item not in unique_items:  # O(n)查找
        unique_items.append(item)
# 时间复杂度:O(n²)

# ✅ 用集合一行搞定
unique_items = list(set(items))  # O(n)

这性能差异在数据量大的时候是天壤之别!

集合的数学运算

set_a = {1, 2, 3, 4, 5}
set_b = {4, 5, 6, 7, 8}

# 交集:共同元素
common = set_a & set_b  # {4, 5}

# 并集:所有元素
all_items = set_a | set_b  # {1, 2, 3, 4, 5, 6, 7, 8}

# 差集:a有b没有
only_in_a = set_a - set_b  # {1, 2, 3}

# 对称差集:只在其中一个集合的元素
exclusive = set_a ^ set_b  # {1, 2, 3, 6, 7, 8}

这些操作在处理数据分析、权限控制等场景时特别好用。

集合的限制:元素必须可哈希

# ❌ 这会报错
invalid_set = {[1, 2], [3, 4]}  # TypeError: unhashable type: 'list'

# ✅ 正确做法
valid_set = {(1, 2), (3, 4)}  # 元组是不可变的,可以放入集合

实战场景选择指南

记不住这么多规则?记住这个选择指南:

数据量大且需要频繁查找

# 用户黑名单检查
blacklist = {'user123', 'user456', 'user789'}  # 用set
if user_id in blacklist:  # O(1)查找
    reject_access()

数据需要保持不变性

# 配置参数
CONFIG = ('prod', 'mysql', 'localhost', 3306)  # 用tuple
# 防止意外修改,还可以作为字典的键

需要快速通过键值查找

# 用户信息映射
user_info = {
    'user123': {'name': 'Alice', 'age': 25},
    'user456': {'name': 'Bob', 'age': 30}
}  # 用dict

需要灵活增删改查

# 动态任务列表
tasks = ['task1', 'task2', 'task3']  # 用list
tasks.append('task4')
tasks.remove('task1')
tasks[2] = 'updated_task3'

性能对比:眼见为实

import timeit
import random

# 准备测试数据
data = list(range(100000))
search_target = 99999

# 列表查找
list_time = timeit.timeit(
    lambda: search_target in data,
    number=1000
)

# 集合查找
data_set = set(data)
set_time = timeit.timeit(
    lambda: search_target in data_set,
    number=1000
)

print(f'列表查找时间: {list_time:.4f}s')
print(f'集合查找时间: {set_time:.4f}s')
print(f'性能差异: {list_time/set_time:.1f}倍')

在我的机器上,集合查找比列表快了1000倍以上!

总结:选择数据结构的三部曲

记住这个简单的决策流程:

  1. 需要查找吗?

    • 是 → 2
    • 否 → 4
  2. 需要关联键值对吗?

    • 是 → 用dict
    • 否 → 用set
  3. 数据需要修改吗?

    • 是 → 4
    • 否 → 用tuple
  4. list

一句话总结:

  • 列表是"万金油",但不是"最优解"
  • 元组是"保护伞",适合不变的配置
  • 字典是"查表神器",键值对的首选
  • 集合是"去重神器",数学运算的利器

下次写代码的时候,停下来想一想:我真的要用list吗?也许有更好的选择。

毕竟,选择合适的数据结构,比学会复杂的算法更重要


你在项目里踩过哪些数据结构的坑?评论区聊聊,看看有没有比我更惨的案例~