【问题标题】:Why is updating a list faster when using a list comprehension as opposed to a generator expression?为什么在使用列表推导而不是生成器表达式时更新列表更快?
【发布时间】:2019-09-13 21:58:07
【问题描述】:

根据this answer 的说法,在许多情况下,列表的性能优于生成器,例如与str.join 一起使用时(因为算法需要传递数据两次)。

在以下示例中,使用 列表推导 似乎比使用相应的生成器表达式产生更好的性能,尽管直观地,列表推导伴随着分配和复制到生成器回避的额外内存的开销。

In [1]: l = list(range(2_000_000))

In [2]: %timeit l[:] = [i*3 for i in range(len(l))]
190 ms ± 4.65 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [3]: %timeit l[:] = (i*3 for i in range(len(l)))
261 ms ± 7.14 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [4]: %timeit l[::2] = [i*3 for i in range(len(l)//2)]
97.1 ms ± 2.07 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [5]: %timeit l[::2] = (i*3 for i in range(len(l)//2))
129 ms ± 2.21 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [6]: %timeit l[:len(l)//2] = [i*3 for i in range(len(l)//2)]
92.6 ms ± 2.34 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [7]: %timeit l[:len(l)//2] = (i*3 for i in range(len(l)//2))
118 ms ± 2.17 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

为什么列表推导在这些情况下会产生更好的性能?

【问题讨论】:

  • 可能l[:]是一个切片,所以要让类型匹配,生成器必须在后台转换为列表
  • @C.Nivs l[:] = ... 等价于l.__setitem__(slice(None), ...) 但是为什么生成器需要转成列表呢?
  • 来自Python language reference: If the target is a slicing: The primary expression in the reference is evaluated. It should yield a mutable sequence object (such as a list). The assigned object should be a sequence object of the same type. 因此,必须将生成器强制转换为list 类型
  • 顺便补充一句,迭代生成器很慢。尝试对for x in [i for i in range(10_000)]: passfor x in (i for i in range(10_000)): pass 计时,您会发现,即使您必须使用列表解析版本进行两次遍历,列表解析的迭代仍然总体上更快。直到我们处理大约 1_000_000 个项目,我才开始看到生成器表达式获胜,即使那样它也只是稍微快一点......
  • @juanpa.arrivillaga 好的,但是虽然我为了示例而使用了生成器表达式,但想象一下我从其他地方获取生成器。乍一看,首先用尽生成器,然后将其复制到原始列表中似乎很浪费——而不是立即覆盖列表中的项目(对于非扩展切片分配)。我知道,因为原始列表的大小可能会在该操作期间发生变化,所以从一开始就知道新的大小是有利的(尽管我可以想象一个动态调整大小的算法 - 如果有必要的话)。

标签: python python-3.x list list-comprehension generator-expression


【解决方案1】:

此答案仅涉及 CPython 实现。 使用列表推导式更快,因为无论如何都会首先将生成器转换为列表。 这样做是因为应该在继续替换数据之前确定序列的长度,并且生成器无法告诉你它的长度。

对于列表切片分配,此操作由有趣的命名list_ass_slice 处理。分配列表或元组有一个特殊情况处理,here - 他们可以使用PySequence_Fast ops。

ThisPySequence_Fast 的 v3.7.4 实现,您可以清楚地看到列表或元组的类型检查:

PyObject *
PySequence_Fast(PyObject *v, const char *m)
{
    PyObject *it;

    if (v == NULL) {
        return null_error();
    }

    if (PyList_CheckExact(v) || PyTuple_CheckExact(v)) {
        Py_INCREF(v);
        return v;
    }

    it = PyObject_GetIter(v);
    if (it == NULL) {
        if (PyErr_ExceptionMatches(PyExc_TypeError))
            PyErr_SetString(PyExc_TypeError, m);
        return NULL;
    }

    v = PySequence_List(it);
    Py_DECREF(it);

    return v;
}

生成器表达式将无法通过此类型检查并继续执行回退代码,将其转换为列表对象,以便the length can be predetermined

在一般情况下,需要预先确定的长度,以便有效分配列表存储,以及 to provide useful error messages 扩展切片分配:

>>> vals = (x for x in 'abc')
>>> L = [1,2,3]
>>> L[::2] = vals  # attempt assigning 3 values into 2 positions
---------------------------------------------------------------------------
                                          Traceback (most recent call last)
...
ValueError: attempt to assign sequence of size 3 to extended slice of size 2
>>> L  # data unchanged
[1, 2, 3]
>>> list(vals)  # generator was fully consumed
[]

【讨论】:

  • 感谢您对这个话题有所了解。我怀疑转换,但不完全清楚为什么这是必要的(除了扩展切片分配)。查看 C 代码,原因似乎是"d items are inserted" 的性能(因为可以在不事先知道新大小的情况下处理“delete -d items”)。我设想了一个类似于list_extend 的解决方案,但这可能会导致不必要的数据复制。顺便问一下l[::2]是由同一个函数处理的吗(因为没有步长)?
  • 扩展切片分配将进入list_ass_subscript。然后关于使用PySequence_Fast 的相同论点最终再次适用,here
  • 好的,谢谢。我又看了一下 C 代码,但并不完全清楚为什么必须预先知道分配对象的大小(除了扩展切片分配)。为什么算法不能使用类似于list_extend 的大小提示,并且仅在大小提示超过切片长度的情况下才扩展迭代器?否则,对应于切片的内存可能会被覆盖,如果结果表明有太多项目,迭代器仍然可以扩展,并为剩余项目调整列表的大小,就像现在对整个事情所做的那样。你知道这是什么原因吗?
  • 需要提供提示的是作业的右侧(通过__length_hint__ 方法)。但是生成器不能给你任何合理的尺寸提示。从字面上看,它可能是来自套接字(生成器的典型用例)或随机数生成器的一些数据。在实践中,如果您知道数据的长度,通常一开始就没有生成器。我猜不希望过度复杂化典型用例来解释病态边缘情况?
  • 我只是想知道为什么list_extend 可以使用长度提示和动态调整大小(而不是预先扩展迭代器并使用实际大小)但list_ass_slice 不能(尽管它可以) .生成器表达式只是该问题的一个示例,但这实际上涉及任何迭代器,例如mapfilter 或任何自定义迭代器。但是,是的,也许这是一个小众案例,对于大量数据,性能差异变得明显,人们可能无论如何都使用 numpy。
猜你喜欢
  • 2020-11-08
  • 2011-05-08
  • 1970-01-01
  • 2010-10-13
  • 2013-04-24
  • 2011-07-27
  • 1970-01-01
  • 2020-06-25
  • 1970-01-01
相关资源
最近更新 更多