Pythonizations

Automatic bindings generation mostly gets the job done, but unless a C++ library was designed with expressiveness and interactivity in mind, using it will feel stilted. Thus, if you are not the end-user of a set of bindings, it is beneficial to implement pythonizations. Some of these are already provided by default, e.g. for STL containers. Consider the following code, iterating over an STL map, using naked bindings (i.e. “the C++ way”):

>>> from cppyy.gbl import std
>>> m = std.map[int, int]()
>>> for i in range(10):
...     m[i] = i*2
...
>>> b = m.begin()
>>> while b != m.end():
...     print(b.__deref__().second, end=' ')
...     b.__preinc__()
...
0 2 4 6 8 10 12 14 16 18
>>>

Yes, that is perfectly functional, but it is also very clunky. Contrast this to the (automatic) pythonization:

>>> for key, value in m:
...    print(value, end=' ')
...
0 2 4 6 8 10 12 14 16 18
>>>

Such a pythonization can be written completely in Python using the bound C++ methods, with no intermediate language necessary. Since it is written on abstract features, there is also only one such pythonization that works for all STL map instantiations.

Python callbacks

Since bound C++ entities are fully functional Python ones, pythonization can be done explicitly in an end-user facing Python module. However, that would prevent lazy installation of pythonizations, so instead a callback mechanism is provided.

A callback is a function or callable object taking two arguments: the Python proxy class to be pythonized and its C++ name. The latter is provided to allow easy filtering. This callback is then installed through cppyy.py.add_pythonization and ideally only for the relevant namespace (installing callbacks for classes in the global namespace is supported, but beware of name clashes).

Pythonization is most effective of well-structured C++ libraries that have idiomatic behaviors. It is then straightforward to use Python reflection to write rules. For example, consider this callback that looks for the conventional C++ function GetLength and replaces it with Python’s __len__:

>>> import cppyy
>>>
>>> def replace_getlength(klass, name):
...    try:
...        klass.__len__ = klass.__dict__['GetLength']
...        del klass.GetLength
...    except KeyError:
...        pass
...
>>> cppyy.py.add_pythonization(replace_getlength, 'MyNamespace')
>>>
>>> cppyy.cppdef("""
... namespace MyNamespace {
... class MyClass {
... public:
...     MyClass(int i) : fInt(i) {}
...     int GetLength() { return fInt; }
...
... private:
...     int fInt;
... };
... }""")
True
>>> m = cppyy.gbl.MyNamespace.MyClass(42)
>>> len(m)
42
>>> m.GetLength()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute 'GetLength'
>>>

The deletion of GetLength method with del can be omitted if both MyClass.GetLength and MyClass.__len__ should be valid.

C++ callbacks

If you are familiar with the Python C-API, it may sometimes be beneficial to add unique optimizations to your C++ classes to be picked up by the pythonization layer. There are two conventional function that cppyy will look for (no registration of callbacks needed):

static void __cppyy_explicit_pythonize__(PyObject* klass, const std::string&);

which is called only for the class that declares it. And:

static void __cppyy_pythonize__(PyObject* klass, const std::string&);

which is also called for all derived classes.

Just as with the Python callbacks, the first argument will be the Python class proxy, the second the C++ name, for easy filtering. When called, cppyy will be completely finished with the class proxy, so any and all changes are fair game, including the low-level ones such as the replacement of iteration or buffer protocols.

An example pythonization replacing MyClass.GetLength method with Python’s __len__ done with the C++ callbacks:

>>> import cppyy
>>>
>>> cppyy.cppdef("""
... #include <Python.h>
...
... namespace MyNamespace {
... class MyClass {
... public:
...     MyClass(int i) : fInt(i) {}
...     int GetLength() { return fInt; }
...
... private:
...     int fInt;
...
... // pythonizations
... public:
...     static void __cppyy_pythonize__(PyObject* klass, const std::string&){
...         auto cppName = "GetLength";
...         auto pythonizationName = "__len__";
...         auto* methodObject = PyObject_GetAttrString(klass, cppName);
...         PyObject_SetAttrString(klass, pythonizationName, methodObject);
...         Py_DECREF(methodObject);
...         PyObject_DelAttrString(klass, cppName);
...     }
... };
... }""")
True
>>> m = cppyy.gbl.MyNamespace.MyClass(42)
>>> len(m)
42
>>> m.GetLength()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute 'GetLength'
>>>