Note: I prepared the answer for [SO]: Why __len__ is called and the result is not used when iterating with __getitem__?, which was marked as a dupe (as it's exactly this question) while I was writing it, so it was no longer possible to post it there, and since I already had it, I decided to post it here (with small adjustments).
Here's a modified version of your code that makes things a bit clearer.
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.")
Output:
[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.
As seen, __len__ is called when the list is constructed. Browsing [GitHub]: python/cpython - (master) cpython/Objects/listobject.c:
- list___init__ (which is the initializer: __init__ (tp_init member in PyList_Type)) calls list___init___impl
- list___init___impl calls list_extend
- list_extend calls PyObject_LengthHint (
n = PyObject_LengthHint(iterable, 8);)
PyObject_LengthHint (in abstract.c), does the check:
Py_ssize_t
PyObject_LengthHint(PyObject *o, Py_ssize_t defaultvalue)
// ...
if (_PyObject_HasLen(o)) {
res = PyObject_Length(o);
// ...
So, it's an optimization feature that works for iterables that define __len__.
This is particularly handy when the iterable has a large number of elements, so that they are allocated at once, and therefore skip the list growth mechanism (didn't check if still applies, but at one point, it was): "Space increases by ~12.5% when full" (according to David M. Beazley). It is very useful when lists were constructed out of (other) lists or tuples.
For example, constructing a list from an iterable (that doesn't define __len__) with 1000 elements, instead of allocating everything at once, there will be ~41 (log1.125(1000 / 8)) operations (allocation, data shifting, deallocation) required only for increasing the new list as it gets filled (with elements from the source iterable).
Needless to say that for "modern" iterables, the improvement no longer applies.