元组构造器 vs 列表推导式

5

假设我有一个列表

a = (3, 2, 9, 4)

如果我想给每个数字加上1并存储结果(之后不需要对结果进行操作),我的第一个想法是这样的:

[x + 1 for x in a]

但是这里还有一个问题:
tuple(x + 1 for x in a)

元组速度更快,对吗?如果我在之后不需要更改结果,那么这段代码是否更加高效呢?实际上,它是如何工作的?tuple 构造函数是否必须将生成器表达式转换为列表以预先知道大小?感谢任何解释。


最好的方法是实际计时。结果可能会因许多因素而异,包括您使用的Python实现方式。 - Vaughn Cato
3个回答

7

只需使用timeit()

In : a = (3, 2, 9, 4)

In : f1 = lambda: [x + 1 for x in a]

In : f2 = lambda: tuple(x + 1 for x in a)

In : timeit.timeit(f1)
Out: 0.595026969909668

In : timeit.timeit(f2)
Out: 2.360887050628662

看起来元组构造函数的变体需要的时间大约要多四倍,我猜是因为列表解析(在CPython中)已经被相当优化了。

但让我们仔细看一下:

In : f3 = lambda: list(x + 1 for x in a)

In : timeit.timeit(f3)
Out: 2.5421998500823975

因此,这需要与元组构造大约相同的时间,这表明性能惩罚在于生成器表达式开销。(我们可以排除列表/元组构造,请参见下面的编辑)

甚至比使用map()函数慢两倍:

In : inc = partial(operator.add,1)

In : f4 = lambda:map(inc, a)

In : timeit.timeit(f4)
Out: 1.2346529960632324

我认为这实际上取决于(CPython)实现细节,因此不要依赖于此。无论如何,不要担心性能,这只是2-4的因素,请使用最好阅读的方法。
如果您真的遇到性能瓶颈,请在注意到它们后进行调查和优化。而且我敢打赌,在列表操作中四倍因素将是您的最小问题。
编辑:有人提到“元组”的查找成本可能会导致减速,但事实并非如此:
In : f5 = lambda: tuple([x + 1 for x in a])

In : timeit.timeit(f5)
Out: 0.7900090217590332

所以我猜测是生成器表达式的开销减缓了事情的进展。

我愿意打赌,时间差异不在生成器表达式中。我认为更可能是在全局命名空间查找名称'list'和'tuple'时发生的。[]是内置语法,不需要查找。 - krousey

3

dis模块可以让你大概了解代码在内部是如何执行的...

dis.dis(lambda a: [x + 1 for x in a]) 将产生以下结果...

  1           0 BUILD_LIST               0
              3 LOAD_FAST                0 (a)
              6 GET_ITER
        >>    7 FOR_ITER                16 (to 26)
             10 STORE_FAST               1 (x)
             13 LOAD_FAST                1 (x)
             16 LOAD_CONST               1 (1)
             19 BINARY_ADD
             20 LIST_APPEND              2
             23 JUMP_ABSOLUTE            7
        >>   26 RETURN_VALUE

...而dis.dis(lambda a: tuple(x + 1 for x in a))则会产生...

  1           0 LOAD_GLOBAL              0 (tuple)
              3 LOAD_CONST               1 (<code object <genexpr> at 0x7f62e9eda930, file "<stdin>", line 1>)
              6 MAKE_FUNCTION            0
              9 LOAD_FAST                0 (a)
             12 GET_ITER
             13 CALL_FUNCTION            1
             16 CALL_FUNCTION            1
             19 RETURN_VALUE

...但你可能无法从中推断出太多信息。如果您想知道哪个更快,请查看timeit模块。


1
在大多数情况下,元组和列表之间的效率并不重要。如果您真的关心它,可以使用timeit进行测试。 元组和列表最重要的区别是元组是不可变的,而列表是可变的。这意味着您可以更改列表的值,但无法更改元组的值。您可以对元组进行哈希,但不能对列表进行哈希。例如:
k_tuple = ('a', 'b')
k_list = ['a', 'b']
d = {}
d[k_tuple] = 'c' # It is ok
d[k_list] = 'c' #It raise exception.

此外,当列表作为函数的参数时,它是按引用分配的。当元组作为函数的参数时,它是按值分配的。

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