buffer circolare efficiente?


109

Voglio creare un buffer circolare efficiente in Python (con l'obiettivo di prendere le medie dei valori interi nel buffer).

È un modo efficiente per utilizzare un elenco per raccogliere valori?

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

Cosa sarebbe più efficiente (e perché)?


Questo non è un modo efficiente per implementare il buffer circolare perché pop (0) è un'operazione O (n) nell'elenco. pop (0) rimuove il primo elemento dalla lista e tutti gli elementi devono essere spostati a sinistra. Utilizza invece collections.deque con l'attributo maxlen. deque ha l'operazione O (1) per append e pop.
Vlad Bezden,

Risposte:


205

Vorrei usare 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)

C'è una ricetta nei documenti per dequequesto è simile a quello che vuoi. La mia affermazione che sia il più efficiente si basa interamente sul fatto che è implementato in C da una squadra incredibilmente qualificata che ha l'abitudine di sfornare codice di prim'ordine.


7
+1 Sì, sono le belle batterie incluse. Le operazioni per il buffer circolare sono O (1) e come dici tu l'overhead extra è in C, quindi dovrebbe essere ancora abbastanza veloce
John La Rooy

7
Non mi piace questa soluzione perché i documenti non garantiscono l'accesso casuale O (1) quando maxlenè definito. O (n) è comprensibile quando dequepuò crescere all'infinito, ma se maxlenè dato, l'indicizzazione di un elemento dovrebbe essere tempo costante.
lvella

1
La mia ipotesi è che sia implementato come un elenco collegato e non come un array.
e-satis

1
Sembra giusto, se i tempi nella mia risposta di seguito sono corretti.
djvg

13

spuntare dalla testa di una lista causa la copia dell'intera lista, quindi è inefficiente

Dovresti invece usare un elenco / array di dimensioni fisse e un indice che si sposta nel buffer mentre aggiungi / rimuovi elementi


4
Essere d'accordo. Non importa quanto possa sembrare elegante o inelegante o qualunque lingua venga utilizzata. In realtà, meno ti preoccupi del garbage collector (o del gestore di heap o dei meccanismi di paging / mapping o qualunque cosa faccia la magia della memoria), meglio è.

@RocketSurgeon Non è magico, è solo che è un array il cui primo elemento viene cancellato. Quindi per un array di dimensione n questo significa n-1 operazioni di copia. Nessun garbage collector o dispositivo simile è coinvolto qui.
Christian

3
Sono d'accordo. Farlo è anche molto più facile di quanto alcune persone pensano. Basta usare un contatore sempre crescente e utilizzare l'operatore modulo (% arraylen) quando si accede all'elemento.
Andre Blum

idem, puoi controllare il mio post sopra, è così che l'ho fatto
MoonCactus

10

Sulla base della risposta di MoonCactus , ecco una circularlistclasse. La differenza con la sua versione è che qui c[0]fornirà sempre l'elemento aggiunto più vecchio, c[-1]l' elemento aggiunto più recente, c[-2]il penultimo ... Questo è più naturale per le applicazioni.

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

Classe:

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

[Modificato]: Aggiunto dataparametro opzionale per consentire l'inizializzazione da elenchi esistenti, ad esempio:

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)

Buona aggiunta. Gli elenchi Python consentono già indici negativi, ma (-1), ad esempio, non restituirebbe il valore atteso una volta che il buffer circolare è pieno, poiché l '"ultima" aggiunta alla lista finisce all'interno della lista.
MoonCactus

1
Funziona @MoonCactus, vedi i 6 esempi che ho dato in cima alla risposta; negli ultimi si vede che c[-1]è sempre l'elemento giusto. __getitem__lo fa bene.
Basj

oh si, voglio dire il mio fallito, non il tuo, scusa: DI renderà il mio commento più chiaro! - oh non posso, il commento è troppo vecchio.
MoonCactus

