在Python中从元组列表获取每个元组的第n个元素的最佳方法

12

我有一些代码包含了 zip(*G)[0](在其他地方,还有一个不同的G,zip(*G)[1])。G 是一个元组列表。它返回G中每个元组的第一个元素(或一般地,对于zip(*G)[n],是第n-1个)作为元组的列表。例如:

>>> G = [(1, 2, 3), ('a', 'b', 'c'), ('you', 'and', 'me')]
>>> zip(*G)[0]
(1, 'a', 'you')
>>> zip(*G)[1]
(2, 'b', 'and')
这很聪明,但问题是它在 Python 3 中不起作用,因为 `zip` 在那里是一个迭代器。此外,2to3 不够聪明,无法修复它。因此,明显的解决方案是使用 `list(zip(*G))[0]`,但这使我想到:可能有更有效的方法来做到这一点。没有必要创建 `zip` 创建的所有元组。我只需要 G 中每个元组的第 `n` 个元素。
是否有更有效但同样紧凑的方法来做到这一点?我可以接受标准库中的任何内容。在我的用例中,G 中的每个元组至少为长度 n,因此不必担心 zip 停止在最小长度元组的情况(即 zip(*G)[n] 总是被定义的)。
如果没有,我想我只会坚持将 zip 包装在 list() 中。
P.S.,我知道这是不必要的优化。我只是好奇。
更新:
如果有人在意的话,我选择了 zip(*G) 选项。首先,这让我为数据提供有意义的名称。我的 G 实际上由长度为 2 的元组组成(表示分子和分母)。列表推导式只会比 zip 稍微更可读,但这种方法更好(并且由于大多数情况下 zip 是我在列表推导式中迭代的列表,所以这使得事情更平坦)。
其次,正如 @thewolf 和 @Sven Marnach 的答案所指出的那样,对于较小的列表,这种方法更快。我的 G 在大多数情况下实际上不是很大(如果它很大,那么这肯定不会是代码的瓶颈!)。
但是有比我预期的更多方法可以做到这一点,包括 Python 3 中的新功能 a, *b, c = G,我甚至不知道该怎么用。
3个回答

18
你可以使用列表推导式。
[x[0] for x in G]

或者使用operator.itemgetter()

from operator import itemgetter
map(itemgetter(0), G)

或者序列解包

[x for x, y, z in G]

编辑:这是我的看法,关于计时不同选项的方式,同样适用于Python 3.2:

from operator import itemgetter
import timeit

G = list(zip(*[iter(range(30000))] * 3))

def f1():
    return [x[0] for x in G]
def f2():
    return list(map(itemgetter(0), G))
def f3():
    return [x for x, y, z in G]
def f4():
    return list(zip(*G))[0]
def f5():
    c0, *rest = zip(*G)
    return c0
def f6():
    c0, c1, c2 = zip(*G)
    return c0
def f7():
    return next(zip(*G))

for f in f1, f2, f3, f4, f5, f6, f7:
    print(f.__name__, timeit.timeit(f, number=1000))

我的电脑上的结果:

f1 0.6753780841827393
f2 0.8274149894714355
f3 0.5576457977294922
f4 0.7980241775512695
f5 0.7952430248260498
f6 0.7965989112854004
f7 0.5748469829559326

注释:

  1. 我使用了一个包含10000个三元组的列表来测量实际处理时间,并使函数调用开销、名称查找等变得可以忽略,否则这些因素将严重影响结果。

  2. 这些函数返回列表或元组 - 任何对于特定解决方案更方便的形式。

  3. the wolf's answer相比,我从f4()中删除了对tuple()的冗余调用(表达式的结果已经是元组了),并添加了一个仅用于提取第一列的函数f7()

正如预期的那样,列表推导式最快,与稍微不太通用的f7()一起。

另外编辑:这里是十列而不是三列的结果,代码在适当的地方进行了调整:

f1 0.7429649829864502
f2 0.881648063659668
f3 1.234360933303833
f4 1.92038893699646
f5 1.9218590259552002
f6 1.9172680377960205
f7 0.6230220794677734

我支持第一种方法是最好的方式。没有必要创建虚拟变量,或使用一些(对我来说奇怪和过于复杂的)运算符和映射。 - asmeurer
@martineau:通常我会选择第一个。在某些情况下,例如在迭代异构元组列表时,我更喜欢最后一个,因为我可以通过使用匹配的循环变量名称来记录元组的字段名称。 - Sven Marnach
有趣的是,你的计时结果与@the wolf的结果差别如此之大。我可以看出你是如何测量的,看起来很可靠。不知道为什么他的结果会如此不同,特别是对于f6()这个函数,你有什么想法吗? - martineau
1
@martineau:主要原因是输入数据集太小了。在wolf的答案中,名称查找等开销非常大。通过导入虚假默认参数等优化,您将获得完全不同的结果。所有这些事情在大数据集上并不重要,因为它们完全被实际处理时间所支配。 - Sven Marnach

14

至少在Python 2.7中,最快的方法是

t0,t1,t2=zip(*G) for SMALLER lists and [x[0] for x in G] in general

以下是测试内容:

from operator import itemgetter

G = [(1, 2, 3), ('a', 'b', 'c'), ('you', 'and', 'me')]

