单列表中的成对元素

124

很多时候,我需要按对处理一个列表。我在想什么是Pythonic和高效的方法来实现它,并在谷歌上找到了这个:

pairs = zip(t[::2], t[1::2])

我认为这已经足够符合Pythonic的标准了,但最近一次关于成语和效率的讨论(idioms versus efficiency)后,我决定进行一些测试:

import time
from itertools import islice, izip

def pairs_1(t):
    return zip(t[::2], t[1::2]) 

def pairs_2(t):
    return izip(t[::2], t[1::2]) 

def pairs_3(t):
    return izip(islice(t,None,None,2), islice(t,1,None,2))

A = range(10000)
B = xrange(len(A))

def pairs_4(t):
    # ignore value of t!
    t = B
    return izip(islice(t,None,None,2), islice(t,1,None,2))

for f in pairs_1, pairs_2, pairs_3, pairs_4:
    # time the pairing
    s = time.time()
    for i in range(1000):
        p = f(A)
    t1 = time.time() - s

    # time using the pairs
    s = time.time()
    for i in range(1000):
        p = f(A)
        for a, b in p:
            pass
    t2 = time.time() - s
    print t1, t2, t2-t1

以下是我的电脑上的结果:

1.48668909073 2.63187503815 1.14518594742
0.105381965637 1.35109519958 1.24571323395
0.00257992744446 1.46182489395 1.45924496651
0.00251388549805 1.70076990128 1.69825601578

如果我正确理解的话,这应该意味着Python中列表、列表索引和列表切片的实现非常高效。这是一个令人欣慰且出乎意料的结果。
有没有另一种“更好”的方法来成对遍历列表?
请注意,如果列表元素数量为奇数,则最后一个元素将不在任何一对中。
哪种方法才是确保包含所有元素的正确方法?
我从答案中添加了这两个建议。
def pairwise(t):
    it = iter(t)
    return izip(it, it)

def chunkwise(t, size=2):
    it = iter(t)
    return izip(*[it]*size)

这是结果:
0.00159502029419 1.25745987892 1.25586485863
0.00222492218018 1.23795199394 1.23572707176

迄今为止的结果

最符合Python风格且非常高效:

pairs = izip(t[::2], t[1::2])

最高效、最符合Python风格的写法:

pairs = izip(*[iter(t)]*2)

我花了一点时间才明白第一个答案使用了两个迭代器而第二个答案使用了一个。

为了应对奇数个元素的序列,建议增加一个元素(None)来增补原始序列,该元素与前一个最后一个元素配对。这可以通过使用itertools.izip_longest()来实现。

最后

请注意,在Python 3.x中,zip()行为类似于itertools.izip(),而itertools.izip()已经不存在了。


@Apalala:我的意思是,有奇数个元素的结果取决于使用方式。例如:您可以省略最后一个元素,或添加特定已知的虚拟元素,或复制最后一个元素。 - Andrew Jaffe
2
@Apalala:因为你使用了一些玄学东西,而不是timeit模块。 - SilentGhost
1
n-duplicated:仅在快速搜索中发现:https://dev59.com/62855IYBdhLWcg3wKw5Y,http://stackoverflow.com/questions/4170295/,https://dev59.com/VHRC5IYBdhLWcg3wCMc6。 - tokland
当t具有奇数个元素时,zip(t[::2], t[1::2])会留下最后一个。 - Pyderman
@Pyderman 这篇文章确实提到了 itertools.izip_longest() - Apalala
显示剩余7条评论
10个回答

65

我的最爱方法:

def pairwise(t):
    it = iter(t)
    return zip(it,it)

# for "pairs" of any length
def chunkwise(t, size=2):
    it = iter(t)
    return zip(*[it]*size)

当您想配对所有元素时,显然可能需要一个填充值:

from itertools import izip_longest
def blockwise(t, size=2, fillvalue=None):
    it = iter(t)
    return izip_longest(*[it]*size, fillvalue=fillvalue)

在Python 3中,itertools.izip现在被简化为zip。如果要使用旧版本的Python,请使用

from itertools import izip as zip

第一个(成对的)函数似乎缺少第二个迭代器的克隆和推进。请参阅itertools配方部分。 - Apalala
当然,你是正确的,而且pairwise到目前为止是最有效的,我不知道为什么。 - Apalala
1
我喜欢这个解决方案:它很懒,而且它充分利用了迭代器的状态性。你甚至可以把它变成一行代码,尽管可能会牺牲可读性:izip(*[iter(t)]*size) - Channing Moore
针对你的第二个解决方案,如果追求性能,你不想创建一个列表吗? - max
@max 使用 izip(*(it,)*size) 也可以,但意图似乎不太清晰,并且性能改进应该不会很大。 - Apalala
显示剩余2条评论

54