bella soluzione semplice. ho aggiunto un argomento opzionale per consentire l'inizializzazione della lista dai dati esistenti, è più pythonpathetic in questo modo.
Orwellophile

9

Il deque di Python è lento. Puoi anche usare numpy.roll invece Come ruoti i numeri in un array numpy di forma (n,) o (n, 1)?

In questo benchmark, la deque è di 448 ms. Numpy.roll è 29 ms http://scimusing.wordpress.com/2013/10/25/ring-buffers-in-pythonnumpy/


1
Ma numpy.rollrestituisce una copia dell'array, giusto?
djvg

3
Questa risposta è molto fuorviante: il deque di Python sembra essere abbastanza veloce, ma la conversione da e verso array numpy lo rallenta notevolmente nei benchmark a cui ti colleghi.
xitrium

7

ok con l'uso della deque class, ma per i requerimenti della domanda (media) questa è la mia soluzione:

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

Ottengo TypeError: 'numpy.float64' object is not callablequando provo a chiamare il averagemetodo
scls

Sì ... in effetti immagino che deque utilizzi array numpy internamente (dopo aver rimosso @property funziona bene)
scls

17
Garantisco che deque non usa array numpy internamente. collectionsfa parte della libreria standard, numpynon lo è. Le dipendenze da librerie di terze parti costituirebbero una libreria standard terribile.

6

Sebbene ci sia già un gran numero di ottime risposte qui, non sono riuscito a trovare alcun confronto diretto dei tempi per le opzioni menzionate. Pertanto, per favore trova il mio umile tentativo di confronto qui sotto.

Solo a scopo di test, la classe può alternare tra un listbuffer basato su, un collections.dequebuffer basato su e un Numpy.rollbuffer basato su.

Nota che il updatemetodo aggiunge un solo valore alla volta, per mantenerlo semplice.

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)

Sul mio sistema questo 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)

4

Che ne dici della soluzione del Python Cookbook , inclusa una riclassificazione dell'istanza del ring buffer quando diventa piena?

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 notevole scelta di progettazione nell'implementazione è che, poiché questi oggetti subiscono una transizione di stato irreversibile a un certo punto della loro vita, da buffer non pieno a buffer completo (e il comportamento cambia a quel punto), l'ho modellato cambiando self.__class__. Funziona anche in Python 2.2, purché entrambe le classi abbiano gli stessi slot (ad esempio, funziona bene per due classi classiche, come RingBuffer e __Fullin questa ricetta).

Cambiare la classe di un'istanza può essere strano in molte lingue, ma è un'alternativa pitonica ad altri modi di rappresentare cambiamenti di stato occasionali, massicci, irreversibili e discreti che influenzano ampiamente il comportamento, come in questa ricetta. Meno male che Python lo supporta per tutti i tipi di classi.

Credito: Sébastien Keim


Ho fatto alcuni test di velocità di questo vs deque. Questo è circa 7 volte più lento di deque.
PolyMesh

@PolyMesh fantastico, dovresti avvisare l'autore!
d8aninja

1
quale sarebbe il punto di questo? È un vecchio documento pubblicato. Il punto del mio commento è far sapere agli altri che questa risposta non è aggiornata e utilizzare invece deque.
PolyMesh

@PolyMesh probabilmente era ancora più lento quando lo ha pubblicato; le istruzioni per contattare l'autore si trovano nell'introduzione del libro. Sto solo raccontando una possibile alternativa. Inoltre, "Se solo la velocità fosse la metrica migliore; ahimè potrebbe essere solo una buona metrica".
d8aninja

3

Puoi anche vedere questa ricetta Python piuttosto vecchia .

Ecco la mia versione con array 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

