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

Как иметь список() потреблять __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__ без начала итерации, поэтому он не работает.

Обратите внимание, что в моем случае я работаю с очень большими коллекциями данных, поэтому кеширование всех элементов не является вариантом.

4b9b3361

Ответ 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']