búfer circular eficiente?

109

Quiero crear un búfer circular eficiente en Python (con el objetivo de tomar promedios de los valores enteros en el búfer).

¿Es esta una forma eficaz de utilizar una lista para recopilar valores?

def add_to_buffer( self, num ):
    self.mylist.pop( 0 )
    self.mylist.append( num )

¿Qué sería más eficiente (y por qué)?

jedierikb
fuente
Esta no es una forma eficiente de implementar un búfer circular porque pop (0) es la operación O (n) en la lista. pop (0) elimina el primer elemento de la lista y todos los elementos deben desplazarse hacia la izquierda. Use collections.deque con el atributo maxlen en su lugar. deque tiene la operación O (1) para agregar y hacer estallar.
Vlad Bezden

Respuestas:

205

Lo usaría collections.dequecon un maxlenarg

>>> import collections
>>> d = collections.deque(maxlen=10)
>>> d
deque([], maxlen=10)
>>> for i in xrange(20):
...     d.append(i)
... 
>>> d
deque([10, 11, 12, 13, 14, 15, 16, 17, 18, 19], maxlen=10)

Hay una receta en los documentos dequeque es similar a lo que desea. Mi afirmación de que es el más eficiente se basa completamente en el hecho de que está implementado en C por un equipo increíblemente capacitado que tiene el hábito de generar código de primera categoría.

aaronasterling
fuente
7
+1 Sí, es la forma agradable de incluir pilas. Las operaciones para el búfer circular son O (1) y, como dices, la sobrecarga adicional está en C, por lo que aún debería ser bastante rápido
John La Rooy
7
No me gusta esta solución porque los documentos no garantizan el acceso aleatorio O (1) cuando maxlenestá definido. O (n) es comprensible cuando el dequepuede crecer hasta el infinito, pero si maxlense da, indexar un elemento debe ser un tiempo constante.
lvella
1
Supongo que está implementado como una lista vinculada y no como una matriz.
e-satis
1
Parece correcto, si los tiempos en mi respuesta a continuación son correctos.
djvg
13

Salir del encabezado de una lista hace que se copie la lista completa, por lo que es ineficiente

En su lugar, debe usar una lista / matriz de tamaño fijo y un índice que se mueve a través del búfer a medida que agrega / elimina elementos

John La Rooy
fuente
4
De acuerdo. No importa lo elegante o poco elegante que pueda parecer o el idioma que se use. En realidad, cuanto menos moleste al recolector de basura (o al administrador del montón o los mecanismos de paginación / mapeo o lo que sea que haga magia de memoria), mejor.
@RocketSurgeon No es magia, es solo que es una matriz cuyo primer elemento se elimina. Entonces, para una matriz de tamaño n, esto significa n-1 operaciones de copia. Aquí no interviene ningún recolector de basura o dispositivo similar.
Christian
3
Estoy de acuerdo. Hacerlo también es mucho más fácil de lo que algunas personas piensan. Simplemente use un contador cada vez mayor y use el operador de módulo (% arraylen) cuando acceda al elemento.
Andre Blum
ídem, puede consultar mi publicación de arriba, así es como lo hice
MoonCactus
10

Según la respuesta de MoonCactus , aquí hay una circularlistclase. La diferencia con su versión es que aquí c[0]siempre se dará el elemento agregado más antiguo, c[-1]el elemento agregado más reciente, c[-2]el penúltimo ... Esto es más natural para las aplicaciones.

c = circularlist(4)
c.append(1); print c, c[0], c[-1]    #[1]              1, 1
c.append(2); print c, c[0], c[-1]    #[1, 2]           1, 2
c.append(3); print c, c[0], c[-1]    #[1, 2, 3]        1, 3
c.append(8); print c, c[0], c[-1]    #[1, 2, 3, 8]     1, 8
c.append(10); print c, c[0], c[-1]   #[10, 2, 3, 8]    2, 10
c.append(11); print c, c[0], c[-1]   #[10, 11, 3, 8]   3, 11

Clase:

