在 Python 中元组比列表更高效吗?

2025-02-05 13:23:00
admin
原创
55
摘要:问题描述:在实例化和检索元素时,元组和列表之间是否存在性能差异?解决方案 1:概括元组在几乎所有类别中的表现都比列表更好:元组可以进行常量折叠。元组可以被重复使用,而不需要复制。元组是紧凑的并且不会过度分配。元组直接引用其元素。元组可以进行常量折叠常量元组可以由 Python 的窥孔优化器或 AST 优化器预...

问题描述:

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


解决方案 1:

概括

元组在几乎所有类别中的表现都比列表更好

  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 

元组不需要复制

运行后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;

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

解决方案 2:

一般而言,您可能希望元组稍微快一些。但是,您一定要测试您的具体情况(如果差异可能会影响程序的性能——请记住“过早优化是万恶之源”)。

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:

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

解决方案 4:

元组是不可变的,因此更节省内存;而列表为了提高速度效率,会过度分配内存,以便允许附加而不使用常量realloc。因此,如果您想在代码中迭代一个常量值序列(例如for direction in 'up', 'right', 'down', 'left':),元组是首选,因为此类元组是在编译时预先计算的。

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

但是,当你处理可变数据时,alist.append(item)这种方法更受欢迎atuple+= (item,)。请记住,元组应被视为没有字段名称的记录。

解决方案 5:

这是另一个小基准,仅仅为了它的目的。

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%

解决方案 6:

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

解决方案 7:

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

在此处输入图片描述

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

在此处输入图片描述

--内存分配

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

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

-复制

对不可变序列进行浅拷贝是没有意义的,因为无论如何你都无法改变它。所以复制元组只会返回自身和内存地址。这就是为什么复制元组更快的原因

检索元素

我对从元组和列表中检索元素进行了计时:

在此处输入图片描述

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

解决方案 8:

元组的效率应该比列表略高一些,而且速度也更快,因为它们是不可变的。

解决方案 9:

Tuple 读取效率高的主要原因是它是不可变的。

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

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

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   1565  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1354  
  信创国产芯片作为信息技术创新的核心领域,对于推动国家自主可控生态建设具有至关重要的意义。在全球科技竞争日益激烈的背景下,实现信息技术的自主可控,摆脱对国外技术的依赖,已成为保障国家信息安全和产业可持续发展的关键。国产芯片作为信创产业的基石,其发展水平直接影响着整个信创生态的构建与完善。通过不断提升国产芯片的技术实力、产...
国产信创系统   21  
  信创生态建设旨在实现信息技术领域的自主创新和安全可控,涵盖了从硬件到软件的全产业链。随着数字化转型的加速,信创生态建设的重要性日益凸显,它不仅关乎国家的信息安全,更是推动产业升级和经济高质量发展的关键力量。然而,在推进信创生态建设的过程中,面临着诸多复杂且严峻的挑战,需要深入剖析并寻找切实可行的解决方案。技术创新难题技...
信创操作系统   27  
  信创产业作为国家信息技术创新发展的重要领域,对于保障国家信息安全、推动产业升级具有关键意义。而国产芯片作为信创产业的核心基石,其研发进展备受关注。在信创国产芯片的研发征程中,面临着诸多复杂且艰巨的难点,这些难点犹如一道道关卡,阻碍着国产芯片的快速发展。然而,科研人员和相关企业并未退缩,积极探索并提出了一系列切实可行的解...
国产化替代产品目录   28  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用