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

Итераторы 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'
4b9b3361

Ответ 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()

Оба метода работают как с новыми, так и с старыми классами.