之前思考过Python2 d.values() vs Python3 list(d.values())的区别,感觉Python3下会慢一点。实际上由于__length_hint__的存在,并不会变慢。
有时候想要把一个dict里的所有values取出来放到一个list里,在Python2下,可以直接用d.values(),返回的直接是个新构造的list;而Python3下是用list(d.values())。当然,在Python2里也可以用list(d.itervalues())。
可以想象,Python2的d.values()应该只有一次主要的内存分配,因为底层知道要转成list,没有其他操作,所以直接就知道目标list的大小,只需要一次内存分配。而Python3的list(d.values())则不一定,因为list的构造函数只知道传来了一个迭代器,迭代器本身是没有大小的。内部就可能像C++的std::vector没有reserve并且不断的push_back那样,可能造成多次内存分配。
查了下发现,并没有问题,因为CPython的一个优化机制,可以提前猜测到一个迭代器里的元素数量,从而即使调用list(iterator),大多数情况下也只有一次内存分配。这个机制就是__length_hint__,即使在Python2里也有。只是在文档里,Python2里没有任何描述,而Python3里有(正式定义)。
在迭代器(iterator)对象里,不能定义__len__,因为容器才可以定义。但是一个iterator常常是可以提前知道大小的,于是,迭代器可以自己定义一个__length_hint__函数,返回“大概”的元素数量。
__length_hint__的具体工作方式可以直接看文档,或者对应的PEP424(PEP 424 -- A method for exposing a length hint),或者CPython源代码。
试验一下:
Python2:
>>> d = {'a': 100, 'b': 200}
>>> d.itervalues()
<dictionary-valueiterator object at 0x00000000037E9728>
>>> d.itervalues().__length_hint__
<built-in method __length_hint__ of dictionary-valueiterator object at 0x0000000003A10C78>
>>> d.itervalues().__length_hint__()
2
Python3:
>>> d = {'a': 100, 'b': 200}
>>> iter(d.values())
<dict_valueiterator object at 0x000002139EDE85E0>
>>> iter(d.values()).__length_hint__
<built-in method __length_hint__ of dict_valueiterator object at 0x000002139EDE8720>
>>> iter(d.values()).__length_hint__()
2
可以看到,迭代器对象上都有这个__length_hint__。需要注意Python3的d.values()返回的并不是迭代器,需要再iter一下。(如果非要和Python2完全等价的话,就把Python2的d.itervalues()改成iter(d.viewvalues()),因为Python2的viewvalues()基本等同于Python3的values())
Python2和Python3关于dict的几个方法的大致对应关系:
|Python2 | Python3|
|d.values() | list(d.values())|
|d.viewvalues() | d.values()|
|d.itervalues() | iter(d.values())|
不只是dict的values,还有一大堆Python内置的对象定义了合适的__length_hint__。Python3甚至在operator里定义了一个函数,operator.length_hint。
>>> import operator
>>> operator.length_hint([1,2,3])
3
>>> operator.length_hint({'a': 1, 'b': 2})
2
>>> operator.length_hint(iter({'a': 1, 'b': 2}))
2
可以在CPython源代码的Objects文件夹里搜索一下“length_hint”,有一大堆定义了__length_hint__
![4ab0abbfb25c6004879b66d5dc5aad53.png](https://i-blog.csdnimg.cn/blog_migrate/9a1f0e71dc8d8ac57fc16b67ac09fb1f.jpeg)
CPython里实际调用这个__length_hint__貌似只有这一个函数:
Py_ssize_t
PyObject_LengthHint(PyObject *o, Py_ssize_t defaultvalue)
{
...
if (_PyObject_HasLen(o)) {
res = PyObject_Length(o);
...
return res;
...
}
hint = _PyObject_LookupSpecial(o, &PyId___length_hint__);
...
result = _PyObject_CallNoArg(hint);
...
return res;
}
基本就是再说,如果有__len__,就直接用__len__,否则用__length_hint__,也没有的话就用defaultvalue。
虽然大家都定义了,但是CPython里貌似只有少数个地方在用,就是顺序容器的构造:
![93ecc23d3439cfcf91789e9e76f916ad.png](https://i-blog.csdnimg.cn/blog_migrate/d8220194678626f26c90c34424133ffd.png)
看下list的构造,最核心的估计是这一行
static PyObject *
list_extend(PyListObject *self, PyObject *iterable)
/*[clinic end generated code: output=630fb3bca0c8e789 input=9ec5ba3a81be3a4d]*/
{
...
/* Guess a result list size. */
n = PyObject_LengthHint(iterable, 8);
...
mn = m + n;
/* Make room. */
if (list_resize(self, mn) < 0)
goto error;
...
可以看到list的构造也是走的extend,这当然也意味着,list.extend(iterator)也是很快的。
所以,可以大胆用iterator做参数构造容器或者扩展容器,由于__length_hint__的存在,没有额外内存分配导致的性能问题。