【问题标题】:Efficient way to maintain a sorted list of access counts in Python在 Python 中维护访问计数的排序列表的有效方法
【发布时间】:2010-06-08 01:35:12
【问题描述】:

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

【问题讨论】:

    标签: python optimization sorting list


    【解决方案1】:

    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] 中,那么除了一个比较之外,这将几乎没有什么工作(而 @987654327 @ 即使在最好的情况下也需要大约 N 个)。如果没有您的访问模式的代表性样本,我无法证实或反驳这一点!-)

    【讨论】:

    • 酷,我不知道!总有一天我将不得不查看实现(从而从我的实际工作中转移更多时间)。
    • @David,除了来源,请务必查看svn.python.org/view/python/trunk/Objects/… 的文字说明。
    • 关于您的编辑:我认为这将是有效的,因为在我的测试中,不同的处理程序对应于不同的 URL,并且大多数命中往往只针对几个不同的 URL(如图像和样式表)。但我不确定这种模式是否普遍存在。
    • @David,不,它通常不成立 - 不同应用程序和用例的访问模式无处不在(有时会随着时间而变化,因此计算很久以前的命中率因为在这种情况下,新的可能会损害性能——点击次数可能需要随着时间的推移逐渐减少,最近次点击占主导地位)。调整这种结构以获得最佳性能需要访问特定应用程序的跟踪/日志,这些跟踪/日志被认为可以很好地代表现实生活中的流量。
    • 我知道,我的意思是“一般”在我的网站的上下文中,这是我将部署它的第一个地方。不幸的是,我的日志分析器出了点问题,因此我无法立即访问有关各种 URL 的相对访问频率的统计信息。
    【解决方案2】:

    听起来像是优先队列(又名 heapq)的工作。 Python 在标准库中有一个优先级队列实现为heapq。基本上,您保留一个树/堆,其中最常使用的项目或最近使用的项目位于顶部。

    【讨论】:

      【解决方案3】:

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

      (当然,如果多个线程正在访问 handlers 数组,您必须进行某种同步。)

      【讨论】:

      • 我在写这个问题时也是这么想的……+1 实现。
      【解决方案4】:

      我猜对 sort() 的所有额外调用都会减慢您的速度,而不是加快您的速度。我的建议是使用这样的包装器来记忆 handle_request() (取自here

      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() 方法(例如,有一个记忆装饰器 here)。

      【讨论】:

      • 我实际上已经在这个应用程序中记住了几个函数,但不是handle_request,因为它永远不会用相同的参数调用两次。而handles 也不是记忆化的好选择,因为这些函数非常简单(同样,永远不会出现两次相同的参数)。
      【解决方案5】:

      这是我用来解决这个问题的一些代码(虽然现在我阅读了其他答案,但我想知道 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 服务器的问题)。但它,恕我直言,非常整洁......

      经过反思,这是 MRU,而具有访问计数的 heapq 将按总使用量排序。因此它们的性能可能略有不同(如果访问模式不变,heapq 可能会更好)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-05-24
        • 1970-01-01
        • 2014-12-11
        • 2016-01-18
        • 2013-02-13
        • 1970-01-01
        • 2012-12-09
        • 1970-01-01
        相关资源
        最近更新 更多