为什么 list 询问 __len__?

Why does list ask about __len__?

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 中没有任何地方提到这一点,它只是谈论 __iter____next__

这是Python提前为榜单预留space,还是什么巧妙之类的?

(CPython 3.6.0 Linux)

list 是一个列表对象构造函数,它将为其内容分配一个初始内存片。列表构造函数试图通过检查长度提示或传递给构造函数的任何对象的长度来为初始内存片计算出合适的大小。查看对 PyObject_LengthHint in the Python source here. This place is called from the list constructor -- list_init

的调用

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

查看介绍 __length_hint__ 并提供对动机的见解的 Rationale section from PEP 424

Being able to pre-allocate lists based on the expected size, as estimated by __length_hint__ , can be a significant optimization. CPython has been observed to run some code faster than PyPy, purely because of this optimization being present.

除此之外,文档 for object.__length_hint__ 证实这纯粹是一个优化功能:

Called to implement operator.length_hint(). Should return an estimated length for the object (which may be greater or less than the actual length). The length must be an integer >= 0. This method is purely an optimization and is never required for correctness.

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

PyObject_LengthHintfirst tries to get a value from object.__len__ (if it is defined) 然后尝试查看 object.__length_hint__ 是否可用。如果两者都不存在,它 returns 列表的默认值 8

listextend,如 Eli 在他的回答中所述,从 list_init 调用,根据此 PEP 进行了修改,以便为定义 __len____length_hint__

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 个创建 an intermediary sequence to 的对象会自行填充:

>>> tuple(Foo())
len
getitem 0
...
(0, 1, 4, 9, 16, 25)

如果有人想知道为什么 'iter' 之前 'len' 在 class Bar 而不是之后打印class Foo:

这是因为如果手头的对象定义了一个__iter__Python will first call it to get the iterator,那么运行也就print('iter')了。如果回退到使用 __getitem__.

,则不会发生同样的情况

注意:我准备了的答案,在我写的时候被标记为骗子(因为正是这个问题),所以它是不再可能 post 它在那里,因为我已经有了它,我决定在这里 post 它(稍作调整)。

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

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\Whosebug\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__ (tp_init PyList_Type) 中的成员调用 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__.

的迭代器

这在可迭代对象有大量元素时特别方便,因为它们是一次分配的,因此跳过了列表增长机制(没有检查是否仍然适用,但在某一时刻,它是):“Space 满时增加 ~12.5%”(根据 David M. Beazley 的说法)。当列表由(其他)列表或元组构成时,它非常有用。
例如,用 1000[= 从可迭代对象(未定义 __len__)构造一个列表69=]个元素,不是一次性全部分配,而是会有~41(log<sub>1.125</sub>(1000 / 8)) 操作(分配、数据转移、释放)只需要增加新列表填充(使用来自源可迭代的元素)。

不用说,对于 "modern" 可迭代对象,改进不再适用。