class circularlist(object):
    def __init__(self, size, data = []):
        """Initialization"""
        self.index = 0
        self.size = size
        self._data = list(data)[-size:]

    def append(self, value):
        """Append an element"""
        if len(self._data) == self.size:
            self._data[self.index] = value
        else:
            self._data.append(value)
        self.index = (self.index + 1) % self.size

    def __getitem__(self, key):
        """Get element by index, relative to the current index"""
        if len(self._data) == self.size:
            return(self._data[(key + self.index) % self.size])
        else:
            return(self._data[key])

    def __repr__(self):
        """Return string representation"""
        return self._data.__repr__() + ' (' + str(len(self._data))+' items)'

[Editado]:data parámetro opcional agregado para permitir la inicialización desde listas existentes, por ejemplo:

circularlist(4, [1, 2, 3, 4, 5])      #  [2, 3, 4, 5] (4 items)
circularlist(4, set([1, 2, 3, 4, 5])) #  [2, 3, 4, 5] (4 items)
circularlist(4, (1, 2, 3, 4, 5))      #  [2, 3, 4, 5] (4 items)
Basj
fuente
Buena adición. Las listas de Python ya permiten índices negativos, pero (-1), por ejemplo, no devolvería el valor esperado una vez que el búfer circular esté lleno, ya que la "última" adición a la lista termina dentro de la lista.
MoonCactus
1
Funciona @MoonCactus, vea los 6 ejemplos que di en la parte superior de la respuesta; en los últimos, se puede ver c[-1]que siempre es el elemento correcto. __getitem__lo hace bien.
Basj
oh sí, quiero decir que el mío falló, no el tuyo, lo siento: ¡DI aclarará mi comentario! - oh no puedo, el comentario es demasiado antiguo.
MoonCactus
buena solución simple. Agregué un argumento opcional para permitir la inicialización de la lista a partir de datos existentes, es más pythonpatético de esa manera.
Orwellophile
9

La deque de Python es lenta. También puedes usar numpy.roll en su lugar. ¿Cómo rotas los números en una matriz numerosa de forma (n,) o (n, 1)?

En este punto de referencia, deque es 448ms. Numpy.roll es de 29 ms http://scimusing.wordpress.com/2013/10/25/ring-buffers-in-pythonnumpy/

Orvar Korvar
fuente
1
Pero numpy.rolldevuelve una copia de la matriz, ¿verdad?
djvg
3
Esta respuesta es muy engañosa: el deque de Python parece ser bastante rápido, pero la conversión desde y hacia matrices numpy lo ralentiza considerablemente en los puntos de referencia a los que se vincula.
xitrium
7

ok con el uso de la clase deque, pero para los requisitos de la pregunta (promedio) esta es mi solución:

>>> from collections import deque
>>> class CircularBuffer(deque):
...     def __init__(self, size=0):
...             super(CircularBuffer, self).__init__(maxlen=size)
...     @property
...     def average(self):  # TODO: Make type check for integer or floats
...             return sum(self)/len(self)
...
>>>
>>> cb = CircularBuffer(size=10)
>>> for i in range(20):
...     cb.append(i)
...     print "@%s, Average: %s" % (cb, cb.average)
...
@deque([0], maxlen=10), Average: 0
@deque([0, 1], maxlen=10), Average: 0
@deque([0, 1, 2], maxlen=10), Average: 1
@deque([0, 1, 2, 3], maxlen=10), Average: 1
@deque([0, 1, 2, 3, 4], maxlen=10), Average: 2
@deque([0, 1, 2, 3, 4, 5], maxlen=10), Average: 2
@deque([0, 1, 2, 3, 4, 5, 6], maxlen=10), Average: 3
@deque([0, 1, 2, 3, 4, 5, 6, 7], maxlen=10), Average: 3
@deque([0, 1, 2, 3, 4, 5, 6, 7, 8], maxlen=10), Average: 4
@deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxlen=10), Average: 4
@deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], maxlen=10), Average: 5
@deque([2, 3, 4, 5, 6, 7, 8, 9, 10, 11], maxlen=10), Average: 6
@deque([3, 4, 5, 6, 7, 8, 9, 10, 11, 12], maxlen=10), Average: 7
@deque([4, 5, 6, 7, 8, 9, 10, 11, 12, 13], maxlen=10), Average: 8
@deque([5, 6, 7, 8, 9, 10, 11, 12, 13, 14], maxlen=10), Average: 9
@deque([6, 7, 8, 9, 10, 11, 12, 13, 14, 15], maxlen=10), Average: 10
@deque([7, 8, 9, 10, 11, 12, 13, 14, 15, 16], maxlen=10), Average: 11
@deque([8, 9, 10, 11, 12, 13, 14, 15, 16, 17], maxlen=10), Average: 12
@deque([9, 10, 11, 12, 13, 14, 15, 16, 17, 18], maxlen=10), Average: 13
@deque([10, 11, 12, 13, 14, 15, 16, 17, 18, 19], maxlen=10), Average: 14
SmartElectron
fuente
Recibo TypeError: 'numpy.float64' object is not callableal intentar llamar al averagemétodo
scls
Sí ... de hecho, supongo que deque usa matrices numpy internamente (después de eliminar @property funciona bien)
scls
17
Te garantizo que deque no usa matrices numpy internamente. collectionses parte de la biblioteca estándar, numpyno lo es. Las dependencias de bibliotecas de terceros constituirían una biblioteca estándar terrible.
6

