#python #deque
#python #deque
Вопрос:
Недавно я занялся исследованием того, как различные структуры данных реализованы в Python, чтобы сделать мой код более эффективным. Исследуя, как работают списки и deques, я обнаружил, что могу получить преимущества, когда хочу сдвигать и отменять смещение, сокращая время с O (n) в списках до O (1) в deques (списки реализуются в виде массивов фиксированной длины, которые приходится полностью копировать каждый раз, когда что-то вставляется спереди, и т.д.). Чего я, похоже, не могу найти, так это специфики реализации deque и специфики его недостатков в списках. Может кто-нибудь просветить меня по этим двум вопросам?
Ответ №1:
https://github.com/python/cpython/blob/v3.8.1/Modules/_collectionsmodule.c
A
dequeobject
состоит из двусвязного спискаblock
узлов.
Итак, deque
это (дважды) связанный список, как предполагает другой ответ.
Уточнение: это означает, что списки Python намного лучше подходят для операций с произвольным доступом и фиксированной длиной, включая нарезку, в то время как deques гораздо полезнее для того, чтобы нажимать и извлекать что-то с концов, при этом индексация (но, что интересно, не нарезка) возможна, но медленнее, чем со списками.
Комментарии:
1. Обратите внимание, что если вам просто нужно добавлять и вставлять в один конец (стек), списки должны работать лучше, поскольку
.append()
и.pop()
амортизируются O (1) (перераспределение и копирование происходят, но очень редко и только до тех пор, пока вы не достигнете максимального размера, который когда-либо будет иметь стек).2. @delnan: Но если вам нужна очередь, то что-то вроде
deque
определенно правильный путь.3. @Eli: Списки не имеют отношения к безопасности потоков (ну, это не встроено в их внутренности) и были настроены многими умными людьми в течение длительного времени.
4. @delnan: На самом деле,
deque
s в CPython на самом деле также не обеспечивают потокобезопасность; они просто выигрывают от того, что GIL делает их операции атомарными (и фактически,append
иpop
с концаlist
имеют одинаковые защиты). На практике, если вы просто используете стек, обаlist
иdeque
имеют фактически одинаковую производительность в CPython; выделение блоков происходит чаще сdeque
(но не с обычным связанным списком; в конечном итоге вы бы выделяли / освобождали каждый раз, когда пересекали границу 64 элементов в реализации CPython), но отсутствие огромных прерывистых копий компенсируется.5. Для чистой реализации Python ознакомьтесь с кодом PyPy . Интересно, что это двусвязный список небольших блоков массива.
Ответ №2:
Ознакомьтесь collections.deque
. Из документов:
Deques поддерживают потокобезопасные, экономящие память добавления и всплывающие окна с любой стороны deque с примерно одинаковой производительностью O (1) в любом направлении.
Хотя объекты списка поддерживают аналогичные операции, они оптимизированы для быстрых операций с фиксированной длиной и требуют затрат на перемещение памяти O (n) для операций pop (0) и insert (0, v), которые изменяют как размер, так и положение базового представления данных.
Как и сказано, использование pop (0) или insert (0, v) влечет за собой большие штрафы для объектов списка. Вы не можете использовать операции срезания / индексирования на deque
, но вы можете использовать popleft
/ appendleft
, для которых эти операции deque
оптимизированы. Вот простой тест, демонстрирующий это:
import time
from collections import deque
num = 100000
def append(c):
for i in range(num):
c.append(i)
def appendleft(c):
if isinstance(c, deque):
for i in range(num):
c.appendleft(i)
else:
for i in range(num):
c.insert(0, i)
def pop(c):
for i in range(num):
c.pop()
def popleft(c):
if isinstance(c, deque):
for i in range(num):
c.popleft()
else:
for i in range(num):
c.pop(0)
for container in [deque, list]:
for operation in [append, appendleft, pop, popleft]:
c = container(range(num))
start = time.time()
operation(c)
elapsed = time.time() - start
print "Completed %s/%s in %.2f seconds: %.1f ops/sec" % (container.__name__, operation.__name__, elapsed, num / elapsed)
Результаты на моей машине:
Completed deque/append in 0.02 seconds: 5582877.2 ops/sec
Completed deque/appendleft in 0.02 seconds: 6406549.7 ops/sec
Completed deque/pop in 0.01 seconds: 7146417.7 ops/sec
Completed deque/popleft in 0.01 seconds: 7271174.0 ops/sec
Completed list/append in 0.01 seconds: 6761407.6 ops/sec
Completed list/appendleft in 16.55 seconds: 6042.7 ops/sec
Completed list/pop in 0.02 seconds: 4394057.9 ops/sec
Completed list/popleft in 3.23 seconds: 30983.3 ops/sec
Комментарии:
1. Ха, только что заметил, что вы не можете выполнять нарезку с помощью deques, хотя вы можете выполнять индексацию. Интересно.
2. 1 для таймингов — интересно, что
list
добавления выполняются немного быстрее, чемdeque
добавления.3. @zeekay: Это довольно странно, учитывая, что для поиска индекса определенного элемента обычно в любом случае требуется перебор элементов коллекции, и что вы можете индексировать в a
deque
точно так же, как и в alist
.4.@senderle: Конечно,
list
pop
они были медленнее, чем уdeque
(вероятно, из-заlist
более высокой стоимости периодического изменения размера по мере его сжатия, гдеdeque
просто освобождаются блоки обратно в свободный список или небольшой пул объектов), поэтому при выборе структуры данных для стека (он же очередь LIFO) производительность от пустого к полному выглядит немного лучше дляdeque
(в среднем 6365K операций в секунду дляappend
/pop
по сравнению сlist
для 5578 Тыс. операций в секунду). Я подозреваю, чтоdeque
в реальном мире это было бы немного лучше, посколькуdeque
свободный список означает, что увеличение в первый раз обходится дороже, чем после сжатия.5. Чтобы уточнить мою ссылку на freelist: CPython на самом деле
deque
не будетfree
содержать до 16 блоков (по модулю, а не поdeque
каждому), вместо этого помещая их в дешевый массив доступных блоков для повторного использования. Таким образом, при выращиванииdeque
в первый раз ему всегда приходится извлекать новые блоки изmalloc
(чтоappend
делает его более дорогим), но если он постоянно немного расширяется, затем немного сжимается и обратно, обычно это вообще не будет включатьmalloc
/free
, пока длина остается примерно в пределах диапазона 1024 элементов (16 блоков в свободном списке, 64 слота на блок).
Ответ №3:
Я подозреваю, что запись документации для deque
объектов содержит большую часть того, что вам нужно знать. Примечательные цитаты:
Deques поддерживают потокобезопасные, экономящие память добавления и всплывающие окна с обеих сторон deque с примерно одинаковой производительностью O (1) в любом направлении.
Но…
Индексированный доступ равен O (1) на обоих концах, но замедляется до O (n) в середине. Для быстрого произвольного доступа вместо этого используйте списки.
Мне пришлось бы взглянуть на исходный код, чтобы определить, является ли реализация связанным списком или чем-то другим, но мне кажется, что deque
имеет примерно те же характеристики, что и двусвязный список.
Ответ №4:
В дополнение ко всем другим полезным ответам, здесь приведена дополнительная информация, сравнивающая временную сложность (Big-Oh) различных операций над списками Python, deques, наборами и словарями. Это должно помочь в выборе правильной структуры данных для конкретной проблемы.