为什么列表会询问__len__?

时间:2017-01-04 23:15:27

标签: python list python-internals

class Foo:
    def __getitem__(self, item):
        print('getitem', item)
        if item == 6:
            raise IndexError
        return item**2
    def __len__(self):
        print('len')
        return 3

class Bar:
    def __iter__(self):
        print('iter')
        return iter([3, 5, 42, 69])
    def __len__(self):
        print('len')
        return 3

演示:

>>> list(Foo())
len
getitem 0
getitem 1
getitem 2
getitem 3
getitem 4
getitem 5
getitem 6
[0, 1, 4, 9, 16, 25]
>>> list(Bar())
iter
len
[3, 5, 42, 69]

为什么list致电__len__?它似乎没有使用任何明显的结果。 for循环不会这样做。 iterator protocol中的任何地方均未提及此问题,{{3}}仅涉及__iter____next__

这个Python是否预先为列表预留空间,或者像这样聪明的东西?

(Linux上的CPython 3.6.0)

3 个答案:

答案 0 :(得分:31)

查看介绍__length_hint__的{​​{3}},并提供有关动机的见解:

  

能够根据__length_hint__估计的预期大小预先分配列表,这可能是一项重要的优化。据观察, CPython比PyPy更快地运行一些代码,纯粹是因为存在这种优化。

除此之外,文档Rationale section from PEP 424验证了这纯粹是一种优化功能:

  

被要求实施operator.length_hint()。应返回对象的估计长度(可能大于或小于实际长度)。长度必须是整数>= 0此方法纯粹是优化,并且永远不需要正确性

所以__length_hint__在这里是因为它可以带来一些不错的优化。

PyObject_LengthHintfor object.__length_hint__然后尝试查看object.__length_hint__是否可用。如果两者都不存在,则为列表返回默认值8

listextend,正如Eli在其回答中所述,从list_init调用,根据此PEP进行了修改,为定义__len__或{{{ 1}}。

__length_hint__并非唯一受益于此的人,当然,first tries to get a value from object.__len__ (if it is defined)

list

所以bytes objects do

>>> bytes(Foo())
len
getitem 0
...
b'\x00\x01\x04\t\x10\x19'
创建do bytearray objects but, only when you extend them

>>> bytearray().extend(Foo()) len getitem 0 ... 个对象填充自己:

tuple

如果有人在徘徊,为什么在课程>>> tuple(Foo()) len getitem 0 ... (0, 1, 4, 9, 16, 25) 'iter'之前打印 {em>,而不是在课程'len'之后打印{<1}}:< / p>

这是因为如果手头的对象定义了Bar an intermediary sequence to,那么也会运行Foo。如果它回归到使用__iter__,则不会发生同样的情况。

答案 1 :(得分:30)

list是一个列表对象构造函数,它将为其内容分配一个初始内存片。列表构造函数通过检查传递给构造函数的任何对象的长度提示或长度,尝试找出该初始内存片的大小。请参阅Python PyObject_LengthHint中对source here的调用。这个地方是从列表构造函数中调用的 - list_init

如果您的对象没有__len____length_hint__,那没关系 - 使用default value of 8;由于重新分配,效率可能会降低。

答案 2 :(得分:0)

注意:我为[SO]: Why __len__ is called and the result is not used when iterating with __getitem__?准备了答案,该答案在我编写时被标记为重复(正好是这个问题),因此不再可能发布在那里,由于我已经有了它,我决定将它发布到这里(稍作调整)。

这是您代码的修改版本,可以使事情更加清晰。

code00.py

#!/usr/bin/env python3

import sys


class Foo:
    def __getitem__(self, item):
        print("{0:s}.{1:s}: {2:d}".format(self.__class__.__name__, "getitem", item))
        if item == 6:
            raise IndexError
        return item ** 2


class Bar:
    def __iter__(self):
        print("{0:s}.{1:s}".format(self.__class__.__name__, "iter"))
        return iter([3, 5, 42, 69])

    def __len__(self):
        result = 3
        print("{0:s}.{1:s}: {2:d}".format(self.__class__.__name__, "len", result))
        return result


def main():
    print("Start ...\n")
    for class_obj in [Foo, Bar]:
        inst_obj = class_obj()
        print("Created {0:s} instance".format(class_obj.__name__))
        list_obj = list(inst_obj)
        print("Converted instance to list")
        print("{0:s}: {1:}\n".format(class_obj.__name__, list_obj))


if __name__ == "__main__":
    print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    main()
    print("\nDone.")

输出

[cfati@CFATI-5510-0:e:\Work\Dev\StackOverflow\q041474829]> "e:\Work\Dev\VEnvs\py_064_03.07.03_test0\Scripts\python.exe" code00.py
Python 3.7.3 (v3.7.3:ef4ec6ed12, Mar 25 2019, 22:22:05) [MSC v.1916 64 bit (AMD64)] 64bit on win32

Start ...

Created Foo instance
Foo.getitem: 0
Foo.getitem: 1
Foo.getitem: 2
Foo.getitem: 3
Foo.getitem: 4
Foo.getitem: 5
Foo.getitem: 6
Converted instance to list
Foo: [0, 1, 4, 9, 16, 25]

Created Bar instance
Bar.iter
Bar.len: 3
Converted instance to list
Bar: [3, 5, 42, 69]


Done.

如所见,在构造列表时会调用 __ len __ 。浏览[GitHub]: python/cpython - (master) cpython/Objects/listobject.c

  • list ___ init __ (这是初始化程序: __ init __ PyList_Type tp_init 成员>))调用 list ___ init ___ impl
  • list ___ init ___ impl 调用 list_extend
  • list_extend 调用 PyObject_LengthHint n = PyObject_LengthHint(iterable, 8);
  • PyObject_LengthHint (在 abstract.c 中),进行检查:

    Py_ssize_t
    PyObject_LengthHint(PyObject *o, Py_ssize_t defaultvalue)
    
        // ...
    
        if (_PyObject_HasLen(o)) {
            res = PyObject_Length(o);
    
        // ...
    

因此,这是一项优化功能,适用于定义 __ len __ 的可迭代对象。

当可迭代对象具有大量元素时,这特别方便,以便立即分配它们,从而跳过列表增长机制(虽然不检查是否仍然适用,但有一次是): “ 已满时空间增加了约12.5% ”(根据David M. Beazley的说法)。当列表由(其他)列表或元组构造而成时,这非常有用。
例如,使用具有 1000 元素的可迭代对象(未定义 __ len __ )构造一个列表,而不是分配所有内容一次,仅需要增加 〜41 log1.125(1000 / 8) )操作(分配,数据移位,释放)新列表填充后(带有可迭代来源中的元素)。

不用说,对于“现代”可迭代对象,改进不再适用。