Як реалізовані декети в Python і коли вони гірші за списки?


84

Нещодавно я дослідив, як різні структури даних реалізовані в Python, щоб зробити мій код більш ефективним. Досліджуючи, як працюють списки та деки, я виявив, що я можу отримати переваги, коли хочу зрушити і зрушити зміну, зменшивши час від O (n) у списках до O (1) у deques (списки реалізовані як масиви фіксованої довжини, які мають повністю копіювати кожного разу, коли щось вставляється спереду, тощо ...). Напевно, я не можу знайти специфіки того, як реалізована дека, та особливості її мінусів проти списків. Хтось може просвітлити мене з цих двох питань?

Відповіді:


73

https://github.com/python/cpython/blob/v3.8.1/Modules/_collectionsmodule.c

A dequeobjectскладається з подвійно пов'язаного списку blockвузлів.

Так що так, a deque- це (подвійно-) зв’язаний список, як підказує інша відповідь.

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


3
Зверніть увагу, що якщо вам просто потрібно додати та вивести на одному кінці (стеку), списки повинні працювати ефективніше, як .append()і .pop()амортизуються O (1) (перерозподіл та копіювання трапляються, але дуже рідко і лише до досягнення максимального розміру стека. ніколи не мали).

@delnan: Але якщо ти хочеш черги, то щось на зразок dequeточно є правильним шляхом.
JAB

@delnan: Як ти фігуруєш? .append () та .pop () амортизуються O (1) у списках, але чи не є вони фактичними O (1) на декетах та копіях ніколи не потрібні.
Елі

1
@Eli: Списки не займаються безпекою потоків (ну, це не підключено до їх внутрішніх органів), і їх давно налаштовують багато розумних людей.

3
@delnan: Насправді, deques в CPython теж не справляються з безпекою потоків; вони просто виграють від того, що GIL робить свої операції атомними (і насправді, appendі popз кінця a listмає однакові засоби захисту). На практиці, якщо ви просто використовуєте стек, обидва listі dequeмають фактично однакову продуктивність у CPython; розподіли блоків частіші deque(але часто не звичайний зв’язаний список; ви в кінцевому підсумку розподіляєте / звільняєте лише кожного разу, коли ви переходили кордон із 64 членів у реалізації CPython), але відсутність величезних періодичних копій компенсує.
ShadowRanger

51

Перевірте collections.deque. З документів:

Декели підтримують безпечне використання потоків, ефективно додає пам'ять і вискакує з обох сторін деке з приблизно однаковою продуктивністю O (1) в будь-якому напрямку.

Хоча об'єкти списку підтримують подібні операції, вони оптимізовані для швидких операцій з фіксованою довжиною та несуть витрати на переміщення пам'яті O (n) для операцій pop (0) та insert (0, v), які змінюють як розмір, так і положення основного представлення даних .

Як сказано, використання pop (0) або insert (0, v) вимагає великих покарань за об'єкти списку. Ви не можете використовувати операції зрізу / індексації на a 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

3
Так, я щойно помітив, що ти не можеш робити нарізки за допомогою deques, хоча ти можеш робити індексацію. Цікаво.
JAB

1
+1 для часу - цікаво, що listдодається трохи швидше, ніж dequeдодається.
senderle

1
@zeekay: Це дивно, враховуючи, що для пошуку індексу конкретного елемента, як правило, потрібно перебирати елементи колекції, і що ви можете індексувати так dequeсамо, як і list.
JAB

