元组比Python中的列表更有效吗?

Rea*_*nly 201 python performance tuples list python-internals

在实例化和检索元素时,元组和列表之间是否存在性能差异?

dF.*_*dF. 200

通常,您可能希望元组稍快一些.但是你绝对应该测试你的特定情况(如果差异可能会影响你的程序的性能 - 记住"过早优化是所有邪恶的根源").

Python使这很容易:timeit是你的朋友.

$ python -m timeit "x=(1,2,3,4,5,6,7,8)"
10000000 loops, best of 3: 0.0388 usec per loop

$ python -m timeit "x=[1,2,3,4,5,6,7,8]"
1000000 loops, best of 3: 0.363 usec per loop
Run Code Online (Sandbox Code Playgroud)

和...

$ python -m timeit -s "x=(1,2,3,4,5,6,7,8)" "y=x[3]"
10000000 loops, best of 3: 0.0938 usec per loop

$ python -m timeit -s "x=[1,2,3,4,5,6,7,8]" "y=x[3]"
10000000 loops, best of 3: 0.0649 usec per loop
Run Code Online (Sandbox Code Playgroud)

所以在这种情况下,元组的实例化速度几乎要快一个数量级,但是对于列表,项目访问实际上要快一些!因此,如果您创建了几个元组并多次访问它们,那么使用列表实际上可能会更快.

当然,如果你想改变一个项目,列表肯定会更快,因为你需要创建一个全新的元组来改变它的一个项目(因为元组是不可变的).

  • FWIW,列表访问比Python 2中的元组访问更快,但仅仅因为Python/ceval.c中BINARY_SUBSCR中的列表有特殊情况.在Python 3中,优化已经消失,元组访问变得比列表访问快一些. (47认同)
  • 似乎奇怪的是,元组访问比列表访问慢.但是,在我的Windows 7 PC上的Python 2.7中尝试,差异只有10%,所以不重要. (3认同)
  • 您使用什么版本的python测试! (2认同)
  • 还有另一个有趣的测试 - "python -m timeit"x = tuple(xrange(999999))"`vs`python -m timeit"x = list(xrange(999999))"`.正如人们所预料的那样,实现一个元组比一个列表需要更长的时间. (2认同)
  • 第一个测试可能是错误的。您正在分配一个常量元组,它是一个常量,因此编译器将元组创建为代码常量,而不是生成代码来创建它。 (2认同)
  • @yoopoo,第一个测试创建了一百万次列表,但第二个创建一个列表并访问它一百万次.`-s"SETUP_CODE"`在实际定时代码之前运行. (2认同)
  • @AdrienLevert 我无法使用新下载的 3.8、3.9 和 3.10 重现您的结果。这些都给出与上面显示的时间类似的时间。查看源代码确认 BINARY_SUBSCR 操作码没有更改:https://github.com/python/cpython/blob/e046aabbe386fdf32bae6ffb7fae5ce479fd10c6/Python/ceval.c#L2129 (2认同)

Ray*_*ger 181

摘要

元组往往比几乎每个类别中的列表表现更好:

1)元组可以不断折叠.

2)可以重用元组而不是复制元组.

3)元组是紧凑的,不会过度分配.

4)元组直接引用它们的元素.

元组可以不断折叠

常量元组可以通过Python的窥孔优化器或AST优化器进行预先计算.另一方面,列表从头开始构建:

    >>> from dis import dis

    >>> dis(compile("(10, 'abc')", '', 'eval'))
      1           0 LOAD_CONST               2 ((10, 'abc'))
                  3 RETURN_VALUE   

    >>> dis(compile("[10, 'abc']", '', 'eval'))
      1           0 LOAD_CONST               0 (10)
                  3 LOAD_CONST               1 ('abc')
                  6 BUILD_LIST               2
                  9 RETURN_VALUE 
Run Code Online (Sandbox Code Playgroud)

元组不需要复制

运行tuple(some_tuple)立即返回.由于元组是不可变的,因此不必复制它们:

>>> a = (10, 20, 30)
>>> b = tuple(a)
>>> a is b
True
Run Code Online (Sandbox Code Playgroud)

相反,list(some_list)要求将所有数据复制到新列表:

>>> a = [10, 20, 30]
>>> b = list(a)
>>> a is b
False
Run Code Online (Sandbox Code Playgroud)

元组不会过度分配

由于元组的大小是固定的,因此它可以比需要过度分配以使append()操作有效的列表更紧凑地存储.

这为元组提供了一个很好的空间优势:

>>> import sys
>>> sys.getsizeof(tuple(iter(range(10))))
128
>>> sys.getsizeof(list(iter(range(10))))
200
Run Code Online (Sandbox Code Playgroud)

以下是Objects/listobject.c中的注释,它解释了列表正在执行的操作:

/* This over-allocates proportional to the list size, making room
 * for additional growth.  The over-allocation is mild, but is
 * enough to give linear-time amortized behavior over a long
 * sequence of appends() in the presence of a poorly-performing
 * system realloc().
 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
 * Note: new_allocated won't overflow because the largest possible value
 *       is PY_SSIZE_T_MAX * (9 / 8) + 6 which always fits in a size_t.
 */
