web-dev-qa-db-fra.com

tampon circulaire efficace?

Je veux créer un efficace buffer circulaire en python (dans le but de prendre des moyennes des valeurs entières dans le tampon).

Est-ce un moyen efficace d’utiliser une liste pour collecter des valeurs?

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

Qu'est-ce qui serait plus efficace (et pourquoi)?

84
jedierikb

J'utiliserais collections.deque avec un maxlen arg

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

Il y a une recette dans la documentation pour deque qui est similaire à ce que vous voulez. Mon affirmation selon laquelle c’est le plus efficace repose entièrement sur le fait qu’elle est mise en œuvre en C par un équipage incroyablement qualifié qui a l’habitude de produire du code de premier ordre.

166
aaronasterling

sauter de la tête d'une liste provoque la copie de toute la liste, ce qui est inefficace

Vous devriez plutôt utiliser une liste/un tableau de taille fixe et un index qui se déplace dans le tampon lorsque vous ajoutez/supprimez des éléments

11
John La Rooy

La deque de Python est lente. Vous pouvez également utiliser numpy.roll à la place Comment faire pivoter les nombres dans un tableau numpy de forme (n,) ou (n, 1)?

Dans ce repère, deque est 448ms. Numpy.roll est 29ms http://scimusing.wordpress.com/2013/10/25/ring-buffers-in-pythonnumpy/

8
Orvar Korvar

ok avec l'utilisation de deque classe, mais pour les exigences de la question (moyenne) c'est ma solution:

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

D'après la réponse de MoonCactus , voici une classe circularlist. La différence avec sa version est qu'ici c[0] donnera toujours l'élément le plus ancien ajouté, c[-1] l'élément le plus récent ajouté, c[-2] l'avant-dernier ... C'est plus naturel pour les applications.

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

Classe:

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

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

Vous pouvez aussi voir cette recette assez ancienne Python .

Voici ma propre version avec le tableau 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
3
scls

Que diriez-vous de la solution du livre de recettes Python , incluant un reclassement de l’instance de tampon circulaire lorsqu’elle est pleine?

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

Le choix de conception notable dans la mise en œuvre est que, depuis ceux-ci les objets subissent une transition d'état irréversible à un moment donné de leur durée de vie - du tampon non plein au tampon complet (et le comportement change à ce moment-là) - j'ai modélisé cela en modifiant self.__class__. Cela fonctionne même en Python 2.2, tant que les deux classes ont le même slots (par exemple, cela fonctionne très bien pour deux classes classiques, telles que RingBuffer et __Full dans cette recette).

Changer la classe d'une instance peut être étrange dans de nombreuses langues, mais c’est une alternative pythonique aux autres manières de représenter changements d'état occasionnels, massifs, irréversibles et discrets qui influer grandement sur le comportement, comme dans cette recette. C'est une bonne chose que Python le supporte pour toutes sortes de cours.

Crédit: Sébastien Keim

2
d8aninja

Celui-ci ne nécessite aucune bibliothèque. Il fait grandir une liste puis parcourt l'intérieur par index.

L'empreinte est très petite (pas de bibliothèque) et elle est au moins deux fois plus rapide que la file d'attente. C'est bien de calculer des moyennes mobiles, mais sachez que les éléments ne sont pas triés par âge comme ci-dessus.

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)

Pour obtenir la valeur moyenne, par exemple:

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

Résulte en:

capacity= 1000000
stored= 40000
average= 19999

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

C'est environ le tiers du temps de l'équivalent avec dequeue.

2
MoonCactus

Bien qu'il y ait déjà un grand nombre d'excellentes réponses ici, je n'ai pu trouver aucune comparaison directe des horaires pour les options mentionnées. Par conséquent, veuillez trouver mon humble tentative de comparaison ci-dessous. 

À des fins de test uniquement, la classe peut basculer entre un tampon basé sur list, un tampon basé sur collections.deque et un tampon basé sur Numpy.roll.

Notez que la méthode update ajoute une seule valeur à la fois, pour que cela reste 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)

Sur mon système, cela donne:

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

J'ai eu ce problème avant de faire de la programmation en série. À l'époque, il y a un peu plus d'un an, je ne pouvais pas trouver d'implémentations efficaces. J'ai donc écrit one sous la forme d'une extension C , qui est également disponible sur pypi sous MIT Licence. Super basique, il ne gère que les tampons de caractères signés sur 8 bits, mais sa longueur est flexible. Vous pouvez donc utiliser Struct ou quelque chose au-dessus de celui-ci si vous avez besoin de quelque chose d'autre que des caractères. Je vois maintenant avec une recherche Google qu'il existe plusieurs options ces jours-ci, alors vous voudrez peut-être aussi les examiner.

1
sirlark

De 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

1
Ijaz Ahmad Khan

La question initiale était: " efficient " circular buffer . Selon cette efficacité demandée, la réponse de aaronasterling semble être définitivement correcte . Utiliser une classe dédiée programmée en Python et comparer le traitement du temps avec collections.deque montre une accélération de 5.2 fois avec deque! Voici un code très simple pour le tester:

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

Pour transformer un deque en une liste, utilisez simplement:

my_list = [v for v in my_deque]

Vous obtiendrez alors O(1) un accès aléatoire aux éléments de deque. Bien entendu, cela n’est utile que si vous devez effectuer de nombreux accès aléatoires à la deque après l’avoir définie une fois.

0
Schmouk

Ceci applique le même principe à certains tampons destinés à contenir les messages texte les plus récents.

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 __== '__main__':
    main(sys.argv[1:])  
0
David Torrens

Vous répondez est faux . Le tampon circulaire principal a deux principes ( https://en.wikipedia.org/wiki/Circular_buffer )

  1. La longueur de la mémoire tampon est réglée;
  2. Premier entré, premier sorti;
  3. Lorsque vous ajoutez ou supprimez un élément, les autres éléments ne doivent pas se déplacer

votre code ci-dessous:

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

Considérons une situation où la liste est pleine, en utilisant votre code:

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

maintenant, nous ajoutons 6, la liste est modifiée en

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

les éléments attendus 1 dans la liste a changé de position

votre code est une file d'attente, pas un tampon circulaire.

Je pense que la réponse de Basj est la plus efficace.

Soit dit en passant, un tampon circulaire peut améliorer les performances de l’opération Pour ajouter un élément.

0
Johnny Wong