1
@senderle: Звичайно, list pops були повільнішими, ніж deque's' (ймовірно, через listвищу вартість періодично змінюваного розміру при зменшенні, де dequeпросто звільняються блоки назад до вільного списку або пулу малих об'єктів), тому при виборі структури даних для стека (він же черга LIFO), продуктивність від порожнього до повного до порожнього виглядає дещо кращою deque(в середньому 6365 тис. операцій / сек для append/ popпроти list5578 тис. операцій / сек). Я підозрюю, dequeщо це було б трохи краще в реальному світі, оскільки dequeфреліст означає, що вирощування вперше дорожче, ніж вирощування після скорочення.
ShadowRanger

1
Щоб пояснити моє посилання на фреліст: CPython dequeнасправді не freeскладатиме до 16 блоків (загальномодульних, а не для кожного deque), натомість розміщуючи їх у дешевому масиві доступних блоків для повторного використання. Тому , коли вирощування dequeв перший раз, він завжди повинен тягнути нові блоки з malloc(роблячи appendбільш дорогий), але якщо він постійно розширюється трохи, а потім стискається трохи, і вперед і назад, він, як правило , не пов'язані з malloc/ freeна все до тих пір, поки довжина залишається приблизно в межах 1024 елементів (16 блоків у вільному списку, 64 слоти на блок).
ShadowRanger

16

Підозрюю, що в документації для dequeоб’єктів викладено більшість того, що вам потрібно знати. Помітні цитати:

Декели підтримують безпечне використання потоків, ефективно додає пам'ять і вискакує з обох сторін деке з приблизно однаковою продуктивністю O (1) в будь-якому напрямку.

Але ...

Індексований доступ - O (1) на обох кінцях, але сповільнюється до O (n) посередині. Для швидкого довільного доступу використовуйте натомість списки.

Мені довелося б поглянути на джерело, щоб сказати, чи є реалізація пов’язаним списком чи чимось іншим, але для мене це звучить так, ніби a dequeмає приблизно ті ж характеристики, що і подвійно пов’язаний список.


10

На додаток до всіх інших корисних відповідей, ось ще трохи інформації, що порівнює складність часу (Big-Oh) різних операцій зі списками, деками, наборами та словниками Python. Це має допомогти у виборі правильної структури даних для конкретної проблеми.


-3

Хоча я не зовсім впевнений, як Python це реалізував, тут я написав реалізацію Queues, використовуючи лише масиви. Він має таку ж складність, як черги Python.

class ArrayQueue:
""" Implements a queue data structure """

def __init__(self, capacity):
    """ Initialize the queue """

    self.data = [None] * capacity
    self.size = 0
    self.front = 0

def __len__(self):
    """ return the length of the queue """

    return self.size

def isEmpty(self):
    """ return True if the queue is Empty """

    return self.data == 0

def printQueue(self):
    """ Prints the queue """

    print self.data 

def first(self):
    """ Return the first element of the queue """

    if self.isEmpty():
        raise Empty("Queue is empty")
    else:
        return self.data[0]

def enqueue(self, e):
    """ Enqueues the element e in the queue """

    if self.size == len(self.data):
        self.resize(2 * len(self.data))
    avail = (self.front + self.size) % len(self.data) 
    self.data[avail] = e
    self.size += 1

def resize(self, num):
    """ Resize the queue """

    old = self.data
    self.data = [None] * num
    walk = self.front
    for k in range(self.size):
        self.data[k] = old[walk]
        walk = (1+walk)%len(old)
    self.front = 0

def dequeue(self):
    """ Removes and returns an element from the queue """

    if self.isEmpty():
        raise Empty("Queue is empty")
    answer = self.data[self.front]
    self.data[self.front] = None 
    self.front = (self.front + 1) % len(self.data)
    self.size -= 1
    return answer

class Empty(Exception):
""" Implements a new exception to be used when stacks are empty """

pass

І тут ви можете протестувати це за допомогою коду:

def main():
""" Tests the queue """ 

Q = ArrayQueue(5)
for i in range(10):
    Q.enqueue(i)
Q.printQueue()    
for i in range(10):
    Q.dequeue()
Q.printQueue()    


if __name__ == '__main__':
    main()

Це буде працювати не так швидко, як реалізація C, але використовує ту саму логіку.


1
Не вигадуйте колесо!
Абхіджіт Саркар

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