Раздражающая ошибка генератора
Исходный контекст этой ошибки - слишком сложный фрагмент для публикации в таком вопросе. Я должен был уничтожить этот код до минимального фрагмента, который все еще показывает ошибку. Вот почему приведенный ниже код выглядит несколько странно.
В приведенном ниже коде класс 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)))