Aunque ya hay una gran cantidad de excelentes respuestas aquí, no pude encontrar ninguna comparación directa de tiempos para las opciones mencionadas. Por lo tanto, encuentre mi humilde intento de comparación a continuación.

Solo con fines de prueba, la clase puede cambiar entre un listbúfer collections.dequebasado en un, un búfer Numpy.rollbasado en un y un búfer basado en uno.

Tenga en cuenta que el updatemétodo agrega solo un valor a la vez, para que sea simple.

import numpy
import timeit
import collections


class CircularBuffer(object):
    buffer_methods = ('list', 'deque', 'roll')

    def __init__(self, buffer_size, buffer_method):
        self.content = None
        self.size = buffer_size
        self.method = buffer_method

    def update(self, scalar):
        if self.method == self.buffer_methods[0]:
            # Use list
            try:
                self.content.append(scalar)
                self.content.pop(0)
            except AttributeError:
                self.content = [0.] * self.size
        elif self.method == self.buffer_methods[1]:
            # Use collections.deque
            try:
                self.content.append(scalar)
            except AttributeError:
                self.content = collections.deque([0.] * self.size,
                                                 maxlen=self.size)
        elif self.method == self.buffer_methods[2]:
            # Use Numpy.roll
            try:
                self.content = numpy.roll(self.content, -1)
                self.content[-1] = scalar
            except IndexError:
                self.content = numpy.zeros(self.size, dtype=float)

# Testing and Timing
circular_buffer_size = 100
circular_buffers = [CircularBuffer(buffer_size=circular_buffer_size,
                                   buffer_method=method)
                    for method in CircularBuffer.buffer_methods]
timeit_iterations = 1e4
timeit_setup = 'from __main__ import circular_buffers'
timeit_results = []
for i, cb in enumerate(circular_buffers):
    # We add a convenient number of convenient values (see equality test below)
    code = '[circular_buffers[{}].update(float(j)) for j in range({})]'.format(
        i, circular_buffer_size)
    # Testing
    eval(code)
    buffer_content = [item for item in cb.content]
    assert buffer_content == range(circular_buffer_size)
    # Timing
    timeit_results.append(
        timeit.timeit(code, setup=timeit_setup, number=int(timeit_iterations)))
    print '{}: total {:.2f}s ({:.2f}ms per iteration)'.format(
        cb.method, timeit_results[-1],
        timeit_results[-1] / timeit_iterations * 1e3)

En mi sistema esto produce:

list:  total 1.06s (0.11ms per iteration)
deque: total 0.87s (0.09ms per iteration)
roll:  total 6.27s (0.63ms per iteration)
djvg
fuente
4

¿Qué tal la solución del Python Cookbook , incluida una reclasificación de la instancia del búfer de anillo cuando se llena?

