¿Cómo se implementan los deques en Python y cuándo son peores que las listas?

84

Recientemente me he dedicado a investigar cómo se implementan varias estructuras de datos en Python para hacer que mi código sea más eficiente. Al investigar cómo funcionan las listas y los deques, descubrí que puedo obtener beneficios cuando quiero cambiar y deshacer el cambio reduciendo el tiempo de O (n) en listas a O (1) en deques (listas que se implementan como matrices de longitud fija que tienen copiar completamente cada vez que se inserta algo en la parte frontal, etc ...). Lo que parece que no puedo encontrar son los detalles de cómo se implementa un deque y los detalles de sus desventajas frente a las listas. ¿Alguien puede iluminarme sobre estas dos preguntas?

Eli
fuente

Respuestas:

73

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

A dequeobjectse compone de una lista de blocknodos doblemente enlazados .

Entonces, sí, a dequees una lista (doblemente) vinculada, como sugiere otra respuesta.

Elaboración: lo que esto significa es que las listas de Python son mucho mejores para operaciones de acceso aleatorio y de longitud fija, incluido el corte, mientras que los deques son mucho más útiles para empujar y sacar cosas de los extremos, con indexación (pero no corte, curiosamente) siendo posible pero más lento que con listas.

PINCHAZO
fuente
3
Tenga en cuenta que si usted sólo tiene que anexar y el pop en un extremo (pila), las listas deben realizar mejor como .append()y .pop()son amortizado O (1) (reasignación y copiar sucede, pero muy rara vez y sólo hasta que llegue al máximo. Tamaño de la pila se alguna vez).
@delnan: Pero si quieres una cola, entonces dequedefinitivamente es el camino correcto a seguir.
JAB
@delnan: ¿Cómo te imaginas? .append () y .pop () se amortizan O (1) en las listas, pero no son O (1) reales en las deques y las copias nunca son necesarias.
Eli
1
@Eli: Las listas no se ocupan de la seguridad de los subprocesos (bueno, no está conectado a sus componentes internos) y muchas personas inteligentes las han ajustado durante mucho tiempo.
3
@delnan: En realidad, los deques en CPython tampoco manejan la seguridad de los subprocesos; simplemente se benefician de que el GIL haga que sus operaciones sean atómicas (y de hecho, appendy popdesde el final de a listtiene las mismas protecciones). En la práctica, si solo está usando una pila, ambos listy dequetienen un rendimiento efectivamente idéntico en CPython; las asignaciones de bloques son más frecuentes con deque(pero no frecuentes con listas enlazadas simples; solo terminaría asignando / liberando cada vez que cruza un límite de 64 miembros en la implementación de CPython), pero la falta de copias intermitentes enormes compensa.
ShadowRanger
51

Echa un vistazo collections.deque. De los documentos:

Los deques admiten agregados y estallidos seguros para subprocesos y eficientes en memoria desde cualquier lado del deque con aproximadamente el mismo rendimiento O (1) en cualquier dirección.

Aunque los objetos de lista admiten operaciones similares, están optimizados para operaciones rápidas de longitud fija e incurren en costos de movimiento de memoria O (n) para operaciones de inserción (0) e inserción (0, v) que cambian tanto el tamaño como la posición de la representación de datos subyacentes .

Tal como dice, el uso de pop (0) o insert (0, v) conlleva grandes penalizaciones con los objetos de lista. No puede usar operaciones de corte / índice en a deque, pero puede usar popleft/ appendleft, que son operaciones para las que dequeestá optimizado. Aquí hay un punto de referencia simple para demostrar esto:

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)

Resultados en mi máquina:

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
zeekay
fuente
3
Eh, acabo de notar que no puedes cortar con deques aunque puedes indexar. Interesante.
JAB
1
+1 para los tiempos: interesante que los listanexos son un poco más rápidos que los dequeanexos.
remitente
1
@zeekay: Eso es bastante extraño, considerando que la búsqueda del índice de un elemento específico normalmente requeriría iterar sobre los elementos de la colección de todos modos, y que puede indexar en un dequetal como lo haría con un list.
JAB
1
@senderle: Por supuesto, los list pops eran más lentos que dequelos de '(probablemente debido al listmayor costo de cambiar el tamaño de forma intermitente a medida que se reduce, donde dequesolo se liberan bloques de nuevo a la lista libre o al grupo de objetos pequeños), por lo que al seleccionar una estructura de datos para una pila (también conocida como cola LIFO), el rendimiento de vacío a lleno a vacío se ve ligeramente mejor para deque(promedios de 6365K operaciones / seg para append/ pop, frente a list5578K operaciones / seg). Sospecho que dequelo haría un poco mejor en el mundo real, ya que dequela lista gratuita significa que crecer por primera vez es más caro que crecer después de reducirse.
ShadowRanger
1
Para aclarar mi referencia de lista libre: CPython's dequeno tendrá freehasta 16 bloques (en todo el módulo, no por deque), sino que los colocará en una matriz barata de bloques disponibles para su reutilización. Entonces, cuando crece un dequepor primera vez, siempre tiene que extraer nuevos bloques malloc(lo que hace que sea appendmás caro), pero si se expande constantemente un poco, luego se encoge un poco, y de ida y vuelta, generalmente no involucrará malloc/ freeen todo ello siempre que la longitud se mantenga aproximadamente dentro de un rango de 1024 elementos (16 bloques en la lista libre, 64 ranuras por bloque).
ShadowRanger
16

La entrada de documentación para dequeobjetos explica la mayor parte de lo que necesita saber, sospecho. Citas notables:

Los deques admiten agregados y estallidos seguros para subprocesos y eficientes en memoria desde cualquier lado del deque con aproximadamente el mismo rendimiento O (1) en cualquier dirección.

Pero...

El acceso indexado es O (1) en ambos extremos, pero se ralentiza hasta O (n) en el medio. Para un acceso aleatorio rápido, use listas en su lugar.

Tendría que echar un vistazo a la fuente para saber si la implementación es una lista vinculada o algo más, pero me parece que dequetiene aproximadamente las mismas características que una lista doblemente vinculada.

remitente
fuente
10

Además de todas las otras respuestas útiles, aquí hay más información que compara la complejidad del tiempo (Big-Oh) de varias operaciones en listas, deques, conjuntos y diccionarios de Python. Esto debería ayudar a seleccionar la estructura de datos adecuada para un problema en particular.

PythonJin
fuente
-3

Si bien no estoy exactamente seguro de cómo lo implementó Python, aquí escribí una implementación de Colas usando solo matrices. Tiene la misma complejidad que las colas de 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

Y aquí puedes probarlo con algún código:

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()

No funcionará tan rápido como la implementación de C, pero usa la misma lógica.

El Revanchista
fuente
1
¡No.invente.la. rueda!
Abhijit Sarkar
La pregunta era cómo se implementa la deque de Python. No pedía una implementación alternativa.
Gino Mempin