Как иметь список() потреблять __iter__ без вызова __len__?
У меня есть класс с методами __iter__
и __len__
. Последний использует первое для подсчета всех элементов.
Он работает следующим образом:
class A:
def __iter__(self):
print("iter")
for _ in range(5):
yield "something"
def __len__(self):
print("len")
n = 0
for _ in self:
n += 1
return n
Теперь, если мы возьмем, например, длина экземпляра он печатает len
и iter
, как ожидалось:
>>> len(A())
len
iter
5
Но если мы назовем list()
, он вызывает как __iter__
, так и __len__
:
>>> list(A())
len
iter
iter
['something', 'something', 'something', 'something', 'something']
Он работает как ожидалось, если мы создадим выражение генератора:
>>> list(x for x in A())
iter
['something', 'something', 'something', 'something', 'something']
Я бы предположил, что list(A())
и list(x for x in A())
работают одинаково, но они не работают.
Обратите внимание, что он сначала вызывает __iter__
, затем __len__
, затем перебирает итератор:
class B:
def __iter__(self):
print("iter")
def gen():
print("gen")
yield "something"
return gen()
def __len__(self):
print("len")
return 1
print(list(B()))
Вывод:
iter
len
gen
['something']
Как я могу получить list()
не для вызова __len__
, чтобы итераторы экземпляров экземпляров не потреблялись дважды? Я мог бы определить, например. a length
или size
, и затем вызывается A().size()
, но это меньше, чем pythonic.
Я попытался вычислить длину в __iter__
и кешировать ее так, чтобы последующие вызовы __len__
не нуждались в повторении, но list()
вызывали __len__
без начала итерации, поэтому он не работает.
Обратите внимание, что в моем случае я работаю с очень большими коллекциями данных, поэтому кеширование всех элементов не является вариантом.
Ответы
Ответ 1
Можно с уверенностью сказать, что конструктор list()
обнаруживает, что len()
доступен и вызывает его, чтобы предварительно выделить хранилище для списка.
Ваша реализация полностью полностью назад. Вы реализуете __len__()
, используя __iter__()
, чего не ожидает Python. Ожидается, что len()
- это быстрый, эффективный способ заранее определить длину.
Я не думаю, что вы можете убедить list(A())
не называть len
. Как вы уже заметили, вы можете создать промежуточный шаг, который предотвращает вызов len
.
Вы должны обязательно кэшировать результат, если последовательность неизменна. Если имеется столько предметов, сколько вы предполагаете, нет смысла вычислять len
более одного раза.
Ответ 2
Вам не нужно реализовывать __len__
. Для класса, который является итерируемым, ему просто нужно реализовать или ниже:
-
__iter__
, который возвращает iterator или generator, как в ваших классах A и B
-
__getitems__
, пока он поднимает IndexError
, когда индекс выходит за пределы диапазона
Код Blow по-прежнему работает:
class A:
def __iter__(self):
print("iter")
for _ in range(5):
yield "something"
print list(A())
Какие выходы:
iter
['something', 'something', 'something', 'something', 'something']