Итераторы Python - как динамически назначать self.next в новом классе стилей?
В рамках некоторого промежуточного ПО WSGI я хочу написать класс python, который обертывает итератор для реализации метода close на итераторе.
Это отлично работает, когда я пытаюсь использовать его в классе старого стиля, но бросает TypeError, когда я пытаюсь использовать его в классе нового стиля. Что мне нужно сделать, чтобы это работало с классом нового стиля?
Пример:
class IteratorWrapper1:
def __init__(self, otheriter):
self._iterator = otheriter
self.next = otheriter.next
def __iter__(self):
return self
def close(self):
if getattr(self._iterator, 'close', None) is not None:
self._iterator.close()
# other arbitrary resource cleanup code here
class IteratorWrapper2(object):
def __init__(self, otheriter):
self._iterator = otheriter
self.next = otheriter.next
def __iter__(self):
return self
def close(self):
if getattr(self._iterator, 'close', None) is not None:
self._iterator.close()
# other arbitrary resource cleanup code here
if __name__ == "__main__":
for i in IteratorWrapper1(iter([1, 2, 3])):
print i
for j in IteratorWrapper2(iter([1, 2, 3])):
print j
Дает следующий вывод:
1
2
3
Traceback (most recent call last):
...
TypeError: iter() returned non-iterator of type 'IteratorWrapper2'
Ответы
Ответ 1
То, что вы пытаетесь сделать, имеет смысл, но здесь есть что-то злое внутри Python.
class foo(object):
c = 0
def __init__(self):
self.next = self.next2
def __iter__(self):
return self
def next(self):
if self.c == 5: raise StopIteration
self.c += 1
return 1
def next2(self):
if self.c == 5: raise StopIteration
self.c += 1
return 2
it = iter(foo())
# Outputs: <bound method foo.next2 of <__main__.foo object at 0xb7d5030c>>
print it.next
# 2
print it.next()
# 1?!
for x in it:
print x
foo() - итератор, который изменяет свой следующий метод "на лету" - совершенно легальный в любом месте Python. Итератор, который мы создаем, имеет метод, который мы ожидаем: it.next next2. Когда мы используем итератор напрямую, вызывая next(), получаем 2. Тем не менее, когда мы используем его в цикле for, мы получаем исходный текст, который мы явно перезаписываем.
Я не знаком с внутренними компонентами Python, но похоже, что объект "следующий" метод кэшируется в tp_iternext
(http://docs.python.org/c-api/typeobj.html#tp_iternext), а затем он не обновляется при изменении класса.
Это определенно ошибка Python. Возможно, это описано в PEP генератора, но это не в основной документации Python, и это полностью противоречит нормальному поведению Python.
Вы можете обойти это, сохранив исходную следующую функцию и явным образом обернув ее:
class IteratorWrapper2(object):
def __init__(self, otheriter):
self.wrapped_iter_next = otheriter.next
def __iter__(self):
return self
def next(self):
return self.wrapped_iter_next()
for j in IteratorWrapper2(iter([1, 2, 3])):
print j
... но это явно менее эффективно, и вам не нужно это делать.
Ответ 2
Существует множество мест, где CPython использует удивительные ярлыки, основанные на свойствах класса, а не на свойствах экземпляра. Это одно из этих мест.
Вот простой пример, демонстрирующий проблему:
def DynamicNext(object):
def __init__(self):
self.next = lambda: 42
И вот что происходит:
>>> instance = DynamicNext()
>>> next(instance)
…
TypeError: DynamicNext object is not an iterator
>>>
Теперь, перейдя в исходный код CPython (начиная с версии 2.7.2), здесь реализована реализация next()
builtin:
static PyObject *
builtin_next(PyObject *self, PyObject *args)
{
…
if (!PyIter_Check(it)) {
PyErr_Format(PyExc_TypeError,
"%.200s object is not an iterator",
it->ob_type->tp_name);
return NULL;
}
…
}
И вот реализация PyIter_Check:
#define PyIter_Check(obj) \
(PyType_HasFeature((obj)->ob_type, Py_TPFLAGS_HAVE_ITER) && \
(obj)->ob_type->tp_iternext != NULL && \
(obj)->ob_type->tp_iternext != &_PyObject_NextNotImplemented)
Первая строка PyType_HasFeature(…)
, после расширения всех констант и макросов и т.д., эквивалентна DynamicNext.__class__.__flags__ & 1L<<17 != 0
:
>>> instance.__class__.__flags__ & 1L<<17 != 0
True
Так что проверка явно не терпит неудачу... Это должно означать, что следующая проверка - (obj)->ob_type->tp_iternext != NULL
- не работает.
В Python эта строка грубо (примерно!) эквивалентна hasattr(type(instance), "next")
:
>>> type(instance)
__main__.DynamicNext
>>> hasattr(type(instance), "next")
False
Что явно не получается, потому что тип DynamicNext
не имеет метода next
- только экземпляры этого типа делают.
Теперь мой CPython foo слаб, поэтому мне придется начинать делать некоторые обоснованные предположения здесь... Но я считаю, что они точны.
Когда создается тип CPython (т.е. когда интерпретатор сначала оценивает блок class
и вызывается метод класса metaclass __new__
), значения типа PyTypeObject
struct инициализируются... Итак если при создании типа DynamicNext
не существует метода next
, поле tp_iternext
будет установлено в NULL
, в результате чего PyIter_Check
будет возвращать значение false.
Теперь, как указывает Гленн, это почти наверняка ошибка в CPython... Особенно учитывая, что исправление будет только влиять на производительность, если либо тестируемый объект не итерируется, либо динамически назначает метод next
(очень приближенно ):
#define PyIter_Check(obj) \
(((PyType_HasFeature((obj)->ob_type, Py_TPFLAGS_HAVE_ITER) && \
(obj)->ob_type->tp_iternext != NULL && \
(obj)->ob_type->tp_iternext != &_PyObject_NextNotImplemented)) || \
(PyObject_HasAttrString((obj), "next") && \
PyCallable_Check(PyObject_GetAttrString((obj), "next"))))
Изменить: после небольшого перекопа, исправление не будет таким простым, потому что хотя бы некоторые части кода предполагают, что если PyIter_Check(it)
возвращает true
, то *it->ob_type->tp_iternext
будет существовать... Что не обязательно в этом случае (т.е. поскольку функция next
существует на экземпляре, а не тип).
SO! Вот почему удивительные вещи случаются, когда вы пытаетесь выполнить итерацию по экземпляру нового стиля с помощью динамически назначенного метода next
.
Ответ 3
Похоже, что встроенный iter
не проверяет на next
вызываемый в экземпляре, но в классе и IteratorWrapper2
не имеет next
. Ниже приведена более простая версия вашей проблемы.
class IteratorWrapper2(object):
def __init__(self, otheriter):
self.next = otheriter.next
def __iter__(self):
return self
it=iter([1, 2, 3])
myit = IteratorWrapper2(it)
IteratorWrapper2.next # fails that is why iter(myit) fails
iter(myit) # fails
поэтому решением было бы вернуть otheriter
в __iter__
class IteratorWrapper2(object):
def __init__(self, otheriter):
self.otheriter = otheriter
def __iter__(self):
return self.otheriter
или напишите свой собственный next
, обернув внутренний итератор
class IteratorWrapper2(object):
def __init__(self, otheriter):
self.otheriter = otheriter
def next(self):
return self.otheriter.next()
def __iter__(self):
return self
Хотя я не понимаю, почему не iter
просто использовать self.next
экземпляра.
Ответ 4
Просто верните итератор. Это для __iter__
. Нет смысла пытаться обезопасить объект, находясь в итераторе, и вернуть его, когда у вас уже есть итератор.
EDIT: теперь два метода. Однажды обезьяна исправляет обернутый итератор, во-вторых, китти-обертывание итератора.
class IteratorWrapperMonkey(object):
def __init__(self, otheriter):
self.otheriter = otheriter
self.otheriter.close = self.close
def close(self):
print "Closed!"
def __iter__(self):
return self.otheriter
class IteratorWrapperKitten(object):
def __init__(self, otheriter):
self.otheriter = otheriter
def __iter__(self):
return self
def next(self):
return self.otheriter.next()
def close(self):
print "Closed!"
class PatchableIterator(object):
def __init__(self, inp):
self.iter = iter(inp)
def next(self):
return self.iter.next()
def __iter__(self):
return self
if __name__ == "__main__":
monkey = IteratorWrapperMonkey(PatchableIterator([1, 2, 3]))
for i in monkey:
print i
monkey.close()
kitten = IteratorWrapperKitten(iter([1, 2, 3]))
for i in kitten:
print i
kitten.close()
Оба метода работают как с новыми, так и с старыми классами.