Подтвердить что ты не робот

Почему список спрашивает о __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__? Кажется, он не использует результат для чего-либо очевидного. A for loop не делает этого. Это нигде не упоминается в протоколе итератора, который просто говорит о __iter__ и __next__.

Является ли этот Python резервированием места для списка заранее или что-то умное?

(CPython 3.6.0 для Linux)

4b9b3361

Ответ 1

Смотрите раздел раздел Rationale из PEP 424, в котором вводится __length_hint__ и дается представление о мотивации:

Возможность предварительного распределения списков на основе ожидаемого размера, как оценивается __length_hint__, может быть значительной оптимизацией. Было замечено, что CPython запускает некоторый код быстрее, чем PyPy, только из-за присутствия этой оптимизации.

В дополнение к этому документация для object.__length_hint__ подтверждает тот факт, что это чисто функция оптимизации:

Вызывается operator.length_hint(). Должен вернуть предполагаемую длину объекта (который может быть больше или меньше фактической длины). Длина должна быть целым числом >= 0. Этот метод является чисто оптимизацией и никогда не требуется для правильности.

Итак, __length_hint__ здесь, потому что это может привести к некоторым хорошим оптимизации.

PyObject_LengthHint, сначала пытается получить значение из object.__len__ (если оно определено), а затем пытается увидеть, если object.__length_hint__. Если он не существует, он возвращает значение по умолчанию 8 для списков.

listextend, который вызывается из list_init, как заявил Эли в своем ответе, был изменен в соответствии с этим PEP, чтобы предложить эту оптимизацию для всего, что определяет либо __len__, либо __length_hint__.

list не единственный, который извлекает выгоду из этого, конечно, bytes objects do:

>>> bytes(Foo())
len
getitem 0
...
b'\x00\x01\x04\t\x10\x19'

поэтому сделать bytearray объекты, но только если вы extend их:

>>> bytearray().extend(Foo())
len
getitem 0
...

и tuple объекты, которые создают промежуточную последовательность, чтобы заполнить себя:

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

Если кто-то блуждает, почему именно 'iter' печатается до 'len' в классе Bar, а не после того, как происходит с классом Foo:

Это потому, что если объект в руке определяет __iter__ Python сначала вызовет его для получения итератора, тем самым запустив print('iter') тоже. То же самое не происходит, если он возвращается к использованию __getitem__.

Ответ 2

list - это конструктор объекта списка, который будет выделять начальный фрагмент памяти для его содержимого. Конструктор списка пытается определить хороший размер для этого начального фрагмента памяти, проверив подсказку длины или длину любого объекта, переданного в конструктор. См. Вызов PyObject_LengthHint в источнике здесь. Это место вызывается из конструктора списка - list_init

Если у вашего объекта нет __len__ или __length_hint__, то используется OK - a значение по умолчанию 8; это может быть менее эффективным из-за перераспределения.