2017-01-04 8 views
26
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 

Демо:Почему список спрашивает о __len__?

>>> 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 цикл этого не делает. Это нигде не упоминается в iterator protocol, в котором говорится только о __iter__ и __next__.

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

(CPython 3.6.0 на Linux)

+2

Да, это, вероятно, резервирование пространства. Возможно, кто-то может просмотреть скомпилированный код и создать хороший ответ. –

ответ

23

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

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

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

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

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

PyObject_LengthHint, first tries to get a value from object.__len__ (if it is defined), а затем пытается узнать, имеется ли 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' 

так 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' в классе Bar, а не после того, как это происходит с классом Foo:

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

+2

Это отличная находка для мотивации! –

+1

@EliBendersky [git wame] (https://github.com/python/cpython/blame/master/Objects/listobject.c#L834) является чудотворцем в этих случаях :-D –

28

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

Если объект не имеет __len__ или __length_hint__, это нормально - это default value of 8 используется; это может быть менее эффективным из-за перераспределения.

+2

Является ли это деталью реализации CPython или документированной частью языка Python? Потому что это может привести к бесконечной рекурсии, если вы не знаете об этом вызове заранее. – wim

+4

«* ему нужен размер *» и «* значение по умолчанию используется *», похоже, противоречат друг другу. Вместо «нужен размер», как «он использует размер для предварительной памяти». –

+0

@ Robᵩ: Я хотел сказать, что для начала нужен какой-то размер. Не то, чтобы он нуждался в '__len__', просто какой-то размер. Размер может быть либо от '__len__', либо от значения по умолчанию. Не стесняйтесь предлагать и редактировать и позволять редакторам решать, что такое более четкая формулировка, - я не очень сильно думаю об этом –