itertools.combinations 是枚举所有组合的最轻量可靠方案,需转为 list 或迭代才能获取具体组合;math.comb 仅返回纯数值 C(n,k),Python 3.8+ 支持,旧版可用阶乘公式替代。

用 itertools.combinations 直接生成组合,别手写递归
Python 求组合数(C(n,k))本身不等于“枚举所有组合”,但多数人实际要的是后者——比如从列表里挑 3 个元素做测试、遍历所有特征子集。这时候 itertools.combinations 是最轻量、最可靠的选择,比自己写递归或用 math.comb(只返回数值)更贴近真实需求。
常见错误是误以为它返回的是“数量”,结果调用后得到一个 itertools.combinations 对象却没转成 list 或迭代,打印出来只看到对象地址。
- 必须用
list(itertools.combinations(items, k))或显式循环才能拿到具体组合 -
items可以是任意可迭代对象(list、str、range),但顺序敏感:组合按输入顺序生成,不自动去重 - 如果
items含重复元素(如[1,1,2]),combinations会把它们当不同位置处理,不会 dedupe
math.comb 算纯数字组合数,Python 3.8+ 才有
如果你真只需要 C(n,k) 的整数值(比如算概率分母、判断是否超限),math.comb 是最快最稳的方案,底层用位运算优化,不构造任何中间对象。
容易踩的坑是版本兼容性:3.7 及更早报 AttributeError: module 'math' has no attribute 'comb';有人退而求其次用 scipy.special.comb,但它默认返回 float,对大数可能丢精度,且引入额外依赖。
立即学习“Python免费学习笔记(深入)”;
- 确认 Python 版本:
import sys; print(sys.version) - 替代方案(兼容旧版):
from math import factorial; factorial(n) // (factorial(k) * factorial(n-k)),但注意阶乘增长极快,n > 1000 就容易溢出或变慢 -
math.comb(1000, 500)没问题,但factorial(1000)计算开销明显更高
去重组合?先排序再用 itertools.combinations + set 不靠谱
当原始数据含重复值(如 [1,1,2,2]),又想枚举“值层面”的不重复组合(即 (1,1,2) 算一种,不是按索引算两种),不能直接套 combinations 后塞进 set —— 因为元组可哈希,但排序混乱会导致等价组合被当成不同项。
正确做法是预处理:对输入排序 + 分组计数,再手写逻辑控制每个值最多取几次。不过大多数场景其实不需要这么复杂——先明确你要的是“索引组合”还是“值组合”。90% 的 case,用户真正需要的只是前者,误以为要后者。
- 错误示范:
set(combinations([1,1,2], 2))→ 得到{(1,1), (1,2), (1,2)},但 set 去重后只剩两个,漏掉一个 (1,2),因为两个 (1,2) 实际来自不同索引,内容相同但不可区分 - 若真需值去重,推荐用
itertools.combinations_with_replacement配合sorted(set(items)),但语义已变成“从唯一值中可重复选”,和原意不同 - 更安全的做法:用
collections.Counter构建多集,再基于频次回溯生成,但这已超出itertools范畴
性能临界点在哪?别在循环里反复调用 combinations
itertools.combinations 本身是惰性生成器,内存友好,但每次调用都会重建迭代器。如果在高频循环里反复执行 combinations(data, k)(比如嵌套在 for i in range(1000): 里),开销主要来自对象创建和状态初始化,而非计算本身。
另一个隐形成本是结果体积:C(20,10) = 184756,C(25,12) ≈ 5e6,内存和遍历时间会陡增。这时候得考虑是否真的需要全量枚举,还是可以 early-stop、采样、或换用生成式逻辑。
- 避免:
for _ in range(100): list(combinations(big_list, 5)) - 改为缓存一次:
all_combos = list(combinations(big_list, 5)),后续复用 - 如果只需随机几个,用
random.sample(list(combinations(...)), k=10),但注意先转 list 会吃内存;更省内存的方式是用itertools.islice随机跳过前 N 项
实际用哪条路,取决于你手头的数据规模、是否允许重复、以及到底要数值还是具体组合——这三者没理清,代码写得再“标准”,也容易跑偏。