Run Code Online (Sandbox Code Playgroud)

元组直接引用它们的元素

对象的引用直接包含在元组对象中.相比之下,列表有一个额外的间接层指向外部指针数组.

这为元组提供了索引查找和解包的小速度优势:

$ python3.6 -m timeit -s 'a = (10, 20, 30)' 'a[1]'
10000000 loops, best of 3: 0.0304 usec per loop
$ python3.6 -m timeit -s 'a = [10, 20, 30]' 'a[1]'
10000000 loops, best of 3: 0.0309 usec per loop

$ python3.6 -m timeit -s 'a = (10, 20, 30)' 'x, y, z = a'
10000000 loops, best of 3: 0.0249 usec per loop
$ python3.6 -m timeit -s 'a = [10, 20, 30]' 'x, y, z = a'
10000000 loops, best of 3: 0.0251 usec per loop
Run Code Online (Sandbox Code Playgroud)

以下是元组(10, 20)的存储方式:

    typedef struct {
        Py_ssize_t ob_refcnt;
        struct _typeobject *ob_type;
        Py_ssize_t ob_size;
        PyObject *ob_item[2];     /* store a pointer to 10 and a pointer to 20 */
    } PyTupleObject;
Run Code Online (Sandbox Code Playgroud)

以下是列表[10, 20]的存储方式:

    PyObject arr[2];              /* store a pointer to 10 and a pointer to 20 */

    typedef struct {
        Py_ssize_t ob_refcnt;
        struct _typeobject *ob_type;
        Py_ssize_t ob_size;
        PyObject **ob_item = arr; /* store a pointer to the two-pointer array */
        Py_ssize_t allocated;
    } PyListObject;
Run Code Online (Sandbox Code Playgroud)

请注意,元组对象直接包含两个数据指针,而列表对象有一个额外的间接层,用于保存两个数据指针的外部数组.

  • 最后,有人把C结构! (16认同)
  • @melvil james你对元组的理解在这里是不正确的,元组是不可变的,所以当你执行`t+=i`时,你认为发生的事情是将元素添加到同一个元素,但实际上你在每次迭代时通过添加创建一个新的元组前一个元组的元素,这就是为什么此操作很慢的原因,因为您要附加到同一列表的列表版本。 (6认同)
  • 使用约50个~100个元素列表列表时,将此结构移动到元组会使查找次数减少多个数量级,以进行多次查找.我相信这是因为一旦你开始使用元组,由于删除你演示的第二层间接,元组的缓存局部性更大. (5认同)
  • @LucianoRamalho 你的评论很容易被证明是不正确的:``t = (10, 20, [30, 40], 50); tuple(t) is s`` 返回``True``。原因是“tuple(sometuple)”只需要进行浅复制,因此允许重用*sometuple*而不检查其内容。 (3认同)
  • `在内部,元组的存储效率比列表高一点,而且元组的访问速度也可以稍微快一些。` 那么你如何解释 dF. 的答案的结果? (2认同)
  • 如果`PyTupleObject`的`PyObject *ob_item[2];`存储了两个指向两个对象的指针,即指针的数量被硬编码为`2`,那么怎么可能存在具有两个以上元素的元组呢?有什么不对劲。@ead 链接到的版本甚至只有 `PyObject *ob_item[1];`。这是如何运作的? (2认同)

Mar*_*son 159

dis模块反汇编函数的字节代码,有助于查看元组和列表之间的区别.

在这种情况下,您可以看到访问元素会生成相同的代码,但分配元组比分配列表要快得多.

>>> def a():
...     x=[1,2,3,4,5]
...     y=x[2]
...
>>> def b():
...     x=(1,2,3,4,5)
...     y=x[2]
...
>>> import dis
>>> dis.dis(a)
  2           0 LOAD_CONST               1 (1)
              3 LOAD_CONST               2 (2)
              6 LOAD_CONST               3 (3)
              9 LOAD_CONST               4 (4)
             12 LOAD_CONST               5 (5)
             15 BUILD_LIST               5
             18 STORE_FAST               0 (x)

  3          21 LOAD_FAST                0 (x)
             24 LOAD_CONST               2 (2)
             27 BINARY_SUBSCR
             28 STORE_FAST               1 (y)
             31 LOAD_CONST               0 (None)
             34 RETURN_VALUE
>>> dis.dis(b)
  2           0 LOAD_CONST               6 ((1, 2, 3, 4, 5))
              3 STORE_FAST               0 (x)

  3           6 LOAD_FAST                0 (x)
              9 LOAD_CONST               2 (2)
             12 BINARY_SUBSCR
             13 STORE_FAST               1 (y)
             16 LOAD_CONST               0 (None)
             19 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

  • 呃,只是生成相同的字节码绝对不意味着在C(因此cpu)级别发生相同的操作.尝试使用`__getitem__`创建一个类`ListLike`,它可以做一些非常慢的事情,然后反汇编`x = ListLike((1,2,3,4,5)); y = x [2]`.字节码将更像上面的元组示例而不是列表示例,但您是否真的相信这意味着性能会相似? (60认同)
  • 虽然你可以通过计算ops得出任何结论的建议被误导,但这确实显示了关键的区别:常量元组在字节码中存储,并且在使用时仅被引用,而列表需要在运行时构建. (17认同)
  • 字节码的数量,如代码行数,与执行速度(因此效率和性能)几乎没有关系. (8认同)
  • 这个答案告诉我们Python确认元组常量.很高兴知道!但是当尝试从变量值构建元组或列表时会发生什么? (4认同)
  • 看来你说有些类型比其他类型更有效.这是有道理的,但列表和元组生成的开销似乎与所涉及的数据类型正交,但需要注意的是它们是相同数据类型的列表和元组. (2认同)

