Python中,元组是否比列表更高效?

319

当涉及元素的实例化和检索时,元组和列表之间是否有性能差异?


11
相关:"为什么元组比列表更快?" - Cristian Ciupitu
4
如果您对不同类型之间使用的内存感兴趣,请查看我制作的这张图表:https://dev59.com/E2Ag5IYBdhLWcg3w7uyk#30008338 - tmthydvnprt
9个回答

363

概述

在几乎所有方面,元组比列表表现更好。

  1. 元组可以进行常量折叠

  2. 元组可以被重复使用而不是被复制。

  3. 元组紧凑且不会过度分配内存。

  4. 元组直接引用其元素。

元组可以进行常量折叠

Python的Peephole优化器或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 

元组不需要被复制

运行tuple(some_tuple)会立即返回它本身。由于元组是不可变的,所以不需要复制:

>>> a = (10, 20, 30)
>>> b = tuple(a)
>>> a is b
True

相比之下,list(some_list) 需要将所有数据复制到一个新的列表中:

>>> a = [10, 20, 30]
>>> b = list(a)
>>> a is b
False

元组不会过度分配

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

这使得元组具有很好的空间优势:

>>> import sys
>>> sys.getsizeof(tuple(iter(range(10))))
128
>>> sys.getsizeof(list(iter(range(10))))
200

下面是位于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.
 */

元组直接引用它们的元素

元组对象中直接包含了对象的引用。相比之下,列表具有额外的间接层来指向外部指针数组。

这使得元组在索引查找和解包方面具有较小的速度优势:

$ 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

这里是元组(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;

这里是列表[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;

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

3
那么,您如何解释dF回答中的结果呢?Internally, tuples are stored a little more efficiently than lists, and also tuples can be accessed slightly faster.(原文)元组在内部存储上比列表要高效一些,同时也可以稍微更快地访问元组。 (翻译) 我无法提供dF回答的上下文或内容,因此无法对其结果进行解释。 - DRz
9
当处理大约50,000个列表,每个列表有大约100个元素时,将这种结构转换为元组可以显著减少多次查找的查询时间。我认为这是由于使用元组后更好的缓存本地化,因为你消除了演示中第二层间接性。 - horta
8
@LucianoRamalho,你的评论很容易被证明是错误的:“t = (10, 20, [30, 40], 50); tuple(t) is s” 返回“True”。原因是 “tuple(sometuple)” 只需要创建一个浅层副本,因此允许在不检查其内容的情况下重用 sometuple - Raymond Hettinger
6
@melvil james 您在这里对元组的理解是错误的,元组是不可变的,因此当您执行t+=i时,您认为发生的是将元素添加到同一元素中,但实际上,您正在通过添加先前元组的元素来创建一个新元组,这就是为什么此操作很慢,而使用列表版本时,您是在向同一列表中添加元素。 - Rohit
2
如果 PyTupleObjectPyObject *ob_item[2]; 存储了两个对象的指针,即指针数量被硬编码为 2,那么如何有包含超过两个元素的元组呢?这里似乎有些问题。甚至由 @ead 链接的版本只有 PyObject *ob_item[1];。那这是怎么工作的呢? - Kelly Bundy
显示剩余3条评论

230

通常情况下,元组可能会稍微快一些。但是您应该确实测试您的特定情况(如果差异可能会影响程序的性能-请记住“过早优化是万恶之源”)。

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

而且...

$ 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

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

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


3
你的测试使用的是哪个版本的Python? - Matt Joiner
4
元组访问速度比列表访问速度慢似乎有些奇怪。然而,在我使用Python2.7在Windows 7上进行测试后,它们之间的差距仅为10%,因此可以忽略不计。 - ToolmakerSteve
82
在Python 2中,访问列表比访问元组更快,但这仅因为Python/ceval.c中的BINARY_SUBSCR对列表有一个特殊情况。在Python 3中,该优化已经被取消,元组访问变得比列表访问略快。 - Raymond Hettinger
4
第一次测试可能是错误的。您正在分配一个由常量组成的元组,这是一个常量,因此编译器将该元组创建为代码常量,而不是生成用于创建它的代码。 - leewz
3
@yoopoo,第一个测试会创建一个列表一百万次,而第二个测试只会创建一个列表并访问它一百万次。"-s"SETUP_CODE""将在实际计时代码之前运行。 - leewz
显示剩余9条评论

208

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

78
仅仅因为生成了相同的字节码,绝对不意味着在C(以及CPU)级别发生了相同的操作。尝试创建一个名为ListLike 的类,并使用一个非常慢的 __getitem__ 方法,然后反汇编 x = ListLike((1, 2, 3, 4, 5)); y = x[2]。生成的字节码会更像上面的元组示例而不是列表示例,但你真的认为这意味着性能会相似吗? - mzz
3
看起来你是在说有些类型比其他类型更有效率。这很有道理,但列表和元组生成的开销似乎与所涉及的数据类型无关,除非它们是相同数据类型的列表和元组。 - Mark Harrison
12
字节码数量和代码行数一样,与执行速度(因此也包括效率和性能)关系不大。 - martineau
26
虽然从计算操作次数中得出任何结论的建议是错误的,但这确实显示了一个关键区别:常量元组在字节码中被存储为此类,并在使用时仅作引用,而列表需要在运行时构建。 - poolie
10
这个答案告诉我们Python支持元组常量,这很好!但是当尝试用变量值构建元组或列表时会发生什么? - Tom
1
在Python >= 3.9中,listtuple的整个项目将存储在一个LOAD_CONST操作符中。 - Reza K Ghazi

44

元组是不可变的,因此更加节省内存;列表为了速度效率会过度分配内存来允许在没有常量 realloc 的情况下进行附加操作。因此,如果您想在代码中迭代常量序列的值(例如for direction in 'up','right','down','left'),则应优先选择元组,因为这些元组在编译时被预计算。

读取访问速度应该相同(它们都被存储为连续的数组在内存中)。

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


1
Python中的编译时间是什么? - balki
1
@balki:Python源代码编译成字节码的时间(该字节码可能保存为.py[co]文件)。 - tzot
如果可能的话,请提供引用。 - Grijesh Chauhan

15

这里有另一个小基准测试,只是为了好玩而已。

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)

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)

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)

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)

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)

