为什么 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_LengthHint
、first 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" 可迭代对象,改进不再适用。
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_LengthHint
、first 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__
.
注意:我准备了
这是您的代码的修改版本,可以使事情更清晰一些。
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" 可迭代对象,改进不再适用。