4

How do I create a type using the Python C API that inherits from multiple other types?

The Python documentation includes an example of a type that inherits from one other type, but there is no example or mention of multiple inheritance I could find.

tbodt
  • 16,609
  • 6
  • 58
  • 83
  • 1
    Please include links to the documentation and also add relevant fragments from it to your question. – Leon Nov 14 '16 at 17:06

2 Answers2

1

The C API does not support multiple inheritance. You'd have to call PyType_Type yourself, simulating a standard Python class statement. This is documented under the C API section on specifying a base type for an extension type:

PyTypeObject* PyTypeObject.tp_base

An optional pointer to a base type from which type properties are inherited. At this level, only single inheritance is supported; multiple inheritance require dynamically creating a type object by calling the metatype.

This field is not inherited by subtypes (obviously), but it defaults to &PyBaseObject_Type (which to Python programmers is known as the type object).

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • If I do that, I can still set `tp_methods` to point to some C implementations, right? – tbodt Nov 14 '16 at 17:40
  • @tbodt: No. You get basically none of the power of the C API. You might as well write the class in Python and have the methods delegate to C functions in their implementations: `def some_method(self): return some_c_function(self)`. – user2357112 Nov 14 '16 at 17:43
  • Is there any way to do that without any Python code? – tbodt Nov 14 '16 at 18:01
  • @tbodt: You could probably construct `PyCFunction` objects yourself with `PyCFunction_New` and insert them into the dict you pass to `PyType_Type`. I *think* that'd work. You'd be getting into undocumented territory if you do it. – user2357112 Nov 14 '16 at 18:08
  • argh. does anything bad happen if you try to set `tp_methods`? would setting `tp_bases` do any good? – tbodt Nov 14 '16 at 18:15
  • @tbodt: I believe setting `tp_methods` on a class created by calling `PyType_Type` would have no effect. I'm less clear on what setting `tp_bases` would do on a class created the usual C API way, but I believe it would cause inconsistent and undesirable behavior; particularly, I think the class's `__bases__` and `__mro__` would go out of sync. (As for setting `tp_bases` on a class created through `PyType_Type`, you wouldn't want to do that, because you can just pass those bases in when you create the class.) – user2357112 Nov 14 '16 at 18:23
  • Just checked the code, and it looks like `mro_implementation` which `PyType_Ready` calls to initialize the MRO does the right thing with multiple base classes. Would there be any other possible problems? – tbodt Nov 14 '16 at 20:35
  • @tbodt: You're likely to run into problems with incompatible base class layouts. – user2357112 Nov 14 '16 at 20:52
  • If I make the data layout compatible with all the base classes, would it work? This would be possible if there's only one base class I didn't write. – tbodt Nov 14 '16 at 20:55
  • @tbodt: It looks like there'd be other problems, such as wrong inheritance of fields like `tp_as_number`. You can try to make it work, but at this point, you're deliberately violating the preconditions of the API you're using. Expect problems, test heavily, and don't be surprised if more problems appear when you try to bring in new tools or port this to a different Python version. – user2357112 Nov 14 '16 at 21:07
  • Alright, yeah. It's probably better just to find a design that doesn't require multiple inheritance by C types. – tbodt Nov 14 '16 at 21:11
1

Let's say you have a module named test, with the following classes:

class A:
    a = 1

class B:
    b = 2

And the idea is to create a new class C which inherits from A and B:

import test

class C(test.A, test.B):
    pass

The specification on PyTypeObject.tp_base says that it does not support multiple bases classes and that you have to create the class "by calling the metatype". In this case, the metatype is type, so the class can be created this way:

from test import A, B

C = type("C", (A, B), {})

Translating that to C is straightforward, although a bit verbose:

// from test import A, B
PyObject* test_module = PyImport_ImportModuleNoBlock("test");
if (test_module == NULL) return NULL;
PyObject* ClassA = PyObject_GetAttrString(test_module, "A");
if (ClassA == NULL) return NULL;
PyObject* ClassB = PyObject_GetAttrString(test_module, "B");
if (ClassB == NULL) return NULL;

// name, bases, classdict = "C", (A, B), {}
PyObject *name = PyUnicode_FromString("C");
if (name == NULL) return NULL;
PyObject *bases = PyTuple_Pack(2, ClassA, ClassB);
if (bases == NULL) return NULL;
PyObject *classdict = PyDict_New();
if (dict == NULL) return NULL;

// C = type(name, bases, classdict)
PyObject *ClassC = PyObject_CallObject(
    (PyObject*)&PyType_Type, PyTuple_Pack(3, name, bases, classdict));
if (ClassC == NULL) return NULL;
if (PyModule_AddObject(m, "C", ClassC)) return NULL;
zvone
  • 18,045
  • 3
  • 49
  • 77