让我们求这些的平均值:

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

你可以说这几乎是没有结论的。

但是,当然,与列表相比,元组需要花费101.239%的时间,或者额外花费1.239%的时间来完成工作。


你能说明你使用的是哪个Python版本吗? - Neuron

9

如果你的列表或元组中的所有项都是相同的C类型,那么还应考虑使用标准库中的array模块。它将占用更少的内存并且可能更快。


21
使用压缩格式可以减少内存占用,但访问时间可能会比较慢,而不是更快。访问一个元素需要将压缩值解压成实际的整数,这会拖慢过程。 - Brian

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

enter image description here

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

enter image description here

-- 内存分配

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

另一方面,由于不可变容器一旦创建其项数就固定了,所以它们不需要这种超额分配 - 因此它们的存储效率更高。随着元组变得越来越大,它们的大小也会增加。

-- 复制

对于不可变序列来说,进行浅层复制没有意义,因为您无论如何都不能对其进行改变。因此,复制元组只返回它本身,具有相同的内存地址。这就是为什么复制元组更快的原因。

检索元素

我比较了从元组和列表中检索元素所花费的时间:

enter image description here

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

你能验证一下你最初的陈述吗?我认为你的意思是:如果所有元素都在,元组 的性能更好,或者 元组 的性能更好,但仅当所有元素都在时。 - gerardw
我的意思是,如果元组内的所有元素都是不可变的。例如,元组内包含([1,2])列表,而列表是可变的,因此它不会表现得更好。 - Yilmaz

3

元组应该比列表更有效,因为它们是不可变的,所以更快。


7
为什么你说不变性本身能增加效率?特别是在实例化和检索方面的效率? - Blair Conrad
1
看起来马克在我的回复之上已经涵盖了Python内部发生的反汇编指令。你可以看到实例化需要更少的指令,但在这种情况下,检索显然是相同的。 - Chris Cherry
不可变元组比可变列表具有更快的访问速度。 - noobninja

-6

Tuple之所以在读取时非常高效,主要是因为它是不可变的。

为什么不可变对象易于读取?

原因在于元组可以存储在内存缓存中,而列表则不行。程序总是从列表的内存位置读取,因为它是可变的(随时可以更改)。


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