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

从Python中的元组列表中获取每个元组的第n个元素的最佳方法,python,Python,我有一些代码包含zip(*G)[0](在其他地方,zip(*G)[1],带有不同的G)G是元组列表。这样做的目的是返回G中每个元组的第一个(或者通常,对于zip(*G)[n],是n-1th)元素作为元组的列表。比如说, >>> G = [(1, 2, 3), ('a', 'b', 'c'), ('you', 'and', 'me')] >>> zip(*G)[0] (1, 'a', 'you') >>> zip(*G)[1] (2, 'b',

我有一些代码包含
zip(*G)[0]
(在其他地方,
zip(*G)[1]
,带有不同的G)
G
是元组列表。这样做的目的是返回G中每个元组的第一个(或者通常,对于
zip(*G)[n]
,是
n-1
th)元素作为元组的列表。比如说,

>>> G = [(1, 2, 3), ('a', 'b', 'c'), ('you', 'and', 'me')]
>>> zip(*G)[0]
(1, 'a', 'you')
>>> zip(*G)[1]
(2, 'b', 'and')
这非常聪明,但问题是它在Python3中不起作用,因为
zip
是一个迭代器。此外,2to3不够聪明,无法修复它。因此,显而易见的解决方案是使用
list(zip(*G))[0]
,但这让我想到:可能有一种更有效的方法可以做到这一点。不需要创建zip创建的所有元组。我只需要G中每个元组的
n
th元素

是否有更高效、同样紧凑的方法来实现这一点?我对标准图书馆的任何东西都没意见。在我的用例中,G中的每个元组将至少是长度
n
,因此无需担心zip在最小长度元组处停止的情况(即,
zip(*G)[n]
将始终定义)

如果没有,我想我会坚持在
list()
中包装
zip

(另外,我知道这是不必要的优化。我只是好奇而已)

更新:

如果有人关心,我选择了
t0,t1,t2=zip(*G)
选项。首先,这使我能够为数据指定有意义的名称。我的
G
实际上由长度为2的元组(表示分子和分母)组成。列表理解的可读性只比zip稍微好一点,但这种方式要好得多(因为在大多数情况下,zip是我在列表理解中反复使用的列表,这使事情变得更加平淡)

第二,正如@thewolf和@Sven Marnach的优秀答案所指出的,这种方法对于较小的列表更快。在大多数情况下,我的G实际上并不大(如果它很大,那么这肯定不会成为代码的瓶颈!)


但是有比我预想的更多的方法可以做到这一点,包括Python 3的新的
a、*b、c=G
特性,我甚至都不知道

您可以使用列表

[x[0] for x in G]
operator.itemgetter()

或顺序拆包

[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
评论:

  • 我使用了一个包含10000个三元组的列表来度量实际的处理时间,并使函数调用开销、名称查找等可以忽略不计,否则会严重影响结果

  • 这些函数返回一个列表或元组–对于特定的解决方案来说更方便的任何内容

  • 与之相比,我从
    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
    

    一种非常聪明的仅Python 3方法是使用星号赋值或:

    因为您正在为这两种语言编写代码,所以可以使用显式解包(在Python2和Python3上工作):

    至少Python2.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)生成此图:

    此输出适用于更大范围(25至250):


    您可以看到
    f1
    ,列表理解速度最快
    f6
    f1t
    以最快的速度返回元组

    为什么要投否决票?星号赋值在Py3中是唯一的,并且最接近OP的代码?这很酷!它甚至在列表理解中起作用:
    [j代表j,*.[in[(1,2,3),('a','b','c')]
    ->
    [1',a']
    。不幸的是,正如我所指出的,我正在用Python 2编写代码并将其转换为Python 3。@asmurer:然后使用
    z1,z2,z3=zip(*G)
    的形式,它在Python 2和Python 3中都有效,并且速度最快。我投票赞成第一种方式,认为这是最好的方式。不需要创建虚拟变量,也不需要使用一些(对我来说是奇怪而过于复杂的)运算符和映射。@martineau:我通常会选择第一个。在某些情况下,我更喜欢最后一个,例如,在遍历异构元组列表时,因为我可以通过使用匹配的循环变量名来记录元组的字段名。有趣的是,您的计时结果与@thewolf的有如此大的不同。我能看到你是如何测量的,它看起来很好。你知道他为什么如此不同吗,尤其是对于
    f6()
    ?@martineau:主要原因是输入数据集太小。在wolf的回答中,姓名查找等开销非常大。通过优化,例如通过伪造的默认参数将全局名称导入本地名称空间等,您会得到完全不同的结果。所有这些对于大数据集来说都无关紧要,因为它们完全由实际处理时间决定。我不熟悉CMP。我应该如何解释结果?@asmeurer:
    cmp这些
    打印Perl风格的性能比较。它基于Python的
    timeit
    模块。它只是运行所有子程序并比较速度。表格从顶部打印最慢到底部打印最快。速率/秒越快,子例程的速度越快。右边的表格显示了速度有多快:f6比f4快208.9%;f3比f2快3.1%,但比f2慢55.9%
    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()