¿Cómo crear un generador / iterador con la API de Python C?

¿Cómo puedo replicar el siguiente código de Python con la API de Python C?

class Sequence(): def __init__(self, max): self.max = max def data(self): i = 0 while i < self.max: yield i i += 1 

Hasta ahora, tengo esto:

 #include  #include  /* Define a new object class, Sequence. */ typedef struct { PyObject_HEAD size_t max; } SequenceObject; /* Instance variables */ static PyMemberDef Sequence_members[] = { {"max", T_UINT, offsetof(SequenceObject, max), 0, NULL}, {NULL} /* Sentinel */ }; static int Sequence_Init(SequenceObject *self, PyObject *args, PyObject *kwds) { if (!PyArg_ParseTuple(args, "k", &(self->max))) { return -1; } return 0; } static PyObject *Sequence_data(SequenceObject *self, PyObject *args); /* Methods */ static PyMethodDef Sequence_methods[] = { {"data", (PyCFunction)Sequence_data, METH_NOARGS, "sequence.data() -> iterator object\n" "Returns iterator of range [0, sequence.max)."}, {NULL} /* Sentinel */ }; /* Define new object type */ PyTypeObject Sequence_Type = { PyObject_HEAD_INIT(NULL) 0, /* ob_size */ "Sequence", /* tp_name */ sizeof(SequenceObject), /* tp_basicsize */ 0, /* tp_itemsize */ 0, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_compare */ 0, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ 0, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags*/ "Test generator object", /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ 0, /* tp_methods */ Sequence_members, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ 0, /* tp_dictoffset */ (initproc)Sequence_init, /* tp_init */ 0, /* tp_alloc */ PyType_GenericNew, /* tp_new */ }; static PyObject *Sequence_data(SequenceObject *self, PyObject *args) { /* Now what? */ } 

Pero no estoy seguro de a dónde ir a continuación. ¿Alguien podría ofrecer algunas sugerencias?

Editar

Supongo que el principal problema que tengo con esto es simular la statement de yield . Como lo entiendo, es una statement bastante simple, pero en realidad compleja: crea un generador con sus propios __iter__() y next() que se llaman automáticamente. Al buscar en los documentos, parece estar asociado con PyGenObject ; sin embargo, no está claro cómo crear una nueva instancia de este objeto. PyGen_New() toma como argumento un PyFrameObject , la única referencia a la que puedo encontrar es PyEval_GetFrame() , que no parece ser lo que quiero (¿o me equivoco?). ¿Alguien tiene alguna experiencia con esto que puedan compartir?

Edición adicional

Descubrí que esto era más claro cuando (esencialmente) expandí lo que Python estaba haciendo detrás de escena:

 class IterObject(): def __init__(self, max): self.max = max def __iter__(self): self.i = 0 return self def next(self): if self.i >= self.max: raise StopIteration self.i += 1 return self.i class Sequence(): def __init__(self, max): self.max = max def data(self): return IterObject(self.max) 

Técnicamente la secuencia está desactivada por uno, pero entiendes la idea.

