Python collections 实战:defaultdict、Counter、deque 解决三类高频痛点你写 Python 处理数据时,大概率反复写过这三段代码:判断 key 在不在字典里再初始化、手动累加计数、用list.pop(0)从头部删元素。它们能跑,但要么啰嗦要么慢。标准库collections里有三个容器专治这三种痛点。这篇用真实场景对比「朴素写法」和「正确写法」,让你以后条件反射就用对。痛点一:分组时反复判断 key 存在——用 defaultdict把一批订单按用户分组,朴素写法:orders[(alice,100),(bob,50),(alice,30),(bob,20),(alice,10),]grouped{}foruser,amountinorders:ifusernotingrouped:# 每次都要判断初始化grouped[user][]grouped[user].append(amount)# {alice: [100, 30, 10], bob: [50, 20]}那句if user not in grouped出现在每一个分组循环里,重复又容易忘。defaultdict让「key 不存在时自动初始化」变成默认行为:fromcollectionsimportdefaultdict groupeddefaultdict(list)# 访问不存在的 key 时,自动 list()foruser,amountinorders:grouped[user].append(amount)# 直接 append,不用判断defaultdict(list)里的list是一个工厂函数:每当访问一个不存在的 key,它就调用list()生成空列表作为默认值。换成int就能做累加计数:countsdefaultdict(int)# 默认值 0foruser,_inorders:counts[user]1# 不存在时先当 0 再 1# {alice: 3, bob: 2}一个坑:defaultdict的「自动创建」发生在读取时。d[k]只要访问就会创建 key,哪怕你只是想检查:ddefaultdict(list)ifd[missing]:# 这一句就凭空创建了 d[missing] []passprint(dict(d))# {missing: []} ← 意外多了一个 key想检查而不创建,用k in d或d.get(k),别用d[k]。痛点二:计数与找 Top N——用 Counter统计词频,朴素写法要么手动defaultdict(int),要么更原始。Counter一步到位:fromcollectionsimportCounter wordsapple banana apple cherry banana apple.split()cCounter(words)# Counter({apple: 3, banana: 2, cherry: 1})它的杀手锏是most_common(n),直接按出现次数排序拿 Top N,省掉手写sorted(..., key...):c.most_common(2)# [(apple, 3), (banana, 2)]Counter还支持算术运算,做「两批数据的差异」特别顺手:beforeCounter(a3,b1,c2)afterCounter(a5,b1,d1)after-before# Counter({a: 2, d: 1}) 只保留正数增量afterbefore# 合并累加(afterbefore)# 交集取较小值:Counter({a: 3, b: 1})注意-运算会丢弃结果为 0 或负数的项(c从 2 变 0 直接消失)。想保留负数用c.subtract():dCounter(a1)d.subtract(Counter(a3))# d 变成 Counter({a: -2}),负数保留痛点三:从头部频繁增删——用 deque实现一个「最近 N 条」的滑动窗口,或者 BFS 队列,朴素写法用 list:queue[]queue.append(x)# 尾部加,O(1),没问题queue.pop(0)# 头部删,O(n)!每次都要把后面所有元素前移list.pop(0)是O(n)的——列表底层是连续数组,删头部要把后面全部元素往前挪一格。数据一大,性能就崩。deque(双端队列)两头增删都是O(1):fromcollectionsimportdeque queuedeque()queue.append(1)# 右侧入队 O(1)queue.append(2)queue.popleft()# 左侧出队 O(1),取代 list.pop(0)# → 1deque最实用的一个特性是maxlen,自动实现定长滑动窗口:满了之后,一端进、另一端自动挤出去:recentdeque(maxlen3)# 只保留最近 3 条foriinrange(5):recent.append(i)# deque([2, 3, 4], maxlen3) ← 0 和 1 被自动挤掉了用它做「最近访问记录」「移动平均窗口」再合适不过,完全不用手动判断长度再删头。一个性能直觉对比(10 万次头部出队):importtimefromcollectionsimportdeque N100_000lstlist(range(N))ttime.perf_counter()whilelst:lst.pop(0)# O(n) 每次print(list:,time.perf_counter()-t)# 秒级dqdeque(range(N))ttime.perf_counter()whiledq:dq.popleft()# O(1) 每次print(deque:,time.perf_counter()-t)# 毫秒级差距是数量级的。小结defaultdict(factory):分组、累加时告别if key not in d;但注意d[k]读取即创建,检查用in或.get()。Counter:计数一步到位,most_common(n)拿 Top N,还能用 - 做集合算术求差异(-会丢弃非正数项)。deque:两端增删 O(1),取代慢成 O(n) 的list.pop(0);maxlen自动做定长滑动窗口。一句话记忆点:分组用 defaultdict、计数用 Counter、队列用 deque——看到pop(0)就该警觉换 deque。