David Redish

Lessons in Python/C++ Interactions

I've just finished a large research project (my PhD thesis) in which I wrote a neural simulator using Python and C++. The simulator consisted of more than 50 objects (more than 5 levels deep) in both Python and C++. Along the way, I learned a number of lessons on ways to implement an object hierarchy that was both a C++ and a Python hierarchy but was still simple to add objects to and easy to maintain. About a year and a half ago when I started the project I posted a few notes to the newsgroup about what I'd learned. I promised then that I would write up the lessons I'd learned in a more accessable manner. Now that I have some time, here it is.
  • (Caveats)
  • Desired Properties
  • Specifics
  • Code for the PyObjectPlus Abstract Class
  • An Example Object

  • Caveats

    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.


    Desired Properties

    I wanted a code structure that would allow the following properties:

    Specifics

    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.

    The PyObjectPlus abstract class

    All objects are subclassed from the PyObjectPlus abstract class. This class includes a number of key methods, including
    1. A C++ constructor which takes a standard PythonType object as input. This new constructor sets the ob_type Iiable and creates a new python reference to the object. This was necessary because Python 1.3 uses malloc not new and C++ requires that one use new.
    2. A virtual C++ destructor.
    3. A python wrapper for the C++ destructor which deletes the C++ object. PyDestructor will serve as the python destructor in the PythonType structure.
    4. INCREF and DECREF methods which call the appropriate python functions. Thus to incref an object in C++, one simply uses X.INCREF() and to decref it, one uses X.DECREF().
    5. Base versions of the _getattr and _setattr methods. See below for details.
    6. Base version of the _repr method. See below for details.
    7. The isA method. The isA method is used to check to make sure that an object is an appropriate type before using it. See example below.

    Defining a new object

    Each object is subclassed from a parent object. The top of the hierarchy must be the PyObjectPlus class. Each new object has three structures which define it:
    1. The PyTypeObject. The PyTypeObject is identical to that provided in the C version of Python, but is a member of the C++ object called Type. The tp_name should be set to the name of the class, the tp_basicsize to sizeof(the class), the destructor to PyDestructor, tp_getattr to __getattr, tp_setattr to __setattr, and tp_repr to __repr. You can also include normal versions of tp_as_number, tp_as_sequence, etc. I generally did include tp_call for speed, but not the others.

      So a typical Type member would be:
      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 */
      };
      
    2. The PyMethodDef. The PyMethodDef is identical to that provided in the C version of Python, but is a member of the C++ object called Methods. Each method is listed as a single line just as when making a C object for inclusion in Python. I found that in order for my code to work on the SP2 under xlC I could only call static functions. However, under an earlier version of this code running under gcc on HPUX this was not necessary and the code could directly use the python wrapper. See Methods, below.

      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 */
      };
      
    3. The PyParentObject. This is new to including a C++ object in python. In order to accomodate the C++ hierarchy in attribute and method calls, it was necessary to explicitly store the C++ hierarchy. This is a minor pain, but turns out not to be too much trouble if the hierarchy does not change often. This is stored in the Parents member of the C++ type. It is simply a list of addresses of PyTypeObjects begining with the class type itself and ending with a NULL sentinel. It is important that Parents begin with the class itself because many functions search through Parents to determine who should handle the call.

      A typical Parents member would be:

       PyParentObject NA_PWC::Parents[] = {&NA_PWC::Type, 
      				     &NA::Type, 
      				     &NS::Type, 
      				     NULL};
      

    Constructor

    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:

      your_object(your_cpp_parms, PyTypeObject *T = &Type);
    
    Each C++ object should pass T up the hierarchy.

    I always call the Python wrapper PyMake. It must be defined as static in the header file (within the object!):

    static PyObject *PyMake(PyObject *, PyObject *);
    
    Then in the C++ file it looks like a straight-forward combination of a static C++ method and a Python CFunction. It typically starts with
    PyObject *your_object::PyMake(PyObject *ignored, PyObject *args)
    
    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:
      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

      a = (A*) a0;
      Py_Assert(a->isA(&A::Type), PyExc_TypeError, "a is not a valid instance of A.");
    
    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:
      NS(PyTypeObject *T) : PyObjectPlus(T) {name="NS_NoName";};
    
    and a more complicated one:
    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);
    }
    

    Destructor

    The destructor is easy. Because the destructor is defined as virtual in the PyObjectPlus abstract class, you just need to be sure that you have a valid C++ destructor in each class.

    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().

    Methods

    Adding a C++ method that is also callable from Python requires three methods in the C++ object:

    1. the basic C++ method (say an example Print)
    2. the Python wrapper (PyPrint)
    3. the static wrapper (sPyPrint)
    (Note: on other machines, the static wrapper may not be necessary. It was necessary under xlC on the SP2.)

    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.

    Attributes

    In order to get and set attributes of the PyC++ objects, you will need _getattr and _setattr methods. As with the PyC object versions, the _getattr method needs to be called with char *attr as its parameter. Then each attribute added by the specific class (i.e. not its parents) should be checked for and handled with a standard
      if (streq(attr, attribute_name))
        return Py_BuildValue(format_string, c_attribute);
    
    The _getattr method should end with
      _getattr_up(direct_parent);
    
    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
      if (streq(attr, attribute_name))
        c_attribute = PyConversionFunction(value);
      else 
    
    The _setattr method should end with
      return direct_parent::_setattr(attr,value)
    
    This passes any unresolved calls up the hierarchy.

    Repr

    In order to handle the python call 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.

    isA

    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 by
    	x->isA(&X::Type)
    
    In Python it is called by
    	x->isA("X_name")
    
    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.

    Invoking your object

    The PyC++ Object file is ended with a static PyMethodDef defining the file methods available. I only allow one file method in each Object file "new". This should look like the following:
    static PyMethodDef FileMethods[] = {
      { "new", A::PyMake, Py_NEWARGS},
      {NULL, NULL}		// Sentinel
    };
    
    extern "C" {
      void initA(void)
        {      
          Py_InitModule("A", FileMethods);      
        }
    }
    
    where A is your object. Then your object can be defined in Python by including the file and constructing it:
    include A_Object_File
    a0 = A()
    
    If A has parameters, they are passed into the function normally.

    Code for the PyObjectPlus Abstract Class

    In addition to defining the PyObjectPlus abstract class, the header file also defines a number of other key definitions. I've simply included the whole header and C++ file below.

    Header File

    #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_
    

    C++ File

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

    An Example Object

    Header File

    /*------------------------------	
     * 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_
    

    C++ File

    /*--------------------------------------------------
     * 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.

    David Redish
    graduate student
    Computer Science Department
    Carnegie Mellon University (CMU)
    Center for the Neural Basis of Cognition (CNBC)