跳到主要内容

collections、itertools、functools 与 operator

本节目标

查询 Python collections、itertools、functools 和 operator 的选择与惰性边界。

这些工具的共同问题不是“函数够不够短”,而是容器、迭代器和缓存何时拥有数据。以下以 collectionsitertoolsfunctoolsoperator 的 Python 3.14 文档为准。数据格式的输入输出边界见结构化数据、序列化、压缩与 SQLite,数值和时间值的选择见数值、随机、统计与日期时间

Counter 与多重集合

Counter 把元素映射到计数,适合频次和多重集合运算,也能明确表示零、负计数,无需再手写字典累加。most_common() 返回排序后的快照,输出顺序和并列规则应成为展示 API 的一部分。

from collections import Counter

top = Counter(["docs", "tests", "docs"]).most_common(1)

存储的零计数键会出现在成员测试和键迭代中,而缺失键不会;两者用下标读取都返回 0,且 Counter 相等性把缺失计数视为零。elements() 也只产生正计数元素。不要把未排序的输入、并列频次或 Counter 的缺失计数语义当成跨系统稳定的业务排序合同。

defaultdict 与缺失值工厂

defaultdict 在下标读取缺失键时可创建默认值。累积列表或计数时可让工厂服务于明确的写入路径;查询应改用不会改变映射的 API。defaultdict.__missing__() 只由 __getitem__ 调用,get() 不会调用它,因此两种读取的副作用边界不同。

from collections import defaultdict

groups = defaultdict(list)
groups["python"].append("collections")

groups.get("missing") 返回 None(或给定默认值)而不插入键;把它误认为会运行 default_factory 会漏掉初始化或意外制造状态。工厂也不应执行网络、数据库或其他昂贵副作用。

deque、队列与滑动窗口

deque 的两端追加和弹出适合 FIFO 队列及最近 N 项窗口,避免了在列表开头反复插入或删除。设置 maxlen 也就设定了容器保留边界:新元素加入时最旧元素会自动丢弃。

from collections import deque

window = deque("abcd", maxlen=3)

窗口只保留当前值,并不保存被淘汰的历史;迭代它得到的是此刻的顺序。不要把 deque 当成线程间带阻塞、任务完成计数或关闭协议的队列;这些是 queue 的 API 边界。

namedtupleChainMap 与轻量结构

namedtuple 提供带字段名的不可变 tuple,适合稳定的轻量记录;ChainMap 按映射层次查找键,适合读取多层配置,但写入目标必须显式。后者不复制底层映射,读取和写入都会受这些共享映射后续变化影响。

from collections import ChainMap, namedtuple

Course = namedtuple("Course", "name level")
settings = ChainMap({"level": 2}, {"level": 1})

字段名不是运行时类型校验,namedtuple 也不替代有默认值或验证的领域模型。不要以为 ChainMap 合并了字典;默认写入只落到第一个映射,缓存它的查找结果也会跨越配置更新边界。

惰性与无限迭代工具

count()cycle()repeat() 和组合器通常按需产生值,因此惰性迭代器必须接到明确的消费者,无限来源还要先设定终止边界。迭代器一旦被消费就前进,不能假定能再次从头遍历。

from itertools import count, islice

first_three = list(islice(count(10), 3))

直接 list(count()) 不会结束,cycle() 还可能为重放而缓存输入。不要把惰性当作“不占资源”:下游 listsortedtee 会物化、缓冲或延迟暴露上游错误。

组合、排列与批处理

product()permutations()combinations() 会在构造迭代器时先把各输入池化为 tuple,尽管其结果按需产生;batched() 则逐步消费单个输入迭代器,不会在构造时收集全部输入。组合规模应在生成前受限,结果再由消费者逐项或按批处理。batched() 的最后一个 tuple 可以短于批大小,因此下游 API 必须接受尾批或在需要固定大小时启用严格合同。

from itertools import batched

batches = list(batched(range(1, 6), 2))
functional_tools_report.py
from collections import Counter, deque
from functools import cache
from itertools import batched
from operator import itemgetter


counts = Counter(["docs", "tests", "docs"])
top_name, top_count = counts.most_common(1)[0]
batches = list(batched(range(1, 6), 2))
window = deque("abcd", maxlen=3)
people = [{"name": "Lin"}, {"name": "Ada"}]
sorted_names = [item["name"] for item in sorted(people, key=itemgetter("name"))]
calls = 0


@cache
def normalize(value: str) -> str:
global calls
calls += 1
return value.strip().lower()


normalize("docs")
normalize("docs")
print(f"top={top_name}:{top_count}")
print(f"batched={batches}")
print(f"window={list(window)}")
print(f"sorted={sorted_names}")
print(f"cache-hit={calls == 1}")
top=docs:2
batched=[(1, 2), (3, 4), (5,)]
window=['b', 'c', 'd']
sorted=['Ada', 'Lin']
cache-hit=True

报告把有限范围物化为批次以便确定性展示;原始 batched 本身仍会消费输入。不要把排列、组合或笛卡尔积无界地交给 list,组合数量会迅速增长,也不要忽略尾批长度。

groupby 与相邻分组

groupby() 不会汇总整个输入中的同键值;需要全局分组时应先按同一键排序,并在外层迭代前进前消费当前组。groupby() 只合并相邻且键相等的元素,并与源迭代器共享同一个迭代器。

from itertools import groupby

groups = [(key, list(items)) for key, items in groupby(sorted("baab"))]

外层循环进入下一组后,上一个组迭代器已不能再可靠消费;这不是独立缓存。不要把未排序的日志或流输入当作数据库式 GROUP BY,也不要把共享迭代器传给两个消费者期待各自完整读取。

缓存、偏函数与 wraps

cache 记住可哈希参数对应的返回值,partial 固定一部分参数,wraps 保留被包装函数的元数据。缓存应限于纯且边界明确的计算,调用适配则要保留可诊断的函数身份。无界缓存会保留参数和返回值的强引用,直到显式清除或进程结束。

from functools import Placeholder, cache, partial

trimmed = partial(str.strip, Placeholder, " ")

str.strip()chars 是位置参数;Placeholder 为第一个位置参数留出调用时传入的位置,因此 trimmed(" docs ") 会返回 "docs"。缓存不会使副作用函数安全,也不会自动感知文件、时间或配置变化;cache_clear() 是调用方管理生命周期的 API。不要缓存无限键空间或大对象,也不要让缺少 wraps 的装饰器丢失名称、文档和签名线索。

reduce 与单分派

reduce 把二元归约累计为一个值,只有初值和运算顺序清楚时才适用。singledispatch 按第一个参数的类型分派,因此首参数类型必须正是预期的扩展轴。

from functools import reduce, singledispatch

total = reduce(lambda left, right: left + right, [1, 2, 3], 0)

省略初值会让空输入失败,且不可结合运算的顺序会改变结果。不要期待 singledispatch 按第二个参数、返回值或值内容选择实现;多维规则应由显式分支或其他分派设计承担。

operator 与 key 函数

itemgetterattrgettermethodcaller 把常见访问操作变成可调用对象,适合字段访问稳定且简单的排序与提取,也能直接呈现 key 合同。sorted() 会消费输入迭代器并返回新列表,因此排序是物化与内存边界。

from operator import itemgetter

names = sorted(records, key=itemgetter("name"))

缺失键、缺失属性和不同类型键会在调用时失败或不可比较;不要把便利的访问器当作输入校验。排序 key 也不替代业务并列规则、区域化比较或无限流处理。