El único problema con esto es que es muy molesto crear un nuevo objeto cada vez que se necesita un generador, incluso más en Python que en C debido a la monstruosidad requerida que viene con la definición de un nuevo tipo. Y no puede haber una statement de yield en C porque C no tiene cierres. Lo que hice en lugar de hacerlo (ya que no lo pude encontrar en la API de Python, ¡ por favor , indíqueme un objeto estándar si ya existe) fue crear una clase de objeto generadora simple y genérica que devolvió una función de C para cada next() método de llamada. Aquí está (tenga en cuenta que aún no he intentado comstackr esto porque no está completo, vea a continuación):

 #include  #include  #include  /* A convenient, generic generator object. */ typedef PyObject *(*callback)(PyObject *callee, void *info) PyGeneratorCallback; typedef struct { PyObject HEAD PyGeneratorCallback callback; PyObject *callee; void *callbackInfo; /* info to be passed along to callback function. */ bool freeInfo; /* true if |callbackInfo| should be free'()d when object * dealloc's, false if not. */ } GeneratorObject; static PyObject *Generator_iter(PyObject *self, PyObject *args) { Py_INCREF(self); return self; } static PyObject *Generator_next(PyObject *self, PyObject *args) { return self->callback(self->callee, self->callbackInfo); } static PyMethodDef Generator_methods[] = { {"__iter__", (PyCFunction)Generator_iter, METH_NOARGS, NULL}, {"next", (PyCFunction)Generator_next, METH_NOARGS, NULL}, {NULL} /* Sentinel */ }; static void Generator_dealloc(GenericEventObject *self) { if (self->freeInfo && self->callbackInfo != NULL) { free(self->callbackInfo); } self->ob_type->tp_free((PyObject *)self); } PyTypeObject Generator_Type = { PyObject_HEAD_INIT(NULL) 0, /* ob_size */ "Generator", /* tp_name */ sizeof(GeneratorObject), /* tp_basicsize */ 0, /* tp_itemsize */ Generator_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_compare */ 0, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ 0, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags*/ 0, /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ 0, /* tp_methods */ 0, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ 0, /* tp_dictoffset */ 0, /* tp_init */ 0, /* tp_alloc */ PyType_GenericNew, /* tp_new */ }; /* Returns a new generator object with the given callback function * and arguments. */ PyObject *Generator_New(PyObject *callee, void *info, bool freeInfo, PyGeneratorCallback callback) { GeneratorObject *generator = (GeneratorObject *)_PyObject_New(&Generator_Type); if (generator == NULL) return NULL; generator->callee = callee; generator->info = info; generator->callback = callback; self->freeInfo = freeInfo; return (PyObject *)generator; } /* End of Generator definition. */ /* Define a new object class, Sequence. */ typedef struct { PyObject_HEAD size_t max; } SequenceObject; /* Instance variables */ static PyMemberDef Sequence_members[] = { {"max", T_UINT, offsetof(SequenceObject, max), 0, NULL}, {NULL} /* Sentinel */ } static int Sequence_Init(SequenceObject *self, PyObject *args, PyObject *kwds) { if (!PyArg_ParseTuple(args, "k", &self->max)) { return -1; } return 0; } static PyObject *Sequence_data(SequenceObject *self, PyObject *args); /* Methods */ static PyMethodDef Sequence_methods[] = { {"data", (PyCFunction)Sequence_data, METH_NOARGS, "sequence.data() -> iterator object\n" "Returns generator of range [0, sequence.max)."}, {NULL} /* Sentinel */ }; /* Define new object type */ PyTypeObject Sequence_Type = { PyObject_HEAD_INIT(NULL) 0, /* ob_size */ "Sequence", /* tp_name */ sizeof(SequenceObject), /* tp_basicsize */ 0, /* tp_itemsize */ 0, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ 0, /* tp_compare */ 0, /* tp_repr */ 0, /* tp_as_number */ 0, /* tp_as_sequence */ 0, /* tp_as_mapping */ 0, /* tp_hash */ 0, /* tp_call */ 0, /* tp_str */ 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags*/ "Test generator object", /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ 0, /* tp_iter */ 0, /* tp_iternext */ 0, /* tp_methods */ Sequence_members, /* tp_members */ 0, /* tp_getset */ 0, /* tp_base */ 0, /* tp_dict */ 0, /* tp_descr_get */ 0, /* tp_descr_set */ 0, /* tp_dictoffset */ (initproc)Sequence_init, /* tp_init */ 0, /* tp_alloc */ PyType_GenericNew, /* tp_new */ }; static PyObject *Sequence_data(SequenceObject *self, PyObject *args) { size_t *info = malloc(sizeof(size_t)); if (info == NULL) return NULL; *info = 0; /* |info| will be free'()d by the returned generator object. */ GeneratorObject *ret = Generator_New(self, info, true, &Sequence_data_next_callback); if (ret == NULL) { free(info); /* Watch out for memory leaks! */ } return ret; } PyObject *Sequence_data_next_callback(PyObject *self, void *info) { size_t i = info; if (i > self->max) { return NULL; /* TODO: How do I raise StopIteration here? I can't seem to find * a standard exception. */ } else { return Py_BuildValue("k", i++); } } 

