length()函数_Python的list(d.values())与__length_hint__

本文探讨了Python2与Python3中构造list(d.values())时的性能差异,指出由于__length_hint__的存在,即使在Python3中也不会慢于Python2。__length_hint__允许迭代器预估长度,减少内存分配次数。实验显示,Python2和Python3的dict方法如values()、viewvalues()、itervalues()在新版本中有对应变化,且许多内置对象实现了__length_hint__。在CPython源代码中,__length_hint__主要用于顺序容器如list的构造,确保使用iterator参数时的高性能。
摘要由CSDN通过智能技术生成

之前思考过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

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

看下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__的存在,没有额外内存分配导致的性能问题。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值