効率的な循環バッファーを作成したいPythonで(内の整数値の平均を取ることを目的としています)。
これは、リストを使用して値を収集する効率的な方法ですか?
def add_to_buffer( self, num ):
self.mylist.pop( 0 )
self.mylist.append( num )
何がより効率的でしょうか(そしてその理由)?
効率的な循環バッファーを作成したいPythonで(内の整数値の平均を取ることを目的としています)。
これは、リストを使用して値を収集する効率的な方法ですか?
def add_to_buffer( self, num ):
self.mylist.pop( 0 )
self.mylist.append( num )
何がより効率的でしょうか(そしてその理由)?
回答:
引数を使用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で実装されているという事実に完全に依存しています。
maxlen
が定義されている場合、ドキュメントはO(1)ランダムアクセスを保証しないため、このソリューションは好きではありません。O(n)はdeque
、が無限に成長できる場合に理解できますが、maxlen
指定されている場合、要素のインデックス付けは一定の時間である必要があります。
リストの先頭からポップすると、リスト全体がコピーされるため、非効率的です
代わりに、固定サイズのリスト/配列と、項目を追加/削除するときにバッファーを移動するインデックスを使用する必要があります
MoonCactusの回答に基づいて、ここにcircularlist
クラスがあります。彼のバージョンとの違いは、ここで c[0]
は常に最も古いc[-1]
要素、c[-2]
最後に追加された要素、最後から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)
c[-1]
は、常に正しい要素であることがわかります。__getitem__
それは正しく行います。
Pythonの両端キューは遅いです。代わりにnumpy.rollを使用することもでき ます。形状(n、)または(n、1)のnumpy配列の数値をどのように回転させるのですか?
このベンチマークでは、dequeは448msです。Numpy.rollは29ミリ秒です http://scimusing.wordpress.com/2013/10/25/ring-buffers-in-pythonnumpy/
numpy.roll
、配列のコピーを返しますよね?
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
の方法を
ここにはすでに多くの素晴らしい答えがありますが、言及されたオプションのタイミングを直接比較することはできませんでした。したがって、下の比較で私の控えめな試みを見つけてください。
テストのみを目的として、クラスは、list
ベースのバッファ、collections.deque
ベースのバッファ、およびベースのバッファを切り替えることができNumpy.roll
ます。
このupdate
メソッドは、簡単にするために、一度に1つの値のみを追加することに注意してください。
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)
リングバッファーインスタンスがいっぱいになったときの再分類を含む、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の代替手段です。Pythonがあらゆる種類のクラスでそれをサポートしているのは良いことです。
クレジット:SébastienKeim
このかなり古い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
O(n)
。適切な循環バッファーを実装するには、インデックスとサイズ変数の両方が必要です。また、データがバッファーの最後を「折り返す」場合を正しく処理する必要があります。データを取得するとき、バッファの最初と最後で2つのセクションを連結しなければならない場合があります。
これはライブラリを必要としません。リストが大きくなり、インデックスごとに循環します。
フットプリントは非常に小さく(ライブラリなし)、少なくともデキューの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です。
__getitem__
もう少し強力で はないself._data[(key + self._index + 1) % self._size]
でしょうか?
あなたの答えは正しくありません。循環バッファーのメインには2つの原則があります(https://en.wikipedia.org/wiki/Circular_buffer)
以下のコード:
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を期待する項目は、位置が変更されました
コードはキューであり、サークルバッファではありません。
Basjの答えは、私が最も効率的なものだと思います。
ちなみに、サークルバッファーは、アイテムを追加する操作のパフォーマンスを向上させることができます。
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
元の質問は、「効率的な」循環バッファでした。求められたこの効率性によると、aaronasterlingからの答えは間違いなく正しいようです。Pythonでプログラムされた専用クラスを使用して、時間処理とcollections.dequeを比較すると、dequeでx5.2倍の高速化が示されます!これをテストするための非常に単純なコードを次に示します。
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]
次に、dequeアイテムへのO(1)ランダムアクセスを取得します。もちろん、これは、1回設定した後に両端キューにランダムアクセスを何度も行う必要がある場合にのみ価値があります。
これは、最新のテキストメッセージを保持することを目的とした一部のバッファに同じプリンシパルを適用しています。
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:])