Sin embargo, lamentablemente, todavía no he terminado. La única pregunta que me queda es: ¿Cómo puedo generar una excepción de StopIteration con la API de C? Parece que no puedo encontrarlo en la lista de excepciones estándar . Además, quizás lo más importante, ¿es esta la forma correcta de abordar este problema?

Gracias a todos los que todavía están siguiendo esto.

A continuación se muestra una implementación simple del módulo spam con una función myiter(int) devuelve iterador:

 import spam for i in spam.myiter(10): print i 

Imprime números del 0 al 9.

Es más sencillo que su caso, pero muestra los puntos principales: definir el objeto con los __iter__() estándar __iter__() y next() , e implementar el comportamiento del iterador, incluida la StopIteration cuando sea apropiado.

En su caso, el objeto iterador debe mantener la referencia a la secuencia (por lo que necesitará el método de desasignador para Py_DECREF). La secuencia en sí necesita implementar __iter()__ y crear un iterador dentro de ella.


Estructura que contiene el estado del iterador. (En su versión en lugar de m, tendría referencia a la secuencia.)

 typedef struct { PyObject_HEAD long int m; long int i; } spam_MyIter; 

__iter__() del iterador __iter__() . Siempre simplemente vuelve a self . Permite que tanto el iterador como la colección sean tratados de la misma manera en constructos como for ... in ...

 PyObject* spam_MyIter_iter(PyObject *self) { Py_INCREF(self); return self; } 

Implementación de nuestra iteración: método next() .

 PyObject* spam_MyIter_iternext(PyObject *self) { spam_MyIter *p = (spam_MyIter *)self; if (p->i < p->m) { PyObject *tmp = Py_BuildValue("l", p->i); (p->i)++; return tmp; } else { /* Raising of standard StopIteration exception with empty value. */ PyErr_SetNone(PyExc_StopIteration); return NULL; } } 

Necesitamos una versión extendida de la estructura PyTypeObject para proporcionar a Python información sobre __iter__() y next() . Queremos que se les llame de manera eficiente, por lo que no hay búsquedas basadas en nombres en el diccionario.

 static PyTypeObject spam_MyIterType = { PyObject_HEAD_INIT(NULL) 0, /*ob_size*/ "spam._MyIter", /*tp_name*/ sizeof(spam_MyIter), /*tp_basicsize*/ 0, /*tp_itemsize*/ 0, /*tp_dealloc*/ 0, /*tp_print*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ 0, /*tp_compare*/ 0, /*tp_repr*/ 0, /*tp_as_number*/ 0, /*tp_as_sequence*/ 0, /*tp_as_mapping*/ 0, /*tp_hash */ 0, /*tp_call*/ 0, /*tp_str*/ 0, /*tp_getattro*/ 0, /*tp_setattro*/ 0, /*tp_as_buffer*/ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_ITER, /* tp_flags: Py_TPFLAGS_HAVE_ITER tells python to use tp_iter and tp_iternext fields. */ "Internal myiter iterator object.", /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ 0, /* tp_richcompare */ 0, /* tp_weaklistoffset */ spam_MyIter_iter, /* tp_iter: __iter__() method */ spam_MyIter_iternext /* tp_iternext: next() method */ }; 

myiter(int) crea un iterador.

 static PyObject * spam_myiter(PyObject *self, PyObject *args) { long int m; spam_MyIter *p; if (!PyArg_ParseTuple(args, "l", &m)) return NULL; /* I don't need python callable __init__() method for this iterator, so I'll simply allocate it as PyObject and initialize it by hand. */ p = PyObject_New(spam_MyIter, &spam_MyIterType); if (!p) return NULL; /* I'm not sure if it's strictly necessary. */ if (!PyObject_Init((PyObject *)p, &spam_MyIterType)) { Py_DECREF(p); return NULL; } p->m = m; p->i = 0; return (PyObject *)p; } 

El rest es bastante aburrido …

 static PyMethodDef SpamMethods[] = { {"myiter", spam_myiter, METH_VARARGS, "Iterate from i=0 while i 

En Sequence_data , debe devolver una nueva instancia de PyInt o lanzar una excepción StopIteration que indique al código que no hay más valores. Vea PEP 255 para detalles y 9.10 Generadores .

Consulte el Protocolo Iterator para conocer las funciones de ayuda en la API de Python / C.