假设我有一个对象列表。 (现在一起:“我有一个对象列表。”)在我正在编写的Web应用程序中,每次请求进入时,我会根据未指定的标准选择其中一个对象并使用它来处理请求。基本上是这样的:
def handle_request(req):
for h in handlers:
if h.handles(req):
return h
return None
假设列表中对象的顺序并不重要,我可以通过保持列表排序使得最常用(或可能是最近使用的)对象位于前面来减少不必要的迭代。我知道这不是一件值得关注的事情 - 它只能在应用程序的执行时间内产生微不足道的,无法察觉的差异 - 但调试其余代码会让我发疯,我需要分心:)所以我是求出于好奇心:按照排序顺序维护列表的最有效方法是什么,降序,按每个处理程序的选择次数?
显而易见的解决方案是使handlers
成为(count, handler)
对的列表,每次选择一个处理程序时,递增计数并求助于列表。
def handle_request(req):
for h in handlers[:]:
if h[1].handles(req):
h[0] += 1
handlers.sort(reverse=True)
return h[1]
return None
但是因为最多只有一个元素出现故障,而且我知道它是哪一个,所以似乎应该可以进行某种优化。或许标准库中有什么东西特别适合这项任务吗?还是其他一些数据结构? (即使它没有在Python中实现)或者我应该/可以做一些完全不同的事情吗?
答案 0 :(得分:4)
Python的排序算法timsort
非常神奇:如果列出的列表除了一个元素外,它本身会(发现并)使用这个事实,在O(N)
时间排序。 (Java大师Josh Bloch对于timsort的性能特征的演示印象非常深刻,他开始在他的笔记本电脑上为Java编写代码 - 它很快就会成为Java的标准类型)。我只是在每次定位和增量计数后进行排序,并且非常怀疑其他方法可以击败timsort。
编辑:当然,第一个想到的选择就是可能“向上移动”只是你刚刚递增计数的项目。但首先,进行一些优化以避免复制handlers
...):
def handle_request(req):
for h in handlers:
if h[1].handles(req):
h[0] += 1
handlers.sort(reverse=True)
break
else:
return None
return h[1]
现在,“升级”变种
def handle_request(req):
for i, h in enumerate(handlers):
if h[1].handles(req):
h[0] += 1
for j in reversed(range(i+1)):
if handlers[j][0] <= h[0]:
break
if j < i:
handlers[j+1:i+1] = handlers[j:i]
handlers[j] = h
break
else:
return None
return h[1]
我可以想象这种方法可以节省一点时间的访问模式 - 例如,如果分布如此偏斜以至于大多数命中都在处理程序[0]中,那么除了一次比较之外,这将做很少的工作(而{{ 1}}即使在最好的情况下也需要大约N个。)如果没有代表性的访问模式样本,我无法证实或反驳这一点! - )
答案 1 :(得分:1)
听起来像是优先队列的工作(a.k.a. heapq)。 Python在标准库中将优先级队列实现为heapq。基本上,您将树/堆与最常用的项目或最近使用的项目保持在顶部。
答案 2 :(得分:1)
尽管timsort是神奇的,但使用list.sort()并不是一个好主意,因为(至少)它需要每次比较每对相邻的条目以确保列表按排序顺序。
使用优先级队列(也称为Python的heapq模块)对于许多此类问题来说是一个很好的解决方案,但对于您的应用程序来说并不理想,因为按顺序遍历heapq是很昂贵的。
令人惊讶的是,针对您的情况的最佳方法是使用类似对齐的冒泡排序。由于除了刚才调整的计数器之外的所有条目都是有序的,所以可能发生的是一个条目在列表中向上移动一点。而且由于你只是增加一个,所以它不应该走远。所以只需将它与之前的条目进行比较,如果它们乱序,则将它们交换掉。类似的东西:
def handle_request(req):
for (i, h) in enumerate(handlers):
if h[1].handles(req):
h[0] += 1
while i > 0 and handlers[i][0] > handlers[i-1][0]:
handlers[i-1], handlers[i] = handlers[i], handlers[i-1]
i -= 1
return h[1]
return None
(当然,如果多个线程正在访问处理程序数组,则必须进行某种同步。)
答案 3 :(得分:0)
我猜测所有那些对sort()的额外调用会减慢你的速度,而不是加速你。我的建议是使用这样的包装器(取自here)来记忆handle_request()
class Memoize:
"""Memoize(fn) - an instance which acts like fn but memoizes its arguments
Will only work on functions with non-mutable arguments
"""
def __init__(self, fn):
self.fn = fn
self.memo = {}
def __call__(self, *args):
if not self.memo.has_key(args):
self.memo[args] = self.fn(*args)
return self.memo[args]
你可以像这样使用它:
handle_request = Memoize(handle_request)
这将导致handle_request的各种返回值被缓存,并且实际上可以提供明显的加速。我建议您尝试使用Memoize()在应用程序中包装各种函数,以查看它占用了多少内存以及它加速(或不加速)各种函数的程度。你也可以使用类似的方法记住你的.handles()方法(例如,有一个memoizing decorator here)。
答案 4 :(得分:-1)
这是我用来解决这个问题的一些代码(虽然现在我读了其他答案,我想知道heapq会更好):
class MRUSortedIterable:
def __init__(self, data):
self._data = list(data)
self._i = 0
def __iter__(self):
if self._i: # if previous use had a success, move to top
self._data[0], self._data[1:self._i+1] = self._data[self._i], self._data[0:self._i]
for self._i, value in enumerate(self._data):
yield value
self._i = 0 # reset on exhaustion (ie failed to find what we wanted)
您可以像这样使用它(例如):
MY_DATA = MRUSortedIterable(a_list_of_objects)
...
def handler(criteria):
for data in MY_DATA:
if test(data, criteria):
return data
并根据需要自动重新排列基础数据以使最近使用的项目位于顶部(重新安排实际上是在处理下一个请求时完成的)。唯一的要求是您在成功时停止迭代数据(并在失败时使用所有数据)。
重要提示:这非常不线程安全(这可能是2年前您的Web服务器出现问题)。但它是,imho,非常整洁......
反射时,这是MRU,而具有访问计数的heapq将按总使用次序排序。所以他们的表现可能略有不同(如果访问模式不变,heapq可能会更好)。