Come creare un generatore / iteratore con l'API Python C?
-
08-07-2019 - |
Domanda
Come posso replicare il seguente codice Python con l'API Python C?
class Sequence():
def __init__(self, max):
self.max = max
def data(self):
i = 0
while i < self.max:
yield i
i += 1
Finora, ho questo:
#include <Python/Python.h>
#include <Python/structmember.h>
/* 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? */
}
Ma non sono sicuro di dove andare dopo. Qualcuno potrebbe offrire alcuni suggerimenti?
Modifica
Suppongo che il problema principale che sto riscontrando sia la simulazione dell'istruzione yield
. A quanto ho capito, è un'affermazione piuttosto semplice, ma nella realtà complessa - & nbsp; crea un generatore con i suoi __iter __ ()
e next ()
metodi che vengono chiamati automaticamente. La ricerca nei documenti sembra essere associata al PyGenObject ; tuttavia, non è chiaro come creare una nuova istanza di questo oggetto. PyGen_New ()
prende come argomento un PyFrameObject
, l'unico riferimento a cui posso trovare è PyEval_GetFrame ()
, che non sembra essere quello che voglio (o sbaglio?). Qualcuno ha qualche esperienza con questo che può condividere?
Ulteriore modifica
Ho trovato questo più chiaro quando ho (essenzialmente) ampliato ciò che Python stava facendo dietro le quinte:
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)
Tecnicamente la sequenza è disattivata da una, ma hai l'idea.
L'unico problema con questo è che è molto fastidioso creare un nuovo oggetto ogni volta che si ha bisogno di un generatore - e ancora di più in Python che in C a causa della mostruosità richiesta che deriva dalla definizione di un nuovo tipo. E non ci può essere alcuna istruzione yield
in C perché C non ha chiusure. Quello che ho fatto invece (dal momento che non sono riuscito a trovarlo nell'API Python - & nbsp; per favore indicarmi un oggetto standard se esiste già!) È stato creare una semplice classe di oggetti generatori generici che ha chiamato indietro una funzione C per ogni chiamata del metodo next ()
. Eccolo (nota che non ho ancora provato a compilare questo perché non è completo - & nbsp; vedi sotto):
#include <Python/Python.h>
#include <Python/structmember.h>
#include <stdlib.h>
/* 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++);
}
}
Tuttavia, sfortunatamente, non ho ancora finito. L'unica domanda che mi rimane è: come posso sollevare un'eccezione StopIteration
con l'API C? Non riesco a trovarlo elencato nelle Eccezioni standard . Inoltre, forse ancora più importante, è questo il modo corretto di affrontare questo problema?
Grazie a chiunque stia ancora seguendo questo.
Soluzione
Di seguito è riportata una semplice implementazione del modulo spam
con una funzione myiter (int)
che restituisce iteratore:
import spam
for i in spam.myiter(10):
print i
stampa numeri da 0 a 9.
È più semplice del tuo caso, ma mostra i punti principali: definire l'oggetto con i metodi __iter __ ()
e next ()
standard e implementare il comportamento dell'iteratore incluso aumentare StopIteration
quando appropriato.
Nel tuo caso, l'oggetto iteratore deve contenere il riferimento a Sequence (quindi avrai bisogno del metodo deallocatore per farlo su Py_DECREF).
La sequenza stessa deve implementare __iter () __
e creare un iteratore al suo interno.
Struttura contenente lo stato dell'iteratore. (Nella tua versione anziché in m, farebbe riferimento a Sequence.)
typedef struct {
PyObject_HEAD
long int m;
long int i;
} spam_MyIter;
Metodo __iter __ ()
di Iterator.
Restituisce sempre semplicemente self
.
Consente di trattare allo stesso modo sia l'iteratore che la raccolta
in costrutti come per ... in ...
.
PyObject* spam_MyIter_iter(PyObject *self)
{
Py_INCREF(self);
return self;
}
Implementazione della nostra iterazione: metodo 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;
}
}
Abbiamo bisogno della versione estesa della struttura PyTypeObject
per fornire Python
informazioni su __iter __ ()
e next ()
.
Vogliamo che vengano chiamati in modo efficiente, quindi nessuna ricerca basata sul nome nel dizionario.
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 */
};
La funzione myiter (int)
crea iteratore.
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;
}
Il resto è piuttosto noioso ...
static PyMethodDef SpamMethods[] = {
{"myiter", spam_myiter, METH_VARARGS, "Iterate from i=0 while i<m."},
{NULL, NULL, 0, NULL} /* Sentinel */
};
PyMODINIT_FUNC
initspam(void)
{
PyObject* m;
spam_MyIterType.tp_new = PyType_GenericNew;
if (PyType_Ready(&spam_MyIterType) < 0) return;
m = Py_InitModule("spam", SpamMethods);
Py_INCREF(&spam_MyIterType);
PyModule_AddObject(m, "_MyIter", (PyObject *)&spam_MyIterType);
}
Altri suggerimenti
In Sequence_data
, è necessario restituire una nuova istanza PyInt o lanciare un'eccezione StopIteration
che dice al codice all'esterno che non ci sono più valori. Vedi PEP 255 per i dettagli e 9.10 Generatori .
Vedi Iterator Protocol per le funzioni di supporto nell'API Python / C.