为什么在使用列表理解而不是生成器表达式时更新列表更快?
Why is updating a list faster when using a list comprehension as opposed to a generator expression?
根据 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)
为什么列表理解在这些情况下会产生更好的性能?
此回答仅涉及 CPython 实现。 使用列表理解更快,因为无论如何生成器都会首先转换为列表。这样做是因为序列的长度应该在之前确定] 继续替换数据,生成器不能告诉你它的长度。
对于列表切片分配,此操作由有趣的命名 list_ass_slice
. There is a special-case handling for assigning a list or tuple, here 处理 - 他们可以使用 PySequence_Fast
操作。
This 是 PySequence_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
[]
根据 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)
为什么列表理解在这些情况下会产生更好的性能?
此回答仅涉及 CPython 实现。 使用列表理解更快,因为无论如何生成器都会首先转换为列表。这样做是因为序列的长度应该在之前确定] 继续替换数据,生成器不能告诉你它的长度。
对于列表切片分配,此操作由有趣的命名 list_ass_slice
. There is a special-case handling for assigning a list or tuple, here 处理 - 他们可以使用 PySequence_Fast
操作。
This 是 PySequence_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
[]