ефективний круговий буфер?


109

Я хочу створити ефективний круговий буфер у python (з метою взяття середніх значень цілих значень у буфері).

Це ефективний спосіб використання списку для збору значень?

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

Що було б ефективнішим (і чому)?


Це не ефективний спосіб реалізації кругового буфера, оскільки pop (0) є операцією O (n) у списку. pop (0) видаляє перший елемент зі списку, і всі елементи повинні бути зміщені вліво. Використовуйте замість collection.deque з атрибутом maxlen. deque має операцію O (1) для додавання та поп.
Влад Безден

Відповіді:


205

Я б використав collections.dequeз maxlenарг

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

Існує рецепт в документації для , dequeякий схожий на те , що ви хочете. Моє твердження, що це найефективніше, повністю ґрунтується на тому, що він реалізується в C неймовірно кваліфікованою командою, яка має звичку викручувати першокласний код.


7
+1 Так, це приємні батареї, включені. Операції для кругового буфера - це O (1), і, як ви кажете, додаткові накладні витрати є в C, тому все ще слід бути досить швидким
Джон Ла Рой

7
Мені це рішення не подобається, тому що документи не гарантують O (1) випадковий доступ, коли maxlenвизначено. O (n) зрозуміло, коли dequeможе зростати до нескінченності, але якщо maxlenце дано, індексація елемента має бути постійним часом.
lvella

1
Я здогадуюсь, що його реалізовано як пов'язаний список, а не масив.
e-satis

1
Здається, що правильно, якщо моменти в моїй відповіді нижче правильні.
djvg

13

вискакування з голови списку призводить до того, що весь список буде скопійовано, тому це неефективно

Натомість слід використовувати список / масив фіксованого розміру та індекс, який переміщується через буфер під час додавання / видалення елементів


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

@RocketSurgeon Це не магія, це просто масив, перший елемент якого видалено. Отже, для масиву розміром n це означає n-1 копіювання операцій. Тут не бере участь сміттєзбірник чи подібний пристрій.
Крістіан

3
Я згоден. Зробити це також набагато простіше, ніж деякі люди думають. Просто використовуйте постійно зростаючий лічильник і використовуйте оператор модуля (% масив) під час доступу до елемента.
Андре Блюм

Ідеме, ви можете перевірити мою публікацію вище, ось як я це зробив
MoonCactus

10

Виходячи з відповіді MoonCactus , ось circularlistклас. Відмінність його версії полягає в тому, що тут c[0]завжди буде наданий найдавніший доданий елемент, c[-1]останній доданий елемент, c[-2]передостанній ... Це більш природно для додатків.

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

Клас:

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

[Відредаговано]: Додано додатковий dataпараметр, щоб дозволити ініціалізацію з існуючих списків, наприклад:

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)

Гарне доповнення. Списки Python вже дозволяють негативні індекси, але (-1), наприклад, не повертають очікуваного значення, коли круговий буфер буде заповнений, оскільки "останнє" доповнення до списку закінчується в списку.
MoonCactus

1
Це спрацьовує @MoonCactus, дивіться 6 прикладів, які я дав на вершині відповіді; в останніх ви бачите, що c[-1]це завжди правильний елемент. __getitem__чи правильно.
Бась

о так, я маю на увазі мою невдачу, не вашу, вибачте: DI зробить мій коментар яснішим! - О, я не можу, коментар занадто старий.
MoonCactus

приємне просте рішення. я додав необов'язковий аргумент, щоб дозволити ініціалізацію списку з існуючих даних, це більш пітопатично.
Орвелофіл

9

Дека Пітона йде повільно. Ви також можете використовувати numpy.roll замість того, як ви обертаєте числа в масивному масиві фігур (n,) або (n, 1)?

У цьому орієнтирі деке 448 мс. Numpy.roll складає 29 мс http://scimusing.wordpress.com/2013/10/25/ring-buffers-in-pythonnumpy/


1
Але numpy.rollповертає копію масиву, правда?
djvg

3
Ця відповідь є дуже оманливою - дека Python здається досить швидкою, але перетворення з масивів на масиви та їх уповільнення значно уповільнюють показники, на які ви посилаєтесь.
xitrium

7

добре з використанням класу deque, але для запитань (середнього) це моє рішення:

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

Я отримую, TypeError: 'numpy.float64' object is not callableколи намагаюся викликати averageметод
scls

Так ... насправді я думаю, що deque використовує внутрішньо масивні масиви (після вилучення @property це працює добре)
scls

17
Я гарантую, що deque не використовує внутрішні масиви. collectionsє частиною стандартної бібліотеки, numpyні. Залежність від сторонніх бібліотек спричинила б за собою жахливу стандартну бібліотеку.

6

Хоча тут вже існує велика кількість чудових відповідей, я не зміг знайти жодного прямого порівняння термінів для згаданих варіантів. Тому, будь ласка, знайдіть мою скромну спробу порівняння нижче.

