【问题标题】:I found myself swinging the list comprehension hammer我发现自己在挥动列表理解锤
【发布时间】:2012-03-17 00:17:26
【问题描述】:

... 每个 for 循环看起来都像是一个列表推导式。

代替:

for stuff in all_stuff:
    do(stuff)

我在做(没有将列表分配给任何东西):

[ do(stuff) for stuff in all_stuff ]

这是list-comp how-to's 上的常见模式。 1) 好的,没什么大不了的吧?错了。 2) 这不能只是代码风格吗?超级错误。

1) 是的,那是错误的。正如 NiklasB 指出的那样,HowTos 的目的是建立一个新的列表。

2) 可能,但它并不明显和明确,所以最好不要使用它。

我没有记住那些操作指南主要是基于命令行的。在我的团队对我大喊大叫,想知道我到底为什么要建立大量列表然后放手之后,我突然想到我可能会引入一个与内存相关的重大错误。

所以这是我的问题。如果我要在一个非常长的运行过程中执行此操作,其中消耗了大量数据,这个“列表”会继续消耗我的内存直到放手吗?垃圾收集器何时会收回内存?在这个列表建立的范围之后丢失了?

我的猜测是肯定的,它会不断消耗我的记忆。我不知道 python 垃圾收集器是如何工作的,但我敢说这个列表将一直存在,直到在 all_stuff 上调用最后一个 next 之后。

编辑。

我的问题的本质是更清晰地传达了in this question(感谢 Niklas 的链接)

【问题讨论】:

    标签: python list-comprehension


    【解决方案1】:

    如果我要在一个非常长的运行过程中执行此操作,其中消耗了大量数据,这个“列表”会继续消耗我的内存直到放手吗?

    绝对的。

    垃圾收集器何时会收回内存?在这个列表建立的范围之后丢失了?

    CPython 使用引用计数,所以这是最可能的情况。其他实现的工作方式不同,所以不要指望它。

    感谢 Karl 指出,由于 CPython 使用了复杂的内存管理机制,这意味着在此之后内存会立即返回给操作系统。

    我不知道 python 垃圾收集器是如何工作的,但我敢说这个列表将一直存在,直到在 all_stuff 上调用最后一个 next 之后。

    我不认为 任何 垃圾收集器是这样工作的。通常它们会进行标记和清除,因此可能需要相当长的时间才能对列表进行垃圾收集。

    这是在 list-comp how-to 中发现的常见模式。

    绝对不是。关键是你迭代列表的目的是对每个项目做一些事情(do 被称为side-effects)。在 List-comp HOWTO 的所有示例中,列表被迭代以建立一个新列表,基于旧列表的项目。我们来看一个例子:

    # list comp, creates the list [0,1,2,3,4,5,6,7,8,9]
    [i for i in range(10)]
    
    # loop, does nothing
    for i in range(10):
        i  # meh, just an expression which doesn't have an effect
    

    也许你会同意这个循环是完全没有意义的,因为它没有做任何事情,这与构建列表的理解相反。在您的示例中,情况正好相反:理解完全没有意义,因为您不需要列表!您可以在 related question

    上找到有关该问题的更多信息

    顺便说一句,如果您真的想在一行中编写该循环,请使用像 deque.extend 这样的生成器使用者。不过,在这个简单的示例中,这将比原始的 for 循环稍慢:

    >>> from collections import deque
    >>> consume = deque(maxlen=0).extend
    >>> consume(do(stuff) for stuff in all_stuff)
    

    【讨论】:

    • 你能为你的最后一个代码块做一些timeit 基准测试吗?
    • @Blender:嗯,似乎无法证明这一点......感谢您强迫我努力学习:P
    • 如果对象未使用,CPython 将立即对其进行垃圾收集(可能直到函数结束,因为范围规则很幼稚),但这并不能保证内存将返回到操作系统很快。 CPython 有几层内存管理(池分配器等)在幕后进行。
    • 扩展还是需要内存,'any'更好
    【解决方案2】:

    尝试手动进行 GC 并转储统计信息。

    gc.DEBUG_STATS

    在收集期间打印统计信息。此信息在调整收集频率时很有用。

    来自

    http://docs.python.org/library/gc.html

    【讨论】:

      【解决方案3】:

      一旦在循环之外没有对它的引用,CPython GC 就会获取它。 Jython 和 IronPython 遵循底层 GC 的规则。

      【讨论】:

        【解决方案4】:

        如果您喜欢这个习惯用法,do 返回的结果 总是 评估为 True 或 False,并且会考虑一个没有丑陋副作用的类似替代方案,您可以使用生成器表达式与anyall

        对于返回 False 值(或不返回)的函数:

        any(do(stuff) for stuff in all_stuff)
        

        对于返回 True 值的函数:

        all(do(stuff) for stuff in all_stuff)
        

        【讨论】:

        • 除非do 有一个有意义的返回值,这里根本不考虑。 any 只会耗尽迭代器,直到它产生第一个 True 值。
        • 请注意,与普通循环相比,此方法有时具有性能优势(至少对于 CPython 而言)。
        【解决方案5】:

        我不知道 python 垃圾收集器是如何工作的,但我敢说这个列表将一直存在,直到在 all_stuff 上调用最后一个 next 之后。

        嗯,当然会,因为您正在构建一个列表,该列表将具有相同数量的 all_stuff 元素。解释器不能在完成之前丢弃列表,可以吗?您可以在其中一个循环和另一个循环之间调用gc.collect,但每个循环都将在被回收之前完全构造。

        在某些情况下,您可以使用生成器表达式而不是列表推导式,因此它不必构建包含所有值的列表:

        (do_something(i) for i in xrange(1000))
        

        但是你仍然需要以某种方式“exaust”那个生成器......

        【讨论】:

        • 这就是问题所在,all_stuff 是生成网络数据的生成器。它不会很快筋疲力尽。
        • 我的意思是你必须确保解释器会遍历生成器(对不起我的英语)。使用其他人的建议之一,例如 anydeque.extend 将在每个元素生成后立即使用它们,而不会将它们存储在列表中。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2021-06-25
        • 2016-01-15
        • 1970-01-01
        • 2018-06-20
        • 1970-01-01
        • 2013-03-04
        • 2023-02-05
        相关资源
        最近更新 更多