Являются ли кортежи более эффективными, чем списки в Python?

Есть ли разница в производительности между кортежами и списками, когда дело доходит до создания и поиска элементов?

Ответ 1

Модуль 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

Ответ 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

Резюме

Кортежи имеют тенденцию работать лучше, чем списки почти в каждой категории:

1) Кортежи могут быть сложенными.

2) Кортежи могут быть повторно использованы вместо копирования.

3) Кортежи компактны и не перераспределяют.

4) Кортежи напрямую ссылаются на свои элементы.

Кортежи могут быть сложенными

Кортежи констант можно предварительно вычислить с помощью оптимизатора Python peephole или AST-optimizer. С другой стороны, списки получаются с нуля:

    >>> 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;

Обратите внимание, что объект кортежа включает в себя два указателя данных непосредственно, в то время как объект списка имеет дополнительный слой косвенности внешнему массиву, содержащему два указателя данных.

Ответ 4

Кортежи, будучи неизменными, более эффективны с точки зрения памяти; списки, для эффективности, память памяти в целом, чтобы позволить добавлять без константы realloc s. Итак, если вы хотите итерации через постоянную последовательность значений в вашем коде (например, for direction in 'up', 'right', 'down', 'left':), кортежи предпочтительнее, так как такие кортежи предварительно вычисляются во время компиляции.

Скорости доступа должны быть одинаковыми (они сохраняются как непрерывные массивы в памяти).

Но alist.append(item) гораздо предпочтительнее atuple+= (item,), когда вы имеете дело с изменяемыми данными. Помните, что кортежи предназначены для обработки как записи без имен полей.

Ответ 5

Вы также должны рассмотреть модуль array в стандартной библиотеке, если все элементы в вашем списке или кортеже имеют один и тот же тип. Это может быть быстрее и меньше памяти.

Ответ 6

Кортежи должны быть немного более эффективными и из-за этого быстрее, чем списки, потому что они неизменяемы.

Ответ 7

Вот еще один маленький ориентир, только ради этого.

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% дополнительного времени для выполнения задания по сравнению со списками.