Тільки для цілей тестування клас може перемикатися між listбуфером, collections.dequeзаснованим на базі, буфером на основі бази та буфером на Numpy.rollоснові бази.

Зауважте, що updateметод додає лише одне значення за один раз, щоб зробити його простим.

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)

У моїй системі це дає:

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

Як щодо рішення з кулінарної книги Python , включаючи перекласифікацію екземпляра буфера кільця, коли він стає повним?

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

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

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

Кредит: Себастьян Кейм


Я зробив кілька тестів на швидкість цього vs deque. Це приблизно в 7 разів повільніше, ніж деке.
PolyMesh

@PolyMesh дивним, ви повинні повідомити автора про це!
d8aninja

1
який би був сенс цього? Це старий опублікований документ. Сенс мого коментаря полягає в тому, щоб повідомити іншим, що ця відповідь застаріла, і використовувати замість неї deque.
PolyMesh

@PolyMesh, мабуть, все-таки повільніше, коли він опублікував це; вказівки щодо контакту з автором містяться у вступі до книги. Я просто пов'язую одну, можливу альтернативу. Крім того, "Якби швидкість була найкращою метрикою; на жаль, вона може бути лише хорошою".
d8aninja

3

Ви також можете побачити цей досить старий рецепт Python .

Ось моя власна версія з масивом 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 для використання numpy, але -1 для не застосовування кругового буфера. Як ви це реалізували, ви переміщуєте всі дані щоразу, коли додаєте один елемент, це коштує O(n)часу. Щоб реалізувати правильний круговий буфер , ви повинні мати як індекс, так і змінну розміру, і вам потрібно правильно обробити випадок, коли дані "обертаються навколо" кінця буфера. Під час отримання даних вам, можливо, доведеться об'єднати два розділи на початку та в кінці буфера.
Bas Swinckels

2

Для цього не потрібна бібліотека. Він збільшує список, а потім здійснює цикл за індексом.

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

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)

Щоб отримати середнє значення, наприклад:

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

Призводить до:

capacity= 1000000
stored= 40000
average= 19999

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

Це приблизно 1/3 часу еквівалента декуе.


1
Чи не повинен бути твій __getitem__трохи потужнішим self._data[(key + self._index + 1) % self._size]:?
Мейтін Ульхак

Чому ви хочете перейти на +1? Тепер так, дивіться варіант Basj нижче для ідеї
MoonCactus

1

У мене була ця проблема перед тим, як робити серійне програмування. У той час, трохи більше року тому, я не міг знайти жодної ефективної реалізації, тому я закінчив писати її як розширення C, і він також доступний на pypi під ліцензією MIT. Це супер базовий, обробляє лише буфери 8-бітових знаків, підписаних, але він має гнучку довжину, тому ви можете використовувати Struct або щось над ним, якщо вам потрібно щось, крім символів. Зараз я знаю, що в пошуку Google є кілька варіантів, хоча сьогодні є кілька варіантів, і ви, можливо, також захочете їх переглянути.


1

Ви відповідаєте неправильно. Основні кругові буфери мають два принципи (https://en.wikipedia.org/wiki/Circular_buffer )

  1. Встановлюється тривалість буфера;
  2. Перший у першому виході;
  3. Коли ви додаєте або видаляєте елемент, інші елементи не повинні переміщувати своє положення

ваш код нижче:

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

Розглянемо ситуацію, коли список заповнений, використовуючи ваш код:

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

тепер додаємо 6, список змінено на

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

елементи, які очікують, 1 у списку змінили свою позицію

ваш код - це черга, а не буфер кола.

Відповідь Бася, я думаю, є найбільш ефективною.

До речі, буфер кола може імпортувати виконання операції для додавання елемента.


1

Від 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

Первісне питання було: " ефективний " круговий буфер. Відповідно до цієї ефективності, яку вимагають, відповідь від ааронастерлінгу видається остаточно правильною. Використання виділеного класу, запрограмованого на Python, та порівняння обробки часу з collection.deque показує прискорення x5,2 раза з deque! Ось дуже простий код для перевірки цього:

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

Щоб перетворити деке в список, просто використовуйте:

my_list = [v for v in my_deque]

Потім ви отримаєте O (1) випадковий доступ до предметів декету. Звичайно, це цінно лише в тому випадку, якщо вам потрібно зробити багато випадкових звернень до деке після того, як один раз встановили її.


0

Це стосується тієї ж основної частини до деяких буферів, призначених для зберігання останніх текстових повідомлень.

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

Ви можете перевірити цей круговий буфер на основі заданого масиву numpy масиву. Ідея полягає у тому, щоб ви створили буфер (виділили пам’ять для масиву numpy) і пізніше додали до нього. Вставлення даних і пошук дуже швидко. Я створив цей модуль з аналогічною метою, як вам потрібно. У моєму випадку у мене є пристрій, який генерує цілі дані. Я прочитав дані і помістив їх у круговий буфер для подальшого аналізу та обробки.

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