2009-11-29 3 views
38

Comment répliquer le code Python suivant avec l'API Python C?Comment créer un générateur/itérateur avec 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 

Jusqu'à présent, j'ai ceci:

#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? */ 
} 

Mais je ne sais pas où aller. Quelqu'un pourrait-il offrir quelques suggestions?

Modifier

Je suppose que le principal problème que je vais avoir avec ce simule la déclaration yield. Si je comprends bien, c'est une déclaration assez simple, mais en réalité, complexe, il crée un générateur avec ses propres méthodes qui sont appelées automatiquement __iter__() et next(). En cherchant dans les documents, il semble être associé à PyGenObject; cependant, comment créer une nouvelle instance de cet objet n'est pas clair. PyGen_New() prend comme argument un PyFrameObject, la seule référence à laquelle je peux trouver est PyEval_GetFrame(), ce qui ne semble pas être ce que je veux (ou est-ce que je me trompe?). Est-ce que quelqu'un a une expérience avec ce qu'ils peuvent partager?

De plus Modifier

Je trouve que cela soit plus clair quand je (essentiellement) a élargi ce que Python a été fait dans les coulisses:

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) 

Techniquement, la séquence est désactivée par une, mais vous voyez l'idée. Le seul problème est qu'il est très ennuyeux de créer un nouvel objet chaque fois que l'on a besoin d'un générateur - et encore plus en Python que C en raison de la monstruosité requise qui vient avec la définition d'un nouveau type. Et il ne peut y avoir d'instruction yield en C car C n'a pas de fermetures. Ce que j'ai fait à la place (puisque je ne pouvais pas le trouver dans l'API Python - s'il vous plaît pointez moi sur un objet standard s'il existe déjà!) Était de créer une classe d'objets générateur générique simple qui rappelait une fonction C pour chaque next() appel de méthode. Ici, il est (notez que je ne l'ai pas encore essayé la compilation car il n'est pas complète - voir ci-dessous):

#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++); 
    } 
} 

Cependant, malheureusement, je ne suis toujours pas fini. La seule question qui me reste est: Comment puis-je déclencher une exception StopIteration avec l'API C? Je n'arrive pas à le trouver dans le Standard Exceptions. Aussi, peut-être plus important encore, est-ce la bonne façon d'aborder ce problème?

Merci à tous ceux qui suivent encore cela.

+1

Vous savez que cela est 'xrange (max)'? –

+1

Oui, mais ce n'est qu'un exemple simple. J'ai un usage pratique pour ça. – Michael

Répondre

58

Voici une implémentation simple du module spam avec une fonction myiter(int) retour iterator:

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

numéros imprime de 0 à 9.Il est plus simple que votre cas mais montre les points principaux: la définition de l'objet avec les méthodes standard __iter__() et next(), et la mise en œuvre du comportement de l'itérateur, y compris l'augmentation StopIteration le cas échéant.

Dans votre cas, l'objet itérateur doit contenir une référence à Sequence (vous aurez donc besoin de la méthode deallocator pour Py_DECREF). La séquence elle-même doit implémenter __iter()__ et créer un itérateur à l'intérieur.


Structure contenant l'état de l'itérateur. (Dans votre version au lieu de m, il aurait référence à la séquence.) Méthode

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

__iter__() de Iterator. Il renvoie toujours simplement self. Il permet de traiter à la fois l'itérateur et la collection dans des constructions comme for ... in ....

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

La mise en œuvre de notre itération: méthode 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; 
    } 
} 

Nous avons besoin version étendue de PyTypeObject structure pour fournir Python avec informations sur __iter__() et next(). Nous voulons qu'ils soient appelés efficacement, donc pas de recherche par nom dans le dictionnaire.

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 */ 
}; 

fonction myiter(int) crée iterator.

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; 
} 

Le reste est assez ennuyeux ...

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); 
} 
5

Dans Sequence_data, vous devez renvoyer une nouvelle instance PyInt ou lancer une exception StopIteration qui indique au code à l'extérieur qu'il n'y a plus de valeurs. Voir PEP 255 pour les détails et 9.10 Generators. Voir Iterator Protocol pour les fonctions d'assistance dans l'API Python/C.

+0

Ceci décrit comment créer une méthode next(), mais pas comment créer et retourner un objet générateur contenant la méthode. – Michael

+0

Il est très difficile de simuler "yield" en C; faire de la classe 'Sequence' un itérateur à la place. –

Questions connexes