我有一个Python函数,它接受一个列表并返回一个生成器,产生每个相邻对的2元组,例如。
>>> list(pairs([1, 2, 3, 4]))
[(1, 2), (2, 3), (3, 4)]
我考虑过使用2个切片的实现:
def pairs(xs):
for p in zip(xs[:-1], xs[1:]):
yield p
和一个以更程序化的方式写的:
def pairs(xs):
last = object()
dummy = last
for x in xs:
if last is not dummy:
yield last,x
last = x
使用range(2 ** 15)
作为输入进行测试会产生以下时间(您可以找到我的测试代码并输出here):
2 slices: 100 loops, best of 3: 4.23 msec per loop
0 slices: 100 loops, best of 3: 5.68 msec per loop
无切片实现的部分性能影响是循环中的比较(if last is not dummy
)。删除它(使输出不正确)可以改善其性能,但它仍然比zip-a-pair-of slice实现慢:
2 slices: 100 loops, best of 3: 4.48 msec per loop
0 slices: 100 loops, best of 3: 5.2 msec per loop
所以,我很难过。为什么要将2个切片压缩在一起,有效地在列表上并行迭代两次,比迭代一次更快,更新last
和x
?
修改
Dan Lenski proposed第三个实施:
def pairs(xs):
for ii in range(1,len(xs)):
yield xs[ii-1], xs[ii]
这是与其他实现的比较:
2 slices: 100 loops, best of 3: 4.37 msec per loop
0 slices: 100 loops, best of 3: 5.61 msec per loop
Lenski's: 100 loops, best of 3: 6.43 msec per loop
它甚至更慢!这让我感到困惑。
编辑2:
ssm suggested使用itertools.izip
代替zip
,它甚至比zip
更快:
2 slices, izip: 100 loops, best of 3: 3.68 msec per loop
所以,到目前为止,izip
是赢家!但仍然是出于难以检查的原因。
答案 0 :(得分:2)
这是iZip
的结果,它实际上更接近您的实现。看起来像你期望的那样。 zip
版本在函数内部创建整个列表,因此速度最快。循环版本只是通过列表,所以它有点慢。 izip
与代码最相似,但我猜测有一些内存管理后端进程会增加执行时间。
In [11]: %timeit pairsLoop([1,2,3,4,5])
1000000 loops, best of 3: 651 ns per loop
In [12]: %timeit pairsZip([1,2,3,4,5])
1000000 loops, best of 3: 637 ns per loop
In [13]: %timeit pairsIzip([1,2,3,4,5])
1000000 loops, best of 3: 655 ns per loop
代码版本如下所示:
from itertools import izip
def pairsIzip(xs):
for p in izip(xs[:-1], xs[1:]):
yield p
def pairsZip(xs):
for p in zip(xs[:-1], xs[1:]):
yield p
def pairsLoop(xs):
last = object()
dummy = last
for x in xs:
if last is not dummy:
yield last,x
last = x
答案 1 :(得分:2)
这个帖子中的其他地方有很多有趣的讨论。基本上,我们开始比较这个函数的两个版本,我将用以下哑名来描述:
" zip
- py"版本:
def pairs(xs):
for p in zip(xs[:-1], xs[1:]):
yield p
" loopy"版本:
def pairs(xs):
last = object()
dummy = last
for x in xs:
if last is not dummy:
yield last,x
last = x
那么为什么loopy版本变慢?基本上,我认为这取决于几件事:
loopy版本明确地做了额外的工作:它比较了两个对象'内圈的每一对生成迭代的身份(if last is not dummy: ...
)。
zippy版本在循环版本在Python代码中执行的编译C代码中做了更多的事情:
将两个对象合并为tuple
。 loopy版本为yield last,x
,而在zippy版本中,元组p
直接来自zip
,因此只有yield p
。
将变量名称绑定到object:loopy版本在每个循环中执行两次,在x
循环和for
中分配last=x
。 zippy版本只在for
循环中执行此操作一次。
有趣的是,zippy版本实际上有一种方式更多工作:它使用两个 listiterator
s,{{1 }}和iter(xs[:-1])
,传递给iter(xs[1:])
。 loopy版本仅使用一个 zip
(listiterator
)。
for x in xs
对象(listiterator
的输出)可能是一个非常高度优化的操作,因为Python程序员经常使用它。iter([])
和xs[:-1]
是一个非常轻量级的操作,与迭代整个列表相比,几乎不增加任何开销。本质上,它只是意味着移动迭代器的起点或终点,但不会改变每次迭代时发生的事情。答案 2 :(得分:1)
我怀疑第二个版本较慢的主要原因是因为它对yield
s的每一对进行了比较操作:
# pair-generating loop
for x in xs:
if last is not dummy:
yield last,x
last = x
第一个版本除了吐出值之外什么都不做。重命名循环变量后,它等同于:
# pair-generating loop
for last,x in zip(xs[:-1], xs[1:]):
yield last,x
它不是特别漂亮或Pythonic,但你可以编写一个程序版本,而不需要在内循环中进行比较。这个有多快?
def pairs(xs):
for ii in range(1,len(xs)):
yield xs[ii-1], xs[ii]