Итераторы Python - как динамически назначать self.next в новом классе стилей? - PullRequest
12 голосов
/ 20 июля 2009

Как часть некоторого промежуточного программного обеспечения 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'

Ответы [ 4 ]

9 голосов
/ 20 июля 2009

То, что вы пытаетесь сделать, имеет смысл, но внутри 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, мы получаем оригинал next, который мы явно перезаписали.

Я не знаком с внутренними компонентами Python, но похоже, что метод "next" объекта кэшируется в 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

... но это, очевидно, менее эффективно, и вы должны не делать это.

6 голосов
/ 30 сентября 2012

Существует множество мест, где CPython использует удивительные ярлыки, основанные на свойствах class вместо свойств instance . Это одно из тех мест.

Вот простой пример, демонстрирующий проблему:

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():

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 - только экземпляры этого типа.

Теперь, мой fy на CPython слабый, поэтому мне придется начать делать здесь некоторые образованные догадки ... Но я верю, что они точны.

Когда создается тип CPython (то есть когда интерпретатор впервые оценивает блок class и вызывается метод класса 'metaclass' __new__), значения в структуре PyTypeObject типа инициализируются… Итак, если при создании типа 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.

4 голосов
/ 20 июля 2009

Похоже, что встроенный 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 экземпляра.

3 голосов
/ 20 июля 2009

Просто верните итератор. Вот для чего __iter__. Нет смысла пытаться «заштриховать» объект в итераторе и вернуть его, когда у вас уже есть итератор.

РЕДАКТИРОВАТЬ: теперь с двумя методами. Однажды, обезьяна исправляет обернутый итератор, во-вторых, обертывающая кошку итератор.

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()

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...