Python 优化盈亏平衡点:在集合上迭代多次还是先转换为列表?

Python 优化盈亏平衡点:在集合上迭代多次还是先转换为列表?,python,optimization,hash,list,iterator,Python,Optimization,Hash,List,Iterator,这是我一直想知道的事情。我将向Python提出这个问题,但我也欢迎回答爪哇和C++标准库中的答案。 假设您有一个名为“my_list”的Python列表,您希望迭代它的唯一的元素。有两种自然方法: #iterate over set for x in set(my_list): do_something(x) 或 紧张的是,在列表上迭代要比在集合上迭代快,但将集合转换为列表需要时间。我猜这个问题的答案将取决于许多因素,例如: 我们需要迭代多少次 原始列表有多大 原始列表中应该重复多少

这是我一直想知道的事情。我将向Python提出这个问题,但我也欢迎回答爪哇和C++标准库中的答案。 假设您有一个名为“my_list”的Python列表,您希望迭代它的唯一的元素。有两种自然方法:

#iterate over set
for x in set(my_list):
    do_something(x)

紧张的是,在列表上迭代要比在集合上迭代快,但将集合转换为列表需要时间。我猜这个问题的答案将取决于许多因素,例如:

  • 我们需要迭代多少次
  • 原始列表有多大
  • 原始列表中应该重复多少次
所以我想我在寻找这样一个经验法则:“如果列表中有x多个元素,每个元素的重复次数不超过y次,并且只需要迭代z次,那么应该迭代集合;否则应该将其转换为列表。”

我在寻找经验法则。。。 经验法则 下面是编写最佳Python的最佳经验法则:使用尽可能少的中间步骤,避免具体化不必要的数据结构

应用于这个问题:集合是可数的。不要仅仅为了迭代而将它们转换为另一个数据结构。相信Python知道迭代集合的最快方法。如果能更快地将它们转换为列表,Python就会这样做

关于优化: 不要试图通过增加程序的复杂性来过早地进行优化。如果您的程序花费的时间太长,请对其进行分析,然后优化瓶颈。如果您使用的是Python,那么您可能更关心的是开发时间,而不是程序运行的确切时间

示范 在Python 2.7中:

import collections
import timeit
blackhole = collections.deque(maxlen=0).extend
s = set(xrange(10000))
我们看到,对于较大的
n
,越简单越好:

>>> timeit.timeit(lambda: blackhole(s))
108.87403416633606
>>> timeit.timeit(lambda: blackhole(list(s)))
189.0135440826416
对于较小的
n
而言,相同的关系成立:

>>> s = set(xrange(10))
>>> timeit.timeit(lambda: blackhole(s))
0.2969839572906494
>>> timeit.timeit(lambda: blackhole(list(s)))
0.630713939666748
是的,列表的迭代速度比集合快(在您自己的Python解释器上尝试):

但这并不意味着您应该将集合转换为列表以进行迭代

盈亏平衡分析 好的,您已经分析了您的代码,并且发现您在一个集合上迭代了很多次(我假设这个集合是静态的,否则我们所做的是非常有问题的)。我希望您熟悉set方法,而不是复制该功能。(我也认为你应该考虑把FuffeSSET和元组联系起来,因为使用一个列表(可变的)来代替一个规范的集合(也是可变的)似乎是容易出错的。)用这个警告,让我们做一个分析。 这可能是因为,通过投资复杂性和承担更多代码行错误的更大风险,您可以获得良好的回报。该分析将证明这方面的盈亏平衡点。我不知道您需要为更大的风险和开发时间付出多大的性能代价,但这将告诉您在什么时候可以开始为这些付出代价:

import collections
import timeit
import pandas as pd

BLACKHOLE = collections.deque(maxlen=0).extend

SET = set(range(1000000))

def iterate(n, iterable):
    for _ in range(n):
        BLACKHOLE(iterable)

def list_iterate(n, iterable):
    l = list(iterable)
    for _ in range(n):
        BLACKHOLE(l)

columns = ('ConvertList', 'n', 'seconds')

def main():
    results = {c: [] for c in columns}

    for n in range(21):
        for fn in (iterate, list_iterate):
            time = min(timeit.repeat((lambda: fn(n, SET)), number=10))
            results['ConvertList'].append(fn.__name__)
            results['n'].append(n)
            results['seconds'].append(time)

    df = pd.DataFrame(results)
    df2 = df.pivot('n', 'ConvertList')
    df2.plot()
    import pylab
    pylab.show()
看起来你的盈亏平衡点是在5次完整的迭代中。平均5个或更少,这样做是没有意义的。只有在5行或更多行代码时,您才开始补偿额外的开发时间、复杂性(增加的维护成本)和更多行代码带来的风险

我认为你必须做很多,才能在项目中增加复杂性和代码行

这些结果是使用Anaconda的Python 2.7从Ubuntu 14.04终端创建的。对于不同的实现和版本,您可能会得到不同的结果


担心 我关心的是集合是可变的,而列表是可变的。集合将阻止您在迭代时对其进行修改,但从该集合创建的列表不会:

>>> s = set('abc')
>>> for e in s:
...     s.add(e + e.upper())
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: Set changed size during iteration
这就是为什么我还建议使用冻结集和元组。它将是一个内置的保护,防止对数据进行语义错误的更改

>>> s = frozenset('abc')
>>> t_s = tuple(s)
>>> for e in t_s:
...     s.add(e + e.upper())
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
AttributeError: 'frozenset' object has no attribute 'add'
>s=frozenset('abc'))
>>>t_s=元组(s)
>>>对于e in t_s:
...     s、 添加(e+e.upper())
... 
回溯(最近一次呼叫最后一次):
文件“”,第2行,在
AttributeError:“frozenset”对象没有属性“add”
最后,你必须相信自己,才能使你的算法正确。经常有人告诉我,当我警告新的Python用户这类事情时,我给出了很好的建议。他们知道这是一个很好的建议,因为一开始他们没有倾听,发现这会造成不必要的复杂性、复杂性和由此产生的问题。但也有一些事情,比如逻辑正确性,如果你做得不对,你只能怪你自己。将可能出错的事情最小化是一个好处,通常值得进行性能权衡。
同样,如果性能(而不是开发的正确性或速度)是解决此项目的首要问题,那么您就不会使用Python。

1在Python中,因为迭代器是如何实现的。一般来说,这可能取决于,如果语言没有迭代器的概念,迭代器可以保留它们在迭代序列中的位置,那么最好事先显式地创建一个向量,但是很难想象在哪种语言中会发生这种情况。可能是JavaScript之类的随机不合逻辑的东西?Paul,谢谢你给我提供了回答这个问题的机会,我希望这会有所帮助,如果有任何部分不清楚,请告诉我,我会澄清。@kdgregory python集是一个隐藏的哈希表,对哈希表的迭代与元素的数量加上桶的数量成正比;对于列表来说,这只是元素的数量。在哈希表上迭代可能是O(条目+存储桶),但由于存储桶计数(通常)与条目数量成比例,这相当于O(条目),即,它具有与在典型列表结构上迭代相同的算法复杂性。相对速度下降到特定的i
>>> s = set('abc')
>>> for e in s:
...     s.add(e + e.upper())
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: Set changed size during iteration
>>> for e in list(s):
...     s.add(e + e.upper())
>>> s = frozenset('abc')
>>> t_s = tuple(s)
>>> for e in t_s:
...     s.add(e + e.upper())
... 
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
AttributeError: 'frozenset' object has no attribute 'add'