元组在Python中比列表更高效吗?
在创建和获取元素时,元组和列表之间有没有性能差异?
9 个回答
一般来说,你可能会觉得元组(tuple)会稍微快一点。不过,你一定要测试一下你具体的情况(如果这个差异会影响你程序的性能——记住“过早优化是万恶之源”)。
在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
所以在这个例子中,创建元组的速度几乎快了一个数量级,但访问列表中的项目实际上要快一些!所以如果你创建几个元组,但要访问它们很多次,使用列表可能会更快。
当然,如果你想要改变某个项目,列表肯定会更快,因为你需要创建一个全新的元组来改变其中的一个项目(因为元组是不可变的)。
总结
元组在几乎所有方面的表现都比列表好:
元组可以被常量折叠。
元组可以被重用,而不是复制。
元组比较紧凑,不会过度分配空间。
元组直接引用它们的元素。
元组可以进行常量折叠
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;
注意,元组对象直接包含两个数据指针,而列表对象则有一个额外的间接层,指向一个外部数组,该数组保存这两个数据指针。
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