Ci sono un sacco di posti in cui CPython prendere scorciatoie sorprendenti sulla base di classe proprietà invece di esempio proprietà. Questo è uno di quei posti.
Ecco un semplice esempio che illustra il problema:
def DynamicNext(object):
def __init__(self):
self.next = lambda: 42
Ed ecco cosa succede:
>>> instance = DynamicNext()
>>> next(instance)
…
TypeError: DynamicNext object is not an iterator
>>>
Ora, scavando nel codice sorgente CPython (da 2.7.2), ecco la attuazione del next()
incorporato:
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;
}
…
}
Ed ecco l'implementatio n di 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)
La prima linea, PyType_HasFeature(…)
, è, dopo l'espansione tutte le costanti e macro e roba, equivalenti a DynamicNext.__class__.__flags__ & 1L<<17 != 0
:
>>> instance.__class__.__flags__ & 1L<<17 != 0
True
Così che il check, ovviamente, non è fallendo ... Quale deve significare che il prossimo controllo - (obj)->ob_type->tp_iternext != NULL
- è in errore.
In Python, questa linea è grosso modo (più o meno!) Equivalente a hasattr(type(instance), "next")
:
>>> type(instance)
__main__.DynamicNext
>>> hasattr(type(instance), "next")
False
che non riesce, ovviamente, perché il tipo di DynamicNext
non ha un metodo next
- solo le istanze di quel tipo fare.
Ora, il mio CPython foo è debole, quindi dovrò iniziare a fare alcune ipotesi plausibili qui ... Ma credo che siano accurate.
Quando si crea un tipo CPython (cioè, quando l'interprete prima valuta il blocco class
e la classe metaclasse __new__
metodo viene chiamato), i valori sul PyTypeObject
struct del tipo vengono inizializzati ... Quindi, se, quando il DynamicNext
il tipo è stato creato, non esiste il metodo next
, il campo tp_iternext
, verrà impostato su NULL
, provocando PyIter_Check
per restituire false.
Ora, come sottolinea il Glenn, questo è quasi certamente un bug in CPython ... Soprattutto in considerazione che la correzione sarebbe solo impatto sulle prestazioni quando sia l'oggetto in fase di sperimentazione non è iterabile o assegna un next
metodo (molto dinamicamente circa):
#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"))))
Edit: dopo un po 'di scavo, la correzione non sarebbe questo semplice, perché almeno alcune porzioni del codice per scontato che, se PyIter_Check(it)
rendimenti true
, quindi *it->ob_type->tp_iternext
esisteranno ... Che non è neces sarily caso (cioè, perché la funzione next
esiste sull'istanza, non il tipo).
SO! Ecco perché le cose sorprendenti accadono quando si tenta di scorrere su un'istanza di nuovo stile con un metodo next
dinamicamente assegnato.
Hai sollevato un bug su http://bugs.python.org? –