tzo*_*zot 31

元组是不可变的,更有内存效率; 列表,为了效率,分配内存,以允许附加没有常量reallocs.因此,如果您想在代码中迭代一系列常量值(例如for direction in 'up', 'right', 'down', 'left':),则首选元组,因为这些元组是在编译时预先计算的.

访问速度应该相同(它们都作为连续数组存储在内存中).

但是,当您处理可变数据时,alist.append(item)它更atuple+= (item,)受欢迎.请记住,元组旨在被视为没有字段名称的记录.


Dev*_*wal 10

这是另一个小基准,只是为了它..

In [11]: %timeit list(range(100))
749 ns ± 2.41 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

In [12]: %timeit tuple(range(100))
781 ns ± 3.34 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
Run Code Online (Sandbox Code Playgroud)
In [1]: %timeit list(range(1_000))
13.5 µs ± 466 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [2]: %timeit tuple(range(1_000))
12.4 µs ± 182 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Run Code Online (Sandbox Code Playgroud)
In [7]: %timeit list(range(10_000))
182 µs ± 810 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

In [8]: %timeit tuple(range(10_000))
188 µs ± 2.38 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
Run Code Online (Sandbox Code Playgroud)
In [3]: %timeit list(range(1_00_000))
2.76 ms ± 30.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [4]: %timeit tuple(range(1_00_000))
2.74 ms ± 31.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Run Code Online (Sandbox Code Playgroud)
In [10]: %timeit list(range(10_00_000))
28.1 ms ± 266 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [9]: %timeit tuple(range(10_00_000))
28.5 ms ± 447 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Run Code Online (Sandbox Code Playgroud)

让我们平均这些:

In [3]: l = np.array([749 * 10 ** -9, 13.5 * 10 ** -6, 182 * 10 ** -6, 2.76 * 10 ** -3, 28.1 * 10 ** -3])

In [2]: t = np.array([781 * 10 ** -9, 12.4 * 10 ** -6, 188 * 10 ** -6, 2.74 * 10 ** -3, 28.5 * 10 ** -3])

In [11]: np.average(l)
Out[11]: 0.0062112498000000006

In [12]: np.average(t)
Out[12]: 0.0062882362

In [17]: np.average(t) / np.average(l)  * 100
Out[17]: 101.23946713590554
Run Code Online (Sandbox Code Playgroud)

你可以称之为几乎没有定论。

但是可以肯定的是,与列表相比,元组花费101.239%了时间或1.239%额外的时间来完成这项工作。


小智 9

array如果列表或元组中的所有项目都是相同的C类型,您还应该考虑标准库中的模块.它将占用更少的内存,并且可以更快.

  • 这将占用更少的内存,但访问时间可能会慢一点,而不是更快.访问元素需要将打包值取消装箱到实际整数,这将减慢进程. (15认同)

Yil*_*maz 8

元组性能更好,但如果元组的所有元素都是不可变的。如果元组的任何元素是可变的列表或函数,则编译时间将更长。这里我编译了 3 个不同的对象:

在此输入图像描述

在第一个示例中,我编译了一个元组。它作为常量加载到元组中,加载并返回值。只需要一步即可完成编译。这称为常量折叠。当我编译具有相同元素的列表时,它必须首先加载每个单独的常量,然后构建列表并返回它。在第三个示例中,我使用了一个包含列表的元组。我为每次手术计时。

在此输入图像描述

--内存分配

当创建可变容器对象(例如列表、集合、字典等)时,在其生命周期内,这些容器的分配容量(它们可以包含的项目数)大于容器中的元素数。这样做是为了提高向集合添加元素的效率,称为过度分配。因此,每次我们追加一个元素时,列表的大小都不会增加——只是偶尔会增加。调整列表的大小非常昂贵,因此每次添加项目时不要调整大小会有所帮助,但您不想过度分配太多,因为这会产生内存成本。

另一方面,不可变容器,由于它们的项目计数在创建后就是固定的,因此不需要这种过度分配- 因此它们的存储效率更高。随着元组变大,它们的大小也会增加。

-复制

制作不可变序列的浅表副本是没有意义的,因为无论如何你都无法改变它。因此,复制元组只会返回其本身以及内存地址。这就是为什么复制元组更快

检索元素

我 timeD 从元组和列表中检索元素:

在此输入图像描述

从元组中检索元素比从列表中检索元素要快得多。因为在 CPython 中,元组可以直接访问(指针)其元素,而列表需要首先访问另一个包含指向列表元素的指针的数组。