4
+1 per l'utilizzo di numpy, ma -1 per non implementare un buffer circolare. Il modo in cui lo hai implementato, sposti tutti i dati ogni volta che aggiungi un singolo elemento, questo costa O(n)tempo. Per implementare un buffer circolare appropriato , dovresti avere sia un indice che una variabile di dimensione, e devi gestire correttamente il caso quando i dati "avvolgono" la fine del buffer. Durante il recupero dei dati, potrebbe essere necessario concatenare due sezioni all'inizio e alla fine del buffer.
Bas Swinckels

2

Questo non richiede alcuna libreria. Aumenta un elenco e quindi scorre in base all'indice.

L'impronta è molto piccola (nessuna libreria) e funziona almeno il doppio della velocità di dequeue. Questo è utile per calcolare le medie mobili, ma tieni presente che gli elementi non sono ordinati per età come sopra.

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)

Per ottenere il valore medio, ad esempio:

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

Risultati in:

capacity= 1000000
stored= 40000
average= 19999

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

Questo è circa 1/3 del tempo dell'equivalente con dequeue.


1
Non dovresti __getitem__essere un po 'più potente self._data[(key + self._index + 1) % self._size]:?
Mateen Ulhaq

Perché dovresti cambiare di +1? Ora, sì, vedi la variante Basj di seguito per l'idea
MoonCactus

1

Ho avuto questo problema prima di fare la programmazione seriale. All'epoca, poco più di un anno fa, non riuscivo a trovare nemmeno implementazioni efficienti, quindi ho finito per scriverne una come estensione C ed è anche disponibile su pypi con una licenza MIT. È super semplice, gestisce solo buffer di caratteri con segno a 8 bit, ma è di lunghezza flessibile, quindi puoi usare Struct o qualcosa sopra se hai bisogno di qualcosa di diverso dai caratteri. Vedo ora con una ricerca su Google che ci sono diverse opzioni in questi giorni, quindi potresti voler guardare anche quelle.


1

La tua risposta non è giusta. Il buffer circolare principale ha due principi (https://en.wikipedia.org/wiki/Circular_buffer )

  1. La lunghezza del buffer viene impostata;
  2. Il primo che entra è il primo ad uscire;
  3. Quando aggiungi o elimini un elemento, gli altri elementi non dovrebbero spostarsi nella loro posizione

il tuo codice qui sotto:

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

Consideriamo una situazione in cui l'elenco è pieno, utilizzando il tuo codice:

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

ora aggiungiamo 6, l'elenco viene modificato in

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

gli elementi che si aspettano 1 nell'elenco hanno cambiato posizione

il tuo codice è una coda, non un buffer circolare.

La risposta di Basj, credo sia la più efficace.

A proposito, un buffer circolare può migliorare le prestazioni dell'operazione per aggiungere un elemento.


1

Da 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


0

La domanda originale era: buffer circolare " efficiente ". Secondo questa efficienza richiesta, la risposta di aaronasterling sembra essere definitivamente corretta. L'uso di una classe dedicata programmata in Python e il confronto dell'elaborazione del tempo con collections.deque mostra un'accelerazione x5,2 volte con deque! Ecco un codice molto semplice per testarlo:

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

Per trasformare una deque in una lista, usa semplicemente:

my_list = [v for v in my_deque]

Otterrai quindi O (1) accesso casuale agli oggetti deque. Naturalmente, questo è utile solo se è necessario eseguire molti accessi casuali al deque dopo averlo impostato una volta.


0

Si applica lo stesso principio ad alcuni buffer destinati a contenere i messaggi di testo più recenti.

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:])  

0

Puoi controllare questo buffer circolare basato su un array numpy di dimensioni predefinite. L'idea è creare un buffer (allocare memoria per l'array numpy) e successivamente aggiungervi. L'inserimento dei dati e il recupero è molto veloce. Ho creato questo modulo per uno scopo simile a quello di cui hai bisogno. Nel mio caso, ho un dispositivo che genera dati interi. Ho letto i dati e li ho inseriti nel buffer circolare per analisi ed elaborazioni future.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.