Существует множество мест, где 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
.