【问题标题】:Python: fastest way to extract sublist from a list of objects given an attributePython:从给定属性的对象列表中提取子列表的最快方法
【发布时间】:2018-03-20 15:35:09
【问题描述】:

假设我有这个简单的类:

class Foo(object):
    def __init__(self, number, name):
        self.number = number
        self.name = name

以及 Foo 实例的列表:

l = [Foo(10, 'a'), Foo(9, 'a'), Foo(8, 'a'), Foo(7,'a'), Foo (5, 'b'), Foo (4, 'b') ,Foo (3, 'b')]

假设“name”属性只能是“a”或“b”。

提取“name”为“a”(或“b”)的所有对象的子列表的最快方法是什么?请注意,此操作可能会被调用数百万次,这就是我想尽可能优化它的原因。

请注意,列表的构建方式使得列表的前半部分或后半部分中的所有元素都“组合在一起”。该列表是对称的,并按递减属性“数字”排序。 编辑:不一定有相同数量的“a”和“b”。


我是怎么做的:

一开始我只是在做一个for循环:

sublist = []
for o in l:
  if o.name == 'a'
  sublist.append(o)

然后我尝试了列表理解:

sublist = [o for o in l if o.name=='a']

但这似乎大致相同,如果不是慢一点的话。

无论哪种方式,这些都没有利用所有属性已经在原始(排序)列表中“组合在一起”的假设。即使不再需要它也会继续循环。速度非常重要,所以我需要它尽可能地高效。

【问题讨论】:

  • 如果不遍历整个列表,就无法对所有需要的元素进行分组。我只能建议您在允许的情况下使用生成器。
  • 如果只有 'a' 和 'b' 值,并且 'a' 总是在前,那么你可以使用二分搜索找到最后一个 'a' 元素,然后使用切片来获取所有 ' a' 元素。 O(logN) 找到最后一个 'a' 元素,但它仍然是 O(N) 来制作列表的副本
  • 它们是按字典顺序排列的还是只是成簇的?你能优化更高吗?在建立清单之前?这种数据结构不适合这类事情。
  • 好吧,我完全错过了'a'和'b'元素的数量相同。二进制搜索在这里没用,因为您可以按照 Shaido 的建议在 O(1) 中找到中间点
  • 那么天真的方法(带循环)应该不超过 1 秒。您确定这是优化的正确位置吗?另外,正如我已经说过的,如果你想复制这个列表,无论你能多快找到最后一个“a”元素,它仍然会花费 O(N)。不过,我正在准备完整的答案。

标签: python performance list


【解决方案1】:

使用二分查找O(logN)中的中间点:

In [19]: class Foo(object):
    ...:     def __init__(self, number, name):
    ...:         self.number = number
    ...:         self.name = name
    ...:         
    ...:     def __repr__(self):
    ...:         return 'Foo(number={self.number}, name={self.name})'.format(self=self)
    ...:     

In [20]: def binary_search(lst, predicate):
    ...:     """
    ...:     Finds the first element for which predicate(x) == True
    ...:     """
    ...:     lo, hi = 0, len(lst)
    ...:     while lo < hi:
    ...:         mid = (lo + hi) // 2
    ...:         if predicate(lst[mid]):
    ...:             hi = mid
    ...:         else:
    ...:             lo = mid + 1
    ...:     return lo
    ...: 

In [21]: l = [Foo(10, 'a'), Foo(9, 'a'), Foo(8, 'a'), Foo(7,'a'), Foo (5, 'b'), Foo (4, 'b'
    ...: ) ,Foo (3, 'b')]

In [22]: binary_search(l, lambda x: x.name == 'b')
Out[22]: 4

In [23]: l[:binary_search(l, lambda x: x.name == 'b')]
Out[23]: 
[Foo(number=10, name=a),
 Foo(number=9, name=a),
 Foo(number=8, name=a),
 Foo(number=7, name=a)]

In [24]: l[binary_search(l, lambda x: x.name == 'b'):]
Out[24]: [Foo(number=5, name=b), Foo(number=4, name=b), Foo(number=3, name=b)]

但是,请注意:

  1. 对于 104 个元素,复杂度为 O(N) 的简单方法应在 1 秒内完成。
  2. 在制作副本时,您仍然需要遍历数组,这会导致 O(N)
  3. 如果您遇到性能问题,最好使用分析器来查找程序中的瓶颈。迭代 104 元素通常不是瓶颈(除非你在 104 元素上迭代 104 次 - 结果是 108)。但是,从 db 查询 104 可能是一个瓶颈,因为它也使用网络,可能会查询其他项目等等。如有疑问 - 使用分析器

【讨论】:

  • 谢谢,二分搜索有很大的不同。
【解决方案2】:

一旦匹配后遇到不匹配,就跳出循环

sublist = []
for o in l:
    if o.name == 'a'
        sublist.append(o)
    elif sublist:
        break

如果您想使用生成器,可以使用 itertools 函数

from itertools import takewhile, dropwhile

sublist = list(takewhile(lambda o: o.name == 'a', dropwhile(lambda o: o.name != 'a', l))

这些都利用了列表已排序的事实,并在项目停止匹配后停止处理列表。

【讨论】:

    【解决方案3】:

    由于name 属性只能是有序的“a”或“b”,并且“a”和“b”的数量相同,因此最简单的方法是找到中间点并切片列表:

    mid = int(len(aList)/2)
    sublist = l[:mid]
    

    上面将给你所有'a',而l[mid:]给你所有'b'。


    编辑:由于问题已更改,并且“a”和“b”的元素数量不再相同,因此上述答案不再有效。

    根据列表的长度,我的猜测是二分搜索(对于较长的列表)或 Brendan 建议的跳出循环(对于较短的列表)将是最快的方法。

    【讨论】:

    • 正是我刚刚意识到的!把它写下来很有帮助,当你发布你的答案时,我正要更新。
    • 不幸的是,我也刚刚发现可能并不总是相同,因此无法正常工作。无论如何都赞成,因为它与我的原始答案一致。不过必须解决非对称情况。
    猜你喜欢
    • 2021-01-18
    • 2011-02-13
    • 2016-07-12
    • 1970-01-01
    • 2015-02-05
    • 2012-09-24
    • 2010-10-15
    • 1970-01-01
    相关资源
    最近更新 更多