class RingBuffer:
    """ class that implements a not-yet-full buffer """
    def __init__(self,size_max):
        self.max = size_max
        self.data = []

    class __Full:
        """ class that implements a full buffer """
        def append(self, x):
            """ Append an element overwriting the oldest one. """
            self.data[self.cur] = x
            self.cur = (self.cur+1) % self.max
        def get(self):
            """ return list of elements in correct order """
            return self.data[self.cur:]+self.data[:self.cur]

    def append(self,x):
        """append an element at the end of the buffer"""
        self.data.append(x)
        if len(self.data) == self.max:
            self.cur = 0
            # Permanently change self's class from non-full to full
            self.__class__ = self.__Full

    def get(self):
        """ Return a list of elements from the oldest to the newest. """
        return self.data

# sample usage
if __name__=='__main__':
    x=RingBuffer(5)
    x.append(1); x.append(2); x.append(3); x.append(4)
    print(x.__class__, x.get())
    x.append(5)
    print(x.__class__, x.get())
    x.append(6)
    print(x.data, x.get())
    x.append(7); x.append(8); x.append(9); x.append(10)
    print(x.data, x.get())

La elección de diseño notable en la implementación es que, dado que estos objetos experimentan una transición de estado no reversible en algún momento de su vida útil, de búfer no lleno a búfer completo (y cambios de comportamiento en ese punto), modelé eso cambiando self.__class__. Esto funciona incluso en Python 2.2, siempre que ambas clases tengan los mismos espacios (por ejemplo, funciona bien para dos clases clásicas, como RingBuffer y __Fullen esta receta).

Cambiar la clase de una instancia puede resultar extraño en muchos idiomas, pero es una alternativa pitónica a otras formas de representar cambios de estado ocasionales, masivos, irreversibles y discretos que afectan enormemente el comportamiento, como en esta receta. Menos mal que Python lo admite para todo tipo de clases.

Crédito: Sébastien Keim

d8aninja
fuente
Hice algunas pruebas de velocidad de este vs deque. Esto es aproximadamente 7 veces más lento que deque.
PolyMesh
@PolyMesh increíble, ¡debes avisarle al autor!
d8aninja
1
cual seria el punto de eso? Es un documento publicado antiguo. El objetivo de mi comentario es que los demás sepan que esta respuesta está desactualizada y que usen deque en su lugar.
PolyMesh
@PolyMesh probablemente fue aún más lento cuando lo publicó; Las instrucciones para contactar al autor se encuentran en la introducción al libro. Solo estoy relatando una posible alternativa. Además, "Si solo la velocidad fuera la mejor métrica, por desgracia, puede que solo sea una buena".
d8aninja
3

También puedes ver esta receta de Python bastante antigua .

Aquí está mi propia versión con la matriz NumPy:

#!/usr/bin/env python

import numpy as np

class RingBuffer(object):
    def __init__(self, size_max, default_value=0.0, dtype=float):
        """initialization"""
        self.size_max = size_max

        self._data = np.empty(size_max, dtype=dtype)
        self._data.fill(default_value)

        self.size = 0

    def append(self, value):
        """append an element"""
        self._data = np.roll(self._data, 1)
        self._data[0] = value 

        self.size += 1

        if self.size == self.size_max:
            self.__class__  = RingBufferFull

    def get_all(self):
        """return a list of elements from the oldest to the newest"""
        return(self._data)

    def get_partial(self):
        return(self.get_all()[0:self.size])

    def __getitem__(self, key):
        """get element"""
        return(self._data[key])

    def __repr__(self):
        """return string representation"""
        s = self._data.__repr__()
        s = s + '\t' + str(self.size)
        s = s + '\t' + self.get_all()[::-1].__repr__()
        s = s + '\t' + self.get_partial()[::-1].__repr__()
        return(s)

class RingBufferFull(RingBuffer):
    def append(self, value):
        """append an element when buffer is full"""
        self._data = np.roll(self._data, 1)
        self._data[0] = value
scls
fuente
4
+1 por usar numpy, pero -1 por no implementar un búfer circular. La forma en que lo implementó, está cambiando todos los datos cada vez que agrega un solo elemento, esto cuesta O(n)tiempo. Para implementar un búfer circular adecuado , debe tener tanto un índice como una variable de tamaño, y debe manejar correctamente el caso cuando los datos "envuelven" el final del búfer. Al recuperar datos, es posible que deba concatenar dos secciones al principio y al final del búfer.
Bas Swinckels
2

