Раздражающая ошибка генератора

Исходный контекст этой ошибки - слишком сложный фрагмент для публикации в таком вопросе. Я должен был уничтожить этот код до минимального фрагмента, который все еще показывает ошибку. Вот почему приведенный ниже код выглядит несколько странно.

В приведенном ниже коде класс Foo может считаться запутанным способом получить что-то вроде xrange.

class Foo(object):
    def __init__(self, n):
        self.generator = (x for x in range(n))

    def __iter__(self):
        for e in self.generator:
            yield e

Действительно, Foo, похоже, очень похож на xrange:

for c in Foo(3):
    print c
# 0
# 1
# 2

print list(Foo(3))
# [0, 1, 2]

Теперь подкласс Bar в Foo добавляет только метод __len__:

class Bar(Foo):
    def __len__(self):
        return sum(1 for _ in self.generator)

Bar ведет себя точно так же, как Foo при использовании в for -loop:

for c in Bar(3):
    print c
# 0
# 1
# 2

НО:

print list(Bar(3))
# []

Я предполагаю, что при оценке list(Bar(3)) метод __len__ Bar(3) получает вызов, тем самым используя генератор.

(Если это предположение верно, вызов Bar(3).__len__ не нужен, ведь list(Foo(3)) дает правильный результат, даже если Foo не имеет метода __len__.)

Эта ситуация раздражает: нет никаких веских причин для list(Foo(3)) и list(Bar(3)) для получения разных результатов.

Можно ли исправить Bar (без, конечно, избавления от своего метода __len__), так что list(Bar(3)) возвращает [0, 1, 2]?

Ответ 1

Ваша проблема в том, что Foo не ведет себя так же, как xrange: xrange дает вам новый итератор каждый раз, когда вы запрашиваете его метод iter, в то время как Foo дает вам всегда то же самое, что означает, что когда он исчерпан, объект тоже

>>> a = Foo(3)
>>> list(a)
[0, 1, 2]
>>> list(a)
[]
>>> a = range(3)
>>> list(a)
[0, 1, 2]
>>> list(a)
[0, 1, 2]

Я легко могу подтвердить, что метод __len__ вызывается list, добавляя spys к вашим методам:

class Bar(Foo):
    def __len__(self):
        print "LEN"
        return sum(1 for _ in self.generator)

(и я добавил a print "ITERATOR" в Foo.__iter__). Это дает:

>>> list(Bar(3))
LEN
ITERATOR
[]

Я могу только представить два обходных пути:

  • мой предпочтительный: верните новый итератор при каждом вызове __iter__ на уровне Foo, чтобы имитировать xrange:

    class Foo(object):
        def __init__(self, n):
            self.n = n
    
        def __iter__(self):
            print "ITERATOR"
            return ( x for x in range(self.n))
    
    class Bar(Foo):
        def __len__(self):
            print "LEN"
            return sum(1 for _ in self.generator)
    

    мы получим правильно:

    >>> list(Bar(3))
    ITERATOR
    LEN
    ITERATOR
    [0, 1, 2]
    
  • Альтернатива: изменить len, чтобы не вызывать итератор, и Foo нетронутый:

    class Bar(Foo):
        def __init__(self, n):
            self.len  = n
            super(Bar, self).__init__(n)
        def __len__(self):
            print "LEN"
            return self.len
    

    Здесь снова получаем:

    >>> list(Bar(3))
    LEN
    ITERATOR
    [0, 1, 2]
    

    но объекты Foo и Bar исчерпаны, как только первый итератор достигает своего конца.

Но я должен признать, что я не знаю контекста ваших реальных классов...

Ответ 2

Такое поведение может быть раздражающим, но на самом деле это вполне понятно. Внутренне a list представляет собой просто массив, а массив - фиксированная размерная структура данных. Результатом этого является то, что если у вас есть list с размером n, и вы хотите добавить дополнительный элемент для достижения n+1, ему придется создать целый новый массив и полностью скопировать старый в новый один. Эффективно ваш list.append(x) теперь является операцией O(n) вместо обычного O(1).

Чтобы предотвратить это, list() пытается получить размер вашего ввода, чтобы он мог догадаться, какой размер должен быть массивом.

Итак, одним из решений этой проблемы является заставить его угадать, используя iter:

list(iter(Bar(3)))