def f1():
   return tuple(x[0] for x in G)

def f2():
   return tuple(map(itemgetter(0), G))

def f3():
    return tuple(x for x, y, z in G)     

def f4():
    return tuple(list(zip(*G))[0])

def f5():
    t0,*the_rest=zip(*G)
    return t0

def f6():
    t0,t1,t2=zip(*G)
    return t0                

cmpthese.cmpthese([f1,f2,f3,f4,f5,f6],c=100000) 

结果:

    rate/sec     f4     f5     f1     f2     f3     f6
f4   494,220     -- -21.9% -24.1% -24.3% -26.6% -67.6%
f5   632,623  28.0%     --  -2.9%  -3.0%  -6.0% -58.6%
f1   651,190  31.8%   2.9%     --  -0.2%  -3.2% -57.3%
f2   652,457  32.0%   3.1%   0.2%     --  -3.0% -57.3%
f3   672,907  36.2%   6.4%   3.3%   3.1%     -- -55.9%
f6 1,526,645 208.9% 141.3% 134.4% 134.0% 126.9%     --

如果您不介意结果是列表,则列表推导式更快。

这里有一个更详细的基准测试,包括可变列表大小:

from operator import itemgetter
import time
import timeit 
import matplotlib.pyplot as plt

def f1():
   return [x[0] for x in G]

def f1t():
   return tuple([x[0] for x in G])

def f2():
   return tuple([x for x in map(itemgetter(0), G)])

def f3():
    return tuple([x for x, y, z in G])    

def f4():
    return tuple(list(zip(*G))[0])

def f6():
    t0,t1,t2=zip(*G)
    return t0     

n=100    
r=(5,35)
results={f1:[],f1t:[],f2:[],f3:[],f4:[],f6:[]}    
for c in range(*r):
    G=[range(3) for i in range(c)] 
    for f in results.keys():
        t=timeit.timeit(f,number=n)
        results[f].append(float(n)/t)

for f,res in sorted(results.items(),key=itemgetter(1),reverse=True):
    if f.__name__ in ['f6','f1','f1t']:
        plt.plot(res, label=f.__name__,linewidth=2.5)
    else:    
        plt.plot(res, label=f.__name__,linewidth=.5)

plt.ylabel('rate/sec')
plt.xlabel('data size => {}'.format(r))  
plt.legend(loc='upper right')
plt.show()

对于较小的数据量(5到35),将会生成以下绘图:

smaller

对于较大的范围(25到250),则会生成以下输出:

larger

可以看出,列表推导式 f1 是最快的。而在返回元组方面,f6f1t 则交替成为最快的。


3
cmpthese 是一种基于 Python 的 timeit 模块的 Perl 风格性能比较工具。它运行了所有子程序并比较它们的速度,以表格形式打印出来,慢的在上面,快的在下面。每秒执行次数(rate/sec)越快,子程序的速度越快。右侧的表格显示了相对速度:f6 比 f4 快 208.9%;f3 比 f2 快 3.1%,但比 f6 慢 55.9% 等等。您从左侧阅读,可以看出每个子程序与其他子程序相比速度快慢的百分比。 - the wolf
我现在将其标记为答案。我对f6是最快的感到非常惊讶。也许你应该用一个更大的G来测试一下。不管怎样,这也会使我的列表理解更容易阅读,在那里我倾向于有zip(*G),因为它会将其中的一部分拆分成单独的行。 - asmeurer
我也刚注意到至少有一个地方我在使用 zip(*G)[0]zip(*G)[1] 来表示同一个 G。因此,这种方式显然是最好的。 - asmeurer
@SvenMarnach:是的,你关于第4点是正确的。实际上,即使使用更大的3元组字典(无论它是返回列表还是元组),f6的速度也要快得多。试试看吧。 - the wolf
@the wolf:我在自己的微基准测试中解决了我提出的问题,事实上,我的结果与你的相当不同-请参阅我的答案。(我还删除了f4()中我昨天错过的tuple()的多余调用。) - Sven Marnach
显示剩余6条评论

6

一种非常聪明的 Python 3-only 方法是使用星号赋值或 扩展迭代拆包:

>>> G = [(1, 2, 3), ('a', 'b', 'c'), ('you', 'and', 'me')]
>>> items_I_want,*the_rest=zip(*G)
>>> items_I_want
(1, 'a', 'you')
>>> the_rest
[(2, 'b', 'and'), (3, 'c', 'me')]

既然您要为Python 2和Python 3编写代码,可以使用显式解包的方式:

>>> z1,z2,z3=zip(*G)
>>> z1
(1, 'a', 'you')
>>> z2
(2, 'b', 'and')
>>> z3
(3, 'c', 'me')

为什么要踩这个问题?在Py3中,加星的赋值语句是独特的,也是最接近原始问题代码的。 - user688635
太酷了!它甚至可以在列表推导中工作:[j for j, *_ in [(1, 2, 3), ('a', 'b', 'c')]] -> [1, 'a']。不幸的是,正如我所指出的,我正在编写Python 2代码并将其转换为Python 3。 - asmeurer
@asmeurer: 那么使用z1,z2,z3 = zip(* G)的形式,在Python 2和Python 3中都可以工作,并且是最快的方法。 - user688635

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