Éste no requiere biblioteca. Crece una lista y luego se desplaza por índice.

La huella es muy pequeña (sin biblioteca) y se ejecuta al menos el doble de rápido que la eliminación de la cola. De hecho, esto es bueno para calcular promedios móviles, pero tenga en cuenta que los elementos no se mantienen ordenados por edad como se indicó anteriormente.

class CircularBuffer(object):
    def __init__(self, size):
        """initialization"""
        self.index= 0
        self.size= size
        self._data = []

    def record(self, value):
        """append an element"""
        if len(self._data) == self.size:
            self._data[self.index]= value
        else:
            self._data.append(value)
        self.index= (self.index + 1) % self.size

    def __getitem__(self, key):
        """get element by index like a regular array"""
        return(self._data[key])

    def __repr__(self):
        """return string representation"""
        return self._data.__repr__() + ' (' + str(len(self._data))+' items)'

    def get_all(self):
        """return a list of all the elements"""
        return(self._data)

Para obtener el valor promedio, por ejemplo:

q= CircularBuffer(1000000);
for i in range(40000):
    q.record(i);
print "capacity=", q.size
print "stored=", len(q.get_all())
print "average=", sum(q.get_all()) / len(q.get_all())

Resultados en:

capacity= 1000000
stored= 40000
average= 19999

real 0m0.024s
user 0m0.020s
sys  0m0.000s

Esto es aproximadamente 1/3 del tiempo del equivalente con dequeue.

LunaCactus
fuente
1
¿No deberías __getitem__ser un poco más poderoso self._data[(key + self._index + 1) % self._size]?
Mateen Ulhaq
¿Por qué querrías cambiar en +1? Ahora, sí, vea la variante de Basj a continuación para la idea
MoonCactus
1

Tuve este problema antes de hacer programación en serie. En ese momento, hace poco más de un año, tampoco pude encontrar ninguna implementación eficiente, así que terminé escribiendo una como una extensión C y también está disponible en pypi con una licencia del MIT. Es súper básico, solo maneja búferes de caracteres firmados de 8 bits, pero tiene una longitud flexible, por lo que puede usar Struct o algo más si necesita algo más que caracteres. Sin embargo, ahora veo con una búsqueda en Google que hay varias opciones en estos días, por lo que es posible que también desee verlas.

sirlark
fuente
1

