【问题标题】:How to speed up list comprehension如何加快列表理解
【发布时间】:2013-10-16 21:13:33
【问题描述】:

以下是我的清单:

col = [['red', 'yellow', 'blue', 'red', 'green', 'yellow'],
       ['pink', 'orange', 'brown', 'pink', 'brown']
      ]

我的目标是消除在每个列表中出现一次的项目。

这是我的代码:

eliminate = [[w for w in c if c.count(w)>1]for c in col]

Output: [['red', 'red', 'yellow','yellow'], ['pink','pink', 'brown','brown']]

该代码适用于上述列表等小型数据集,但是,我的数据集非常大。每个列表最多包含 1000 个项目。

有没有办法让上面的代码运行得更快?就像将代码分解为两个或多个 for 循环一样,因为我的理解是普通的 for 循环比列表理解要快。

有什么建议吗?谢谢。

【问题讨论】:

  • 您好Martijin,感谢您的回复。那么问题在于速度(复杂性)而不是重复。
  • 顺序不重要
  • 在所有情况下,我都测量了列表理解总是比标准 for 循环快的时间。您可以通过在循环外生成列表项的计数器来加快此速度。
  • 是的,包含多个元素的项目。问题中的代码可以解决这个问题,但考虑到我的数据量,速度方面的效率非常重要。
  • @Tiger1:那我也更新了答案。

标签: python list python-2.7 list-comprehension


【解决方案1】:

我的理解是普通的for循环比列表理解要快。

不。

您的循环很慢,因为它重复了操作。对于col 中每个嵌套列表中的每个字符串,它都会计算该字符串的实例数,因此对于col 中的每个c,它都会执行len(c)**2 比较。这是一个O(NM^2) 平方算法。这很快就会变慢。

为了加快速度,请使用更好的数据结构。使用collections.Counter

【讨论】:

  • 嗨,Marcin,您的解决方案运行良好。我使用了 collections.Counter,它的速度快了 100 倍。
【解决方案2】:

我会尝试使用OrderedCounter 来避免重复的.count() 调用:

from collections import OrderedDict, Counter

col=[['red', 'yellow', 'blue', 'red', 'green', 'yellow'],['pink', 'orange', 'brown', 'pink', 'brown']]

class OrderedCounter(Counter, OrderedDict):
    pass

new = [[k for k, v in OrderedCounter(el).iteritems() if v != 1] for el in col]
# [['red', 'yellow'], ['pink', 'brown']]

如果我们只想迭代一次,那么(类似于 Martijn 的——加上少玩集合):

from itertools import count
def unique_plurals(iterable):
    seen = {}
    return [el for el in iterable if next(seen.setdefault(el, count())) == 1]

new = map(unique_plurals, col)

这在指定需要出现的次数方面更加灵活,并且保留一个 dict 而不是多个 sets。

【讨论】:

  • 很好地使用了继承。根据 cmets 的说法,顺序无所谓,但我还是喜欢这个。
  • 我只是用一个经典的 for 循环写的。我向你的天才低头。
  • @Marcin 我没有注意到顺序并不重要 - 正在查看示例输出 - 而不是 cmets。无论如何,这里的OrderedCounter 刚刚从docs.python.org/2/library/… 列出的食谱末尾提升(减去repr 和reduce)@
  • 我理解OrderedCounter 继承了CounterOrderedDict,但我无法理解你的第一个技巧(我累了)这是如何工作的?能不能给点提示。谢谢!
  • 我想知道我知道了OrderedCounter(el)returns 一个 ordered 字典,它既是 Counter (表示关键频率对)。我不明白你为什么要创建一个类OrderedCounter(Counter, OrderedDict):
【解决方案3】:

不要使用.count(),因为它会扫描您的列表每个元素。此外,如果项目在输入中出现 3 次或更多次,它会将项目多次添加到输出中。

你最好在这里使用一个生成器函数,它只生成它以前见过的项目,但只生成一次:

def unique_plurals(lst):
    seen, seen_twice = set(), set()
    seen_add, seen_twice_add = seen.add, seen_twice.add
    for item in lst:
        if item in seen and item not in seen_twice:
            seen_twice_add(item)
            yield item
            continue
        seen_add(item)

[list(unique_plurals(c)) for c in col]

这对每个列表仅迭代一次(与使用Counter() 不同)。

这种方法快:

>>> timeit('[[k for k, v in OrderedCounter(el).iteritems() if v != 1] for el in col]', 'from __main__ import col, OrderedCounter')
52.00807499885559
>>> timeit('[[k for k, v in Counter(el).iteritems() if v != 1] for el in col]', 'from __main__ import col, Counter')
15.766052007675171
>>> timeit('[list(unique_plurals(c)) for c in col]', 'from __main__ import col, unique_plurals')
6.946599006652832
>>> timeit('[list(unique_plurals_dict(c)) for c in col]', 'from __main__ import col, unique_plurals_dict')
6.557853937149048

这比 OrderedCounter 方法快了大约 8 倍,是 Counter 方法的 2.2 倍。

不过,Jon 的单字典加计数器方法更快!

但是,如果您只需要删除仅出现一次的值,但保持其余部分(包括重复)完整,那么您可以使用(借用 Jon):

from itertools import count
from collections import defaultdict

def nonunique_plurals(lst):
    seen = defaultdict(count)
    for item in lst:
        cnt = next(seen[item])
        if cnt:
            if cnt == 1:
                # yield twice to make up for skipped first item
                yield item
            yield item

这会产生:

>>> [list(nonunique_plurals(c)) for c in col]
[['red', 'red', 'yellow', 'yellow'], ['pink', 'pink', 'brown', 'brown']]
>>> timeit('[non_uniques(c) for c in col]', 'from __main__ import col, non_uniques')
17.75499200820923
>>> timeit('[list(nonunique_plurals(c)) for c in col]', 'from __main__ import col, unique_plurals')
9.306739091873169

这几乎是 Counter() 解决方案 proposed by FMc 的两倍,但它并没有准确地保留顺序:

>>> list(nonunique_plurals(['a', 'a', 'b', 'a', 'b', 'c']))
['a', 'a', 'a', 'b', 'b']
>>> non_uniques(['a', 'a', 'b', 'a', 'b', 'c'])
['a', 'a', 'b', 'a', 'b']

【讨论】:

  • 我怀疑对于更大的输入,差异会显着缩小。
  • @Marcin:为什么?我认为它会加宽Counter 不断更新超过 2 的计数,因此要计数的值越多,它就会越多地增加计数器,发出更新。
  • @Marcin:除了迭代完整的输入列表和唯一值之外,我的省略了额外的迭代步骤。
  • 因为Counter 方法应该只在每个列表上迭代两次(一次用于构建Counter,一次用于Counter)。
  • @Grijesh:谢谢,是的,应该使用本地的。本地查找比属性查找快;在可以产生重大影响的关键循环中。
【解决方案4】:

这解决了您的修订问题:它确实对内部列表进行了两次传递(首先计数,然后检索),因此不是尽可能快;但是,它保留了顺序并且非常易读。像往常一样,权衡取舍比比皆是!

from collections import Counter

cols = [
    ['red', 'yellow', 'blue', 'red', 'green', 'yellow'],
    ['pink', 'orange', 'brown', 'pink', 'brown'],
]

def non_uniques(vals):
    counts = Counter(vals)
    return [v for v in vals if counts[v] > 1]

non_uniqs = map(non_uniques, cols)

# [
#    ['red', 'yellow', 'red', 'yellow'],
#    ['pink', 'brown', 'pink', 'brown'],
# ]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-02-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多