关于问题1:为什么range比combinations慢?
虽然for j in range(...) 确实具有只分配一个数字的优势,但它具有坏处一遍又一遍地创造它们。在 Python 中,数字是对象,它们的创建(和删除)需要一点时间。
另一方面,combinations(...) 首先只创建和存储一次数字对象,然后成对地重复使用它们。你可能认为“等一下,它可以重复使用数字,但它产生的对为tuple 对象,所以它还每次迭代创建一个对象!”.嗯......它有一个优化。它实际上一遍又一遍地重用相同的tuple 对象,用不同的数字填充它。“什么?不可能!元组是不可变的!”嗯......表面上它们是不可变的,是的。但是,如果combinations 迭代器发现没有其他对其结果元组的引用,那么它会“作弊”并无论如何都会对其进行修改。在 C 代码级别,它可以做到这一点。如果没有其他东西可以参考它,那么就没有害处。请注意,for i, j in ... 解包元组并且不保留对它的引用。如果您改为使用for pair in ...,那么pair 是对它的引用,并且不会应用优化,实际上每次都会创建一个新的result 元组。有兴趣的可以看combinations_next的源码。
关于问题2:最快的方法是什么?
我发现了四种更快的方法:
44.1 ms ± 0.2 ms f_combinations_pure
51.7 ms ± 0.1 ms f_list
52.7 ms ± 0.2 ms f_tee
53.6 ms ± 0.1 ms f_copy_iterator
54.6 ms ± 0.1 ms f_tuples
57.7 ms ± 0.3 ms f_combinations
66.6 ms ± 0.1 ms f_ranges
所有四种更快的方法都避免了使 range 解决方案变慢的原因:它们不是创建(和删除)Θ(n²) int 对象,而是一遍又一遍地重用相同的对象。
f_tuples 将它们放入一个元组并遍历切片:
def f_tuples(n):
nums = tuple(range(n))
return [(i, j)
for i in nums
for j in nums[i+1:]]
f_list 将它们放入一个列表中,然后在每个 j-loop 之前,它会删除第一个数字:
def f_list(n):
js = list(range(n))
return [(i, j)
for i in range(n)
if [js.pop(0)]
for j in js]
f_copy_iterator 将它们放入一个元组中,然后为i 使用一个迭代器,为j 使用该迭代器的copy(这是一个在i 之后开始一个位置的迭代器):
def f_copy_iterator(n):
nums = iter(tuple(range(n)))
return [(i, j)
for i in nums
for j in copy(nums)]
f_tee 使用 itertools.tee 获得与 copy 类似的效果。它的JS 是j 值的主要迭代器,在每个j-loop 之前,它会丢弃第一个值,然后对JS 进行开球以获取剩余值的第二个迭代器:
def f_tee(n):
return [(i, j)
for JS in [iter(range(n))]
for i in range(n)
for _, (JS, js) in [(next(JS), tee(JS))]
for j in js]
额外的问题:像那些更快的方法那样优化是否值得?
嗯,应该不会吧。可能你最好只使用for i, j in combinations(...)。更快的方法并没有快多少,而且它们更复杂一些。另外,实际上,您实际上会使用i 和j 做一些事情(例如获取子字符串),因此相对较小的速度优势变得相对较小。
但我希望你至少觉得这很有趣,也许学到了一些新的东西是有朝一日有用。
完整的基准代码
Try it online!
def f_combinations_pure(n):
return list(combinations(range(n), 2))
def f_combinations(n):
return [(i, j) for i, j in combinations(range(n), 2)]
def f_ranges(n):
return [(i, j) for i in range(n) for j in range(i+1, n)]
def f_tuples(n):
nums = tuple(range(n))
return [(i, j) for i in nums for j in nums[i+1:]]
def f_list(n):
js = list(range(n))
return [(i, j) for i in range(n) if [js.pop(0)] for j in js]
def f_copy_iterator(n):
nums = iter(tuple(range(n)))
return [(i, j) for i in nums for j in copy(nums)]
def f_tee(n):
return [(i, j)
for JS in [iter(range(n))]
for i in range(n)
for _, (JS, js) in [(next(JS), tee(JS))]
for j in js]
fs = [
f_combinations_pure,
f_combinations,
f_ranges,
f_tuples,
f_list,
f_copy_iterator,
f_tee
]
from timeit import default_timer as time
from itertools import combinations, tee
from statistics import mean, stdev
from random import shuffle
from copy import copy
# Correctness
expect = fs[0](1000)
for f in fs:
result = f(1000)
assert result == expect
# Prepare for timing
times = {f: [] for f in fs}
def stats(f):
ts = [t * 1e3 for t in sorted(times[f])[:5]]
return f'{mean(ts):4.1f} ms ± {stdev(ts):3.1f} ms '
# Timing
for i in range(25):
shuffle(fs)
for f in fs:
start = time()
result = f(1000)
end = time()
times[f].append(end - start)
del result
# Results
for f in sorted(fs, key=stats):
print(stats(f), f.__name__)