Tu respuesta no es correcta. El búfer circular principal tiene dos principios (https://en.wikipedia.org/wiki/Circular_buffer )

  1. Se establece la longitud del búfer;
  2. Primero en llegar y primero en salir;
  3. Cuando agrega o elimina un elemento, los otros elementos no deben mover su posición

su código a continuación:

def add_to_buffer( self, num ):
    self.mylist.pop( 0 )
    self.mylist.append( num )

Consideremos una situación en la que la lista está llena, usando su código:

self.mylist = [1, 2, 3, 4, 5]

ahora agregamos 6, la lista se cambia a

self.mylist = [2, 3, 4, 5, 6]

los elementos que esperan 1 en la lista han cambiado de posición

su código es una cola, no un búfer circular.

La respuesta de Basj, creo que es la más eficaz.

Por cierto, un búfer circular puede mejorar el rendimiento de la operación para agregar un elemento.

Johnny Wong
fuente
1

Desde Github:

class CircularBuffer:

    def __init__(self, size):
        """Store buffer in given storage."""
        self.buffer = [None]*size
        self.low = 0
        self.high = 0
        self.size = size
        self.count = 0

    def isEmpty(self):
        """Determines if buffer is empty."""
        return self.count == 0

    def isFull(self):
        """Determines if buffer is full."""
        return self.count == self.size

    def __len__(self):
        """Returns number of elements in buffer."""
        return self.count

    def add(self, value):
        """Adds value to buffer, overwrite as needed."""
        if self.isFull():
            self.low = (self.low+1) % self.size
        else:
            self.count += 1
        self.buffer[self.high] = value
        self.high = (self.high + 1) % self.size

    def remove(self):
        """Removes oldest value from non-empty buffer."""
        if self.count == 0:
            raise Exception ("Circular Buffer is empty");
        value = self.buffer[self.low]
        self.low = (self.low + 1) % self.size
        self.count -= 1
        return value

    def __iter__(self):
        """Return elements in the circular buffer in order using iterator."""
        idx = self.low
        num = self.count
        while num > 0:
            yield self.buffer[idx]
            idx = (idx + 1) % self.size
            num -= 1

    def __repr__(self):
        """String representation of circular buffer."""
        if self.isEmpty():
            return 'cb:[]'

        return 'cb:[' + ','.join(map(str,self)) + ']'

https://github.com/heineman/python-data-structures/blob/master/2.%20Ubiquitous%20Lists/circBuffer.py

Ijaz Ahmad Khan
fuente
0

La pregunta original era: búfer circular " eficiente ". De acuerdo con esta eficiencia solicitada, la respuesta de aaronasterling parece ser definitivamente correcta. Usando una clase dedicada programada en Python y comparando el procesamiento del tiempo con las colecciones. Aquí hay un código muy simple para probar esto:

class cb:
    def __init__(self, size):
        self.b = [0]*size
        self.i = 0
        self.sz = size
    def append(self, v):
        self.b[self.i] = v
        self.i = (self.i + 1) % self.sz

b = cb(1000)
for i in range(10000):
    b.append(i)
# called 200 times, this lasts 1.097 second on my laptop

from collections import deque
b = deque( [], 1000 )
for i in range(10000):
    b.append(i)
# called 200 times, this lasts 0.211 second on my laptop

Para transformar una deque en una lista, simplemente use:

my_list = [v for v in my_deque]

A continuación, obtendrá O (1) acceso aleatorio a los elementos deque. Por supuesto, esto solo es valioso si necesita hacer muchos accesos aleatorios al deque después de haberlo configurado una vez.

Schmouk
fuente
0

Esto aplica el mismo principio a algunos búferes destinados a contener los mensajes de texto más recientes.

import time
import datetime
import sys, getopt

class textbffr(object):
    def __init__(self, size_max):
        #initialization
        self.posn_max = size_max-1
        self._data = [""]*(size_max)
        self.posn = self.posn_max

    def append(self, value):
        #append an element
        if self.posn == self.posn_max:
            self.posn = 0
            self._data[self.posn] = value   
        else:
            self.posn += 1
            self._data[self.posn] = value

    def __getitem__(self, key):
        #return stored element
        if (key + self.posn+1) > self.posn_max:
            return(self._data[key - (self.posn_max-self.posn)])
        else:
            return(self._data[key + self.posn+1])


def print_bffr(bffr,bffer_max): 
    for ind in range(0,bffer_max):
        stored = bffr[ind]
        if stored != "":
            print(stored)
    print ( '\n' )

def make_time_text(time_value):
    return(str(time_value.month).zfill(2) + str(time_value.day).zfill(2)
      + str(time_value.hour).zfill(2) +  str(time_value.minute).zfill(2)
      + str(time_value.second).zfill(2))


def main(argv):
    #Set things up 
    starttime = datetime.datetime.now()
    log_max = 5
    status_max = 7
    log_bffr = textbffr(log_max)
    status_bffr = textbffr(status_max)
    scan_count = 1

    #Main Loop
    # every 10 secounds write a line with the time and the scan count.
    while True: 

        time_text = make_time_text(datetime.datetime.now())
        #create next messages and store in buffers
        status_bffr.append(str(scan_count).zfill(6) + " :  Status is just fine at : " + time_text)
        log_bffr.append(str(scan_count).zfill(6) + " : " + time_text + " : Logging Text ")

        #print whole buffers so far
        print_bffr(log_bffr,log_max)
        print_bffr(status_bffr,status_max)

        time.sleep(2)
        scan_count += 1 

if __name__ == '__main__':
    main(sys.argv[1:])  
David Torrens
fuente
0

Puede consultar este búfer circular basado en una matriz numérica de tamaño predefinido. La idea es que cree un búfer (asigne memoria para la matriz numpy) y luego la agregue. La inserción y recuperación de datos es muy rápida. He creado este módulo para un propósito similar al que necesita. En mi caso, tengo un dispositivo que genera datos enteros. Leo los datos y los coloco en el búfer circular para futuros análisis y procesamiento.

Valentyn
fuente