Чи кортежі ефективніші, ніж списки в Python?


225

Чи є різниця між кортежами та списками, коли мова йде про інстанціювання та пошук елементів?



2
Якщо ви зацікавлені в пам'яті , використовуваної між Варіюється типу подивитися сюжет , який я зробив: stackoverflow.com/a/30008338/2087463
tmthydvnprt

Відповіді:


172

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

66
Помилка, лише те, що генерується той самий байт-код, зовсім не означає, що такі ж операції відбуваються на рівні C (а отже і cpu). Спробуйте створити клас ListLikeз а, __getitem__який робить щось жахливо повільно, а потім розберіть x = ListLike((1, 2, 3, 4, 5)); y = x[2]. Байт-код буде більше схожий на приклад кортежу вище, ніж приклад списку, але чи дійсно ви вірите, що означає, що продуктивність буде подібною?
mzz

2
Здається, ви говорите, що деякі типи ефективніші, ніж інші. Це має сенс, але накладні покоління списків та кортежів, схоже, є ортогональними для відповідного типу даних, із застереженням, що вони є списками та кортежами одного типу даних.
Марк Гаррісон

11
Кількість байт-кодів, як і кількість рядків коду, мало стосується швидкості виконання (а отже, ефективності та продуктивності).
мартіно

18
Хоча пропозиція, з якої ви можете зробити що-небудь висновок із підрахунку ops, помилкова, це свідчить про ключову різницю: постійні кортежі зберігаються як такі в байтовому коді та просто посилаються при використанні, тоді як списки потрібно створювати під час виконання.
poolie

6
Ця відповідь показує нам, що Python визнає кортежні константи. Це добре знати! Але що відбувається при спробі скласти кортеж або список зі змінних значень?
Том

211

Взагалі, ви можете очікувати, що кортежі будуть трохи швидшими. Однак ви обов'язково повинні перевірити свій конкретний випадок (якщо різниця може вплинути на ефективність вашої програми - пам’ятайте, що "передчасна оптимізація є коренем усього зла").

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 були ваші тести!
Метт Столяр

2
Існує ще один цікавий тест - python -m timeit "x=tuple(xrange(999999))"проти python -m timeit "x=list(xrange(999999))". Як можна було б очікувати, на матеріалізацію кортежу, ніж списку, потрібно трохи більше часу.
Гаміш Грубіян

3
Здається дивним, що кортеж доступ повільніше, ніж доступ до списку. Однак, спробувавши це в Python 2.7 на моєму ПК з Windows 7, різниця становить лише 10%, так що неважливо.
ToolmakerSteve

51
FWIW, доступ до списку швидший, ніж кортеж доступу в Python 2, але лише тому, що в списку BINARY_SUBSCR в Python / ceval.c є спеціальний випадок. У Python 3 оптимізації немає, і кортежі доступу стають дещо швидшими, ніж доступ до списку.
Реймонд Хеттінгер

3
@yoopoo, перший тест створює список мільйон разів, але другий створює список один раз і звертається до нього мільйон разів. -s "SETUP_CODE"Виконуються до фактичного таймерного коду.
leewz

203

Підсумок

Кортежі, як правило, краще, ніж списки майже в кожній категорії:

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

Кортежі не перерозподіляють

Оскільки розмір кортежу фіксований, його можна зберігати більш компактно, ніж списки, яким потрібно перерозподілити, щоб зробити операції додавання () ефективними.

Це дає кортежам хорошу космічну перевагу:

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

Зауважте, що об'єкт кортежу містить два вказівника даних безпосередньо, тоді як об'єкт списку має додатковий шар непрямості до зовнішнього масиву, що містить два покажчики даних.


19
Нарешті, хтось ставить С-структури!
осман

1
Internally, tuples are stored a little more efficiently than lists, and also tuples can be accessed slightly faster. Як ви могли б пояснити результати відповіді df. Тоді?
DRz

5
Під час роботи зі списками ~ 50k зі списків ~ 100 елементів переміщення цієї структури в кортежі зменшило час пошуку на кілька порядків для кількох пошукових запитів. Я вважаю, що це пов'язано з більшою локальністю кешу кортежу, коли ви починаєте використовувати кортеж через видалення другого шару непрямості, який ви демонструєте.
хорта

tuple(some_tuple)повертається лише в some_tupleтому випадку, коли він some_tupleє хешируемим - коли його вміст є рекурсивно незмінним і хешируемим. В іншому випадку tuple(some_tuple)повертається новий кортеж. Наприклад, коли some_tupleмістяться елементи, що змінюються.
Лучано

Кортежі не завжди швидші. Розгляньте `` `t = () для i в діапазоні (1,100): t + = il = [] для i в діапазоні (1,1000): a.append (i)` `` Другий швидше
мелвіл Джеймс

32

Кортежі, будучи непорушними, ефективніше пам’яті; списки, для ефективності перерозподіляють пам'ять, щоб дозволити додавання без постійних reallocs. Отже, якщо ви хочете повторити через постійну послідовність значень у вашому коді (наприклад for direction in 'up', 'right', 'down', 'left':), кращі кортежі є переважними, оскільки такі кортежі попередньо розраховуються за час компіляції.

Швидкість доступу повинна бути однаковою (вони обидва зберігаються як суміжні масиви в пам'яті).

Але, alist.append(item)це набагато більше переваги, atuple+= (item,)коли ви маєте справу з змінними даними. Пам'ятайте, що кортежі призначені для трактування як записи без назви полів.


1
який час компіляції в python?
балки

1
@balki: час, коли джерело python збирається в байт-код (який байтовий код може бути збережений як файл .py [co]).
tzot

Цитування було б чудово, якщо можливо.
Grijesh Chauhan

9

Ви також повинні врахувати arrayмодуль у стандартній бібліотеці, якщо всі елементи у вашому списку або кортежі одного типу C. Це займе менше пам'яті і може бути швидше.


15
Це займе менше пам'яті, але час доступу, ймовірно, буде трохи повільніше, ніж швидше. Доступ до елемента вимагає, щоб упаковане значення не було розміщене в коді до реального цілого числа, що сповільнить процес.
Брайан

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%додаткового часу, щоб виконати роботу порівняно зі списками.


4

Кортежі повинні бути трохи ефективнішими, а тому швидшими, ніж списки, оскільки вони незмінні.


4
Чому ви кажете, що незмінність сама по собі підвищує ефективність? Особливо ефективність інстанції та пошуку?
Блер Конрад

1
Здається, відповідь Марка вище моєї охопила розібрані вказівки того, що відбувається всередині Python. Ви можете бачити, що інстанція вимагає меншої кількості інструкцій, однак у цьому випадку пошук, мабуть, ідентичний.
ctcherry

незмінні кортежі мають швидший доступ, ніж змінні списки
noobninja

-6

Основна причина, щоб Tuple був дуже ефективним у читанні, це тому, що він непорушний.

Чому незмінні предмети легко читати?

Причина в тому, що кортежі можуть зберігатися в кеш-пам'яті, на відміну від списків. Програма завжди читає зі списку пам'яті місця, оскільки воно є змінним (може змінюватися будь-коли).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.