我认为你最初的解决方案 pairs = zip(t[::2], t[1::2]) 是最好的,因为它易于阅读(并且在Python 3中,zip自动返回一个迭代器而不是列表)。

为确保包含所有元素,你可以通过 None 来扩展列表。

然后,如果列表中有奇数个元素,最后一对将是 (item, None)

>>> t = [1,2,3,4,5]
>>> t.append(None)
>>> zip(t[::2], t[1::2])
[(1, 2), (3, 4), (5, None)]
>>> t = [1,2,3,4,5,6]
>>> t.append(None)
>>> zip(t[::2], t[1::2])
[(1, 2), (3, 4), (5, 6)]

你如何访问zip文件? - BanAckerman

6

首先声明一下 - 不要使用下面的代码。这个代码不够Pythonic,我只是为了好玩而写的。它类似于@THC4k的pairwise函数,但它使用了iterlambda闭包。它没有使用itertools模块,也不支持fillvalue。我把它放在这里是因为有人可能会觉得它有趣:

pairwise = lambda t: iter((lambda f: lambda: (f(), f()))(iter(t).next), None)

4
就大多数Python来说,我认为Python源代码文档中提供的配方(其中一些看起来很像@JochenRitzel提供的答案)可能是最好的选择。请参考:Python源代码文档
def grouper(iterable, n, fillvalue=None):
    "Collect data into fixed-length chunks or blocks"
    # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx
    args = [iter(iterable)] * n
    return izip_longest(fillvalue=fillvalue, *args)

在现代Python中,您只需使用zip_longest(*args, fillvalue=fillvalue),根据相应的文档页进行操作

4
>>> my_list = [1,2,3,4,5,6,7,8,9,10]
>>> my_pairs = list()
>>> while(my_list):
...     a = my_list.pop(0); b = my_list.pop(0)
...     my_pairs.append((a,b))
... 
>>> print(my_pairs)
[(1, 2), (3, 4), (5, 6), (7, 8), (9, 10)]

索引错误:从空列表中弹出 - HQuser
@HQuser 如果列表中有奇数项,您将会得到该错误。您必须确定您有成对的项或检查此错误条件。 - WaterMolecule

3

有没有另一种“更好”的方法以成对的方式遍历列表?

我不能确定,但我怀疑:任何其他遍历都会包括更多需要解释的 Python 代码。内置函数如 zip() 是用 C 编写的,因此更快。

哪种方法是确保所有元素都被包含的正确方式?

检查列表的长度,如果是奇数 (len(list) & 1 == 1),则复制列表并添加一个项目。


3

只需要这样做:

>>> l = [1, 2, 3, 4, 5, 6]
>>> [(x,y) for x,y in zip(l[:-1], l[1:])]
[(1, 2), (2, 3), (3, 4), (4, 5), (5, 6)]

1
你的代码等同于更简单的 list(zip(l, l[1:])),并且它不会将列表拆分成一对对。 - Apalala
1
最佳答案来自@Apalala。 - erickfis

1
这是一个使用生成器创建对/腿的示例。生成器不受堆栈限制的限制。
def pairwise(data):
    zip(data[::2], data[1::2])

例子:

print(list(pairwise(range(10))))

输出:

[(0, 1), (2, 3), (4, 5), (6, 7), (8, 9)]

执行时间的比较? - Alan
该列表未被分成一对,因为原始列表中的大多数数字出现在两个元组中。预期输出为[(0, 1), (2, 3), (4, 5).... - Apalala
@Apalala 谢谢您指出。我已经修复了代码,以提供正确的输出。 - Vlad Bezden
zip()在Python 3.x中已经返回生成器了,@VladBezden - Apalala
如果列表长度不是偶数,则最后一个元素将被删除。 - Daniil Okhlopkov

0

这个片段对我很有用。它创建元组对并在列表长度为奇数时向最后一对添加空字符串 (fillvalue="")。

zip_longest(*[iter(my_list)] * 2, fillvalue="")

# odd
list(zip_longest(*[iter([0, 1, 2, 3, 4, 5, 6])] * 2, fillvalue=""))
[(0, 1), (2, 3), (4, 5), (6, '')]

# even
list(zip_longest(*[iter([0, 1, 2, 3, 4, 5])] * 2, fillvalue=""))
[(0, 1), (2, 3), (4, 5)]

0

以防万一有人需要算法答案,这里是:

>>> def getPairs(list):
...     out = []
...     for i in range(len(list)-1):
...         a = list.pop(0)
...         for j in a:
...             out.append([a, j])
...     return b
>>> 
>>> k = [1, 2, 3, 4]
>>> l = getPairs(k)
>>> l
[[1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]]

但请注意,由于您在其上使用了 pop,因此您的原始列表也将被减少为其最后一个元素。
>>> k
[4]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接