【问题标题】:sorted() using generator expressions rather than listssorted() 使用生成器表达式而不是列表
【发布时间】:2011-05-08 11:05:20
【问题描述】:

看到这里的讨论后:Python - generate the time difference 我很好奇。我最初也认为生成器比列表快,但谈到 sorted() 我不知道。将生成器表达式发送到 sorted() 而不是列表有什么好处?生成器表达式是否最终在排序之前被放入 sorted() 中的列表?

编辑:我只能接受一个答案,这让我很伤心,因为我觉得很多回复都有助于澄清这个问题。再次感谢大家。

【问题讨论】:

    标签: python optimization


    【解决方案1】:

    sorted() 所做的第一件事是将数据转换为列表。基本上实现的第一行(在参数验证之后)是

    newlist = PySequence_List(seq);
    

    另请参阅the full source code version 2.7version 3.1.2

    编辑:正如answer by aaronasterling 中所指出的,变量newlist 是一个 列表。如果参数已经是一个列表,则将其复制。所以生成器表达式确实具有使用更少内存的优势。

    【讨论】:

    • 太棒了。谢谢你。你认为在生成器的第一次通过时执行一些工作会有什么好处吗?我知道这总体上相对无关紧要,但似乎它可能会更有效。
    • 我认为他们使用快速排序。在第一次通过时似乎不可能做“一些工作”——这将涉及与列表末尾的元素交换元素,这还不知道。
    • 从我所读到的关于 Python 排序的内容中,他们做了很多优化,而不只是退回到快速排序。从生成器表达式中传输值时,理论上您可以与已放入列表中的值进行一些比较。
    • @Sven 他们使用 TimSort,这是一种自适应合并排序。
    • @aaronsterling:这很有趣!只需阅读 Tim Peters 在the Wikipedia article 中对 in 的描述。 “它在多种偏序数组上都有超自然的表现”——太棒了!
    【解决方案2】:

    查看哪个更快的最简单方法是使用timeit,它告诉我传递列表而不是生成器更快:

    >>> import random
    >>> randomlist = range(1000)
    >>> random.shuffle(randomlist)
    >>> import timeit
    >>> timeit.timeit("sorted(x for x in randomlist)",setup = "from __main__ import randomlist",number = 10000)
    4.944492386602178
    >>> timeit.timeit("sorted([x for x in randomlist])",setup = "from __main__ import randomlist",number = 10000)
    4.635165083830486
    

    还有:

    >>> timeit.timeit("sorted(x for x in xrange(1000,1,-1))",number = 10000)
    1.411807087213674
    >>> timeit.timeit("sorted([x for x in xrange(1000,1,-1)])",number = 10000)
    1.0734657617099401
    

    我认为这是因为当sorted() 将传入的值转换为列表时,对于已经是列表的内容,它可以比生成器更快地执行此操作。 The source code seems to confirm this(但这是通过阅读 cmets 而不是完全理解正在发生的一切)。

    【讨论】:

    • 我一直不清楚的一点是:python 在检测丢弃值和其他更棘手的情况方面到底有多好?它确实检测到某些情况,因此当您说print( id( [42,] ) ); print( id( [42,] ) ); 时,您经常会收到相同的报告。 python 保证当您比较两个列表实例时,它们将具有不同的 id,但由于这不会在这里发生,python 会更有效地执行此操作并重用内存。出于这个原因,确保列表不是一次性值会更公平,因为这样排序后无法避免复制它。
    【解决方案3】:

    如果不知道序列的所有元素,就无法对序列进行排序,因此任何传递给sorted() 的生成器都会被耗尽。

    【讨论】:

    • 这是有道理的。我也很想知道 sorted() 在接收到生成器时会做什么。它是在执行排序之前立即将其转换为列表,还是排序算法在生成器上的第一次通过对实际排序进行任何工作。
    • 查看“在线排序”,如“稳定排序”,但它会在您获得元素时进行排序,也就是在不知道序列的所有元素的情况下进行排序
    【解决方案4】:

    有一个巨大的好处。因为 sorted 不会影响传入的序列,所以它必须对其进行复制。如果它从生成器表达式中创建一个列表,那么只会创建一个列表。如果传入了列表推导式,则首先构建它,然后 sorted 复制它以进行排序。

    这反映在行中

    newlist = PySequence_List(seq);
    

    引用于Sven Marnach's answer。本质上,这将无条件地复制传递给它的任何序列。

    【讨论】:

    • 你是对的 :) 但也要注意 Dave Webb 的时间安排。我会更新我的答案。
    • 好点,但是实现不检查输入的可迭代对象是否为列表,如果是,则不进行复制?
    • @Chris_Rands 因为 sorted 不影响传入的序列,所以必须对其进行复制。
    【解决方案5】:

    Python 使用 Timsort。 Timsort 需要预先知道元素的总数,以计算 minrun 参数。因此,正如 Sven 所报告的,当给定一个生成器时,sorted 所做的第一件事就是将它变成一个列表。

    也就是说,可以编写 Timsort 的增量版本,它从生成器中消耗值的速度更慢 - 您只需要在开始之前修复 minrun,并接受在最后进行一些不平衡合并的痛苦. Timsort 分两个阶段工作。第一阶段涉及遍历整个数组,识别运行并进行插入排序以使数据无序运行。运行查找和插入排序本质上都是增量的。第二阶段涉及排序运行的合并;就像现在一样。

    不过,我认为这没有什么意义。也许它会使内存管理更容易,因为不必从生成器读取到一个不断增长的数组(就像我毫无根据地假设当前的实现那样),您可以将每个运行读取到一个小缓冲区,然后只分配一个 final-大小缓冲区一次,最后。但是,这将涉及一次在内存中拥有 2N 个数组插槽,而一个不断增长的数组可以使用 1.5N 来完成,如果它在增长时翻倍。所以,可能不是一个好主意。

    【讨论】:

    • 很好地讨论了在 sorted() 中处理生成器的利弊。谢谢。
    【解决方案6】:

    我最初也认为是一个列表 理解比列表快

    比列表更快是什么意思?您的意思是比明确的for 更快吗?为此,我会说这取决于:列表推导更像是语法糖,但在简单循环时非常方便。

    但是当谈到 sorted() 我不 知道。发送有什么好处吗 sorted() 的生成器表达式 而不是列表?

    列表推导式和生成器表达式的主要区别在于生成器表达式避免了一次生成整个列表的开销。相反,它们返回一个可以被一个一个迭代的生成器对象,因此生成器表达式更有可能用于节省内存使用。

    但是您必须了解 Python 中的一件事:仅通过观察很难判断一种方式是否比另一种方式更快(乐观),如果您想这样做,您应该使用timeit 进行基准测试(并且基准测试比仅在一台机器上运行一个 timeit 更复杂)。

    阅读this 了解有关一些优化技术的更多信息。

    【讨论】:

    • 在这种情况下,我是在询问 sorted() 的具体行为。我不会在争论列表推导和生成器的语法的道路上走得太远。编辑:我还关心在迭代生成器时处理生成器是否有任何理论上的优势。
    • @Brent Newey :我认为您已经有了使用 Sven Marnach 的生成器表达式排序的答案,并且对于 在迭代生成器时处理生成器有任何理论上的优势 就像我在回答中所说的那样,它主要是为了节省内存使用量,当您将genexpr 传递给循环时,请考虑这样的生成器,循环每次都会询问给我下一个项目,并且每次genexpr 都会生成这个项目就像 Just In Time (JIT) 生成一样,希望我的解释是好的 :)
    【解决方案7】:

    我应该添加到 Dave Webb 的计时答案 [我输入了可能是匿名编辑的内容],当您直接访问优化的生成器时,它可能是快多了;大部分开销可能是代码创建自己的列表或生成器:

    >>> timeit.timeit("sorted(xrange(1000, 1, -1))", number=10000)
    0.34192609786987305
    >>> timeit.timeit("sorted(range(1000, 1, -1))", number=10000)
    0.4096639156341553
    >>> timeit.timeit("sorted([el for el in xrange(1000, 1, -1)])", number=10000)
    0.6886589527130127
    >>> timeit.timeit("sorted(el for el in xrange(1000, 1, -1))", number=10000)
    0.9492318630218506
    

    【讨论】:

      【解决方案8】:

      如果性能很重要,为什么不处理生成器生成的数据,并对迭代结果进行排序?当然,这只能在迭代之间没有因果关系的情况下使用(即排序迭代#[i]的数据不需要对排序迭代#[i + 1]进行任何计算)。 在这种情况下,我想说的是,对生成器产生的一组可能更大的结构进行排序可能会给排序增加许多不必要的复杂性,这可能发生在处理所有元素之后。

      【讨论】:

        猜你喜欢
        • 2012-08-01
        • 2019-09-13
        • 1970-01-01
        • 2019-03-28
        • 2023-02-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-09-01
        相关资源
        最近更新 更多