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

元组和列表在元素的实例化和检索方面有什么性能差异吗?

122254 次浏览

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

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

Python让这变得非常简单:时间是你的朋友。

$ 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

因此,在这种情况下,元组的实例化几乎快了一个数量级,但列表的项访问实际上要快一些!因此,如果你创建了一些元组,并多次访问它们,实际上,使用列表可能会更快。

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

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

元组是不可变的,内存效率更高;列表,为了速度效率,过度分配内存,以允许没有常量__abc0的追加。因此,如果你想在你的代码中迭代一个常量序列的值(例如for direction in 'up', 'right', 'down', 'left':),元组是首选的,因为这样的元组是在编译时预先计算的。

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

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

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

总结

元组往往比列表执行得更好几乎在每个类别:

  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

下面是来自对象/ 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;

请注意,tuple对象直接合并了这两个数据指针,而list对象有一个额外的间接层,用于保存这两个数据指针的外部数组。

这里是另一个小基准,只是为了它的缘故。

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%额外的时间来完成这项工作。

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

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

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

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

enter image description here

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

enter image description here

——内存分配

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

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

——复制

对不可变序列做一个浅拷贝是没有意义的,因为无论如何你都不能改变它。复制tuple只是返回它自己,还有内存地址。这就是为什么复制tuple更快

检索元素

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

enter image description here

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