The code was written for a specific project and was never intended to be a general Python/C++ interaction tool. The code was also written for C++ (the xlC compiler) on an IBM SP2 running AIX. I have made no attempt to make the code cross-platform compatible. Although I am willing to show examples from the code, it is only intended to be used as a template. It worked well for me, but I make no guarantees.
The code was written to use Python version 1.3 and was not compiled with dynamic linking. However, I have used a similar structure on HPUX with dynamic linking with no problems.
PS. If you do find this useful, drop me an email.
Each object is defined by its own file. Each object has its own C++ class and will be a single Python class. It will not have official Python parents, but will have a C++ parent. However, Python calls will be passed up the C++ parent hierarchy. This does not allow one of these PyC++ objects to have a Python parent, but I found that that was not a serious problem for my code.
PyTypeObject NA_PWC::Type = { PyObject_HEAD_INIT(&PyType_Type) 0, /*ob_size*/ "NA_PWC", /*tp_name*/ sizeof(NA_PWC), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ PyDestructor, /*tp_dealloc*/ 0, /*tp_print*/ __getattr, /*tp_getattr -- note double underscore */ __setattr, /*tp_setattr -- note double underscore */ 0, /*tp_compare*/ __repr, /*tp_repr -- note double underscore */ 0, /*tp_as_number*/ 0, /*tp_as_sequence*/ 0, /*tp_as_mapping*/ 0, /*tp_hash*/ sPyCycle, /*tp_call */ };
So a typical Methods member would be:
PyMethodDef NA_PWC::Methods[] = { {"Lock", (PyCFunction) sPyLock, Py_NEWARGS}, {"UnLock", (PyCFunction) sPyUnLock, Py_NEWARGS}, {"PrintVoltage", (PyCFunction) sPyPrintVoltage, Py_NEWARGS}, {NULL, NULL} /* Sentinel */ };
A typical Parents member would be:
PyParentObject NA_PWC::Parents[] = {&NA_PWC::Type, &NA::Type, &NS::Type, NULL};
The trick to constructors in PyC++ objects is that there are two constructors, a C++ constructor which is done normally and a Python wrapper.
The parameters for the C++ constructor should always end in an address of a PythonTypeObject. In the header it can be defaulted to Type, so the forward declaration of the C++ constructor defined in the header (in the object) would look like:
Each C++ object should pass T up the hierarchy.your_object(your_cpp_parms, PyTypeObject *T = &Type);
I always call the Python wrapper PyMake. It must be defined as static in the header file (within the object!):
Then in the C++ file it looks like a straight-forward combination of a static C++ method and a Python CFunction. It typically starts withstatic PyObject *PyMake(PyObject *, PyObject *);
After defining the variables you want to read in, you need to use PyArg_ParseTuple to read them in. (I assume you know how to do this. Look in the Python C extension information if you don't). The Python wrapper simply returns a new object:PyObject *your_object::PyMake(PyObject *ignored, PyObject *args)
return new your_object(your_cpp_parameters);
Including pointers to other PyC++ classes within a PyC++ class is a little complicated, but it becomes a standard template pretty quickly. Let's assume that you have a class A with an instance a and another class B that includes a pointer to A. The first thing is that when parsing the args in PyMake, B should expect an instance of type PyObject (call it a0). Read this in as an "O" in the PyArg_ParseTuple format string. Then use
This converts a0 to a and then checks to make sure that it really is an instance of class A. The isA method (defined in the PyObjectPlus class) passes the Type address up the hierarchy returning true if a0 is an instance of A or one of its parents. (This is why we needed a Parents member). Here is a complete example of one of my constructors:a = (A*) a0; Py_Assert(a->isA(&A::Type), PyExc_TypeError, "a is not a valid instance of A.");
and a more complicated one:NS(PyTypeObject *T) : PyObjectPlus(T) {name="NS_NoName";};
NA_PI::NA_PI(char *name, int x, int y, NA_1D *HD, float sigma0, float scale0, PyTypeObject *T) : NA_2D(name, x, y, 0, 0, T) { Reset(0, 0); HD_input = HD; HD->INCREF(); sigma = sigma0/360.0; scale = scale0/360.0; }; PyObject *NA_PI::PyMake(PyObject *ignored, PyObject *args) { // expects (name) (x y) (HD) char *name; int x, y; float sigma, scale; PyObject *PyN; NA_1D *N; Py_Try(PyArg_ParseTuple(args, "s(ii)O|ff", &name, &x, &y, &PyN, &sigma, &scale)); Py_Assert(x > 0, PyExc_ValueError, "x <= 0"); Py_Assert(y > 0, PyExc_ValueError, "y <= 0"); N = (NA_1D*) PyN; Py_Assert(N->isA(&NA_1D::Type), PyExc_TypeError, "HD is not NA_1D."); return new NA_PI(name, x,y, N, sigma, scale); }
An important thing to remember is that if your class includes a pointer to another PyC++ object you need to DECREF it by calling X.DECREF().
Adding a C++ method that is also callable from Python requires three methods in the C++ object:
The basic C++ method is just a normal C++ method. It should be callable from C++.
The Python wrapper is a normal C++ method that takes one PyObject* as input ("args") and returns a PyObject* as output. In it, the args should be parsed and the C++ method called. The return value should be Py_None. (Remember to Py_INCREF(Py_None) before returning it. I define Py_Return as a macro to handle this in my PyObjectPlus header file).
The static wrapper should cast the "self" input to the appropriate class and then call the python wrapper. It is not necessary to make this an extra function (on some machines it might not be necessary at all), but I found the code more maintainable by including the static wrapper as well. By defining it as an inline function in the header file, there shouldn't be a large cost in speed.
The _getattr method should end withif (streq(attr, attribute_name)) return Py_BuildValue(format_string, c_attribute);
where direct_parent is the class's parent. _getattr_up is a macro defined in my PyObjectPlus header file which passes unresolved calls up the hierarchy after checking for them against the Methods member. The _setattr method is also similar to the PyC version, and so needs to be called with char *attr, PyObject *value as its parameters. Then each attribute should be checked for and handled with a standard_getattr_up(direct_parent);
The _setattr method should end withif (streq(attr, attribute_name)) c_attribute = PyConversionFunction(value); else
This passes any unresolved calls up the hierarchy.return direct_parent::_setattr(attr,value)
print X
where
X is an instance of your class, there needs to be a
_repr function which returns a Python string of appropriate
text. It should be called with no parameters.
The isA method does not have to be defined in each class, but since it is defined in the PyObjectPlus class, it can be used in both C++ and Python to check whether an instance x is a member of a class X or a parent of X.
In C++ it is called byIn Python it is called byx->isA(&X::Type)
where X_name is the name of X defined in the tp_name entry of X's Type member. In both cases, it returns a boolean value.x->isA("X_name")
where A is your object. Then your object can be defined in Python by including the file and constructing it:static PyMethodDef FileMethods[] = { { "new", A::PyMake, Py_NEWARGS}, {NULL, NULL} // Sentinel }; extern "C" { void initA(void) { Py_InitModule("A", FileMethods); } }
If A has parameters, they are passed into the function normally.include A_Object_File a0 = A()
#ifndef _adr_py_lib_h_ // only process once, #define _adr_py_lib_h_ // even if multiply included #ifndef __cplusplus // c++ only #error Must be compiled with C++ #endif #include#include #include #include #include "Python.h" /*------------------------------ * Basic defines ------------------------------*/ typedef const char * version; // define "version" enum boolean {false=0, true=1}; // define "boolean" inline streq(const char *A, const char *B) // define "streq" { return strcmp(A,B) == 0;}; template // min inline T min(const T& a, const T& b) {return a < b ? a : b;} template // max inline T max(const T& a, const T& b) {return a > b ? a : b;} inline void Assert(int expr, char *msg) // C++ assert { if (!expr) { fprintf(stderr, "%s\n", msg); exit(-1); }; } const float TWOPI = 2 * M_PI; // 2 * PI /*------------------------------ * Python defines ------------------------------*/ // some basic python macros #define Py_NEWARGS 1 #define Py_Return Py_INCREF(Py_None); return Py_None; #define Py_Error(E, M) {PyErr_SetString(E, M); return NULL;} #define Py_Try(F) {if (!(F)) return NULL;} #define Py_Assert(A,E,M) {if (!(A)) {PyErr_SetString(E, M); return NULL;}} inline void Py_Fatal(char *M) {cout << M << endl; exit(-1);}; // This must be the first line of each // PyC++ class #define Py_Header \ public: \ static PyTypeObject Type; \ static PyMethodDef Methods[]; \ static PyParentObject Parents[]; \ virtual PyTypeObject *GetType(void) {return &Type;}; \ virtual PyParentObject *GetParents(void) {return Parents;} // This defines the _getattr_up macro // which allows attribute and method calls // to be properly passed up the hierarchy. #define _getattr_up(Parent) \ PyObject *rvalue = Py_FindMethod(Methods, this, attr); \ if (rvalue == NULL) \ { \ PyErr_Clear(); \ return Parent::_getattr(attr); \ } \ else \ return rvalue /*------------------------------ * PyObjectPlus ------------------------------*/ typedef PyTypeObject * PyParentObject; // Define the PyParent Object class PyObjectPlus : public PyObject { // The PyObjectPlus abstract class Py_Header; // Always start with Py_Header public: PyObjectPlus(PyTypeObject *T) // constructor {this->ob_type = T; _Py_NewReference(this);}; virtual ~PyObjectPlus() {}; // destructor static void PyDestructor(PyObject *P) // python wrapper { delete ((PyObjectPlus *) P); }; void INCREF(void) {Py_INCREF(this);}; // incref method void DECREF(void) {Py_DECREF(this);}; // decref method virtual PyObject *_getattr(char *attr); // _getattr method static PyObject *__getattr(PyObject * PyObj, char *attr) // This should be the entry in Type. { return ((PyObjectPlus*) PyObj)->_getattr(attr); }; virtual int _setattr(char *attr, PyObject *value); // _setattr method static int __setattr(PyObject *PyObj, // This should be the entry in Type. char *attr, PyObject *value) { return ((PyObjectPlus*) PyObj)->_setattr(attr, value); }; virtual PyObject *_repr(void); // _repr method static PyObject *__repr(PyObject *PyObj) // This should be the entry in Type. { return ((PyObjectPlus*) PyObj)->_repr(); }; // isA methods boolean isA(PyTypeObject *T); boolean isA(const char *typename); PyObject *Py_isA(PyObject *args); static PyObject *sPy_isA(PyObject *self, PyObject *args, PyObject *kwd) {return ((PyObjectPlus*)self)->Py_isA(args);}; }; #endif // _adr_py_lib_h_
/*------------------------------ * PyObjectPlus cpp * * C++ library routines for Crawl 3.2 ------------------------------*/ #include "stdlib.h" #include "PyObjectPlus.h" /*------------------------------ * PyObjectPlus Type -- Every class, even the abstract one should have a Type ------------------------------*/ PyTypeObject PyObjectPlus::Type = { PyObject_HEAD_INIT(&PyType_Type) 0, /*ob_size*/ "PyObjectPlus", /*tp_name*/ sizeof(PyObjectPlus), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ PyDestructor, /*tp_dealloc*/ 0, /*tp_print*/ __getattr, /*tp_getattr*/ __setattr, /*tp_setattr*/ 0, /*tp_compare*/ __repr, /*tp_repr*/ 0, /*tp_as_number*/ 0, /*tp_as_sequence*/ 0, /*tp_as_mapping*/ 0, /*tp_hash*/ 0, /*tp_call */ }; /*------------------------------ * PyObjectPlus Methods -- Every class, even the abstract one should have a Methods ------------------------------*/ PyMethodDef PyObjectPlus::Methods[] = { {"isA", (PyCFunction) sPy_isA, Py_NEWARGS}, {NULL, NULL} /* Sentinel */ }; /*------------------------------ * PyObjectPlus Parents -- Every class, even the abstract one should have parents ------------------------------*/ PyParentObject PyObjectPlus::Parents[] = {&PyObjectPlus::Type, NULL}; /*------------------------------ * PyObjectPlus attributes -- attributes ------------------------------*/ PyObject *PyObjectPlus::_getattr(char *attr) { if (streq(attr, "type")) return Py_BuildValue("s", (*(GetParents()))->tp_name); return Py_FindMethod(Methods, this, attr); } int PyObjectPlus::_setattr(char *attr, PyObject *value) { cerr << "Unknown attribute" << endl; return 1; } /*------------------------------ * PyObjectPlus repr -- representations ------------------------------*/ PyObject *PyObjectPlus::_repr(void) { Py_Error(PyExc_SystemError, "Representation not overridden by object."); } /*------------------------------ * PyObjectPlus isA -- the isA functions ------------------------------*/ boolean PyObjectPlus::isA(PyTypeObject *T) // if called with a Type, use "typename" { return isA(T->tp_name); } boolean PyObjectPlus::isA(const char *typename) // check typename of each parent { int i; PyParentObject P; PyParentObject *Ps = GetParents(); for (P = Ps[i=0]; P != NULL; P = Ps[i++]) if (streq(P->tp_name, typename)) return true; return false; } PyObject *PyObjectPlus::Py_isA(PyObject *args) // Python wrapper for isA { char *typename; Py_Try(PyArg_ParseTuple(args, "s", &typename)); if(isA(typename)) {Py_INCREF(Py_True); return Py_True;} else {Py_INCREF(Py_False); return Py_False;}; }
/*------------------------------ * NA_PWC h * * Neural Array C++ * for inclusion in python * * subclassed from NA (Neural Array) * * adds pinto-wilson-cowan dynamics * * State * V -- voltage * gamma -- voltage leak * * F = 0.5 + 0.5 * tanh(V + gamma) ------------------------------*/ #ifndef _na_pwc_h_ #define _na_pwc_h_ #include "array1d.h" // An array class (only in C++, implements one // dimensional arrays in a standard C++ manner // These arrays are not available to python. #include "NA.h" // The neural array header file class NA_PWC : public NA { Py_Header; // always start with Py_Header protected: // additional state added by this subclass array1d <float> V; float gamma; boolean locked; public: NA_PWC(char *name, int n, float tau, float gamma, PyTypeObject *T = &Type); // C++ constructor static PyObject *PyMake(PyObject *, PyObject *); // Python constructor // (called by "new", see below) ~NA_PWC(); // C++ destructor PyObject *_getattr(char *attr); // __getattr__ function int _setattr(char *attr, PyObject *value); // __setattr__ function // A typical new method. virtual void PrintVoltage(void); // Actual C++ function, directly callable from C++ PyObject *PyPrintVoltage(PyObject *args); // Python wrapper static PyObject *sPyPrintVoltage(PyObject *self, // static python wrapper PyObject *args, PyObject *kwd) {return ((NA_PWC*)self)->PyPrintVoltage(args);}; // Another typical new method void Lock(void) {locked = true;} PyObject *PyLock(PyObject *args) {Lock(); Py_Return;}; static PyObject *sPyLock(PyObject *self, PyObject *args, PyObject *kwds) {return ((NA_PWC*)self)->PyLock(args);}; // And another void UnLock(void) {locked = false;} PyObject *PyUnLock(PyObject *args) {UnLock(); Py_Return;}; static PyObject *sPyUnLock(PyObject *self, PyObject *args, PyObject *kwds) {return ((NA_PWC*)self)->PyUnLock(args);}; // Methods that are modifications of parents. If the // method calls are identical from parent to child, then // only the C++ function needs to be rewritten. void FillWithRandom(float min, float max); void FillLinearly(void); void FillWithZero(void); void FillWithConst(float c); void FireOneNeuron(int i = -1); void ReadFromFile(FILE *fp); void Cycle(AgentGlobalState &AGS); // C++ only methods. These methods are not required // by any python calls, so they only need C++ versions. void Invert(void); void IncrementVoltage(int i, float dv) {V[i] += dv;}; }; #endif // _na_pwc_h_
/*-------------------------------------------------- * NA_PWC cpp * * Neural Array Pinto/Wilson/Cowan C++ * for inclusion in python * * subclassed from NA (Neural Array) * 3.2.2: Readfromfile * 3.2.3: LockNA * 3.2.4: Save/Load * 3.2.5: UnLock * 3.2.6: Copy * 3.2.7: Cleaned up as Py/C++ sample --------------------------------------------------*/ #include#include #include #include #include "PyObjectPlus.h" #include "NA_PWC.h" version NA_PWC_version = "3.2.7"; /*------------------------------ * NA_PWC Type // TYPE structure ------------------------------*/ PyTypeObject NA_PWC::Type = { PyObject_HEAD_INIT(&PyType_Type) 0, /*ob_size*/ "NA_PWC", /*tp_name*/ sizeof(NA_PWC), /*tp_basicsize*/ 0, /*tp_itemsize*/ /* methods */ PyDestructor, /*tp_dealloc*/ 0, /*tp_print*/ __getattr, /*tp_getattr*/ __setattr, /*tp_setattr*/ 0, /*tp_compare*/ __repr, /*tp_repr*/ 0, /*tp_as_number*/ 0, /*tp_as_sequence*/ 0, /*tp_as_mapping*/ 0, /*tp_hash*/ sPyCycle, /*tp_call */ }; /*------------------------------ * NA_PWC Methods // Methods structure ------------------------------*/ PyMethodDef NA_PWC::Methods[] = { {"Lock", (PyCFunction) sPyLock, Py_NEWARGS}, {"UnLock", (PyCFunction) sPyUnLock, Py_NEWARGS}, {"PrintVoltage", (PyCFunction) sPyPrintVoltage, Py_NEWARGS}, {NULL, NULL} /* Sentinel */ }; /*------------------------------ * NA_PWC Parents // Parents structure ------------------------------*/ PyParentObject NA_PWC::Parents[] = {&NA_PWC::Type, &NA::Type, &NS::Type, NULL}; // Sentinel /*------------------------------ * NA_PWC constructor ------------------------------*/ NA_PWC::NA_PWC(char *name, int n0, // C++ constructor float tau0, float gamma0, PyTypeObject *T) : V(n0), NA(name, n0, tau0, T) { gamma = gamma0; locked = false; } PyObject *NA_PWC::PyMake(PyObject *ignored, PyObject *args) // Python wrapper { // expects (name) (n) (tau=0) (gamma=0) char *name; int n; float tau = 0; float gamma = 0; Py_Try(PyArg_ParseTuple(args, "si|ff", &name, &n, &tau, &gamma)); // Read arguments Py_Assert(n > 0, PyExc_ValueError, "n <= 0"); // Check values ok Py_Assert(tau >= 0, PyExc_ValueError, "tau << 0"); // Check values ok return new NA_PWC(name, n, tau, gamma); // Make new Python-able object } /*------------------------------ * NA_PWC destructor ------------------------------*/ NA_PWC::~NA_PWC() // Everything handled in parent {} /*------------------------------ * NA_PWC Attributes ------------------------------*/ PyObject *NA_PWC::_getattr(char *attr) // __getattr__ function: note only need to handle new state { if (streq(attr, "gamma")) // accessable new state return Py_BuildValue("f", gamma); if (streq(attr, "locked")) // accessable new state return Py_BuildValue("i", int(locked)); _getattr_up(NA); // send to parent } int NA_PWC::_setattr(char *attr, PyObject *value) // __setattr__ function: note only need to handle new state { if (streq(attr, "gamma")) // settable new state gamma = PyFloat_AsDouble(value); else if (streq(attr, "locked")) // settable new state { if (PyObject_IsTrue(value)) locked = true; else locked = false; } else return NA::_setattr(attr, value); // send up to parent return 0; // never reaches here -- keeps compiler from complaining } /*------------------------------ * NA_PWC print voltage // A typical new method ------------------------------*/ void NA_PWC::PrintVoltage(void) // C++ method { printf("%s(V): ", name); for (int i=0; i < n; i++) printf("%5.2f ", V[i]); printf("\n"); } PyObject *NA_PWC::PyPrintVoltage(PyObject *args) // Python wrapper { PrintVoltage(); Py_Return; } /*------------------------------ * NA_PWC invert // C++ method only, does not require python wrapper ------------------------------*/ void NA_PWC::Invert(void) { for (int i=0; i<n; i++) V[i] = atanh(2 * F[i] - 1); } // These methods are modifications of methods already // available in the parent. This means that they // only need new C++ versions. The python wrappers // do not change. (Just make sure the C++ methods in // the parent are declared virtual.) /*------------------------------ * NA_PWC cycle ------------------------------*/ void NA_PWC::Cycle(AgentGlobalState &AGS) { if (!locked) for (int i=0; i<n; i++) { // Firing rate is sigmoidal function of voltage F[i] = 0.5 + 0.5 * tanh(V[i] + gamma); if (!(F[i] >= 0.0 && F[i] <= 1.0)) fprintf(stderr, "%s: F[%d]=0.5 + 0.5 * tanh(%f + %f out of range\n", name, i, F[i], V[i], gamma); Assert(F[i] >= 0.0 && F[i] <= 1.0, "F[i] out of range"); // reset voltage V[i] = 0; } NA::Cycle(AGS); } /*------------------------------ * NA_PWC fill ------------------------------*/ void NA_PWC::FillWithRandom(float min, float max) { if (!locked) { NA::FillWithRandom(min,max); Invert(); } } /*------------------------------ * NA_PWC fill linearly ------------------------------*/ void NA_PWC::FillLinearly(void) { if (!locked) { NA::FillLinearly(); Invert(); } } /*------------------------------ * NA_PWC FillWithZero ------------------------------*/ void NA_PWC::FillWithZero(void) { if (!locked) { NA::FillWithZero(); Invert(); } } /*------------------------------ * NA_PWC FillWithConst ------------------------------*/ void NA_PWC::FillWithConst(float c) { if (!locked) { NA::FillWithConst(c); Invert(); } } /*------------------------------ * NA_PWC Fire One Neuron ------------------------------*/ void NA_PWC::FireOneNeuron(int i) { if (!locked) { NA::FireOneNeuron(i); Invert(); } } /*------------------------------ * NA_PWC ReadFromFile ------------------------------*/ void NA_PWC::ReadFromFile(FILE *fp) { if (!locked) { NA::ReadFromFile(fp); Invert(); } } // This is the module initialization. /*------------------------------ * Module Initialization ------------------------------*/ static PyMethodDef FileMethods[] = { // Only one file method available to python: // make a new NA_PWC object. {"new", NA_PWC::PyMake, Py_NEWARGS}, {NULL, NULL} // Sentinel }; extern "C" { // Python is a C program and wants to call // init with a C protocol. void initNA_PWC(void) { Py_InitModule("NA_PWC", FileMethods); } }
Now, (assuming that the header and C++ files have been compiled
and linked in -- with static linking you will need to change
config.c as well) you can create a new "NA_PWC" object in
python by including NA_PWC (include NA_PWC
) and invoking
its new method (N = NA_PWC.new("name", n_neurons,
tau_parm, gamma_parm)
). Then N is a perfectly good
python object; it can be passed around, invoke methods, read state, etc.
Hope this helps. Good hacking.
-- David Redish, 17 July 1997.