在Python中维护有序访问计数列表的有效方法

时间:2010-06-08 01:35:12

标签: python optimization sorting list

假设我有一个对象列表。 (现在一起:“我有一个对象列表。”)在我正在编写的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中实现)或者我应该/可以做一些完全不同的事情吗?

5 个答案:

答案 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可能会更好)。